#!/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