Skip to content

Commit

Permalink
Allow use of interactive debugger when running tests in watch mode (#30)
Browse files Browse the repository at this point in the history
Before, `mt --watch` would take full control of stdin to listen for keyboard
commands. This prevented the use of an interactive debugger when a test is 
being executed.

This PR changes the event loop implementation so that control of stdin is 
relinquished when running tests. This allows an interactive debugger to be
used while tests are running.

Specifically:

- Move stdin reader (i.e. `$stdin.getc`) out of a background thread and into
  the main event loop.
- `$stdin.getc` blocks until a character is typed.
- When the file system listener background thread detects a change, raise an 
  exception in the main event loop to break out of `$stdin.getc` and proceed
  to run the tests.
  • Loading branch information
mattbrictson committed Apr 2, 2024
1 parent cd9fab0 commit 0b3bb2d
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 44 deletions.
13 changes: 10 additions & 3 deletions lib/mighty_test/console.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ def clear
end

def wait_for_keypress
return stdin.getc unless stdin.respond_to?(:raw) && tty?

stdin.raw(intr: true) { stdin.getc }
with_raw_input do
sleep if stdin.eof?
stdin.getc
end
end

def play_sound(name, wait: false)
Expand Down Expand Up @@ -54,5 +55,11 @@ def play_sound(name, wait: false)
def tty?
$stdout.respond_to?(:tty?) && $stdout.tty?
end

def with_raw_input(&)
return yield unless stdin.respond_to?(:raw) && tty?

stdin.raw(intr: true, &)
end
end
end
75 changes: 34 additions & 41 deletions lib/mighty_test/watcher.rb
Original file line number Diff line number Diff line change
@@ -1,47 +1,58 @@
module MightyTest
class Watcher # rubocop:disable Metrics/ClassLength
class Watcher
class ListenerTriggered < StandardError
attr_reader :paths

def initialize(paths)
@paths = paths
super()
end
end

WATCHING_FOR_CHANGES = 'Watching for changes to source and test files. Press "h" for help or "q" to quit.'.freeze

def initialize(console: Console.new, extra_args: [], file_system: FileSystem.new, system_proc: method(:system))
@queue = Thread::Queue.new
@console = console
@extra_args = extra_args
@file_system = file_system
@system_proc = system_proc
end

def run(iterations: :indefinitely) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
start_file_system_listener
start_keypress_listener
puts WATCHING_FOR_CHANGES
def run(iterations: :indefinitely) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
started = false
@foreground_thread = Thread.current

loop_for(iterations) do
case await_next_event
in [:file_system_changed, [_, *] => paths]
run_matching_test_files(paths)
in [:keypress, "\r" | "\n"]
start_file_system_listener && puts(WATCHING_FOR_CHANGES) unless started
started = true

case console.wait_for_keypress
when "\r", "\n"
run_all_tests
in [:keypress, "a"]
when "a"
run_all_tests(flags: ["--all"])
in [:keypress, "d"]
when "d"
run_matching_test_files_from_git_diff
in [:keypress, "h"]
when "h"
show_help
in [:keypress, "q"]
when "q"
file_system_listener.stop
break
else
nil
end
rescue ListenerTriggered => e
run_matching_test_files(e.paths)
file_system_listener.start if file_system_listener.paused?
rescue Interrupt
file_system_listener&.stop
raise
end
ensure
puts "\nExiting."
file_system_listener&.stop
keypress_listener&.kill
end

private

attr_reader :console, :extra_args, :file_system, :file_system_listener, :keypress_listener, :system_proc
attr_reader :console, :extra_args, :file_system, :file_system_listener, :system_proc, :foreground_thread

def show_help
console.clear
Expand Down Expand Up @@ -105,36 +116,18 @@ def start_file_system_listener
file_system_listener.stop if file_system_listener && !file_system_listener.stopped?

@file_system_listener = file_system.listen do |modified, added, _removed|
paths = [*modified, *added].uniq
next if paths.empty?

# Pause listener so that subsequent changes are queued up while we are running the tests
file_system_listener.pause unless file_system_listener.stopped?
post_event(:file_system_changed, [*modified, *added].uniq)
foreground_thread.raise ListenerTriggered.new(paths)
end
end
alias restart_file_system_listener start_file_system_listener

def start_keypress_listener
@keypress_listener = Thread.new do
loop do
key = console.wait_for_keypress
break if key.nil?

post_event(:keypress, key)
end
end
@keypress_listener.abort_on_exception = true
end

def loop_for(iterations, &)
iterations == :indefinitely ? loop(&) : iterations.times(&)
end

def await_next_event
file_system_listener.start if file_system_listener.paused?
@queue.pop
end

def post_event(*event)
@queue << event
end
end
end

0 comments on commit 0b3bb2d

Please sign in to comment.