sealights-rspec-agent 2.0.2
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/agent/config.rb +120 -0
- data/agent/listener.rb +229 -0
- data/agent/rest-client-wrapper.rb +27 -0
- data/agent/sealights-rspec-agent.rb +30 -0
- data/agent/tia.rb +154 -0
- data/agent/utils.rb +51 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5cc91027b052b89b83a7d22663733f1bf20ff0263b76e97e84d5978b993e62d0
|
4
|
+
data.tar.gz: d71970e5fab158eca4e44cdb1167a879ffdb82779b73dabfa542b474125c7030
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 27ee78793e7e80babf278a255cc1f36f54c07a5c573051f1348c8e8af64778aa5e1da055f8a4cd13dc4c29c1cc5ae87e061664b74466a963218fce8422b48a7f
|
7
|
+
data.tar.gz: 886188ade8f1b296b60bf3f413a552955063f9f5e2a956aaa357cd31f7974304e35a6adb5a774f37c904c7c0f7d80612ce1f225faccdcde8f8e21068374b0822
|
data/agent/config.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
#
|
2
|
+
# This script contains code responsible for gathering the Agent's configuration.
|
3
|
+
#
|
4
|
+
|
5
|
+
require_relative './utils'
|
6
|
+
|
7
|
+
require 'json'
|
8
|
+
require 'jwt'
|
9
|
+
|
10
|
+
DEFAULT_TIA_DISABLED = 'false'
|
11
|
+
DEFAULT_TIA_POLLING_INTERVAL_SECS = 5
|
12
|
+
DEFAULT_TIA_POLLING_TIMEOUT_SECS = 60
|
13
|
+
|
14
|
+
# environment variables used by the agent
|
15
|
+
TOKEN_FILE_PATH_ENV_VAR = 'SL_TOKEN_FILE_PATH'
|
16
|
+
TOKEN_ENV_VAR = 'SL_TOKEN'
|
17
|
+
BUILD_SESSION_ID_ENV_VAR = 'SL_BUILD_SESSION_ID'
|
18
|
+
BUILD_SESSION_ID_PATH_ENV_VAR = 'SL_BUILD_SESSION_ID_PATH'
|
19
|
+
TEST_STAGE_ENV_VAR = 'SL_TEST_STAGE'
|
20
|
+
TIA_DISABLED_ENV_VAR = 'SL_TIA_DISABLED'
|
21
|
+
TIA_POLLING_INTERVAL_ENV_VAR = 'SL_TIA_POLL_INTERVAL'
|
22
|
+
TIA_POLLING_TIMEOUT_ENV_VAR = 'SL_TIA_POLL_TIMEOUT'
|
23
|
+
|
24
|
+
#
|
25
|
+
# This class represents the Agent's configuration. On initialization, the fields get populated based on environment
|
26
|
+
# variables, calls to the back-end server, and (optionally) files pointed to the by environment variables.
|
27
|
+
#
|
28
|
+
class AgentConfiguration
|
29
|
+
BUILD_SESSION_ENDPOINT = "/v2/agents/buildsession/"
|
30
|
+
TRUE_STRING = 'true'
|
31
|
+
SL_SERVER = 'x-sl-server'
|
32
|
+
|
33
|
+
attr_reader :build_session_id, :server, :customer_id, :test_stage, :tia_enabled, :jwt, :app_name, :branch, :build, :polling_timeout_secs, :polling_interval_secs
|
34
|
+
|
35
|
+
#
|
36
|
+
# An important side effect of AgentConfiguration's initialization is that it will set the value of `rest_client.jwt`.
|
37
|
+
#
|
38
|
+
def initialize(rest_client, env_reader = EnvironmentVariablesReader.new, file_reader = FileReader.new)
|
39
|
+
@env_vars = env_reader.read_vars
|
40
|
+
@file_reader = file_reader
|
41
|
+
@jwt = take_from_path_or_env(@env_vars, :tokenFile, :token)
|
42
|
+
@build_session_id = take_from_path_or_env(@env_vars, :buildSessionIdFile, :buildSessionId)
|
43
|
+
@server = server_from_token(@jwt)
|
44
|
+
rest_client.jwt = @jwt
|
45
|
+
build_session = JSON.parse(rest_client.get @server + BUILD_SESSION_ENDPOINT + @build_session_id)
|
46
|
+
@customer_id = build_session['customerId']
|
47
|
+
@app_name = build_session['appName']
|
48
|
+
@branch = build_session['branchName']
|
49
|
+
@build = build_session['buildName']
|
50
|
+
@test_stage = @env_vars[:testStage]
|
51
|
+
@tia_enabled = @env_vars.fetch(:tiaDisabled, DEFAULT_TIA_DISABLED) != TRUE_STRING
|
52
|
+
|
53
|
+
@polling_interval_secs = get_integer_from_env_vars(:pollingIntervalSecs, DEFAULT_TIA_POLLING_INTERVAL_SECS)
|
54
|
+
@polling_timeout_secs = get_integer_from_env_vars(:pollingTimeoutSecs, DEFAULT_TIA_POLLING_TIMEOUT_SECS)
|
55
|
+
|
56
|
+
info "RSpec agent config: #{pretty_fields(self, 'env_vars', 'file_reader')}"
|
57
|
+
rescue StandardError => e
|
58
|
+
error "Error while resolving agent configuration: #{e.message}"
|
59
|
+
raise(e)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def get_integer_from_env_vars(key, default_value)
|
65
|
+
Integer(@env_vars.fetch(key))
|
66
|
+
rescue
|
67
|
+
default_value
|
68
|
+
end
|
69
|
+
|
70
|
+
def take_from_path_or_env(env_vars, path_key, value_key)
|
71
|
+
path = env_vars[path_key]
|
72
|
+
@build_session_id = path.nil? ? env_vars[value_key] : read_value_from_file(path)
|
73
|
+
end
|
74
|
+
|
75
|
+
def read_value_from_file(path)
|
76
|
+
@file_reader.read(path).strip
|
77
|
+
end
|
78
|
+
|
79
|
+
def server_from_token(token)
|
80
|
+
JWT.decode(token, nil, false)[0].fetch(SL_SERVER)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class EnvironmentVariablesReader
|
85
|
+
def read_vars
|
86
|
+
env_vars = {
|
87
|
+
:tokenFile => ENV[TOKEN_FILE_PATH_ENV_VAR],
|
88
|
+
:token => ENV[TOKEN_ENV_VAR],
|
89
|
+
:buildSessionIdFile => ENV[BUILD_SESSION_ID_PATH_ENV_VAR],
|
90
|
+
:buildSessionId => ENV[BUILD_SESSION_ID_ENV_VAR],
|
91
|
+
:tiaDisabled => ENV[TIA_DISABLED_ENV_VAR],
|
92
|
+
:pollingIntervalSecs => ENV[TIA_POLLING_INTERVAL_ENV_VAR],
|
93
|
+
:pollingTimeoutSecs => ENV[TIA_POLLING_TIMEOUT_ENV_VAR],
|
94
|
+
:testStage => ENV.fetch(TEST_STAGE_ENV_VAR)
|
95
|
+
}
|
96
|
+
|
97
|
+
validate_exactly_one_is_set(TOKEN_FILE_PATH_ENV_VAR, env_vars[:tokenFile], TOKEN_ENV_VAR, env_vars[:token])
|
98
|
+
validate_exactly_one_is_set(BUILD_SESSION_ID_PATH_ENV_VAR, env_vars[:buildSessionIdFile], BUILD_SESSION_ID_ENV_VAR, env_vars[:buildSessionId])
|
99
|
+
|
100
|
+
env_vars
|
101
|
+
rescue StandardError => e
|
102
|
+
error "Error while resolving environment variables: #{e.message}."
|
103
|
+
raise(e)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def validate_exactly_one_is_set(first_name, first_value, second_name, second_value)
|
109
|
+
message = "Exactly one of the environment variables: '#{first_name}' or '#{second_name}' should be set."
|
110
|
+
|
111
|
+
# XORing to check if exactly one non-nil value
|
112
|
+
raise(KeyError.new(message)) unless first_value.nil? ^ second_value.nil?
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class FileReader
|
117
|
+
def read(path)
|
118
|
+
File.read(path)
|
119
|
+
end
|
120
|
+
end
|
data/agent/listener.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
#
|
2
|
+
# This script contains code for listening for test events and in turn sending notifications to the back-end.
|
3
|
+
#
|
4
|
+
|
5
|
+
require_relative './rest-client-wrapper'
|
6
|
+
require_relative './utils'
|
7
|
+
|
8
|
+
require 'json'
|
9
|
+
require 'securerandom'
|
10
|
+
require 'singleton'
|
11
|
+
|
12
|
+
#
|
13
|
+
# RSpecListener implements methods needed for subscription to RSpec::Core::Reporter. The messages from RSpec are then
|
14
|
+
# passed to the EventsDispatcher.
|
15
|
+
#
|
16
|
+
class RSpecListener
|
17
|
+
def initialize(http_client, server, customer_id, app_name, build, branch, test_stage, build_session_id, test_selection_status)
|
18
|
+
@events_handler = EventsDispatcher.new(http_client, server, customer_id, app_name, build, branch, test_stage, build_session_id, test_selection_status)
|
19
|
+
info "Started RSpec Agent for application name: '#{customer_id}', build: '#{branch}', build '#{build}', build session id: '#{build_session_id}', test selection status: '#{test_selection_status}'"
|
20
|
+
end
|
21
|
+
|
22
|
+
def start(notification)
|
23
|
+
@execution_id = SecureRandom.uuid
|
24
|
+
@events_handler.execution_started(@execution_id)
|
25
|
+
end
|
26
|
+
|
27
|
+
def stop(notification)
|
28
|
+
@events_handler.execution_ended(@execution_id)
|
29
|
+
@execution_id = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def example_started(notification)
|
33
|
+
TestIdTracker.instance.set_current_test_identifier(notification.example.full_description)
|
34
|
+
@events_handler.test_started(notification.example.full_description, notification.example.example_group.description, @execution_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def example_passed(notification)
|
38
|
+
@events_handler.test_passed(notification.example.full_description, notification.example.example_group.description, @execution_id, get_duration_millis(notification))
|
39
|
+
end
|
40
|
+
|
41
|
+
def example_failed(notification)
|
42
|
+
@events_handler.test_failed(notification.example.full_description, notification.example.example_group.description, @execution_id, get_duration_millis(notification))
|
43
|
+
end
|
44
|
+
|
45
|
+
def example_pending(notification)
|
46
|
+
@events_handler.test_skipped(notification.example.full_description, notification.example.example_group.description, @execution_id, get_duration_millis(notification))
|
47
|
+
end
|
48
|
+
|
49
|
+
def example_finished(notification)
|
50
|
+
TestIdTracker.instance.set_current_test_identifier(nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
MILLIS_PER_SECOND = 1000
|
56
|
+
|
57
|
+
def get_duration_millis(notification)
|
58
|
+
notification.example.execution_result.run_time * MILLIS_PER_SECOND
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# EventsDispatcher is used for sending the test events to the back-end. The events are aggregated into batches of 10.
|
64
|
+
#
|
65
|
+
class EventsDispatcher
|
66
|
+
RSPEC_FRAMEWORK_NAME = :RSpec
|
67
|
+
EVENTS_ENDPOINT = '/v2/agents/events'
|
68
|
+
|
69
|
+
PASSED_STATUS = :passed
|
70
|
+
FAILED_STATUS = :failed
|
71
|
+
SKIPPED_STATUS = :skipped
|
72
|
+
|
73
|
+
def initialize(http_client, server, customer_id, app_name, build, branch, test_stage, build_session_id, test_selection_status)
|
74
|
+
@http_client = http_client
|
75
|
+
@server = server
|
76
|
+
@customer_id = customer_id
|
77
|
+
@app_name = app_name
|
78
|
+
@build = build
|
79
|
+
@branch = branch
|
80
|
+
@test_stage = test_stage
|
81
|
+
@build_session_id = build_session_id
|
82
|
+
@test_selection_status = test_selection_status
|
83
|
+
@buffered_events = []
|
84
|
+
|
85
|
+
# adding an exit hook to make sure that any remaining events get sent in case of a problem
|
86
|
+
at_exit do
|
87
|
+
flush_events_buffer
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_started(test_name, example_group, execution_id)
|
92
|
+
evt = create_test_event(:testStart, example_group, test_name, execution_id)
|
93
|
+
add_to_buffer evt
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_passed(test_name, example_group, execution_id, duration)
|
97
|
+
evt = create_test_end_event(example_group, test_name, execution_id, PASSED_STATUS, duration)
|
98
|
+
add_to_buffer evt
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_failed(test_name, example_group, execution_id, duration)
|
102
|
+
evt = create_test_end_event(example_group, test_name, execution_id, FAILED_STATUS, duration)
|
103
|
+
add_to_buffer evt
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_skipped(test_name, example_group, execution_id, duration)
|
107
|
+
evt = create_test_end_event(example_group, test_name, execution_id, SKIPPED_STATUS, duration)
|
108
|
+
add_to_buffer evt
|
109
|
+
end
|
110
|
+
|
111
|
+
def execution_started(execution_id)
|
112
|
+
evt = create_basic_test_event(:executionIdStarted, execution_id)
|
113
|
+
add_to_buffer evt
|
114
|
+
end
|
115
|
+
|
116
|
+
def execution_ended(execution_id)
|
117
|
+
evt = create_basic_test_event(:executionIdEnded, execution_id)
|
118
|
+
add_to_buffer evt
|
119
|
+
|
120
|
+
flush_events_buffer
|
121
|
+
end
|
122
|
+
|
123
|
+
def flush_events_buffer
|
124
|
+
unless @buffered_events.empty?
|
125
|
+
send_buffered_events
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
EVENTS_BATCH_SIZE = 10
|
132
|
+
MILLIS_IN_SECOND = 1000
|
133
|
+
|
134
|
+
def add_to_buffer evt
|
135
|
+
@buffered_events.append(evt)
|
136
|
+
|
137
|
+
if @buffered_events.size >= EVENTS_BATCH_SIZE
|
138
|
+
send_buffered_events
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def send_buffered_events
|
143
|
+
msg = {
|
144
|
+
:appName => @app_name,
|
145
|
+
:customerId => @customer_id,
|
146
|
+
:environment => {
|
147
|
+
:agentType => RSPEC_FRAMEWORK_NAME,
|
148
|
+
:environmentName => @test_stage,
|
149
|
+
:testStage => @test_stage,
|
150
|
+
:labId => @build_session_id
|
151
|
+
},
|
152
|
+
:testSelectionStatus => @test_selection_status,
|
153
|
+
:events => @buffered_events
|
154
|
+
}
|
155
|
+
|
156
|
+
unless @branch.nil?
|
157
|
+
msg[:branch] = @branch
|
158
|
+
end
|
159
|
+
unless @build.nil?
|
160
|
+
msg[:build] = @build
|
161
|
+
end
|
162
|
+
|
163
|
+
url = @server + EVENTS_ENDPOINT
|
164
|
+
begin
|
165
|
+
msg_json = msg.to_json
|
166
|
+
info "Sending #{@buffered_events.size} buffered events"
|
167
|
+
@http_client.post url, msg_json
|
168
|
+
clear_buffer
|
169
|
+
rescue Exception => e
|
170
|
+
error "Failed sending data '#{msg}' to '#{url}'", e
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def clear_buffer
|
175
|
+
@buffered_events = []
|
176
|
+
end
|
177
|
+
|
178
|
+
def create_basic_test_event(type, execution_id)
|
179
|
+
{
|
180
|
+
:type => type,
|
181
|
+
:testFramework => RSPEC_FRAMEWORK_NAME,
|
182
|
+
:executionId => execution_id,
|
183
|
+
:timestamp => current_millis
|
184
|
+
}
|
185
|
+
end
|
186
|
+
|
187
|
+
def create_test_event(type, example_group, test_name, execution_id)
|
188
|
+
evt = create_basic_test_event(type, execution_id)
|
189
|
+
evt[:testName] = replace_reserved_characters(test_name)
|
190
|
+
evt[:suitePath] = example_group
|
191
|
+
evt
|
192
|
+
end
|
193
|
+
|
194
|
+
def create_test_end_event(example_group, test_name, execution_id, result, duration)
|
195
|
+
evt = create_test_event(:testEnd, example_group, test_name, execution_id)
|
196
|
+
evt[:result] = result
|
197
|
+
evt[:duration] = duration
|
198
|
+
evt
|
199
|
+
end
|
200
|
+
|
201
|
+
def current_millis
|
202
|
+
(Time.now.to_f * MILLIS_IN_SECOND).to_i
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class TestIdTracker
|
207
|
+
include Singleton
|
208
|
+
|
209
|
+
def initialize
|
210
|
+
@current_test_identifier = nil
|
211
|
+
end
|
212
|
+
|
213
|
+
def to_s
|
214
|
+
unless @current_test_identifier.nil?
|
215
|
+
return "State:" + @current_test_identifier
|
216
|
+
end
|
217
|
+
"Not in test"
|
218
|
+
end
|
219
|
+
|
220
|
+
def get_current_test_identifier
|
221
|
+
@current_test_identifier
|
222
|
+
end
|
223
|
+
|
224
|
+
def set_current_test_identifier(test_id)
|
225
|
+
if @current_test_identifier != test_id
|
226
|
+
@current_test_identifier = test_id
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative './utils'
|
2
|
+
|
3
|
+
require 'rest-client'
|
4
|
+
|
5
|
+
#
|
6
|
+
# This simple RestClient wrapper automatically adds the necessary headers to HTTP requests. It is required to
|
7
|
+
# initialize the :jwt field for RestClientWrapper to work.
|
8
|
+
#
|
9
|
+
class RestClientWrapper
|
10
|
+
attr_writer :jwt
|
11
|
+
|
12
|
+
def get(url)
|
13
|
+
RestClient.get URI::encode(url), :Authorization => auth_header_value(@jwt)
|
14
|
+
end
|
15
|
+
|
16
|
+
def post(url, msg_json)
|
17
|
+
info "Sending request: POST #{url} #{msg_json}"
|
18
|
+
RestClient.post URI::encode(url), msg_json, :content_type => :json, :accept => :json, :Authorization => auth_header_value(@jwt)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def auth_header_value(jwt)
|
24
|
+
"Bearer #{jwt}"
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#
|
2
|
+
# This script is used for:
|
3
|
+
# - subscribing the SeaLights Agent to RSpec test events
|
4
|
+
# - apply TIA configuration to perform test selections.
|
5
|
+
#
|
6
|
+
# This script has to be run before the RSpec test script to take effect.
|
7
|
+
#
|
8
|
+
|
9
|
+
require_relative './config'
|
10
|
+
require_relative './listener'
|
11
|
+
require_relative './rest-client-wrapper'
|
12
|
+
require_relative './tia'
|
13
|
+
require_relative './utils'
|
14
|
+
|
15
|
+
require 'rspec'
|
16
|
+
|
17
|
+
RSpec.configure do |rspec_config|
|
18
|
+
http_client = RestClientWrapper.new
|
19
|
+
|
20
|
+
cfg = AgentConfiguration.new(http_client, EnvironmentVariablesReader.new)
|
21
|
+
|
22
|
+
tia_config = TiaConfig.new(cfg.tia_enabled, cfg.server, cfg.build_session_id, cfg.test_stage, cfg.polling_interval_secs, cfg.polling_timeout_secs)
|
23
|
+
tia_applier = TiaApplier.new(http_client, RspecExampleClassModifier.new, tia_config)
|
24
|
+
tia_applier.apply_configuration
|
25
|
+
|
26
|
+
listener = RSpecListener.new(http_client, cfg.server, cfg.customer_id, cfg.app_name, cfg.build, cfg.branch, cfg.test_stage, cfg.build_session_id, tia_applier.test_selection_status)
|
27
|
+
rspec_config.reporter.register_listener listener, :start, :example_started, :example_passed, :example_failed, :example_pending, :example_finished, :stop
|
28
|
+
rescue Exception => e
|
29
|
+
error "Could not subscribe RSpec Agent to test run. #{e.message}.", e
|
30
|
+
end
|
data/agent/tia.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
#
|
2
|
+
# This script contains code for obtaining and applying TIA recommendations.
|
3
|
+
#
|
4
|
+
|
5
|
+
require_relative './utils'
|
6
|
+
|
7
|
+
require 'json'
|
8
|
+
require 'rspec'
|
9
|
+
|
10
|
+
TEST_SELECTION_DISABLED = 'disabled'
|
11
|
+
TEST_SELECTION_DISABLED_BY_CONFIG = 'disabledByConfiguration'
|
12
|
+
TEST_SELECTION_RECOMMENDATION_TIMEOUT = 'recommendationsTimeout'
|
13
|
+
TEST_SELECTION_ERROR = 'error'
|
14
|
+
TEST_SELECTION_RECOMMENDED_TEST = 'recommendedTests'
|
15
|
+
|
16
|
+
class TiaConfig
|
17
|
+
attr_reader :tia_enabled, :server, :build_session_id, :test_stage, :polling_interval_secs, :polling_timeout_secs
|
18
|
+
|
19
|
+
def initialize(tia_enabled, server, build_session_id, test_stage, polling_interval_secs, polling_timeout_secs)
|
20
|
+
@tia_enabled = tia_enabled
|
21
|
+
@server = server
|
22
|
+
@build_session_id = build_session_id
|
23
|
+
@test_stage = test_stage
|
24
|
+
@polling_interval_secs = polling_interval_secs
|
25
|
+
@polling_timeout_secs = polling_timeout_secs
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# TiaApplier uses the TIA config to get the list of excluded tests and then calls the RSpec modifier to apply test
|
31
|
+
# selection.
|
32
|
+
#
|
33
|
+
class TiaApplier
|
34
|
+
attr_reader :test_selection_status
|
35
|
+
|
36
|
+
def initialize(http_client, rspec_modifier, cfg)
|
37
|
+
@http_client = http_client
|
38
|
+
@cfg = cfg
|
39
|
+
@rspec_modifier = rspec_modifier
|
40
|
+
|
41
|
+
info "TIA applier initialized with configuration: #{pretty_fields(@cfg)}."
|
42
|
+
|
43
|
+
# We're starting with the status indicating that TIA recommendations were acquired successfully.
|
44
|
+
# The status gets changed, if something goes "wrong" on the way.
|
45
|
+
@test_selection_status = TEST_SELECTION_RECOMMENDED_TEST
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_configuration
|
49
|
+
unless @cfg.tia_enabled
|
50
|
+
@test_selection_status = TEST_SELECTION_DISABLED_BY_CONFIG
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
excluded_tests = get_excluded_tests
|
55
|
+
@rspec_modifier.add_test_exclusion(excluded_tests)
|
56
|
+
|
57
|
+
info "Applied TIA with excluded tests: #{excluded_tests}"
|
58
|
+
rescue Exception => e
|
59
|
+
error "An error occurred while applying TIA configuration. Test selection will be skipped.", e
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
TEST_RECOMMENDATIONS_ENDPOINT = "/v1/test-recommendations/"
|
65
|
+
URL_SEPARATOR = "/"
|
66
|
+
HTTP_OK = 200
|
67
|
+
TEST_SELECTION_ENABLED_FIELD = 'testSelectionEnabled'
|
68
|
+
EXCLUDED_TESTS_FIELD = 'excludedTests'
|
69
|
+
RECOMMENDATION_SET_STATUS_FIELD = 'recommendationSetStatus'
|
70
|
+
NOT_READY_RECOMMENDATION_STATUS = 'notReady'
|
71
|
+
|
72
|
+
def get_excluded_tests
|
73
|
+
url = @cfg.server + TEST_RECOMMENDATIONS_ENDPOINT + @cfg.build_session_id + URL_SEPARATOR + @cfg.test_stage
|
74
|
+
recommendations = poll_for_recommendations(url)
|
75
|
+
|
76
|
+
selection_enabled = recommendations[TEST_SELECTION_ENABLED_FIELD]
|
77
|
+
unless selection_enabled
|
78
|
+
unless selection_enabled.nil?
|
79
|
+
@test_selection_status = TEST_SELECTION_DISABLED
|
80
|
+
end
|
81
|
+
return []
|
82
|
+
end
|
83
|
+
|
84
|
+
recommendations.fetch(EXCLUDED_TESTS_FIELD, []).map { |excluded_test| excluded_test['name'] }
|
85
|
+
rescue Exception => e
|
86
|
+
@test_selection_status = TEST_SELECTION_ERROR
|
87
|
+
error "Error while getting excluded tests: '#{e.message}'. Proceeding without excluded tests.", e
|
88
|
+
end
|
89
|
+
|
90
|
+
def poll_for_recommendations(url)
|
91
|
+
total_wait = 0
|
92
|
+
while total_wait < @cfg.polling_timeout_secs
|
93
|
+
recommendations = fetch_recommendations(url)
|
94
|
+
unless should_keep_polling(recommendations)
|
95
|
+
return recommendations
|
96
|
+
end
|
97
|
+
sleep(@cfg.polling_interval_secs)
|
98
|
+
total_wait += @cfg.polling_interval_secs
|
99
|
+
end
|
100
|
+
|
101
|
+
@test_selection_status = TEST_SELECTION_RECOMMENDATION_TIMEOUT
|
102
|
+
{}
|
103
|
+
end
|
104
|
+
|
105
|
+
def should_keep_polling(recommendations)
|
106
|
+
!recommendations.empty? &&
|
107
|
+
recommendations[TEST_SELECTION_ENABLED_FIELD] &&
|
108
|
+
recommendations[RECOMMENDATION_SET_STATUS_FIELD] == NOT_READY_RECOMMENDATION_STATUS &&
|
109
|
+
@cfg.polling_interval_secs > 0 && @cfg.polling_timeout_secs > 0
|
110
|
+
end
|
111
|
+
|
112
|
+
def fetch_recommendations(url)
|
113
|
+
http_response = @http_client.get url
|
114
|
+
if http_response.code != HTTP_OK
|
115
|
+
@test_selection_status = TEST_SELECTION_ERROR
|
116
|
+
return {}
|
117
|
+
end
|
118
|
+
|
119
|
+
JSON.parse(http_response)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# This class modifies the `RSpec::Core::Example.run` method to skip the currently run test based on checking if
|
125
|
+
# the test's name is in the `excluded_tests` list.
|
126
|
+
#
|
127
|
+
class RspecExampleClassModifier
|
128
|
+
def add_test_exclusion(excluded_tests)
|
129
|
+
if excluded_tests.empty?
|
130
|
+
return
|
131
|
+
end
|
132
|
+
|
133
|
+
mod = Module.new do
|
134
|
+
define_method(:run) do |*args, &blk|
|
135
|
+
begin
|
136
|
+
test_name = self.metadata[:full_description]
|
137
|
+
if excluded_tests.include? replace_reserved_characters(test_name)
|
138
|
+
info "Test '#{test_name}' excluded by TIA."
|
139
|
+
|
140
|
+
def skipped?
|
141
|
+
return true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
rescue Exception => e
|
145
|
+
error "Unsuccessful TIA test exclusion check. #{e.message}", e
|
146
|
+
end
|
147
|
+
|
148
|
+
super(*args, &blk)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
RSpec::Core::Example.prepend(mod)
|
153
|
+
end
|
154
|
+
end
|
data/agent/utils.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
DOT = '.'
|
2
|
+
DOT_REPLACEMENT = '_'
|
3
|
+
SLASH = '/'
|
4
|
+
SLASH_REPLACEMENT = '#'
|
5
|
+
|
6
|
+
#
|
7
|
+
# The '.' and '/' are treated by the back-end as separators of package/class/method names. To avoid unexpected
|
8
|
+
# processing of RSpec test names, these characters are replaced.
|
9
|
+
#
|
10
|
+
def replace_reserved_characters(test_name)
|
11
|
+
test_name.gsub(DOT, DOT_REPLACEMENT).gsub(SLASH, SLASH_REPLACEMENT)
|
12
|
+
end
|
13
|
+
|
14
|
+
def info(message)
|
15
|
+
log(message, :INFO)
|
16
|
+
end
|
17
|
+
|
18
|
+
def error(message, e = nil)
|
19
|
+
log(message + (e.nil? ? '' : pretty_backtrace(e)), :ERROR)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def log(message, level)
|
25
|
+
puts "[SEALIGHTS] #{Time.now.strftime("%F %T.%3N")} #{level} #{message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# This method creates a mapping of the field names (without the '@' sign) to their values.
|
30
|
+
# An optional list of excluded fields can be passed.
|
31
|
+
# For any field called `jwt`only the last six characters of its value are taken. It is assumed that such a field will
|
32
|
+
# always hold a JSON Web Token.
|
33
|
+
#
|
34
|
+
def pretty_fields(object, *excluded_fields)
|
35
|
+
fields_values = {}
|
36
|
+
object.instance_variables.each do |var|
|
37
|
+
field_name = var.to_s.delete('@')
|
38
|
+
field_value = object.instance_variable_get(var)
|
39
|
+
unless excluded_fields.include? field_name
|
40
|
+
fields_values[field_name] = field_name == 'jwt' ? "...#{field_value[-6..-1]}" : field_value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
fields_values
|
45
|
+
end
|
46
|
+
|
47
|
+
def pretty_backtrace(exception)
|
48
|
+
initial_whitespace = "\n\t"
|
49
|
+
backtrace_lines = exception.backtrace.map { |s| "#{initial_whitespace}#{s}" }.join
|
50
|
+
"#{initial_whitespace}Backtrace:#{backtrace_lines}"
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sealights-rspec-agent
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- SeaLights
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-10-31 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- agent/config.rb
|
20
|
+
- agent/listener.rb
|
21
|
+
- agent/rest-client-wrapper.rb
|
22
|
+
- agent/sealights-rspec-agent.rb
|
23
|
+
- agent/tia.rb
|
24
|
+
- agent/utils.rb
|
25
|
+
homepage:
|
26
|
+
licenses: []
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- agent
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubygems_version: 3.0.3
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: SeaLights RSpec Agent
|
47
|
+
test_files: []
|