fast_ci 0.1.2 → 1.0.1

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.
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubycritic/cli/application'
4
+ require 'fast_ci'
5
+ require 'base64'
6
+ require 'zlib'
7
+
8
+ module FastCI
9
+ module RubyCritic
10
+ module Cli
11
+ class Application < ::RubyCritic::Cli::Application
12
+ def execute
13
+ events = []
14
+ events << ['ruby_critic_run'.upcase, {}]
15
+ status = super
16
+
17
+ content = File.read('tmp/rubycritic/report.json')
18
+ report = JSON.load(content)
19
+ modules = report['analysed_modules']
20
+ report['analysed_modules'] = {}
21
+ modules.each do |mod|
22
+ report['analysed_modules'][ mod['path'] ] = mod
23
+ mod['smells'].each do |smell|
24
+ location = smell['locations'].first
25
+ start_line = location['line'] - 1
26
+ end_line = start_line + 3
27
+ lines = File.readlines(location['path'])[start_line..end_line]
28
+ location['src'] = lines.join
29
+ smell['locations'][0] = location
30
+ end
31
+ end
32
+
33
+ compressed_data = ::Base64.strict_encode64(Zlib::Deflate.deflate(report.to_json, 9))
34
+ events << ['ruby_critic_exit_status'.upcase, ['0', { exitstatus: status, output: '', compressed_data: compressed_data }]]
35
+
36
+ if ENV['FSCI_REMOTE_TESTS'] == 'true'
37
+ json_events = {
38
+ build_id: FastCI.configuration.orig_build_id,
39
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate(events), 9)),
40
+ }
41
+
42
+ FastCI.send_events(json_events)
43
+ else
44
+ FastCI.report_ruby_critic(compressed_data, 'passed')
45
+ end
46
+ return status
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ require_relative "rspec_formatter"
3
+ require_relative "extract_definitions"
4
+
5
+ module FastCI
6
+ module RunnerPrepend
7
+ def run_specs(example_groups)
8
+ @rspec_started_at = Time.now
9
+ json_events = {
10
+ build_id: FastCI.configuration.orig_build_id,
11
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate([['RSPEC_RUN', { started_at: @rspec_started_at, test_env_number: ENV["TEST_ENV_NUMBER"] }]]), 9)),
12
+ }
13
+ FastCI.send_events(json_events)
14
+
15
+ examples_count = @world.example_count(example_groups)
16
+
17
+ example_groups = example_groups.reduce({}) do |acc, ex_group|
18
+ if acc[ex_group.file_path]
19
+ acc[ex_group.file_path] << ex_group
20
+ else
21
+ acc[ex_group.file_path] = [ex_group]
22
+ end
23
+ acc
24
+ end
25
+
26
+ FastCI.configure { |c| c.run_key = "rspec" }
27
+
28
+ FastCI.rspec_ws.on(:enq_request) do
29
+ example_groups.reduce({}) do |example_group_descriptions, (file, example_groups)|
30
+ example_groups.each do |example_group|
31
+ data = FastCI::ExtractDescriptions.new.call(example_group, count: true)
32
+
33
+ next if data[:test_count] == 0
34
+
35
+ if example_group_descriptions[file]
36
+ example_group_descriptions[file].merge!(data) do |k, v1, v2|
37
+ v1 + v2
38
+ end
39
+ else
40
+ example_group_descriptions[file] = data
41
+ end
42
+ end
43
+ example_group_descriptions
44
+ end
45
+ end
46
+
47
+ examples_passed = @configuration.reporter.report(examples_count) do |reporter|
48
+ @configuration.with_suite_hooks do
49
+ if examples_count == 0 && @configuration.fail_if_no_examples
50
+ return @configuration.failure_exit_code
51
+ end
52
+
53
+ formatter = FastCI::RspecFormatter.new(STDOUT)
54
+
55
+ reporter.register_listener(formatter, :example_finished)
56
+ if ENV['FSCI_REMOTE_TESTS'] == 'true'
57
+ reporter.register_listener(formatter, :start)
58
+ reporter.register_listener(formatter, :example_group_started)
59
+ reporter.register_listener(formatter, :example_started)
60
+ reporter.register_listener(formatter, :example_passed)
61
+ reporter.register_listener(formatter, :example_failed)
62
+ reporter.register_listener(formatter, :example_pending)
63
+ reporter.register_listener(formatter, :example_group_finished)
64
+ reporter.register_listener(formatter, :close)
65
+ end
66
+
67
+ FastCI.rspec_ws.on(:deq) do |tests|
68
+ tests.each do |test|
69
+ file, scoped_id = test.split(":", 2)
70
+ Thread.current[:fastci_scoped_ids] = scoped_id
71
+ example_groups[file].each do |file_group|
72
+ formatter.current_test_key = test
73
+
74
+ file_group.run(reporter)
75
+ end
76
+ end
77
+
78
+ formatter.dump_and_reset
79
+ end
80
+
81
+ FastCI.rspec_await
82
+
83
+ formatter.passed?
84
+ end
85
+ end
86
+
87
+ exit_code(examples_passed)
88
+ end
89
+
90
+ def exit_code(examples_passed=false)
91
+ if ENV['FSCI_REMOTE_TESTS'] == 'true'
92
+ run_time = Time.now - (@rspec_started_at || 1.seconds.ago)
93
+ events = @world.non_example_failure ? [['RSPEC_RUN', { failed_after: run_time, test_env_number: ENV["TEST_ENV_NUMBER"] }]] : [['RSPEC_RUN', { succeed_after: run_time, test_env_number: ENV["TEST_ENV_NUMBER"] }]]
94
+ json_events = {
95
+ build_id: FastCI.configuration.orig_build_id,
96
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate(events), 9)),
97
+ }
98
+ FastCI.send_events(json_events)
99
+ end
100
+
101
+ return @configuration.error_exit_code || @configuration.failure_exit_code if @world.non_example_failure
102
+ return @configuration.failure_exit_code unless examples_passed
103
+
104
+ 0
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,39 @@
1
+ module FastCI
2
+ module SimpleCov
3
+ module Reporting
4
+ def self.included base
5
+ base.instance_eval do
6
+ unless (ENV['FAST_CI_SECRET_KEY'] || '').empty?
7
+ def write_last_run(result)
8
+ ::SimpleCov::LastRun.write(result:
9
+ result.coverage_statistics.transform_values do |stats|
10
+ round_coverage(stats.percent)
11
+ end)
12
+
13
+ source = {}
14
+
15
+ result.source_files.each do |source_file|
16
+ source[source_file.filename.gsub(root, '')] = source_file.src
17
+ end
18
+
19
+ result_json = {}
20
+
21
+ result.as_json.each do |command, data|
22
+ result_json[command] = data
23
+ data['coverage'].clone.each do |src, file_data|
24
+ result_json[command]['coverage'].delete(src)
25
+
26
+ file_data['src'] = source[src.gsub(root, '')]
27
+
28
+ result_json[command]['coverage'][src.gsub(root, '')] = file_data
29
+ end
30
+ end
31
+
32
+ FastCI.report_simplecov(result_json.to_json)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,68 @@
1
+ require_relative "simple_cov/reporting"
2
+
3
+ module FastCI
4
+ module SimpleCov
5
+ if ENV['FSCI_REMOTE_TESTS'] == 'true' && ENV["SIMPLECOV_ACTIVE"] && ENV['DRYRUN'] != 'true'
6
+ ::SimpleCov.send(:at_exit) do
7
+ ::SimpleCov.result.format!
8
+
9
+ config = {
10
+ minimum_coverage: ::SimpleCov.minimum_coverage,
11
+ maximum_coverage_drop: ::SimpleCov.maximum_coverage_drop,
12
+ minimum_coverage_by_file: ::SimpleCov.minimum_coverage_by_file,
13
+ }
14
+ rspec_runner_index = ENV["TEST_ENV_NUMBER".freeze].to_i
15
+ events = [['simplecov_config'.upcase, [ rspec_runner_index, config ]]]
16
+
17
+ json_events = {
18
+ build_id: FastCI.configuration.orig_build_id,
19
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate(events), 9)),
20
+ }
21
+
22
+ FastCI.send_events(json_events)
23
+ end
24
+
25
+ module PrependSc
26
+ def start(*args, &block)
27
+ add_filter "tmp"
28
+ merge_timeout 3600
29
+ command_name "RSpec_#{ENV["TEST_ENV_NUMBER".freeze].to_i}"
30
+
31
+ if ENV["NO_COVERAGE"]
32
+ use_merging false
33
+ return
34
+ end
35
+ super
36
+ end
37
+ end
38
+ ::SimpleCov.singleton_class.prepend(PrependSc)
39
+
40
+ module Scf
41
+ def format!
42
+ return if ENV["NO_COVERAGE"]
43
+ rspec_runner_index = ENV["TEST_ENV_NUMBER".freeze].to_i
44
+
45
+ original_result_json = if ENV['CI_PROJECT_DIR'].present?
46
+ JSON.fast_generate(original_result.transform_keys {|key| key.sub(ENV['CI_PROJECT_DIR'], '/app') })
47
+ else
48
+ JSON.fast_generate(original_result)
49
+ end
50
+ compressed_data = Base64.strict_encode64(Zlib::Deflate.deflate(original_result_json, 9))
51
+ events = [['simplecov_result'.upcase, [ rspec_runner_index, compressed_data ]]]
52
+
53
+ json_events = {
54
+ build_id: FastCI.configuration.orig_build_id,
55
+ compressed_data: Base64.strict_encode64(Zlib::Deflate.deflate(JSON.fast_generate(events), 9)),
56
+ }
57
+
58
+ FastCI.send_events(json_events)
59
+ super
60
+ end
61
+ end
62
+
63
+ ::SimpleCov::Result.prepend(Scf)
64
+ else
65
+ ::SimpleCov.send(:include, FastCI::SimpleCov::Reporting) unless ENV['DRYRUN'] == 'true'
66
+ end
67
+ end
68
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FastCI
4
- VERSION = "0.1.2"
4
+ VERSION = "1.0.1"
5
5
  end
