Skip to content

Commit

Permalink
Rewrite using pattern matching
Browse files Browse the repository at this point in the history
  • Loading branch information
mattbrictson committed Feb 18, 2024
1 parent 64ebbc1 commit 08c1fac
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 67 deletions.
71 changes: 32 additions & 39 deletions lib/mighty_test/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,64 @@ class Watcher
WATCHING_FOR_CHANGES = "Watching for changes to source and test files. Press ctrl-c to exit.".freeze

def initialize(extra_args: [], file_system: FileSystem.new, system_proc: method(:system))
@dispatcher = Concurrent::MVar.new
@event = Concurrent::MVar.new
@extra_args = extra_args
@file_system = file_system
@system_proc = system_proc
end

def run
def run(iterations: :indefinitely)
start_listener
puts WATCHING_FOR_CHANGES
process_dispatched_events

loop = iterations == :indefinitely ? method(:loop) : iterations.method(:times)
loop.call do
case await_next_event
in [:file_system_changed, paths]
mt(*paths) if paths.any?
in [:tests_completed, :pass | :fail]
puts WATCHING_FOR_CHANGES
end
end
ensure
listener&.stop
end

# Normally `run` proceeds indefinitely until the user interrupts with ctrl-c.
# This is a way to stop it gracefully in unit tests.
def interrupt
dispatch { :stop }
end

private

attr_reader :extra_args, :file_system, :listener, :system_proc

def file_system_changed(modified, added, _removed)
test_paths = [*modified, *added].filter_map do |path|
file_system.find_matching_test_file(path)
end
return if test_paths.empty?

run_tests(*test_paths.uniq)
end

def run_tests(*test_paths)
begin
system_proc.call("mt", *extra_args, "--", *test_paths.flatten)
rescue Interrupt
# Pressing ctrl-c kills the fs_event background process, so we have to manually restart it.
restart_listener
end
puts WATCHING_FOR_CHANGES
def mt(*test_paths)
success = system_proc.call("mt", *extra_args, "--", *test_paths.flatten)
post_event(:tests_completed, success ? :pass : :fail)
rescue Interrupt
# Pressing ctrl-c kills the fs_event background process, so we have to manually restart it.
restart_listener
end

def start_listener
listener.stop if listener && !listener.stopped?
@listener = file_system.listen do |*args|
listener.pause
dispatch do
file_system_changed(*args)
listener.start if listener.paused?

@listener = file_system.listen do |modified, added, _removed|
# Pause listener so that subsequent changes are queued up while we are running the tests
listener.pause unless listener.stopped?

test_paths = [*modified, *added].filter_map do |path|
file_system.find_matching_test_file(path)
end

post_event(:file_system_changed, test_paths.uniq)
end
end
alias restart_listener start_listener

def dispatch(&block)
@dispatcher.put(block)
true
def await_next_event
listener.start if listener.paused?
@event.take
end

def process_dispatched_events
loop do
result = @dispatcher.take.call
break if result == :stop
end
def post_event(*event)
@event.put(event)
end
end
end
49 changes: 21 additions & 28 deletions test/mighty_test/watcher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,31 @@ def test_watcher_passes_unique_set_of_test_files_to_mt_command_based_on_changes_
system_proc { |*args| puts "[SYSTEM] #{args.join(' ')}" }
listen_thread do |callback|
callback.call(["lib/example.rb", "test/focused_test.rb"], ["test/focused_test.rb"], [])
@watcher.interrupt
end

stdout, = run_watcher(in: fixtures_path.join("example_project"))
stdout, = run_watcher(iterations: 1, in: fixtures_path.join("example_project"))

assert_includes(stdout, "[SYSTEM] mt -- test/example_test.rb test/focused_test.rb\n")
end

def test_watcher_does_nothing_if_a_detected_change_has_no_corresponding_test_file
system_proc { |*args| puts "[SYSTEM] #{args.join(' ')}" }
listen_thread do |callback|
callback.call(["lib/example/version.rb"], [], [])
end

stdout, = run_watcher(iterations: 1, in: fixtures_path.join("example_project"))

refute_includes(stdout, "[SYSTEM]")
end

def test_watcher_passes_extra_args_through_to_mt_command
system_proc { |*args| puts "[SYSTEM] #{args.join(' ')}" }
listen_thread do |callback|
callback.call(["test/example_test.rb"], [], [])
@watcher.interrupt
end

stdout, = run_watcher(extra_args: ["--fail-fast"], in: fixtures_path.join("example_project"))
stdout, = run_watcher(iterations: 1, extra_args: ["--fail-fast"], in: fixtures_path.join("example_project"))

assert_includes(stdout, "[SYSTEM] mt --fail-fast -- test/example_test.rb\n")
end
Expand All @@ -35,10 +44,9 @@ def test_watcher_prints_a_status_message_after_successful_test_run
end
listen_thread do |callback|
callback.call(["test/example_test.rb"], [], [])
@watcher.interrupt
end

stdout, = run_watcher(in: fixtures_path.join("example_project"))
stdout, = run_watcher(iterations: 2, in: fixtures_path.join("example_project"))

assert_includes(stdout, <<~EXPECTED)
[SYSTEM] mt -- test/example_test.rb
Expand All @@ -53,46 +61,31 @@ def test_watcher_prints_a_status_message_after_failed_test_run
end
listen_thread do |callback|
callback.call(["test/example_test.rb"], [], [])
@watcher.interrupt
end

stdout, = run_watcher(in: fixtures_path.join("example_project"))
stdout, = run_watcher(iterations: 2, in: fixtures_path.join("example_project"))

assert_includes(stdout, <<~EXPECTED)
[SYSTEM] mt -- test/example_test.rb
Watching for changes to source and test files. Press ctrl-c to exit.
EXPECTED
end

def test_watcher_does_nothing_if_a_detected_change_has_no_corresponding_test_file
system_proc { |*args| puts "[SYSTEM] #{args.join(' ')}" }
listen_thread do |callback|
callback.call(["lib/example/version.rb"], [], [])
@watcher.interrupt
end

stdout, = run_watcher(in: fixtures_path.join("example_project"))

refute_includes(stdout, "[SYSTEM]")
end

def test_watcher_restarts_the_listener_when_a_test_run_is_interrupted
thread_count = 0
system_proc { |*| raise Interrupt }
listen_thread do |callback|
thread_count += 1
callback.call(["test/example_test.rb"], [], [])
@watcher.interrupt if thread_count > 1
callback.call(["test/example_test.rb"], [], []) unless thread_count > 2
end

run_watcher(in: fixtures_path.join("example_project"))

run_watcher(iterations: 2, in: fixtures_path.join("example_project"))
assert_equal(2, thread_count)
end

private

class ListenThread
class Listener
def initialize(thread, callback)
Thread.new do
thread.call(callback)
Expand All @@ -117,14 +110,14 @@ def paused?
end
end

def run_watcher(in: ".", extra_args: [])
def run_watcher(iterations:, in: ".", extra_args: [])
listen_thread = @listen_thread
file_system = FileSystem.new
file_system.define_singleton_method(:listen) { |&callback| ListenThread.new(listen_thread, callback) }
file_system.define_singleton_method(:listen) { |&callback| Listener.new(listen_thread, callback) }
capture_io do
Dir.chdir(binding.local_variable_get(:in)) do
@watcher = Watcher.new(extra_args:, file_system:, system_proc: @system_proc)
@watcher.run
@watcher.run(iterations:)
end
end
end
Expand Down

0 comments on commit 08c1fac

Please sign in to comment.