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