|
| 1 | +require "optparse" |
| 2 | + |
| 3 | +module RSpecQ |
| 4 | + class Parser |
| 5 | + DEFAULT_REDIS_HOST = "127.0.0.1".freeze |
| 6 | + DEFAULT_REPORT_TIMEOUT = 3600 # 1 hour |
| 7 | + DEFAULT_MAX_REQUEUES = 3 |
| 8 | + DEFAULT_FAIL_FAST = 0 |
| 9 | + |
| 10 | + def self.parse!(args) |
| 11 | + new(args).parse! |
| 12 | + end |
| 13 | + |
| 14 | + attr_reader :args, :opts |
| 15 | + |
| 16 | + def initialize(args) |
| 17 | + @args = args |
| 18 | + @opts = {} |
| 19 | + end |
| 20 | + |
| 21 | + # This method mutates `args` in order to allow both rspecq and |
| 22 | + # rspec options to be passed to rspecq. ["--build", "foo", |
| 23 | + # "--", "--pattern", "bar"] will set `build: "foo"` for rspecq |
| 24 | + # options and leave ["--pattern", "bar"] to be passed to rspec |
| 25 | + def parse! |
| 26 | + parse_args! |
| 27 | + parse_env |
| 28 | + |
| 29 | + # rubocop:disable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause |
| 30 | + raise OptionParser::MissingArgument.new(:build) if opts[:build].nil? |
| 31 | + raise OptionParser::MissingArgument.new(:worker) if !opts[:report] && opts[:worker].nil? |
| 32 | + # rubocop:enable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause |
| 33 | + |
| 34 | + opts |
| 35 | + end |
| 36 | + |
| 37 | + private |
| 38 | + |
| 39 | + def parse_args! |
| 40 | + OptionParser.new do |o| |
| 41 | + name = File.basename($PROGRAM_NAME) |
| 42 | + |
| 43 | + o.banner = <<~BANNER |
| 44 | + NAME: |
| 45 | + #{name} - Optimally distribute and run RSpec suites among parallel workers |
| 46 | +
|
| 47 | + USAGE: |
| 48 | + #{name} [<options>] [spec files or directories] |
| 49 | + #{name} [<options>] -- [<rspec options>] [spec files or directories] |
| 50 | + BANNER |
| 51 | + |
| 52 | + o.separator "" |
| 53 | + o.separator "OPTIONS:" |
| 54 | + |
| 55 | + o.on("-b", "--build ID", "A unique identifier for the build. Should be " \ |
| 56 | + "common among workers participating in the same build.") do |v| |
| 57 | + opts[:build] = v |
| 58 | + end |
| 59 | + |
| 60 | + o.on("-w", "--worker ID", "An identifier for the worker. Workers " \ |
| 61 | + "participating in the same build should have distinct IDs.") do |v| |
| 62 | + opts[:worker] = v |
| 63 | + end |
| 64 | + |
| 65 | + o.on("-r", "--redis HOST", "Redis host to connect to " \ |
| 66 | + "(default: #{DEFAULT_REDIS_HOST}).") do |v| |
| 67 | + puts "--redis is deprecated. Use --redis-host or --redis-url instead" |
| 68 | + opts[:redis_host] = v |
| 69 | + end |
| 70 | + |
| 71 | + o.on("--redis-host HOST", "Redis host to connect to " \ |
| 72 | + "(default: #{DEFAULT_REDIS_HOST}).") do |v| |
| 73 | + opts[:redis_host] = v |
| 74 | + end |
| 75 | + |
| 76 | + o.on("--redis-url URL", "The URL of the Redis host to connect to " \ |
| 77 | + "(e.g.: redis://127.0.0.1:6379/0).") do |v| |
| 78 | + opts[:redis_url] = v |
| 79 | + end |
| 80 | + |
| 81 | + o.on("--update-timings", "Update the global job timings key with the " \ |
| 82 | + "timings of this build. Note: This key is used as the basis for job " \ |
| 83 | + "scheduling.") do |v| |
| 84 | + opts[:timings] = v |
| 85 | + end |
| 86 | + |
| 87 | + o.on("--file-split-threshold N", Integer, "Split spec files slower than N " \ |
| 88 | + "seconds and schedule them as individual examples.") do |v| |
| 89 | + opts[:file_split_threshold] = v |
| 90 | + end |
| 91 | + |
| 92 | + o.on("--report", "Enable reporter mode: do not pull tests off the queue; " \ |
| 93 | + "instead print build progress and exit when it's " \ |
| 94 | + "finished.\n#{o.summary_indent * 9} " \ |
| 95 | + "Exits with a non-zero status code if there were any " \ |
| 96 | + "failures.") do |v| |
| 97 | + opts[:report] = v |
| 98 | + end |
| 99 | + |
| 100 | + o.on("--report-timeout N", Integer, "Fail if build is not finished after " \ |
| 101 | + "N seconds. Only applicable if --report is enabled " \ |
| 102 | + "(default: #{DEFAULT_REPORT_TIMEOUT}).") do |v| |
| 103 | + opts[:report_timeout] = v |
| 104 | + end |
| 105 | + |
| 106 | + o.on("--max-requeues N", Integer, "Retry failed examples up to N times " \ |
| 107 | + "before considering them legit failures " \ |
| 108 | + "(default: #{DEFAULT_MAX_REQUEUES}).") do |v| |
| 109 | + opts[:max_requeues] = v |
| 110 | + end |
| 111 | + |
| 112 | + o.on("--fail-fast N", Integer, "Abort build with a non-zero status code " \ |
| 113 | + "after N failed examples.") do |v| |
| 114 | + opts[:fail_fast] = v |
| 115 | + end |
| 116 | + |
| 117 | + o.on_tail("-h", "--help", "Show this message.") do |
| 118 | + puts o |
| 119 | + exit |
| 120 | + end |
| 121 | + |
| 122 | + o.on_tail("-v", "--version", "Print the version and exit.") do |
| 123 | + puts "#{name} #{RSpecQ::VERSION}" |
| 124 | + exit |
| 125 | + end |
| 126 | + end.parse!(args) |
| 127 | + end |
| 128 | + |
| 129 | + def parse_env |
| 130 | + opts[:report] = opts.fetch(:report, env_set?("RSPECQ_REPORT")) |
| 131 | + opts[:timings] = opts.fetch(:timings, env_set?("RSPECQ_UPDATE_TIMINGS")) |
| 132 | + |
| 133 | + opts[:build] ||= ENV["RSPECQ_BUILD"] |
| 134 | + opts[:worker] ||= ENV["RSPECQ_WORKER"] |
| 135 | + opts[:redis_host] ||= ENV["RSPECQ_REDIS"] || DEFAULT_REDIS_HOST |
| 136 | + opts[:file_split_threshold] ||= Integer(ENV["RSPECQ_FILE_SPLIT_THRESHOLD"] || 9_999_999) |
| 137 | + opts[:report_timeout] ||= Integer(ENV["RSPECQ_REPORT_TIMEOUT"] || DEFAULT_REPORT_TIMEOUT) |
| 138 | + opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEUES) |
| 139 | + opts[:redis_url] ||= ENV["RSPECQ_REDIS_URL"] |
| 140 | + opts[:fail_fast] ||= Integer(ENV["RSPECQ_FAIL_FAST"] || DEFAULT_FAIL_FAST) |
| 141 | + end |
| 142 | + |
| 143 | + def env_set?(var) |
| 144 | + ["1", "true"].include?(ENV[var]) |
| 145 | + end |
| 146 | + end |
| 147 | +end |
0 commit comments