fast_ci 0.1.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0"
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