data/lib/fast_ci.rb CHANGED
@@ -3,10 +3,12 @@
3
3
  require_relative "fast_ci/version"
4
4
  require_relative "fast_ci/configuration"
5
5
  require_relative "fast_ci/exceptions"
6
+ require_relative "minitest/reporters/FastCI_reporter"
6
7
 
7
8
  require "async"
8
9
  require "async/http/endpoint"
9
10
  require "async/websocket/client"
11
+ require "net/http"
10
12
 
11
13
  module FastCI
12
14
  class Error < StandardError; end
@@ -20,27 +22,86 @@ module FastCI
20
22
  yield(configuration)
21
23
  end
22
24
 
23
- def ws
24
- @ws ||= WebSocket.new
25
+ def rspec_ws
26
+ @rspec_ws ||= WebSocket.new('rspec')
25
27
  end
26
28
 
27
- def await
28
- ws.await
29
+ def minitest_ws
30
+ @minitest_ws ||= WebSocket.new('minitest')
31
+ end
32
+
33
+ def report_simplecov(results)
34
+ post_report(report_options('simplecov', results))
35
+ end
36
+
37
+ def report_ruby_critic(compressed_data, status)
38
+ post_report(report_options('ruby_critic', compressed_data).merge({ status: status }))
39
+ end
40
+
41
+ def report_brakeman(compressed_data, status)
42
+ post_report(report_options('brakeman', compressed_data).merge({ status: status }))
43
+ end
44
+
45
+ def report_bundler_audit(compressed_data, status)
46
+ post_report(report_options('bundler_audit', compressed_data).merge({ status: status }))
47
+ end
48
+
49
+ def rspec_await
50
+ rspec_ws.await
51
+ end
52
+
53
+ def minitest_await
54
+ minitest_ws.await
29
55
  end
