selective-ruby-core 0.1.0-aarch64-linux

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a0e4dc18c3cb6d8d0b4821b8ff26bcb0082a3a501593493207d99dd9814305c
4
+ data.tar.gz: 2791a6864d2be9e577d0e5a00600df3f5da2d28f9a3207e64ed1bfddfa7129a1
5
+ SHA512:
6
+ metadata.gz: d8ae66cad83d5e643f5ae882b30424bd06a8c711dfd22b76d805f7f3c610c734901ae7d3adf68c696f07c9a0451b14fbdee3f760945a9775a160a1e5ad199be3
7
+ data.tar.gz: 1d2e7be21539b9fcdccfa23e093892723aa7fedccc8c6d89128fc9d9eff5a34522cd2f1c3e3d4c94ab569fe675f85a6c670488dac11660b4c8a72fd4b088f2f1
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: aarch64-linux
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: []