github.com/nycdavid/zeus@v0.0.0-20201208104106-9ba439429e03/rubygem/lib/zeus/m.rb (about) 1 # This is very largely based on @qrush's M, but there are many modifications. 2 3 # we need to load all dependencies up front, because bundler will 4 # remove us from the load path soon. 5 require "rubygems" 6 require "zeus/m/test_collection" 7 require "zeus/m/test_method" 8 9 # the Gemfile may specify a version of method_source, but we also want to require it here. 10 # To avoid possible "you've activated X; gemfile specifies Y" errors, we actually scan 11 # Gemfile.lock for a specific version, and require exactly that version if present. 12 gemfile_lock = ROOT_PATH + "/Gemfile.lock" 13 if File.exists?(gemfile_lock) 14 version = File.read(ROOT_PATH + "/Gemfile.lock"). 15 scan(/\bmethod_source\s*\(([\d\.]+)\)/).flatten[0] 16 17 gem "method_source", version if version 18 end 19 20 require 'method_source' 21 22 module Zeus 23 #`m` stands for metal, which is a better test/unit test runner that can run 24 #tests by line number. 25 # 26 #[![m ci](https://secure.travis-ci.org/qrush/m.png)](http://travis-ci.org/qrush/m) 27 # 28 #![Rush is a heavy metal band. Look it up on Wikipedia.](https://raw.github.com/qrush/m/master/rush.jpg) 29 # 30 #<sub>[Rush at the Bristol Colston Hall May 1979](http://www.flickr.com/photos/8507625@N02/3468299995/)</sub> 31 ### Install 32 # 33 ### Usage 34 # 35 #Basically, I was sick of using the `-n` flag to grab one test to run. Instead, I 36 #prefer how RSpec's test runner allows tests to be run by line number. 37 # 38 #Given this file: 39 # 40 # $ cat -n test/example_test.rb 41 # 1 require 'test/unit' 42 # 2 43 # 3 class ExampleTest < Test::Unit::TestCase 44 # 4 def test_apple 45 # 5 assert_equal 1, 1 46 # 6 end 47 # 7 48 # 8 def test_banana 49 # 9 assert_equal 1, 1 50 # 10 end 51 # 11 end 52 # 53 #You can run a test by line number, using format `m TEST_FILE:LINE_NUMBER_OF_TEST`: 54 # 55 # $ m test/example_test.rb:4 56 # Run options: -n /test_apple/ 57 # 58 # # Running tests: 59 # 60 # . 61 # 62 # Finished tests in 0.000525s, 1904.7619 tests/s, 1904.7619 assertions/s. 63 # 64 # 1 tests, 1 assertions, 0 failures, 0 errors, 0 skips 65 # 66 #Hit the wrong line number? No problem, `m` helps you out: 67 # 68 # $ m test/example_test.rb:2 69 # No tests found on line 2. Valid tests to run: 70 # 71 # test_apple: m test/examples/test_unit_example_test.rb:4 72 # test_banana: m test/examples/test_unit_example_test.rb:8 73 # 74 #Want to run the whole test? Just leave off the line number. 75 # 76 # $ m test/example_test.rb 77 # Run options: 78 # 79 # # Running tests: 80 # 81 # .. 82 # 83 # Finished tests in 0.001293s, 1546.7904 tests/s, 3093.5808 assertions/s. 84 # 85 # 1 tests, 2 assertions, 0 failures, 0 errors, 0 skips 86 # 87 #### Supports 88 # 89 #`m` works with a few Ruby test frameworks: 90 # 91 #* `Test::Unit` 92 #* `ActiveSupport::TestCase` 93 #* `MiniTest::Unit::TestCase` 94 # 95 ### License 96 # 97 #This gem is MIT licensed, please see `LICENSE` for more information. 98 99 ### M, your metal test runner 100 # Maybe this gem should have a longer name? Metal? 101 module M 102 M::VERSION = "1.2.1" unless defined?(M::VERSION) 103 104 # Accept arguments coming from bin/m and run tests. 105 def self.run(argv) 106 Runner.new(argv).run 107 end 108 109 ### Runner is in charge of running your tests. 110 # Instead of slamming all of this junk in an `M` class, it's here instead. 111 class Runner 112 def initialize(argv) 113 @argv = argv 114 end 115 116 # There's two steps to running our tests: 117 # 1. Parsing the given input for the tests we need to find (or groups of tests) 118 # 2. Run those tests we found that match what you wanted 119 def run 120 parse 121 execute 122 end 123 124 private 125 126 def parse 127 # With no arguments, 128 if @argv.empty? 129 @files = [] 130 add_file("test") 131 else 132 parse_options! @argv 133 134 # Parse out ARGV, it should be coming in in a format like `test/test_file.rb:9` 135 _, line = @argv.first.split(':') 136 @line ||= line.nil? ? nil : line.to_i 137 138 @files = [] 139 @argv.each do |arg| 140 add_file(arg) 141 end 142 end 143 end 144 145 def add_file(arg) 146 file = arg.split(':').first 147 if Dir.exist?(file) 148 files = Dir.glob("#{file}/**/*test*.rb") 149 @files.concat(files) 150 else 151 files = Dir.glob(file) 152 files == [] and abort "Couldn't find test file '#{file}'!" 153 @files.concat(files) 154 end 155 end 156 157 def parse_options!(argv) 158 require 'optparse' 159 160 OptionParser.new do |opts| 161 opts.banner = 'Options:' 162 opts.version = M::VERSION 163 164 opts.on '-h', '--help', 'Display this help.' do 165 puts "Usage: m [OPTIONS] [FILES]\n\n", opts 166 exit 167 end 168 169 opts.on '--version', 'Display the version.' do 170 puts "m #{M::VERSION}" 171 exit 172 end 173 174 opts.on '-l', '--line LINE', Integer, 'Line number for file.' do |line| 175 @line = line 176 end 177 178 opts.on '-n', '--name NAME', String, 'Name or pattern for test methods to run.' do |name| 179 if name[0] == "/" && name[-1] == "/" 180 @test_name = Regexp.new(name[1..-2]) 181 else 182 @test_name = name 183 end 184 end 185 186 opts.parse! argv 187 end 188 end 189 190 def execute 191 generate_tests_to_run 192 193 test_arguments = build_test_arguments 194 195 # directly run the tests from here and exit with the status of the tests passing or failing 196 case framework 197 when :minitest5, :minitest_old 198 ARGV.replace(test_arguments) 199 exit 200 when :testunit1, :testunit2 201 exit Test::Unit::AutoRunner.run(false, nil, test_arguments) 202 else 203 not_supported 204 end 205 end 206 207 def generate_tests_to_run 208 # Locate tests to run that may be inside of this line. There could be more than one! 209 all_tests = tests 210 if @line 211 @tests_to_run = all_tests.within(@line) 212 end 213 end 214 215 def build_test_arguments 216 if @line 217 abort_with_no_test_found_by_line_number if @tests_to_run.empty? 218 219 # assemble the regexp to run these tests, 220 test_names = @tests_to_run.map(&:escaped_name).join('|') 221 222 # set up the args needed for the runner 223 ["-n", "/(#{test_names})/"] 224 elsif user_specified_name? 225 abort_with_no_test_found_by_name unless tests.contains?(@test_name) 226 227 ["-n", test_name_to_s] 228 else 229 [] 230 end 231 end 232 233 def abort_with_no_test_found_by_line_number 234 abort_with_valid_tests_msg "No tests found on line #{@line}. " 235 end 236 237 def abort_with_no_test_found_by_name 238 abort_with_valid_tests_msg "No test name matches '#{test_name_to_s}'. " 239 end 240 241 def abort_with_valid_tests_msg message="" 242 message << "Valid tests to run:\n\n" 243 # For every test ordered by line number, 244 # spit out the test name and line number where it starts, 245 tests.by_line_number do |test| 246 message << "#{sprintf("%0#{tests.column_size}s", test.escaped_name)}: zeus test #{@files[0]}:#{test.start_line}\n" 247 end 248 249 # fail like a good unix process should. 250 abort message 251 end 252 253 def test_name_to_s 254 @test_name.is_a?(Regexp)? "/#{@test_name.source}/" : @test_name 255 end 256 257 def user_specified_name? 258 !@test_name.nil? 259 end 260 261 def framework 262 @framework ||= begin 263 if defined?(Minitest::Runnable) 264 :minitest5 265 elsif defined?(MiniTest) 266 :minitest_old 267 elsif defined?(Test) 268 if Test::Unit::TestCase.respond_to?(:test_suites) 269 :testunit2 270 else 271 :testunit1 272 end 273 end 274 end 275 end 276 277 # Finds all test suites in this test file, with test methods included. 278 def suites 279 # Since we're not using `ruby -Itest -Ilib` to run the tests, we need to add this directory to the `LOAD_PATH` 280 $:.unshift "./test", "./lib" 281 282 if framework == :testunit1 283 Test::Unit::TestCase.class_eval { 284 @@test_suites = {} 285 def self.inherited(klass) 286 @@test_suites[klass] = true 287 end 288 def self.test_suites 289 @@test_suites.keys 290 end 291 def self.test_methods 292 public_instance_methods(true).grep(/^test/).map(&:to_s) 293 end 294 } 295 end 296 297 begin 298 # Fire up the Ruby files. Let's hope they actually have tests. 299 @files.each { |f| load f } 300 rescue LoadError => e 301 # Fail with a happier error message instead of spitting out a backtrace from this gem 302 abort "Failed loading test file:\n#{e.message}" 303 end 304 305 # Figure out what test framework we're using 306 case framework 307 when :minitest5 308 suites = Minitest::Runnable.runnables 309 when :minitest_old 310 suites = MiniTest::Unit::TestCase.test_suites 311 when :testunit1, :testunit2 312 suites = Test::Unit::TestCase.test_suites 313 else 314 not_supported 315 end 316 317 # Use some janky internal APIs to group test methods by test suite. 318 suites.inject({}) do |suites, suite_class| 319 # End up with a hash of suite class name to an array of test methods, so we can later find them and ignore empty test suites 320 test_methods = case framework 321 when :minitest5 322 suite_class.runnable_methods 323 else 324 suite_class.test_methods 325 end 326 suites[suite_class] = test_methods if test_methods.size > 0 327 suites 328 end 329 end 330 331 # Shoves tests together in our custom container and collection classes. 332 # Memoize it since it's unnecessary to do this more than one for a given file. 333 def tests 334 @tests ||= begin 335 # With each suite and array of tests, 336 # and with each test method present in this test file, 337 # shove a new test method into this collection. 338 suites.inject(TestCollection.new) do |collection, (suite_class, test_methods)| 339 test_methods.each do |test_method| 340 find_locations = (@files.size == 1 && @line) 341 collection << TestMethod.create(suite_class, test_method, find_locations) 342 end 343 collection 344 end 345 end 346 end 347 348 # Fail loudly if this isn't supported 349 def not_supported 350 abort "This test framework is not supported! Please open up an issue at https://github.com/qrush/m !" 351 end 352 end 353 end 354 end