From 0b3bb2dc06c38e2b08156f1d90138f0c42d50025 Mon Sep 17 00:00:00 2001 From: Matt Brictson Date: Tue, 2 Apr 2024 11:49:44 -0700 Subject: [PATCH] Allow use of interactive debugger when running tests in watch mode (#30) 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. --- lib/mighty_test/console.rb | 13 +++++-- lib/mighty_test/watcher.rb | 75 +++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/lib/mighty_test/console.rb b/lib/mighty_test/console.rb index 46d2497..7f98df9 100644 --- a/lib/mighty_test/console.rb +++ b/lib/mighty_test/console.rb @@ -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) @@ -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 diff --git a/lib/mighty_test/watcher.rb b/lib/mighty_test/watcher.rb index 0e54011..bbd0c8a 100644 --- a/lib/mighty_test/watcher.rb +++ b/lib/mighty_test/watcher.rb @@ -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 @@ -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