Skip to content

Commit

Permalink
Add interactive watch command for running tests based on git status (#9)
Browse files Browse the repository at this point in the history
* Add interactive watch command for running tests based on git status

The `git ls-files` command allows us to get a list of all files that
have been added or changed since the last commit. This is a convenient
way to determine what tests to run.

This commit adds a "d" keypress command to the watch mode. When the "d"
key is pressed, we check git for new and changed files, then find the
corresponding test files, and run those.

* Refactor by extracting methods to simplify event loop
  • Loading branch information
mattbrictson authored Feb 22, 2024
1 parent 6d58ce3 commit 7648dc1
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 15 deletions.
11 changes: 11 additions & 0 deletions lib/mighty_test/file_system.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "open3"

module MightyTest
class FileSystem
def listen(&)
Expand All @@ -17,5 +19,14 @@ def find_test_paths(directory="test")
glob = File.join(directory, "**/*_test.rb")
Dir[glob]
end

def find_new_and_changed_paths
out, _err, status = Open3.capture3(*%w[git ls-files --deduplicate -m -o --exclude-standard test app lib])
return [] unless status.success?

out.lines(chomp: true).reject(&:empty?).uniq
rescue SystemCallError
[]
end
end
end
44 changes: 31 additions & 13 deletions lib/mighty_test/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,11 @@ def run(iterations: :indefinitely) # rubocop:disable Metrics/MethodLength
loop_for(iterations) do
case await_next_event
in [:file_system_changed, [_, *] => paths]
console.clear
puts paths.join("\n")
puts
mt(*paths)
run_matching_test_files(paths)
in [:keypress, "\r" | "\n"]
console.clear
puts "Running all tests...\n\n"
mt
run_all_tests
in [:keypress, "d"]
run_matching_test_files_from_git_diff
in [:keypress, "q"]
break
else
Expand All @@ -43,6 +40,32 @@ def run(iterations: :indefinitely) # rubocop:disable Metrics/MethodLength

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

def run_all_tests
console.clear
puts "Running all tests..."
puts
mt
end

def run_matching_test_files(paths)
test_paths = paths.flat_map { |path| file_system.find_matching_test_path(path) }.compact.uniq
return false if test_paths.empty?

console.clear
puts test_paths.join("\n")
puts
mt(*test_paths)
true
end

def run_matching_test_files_from_git_diff
return if run_matching_test_files(file_system.find_new_and_changed_paths)

console.clear
puts "No affected test files detected since the last git commit."
puts WATCHING_FOR_CHANGES
end

def mt(*test_paths)
command = ["mt", *extra_args]
command.append("--", *test_paths.flatten) if test_paths.any?
Expand All @@ -63,12 +86,7 @@ def start_file_system_listener
@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_path(path)
end

post_event(:file_system_changed, test_paths.uniq)
post_event(:file_system_changed, [*modified, *added].uniq)
end
end
alias restart_file_system_listener start_file_system_listener
Expand Down
42 changes: 42 additions & 0 deletions test/mighty_test/file_system_test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "test_helper"
require "open3"

module MightyTest
class FileSystemTest < Minitest::Test
Expand Down Expand Up @@ -71,6 +72,47 @@ def test_find_test_paths_returns_test_files_in_specific_directory
)
end

def test_find_new_and_changed_paths_returns_empty_array_if_git_exits_with_error
status = Minitest::Mock.new
status.expect(:success?, false)

paths = Open3.stub(:capture3, ["", "oh no!", status]) do
FileSystem.new.find_new_and_changed_paths
end

assert_empty paths
end

def test_find_new_and_changed_paths_returns_empty_array_if_system_call_fails
paths = Open3.stub(:capture3, ->(*) { raise SystemCallError, "oh no!" }) do
FileSystem.new.find_new_and_changed_paths
end

assert_empty paths
end

def test_find_new_and_changed_paths_returns_array_based_on_git_output
git_output = <<~OUT
lib/mighty_test/file_system.rb
test/mighty_test/file_system_test.rb
OUT

status = Minitest::Mock.new
status.expect(:success?, true)

paths = Open3.stub(:capture3, [git_output, "", status]) do
FileSystem.new.find_new_and_changed_paths
end

assert_equal(
%w[
lib/mighty_test/file_system.rb
test/mighty_test/file_system_test.rb
],
paths
)
end

private

def find_matching_test_path(path, in: ".")
Expand Down
40 changes: 38 additions & 2 deletions test/mighty_test/watcher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,43 @@ def test_watcher_runs_all_tests_when_enter_key_is_pressed
EXPECTED
end

def test_watcher_runs_new_and_changed_files_according_to_git_when_d_key_is_pressed
system_proc do |*args|
puts "[SYSTEM] #{args.join(' ')}"
true
end

file_system = FileSystem.new
stdout, = file_system.stub(:find_new_and_changed_paths, %w[lib/example.rb]) do
run_watcher(file_system:, stdin: "dq", in: fixtures_path.join("example_project"))
end

assert_includes(stdout, <<~EXPECTED)
[CLEAR]
test/example_test.rb
[SYSTEM] mt -- test/example_test.rb
EXPECTED
end

def test_watcher_shows_a_message_if_d_key_is_pressed_and_there_are_no_changes
system_proc do |*args|
puts "[SYSTEM] #{args.join(' ')}"
true
end

file_system = FileSystem.new
stdout, = file_system.stub(:find_new_and_changed_paths, []) do
run_watcher(file_system:, stdin: "dq", in: fixtures_path.join("example_project"))
end

assert_includes(stdout, <<~EXPECTED)
[CLEAR]
No affected test files detected since the last git commit.
Watching for changes to source and test files. Press "q" to quit.
EXPECTED
end

private

class Listener
Expand Down Expand Up @@ -151,12 +188,11 @@ def paused?
end
end

def run_watcher(iterations: :indefinitely, in: ".", extra_args: [], stdin: nil)
def run_watcher(iterations: :indefinitely, in: ".", extra_args: [], stdin: nil, file_system: FileSystem.new)
listen_thread = @listen_thread
console = Console.new(stdin: stdin.nil? ? File::NULL : StringIO.new(stdin))
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
Expand Down

0 comments on commit 7648dc1

Please sign in to comment.