require'parallel_tests'moduleParallelTestsmoduleTestclassRunnerNAME='Test'class<<self# --- usually overwritten by other runnersdefnameNAMEenddefruntime_log'tmp/parallel_runtime_test.log'enddeftest_suffix/_(test|spec).rb$/enddeftest_file_name"test"enddefrun_tests(test_files,process_number,num_processes,options)require_list=test_files.map{|file|file.sub(" ","\\ ")}.join(" ")cmd="#{executable} -Itest -e '%w[#{require_list}].each { |f| require %{./\#{f}} }' -- #{options[:test_options]}"execute_command(cmd,process_number,num_processes,options)enddefline_is_result?(line)line.gsub!(/[.F*]/,'')line=~/\d+ failure/end# --- usually used by other runners# finds all tests and partitions them into groupsdeftests_in_groups(tests,num_groups,options={})tests=find_tests(tests,options)caseoptions[:group_by]when:foundtests.map!{|t|[t,1]}when:filesizesort_by_filesize(tests)when:runtimesort_by_runtime(tests,runtimes(options),options.merge(allowed_missing: 0.5))whennil# use recorded test runtime if we got enough dataruntimes=runtimes(options)rescue[]ifruntimes.size*1.5>tests.sizeputs"Using recorded test runtime"sort_by_runtime(tests,runtimes)elsesort_by_filesize(tests)endelseraiseArgumentError,"Unsupported option #{options[:group_by]}"endGrouper.in_even_groups_by_size(tests,num_groups,options)enddefexecute_command(cmd,process_number,num_processes,options)env=(options[:env]||{}).merge("TEST_ENV_NUMBER"=>test_env_number(process_number),"PARALLEL_TEST_GROUPS"=>num_processes)cmd="nice #{cmd}"ifoptions[:nice]cmd="#{cmd} 2>&1"ifoptions[:combine_stderr]putscmdifoptions[:verbose]execute_command_and_capture_output(env,cmd,options[:serialize_stdout])enddefexecute_command_and_capture_output(env,cmd,silence)# make processes descriptive / visible in ps -efseparator=(WINDOWS?' & ':';')exports=env.mapdo|k,v|ifWINDOWS"(SET \"#{k}=#{v}\")"else"#{k}=#{v};export #{k}"endend.join(separator)cmd="#{exports}#{separator}#{cmd}"output=open("|#{cmd}","r"){|output|capture_output(output,silence)}exitstatus=$?.exitstatus{:stdout=>output,:exit_status=>exitstatus}enddeffind_results(test_output)test_output.split("\n").map{|line|line.gsub!(/\e\[\d+m/,'')nextunlessline_is_result?(line)line}.compactenddeftest_env_number(process_number)process_number==0?'':process_number+1enddefsummarize_results(results)sums=sum_up_results(results)sums.sort.map{|word,number|"#{number}#{word}#{'s'ifnumber!=1}"}.join(', ')endprotecteddefexecutableENV['PARALLEL_TESTS_EXECUTABLE']||determine_executableenddefdetermine_executable"ruby"enddefsum_up_results(results)results=results.join(' ').gsub(/s\b/,'')# combine and singularize resultscounts=results.scan(/(\d+) (\w+)/)counts.inject(Hash.new(0))do|sum,(number,word)|sum[word]+=number.to_isumendend# read output of the process and print it in chunksdefcapture_output(out,silence)result=""loopdobeginread=out.readpartial(1000000)# read whatever chunk we can getifEncoding.default_internalread=read.force_encoding(Encoding.default_internal)endresult<<readunlesssilence$stdout.printread$stdout.flushendendendrescueEOFErrorresultenddefsort_by_runtime(tests,runtimes,options={})allowed_missing=options[:allowed_missing]||1.0allowed_missing=tests.size*allowed_missing# build runtime hashtimes={}runtimes.eachdo|line|test,time=line.split(":",2)nextunlesstestandtimetimes[test]=time.to_fend# set know runtime for each testtests.sort!tests.map!do|test|allowed_missing-=1unlesstime=times[test]raise"Too little runtime info"ifallowed_missing<0[test,time]endifoptions[:verbose]puts"Runtime found for #{tests.count(&:last)} of #{tests.size} tests"end# fill gaps with average runtimeknown,unknown=tests.partition(&:last)average=known.map!(&:last).inject(:+)/known.sizeunknown.each{|set|set[1]=average}enddefruntimes(options)log=options[:runtime_log]||runtime_logFile.read(log).split("\n")enddefsort_by_filesize(tests)tests.sort!tests.map!{|test|[test,File.stat(test).size]}enddeffind_tests(tests,options={})(tests||[]).mapdo|file_or_folder|ifFile.directory?(file_or_folder)files=files_in_folder(file_or_folder,options)files.grep(test_suffix).grep(options[:pattern]||//)elsefile_or_folderendend.flatten.uniqenddeffiles_in_folder(folder,options={})pattern=ifoptions[:symlinks]==false# not nil or true"**/*"else# follow one symlink and direct children# http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob"**{,/*/**}/*"endDir[File.join(folder,pattern)].uniqendendendendend