Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve console feedback and add sound effects to --watch mode #5

Merged
merged 2 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/mighty_test.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
51 changes: 51 additions & 0 deletions lib/mighty_test/console.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 9 additions & 4 deletions lib/mighty_test/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
67 changes: 67 additions & 0 deletions test/mighty_test/console_test.rb
Original file line number Diff line number Diff line change
@@ -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
29 changes: 26 additions & 3 deletions test/mighty_test/watcher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down