30
56
 
31
57
  def debug(msg)
32
- puts "\n\e[36mDEBUG: \e[0m #{msg}\n" if ENV["FAST_CI_DEBUG"]
58
+ puts "\n\e[36mDEBUG: \e[0m #{msg}\n" if ENV["fast_ci_DEBUG"]
59
+ end
60
+
61
+ def report_options(run_key, content)
62
+ {
63
+ build_id: FastCI.configuration.build_id,
64
+ run_key: run_key,
65
+ secret_key: FastCI.configuration.secret_key,
66
+ branch: FastCI.configuration.branch,
67
+ commit: FastCI.configuration.commit,
68
+ commit_msg: FastCI.configuration.commit_msg,
69
+ author: FastCI.configuration.author.to_json,
70
+ content: content
71
+ }
72
+ end
73
+
74
+ def post_report(data)
75
+ uri = URI("#{FastCI.configuration.fastci_api_url}/api/runs")
76
+ res = Net::HTTP.post_form(uri, data)
77
+ end
78
+
79
+ def send_events(data)
80
+ reset_webmock = false
81
+ if defined?(WebMock)
82
+ reset_webmock = !WebMock.net_connect_allowed?
83
+ WebMock.allow_net_connect!
84
+ end
85
+
86
+ uri = URI("#{FastCI.configuration.fastci_main_url}/api/v1/gitlab_events")
87
+ res = Net::HTTP.post_form(uri, data)
88
+
89
+ if reset_webmock
90
+ WebMock.disable_net_connect!
91
+ end
33
92
  end
34
93
  end
35
94
 
36
95
  class WebSocket
37
96
  attr_reader :node_index
97
+ attr_accessor :connection, :task, :run_key
38
98
 
39
99
  SUPPORTED_EVENTS = %i[enq_request deq].freeze
40
100
 
41
- def initialize
101
+ def initialize(run_key)
42
102
  @on = {}
43
103
  @ref = 0
104
+ @run_key = run_key
44
105
  end
45
106
 
46
107
  def on(event, &block)
@@ -50,34 +111,49 @@ module FastCI
50
111
  @on[event] = block
51
112
  end
52
113
 
53
- def send_msg(connection, event, payload = {})
114
+ def send_msg(event, payload = {})
54
115
  FastCI.debug("ws#send_msg: #{event} -> #{payload.inspect}")
