selective-ruby-core 0.1.0-arm64-darwin

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 849e0fe07f77f37bc98bb32c9db6233deac93a17d10209f52a9e15ff66424f51
4
+ data.tar.gz: 04d6f4ca542d524d0a091fd49f2d6e8cfbb08494d0d78c26e4c50bb9d379e6a6
5
+ SHA512:
6
+ metadata.gz: de400c0e0172970a708a25e9f8afc8507eaf4c46b130ce3b7ff12e737538b38f597d3851cd7186700245022af27419679c973628c8f65f045e0ec0e1e00cd448
7
+ data.tar.gz: 6427a3bca992b8d9ebfbe5a35602cbced09278b20a94714c660afb548f7bf7b190a3cf56e25f157da775ee31c3bbce0aa5fc123405876e92cefb4e7eafdad808
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Selective
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ # Override the guard_clean task to be a no-op
6
+ Rake::Task["release:guard_clean"].clear
7
+
8
+ task "release:guard_clean" do
9
+ # Intentionally blank to skip the check
10
+ end
11
+
12
+ require "rspec/core/rake_task"
13
+
14
+ RSpec::Core::RakeTask.new(:spec)
15
+
16
+ require "standard/rake"
17
+
18
+ task default: %i[spec standard]
data/exe/selective ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+
5
+ # We test selective-ruby using selective-ruby. This means that
6
+ # SimpleCov.start must be called before our code is loaded.
7
+ if ENV["SELECTIVE_SIMPLECOV"]
8
+ require "simplecov"
9
+ SimpleCov.start do
10
+ add_filter "/spec/"
11
+ end
12
+ end
13
+
14
+ require "selective-ruby-core"
15
+
16
+ Selective::Ruby::Core::Init.run(ARGV.dup)
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+
3
+ # Detect the platform (only GitHub Actions in this case)
4
+ if [ -n "$GITHUB_ACTIONS" ]; then
5
+ # Get environment variables
6
+ platform="github_actions"
7
+ branch="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
8
+ pr_title="$PR_TITLE"
9
+ target_branch="${GITHUB_BASE_REF}"
10
+ actor="$GITHUB_ACTOR"
11
+ sha="$GITHUB_SHA"
12
+ commit_message=$(git log --format=%s -n 1 $sha)
13
+ else
14
+ platform="$SELECTIVE_PLATFORM"
15
+ branch="$SELECTIVE_BRANCH"
16
+ pr_title="$SELECTIVE_PR_TITLE"
17
+ target_branch="$SELECTIVE_TARGET_BRANCH"
18
+ actor="$SELECTIVE_ACTOR"
19
+ sha="$SELECTIVE_SHA"
20
+ commit_message=$(git log --format=%s -n 1 $sha)
21
+ fi
22
+
23
+ # Output the JSON
24
+ cat <<EOF
25
+ {
26
+ "platform": "$platform",
27
+ "branch": "$branch",
28
+ "pr_title": "$pr_title",
29
+ "target_branch": "$target_branch",
30
+ "actor": "$actor",
31
+ "sha": "$sha",
32
+ "commit_message": "$commit_message"
33
+ }
34
+ EOF
data/lib/bin/transport ADDED
Binary file
@@ -0,0 +1,313 @@
1
+ require "logger"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Selective
6
+ module Ruby
7
+ module Core
8
+ class Controller
9
+ @@selective_suppress_reporting = false
10
+
11
+ def initialize(runner, debug = false)
12
+ @debug = debug
13
+ @runner = runner
14
+ @retries = 0
15
+ @runner_id = ENV.fetch("SELECTIVE_RUNNER_ID", generate_runner_id)
16
+ @logger = Logger.new("log/#{runner_id}.log")
17
+ end
18
+
19
+ def start(reconnect: false)
20
+ @pipe = NamedPipe.new("/tmp/#{runner_id}_2", "/tmp/#{runner_id}_1")
21
+ @transport_pid = spawn_transport_process(reconnect ? transport_url + "&reconnect=true" : transport_url)
22
+
23
+ handle_termination_signals(transport_pid)
24
+ run_main_loop
25
+ rescue NamedPipe::PipeClosedError
26
+ retry!
27
+ rescue => e
28
+ with_error_handling { raise e }
29
+ end
30
+
31
+ def exec
32
+ runner.exec
33
+ rescue => e
34
+ with_error_handling(include_header: false) { raise e }
35
+ end
36
+
37
+ def self.suppress_reporting!
38
+ @@selective_suppress_reporting = true
39
+ end
40
+
41
+ def self.restore_reporting!
42
+ @@selective_suppress_reporting = false
43
+ end
44
+
45
+ def self.suppress_reporting?
46
+ @@selective_suppress_reporting
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :runner, :pipe, :transport_pid, :retries, :logger, :runner_id
52
+
53
+ BUILD_ENV_SCRIPT_PATH = "../../../bin/build_env.sh".freeze
54
+
55
+ def run_main_loop
56
+ loop do
57
+ message = pipe.read
58
+ next sleep(0.1) if message.nil? || message.empty?
59
+
60
+ response = JSON.parse(message, symbolize_names: true)
61
+
62
+ @logger.info("Received Command: #{response}")
63
+ next if handle_command(response)
64
+
65
+ break
66
+ end
67
+ end
68
+
69
+ def retry!
70
+ @retries += 1
71
+
72
+ with_error_handling { raise "Too many retries" } if retries > 4
73
+
74
+ puts("Retrying in #{retries} seconds...")
75
+ sleep(retries)
76
+ kill_transport
77
+
78
+ pipe.reset!
79
+ start(reconnect: true)
80
+ end
81
+
82
+ def write(data)
83
+ pipe.write JSON.dump(data)
84
+ end
85
+
86
+ def generate_runner_id
87
+ "selgen-#{SecureRandom.hex(4)}"
88
+ end
89
+
90
+ def transport_url
91
+ @transport_url ||= begin
92
+ api_key = ENV.fetch("SELECTIVE_API_KEY")
93
+ run_id = ENV.fetch("SELECTIVE_RUN_ID")
94
+ run_attempt = ENV.fetch("SELECTIVE_RUN_ATTEMPT", SecureRandom.uuid)
95
+ host = ENV.fetch("SELECTIVE_HOST", "wss://app.selective.ci")
96
+
97
+ # Validate that host is a valid websocket url(starts with ws:// or wss://)
98
+ raise "Invalid host: #{host}" unless host.match?(/^wss?:\/\//)
99
+
100
+ params = {
101
+ "run_id" => run_id,
102
+ "run_attempt" => run_attempt,
103
+ "api_key" => api_key,
104
+ "runner_id" => runner_id
105
+ }.merge(metadata: build_env.to_json)
106
+
107
+ query_string = URI.encode_www_form(params)
108
+
109
+ "#{host}/transport/websocket?#{query_string}"
110
+ end
111
+ end
112
+
113
+ def build_env
114
+ result = `#{Pathname.new(__dir__) + BUILD_ENV_SCRIPT_PATH}`
115
+ JSON.parse(result)
116
+ end
117
+
118
+ def spawn_transport_process(url)
119
+ root_path = Gem.loaded_specs["selective-ruby-core"].full_gem_path
120
+ transport_path = File.join(root_path, "lib", "bin", "transport")
121
+ get_transport_path = File.join(root_path, "bin", "get_transport")
122
+
123
+ # The get_transport script is not released with the gem, so this
124
+ # code is intended for development/CI purposes.
125
+ if !File.exist?(transport_path) && File.exist?(get_transport_path)
126
+ require "open3"
127
+ output, status = Open3.capture2e(get_transport_path)
128
+ if !status.success?
129
+ puts <<~TEXT
130
+ Failed to download transport binary.
131
+
132
+ #{output}
133
+ TEXT
134
+ end
135
+ end
136
+
137
+ Process.spawn(transport_path, url, runner_id).tap do |pid|
138
+ Process.detach(pid)
139
+ end
140
+ end
141
+
142
+ def handle_termination_signals(pid)
143
+ ["INT", "TERM"].each do |signal|
144
+ Signal.trap(signal) do
145
+ # :nocov:
146
+ kill_transport(signal: signal)
147
+ exit
148
+ # :nocov:
149
+ end
150
+ end
151
+ end
152
+
153
+ def kill_transport(signal: "TERM")
154
+ begin
155
+ pipe.write "exit"
156
+
157
+ # Give up to 5 seconds for graceful exit
158
+ # before killing it below
159
+ 1..5.times do
160
+ Process.getpgid(transport_pid)
161
+
162
+ sleep(1)
163
+ end
164
+ rescue NamedPipe::PipeClosedError
165
+ # If the pipe is close, move straight to killing
166
+ # it forcefully.
167
+ end
168
+
169
+ # :nocov:
170
+ Process.kill(signal, transport_pid)
171
+ # :nocov:
172
+ rescue Errno::ESRCH
173
+ # Process already gone noop
174
+ end
175
+
176
+ def handle_command(response)
177
+ case response[:command]
178
+ when "init"
179
+ print_init(response[:runner_id])
180
+ when "test_manifest"
181
+ handle_test_manifest
182
+ when "run_test_cases"
183
+ handle_run_test_cases(response[:test_case_ids])
184
+ when "remove_failed_test_case_result"
185
+ handle_remove_failed_test_case_result(response[:test_case_id])
186
+ when "reconnect"
187
+ handle_reconnect
188
+ when "print_message"
189
+ handle_print_message(response[:message])
190
+ when "close"
191
+ handle_close(response[:exit_status])
192
+
193
+ # This is here for the sake of test where we
194
+ # cannot exit but we need to break the loop
195
+ return false
196
+ end
197
+
198
+ true
199
+ end
200
+
201
+ def handle_reconnect
202
+ kill_transport
203
+ pipe.reset!
204
+ start(reconnect: true)
205
+ end
206
+
207
+ def handle_test_manifest
208
+ self.class.restore_reporting!
209
+ @logger.info("Sending Response: test_manifest")
210
+ write({type: "test_manifest", data: {
211
+ test_cases: runner.manifest["examples"],
212
+ modified_test_files: modified_test_files
213
+ }})
214
+ end
215
+
216
+ def handle_run_test_cases(test_cases)
217
+ runner.run_test_cases(test_cases, method(:test_case_callback))
218
+ end
219
+
220
+ def test_case_callback(test_case)
221
+ @logger.info("Sending Response: test_case_result: #{test_case[:id]}")
222
+ write({type: "test_case_result", data: test_case})
223
+ end
224
+
225
+ def handle_remove_failed_test_case_result(test_case_id)
226
+ runner.remove_failed_test_case_result(test_case_id)
227
+ end
228
+
229
+ def modified_test_files
230
+ # Todo: This should find files changed in the current branch
231
+ `git diff --name-only`.split("\n").filter do |f|
232
+ f.match?(/^#{runner.base_test_path}/)
233
+ end
234
+ end
235
+
236
+ def handle_print_message(message)
237
+ print_warning(message)
238
+ end
239
+
240
+ def handle_close(exit_status = nil)
241
+ self.class.restore_reporting!
242
+ runner.finish unless exit_status.is_a?(Integer)
243
+
244
+ kill_transport
245
+ pipe.delete_pipes
246
+ exit(exit_status || runner.exit_status)
247
+ end
248
+
249
+ def debug?
250
+ @debug
251
+ end
252
+
253
+ def with_error_handling(include_header: true)
254
+ yield
255
+ rescue => e
256
+ raise e if debug?
257
+ header = <<~TEXT
258
+ An error occurred. Please rerun with --debug
259
+ and contact support at https://selective.ci/support
260
+ TEXT
261
+
262
+ unless @banner_displayed
263
+ header = <<~TEXT
264
+ #{banner}
265
+
266
+ #{header}
267
+ TEXT
268
+ end
269
+
270
+ puts_indented <<~TEXT
271
+ \e[31m
272
+ #{header if include_header}
273
+ #{e.message}
274
+ \e[0m
275
+ TEXT
276
+
277
+ exit 1
278
+ end
279
+
280
+ def print_warning(message)
281
+ puts_indented <<~TEXT
282
+ \e[33m
283
+ #{message}
284
+ \e[0m
285
+ TEXT
286
+ end
287
+
288
+ def print_init(runner_id)
289
+ puts_indented <<~TEXT
290
+ #{banner}
291
+ Runner ID: #{runner_id.gsub("selgen-", "")}
292
+ TEXT
293
+ end
294
+
295
+ def puts_indented(text)
296
+ puts text.gsub(/^/, " ")
297
+ end
298
+
299
+ def banner
300
+ @banner_displayed = true
301
+ <<~BANNER
302
+ ____ _ _ _
303
+ / ___| ___| | ___ ___| |_(_)_ _____
304
+ \\___ \\ / _ \\ |/ _ \\/ __| __| \\ \\ / / _ \\
305
+ ___) | __/ | __/ (__| |_| |\\ V / __/
306
+ |____/ \\___|_|\\___|\\___|\\__|_| \\_/ \\___|
307
+ ________________________________________
308
+ BANNER
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,103 @@
1
+ module Selective
2
+ module Ruby
3
+ module Core
4
+ class NamedPipe
5
+ class PipeClosedError < StandardError; end
6
+
7
+ attr_reader :read_pipe_path, :write_pipe_path
8
+
9
+ def initialize(read_pipe_path, write_pipe_path, skip_reset: false)
10
+ @read_pipe_path = read_pipe_path
11
+ @write_pipe_path = write_pipe_path
12
+
13
+ delete_pipes unless skip_reset
14
+ initialize_pipes
15
+ end
16
+
17
+ def initialize_pipes
18
+ create_pipes
19
+
20
+ # Open the read and write pipes in separate threads
21
+ Thread.new do
22
+ @read_pipe = File.open(read_pipe_path, "r")
23
+ end
24
+ Thread.new do
25
+ @write_pipe = File.open(write_pipe_path, "w")
26
+ end
27
+ end
28
+
29
+ def write(message)
30
+ return unless write_pipe
31
+
32
+ chunk_size = 1024 # 1KB chunks
33
+ offset = 0
34
+ begin
35
+ while offset < message.bytesize
36
+ chunk = message.byteslice(offset, chunk_size)
37
+
38
+ write_pipe.write(chunk)
39
+ write_pipe.flush
40
+
41
+ offset += chunk_size
42
+ end
43
+
44
+ write_pipe.write("\n")
45
+ write_pipe.flush
46
+ rescue Errno::EPIPE
47
+ raise PipeClosedError
48
+ end
49
+ end
50
+
51
+ def read
52
+ return unless read_pipe
53
+ begin
54
+ message = read_pipe.gets.chomp
55
+ rescue NoMethodError => e
56
+ if e.name == :chomp
57
+ raise PipeClosedError
58
+ else
59
+ raise e
60
+ end
61
+ end
62
+ message
63
+ end
64
+
65
+ def reset!
66
+ delete_pipes
67
+ initialize_pipes
68
+ end
69
+
70
+ def delete_pipes
71
+ # Close the pipes before deleting them
72
+ read_pipe&.close
73
+ write_pipe&.close
74
+
75
+ # Allow threads to close before deleting pipes
76
+ sleep(0.1)
77
+
78
+ delete_pipe(read_pipe_path)
79
+ delete_pipe(write_pipe_path)
80
+ rescue Errno::EPIPE
81
+ # Noop
82
+ end
83
+
84
+ private
85
+
86
+ attr_reader :read_pipe, :write_pipe
87
+
88
+ def create_pipes
89
+ create_pipe(read_pipe_path)
90
+ create_pipe(write_pipe_path)
91
+ end
92
+
93
+ def create_pipe(path)
94
+ system("mkfifo #{path}") unless File.exist?(path)
95
+ end
96
+
97
+ def delete_pipe(path)
98
+ File.delete(path) if File.exist?(path)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Selective
4
+ module Ruby
5
+ module Core
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
6
+ loader.ignore("#{__dir__}/selective-ruby-core.rb")
7
+ loader.setup
8
+
9
+ module Selective
10
+ module Ruby
11
+ module Core
12
+ class Error < StandardError; end
13
+
14
+ @@available_runners = {}
15
+
16
+ def self.register_runner(name, runner_class)
17
+ @@available_runners[name] = runner_class
18
+ end
19
+
20
+ def self.runner_for(name)
21
+ @@available_runners[name] || raise("Unknown runner #{name}")
22
+ end
23
+
24
+ class Init
25
+ def initialize(args)
26
+ @debug = !args.delete("--debug").nil?
27
+ @runner_name, @args, @command = parse_args(args)
28
+ require_runner
29
+ end
30
+
31
+ def self.run(args)
32
+ new(args).send(:run)
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :debug, :runner_name, :args, :command
38
+
39
+ def run
40
+ Selective::Ruby::Core::Controller.new(runner, debug).send(command)
41
+ end
42
+
43
+ def parse_args(args)
44
+ # Returns runner_name, args, command
45
+ if args[0] == "exec" # e.g. selective exec rspec
46
+ [args[1], args[2..], :exec]
47
+ else # e.g. selective rspec
48
+ [args[0], args[1..], :start]
49
+ end
50
+ end
51
+
52
+ def runner
53
+ Selective::Ruby::Core.runner_for(runner_name).new(args)
54
+ end
55
+
56
+ def require_runner
57
+ require "selective-ruby-#{runner_name}"
58
+ rescue LoadError
59
+ nil
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: selective-ruby-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: arm64-darwin
6
+ authors:
7
+ - Benjamin Wood
8
+ - Nate Vick
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2023-11-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: zeitwerk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 2.6.12
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 2.6.12
28
+ description: Selective is an intelligent test runner for your current CI provider.
29
+ Get real-time test results, intelligent ordering based on code changes, shorter
30
+ run times, automatic flake detection, the ability to re-enqueue failed tests, and
31
+ more.
32
+ email:
33
+ - ben@hint.io
34
+ - nate@hint.io
35
+ executables:
36
+ - selective
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - LICENSE
41
+ - Rakefile
42
+ - exe/selective
43
+ - lib/bin/build_env.sh
44
+ - lib/bin/transport
45
+ - lib/selective-ruby-core.rb
46
+ - lib/selective/ruby/core/controller.rb
47
+ - lib/selective/ruby/core/named_pipe.rb
48
+ - lib/selective/ruby/core/version.rb
49
+ homepage: https://www.selective.ci
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://www.selective.ci
54
+ source_code_uri: http://github.com/selectiveci/selective-ruby-core
55
+ changelog_uri: https://github.com/selectiveci/selective-ruby-core/blob/main/CHANGELOG.md
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 2.6.0
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.4.10
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Selective Ruby Client Core
75
+ test_files: []