generated from mattbrictson/gem
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement a simple, non-interactive
--watch
mode (#4)
* Impl simple algorithm for finding test file for given path * WIP * Rewrite using pattern matching * Extract loop logic
- Loading branch information
1 parent
9421d21
commit 369b200
Showing
12 changed files
with
310 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
module MightyTest | ||
autoload :VERSION, "mighty_test/version" | ||
autoload :CLI, "mighty_test/cli" | ||
autoload :FileSystem, "mighty_test/file_system" | ||
autoload :MinitestRunner, "mighty_test/minitest_runner" | ||
autoload :OptionParser, "mighty_test/option_parser" | ||
autoload :TestParser, "mighty_test/test_parser" | ||
autoload :Watcher, "mighty_test/watcher" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module MightyTest | ||
class FileSystem | ||
def listen(&) | ||
require "listen" | ||
Listen.to(*%w[app lib test].select { |p| Dir.exist?(p) }, relative: true, &).tap(&:start) | ||
end | ||
|
||
def find_matching_test_file(path) | ||
return nil unless path && File.exist?(path) && !Dir.exist?(path) | ||
return path if path.match?(%r{^test/.*_test.rb$}) | ||
|
||
test_path = path[%r{^(?:app|lib)/(.+)\.[^\.]+$}, 1].then { "test/#{_1}_test.rb" } | ||
test_path if test_path && File.exist?(test_path) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
require "concurrent" | ||
|
||
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)) | ||
@event = Concurrent::MVar.new | ||
@extra_args = extra_args | ||
@file_system = file_system | ||
@system_proc = system_proc | ||
end | ||
|
||
def run(iterations: :indefinitely) | ||
start_listener | ||
puts WATCHING_FOR_CHANGES | ||
|
||
loop_for(iterations) do | ||
case await_next_event | ||
in [:file_system_changed, paths] | ||
mt(*paths) if paths.any? | ||
in [:tests_completed, :pass | :fail] | ||
puts WATCHING_FOR_CHANGES | ||
end | ||
end | ||
ensure | ||
listener&.stop | ||
end | ||
|
||
private | ||
|
||
attr_reader :extra_args, :file_system, :listener, :system_proc | ||
|
||
def mt(*test_paths) | ||
success = system_proc.call("mt", *extra_args, "--", *test_paths.flatten) | ||
post_event(:tests_completed, success ? :pass : :fail) | ||
rescue Interrupt | ||
# Pressing ctrl-c kills the fs_event background process, so we have to manually restart it. | ||
restart_listener | ||
end | ||
|
||
def start_listener | ||
listener.stop if listener && !listener.stopped? | ||
|
||
@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_file(path) | ||
end | ||
|
||
post_event(:file_system_changed, test_paths.uniq) | ||
end | ||
end | ||
alias restart_listener start_listener | ||
|
||
def loop_for(iterations, &) | ||
iterations == :indefinitely ? loop(&) : iterations.times(&) | ||
end | ||
|
||
def await_next_event | ||
listener.start if listener.paused? | ||
@event.take | ||
end | ||
|
||
def post_event(*event) | ||
@event.put(event) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
require "test_helper" | ||
|
||
module MightyTest | ||
class FileSystemTest < Minitest::Test | ||
include FixturesPath | ||
|
||
def test_find_matching_test_file_returns_nil_for_nil_path | ||
assert_nil find_matching_test_file(nil) | ||
end | ||
|
||
def test_find_matching_test_file_returns_nil_for_non_existent_path | ||
assert_nil find_matching_test_file("path/to/nowhere.rb") | ||
end | ||
|
||
def test_find_matching_test_file_returns_nil_for_directory_path | ||
assert_nil find_matching_test_file("lib/example", in: fixtures_path.join("example_project")) | ||
end | ||
|
||
def test_find_matching_test_file_returns_nil_for_path_with_no_corresponding_test | ||
assert_nil find_matching_test_file("lib/example/version.rb", in: fixtures_path.join("example_project")) | ||
end | ||
|
||
def test_find_matching_test_file_returns_nil_for_a_test_support_file | ||
assert_nil find_matching_test_file("test/test_helper.rb", in: fixtures_path.join("example_project")) | ||
end | ||
|
||
def test_find_matching_test_file_returns_argument_if_it_is_already_a_test | ||
test_path = find_matching_test_file("test/example_test.rb", in: fixtures_path.join("example_project")) | ||
assert_equal("test/example_test.rb", test_path) | ||
end | ||
|
||
def test_find_matching_test_file_returns_matching_test_given_an_implementation_path_in_a_gem_project | ||
test_path = find_matching_test_file("lib/example.rb", in: fixtures_path.join("example_project")) | ||
assert_equal("test/example_test.rb", test_path) | ||
end | ||
|
||
def test_find_matching_test_file_returns_matching_test_given_a_model_path_in_a_rails_project | ||
test_path = find_matching_test_file("app/models/user.rb", in: fixtures_path.join("rails_project")) | ||
assert_equal("test/models/user_test.rb", test_path) | ||
end | ||
|
||
private | ||
|
||
def find_matching_test_file(path, in: ".") | ||
Dir.chdir(binding.local_variable_get(:in)) do | ||
FileSystem.new.find_matching_test_file(path) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
require "test_helper" | ||
|
||
module MightyTest | ||
class WatcherTest < Minitest::Test | ||
include FixturesPath | ||
|
||
def test_watcher_passes_unique_set_of_test_files_to_mt_command_based_on_changes_detected | ||
system_proc { |*args| puts "[SYSTEM] #{args.join(' ')}" } | ||
listen_thread do |callback| | ||
callback.call(["lib/example.rb", "test/focused_test.rb"], ["test/focused_test.rb"], []) | ||
end | ||
|
||
stdout, = run_watcher(iterations: 1, in: fixtures_path.join("example_project")) | ||
|
||
assert_includes(stdout, "[SYSTEM] mt -- test/example_test.rb test/focused_test.rb\n") | ||
end | ||
|
||
def test_watcher_does_nothing_if_a_detected_change_has_no_corresponding_test_file | ||
system_proc { |*args| puts "[SYSTEM] #{args.join(' ')}" } | ||
listen_thread do |callback| | ||
callback.call(["lib/example/version.rb"], [], []) | ||
end | ||
|
||
stdout, = run_watcher(iterations: 1, in: fixtures_path.join("example_project")) | ||
|
||
refute_includes(stdout, "[SYSTEM]") | ||
end | ||
|
||
def test_watcher_passes_extra_args_through_to_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, extra_args: ["--fail-fast"], in: fixtures_path.join("example_project")) | ||
|
||
assert_includes(stdout, "[SYSTEM] mt --fail-fast -- test/example_test.rb\n") | ||
end | ||
|
||
def test_watcher_prints_a_status_message_after_successful_test_run | ||
system_proc do |*args| | ||
puts "[SYSTEM] #{args.join(' ')}" | ||
true | ||
end | ||
listen_thread do |callback| | ||
callback.call(["test/example_test.rb"], [], []) | ||
end | ||
|
||
stdout, = run_watcher(iterations: 2, in: fixtures_path.join("example_project")) | ||
|
||
assert_includes(stdout, <<~EXPECTED) | ||
[SYSTEM] mt -- test/example_test.rb | ||
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 | ||
system_proc do |*args| | ||
puts "[SYSTEM] #{args.join(' ')}" | ||
false | ||
end | ||
listen_thread do |callback| | ||
callback.call(["test/example_test.rb"], [], []) | ||
end | ||
|
||
stdout, = run_watcher(iterations: 2, in: fixtures_path.join("example_project")) | ||
|
||
assert_includes(stdout, <<~EXPECTED) | ||
[SYSTEM] mt -- test/example_test.rb | ||
Watching for changes to source and test files. Press ctrl-c to exit. | ||
EXPECTED | ||
end | ||
|
||
def test_watcher_restarts_the_listener_when_a_test_run_is_interrupted | ||
thread_count = 0 | ||
system_proc { |*| raise Interrupt } | ||
listen_thread do |callback| | ||
thread_count += 1 | ||
callback.call(["test/example_test.rb"], [], []) unless thread_count > 2 | ||
end | ||
|
||
run_watcher(iterations: 2, in: fixtures_path.join("example_project")) | ||
assert_equal(2, thread_count) | ||
end | ||
|
||
private | ||
|
||
class Listener | ||
def initialize(thread, callback) | ||
Thread.new do | ||
thread.call(callback) | ||
end | ||
end | ||
|
||
def start | ||
end | ||
|
||
def stop | ||
end | ||
|
||
def pause | ||
end | ||
|
||
def stopped? | ||
false | ||
end | ||
|
||
def paused? | ||
false | ||
end | ||
end | ||
|
||
def run_watcher(iterations:, in: ".", extra_args: []) | ||
listen_thread = @listen_thread | ||
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.run(iterations:) | ||
end | ||
end | ||
end | ||
|
||
def listen_thread(&thread) | ||
@listen_thread = thread | ||
end | ||
|
||
def system_proc(&proc) | ||
@system_proc = proc | ||
end | ||
end | ||
end |