Skip to content

Commit

Permalink
Add interactive watch command for running tests based on git status
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mattbrictson committed Feb 22, 2024
1 parent 6d58ce3 commit 8ce978b
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 3 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
17 changes: 16 additions & 1 deletion lib/mighty_test/watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize(console: Console.new, extra_args: [], file_system: FileSystem.new
@system_proc = system_proc
end

def run(iterations: :indefinitely) # rubocop:disable Metrics/MethodLength
def run(iterations: :indefinitely) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
start_file_system_listener
start_keypress_listener
puts WATCHING_FOR_CHANGES
Expand All @@ -28,6 +28,16 @@ def run(iterations: :indefinitely) # rubocop:disable Metrics/MethodLength
console.clear
puts "Running all tests...\n\n"
mt
in [:keypress, "d"]
console.clear
if (paths = find_matching_tests_for_new_and_changed_paths).any?
puts paths.join("\n")
puts
mt(*paths)
else
puts "No affected test files detected since the last git commit."
puts WATCHING_FOR_CHANGES
end
in [:keypress, "q"]
break
else
Expand All @@ -43,6 +53,11 @@ def run(iterations: :indefinitely) # rubocop:disable Metrics/MethodLength

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

def find_matching_tests_for_new_and_changed_paths
new_changed = file_system.find_new_and_changed_paths
new_changed.flat_map { |path| file_system.find_matching_test_path(path) }.uniq
end

def mt(*test_paths)
command = ["mt", *extra_args]
command.append("--", *test_paths.flatten) if test_paths.any?
Expand Down
41 changes: 41 additions & 0 deletions test/mighty_test/file_system_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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 8ce978b

Please sign in to comment.