55
116
  connection.write({ "topic": topic, "event": event, "payload": payload, "ref": ref })
56
117
  connection.flush
57
118
  end
58
119
 
59
- def await
60
- before_start_connection
120
+ def connect_to_ws
61
121
  Async do |task|
122
+ before_start_connection
62
123
  Async::WebSocket::Client.connect(endpoint) do |connection|
63
124
  after_start_connection
64
- send_msg(connection, "phx_join")
125
+ self.connection = connection
126
+ self.task = task
127
+ yield
128
+ ensure
65
129
 
130
+ leave
131
+ end
132
+ end
133
+ end
134
+
135
+ def await(retry_count = 0)
136
+ connect_to_ws do
137
+ send_msg("phx_join")
138
+
139
+ begin
66
140
  while message = connection.read
67
141
  FastCI.debug("ws#msg_received: #{message.inspect}")
68
142
 
69
143
  response = message.dig(:payload, :response)
70
144
 
71
145
  case response&.dig(:event) || message[:event]
146
+ when "phx_error"
147
+ raise("[FastCI] Unexpected error")
72
148
  when "join"
73
- handle_join(connection, response)
149
+ handle_join(response)
74
150
  when "deq_request"
75
- handle_deq_request(connection, response)
151
+ handle_deq_request(response)
76
152
  when "deq"
77
153
  if (tests = response[:tests]).any?
78
154
  result = @on[:deq].call(tests)
79
155
  task.async do
80
- send_msg(connection, "deq", result)
156
+ send_msg("deq", result)
81
157
  end
82
158
  else
83
159
  break
@@ -88,15 +164,23 @@ module FastCI
88
164
  puts response
89
165
  end
90
166
  end
91
- ensure
92
- send_msg(connection, "leave")
93
- connection.close
167
+ rescue => e
168
+ puts e.message
169
+ puts e.backtrace.join("\n")
170
+ task&.stop
94
171
  end
95
172
  end
96
173
  end
97
174
 
98
175
  private
99
176
 
177
+ def leave
178
+ send_msg("leave")
179
+ connection.close
180
+ rescue StandardError => e
181
+ # noop
182
+ end
183
+
100
184
  # https://github.com/bblimke/webmock/blob/b709ba22a2949dc3bfac662f3f4da88a21679c2e/lib/webmock/http_lib_adapters/async_http_client_adapter.rb#L8
101
185
  def before_start_connection
102
186
  if defined?(WebMock::HttpLibAdapters::AsyncHttpClientAdapter)
@@ -111,18 +195,18 @@ module FastCI
111
195
  end
112
196
  end
113
197
 
114
- def handle_join(connection, response)
198
+ def handle_join(response)
115
199
  @node_index = response[:node_index]
116
200
 
117
201
  FastCI.debug("NODE_INDEX: #{@node_index}")
118
202
 
119
- send_msg(connection, "enq", { tests: @on[:enq_request].call }) if node_index.zero?
203
+ send_msg("enq", { tests: @on[:enq_request].call }) if node_index.zero?
120
204
 
121
- send_msg(connection, "deq") if response[:state] == "running"
205
+ send_msg("deq") if response[:state] == "running"
122
206
  end
123
207
 
124
- def handle_deq_request(connection, _response)
125
- send_msg(connection, "deq")
208
+ def handle_deq_request(_response)
209
+ send_msg("deq")
126
210
  end
127
211
 
128
212
  def ref
@@ -130,21 +214,23 @@ module FastCI
130
214
  end
131
215
 
132
216
  def topic
133
- "test_orchestrator:#{FastCI.configuration.run_key}-#{FastCI.configuration.build_id}"
217
+ "test_orchestrator:#{run_key}-#{FastCI.configuration.build_id}"
134
218
  end
135
219
 
136
220
  def endpoint
137
221
  params = URI.encode_www_form({
138
222
  build_id: FastCI.configuration.build_id,
139
- run_key: FastCI.configuration.run_key,
223
+ run_key: run_key,
140
224
  secret_key: FastCI.configuration.secret_key,
225
+ branch: FastCI.configuration.branch,
141
226
  commit: FastCI.configuration.commit,
142
- branch: FastCI.configuration.branch
227
+ commit_msg: FastCI.configuration.commit_msg,
228
+ author: FastCI.configuration.author.to_json
143
229
  })
144
230
 
145
- url = "ws://#{FastCI.configuration.api_url}/test_orchestrators/socket/websocket?#{params}"
231
+ url = "wss://#{FastCI.configuration.api_url}/test_orchestrators/socket/websocket?#{params}"
146
232
 
147
- Async::HTTP::Endpoint.parse(url)
233
+ Async::HTTP::Endpoint.parse(url, alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
148
234
  end
149
235
  end
150
236
  end