src/derails/passive_resistance/test_case.rb
# frozen_string_literal: true

require "minitest"
require "passive_resistance/testing/tagged_logging"
require "passive_resistance/testing/setup_and_teardown"
require "passive_resistance/testing/tests_without_assertions"
require "passive_resistance/testing/assertions"
require "passive_resistance/testing/error_reporter_assertions"
require "passive_resistance/testing/event_reporter_assertions"
require "passive_resistance/testing/deprecation"
require "passive_resistance/testing/declarative"
require "passive_resistance/testing/isolation"
require "passive_resistance/testing/constant_lookup"
require "passive_resistance/testing/time_helpers"
require "passive_resistance/testing/constant_stubbing"
require "passive_resistance/testing/file_fixtures"
require "passive_resistance/testing/parallelization"
require "passive_resistance/testing/parallelize_executor"
require "passive_resistance/testing/notification_assertions"
require "concurrent/utility/processor_counter"

module PassiveResistance
  class TestCase < ::Minitest::Test
    Assertion = Minitest::Assertion

    # Class variable to store the parallel worker ID
    @@parallel_worker_id = nil

    class << self
      # Returns the current parallel worker ID if tests are running in parallel,
      # nil otherwise.
      #
      #   PassiveResistance::TestCase.parallel_worker_id # => 2
      def parallel_worker_id
        @@parallel_worker_id
      end

      def parallel_worker_id=(value) # :nodoc:
        @@parallel_worker_id = value
      end

      # Sets the order in which test cases are run.
      #
      #   PassiveResistance::TestCase.test_order = :random # => :random
      #
      # Valid values are:
      # * +:random+   (to run tests in random order)
      # * +:parallel+ (to run tests in parallel)
      # * +:sorted+   (to run tests alphabetically by method name)
      # * +:alpha+    (equivalent to +:sorted+)
      def test_order=(new_order)
        PassiveResistance.test_order = new_order
      end

      # Returns the order in which test cases are run.
      #
      #   PassiveResistance::TestCase.test_order # => :random
      #
      # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
      # Defaults to +:random+.
      def test_order
        PassiveResistance.test_order ||= :random
      end

      # Parallelizes the test suite.
      #
      # Takes a +workers+ argument that controls how many times the process
      # is forked. For each process a new database will be created suffixed
      # with the worker number.
      #
      #   test-database_0
      #   test-database_1
      #
      # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
      # and the environment variable will be used instead. This is useful for CI
      # environments, or other environments where you may need more workers than
      # you do for local testing.
      #
      # If the number of workers is set to +1+ or fewer, the tests will not be
      # parallelized.
      #
      # If +workers+ is set to +:number_of_processors+, the number of workers will be
      # set to the actual core count on the machine you are on.
      #
      # The default parallelization method is to fork processes. If you'd like to
      # use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+
      # method. Note the threaded parallelization does not create multiple
      # databases and will not work with system tests.
      #
      #   parallelize(workers: :number_of_processors, with: :threads)
      #
      # The threaded parallelization uses minitest's parallel executor directly.
      # The processes parallelization uses a Ruby DRb server.
      #
      # Because parallelization presents an overhead, it is only enabled when the
      # number of tests to run is above the +threshold+ param. The default value is
      # 50, and it's configurable via +config.passive_resistance.test_parallelization_threshold+.
      #
      # If you want to skip Rails default creation of one database per process in favor of
      # writing your own implementation, you can set +parallelize_databases+, or configure it
      # via +config.passive_resistance.parallelize_test_databases+.
      #
      #   parallelize(workers: :number_of_processors, parallelize_databases: false)
      #
      # Note that your test suite may deadlock if you attempt to use only one database
      # with multiple processes.
      def parallelize(workers: :number_of_processors, with: :processes, threshold: PassiveResistance.test_parallelization_threshold, parallelize_databases: PassiveResistance.parallelize_test_databases)
        case
        when ENV["PARALLEL_WORKERS"]
          workers = ENV["PARALLEL_WORKERS"].to_i
        when workers == :number_of_processors
          workers = (Concurrent.available_processor_count || Concurrent.processor_count).floor
        end

        if with == :processes
          PassiveResistance.parallelize_test_databases = parallelize_databases
        end

        Minitest.parallel_executor = PassiveResistance::Testing::ParallelizeExecutor.new(size: workers, with: with, threshold: threshold)
      end

      # Before fork hook for parallel testing. This can be used to run anything
      # before the processes are forked.
      #
      # In your +test_helper.rb+ add the following:
      #
      #   class PassiveResistance::TestCase
      #     parallelize_before_fork do
      #       # run this before fork
      #     end
      #   end
      def parallelize_before_fork(&block)
        PassiveResistance::Testing::Parallelization.before_fork_hook(&block)
      end

      # Setup hook for parallel testing. This can be used if you have multiple
      # databases or any behavior that needs to be run after the process is forked
      # but before the tests run.
      #
      # Note: this feature is not available with the threaded parallelization.
      #
      # In your +test_helper.rb+ add the following:
      #
      #   class PassiveResistance::TestCase
      #     parallelize_setup do
      #       # create databases
      #     end
      #   end
      def parallelize_setup(&block)
        PassiveResistance::Testing::Parallelization.after_fork_hook(&block)
      end

      # Clean up hook for parallel testing. This can be used to drop databases
      # if your app uses multiple write/read databases or other clean up before
      # the tests finish. This runs before the forked process is closed.
      #
      # Note: this feature is not available with the threaded parallelization.
      #
      # In your +test_helper.rb+ add the following:
      #
      #   class PassiveResistance::TestCase
      #     parallelize_teardown do
      #       # drop databases
      #     end
      #   end
      def parallelize_teardown(&block)
        PassiveResistance::Testing::Parallelization.run_cleanup_hook(&block)
      end

      # :singleton-method: fixture_paths
      #
      # Returns the ActiveRecord::FixtureSet collection.
      #
      # In your +test_helper.rb+ you must have <tt>require "rails/test_help"</tt>.

      # :singleton-method: fixture_paths=
      #
      # :call-seq:
      #   fixture_paths=(fixture_paths)
      #
      # Sets the given path to the fixture set.
      #
      # Can also append multiple paths.
      #
      #   PassiveResistance::TestCase.fixture_paths << "component1/test/fixtures"
      #
      # In your +test_helper.rb+ you must have <tt>require "rails/test_help"</tt>.
    end

    alias_method :method_name, :name

    # Returns the current parallel worker ID if tests are running in parallel
    def parallel_worker_id
      self.class.parallel_worker_id
    end

    include PassiveResistance::Testing::TaggedLogging
    prepend PassiveResistance::Testing::SetupAndTeardown
    prepend PassiveResistance::Testing::TestsWithoutAssertions
    include PassiveResistance::Testing::Assertions
    include PassiveResistance::Testing::ErrorReporterAssertions
    include PassiveResistance::Testing::EventReporterAssertions
    include PassiveResistance::Testing::NotificationAssertions
    include PassiveResistance::Testing::Deprecation
    include PassiveResistance::Testing::ConstantStubbing
    include PassiveResistance::Testing::TimeHelpers
    include PassiveResistance::Testing::FileFixtures
    extend PassiveResistance::Testing::Declarative

    ##
    # :method: assert_not_empty
    #
    # :call-seq:
    #   assert_not_empty(obj, msg = nil)
    #
    # Alias for: refute_empty[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_empty]

    #
    alias :assert_not_empty :refute_empty

    ##
    # :method: assert_not_equal
    #
    # :call-seq:
    #   assert_not_equal(exp, act, msg = nil)
    #
    # Alias for: refute_equal[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_equal]

    #
    alias :assert_not_equal :refute_equal

    ##
    # :method: assert_not_in_delta
    #
    # :call-seq:
    #   assert_not_in_delta(exp, act, delta = 0.001, msg = nil)
    #
    # Alias for: refute_in_delta[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_in_delta]

    #
    alias :assert_not_in_delta :refute_in_delta

    ##
    # :method: assert_not_in_epsilon
    #
    # :call-seq:
    #   assert_not_in_epsilon(a, b, epsilon = 0.001, msg = nil)
    #
    # Alias for: refute_in_epsilon[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_in_epsilon]

    #
    alias :assert_not_in_epsilon :refute_in_epsilon

    ##
    # :method: assert_not_includes
    #
    # :call-seq:
    #   assert_not_includes(collection, obj, msg = nil)
    #
    # Alias for: refute_includes[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_includes]

    #
    alias :assert_not_includes :refute_includes

    ##
    # :method: assert_not_instance_of
    #
    # :call-seq:
    #   assert_not_instance_of(cls, obj, msg = nil)
    #
    # Alias for: refute_instance_of[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_instance_of]

    #
    alias :assert_not_instance_of :refute_instance_of

    ##
    # :method: assert_not_kind_of
    #
    # :call-seq:
    #   assert_not_kind_of(cls, obj, msg = nil)
    #
    # Alias for: refute_kind_of[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_kind_of]

    #
    alias :assert_not_kind_of :refute_kind_of

    ##
    # :method: assert_no_match
    #
    # :call-seq:
    #   assert_no_match(matcher, obj, msg = nil)
    #
    # Alias for: refute_match[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_match]

    #
    alias :assert_no_match :refute_match

    ##
    # :method: assert_not_nil
    #
    # :call-seq:
    #   assert_not_nil(obj, msg = nil)
    #
    # Alias for: refute_nil[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_nil]

    #
    alias :assert_not_nil :refute_nil

    ##
    # :method: assert_not_operator
    #
    # :call-seq:
    #   assert_not_operator(o1, op, o2 = UNDEFINED, msg = nil)
    #
    # Alias for: refute_operator[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_operator]

    #
    alias :assert_not_operator :refute_operator

    ##
    # :method: assert_not_predicate
    #
    # :call-seq:
    #   assert_not_predicate(o1, op, msg = nil)
    #
    # Alias for: refute_predicate[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_predicate]

    #
    alias :assert_not_predicate :refute_predicate

    ##
    # :method: assert_not_respond_to
    #
    # :call-seq:
    #   assert_not_respond_to(obj, meth, msg = nil)
    #
    # Alias for: refute_respond_to[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_respond_to]

    #
    alias :assert_not_respond_to :refute_respond_to

    ##
    # :method: assert_not_same
    #
    # :call-seq:
    #   assert_not_same(exp, act, msg = nil)
    #
    # Alias for: refute_same[https://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-refute_same]

    #
    alias :assert_not_same :refute_same

    PassiveResistance.run_load_hooks(:passive_resistance_test_case, self)

    def inspect # :nodoc:
      Object.instance_method(:to_s).bind_call(self)
    end
  end
end
