summaryrefslogtreecommitdiff
path: root/spec/mspec/lib/mspec/commands/mspec.rb
blob: 6cb1e87a58a37f299e1c3190472e29ede62a8b55 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env ruby

require 'mspec/version'
require 'mspec/utils/options'
require 'mspec/utils/script'
require 'mspec/helpers/tmp'
require 'mspec/runner/actions/filter'
require 'mspec/runner/actions/timer'


class MSpecMain < MSpecScript
  def initialize
    super

    config[:loadpath] = []
    config[:requires] = []
    config[:target]   = ENV['RUBY'] || 'ruby'
    config[:flags]    = []
    config[:command]  = nil
    config[:options]  = []
    config[:launch]   = []
  end

  def options(argv=ARGV)
    config[:command] = argv.shift if ["ci", "run", "tag"].include?(argv[0])

    options = MSpecOptions.new "mspec [COMMAND] [options] (FILE|DIRECTORY|GLOB)+", 30, config

    options.doc " The mspec command sets up and invokes the sub-commands"
    options.doc " (see below) to enable, for instance, running the specs"
    options.doc " with different implementations like ruby, jruby, rbx, etc.\n"

    options.configure do |f|
      load f
      config[:options] << '-B' << f
    end

    options.targets

    options.on("--warnings", "Don't suppress warnings") do
      config[:flags] << '-w'
      ENV['OUTPUT_WARNINGS'] = '1'
    end

    options.on("-j", "--multi", "Run multiple (possibly parallel) subprocesses") do
      config[:multi] = true
    end

    options.version MSpec::VERSION do
      if config[:command]
        config[:options] << "-v"
      else
        puts "#{File.basename $0} #{MSpec::VERSION}"
        exit
      end
    end

    options.help do
      if config[:command]
        config[:options] << "-h"
      else
        puts options
        exit 1
      end
    end

    options.doc "\n Custom options"
    custom_options options

    # The rest of the help output
    options.doc "\n where COMMAND is one of:\n"
    options.doc "   run - Run the specified specs (default)"
    options.doc "   ci  - Run the known good specs"
    options.doc "   tag - Add or remove tags\n"
    options.doc " mspec COMMAND -h for more options\n"
    options.doc "   example: $ mspec run -h\n"

    options.on_extra { |o| config[:options] << o }
    options.parse(argv)

    if config[:multi]
      options = MSpecOptions.new "mspec", 30, config
      options.all
      patterns = options.parse(config[:options])
      @files = files_from_patterns(patterns)
    end
  end

  def register; end

  def multi_exec(argv)
    MSpec.register_files @files

    require 'mspec/runner/formatters/multi'
    formatter = MultiFormatter.new
    if config[:formatter]
      warn "formatter options is ignored due to multi option"
    end

    output_files = []
    processes = cores(@files.size)
    children = processes.times.map { |i|
      name = tmp "mspec-multi-#{i}"
      output_files << name

      env = {
        "SPEC_TEMP_DIR" => "rubyspec_temp_#{i}",
        "MSPEC_MULTI" => i.to_s
      }
      command = argv + ["-fy", "-o", name]
      $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
      IO.popen([env, *command, close_others: false], "rb+")
    }

    puts children.map { |child| child.gets }.uniq
    formatter.start
    last_files = {}

    until @files.empty?
      IO.select(children)[0].each { |io|
        reply = io.read(1)
        case reply
        when '.'
          formatter.unload
        when nil
          raise "Worker died!"
        else
          while chunk = (io.read_nonblock(4096) rescue nil)
            reply += chunk
          end
          reply.chomp!('.')
          msg = "A child mspec-run process printed unexpected output on STDOUT"
          if last_file = last_files[io]
            msg += " while running #{last_file}"
          end
          abort "\n#{msg}: #{reply.inspect}"
        end

        unless @files.empty?
          file = @files.shift
          last_files[io] = file
          io.puts file
        end
      }
    end

    success = true
    children.each { |child|
      child.puts "QUIT"
      _pid, status = Process.wait2(child.pid)
      success &&= status.success?
      child.close
    }

    formatter.aggregate_results(output_files)
    formatter.finish
    success
  end

  def run
    argv = config[:target].split(/\s+/)

    argv.concat config[:launch]
    argv.concat config[:flags]
    argv.concat config[:loadpath]
    argv.concat config[:requires]
    argv << "#{MSPEC_HOME}/bin/mspec-#{config[:command] || 'run'}"
    argv.concat config[:options]

    if config[:multi]
      exit multi_exec(argv)
    else
      $stderr.puts "$ #{argv.join(' ')}"
      $stderr.flush
      exec(*argv, close_others: false)
    end
  end
end