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)tests=ifoptions[:group_by]==:foundtests.map{|t|[t,1]}elsifoptions[:group_by]==:filesizewith_filesize_info(tests)elsewith_runtime_info(tests,options)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.flushendendendrescueEOFErrorresultenddefwith_runtime_info(tests,options={})log=options[:runtime_log]||runtime_loglines=File.read(log).split("\n")rescue[]# use recorded test runtime if we got enough dataiflines.size*1.5>tests.sizeputs"Using recorded test runtime: #{log}"times=Hash.new(1)lines.eachdo|line|test,time=line.split(":")nextunlesstestandtimetimes[test]=time.to_fendtests.sort.map{|test|[test,times[test]]}else# use file sizeswith_filesize_info(tests)endenddefwith_filesize_info(tests)# use filesize to group filestests.sort.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