selective-ruby-core 0.1.0-arm64-darwin
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/LICENSE +21 -0
- data/Rakefile +18 -0
- data/exe/selective +16 -0
- data/lib/bin/build_env.sh +34 -0
- data/lib/bin/transport +0 -0
- data/lib/selective/ruby/core/controller.rb +313 -0
- data/lib/selective/ruby/core/named_pipe.rb +103 -0
- data/lib/selective/ruby/core/version.rb +9 -0
- data/lib/selective-ruby-core.rb +64 -0
- metadata +75 -0
    
        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,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: []
         |