diff --git a/lib/mighty_test.rb b/lib/mighty_test.rb index e1d62d7..68e995d 100644 --- a/lib/mighty_test.rb +++ b/lib/mighty_test.rb @@ -1,6 +1,7 @@ module MightyTest autoload :VERSION, "mighty_test/version" autoload :CLI, "mighty_test/cli" + autoload :Console, "mighty_test/console" autoload :FileSystem, "mighty_test/file_system" autoload :MinitestRunner, "mighty_test/minitest_runner" autoload :OptionParser, "mighty_test/option_parser" diff --git a/lib/mighty_test/console.rb b/lib/mighty_test/console.rb new file mode 100644 index 0000000..c507f51 --- /dev/null +++ b/lib/mighty_test/console.rb @@ -0,0 +1,51 @@ +require "io/console" + +module MightyTest + class Console + def initialize(sound_player: "/usr/bin/afplay", sound_paths: SOUNDS) + @sound_player = sound_player + @sound_paths = sound_paths + end + + def clear + return false unless tty? + + $stdout.clear_screen + true + end + + def play_sound(name, wait: false) + return false unless tty? + + paths = sound_paths.fetch(name) { raise ArgumentError, "Unknown sound name #{name}" } + path = paths.find { |p| File.exist?(p) } + return false unless path && File.executable?(sound_player) + + thread = Thread.new { system(sound_player, path) } + thread.join if wait + true + end + + private + + # rubocop:disable Layout/LineLength + SOUNDS = { + pass: %w[ + /System/Library/PrivateFrameworks/ToneLibrary.framework/Versions/A/Resources/AlertTones/EncoreInfinitum/Milestone-EncoreInfinitum.caf + /System/Library/Sounds/Glass.aiff + ], + fail: %w[ + /System/Library/PrivateFrameworks/ToneLibrary.framework/Versions/A/Resources/AlertTones/EncoreInfinitum/Rebound-EncoreInfinitum.caf + /System/Library/Sounds/Bottle.aiff + ] + }.freeze + private_constant :SOUNDS + # rubocop:enable Layout/LineLength + + attr_reader :sound_player, :sound_paths + + def tty? + $stdout.respond_to?(:tty?) && $stdout.tty? + end + end +end diff --git a/lib/mighty_test/watcher.rb b/lib/mighty_test/watcher.rb index 16c4827..22b825a 100644 --- a/lib/mighty_test/watcher.rb +++ b/lib/mighty_test/watcher.rb @@ -4,8 +4,9 @@ module MightyTest 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)) + def initialize(console: Console.new, extra_args: [], file_system: FileSystem.new, system_proc: method(:system)) @event = Concurrent::MVar.new + @console = console @extra_args = extra_args @file_system = file_system @system_proc = system_proc @@ -19,19 +20,23 @@ def run(iterations: :indefinitely) case await_next_event in [:file_system_changed, paths] mt(*paths) if paths.any? - in [:tests_completed, :pass | :fail] - puts WATCHING_FOR_CHANGES + in [:tests_completed, status] + console.play_sound(status) + puts "\n#{WATCHING_FOR_CHANGES}" end end ensure + puts "\nExiting." listener&.stop end private - attr_reader :extra_args, :file_system, :listener, :system_proc + attr_reader :console, :extra_args, :file_system, :listener, :system_proc def mt(*test_paths) + console.clear + puts [*test_paths.join("\n"), ""] if test_paths.any? success = system_proc.call("mt", *extra_args, "--", *test_paths.flatten) post_event(:tests_completed, success ? :pass : :fail) rescue Interrupt diff --git a/test/mighty_test/console_test.rb b/test/mighty_test/console_test.rb new file mode 100644 index 0000000..db2ff0f --- /dev/null +++ b/test/mighty_test/console_test.rb @@ -0,0 +1,67 @@ +require "test_helper" + +module MightyTest + class ConsoleTest < Minitest::Test + def test_clear_returns_false_if_not_tty + result = nil + capture_io { result = Console.new.clear } + refute result + end + + def test_clear_clears_the_screen_and_returns_true_and_if_tty + result = nil + stdout, = capture_io do + $stdout.define_singleton_method(:tty?) { true } + $stdout.define_singleton_method(:clear_screen) { print "clear!" } + result = Console.new.clear + end + + assert result + assert_equal "clear!", stdout + end + + def test_play_sound_returns_false_if_not_tty + result = nil + capture_io { result = Console.new.play_sound(:pass) } + refute result + end + + def test_play_sound_returns_false_if_player_is_not_executable + result = nil + capture_io do + $stdout.define_singleton_method(:tty?) { true } + console = Console.new(sound_player: "/path/to/nothing") + result = console.play_sound(:pass) + end + refute result + end + + def test_play_sound_returns_false_if_sound_files_are_missing + result = nil + capture_io do + $stdout.define_singleton_method(:tty?) { true } + console = Console.new(sound_player: "/bin/echo", sound_paths: { pass: ["/path/to/nothing"] }) + result = console.play_sound(:pass) + end + refute result + end + + def test_play_sound_raises_argument_error_if_invalid_sound_name_is_specified + capture_io do + $stdout.define_singleton_method(:tty?) { true } + assert_raises(ArgumentError) { Console.new.play_sound(:whatever) } + end + end + + def test_play_sound_calls_sound_player_with_matching_sound_path + result = nil + stdout, = capture_subprocess_io do + $stdout.define_singleton_method(:tty?) { true } + console = Console.new(sound_player: "/bin/echo", sound_paths: { pass: [__FILE__] }) + result = console.play_sound(:pass, wait: true) + end + assert result + assert_equal __FILE__, stdout.chomp + end + end +end diff --git a/test/mighty_test/watcher_test.rb b/test/mighty_test/watcher_test.rb index bf4c41c..b6a9c45 100644 --- a/test/mighty_test/watcher_test.rb +++ b/test/mighty_test/watcher_test.rb @@ -37,7 +37,23 @@ def test_watcher_passes_extra_args_through_to_mt_command assert_includes(stdout, "[SYSTEM] mt --fail-fast -- test/example_test.rb\n") end - def test_watcher_prints_a_status_message_after_successful_test_run + def test_watcher_clears_the_screen_and_prints_the_test_file_being_run_prior_to_executing_the_mt_command + system_proc { |*args| puts "[SYSTEM] #{args.join(' ')}" } + listen_thread do |callback| + callback.call(["test/example_test.rb"], [], []) + end + + stdout, = run_watcher(iterations: 1, in: fixtures_path.join("example_project")) + + assert_includes(stdout, <<~EXPECTED) + [CLEAR] + test/example_test.rb + + [SYSTEM] mt -- test/example_test.rb + EXPECTED + end + + def test_watcher_prints_a_status_message_and_plays_a_sound_after_successful_test_run system_proc do |*args| puts "[SYSTEM] #{args.join(' ')}" true @@ -50,11 +66,13 @@ def test_watcher_prints_a_status_message_after_successful_test_run assert_includes(stdout, <<~EXPECTED) [SYSTEM] mt -- test/example_test.rb + [SOUND] :pass + Watching for changes to source and test files. Press ctrl-c to exit. EXPECTED end - def test_watcher_prints_a_status_message_after_failed_test_run + def test_watcher_prints_a_status_message_and_plays_a_sound_after_failed_test_run system_proc do |*args| puts "[SYSTEM] #{args.join(' ')}" false @@ -67,6 +85,8 @@ def test_watcher_prints_a_status_message_after_failed_test_run assert_includes(stdout, <<~EXPECTED) [SYSTEM] mt -- test/example_test.rb + [SOUND] :fail + Watching for changes to source and test files. Press ctrl-c to exit. EXPECTED end @@ -112,11 +132,14 @@ def paused? def run_watcher(iterations:, in: ".", extra_args: []) listen_thread = @listen_thread + console = Console.new + console.define_singleton_method(:clear) { puts "[CLEAR]" } + console.define_singleton_method(:play_sound) { |sound| puts "[SOUND] #{sound.inspect}" } file_system = FileSystem.new 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 = Watcher.new(console:, extra_args:, file_system:, system_proc: @system_proc) @watcher.run(iterations:) end end