diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c53e0c0..d0e23247 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,13 +20,13 @@ jobs: matrix: os: [ubuntu-latest] ruby: - - 2.5 - - 2.6 - - 2.7 + - '2.5' + - '2.6' + - '2.7' - '3.0' - - 3.1 - - 3.2 - - 3.3 + - '3.1' + - '3.2' + - '3.3' include: - os: macos-13 ruby: 2.5 diff --git a/Gemfile b/Gemfile index d93c6e89..c34b10df 100644 --- a/Gemfile +++ b/Gemfile @@ -5,4 +5,4 @@ gemspec gem "rake", "~> 13.0" gem "minitest", "~> 5.0" -gem "byebug" \ No newline at end of file +gem "byebug", require: false \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 56cbc930..bdaab3d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,28 +1,42 @@ PATH remote: . specs: - retest (1.13.2) + retest (2.0.0.pre5) listen (~> 3.9) observer (~> 0.1) string-similarity (~> 2.1) tty-option (~> 0.1) + tty-prompt (~> 0.1) GEM remote: https://rubygems.org/ specs: byebug (11.1.3) - ffi (1.16.3) + ffi (1.17.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) minitest (5.15.0) observer (0.1.2) + pastel (0.8.0) + tty-color (~> 0.5) rake (13.0.6) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) string-similarity (2.1.0) + tty-color (0.6.0) + tty-cursor (0.7.1) tty-option (0.3.0) + tty-prompt (0.23.1) + pastel (~> 0.8) + tty-reader (~> 0.8) + tty-reader (0.9.0) + tty-cursor (~> 0.7) + tty-screen (~> 0.8) + wisper (~> 2.0) + tty-screen (0.8.2) + wisper (2.0.1) PLATFORMS ruby @@ -34,4 +48,4 @@ DEPENDENCIES retest! BUNDLED WITH - 2.3.22 + 2.3.27 diff --git a/bin/build/watchexec b/bin/build/watchexec new file mode 100755 index 00000000..0f836dfb --- /dev/null +++ b/bin/build/watchexec @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t ghcr.io/alexb52/slim-bullseye-watchexec:latest \ + -f builds/dockerfiles/WatchexecSlimBullseye \ + --push . diff --git a/bin/test/bundler-app b/bin/test/bundler-app index f5f2bc61..1a49ef40 100755 --- a/bin/test/bundler-app +++ b/bin/test/bundler-app @@ -1,7 +1,14 @@ #!/usr/bin/env bash +FOLDER="features/bundler-app" + bundle install bundle exec rake build cp -R features/support features/bundler-app/retest -ls -t pkg | head -n1 | xargs -I {} mv pkg/{} features/bundler-app/retest.gem -docker compose -f features/bundler-app/docker-compose.yml up --build --exit-code-from retest \ No newline at end of file +ls -t pkg | head -n1 | xargs -I {} mv pkg/{} "$FOLDER/retest.gem" + +if [[ "$1" == "--no-build" ]]; then + docker compose -f "$FOLDER/docker-compose.yml" up --exit-code-from retest +else + docker compose -f "$FOLDER/docker-compose.yml" up --build --exit-code-from retest +fi diff --git a/bin/test/ruby-app b/bin/test/ruby-app index 135554a0..ad84d862 100755 --- a/bin/test/ruby-app +++ b/bin/test/ruby-app @@ -4,4 +4,4 @@ bundle install bundle exec rake build cp -R features/support features/ruby-app/retest ls -t pkg | head -n1 | xargs -I {} mv pkg/{} features/ruby-app/retest.gem -docker compose -f features/ruby-app/docker-compose.yml up --build --exit-code-from retest \ No newline at end of file +docker compose -f features/ruby-app/docker-compose.yml up --build --exit-code-from retest diff --git a/builds/dockerfiles/WatchexecSlimBullseye b/builds/dockerfiles/WatchexecSlimBullseye new file mode 100644 index 00000000..bacc0569 --- /dev/null +++ b/builds/dockerfiles/WatchexecSlimBullseye @@ -0,0 +1,12 @@ +# Stage 1: Build watchexec with Rust +FROM rust:1.83.0-slim-bullseye AS rust-builder + +# Install necessary dependencies for Rust +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git + +# Install watchexec +RUN cargo install watchexec-cli + +# Verify installation +RUN watchexec --version diff --git a/exe/retest b/exe/retest index 891430f2..ce307158 100755 --- a/exe/retest +++ b/exe/retest @@ -3,26 +3,32 @@ require 'retest' $stdout.sync = true - -Signal.trap(:INT) { $stdout.puts "Goodbye"; exit } +listen_rd, listen_wr = IO.pipe +Signal.trap(:INT) do + puts "Goodbye" + listen_rd.close + listen_wr.close + exit +end options = Retest::Options.new(ARGV) if options.help? - $stdout.puts options.help + puts options.help return end if options.version? - $stdout.puts Retest::VERSION + puts Retest::VERSION return end prompt = Retest::Prompt.new repository = Retest::Repository.new(files: Retest::VersionControl.files, prompt: prompt) command = Retest::Command.for_options(options) -runner = Retest::Runners.runner_for(command.to_s) +runner = Retest::Runner.new(command) sounds = Retest::Sounds.for(options) +watcher = Retest::Watcher.for(options.watcher) sounds.play(:start) runner.add_observer(sounds) @@ -30,7 +36,6 @@ prompt.add_observer(sounds) program = Retest::Program.new( repository: repository, - command: command, runner: runner ) @@ -39,6 +44,12 @@ if options.params[:diff] return end +if watcher == Retest::Watcher::Watchexec + puts "Watcher: [WATCHEXEC]" +else + puts "Watcher: [LISTEN]" +end + launching_message = "Launching Retest..." if options.force_polling? launching_message = "Launching Retest with polling method..." @@ -46,16 +57,82 @@ end # Main action -$stdout.puts launching_message -Retest.listen(options) do |modified, added, removed| +puts launching_message +Retest.listen(options, listener: watcher) do |modified, added, removed| begin - program.run(modified, added, removed) + repository.sync(added: added, removed: removed) + runner.sync(added: added, removed: removed) + + listen_wr.puts "file changed: #{(modified + added).first}" rescue => e - $stdout.puts "Something went wrong: #{e.message}" + puts "Something went wrong: #{e.message}" end end -$stdout.puts "Ready to refactor! You can make file changes now" +puts "Ready to refactor! You can make file changes now" -# not blocking +def run_command(input:, program:) + case input.strip + when /^file changed:\s(.*)$/ + puts "File changed: #{$1}" + program.run($1) + when 'p', 'pause' + program.pause + puts "Program is paused\n" + when 'u', 'unpause' + program.resume + puts "Program has been resumed\n" + when 'e', 'exit' + Process.kill("INT", 0) + when 'r', 'reset' + program.reset_selection + puts "command reset to '#{program.runner.command.to_s}'" + when 'f', 'force' + require 'tty-prompt' + prompt = TTY::Prompt.new + program.force_selection prompt.multi_select( + "What test files do you want to run when saving a file?", + program.repository.test_files, + filter: true, min: 1 + ) + program.run(nil, force_run: true) + when '' + puts "Running last command: '#{program.last_command}'\n" + program.run_last_command + when 'ra', 'run all' + puts "Running all tests\n" + program.run_all + when /^di?f?f?\s(.*)$/ + program.diff($1) + when 'c' + program.clear_terminal + when 'h', 'help' + puts <<~HELP -sleep + * 'h', 'help' # Prints help. + * 'p', 'pause' # Pauses Retest. Tests aren't run on file change events until unpaused. + * 'u', 'unpause' # Unpauses Retest. + * # Runs last changed triggered command. + * 'ra, 'run all' # Runs all tests. + * 'f', 'force' # Forces a selection of test to run on every file change. + * 'r', 'reset' # Disables forced selection. + * 'd', 'diff' [GIT BRANCH] # Runs matching specs that changed from a target branch. + * 'c' # Clears window. + * 'e', 'exit' # Exits Retest. + HELP + else + puts "Unknown interactive command #{input}\n" + end +end + +connections = [$stdin, listen_rd] +loop do + puts "\nType interactive command and press enter. Enter 'h' for help." + print(">\s") + + ready = IO.select(connections) + readable_connections = ready[0] + readable_connections.each do |conn| + data = conn.readpartial(4096) + run_command(input: data.to_s.chomp, program: program) + end +end diff --git a/features/bundler-app/lib/bundler_app.rb b/features/bundler-app/lib/bundler_app.rb index 68855085..503d8b28 100644 --- a/features/bundler-app/lib/bundler_app.rb +++ b/features/bundler-app/lib/bundler_app.rb @@ -2,6 +2,7 @@ require_relative "bundler_app/version" require_relative "bundler_app/bottles" +require_relative "bundler_app/fibonacci" module BundlerApp class Error < StandardError; end diff --git a/features/bundler-app/lib/bundler_app/fibonacci.rb b/features/bundler-app/lib/bundler_app/fibonacci.rb new file mode 100644 index 00000000..7ee4feea --- /dev/null +++ b/features/bundler-app/lib/bundler_app/fibonacci.rb @@ -0,0 +1,11 @@ +# fibonacci.rb +class Fibonacci + def self.calculate(n) + raise ArgumentError, "Input must be a non-negative integer." unless n.is_a?(Integer) && n >= 0 + return n if n <= 1 + + a, b = 0, 1 + (n - 1).times { a, b = b, a + b } + b + end +end \ No newline at end of file diff --git a/features/bundler-app/retest/retest_test.rb b/features/bundler-app/retest/retest_test.rb index 3c22c4e4..0f4b5c32 100644 --- a/features/bundler-app/retest/retest_test.rb +++ b/features/bundler-app/retest/retest_test.rb @@ -2,8 +2,6 @@ require_relative 'support/test_helper' require 'minitest/autorun' require_relative 'retest_test/file_changes_test' +require_relative 'retest_test/interactive_commands_test' $stdout.sync = true - -include FileHelper - diff --git a/features/bundler-app/retest/retest_test/file_changes_test.rb b/features/bundler-app/retest/retest_test/file_changes_test.rb index b632b561..3938e6b7 100644 --- a/features/bundler-app/retest/retest_test/file_changes_test.rb +++ b/features/bundler-app/retest/retest_test/file_changes_test.rb @@ -1,4 +1,6 @@ class FileChangesTest < Minitest::Test + include RetestHelper + def setup @command = 'retest' end @@ -10,7 +12,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -21,8 +23,9 @@ def test_modifying_existing_file modify_file('lib/bundler_app/bottles.rb') - assert_match "Test File Selected: test/bundler_app/test_bottles.rb", @output.read - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + 'Test file: test/bundler_app/test_bottles.rb', + '12 runs, 12 assertions, 0 failures, 0 errors, 0 skips') end def test_modifying_existing_test_file @@ -30,8 +33,9 @@ def test_modifying_existing_test_file modify_file('test/bundler_app/test_bottles.rb') - assert_match "Test File Selected: test/bundler_app/test_bottles.rb", @output.read - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + 'Test file: test/bundler_app/test_bottles.rb', + '12 runs, 12 assertions, 0 failures, 0 errors, 0 skips') end def test_creating_a_new_test_file @@ -39,7 +43,7 @@ def test_creating_a_new_test_file create_file 'test/bundler_app/test_foo.rb' - assert_match "Test File Selected: test/bundler_app/test_foo.rb", @output.read + assert_output_matches 'Test file: test/bundler_app/test_foo.rb' ensure delete_file 'test/bundler_app/test_foo.rb' @@ -49,19 +53,16 @@ def test_creating_a_new_file launch_retest @command create_file 'lib/bundler_app/foo.rb' - assert_match <<~EXPECTED, @output.read - 404 - Test File Not Found - Retest could not find a matching test file to run. - EXPECTED + assert_output_matches 'FileNotFound - Retest could not find a matching test file to run.' create_file 'test/bundler_app/test_foo.rb' - assert_match "Test File Selected: test/bundler_app/test_foo.rb", @output.read + assert_output_matches 'Test file: test/bundler_app/test_foo.rb' modify_file('lib/bundler_app/bottles.rb') - assert_match "Test File Selected: test/bundler_app/test_bottles.rb", @output.read + assert_output_matches 'Test file: test/bundler_app/test_bottles.rb' modify_file('lib/bundler_app/foo.rb') - assert_match "Test File Selected: test/bundler_app/test_foo.rb", @output.read + assert_output_matches 'Test file: test/bundler_app/test_foo.rb' ensure delete_file 'lib/bundler_app/foo.rb' @@ -69,13 +70,13 @@ def test_creating_a_new_file end def test_untracked_file - create_file 'lib/bundler_app/foo.rb', should_sleep: false - create_file 'test/bundler_app/test_foo.rb', should_sleep: false + create_file 'lib/bundler_app/foo.rb', sleep_for: 0 + create_file 'test/bundler_app/test_foo.rb', sleep_for: 0 launch_retest @command modify_file 'lib/bundler_app/foo.rb' - assert_match "Test File Selected: test/bundler_app/test_foo.rb", @output.read + assert_output_matches 'Test file: test/bundler_app/test_foo.rb' ensure delete_file 'lib/bundler_app/foo.rb' diff --git a/features/bundler-app/retest/retest_test/interactive_commands_test.rb b/features/bundler-app/retest/retest_test/interactive_commands_test.rb new file mode 100644 index 00000000..2e8b91c7 --- /dev/null +++ b/features/bundler-app/retest/retest_test/interactive_commands_test.rb @@ -0,0 +1,142 @@ +class InteractiveCommandTest < Minitest::Test + include RetestHelper + + def setup + @command = 'retest' + end + + def teardown + end_retest + end + + def test_start_help + launch_retest @command + + assert_output_matches <<~EXPECTED.chomp + Setup identified: [RAKE]. Using command: 'bundle exec rake test TEST=' + Watcher: [LISTEN] + Launching Retest... + Ready to refactor! You can make file changes now + + Type interactive command and press enter. Enter 'h' for help. + >\s + EXPECTED + + write_input("h\n") + + assert_output_matches <<~EXPECTED.chomp + * 'h', 'help' # Prints help. + * 'p', 'pause' # Pauses Retest. Tests aren't run on file change events until unpaused. + * 'u', 'unpause' # Unpauses Retest. + * # Runs last changed triggered command. + * 'ra, 'run all' # Runs all tests. + * 'f', 'force' # Forces a selection of test to run on every file change. + * 'r', 'reset' # Disables forced selection. + * 'd', 'diff' [GIT BRANCH] # Runs matching specs that changed from a target branch. + * 'c' # Clears window. + * 'e', 'exit' # Exits Retest. + + Type interactive command and press enter. Enter 'h' for help. + >\s + EXPECTED + end + + def test_pause_unpause + launch_retest @command + + modify_file('lib/bundler_app/bottles.rb') + + assert_output_matches( + "Test file: test/bundler_app/test_bottles.rb", + "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips" + ) + + write_input("p\n") + + assert_output_matches "Program is paused" + + modify_file('lib/bundler_app/bottles.rb') + + assert_output_matches <<~EXPECTED + File changed: lib/bundler_app/bottles.rb + Main program paused. Please resume program first. + EXPECTED + + write_input("\n") # Manually run previous test + + assert_output_matches( + "Running last command: 'bundle exec rake test TEST=test/bundler_app/test_bottles.rb'", + "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips" + ) + + write_input("u\n") + + modify_file('lib/bundler_app/bottles.rb') + + assert_output_matches( + "Test file: test/bundler_app/test_bottles.rb", + "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips" + ) + end + + def test_force_reset + launch_retest @command + + write_input("f\n") + + assert_output_matches "What test files do you want to run when saving a file? (min. 1)" + + write_input("fib\s\n") + + assert_output_matches <<~EXPECTED + Forced selection enabled. + Reset to default settings by typing 'r' in the interactive console. + + Tests selected: + - test/bundler_app/test_fibonacci.rb + EXPECTED + + assert_output_matches "8 runs, 9 assertions, 0 failures, 0 errors, 0 skips" + + modify_file('lib/bundler_app/bottles.rb') + + assert_output_matches <<~EXPECTED + Forced selection enabled. + Reset to default settings by typing 'r' in the interactive console. + + Tests selected: + - test/bundler_app/test_fibonacci.rb + EXPECTED + + write_input("\n") # Manually run previous test + + assert_output_matches( + "Running last command: 'bundle exec rake test TEST=test/bundler_app/test_fibonacci.rb'", + "8 runs, 9 assertions, 0 failures, 0 errors, 0 skips" + ) + + write_input("r\n") + + modify_file('lib/bundler_app/bottles.rb') + + assert_output_matches( + "Test file: test/bundler_app/test_bottles.rb", + "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips") + end + + def test_run_all + launch_retest @command + + write_input("ra\n") + + assert_output_matches( + "Running all tests", + "21 runs, 22 assertions, 0 failures, 0 errors, 0 skips") + + write_input("\n") # Manually run previous test + + assert_output_matches( + "Running last command: 'bundle exec rake test", + "21 runs, 22 assertions, 0 failures, 0 errors, 0 skips") + end +end diff --git a/features/bundler-app/retest/support/output_file.rb b/features/bundler-app/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/bundler-app/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/bundler-app/retest/support/test_helper.rb b/features/bundler-app/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/bundler-app/retest/support/test_helper.rb +++ b/features/bundler-app/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/bundler-app/test/bundler_app/test_fibonacci.rb b/features/bundler-app/test/bundler_app/test_fibonacci.rb new file mode 100644 index 00000000..28c131c9 --- /dev/null +++ b/features/bundler-app/test/bundler_app/test_fibonacci.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'test_helper' + +module BundlerApp + class FibonacciTest < Minitest::Test + def test_fibonacci_zero + assert_equal 0, Fibonacci.calculate(0) + end + + def test_fibonacci_one + assert_equal 1, Fibonacci.calculate(1) + end + + def test_fibonacci_two + assert_equal 1, Fibonacci.calculate(2) + end + + def test_fibonacci_five + assert_equal 5, Fibonacci.calculate(5) + end + + def test_fibonacci_ten + assert_equal 55, Fibonacci.calculate(10) + end + + def test_large_fibonacci + assert_equal 6765, Fibonacci.calculate(20) # Example large Fibonacci number + end + + def test_invalid_input_negative + assert_raises(ArgumentError) { Fibonacci.calculate(-1) } + end + + def test_invalid_input_non_integer + assert_raises(ArgumentError) { Fibonacci.calculate(2.5) } + assert_raises(ArgumentError) { Fibonacci.calculate("five") } + end + end +end \ No newline at end of file diff --git a/features/git-ruby/retest/retest_test.rb b/features/git-ruby/retest/retest_test.rb index 058c035f..5260e111 100644 --- a/features/git-ruby/retest/retest_test.rb +++ b/features/git-ruby/retest/retest_test.rb @@ -4,9 +4,9 @@ $stdout.sync = true -include FileHelper - class FileChangesTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --ruby' end @@ -18,7 +18,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -26,6 +26,8 @@ def test_start_retest end class GitChangesTest < Minitest::Test + include RetestHelper + def setup `git config --global init.defaultBranch main` `git config --global --add safe.directory /usr/src/app` @@ -45,25 +47,24 @@ def teardown end def test_diffs_from_other_branch - delete_file('lib/to_be_deleted.rb') - rename_file('lib/to_be_renamed.rb', 'lib/renamed.rb') - rename_file('lib/to_be_renamed_with_test_file.rb', 'lib/renamed_with_test_file.rb') - rename_file('test/to_be_renamed_with_test_file_test.rb', 'test/renamed_with_test_file_test.rb') - create_file('lib/created.rb', should_sleep: false) - create_file('lib/created_with_test_file.rb', should_sleep: false) - create_file('test/created_with_test_file_test.rb', should_sleep: false) + delete_file('lib/to_be_deleted.rb', sleep_for: 0) + rename_file('lib/to_be_renamed.rb', 'lib/renamed.rb', sleep_for: 0) + rename_file('lib/to_be_renamed_with_test_file.rb', 'lib/renamed_with_test_file.rb', sleep_for: 0) + rename_file('test/to_be_renamed_with_test_file_test.rb', 'test/renamed_with_test_file_test.rb', sleep_for: 0) + create_file('lib/created.rb', sleep_for: 0) + create_file('lib/created_with_test_file.rb', sleep_for: 0) + create_file('test/created_with_test_file_test.rb', sleep_for: 0) `git add .` `git commit -m "Rename, Add and Remove files"` launch_retest 'retest --diff=main --ruby' - sleep 2 - assert_match <<~EXPECTED, @output.read - Tests found: - - test/created_with_test_file_test.rb - - test/renamed_with_test_file_test.rb - - test/to_be_renamed_test.rb + assert_output_matches <<~EXPECTED + Tests selected: + - test/created_with_test_file_test.rb + - test/renamed_with_test_file_test.rb + - test/to_be_renamed_test.rb EXPECTED end end \ No newline at end of file diff --git a/features/git-ruby/retest/support/output_file.rb b/features/git-ruby/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/git-ruby/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/git-ruby/retest/support/test_helper.rb b/features/git-ruby/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/git-ruby/retest/support/test_helper.rb +++ b/features/git-ruby/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/hanami-app/retest/retest_test.rb b/features/hanami-app/retest/retest_test.rb index 5f385522..699f5a3e 100644 --- a/features/hanami-app/retest/retest_test.rb +++ b/features/hanami-app/retest/retest_test.rb @@ -4,9 +4,9 @@ $stdout.sync = true -include FileHelper - class MatchingTestsCommandTest < Minitest::Test + include RetestHelper + def setup @command = "retest --rake" end @@ -18,7 +18,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -29,12 +29,15 @@ def test_modify_a_file modify_file 'apps/web/controllers/books/create.rb' - assert_match "Test File Selected: spec/web/controllers/books/create_spec.rb", @output.read - assert_match "4 runs, 7 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + "Test file: spec/web/controllers/books/create_spec.rb", + "4 runs, 7 assertions, 0 failures, 0 errors, 0 skips") end end class AllTestsCommandTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --rake --all' end @@ -46,7 +49,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -57,11 +60,13 @@ def test_modify_a_file modify_file 'apps/web/controllers/books/create.rb' - assert_match "15 runs, 27 assertions, 0 failures, 0 errors, 1 skips", @output.read + assert_output_matches "15 runs, 27 assertions, 0 failures, 0 errors, 1 skips" end end class AutoFlagTest < Minitest::Test + include RetestHelper + def teardown end_retest end @@ -69,8 +74,9 @@ def teardown def test_with_no_command launch_retest 'retest' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RAKE]. Using command: 'bundle exec rake test TEST=' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT @@ -79,8 +85,9 @@ def test_with_no_command def test_with_no_command_all launch_retest 'retest --all' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RAKE]. Using command: 'bundle exec rake test' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT diff --git a/features/hanami-app/retest/support/output_file.rb b/features/hanami-app/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/hanami-app/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/hanami-app/retest/support/test_helper.rb b/features/hanami-app/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/hanami-app/retest/support/test_helper.rb +++ b/features/hanami-app/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/rails-app/retest/retest_test.rb b/features/rails-app/retest/retest_test.rb index cee31998..980ac666 100644 --- a/features/rails-app/retest/retest_test.rb +++ b/features/rails-app/retest/retest_test.rb @@ -4,9 +4,9 @@ $stdout.sync = true -include FileHelper - class MatchingTestsCommandTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --rails' end @@ -18,7 +18,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -29,12 +29,15 @@ def test_modify_a_file modify_file 'app/models/post.rb' - assert_match "Test File Selected: test/models/post_test.rb", @output.read - assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + "Test file: test/models/post_test.rb", + "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips") end end class AllTestsCommandTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --rails --all' end @@ -46,7 +49,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -57,11 +60,13 @@ def test_modify_a_file modify_file 'app/models/post.rb' - assert_match "8 runs, 10 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches "8 runs, 10 assertions, 0 failures, 0 errors, 0 skips" end end class AutoFlagTest < Minitest::Test + include RetestHelper + def teardown end_retest end @@ -69,8 +74,9 @@ def teardown def test_with_no_command launch_retest 'retest' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RAILS]. Using command: 'bin/rails test ' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT @@ -79,8 +85,9 @@ def test_with_no_command def test_with_no_command_all launch_retest 'retest --all' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RAILS]. Using command: 'bin/rails test' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT @@ -94,6 +101,8 @@ def test_repository_setup end class DiffOptionTest < Minitest::Test + include RetestHelper + def setup `git config --global init.defaultBranch main` `git config --global user.email "you@example.com"` @@ -106,7 +115,6 @@ def setup end def teardown - @output.delete `git checkout -` `git clean -fd .` `git checkout .` @@ -122,20 +130,13 @@ def test_diffs_from_other_branch `git commit -m "Scaffold books"` launch_retest 'retest --diff=main' - wait - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED, "7 runs, 9 assertions, 0 failures, 0 errors, 0 skips" Setup identified: [RAILS]. Using command: 'bin/rails test ' - Tests found: + Tests selected: - test/controllers/books_controller_test.rb - test/models/book_test.rb - test/system/books_test.rb - Running tests... - Test File Selected: test/controllers/books_controller_test.rb test/models/book_test.rb test/system/books_test.rb - EXPECTED - - assert_match <<~EXPECTED, @output.read - 7 runs, 9 assertions, 0 failures, 0 errors, 0 skips EXPECTED end end \ No newline at end of file diff --git a/features/rails-app/retest/support/output_file.rb b/features/rails-app/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/rails-app/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/rails-app/retest/support/test_helper.rb b/features/rails-app/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/rails-app/retest/support/test_helper.rb +++ b/features/rails-app/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/rspec-rails/retest/retest_test.rb b/features/rspec-rails/retest/retest_test.rb index 11c19ba1..f9896a08 100644 --- a/features/rspec-rails/retest/retest_test.rb +++ b/features/rspec-rails/retest/retest_test.rb @@ -4,9 +4,9 @@ $stdout.sync = true -include FileHelper - class MatchingTestsCommandTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --rspec' end @@ -18,7 +18,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -29,12 +29,15 @@ def test_modify_a_file modify_file 'app/models/post.rb' - assert_match "Test File Selected: spec/models/post_spec.rb", @output.read - assert_match "2 examples, 0 failures", @output.read + assert_output_matches( + "Test file: spec/models/post_spec.rb", + "2 examples, 0 failures") end end class AllTestsCommandTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --rspec --all' end @@ -46,7 +49,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -55,13 +58,15 @@ def test_start_retest def test_modify_a_file launch_retest @command - modify_file 'app/models/post.rb', sleep_seconds: 15 + modify_file 'app/models/post.rb' - assert_match "9 examples, 0 failures", @output.read + assert_output_matches "9 examples, 0 failures" end end class AutoFlagTest < Minitest::Test + include RetestHelper + def teardown end_retest end @@ -69,8 +74,9 @@ def teardown def test_with_no_command launch_retest 'retest' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RSPEC]. Using command: 'bundle exec rspec ' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT @@ -79,8 +85,9 @@ def test_with_no_command def test_with_no_command_all launch_retest 'retest --all' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RSPEC]. Using command: 'bundle exec rspec' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT diff --git a/features/rspec-rails/retest/support/output_file.rb b/features/rspec-rails/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/rspec-rails/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/rspec-rails/retest/support/test_helper.rb b/features/rspec-rails/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/rspec-rails/retest/support/test_helper.rb +++ b/features/rspec-rails/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/rspec-ruby/retest/retest_test.rb b/features/rspec-ruby/retest/retest_test.rb index 421cb470..9ae4b9ae 100644 --- a/features/rspec-ruby/retest/retest_test.rb +++ b/features/rspec-ruby/retest/retest_test.rb @@ -7,5 +7,3 @@ $stdout.sync = true -include FileHelper - diff --git a/features/rspec-ruby/retest/retest_test/file_changes_test.rb b/features/rspec-ruby/retest/retest_test/file_changes_test.rb index 2fe24a89..c2ce20f7 100644 --- a/features/rspec-ruby/retest/retest_test/file_changes_test.rb +++ b/features/rspec-ruby/retest/retest_test/file_changes_test.rb @@ -1,4 +1,6 @@ class FileChangesTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --rspec' end @@ -10,7 +12,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -21,8 +23,9 @@ def test_modifying_existing_file modify_file('lib/bottles.rb') - assert_match "Test File Selected: spec/bottles_spec.rb", @output.read - assert_match "12 examples, 0 failures", @output.read + assert_output_matches( + "Test file: spec/bottles_spec.rb", + "12 examples, 0 failures") end def test_modifying_existing_test_file @@ -30,8 +33,9 @@ def test_modifying_existing_test_file modify_file('spec/bottles_spec.rb') - assert_match "Test File Selected: spec/bottles_spec.rb", @output.read - assert_match "12 examples, 0 failures", @output.read + assert_output_matches( + "Test file: spec/bottles_spec.rb", + "12 examples, 0 failures") end def test_creating_a_new_test_file @@ -39,7 +43,7 @@ def test_creating_a_new_test_file create_file 'foo_spec.rb' - assert_match "Test File Selected: foo_spec.rb", @output.read + assert_output_matches "Test file: foo_spec.rb" ensure delete_file 'foo_spec.rb' @@ -49,19 +53,18 @@ def test_creating_a_new_file launch_retest @command create_file 'foo.rb' - assert_match <<~EXPECTED, @output.read - 404 - Test File Not Found - Retest could not find a matching test file to run. + assert_output_matches <<~EXPECTED + FileNotFound - Retest could not find a matching test file to run. EXPECTED create_file 'foo_spec.rb' - assert_match "Test File Selected: foo_spec.rb", @output.read + assert_output_matches "Test file: foo_spec.rb" modify_file('lib/bottles.rb') - assert_match "Test File Selected: spec/bottles_spec.rb", @output.read + assert_output_matches "Test file: spec/bottles_spec.rb" modify_file('foo.rb') - assert_match "Test File Selected: foo_spec.rb", @output.read + assert_output_matches "Test file: foo_spec.rb" ensure delete_file 'foo.rb' @@ -69,13 +72,13 @@ def test_creating_a_new_file end def test_untracked_file - create_file 'foo.rb', should_sleep: false - create_file 'foo_spec.rb', should_sleep: false + create_file 'foo.rb', sleep_for: 0 + create_file 'foo_spec.rb', sleep_for: 0 launch_retest @command modify_file 'foo.rb' - assert_match "Test File Selected: foo_spec.rb", @output.read + assert_output_matches "Test file: foo_spec.rb" ensure delete_file 'foo.rb' diff --git a/features/rspec-ruby/retest/retest_test/flags_test.rb b/features/rspec-ruby/retest/retest_test/flags_test.rb index c882ae16..b6116575 100644 --- a/features/rspec-ruby/retest/retest_test/flags_test.rb +++ b/features/rspec-ruby/retest/retest_test/flags_test.rb @@ -1,4 +1,6 @@ class FlagTest < Minitest::Test + include RetestHelper + def setup end @@ -9,14 +11,15 @@ def teardown def test_with_no_command launch_retest 'retest' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RSPEC]. Using command: 'bundle exec rspec ' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT modify_file('lib/bottles.rb') - assert_match "Test File Selected: spec/bottles_spec.rb", @output.read + assert_output_matches "Test file: spec/bottles_spec.rb" end end \ No newline at end of file diff --git a/features/rspec-ruby/retest/support/output_file.rb b/features/rspec-ruby/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/rspec-ruby/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/rspec-ruby/retest/support/test_helper.rb b/features/rspec-ruby/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/rspec-ruby/retest/support/test_helper.rb +++ b/features/rspec-ruby/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/ruby-app/Dockerfile b/features/ruby-app/Dockerfile index f52b6c3f..a8964376 100644 --- a/features/ruby-app/Dockerfile +++ b/features/ruby-app/Dockerfile @@ -1,11 +1,15 @@ -FROM ruby:2.5.9-alpine3.13 +FROM ruby:2.7-slim-bullseye -ARG BUILD_PACKAGES="build-base git" +# Install necessary dependencies +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git && \ + apt-get clean && rm -rf /var/lib/apt/lists/* -RUN apk update && \ - apk upgrade && \ - apk add --update --no-cache $BUILD_PACKAGES && \ - rm -rf /var/cache/apk/* +# Copy watchexec from the Rust stage +COPY --from=ghcr.io/alexb52/slim-bullseye-watchexec:latest /usr/local/cargo/bin/watchexec /usr/local/bin/watchexec + +# Verify watchexec installation in the final image +RUN watchexec --version # throw errors if Gemfile has been modified since Gemfile.lock RUN bundle config --global frozen 1 @@ -14,7 +18,6 @@ WORKDIR /usr/src/app ENV LANG C.UTF-8 ENV BUNDLER_VERSION 2.1 - ENV GEM_HOME="/usr/local/bundle" ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH diff --git a/features/ruby-app/retest/retest_test.rb b/features/ruby-app/retest/retest_test.rb index f48ba94a..dd99d5e3 100644 --- a/features/ruby-app/retest/retest_test.rb +++ b/features/ruby-app/retest/retest_test.rb @@ -1,12 +1,66 @@ require 'retest' -require_relative 'support/test_helper' +require 'byebug' require 'minitest/autorun' -require_relative 'retest_test/file_changes_test' -require_relative 'retest_test/flags_test' -require_relative 'retest_test/setup_test' -require_relative 'retest_test/matching_unmatching_command_test' +require_relative 'support/test_helper' +require_relative 'shared/file_changes' +require_relative 'shared/setup' +require_relative 'shared/explicit_matching' $stdout.sync = true -include FileHelper +class TestListenWatcher < Minitest::Test + include RetestHelper + + def setup + @command = 'retest -w listen' + end + + def test_start_retest + launch_retest(@command) + + assert_output_matches <<~EXPECTED + Setup identified: [RUBY]. Using command: 'bundle exec ruby ' + Watcher: [LISTEN] + Launching Retest... + Ready to refactor! You can make file changes now + EXPECTED + end +end + +class TestWatchexecWatcher < Minitest::Test + include RetestHelper + + def setup + @command = 'retest -w watchexec' + end + + def test_start_retest + launch_retest(@command) + + assert_output_matches <<~EXPECTED + Setup identified: [RUBY]. Using command: 'bundle exec ruby ' + Watcher: [WATCHEXEC] + Launching Retest... + Ready to refactor! You can make file changes now + EXPECTED + end +end + +class TestDefaultWatcher < Minitest::Test + include RetestHelper + + def setup + @command = 'retest' + end + + def test_uses_watchexec_when_installed + launch_retest(@command) + assert_output_matches <<~EXPECTED + Setup identified: [RUBY]. Using command: 'bundle exec ruby ' + Watcher: [WATCHEXEC] + Launching Retest... + Ready to refactor! You can make file changes now + EXPECTED + end +end diff --git a/features/ruby-app/retest/retest_test/file_changes_test.rb b/features/ruby-app/retest/retest_test/file_changes_test.rb deleted file mode 100644 index 0e6ea7a7..00000000 --- a/features/ruby-app/retest/retest_test/file_changes_test.rb +++ /dev/null @@ -1,84 +0,0 @@ -class FileChangesTest < Minitest::Test - def setup - @command = 'retest --ruby' - end - - def teardown - end_retest - end - - def test_start_retest - launch_retest @command - - assert_match <<~EXPECTED, @output.read - Launching Retest... - Ready to refactor! You can make file changes now - EXPECTED - end - - def test_modifying_existing_file - launch_retest @command - - modify_file('lib/bottles.rb') - - assert_match "Test File Selected: test/bottles_test.rb", @output.read - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", @output.read - end - - def test_modifying_existing_test_file - launch_retest @command - - modify_file('test/bottles_test.rb') - - assert_match "Test File Selected: test/bottles_test.rb", @output.read - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", @output.read - end - - def test_creating_a_new_test_file - launch_retest @command - - create_file 'foo_test.rb' - - assert_match "Test File Selected: foo_test.rb", @output.read - - ensure - delete_file 'foo_test.rb' - end - - def test_creating_a_new_file - launch_retest @command - - create_file 'foo.rb' - assert_match <<~EXPECTED, @output.read - 404 - Test File Not Found - Retest could not find a matching test file to run. - EXPECTED - - create_file 'foo_test.rb' - assert_match "Test File Selected: foo_test.rb", @output.read - - modify_file('lib/bottles.rb') - assert_match "Test File Selected: test/bottles_test.rb", @output.read - - modify_file('foo.rb') - assert_match "Test File Selected: foo_test.rb", @output.read - - ensure - delete_file 'foo.rb' - delete_file 'foo_test.rb' - end - - def test_untracked_file - create_file 'foo.rb', should_sleep: false - create_file 'foo_test.rb', should_sleep: false - - launch_retest @command - - modify_file 'foo.rb' - assert_match "Test File Selected: foo_test.rb", @output.read - - ensure - delete_file 'foo.rb' - delete_file 'foo_test.rb' - end -end diff --git a/features/ruby-app/retest/retest_test/flags_test.rb b/features/ruby-app/retest/retest_test/flags_test.rb deleted file mode 100644 index 8fd86f64..00000000 --- a/features/ruby-app/retest/retest_test/flags_test.rb +++ /dev/null @@ -1,22 +0,0 @@ -class FlagTest < Minitest::Test - def setup - end - - def teardown - end_retest - end - - def test_with_no_command - launch_retest 'retest' - - assert_match <<~OUTPUT, @output.read - Setup identified: [RUBY]. Using command: 'bundle exec ruby ' - Launching Retest... - Ready to refactor! You can make file changes now - OUTPUT - - modify_file('lib/bottles.rb') - - assert_match "Test File Selected: test/bottles_test.rb", @output.read - end -end \ No newline at end of file diff --git a/features/ruby-app/retest/retest_test/matching_unmatching_command_test.rb b/features/ruby-app/retest/retest_test/matching_unmatching_command_test.rb deleted file mode 100644 index e9356174..00000000 --- a/features/ruby-app/retest/retest_test/matching_unmatching_command_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -class MatchingUnmatchingCommandTest < Minitest::Test - def setup - create_file('test/other_bottles_test.rb', should_sleep: false) - end - - def teardown - end_retest - delete_file('test/other_bottles_test.rb') - end - - def test_not_displaying_options_on_unmatching_command - launch_retest "retest 'echo there was no command'" - - modify_file('lib/bottles.rb') - - refute_match "We found few tests matching:", @output.read - assert_match "there was no command", @output.read - end - - def test_displaying_options_on_matching_command - launch_retest('retest --ruby') - - create_file 'foo_test.rb' - assert_match "Test File Selected: foo_test.rb", @output.read - - modify_file('lib/bottles.rb') - assert_match <<~EXPECTED.chomp, @output.read - We found few tests matching: lib/bottles.rb - - [0] - test/bottles_test.rb - [1] - test/other_bottles_test.rb - [2] - none - - Which file do you want to use? - Enter the file number now: - > - EXPECTED - - @input.write "2\n" - wait - - assert_match "Test File Selected: foo_test.rb", @output.read - - ensure - delete_file 'foo_test.rb' - end -end diff --git a/features/ruby-app/retest/shared/explicit_matching.rb b/features/ruby-app/retest/shared/explicit_matching.rb new file mode 100644 index 00000000..b0c457ed --- /dev/null +++ b/features/ruby-app/retest/shared/explicit_matching.rb @@ -0,0 +1,34 @@ +module ExplicitMatching + def teardown + end_retest + end + + def test_displaying_options_on_matching_command + create_file('test/other_bottles_test.rb', sleep_for: 0) + + launch_retest(@command) + + create_file 'foo_test.rb' + assert_output_matches "Test file: foo_test.rb" + + modify_file('lib/bottles.rb') + assert_output_matches <<~EXPECTED.chomp + We found few tests matching: lib/bottles.rb + + [0] - test/bottles_test.rb + [1] - test/other_bottles_test.rb + [2] - none + + Which file do you want to use? + Enter the file number now: + > + EXPECTED + + write_input("2\n") + assert_output_matches "Test file: foo_test.rb" + + ensure + delete_file 'foo_test.rb' + delete_file('test/other_bottles_test.rb') + end +end diff --git a/features/ruby-app/retest/shared/file_changes.rb b/features/ruby-app/retest/shared/file_changes.rb new file mode 100644 index 00000000..94c41790 --- /dev/null +++ b/features/ruby-app/retest/shared/file_changes.rb @@ -0,0 +1,74 @@ +module FileChanges + include RetestHelper + + def teardown + end_retest + end + + def test_modifying_existing_file + launch_retest(@command) + + modify_file('lib/bottles.rb') + + assert_output_matches( + "Test file: test/bottles_test.rb", + "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips") + end + + def test_modifying_existing_test_file + launch_retest(@command) + + modify_file('test/bottles_test.rb') + + assert_output_matches( + "Test file: test/bottles_test.rb", + "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips") + end + + def test_creating_a_new_test_file + launch_retest(@command) + + create_file 'foo_test.rb' + + assert_output_matches "Test file: foo_test.rb" + + ensure + delete_file 'foo_test.rb' + end + + def test_creating_a_new_file + launch_retest(@command) + + create_file 'foo.rb' + assert_output_matches <<~EXPECTED + FileNotFound - Retest could not find a matching test file to run. + EXPECTED + + create_file 'foo_test.rb' + assert_output_matches "Test file: foo_test.rb" + + modify_file('lib/bottles.rb') + assert_output_matches "Test file: test/bottles_test.rb" + + modify_file('foo.rb') + assert_output_matches "Test file: foo_test.rb" + + ensure + delete_file 'foo.rb' + delete_file 'foo_test.rb' + end + + def test_untracked_file + create_file 'foo.rb', sleep_for: 0 + create_file 'foo_test.rb', sleep_for: 0 + + launch_retest(@command) + + modify_file 'foo.rb' + assert_output_matches "Test file: foo_test.rb" + + ensure + delete_file 'foo.rb' + delete_file 'foo_test.rb' + end +end diff --git a/features/ruby-app/retest/retest_test/setup_test.rb b/features/ruby-app/retest/shared/setup.rb similarity index 69% rename from features/ruby-app/retest/retest_test/setup_test.rb rename to features/ruby-app/retest/shared/setup.rb index 346af806..5bcb57a1 100644 --- a/features/ruby-app/retest/retest_test/setup_test.rb +++ b/features/ruby-app/retest/shared/setup.rb @@ -1,5 +1,5 @@ -class SetupTest < Minitest::Test +module Setup def test_repository_setup assert_equal :ruby, Retest::Setup.new.type end -end \ No newline at end of file +end diff --git a/features/ruby-app/retest/support/output_file.rb b/features/ruby-app/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/ruby-app/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/ruby-app/retest/support/test_helper.rb b/features/ruby-app/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/ruby-app/retest/support/test_helper.rb +++ b/features/ruby-app/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/ruby-bare/retest/flags/help.rb b/features/ruby-bare/retest/flags/help.rb index b29fe0ba..ed6cd664 100644 --- a/features/ruby-bare/retest/flags/help.rb +++ b/features/ruby-bare/retest/flags/help.rb @@ -1,4 +1,6 @@ class TestHelpFlag < Minitest::Test + include RetestHelper + def teardown end_retest end @@ -6,7 +8,7 @@ def teardown def test_help launch_retest 'retest --help' - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Usage: retest [OPTIONS] [COMMAND] Watch a file change and run it matching spec. @@ -16,7 +18,7 @@ def test_help def test_help_short_flag launch_retest 'retest -h' - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Usage: retest [OPTIONS] [COMMAND] Watch a file change and run it matching spec. diff --git a/features/ruby-bare/retest/flags/version.rb b/features/ruby-bare/retest/flags/version.rb index 6a1d4376..55b026b1 100644 --- a/features/ruby-bare/retest/flags/version.rb +++ b/features/ruby-bare/retest/flags/version.rb @@ -1,4 +1,6 @@ class TestVersionFlag < Minitest::Test + include RetestHelper + def teardown end_retest end @@ -6,12 +8,12 @@ def teardown def test_version launch_retest 'retest --version' - assert_match /^1\.\d+\.\d+/, @output.read + assert_output_matches /^2\.\d+\.\d+/ end def test_version_short_flag launch_retest 'retest -v' - assert_match /^1\.\d+\.\d+/, @output.read + assert_output_matches /^2\.\d+\.\d+/ end end diff --git a/features/ruby-bare/retest/retest_test.rb b/features/ruby-bare/retest/retest_test.rb index c525f9da..9dae6bd6 100644 --- a/features/ruby-bare/retest/retest_test.rb +++ b/features/ruby-bare/retest/retest_test.rb @@ -15,8 +15,6 @@ $stdout.sync = true -include FileHelper - class SetupTest < Minitest::Test def test_repository_setup assert_equal :ruby, Retest::Setup.new.type @@ -24,6 +22,8 @@ def test_repository_setup end class FileChangesTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --ruby' end @@ -35,7 +35,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -46,8 +46,9 @@ def test_modifying_existing_file modify_file('program.rb') - assert_match "Test File Selected: program_test.rb", @output.read - assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + "Test file: program_test.rb", + "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips") end def test_modifying_existing_test_file @@ -55,8 +56,9 @@ def test_modifying_existing_test_file modify_file('program_test.rb') - assert_match "Test File Selected: program_test.rb", @output.read - assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + "Test file: program_test.rb", + "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips") end def test_creating_a_new_test_file @@ -64,7 +66,7 @@ def test_creating_a_new_test_file create_file 'foo_test.rb' - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" delete_file 'foo_test.rb' end @@ -73,32 +75,31 @@ def test_creating_a_new_file launch_retest @command create_file 'foo.rb' - assert_match <<~EXPECTED, @output.read - 404 - Test File Not Found - Retest could not find a matching test file to run. + assert_output_matches <<~EXPECTED + FileNotFound - Retest could not find a matching test file to run. EXPECTED create_file 'foo_test.rb' - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" modify_file('program.rb') - assert_match "Test File Selected: program_test.rb", @output.read + assert_output_matches "Test file: program_test.rb" modify_file('foo.rb') - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" delete_file 'foo.rb' delete_file 'foo_test.rb' end def test_untracked_file - create_file 'foo.rb', should_sleep: false - create_file 'foo_test.rb', should_sleep: false + create_file 'foo.rb', sleep_for: 0 + create_file 'foo_test.rb', sleep_for: 0 launch_retest @command modify_file 'foo.rb' - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" delete_file 'foo.rb' delete_file 'foo_test.rb' diff --git a/features/ruby-bare/retest/scenarios/auto_flag.rb b/features/ruby-bare/retest/scenarios/auto_flag.rb index 3cf701d7..aaa2af37 100644 --- a/features/ruby-bare/retest/scenarios/auto_flag.rb +++ b/features/ruby-bare/retest/scenarios/auto_flag.rb @@ -1,4 +1,6 @@ class AutoFlag < Minitest::Test + include RetestHelper + def teardown end_retest end @@ -6,14 +8,15 @@ def teardown def test_start_retest launch_retest 'retest' - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Setup identified: [RUBY]. Using command: 'ruby ' + Watcher: [LISTEN] Launching Retest... Ready to refactor! You can make file changes now OUTPUT modify_file('program.rb') - assert_match "Test File Selected: program_test.rb", @output.read + assert_output_matches "Test file: program_test.rb" end end \ No newline at end of file diff --git a/features/ruby-bare/retest/scenarios/changed_and_test_placeholders.rb b/features/ruby-bare/retest/scenarios/changed_and_test_placeholders.rb index 1f3172b9..6a4e96ea 100644 --- a/features/ruby-bare/retest/scenarios/changed_and_test_placeholders.rb +++ b/features/ruby-bare/retest/scenarios/changed_and_test_placeholders.rb @@ -1,4 +1,6 @@ class ChangedAndTestPlaceholders < Minitest::Test + include RetestHelper + def setup @command = %Q{retest 'echo placeholders: and '} end @@ -10,17 +12,16 @@ def teardown def test_file_modification launch_retest @command - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Launching Retest... Ready to refactor! You can make file changes now OUTPUT modify_file('program.rb') - assert_match <<~EXPECTED, @output.read - Files Selected: - - changed: program.rb - - test: program_test.rb + assert_output_matches <<~EXPECTED + Changed file: program.rb + Test file: program_test.rb placeholders: program.rb and program_test.rb EXPECTED diff --git a/features/ruby-bare/retest/scenarios/changed_placeholder.rb b/features/ruby-bare/retest/scenarios/changed_placeholder.rb index f681e3a8..b40cf1c6 100644 --- a/features/ruby-bare/retest/scenarios/changed_placeholder.rb +++ b/features/ruby-bare/retest/scenarios/changed_placeholder.rb @@ -1,4 +1,6 @@ class ChangedPlaceholder < Minitest::Test + include RetestHelper + def setup @command = %Q{retest 'echo file modified: '} end @@ -10,15 +12,16 @@ def teardown def test_file_modification launch_retest @command - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Launching Retest... Ready to refactor! You can make file changes now OUTPUT modify_file('program.rb') - assert_match <<~EXPECTED, @output.read - Changed File Selected: program.rb + assert_output_matches <<~EXPECTED + Changed file: program.rb + file modified: program.rb EXPECTED end diff --git a/features/ruby-bare/retest/scenarios/custom_extensions.rb b/features/ruby-bare/retest/scenarios/custom_extensions.rb index 20063230..db0f42a0 100644 --- a/features/ruby-bare/retest/scenarios/custom_extensions.rb +++ b/features/ruby-bare/retest/scenarios/custom_extensions.rb @@ -1,6 +1,8 @@ class CustomExtensionTest < Minitest::Test + include RetestHelper + def setup - @command = %Q{retest "echo 'I captured a change'" --ext="\\.txt$"} + @command = %Q{retest "echo 'I captured a change'" --exts="txt"} end def teardown @@ -8,20 +10,20 @@ def teardown end def test_custom_extension - create_file 'foo.txt', should_sleep: false - create_file 'foo.rb', should_sleep: false - create_file 'foo_test.rb', should_sleep: false + create_file 'foo.txt', sleep_for: 0 + create_file 'foo.rb', sleep_for: 0 + create_file 'foo_test.rb', sleep_for: 0 launch_retest @command modify_file 'foo.rb' - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED modify_file 'foo.txt' - assert_match "I captured a change", @output.read + assert_output_matches "I captured a change" ensure delete_file 'foo.rb' diff --git a/features/ruby-bare/retest/scenarios/force_polling.rb b/features/ruby-bare/retest/scenarios/force_polling.rb index d2231775..14efc95f 100644 --- a/features/ruby-bare/retest/scenarios/force_polling.rb +++ b/features/ruby-bare/retest/scenarios/force_polling.rb @@ -1,4 +1,6 @@ class ForcePollingTest < Minitest::Test + include RetestHelper + def setup @command = 'retest --ruby --polling' end @@ -10,7 +12,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest with polling method... Ready to refactor! You can make file changes now EXPECTED @@ -21,8 +23,9 @@ def test_modifying_existing_file modify_file('program.rb') - assert_match "Test File Selected: program_test.rb", @output.read - assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + "Test file: program_test.rb", + "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips") end def test_modifying_existing_test_file @@ -30,8 +33,9 @@ def test_modifying_existing_test_file modify_file('program_test.rb') - assert_match "Test File Selected: program_test.rb", @output.read - assert_match "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips", @output.read + assert_output_matches( + "Test file: program_test.rb", + "1 runs, 1 assertions, 0 failures, 0 errors, 0 skips") end def test_creating_a_new_test_file @@ -39,7 +43,7 @@ def test_creating_a_new_test_file create_file 'foo_test.rb' - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo_test.rb' @@ -49,19 +53,16 @@ def test_creating_a_new_file launch_retest @command create_file 'foo.rb' - assert_match <<~EXPECTED, @output.read - 404 - Test File Not Found - Retest could not find a matching test file to run. - EXPECTED + assert_output_matches "FileNotFound - Retest could not find a matching test file to run." create_file 'foo_test.rb' - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" modify_file('program.rb') - assert_match "Test File Selected: program_test.rb", @output.read + assert_output_matches "Test file: program_test.rb" modify_file('foo.rb') - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo.rb' @@ -69,13 +70,13 @@ def test_creating_a_new_file end def test_untracked_file - create_file 'foo.rb', should_sleep: false - create_file 'foo_test.rb', should_sleep: false + create_file 'foo.rb', sleep_for: 0 + create_file 'foo_test.rb', sleep_for: 0 launch_retest @command modify_file 'foo.rb' - assert_match "Test File Selected: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo.rb' diff --git a/features/ruby-bare/retest/scenarios/interruptions.rb b/features/ruby-bare/retest/scenarios/interruptions.rb index 78a2e165..1f905fd5 100644 --- a/features/ruby-bare/retest/scenarios/interruptions.rb +++ b/features/ruby-bare/retest/scenarios/interruptions.rb @@ -1,4 +1,6 @@ class GracefulExitWhenInterrupting < Minitest::Test + include RetestHelper + def teardown end_retest end @@ -6,16 +8,13 @@ def teardown def test_interruption launch_retest 'retest --ruby' - assert_match <<~EXPECTED, @output.read + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED - Process.kill("INT", @pid) - wait + Process.kill("INT", @pid) if @pid - assert_match <<~EXPECTED, @output.read - Goodbye - EXPECTED + assert_output_matches "Goodbye" end end diff --git a/features/ruby-bare/retest/scenarios/multiple_commands.rb b/features/ruby-bare/retest/scenarios/multiple_commands.rb index ad0eeec0..a3f00033 100644 --- a/features/ruby-bare/retest/scenarios/multiple_commands.rb +++ b/features/ruby-bare/retest/scenarios/multiple_commands.rb @@ -1,4 +1,6 @@ class ChangedAndTestPlaceholders < Minitest::Test + include RetestHelper + def setup @command = %Q{retest 'echo files: and && echo hello world'} end @@ -10,17 +12,16 @@ def teardown def test_file_modification launch_retest @command - assert_match <<~OUTPUT, @output.read + assert_output_matches <<~OUTPUT Launching Retest... Ready to refactor! You can make file changes now OUTPUT modify_file('program.rb') - assert_match <<~EXPECTED, @output.read - Files Selected: - - changed: program.rb - - test: program_test.rb + assert_output_matches <<~EXPECTED + Changed file: program.rb + Test file: program_test.rb files: program.rb and program_test.rb hello world diff --git a/features/ruby-bare/retest/support/output_file.rb b/features/ruby-bare/retest/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/ruby-bare/retest/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/ruby-bare/retest/support/test_helper.rb b/features/ruby-bare/retest/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/ruby-bare/retest/support/test_helper.rb +++ b/features/ruby-bare/retest/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/features/support/output_file.rb b/features/support/output_file.rb deleted file mode 100644 index c58f646e..00000000 --- a/features/support/output_file.rb +++ /dev/null @@ -1,21 +0,0 @@ -class OutputFile - attr_reader :output - def initialize - @output = Tempfile.new - end - - def path - @output.path - end - - def read - @output.rewind - @output.read.split('').last - end - - def delete - @output.close - @output.unlink - end - alias :clear :delete -end diff --git a/features/support/test_helper.rb b/features/support/test_helper.rb index 2b41621f..57431346 100644 --- a/features/support/test_helper.rb +++ b/features/support/test_helper.rb @@ -1,59 +1,98 @@ -require_relative 'output_file' +# Can be updated to all feature repositories with +# $ bin/test/reset_helpers -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) +module RetestHelper + # COMMAND + def launch_retest(command, sleep_for: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + require 'open3' + @input, @output, @stderr, @wait_thr = Open3.popen3(command) + @pid = @wait_thr[:pid] + sleep sleep_for end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit + end + + # ASSERTIONS + def assert_output_matches(*expectations, max_retries: 5) + retries = 0 + wait_for = 0.1 + output = "" + begin + output += read_output + expectations.each { |expectation| assert_match(expectation, output) } + rescue Minitest::Assertion => e + raise e if retries >= max_retries + retries += 1 + sleep_seconds = wait_for ** -(wait_for * retries) + sleep sleep_seconds + retry + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable, EOFError + break + end + + if block_given? + yield result + else + result + end end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) + end + + # FILE CHANGES + def modify_file(path, sleep_for: default_sleep_seconds) return unless File.exist? path old_content = File.read(path) File.open(path, 'w') { |file| file.write old_content } - - sleep sleep_seconds + wait(sleep_for) end - def create_file(path, should_sleep: true, sleep_seconds: default_sleep_seconds) - File.open(path, "w").tap(&:close) - - sleep sleep_seconds if should_sleep + def create_file(path, content: "", sleep_for: default_sleep_seconds) + File.open(path, "w") { |f| f.write(content) } + wait(sleep_for) end - def delete_file(path) + def delete_file(path, sleep_for: 0) return unless File.exist? path File.delete path + wait(sleep_for) end - def rename_file(path, new_path) + def rename_file(path, new_path, sleep_for: 0) return unless File.exist? path File.rename path, new_path + wait(sleep_for) end -end -def launch_retest(command, sleep_seconds: launch_sleep_seconds) - @rd, @input = IO.pipe - @output = OutputFile.new - @pid = Process.spawn command, out: @output.path, in: @rd - sleep sleep_seconds -end + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + end + + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end -def end_retest(file = nil, pid = nil) - @output&.delete - @rd&.close - @input&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) + def wait(sleep_for = default_sleep_seconds) + sleep sleep_for end end diff --git a/lib/retest.rb b/lib/retest.rb index 5045bac2..91fb3b8c 100644 --- a/lib/retest.rb +++ b/lib/retest.rb @@ -4,7 +4,7 @@ require 'observer' require "retest/version" -require "retest/runners" +require "retest/runner" require "retest/repository" require "retest/matching_options" require "retest/options" @@ -15,15 +15,17 @@ require "retest/program" require "retest/prompt" require "retest/sounds" +require "retest/watcher" Listen.adapter_warn_behavior = :log module Retest class Error < StandardError; end + class FileNotFound < StandardError; end - def self.listen(options, listener: Listen) - listener.to('.', only: options.extension, relative: true, force_polling: options.force_polling?) do |modified, added, removed| + def self.listen(options, listener: Watcher::Default) + listener.watch(dir: '.', extensions: options.extensions, polling: options.force_polling?) do |modified, added, removed| yield modified, added, removed - end.start + end end end diff --git a/lib/retest/command.rb b/lib/retest/command.rb index 5131cadc..0269179c 100644 --- a/lib/retest/command.rb +++ b/lib/retest/command.rb @@ -1,3 +1,5 @@ +require_relative 'command/base' +require_relative 'command/hardcoded' require_relative 'command/rails' require_relative 'command/rake' require_relative 'command/rspec' @@ -7,12 +9,8 @@ module Retest class Command extend Forwardable - def self.for_options(options) - new(options: options).command - end - - def self.for_setup(setup) - new(setup: setup).command + def self.for_options(options, stdout: $stdout) + new(options: options, stdout: stdout).command end def_delegator :setup, :type @@ -29,17 +27,22 @@ def command options_command || default_command end - def options_command - return params[:command] if params[:command] + private - if params[:rspec] then rspec_command - elsif params[:rails] then rails_command - elsif params[:ruby] then ruby_command - elsif params[:rake] then rake_command - else + def options_command + if params[:command] then hardcoded_command(params[:command]) + elsif params[:rspec] then rspec_command + elsif params[:rails] then rails_command + elsif params[:ruby] then ruby_command + elsif params[:rake] then rake_command end end + def default_command + log "Setup identified: [#{type.upcase}]. Using command: '#{setup_command}'" + setup_command + end + def setup_command case type when :rake then rake_command @@ -50,12 +53,13 @@ def setup_command end end - def default_command - @stdout.puts "Setup identified: [#{type.upcase}]. Using command: '#{setup_command}'" - setup_command + def log(message) + @stdout&.puts(message) end - private + def hardcoded_command(command) + Hardcoded.new(command: command) + end def rspec_command Rspec.new(all: full_suite?) diff --git a/lib/retest/command/base.rb b/lib/retest/command/base.rb new file mode 100644 index 00000000..7309e3c0 --- /dev/null +++ b/lib/retest/command/base.rb @@ -0,0 +1,37 @@ +module Retest + class Command + class MultipleTestsNotSupported < StandardError; end + + class Base + def initialize(all: false, file_system: FileSystem, command: nil) + @file_system = file_system + @all = all + @command = command + end + + def clone(params = {}) + self.class.new(**{ all: all, file_system: file_system, command: command }.merge(params)) + end + + def has_changed? + to_s.include?('') + end + + def has_test? + to_s.include?('') + end + + def to_s + @command + end + + def format_batch(*files) + raise MultipleTestsNotSupported, "Multiple test files run not supported for '#{to_s}'" + end + + private + + attr_reader :all, :file_system, :command + end + end +end diff --git a/lib/retest/command/hardcoded.rb b/lib/retest/command/hardcoded.rb new file mode 100644 index 00000000..f4a44dfe --- /dev/null +++ b/lib/retest/command/hardcoded.rb @@ -0,0 +1,5 @@ +module Retest + class Command + class Hardcoded < Base; end + end +end diff --git a/lib/retest/command/rails.rb b/lib/retest/command/rails.rb index c754a5d5..c1b7aa42 100644 --- a/lib/retest/command/rails.rb +++ b/lib/retest/command/rails.rb @@ -1,16 +1,12 @@ module Retest class Command - class Rails - attr_reader :all, :file_system - - def initialize(all:, file_system: FileSystem) - @file_system = file_system - @all = all - end - + class Rails < Base def to_s - return "#{root_command} " unless all - root_command + if all + root_command + else + "#{root_command} " + end end def format_batch(*files) @@ -20,9 +16,11 @@ def format_batch(*files) private def root_command - return 'bin/rails test' if file_system.exist? 'bin/rails' - - 'bundle exec rails test' + if file_system.exist? 'bin/rails' + 'bin/rails test' + else + 'bundle exec rails test' + end end end end diff --git a/lib/retest/command/rake.rb b/lib/retest/command/rake.rb index 2d71fb10..9cb7330f 100644 --- a/lib/retest/command/rake.rb +++ b/lib/retest/command/rake.rb @@ -1,28 +1,26 @@ module Retest class Command - class Rake - attr_reader :all, :file_system - - def initialize(all:, file_system: FileSystem) - @file_system = file_system - @all = all - end - + class Rake < Base def to_s - return "#{root_command} TEST=" unless all - root_command + if all + root_command + else + "#{root_command} TEST=" + end end def format_batch(*files) - files.size > 1 ? "\"{#{files.join(',')}}\"" : files.first + files.size > 1 ? %Q{"{#{files.join(',')}}"} : files.first end private def root_command - return 'bin/rake test' if file_system.exist? 'bin/rake' - - 'bundle exec rake test' + if file_system.exist? 'bin/rake' + 'bin/rake test' + else + 'bundle exec rake test' + end end end end diff --git a/lib/retest/command/rspec.rb b/lib/retest/command/rspec.rb index cfd7eb80..cdbd39b1 100644 --- a/lib/retest/command/rspec.rb +++ b/lib/retest/command/rspec.rb @@ -1,16 +1,12 @@ module Retest class Command - class Rspec - attr_reader :all, :file_system - - def initialize(all:, file_system: FileSystem) - @file_system = file_system - @all = all - end - + class Rspec < Base def to_s - return "#{root_command} " unless all - root_command + if all + root_command + else + "#{root_command} " + end end def format_batch(*files) @@ -20,9 +16,11 @@ def format_batch(*files) private def root_command - return 'bin/rspec' if file_system.exist? 'bin/rspec' - - 'bundle exec rspec' + if file_system.exist? 'bin/rspec' + 'bin/rspec' + else + 'bundle exec rspec' + end end end end diff --git a/lib/retest/command/ruby.rb b/lib/retest/command/ruby.rb index 34fcb618..fdb26bca 100644 --- a/lib/retest/command/ruby.rb +++ b/lib/retest/command/ruby.rb @@ -1,17 +1,6 @@ module Retest class Command - class Ruby - attr_reader :all, :file_system - - def initialize(all:, file_system: FileSystem) - @file_system = file_system - @all = all - end - - def format_batch(*files) - %Q{-e "#{files.map { |file| "require './#{file}';" }.join}"} - end - + class Ruby < Base def to_s if file_system.exist? 'Gemfile.lock' 'bundle exec ruby ' @@ -19,6 +8,10 @@ def to_s 'ruby ' end end + + def format_batch(*files) + files.size > 1 ? %Q{-e "#{files.map { |file| "require './#{file}';" }.join}"} : files.first + end end end end \ No newline at end of file diff --git a/lib/retest/options.rb b/lib/retest/options.rb index ff890092..553e91c1 100644 --- a/lib/retest/options.rb +++ b/lib/retest/options.rb @@ -63,10 +63,19 @@ class Options long "--diff=git-branch" end - option :ext do - desc "Regex of file extensions to listen to" - long "--ext=regex" - default "\\.rb$" + option :exts do + desc "Comma separated of filenames extensions to filter to" + long "--exts=" + default "rb" + convert :list + end + + option :watcher do + desc "Tool used to watch file events" + permit %i[listen watchexec] + long "--watcher=" + short "-w" + convert :sym end flag :all do @@ -155,8 +164,16 @@ def force_polling? params[:polling] end - def extension - Regexp.new(params[:ext]) + def extensions + params[:exts] + end + + def watcher + params[:watcher] || :installed + end + + def merge(options = []) + self.class.new(@args.dup.concat(options)) end end end \ No newline at end of file diff --git a/lib/retest/program.rb b/lib/retest/program.rb index 655d4d61..c8218309 100644 --- a/lib/retest/program.rb +++ b/lib/retest/program.rb @@ -1,29 +1,66 @@ +require_relative 'program/pausable' +require_relative 'program/forced_selection' + module Retest class Program - attr_accessor :runner, :repository, :command - def initialize(runner: nil, repository: nil, command: nil) + extend Forwardable + include Pausable + include ForcedSelection + + attr_accessor :runner, :repository, :stdout + + def_delegators :runner, + :run_last_command, :last_command + + def initialize(runner: nil, repository: nil, stdout: $stdout) @runner = runner @repository = repository - @command = command + @stdout = stdout + initialize_pause(false) + initialize_forced_selection([]) end - def run(modified, added, removed) - repository.sync(added: added, removed: removed) - runner.sync(added: added, removed: removed) - system('clear 2>/dev/null') || system('cls 2>/dev/null') + def run(file, force_run: false) + if paused? && !force_run + @stdout.puts "Main program paused. Please resume program first." + return + end + + if forced_selection? + @stdout.puts <<~HINT + Forced selection enabled. + Reset to default settings by typing 'r' in the interactive console. + + HINT + + runner.run(test_files: selected_test_files) + return + end - runner.run (modified + added).first, repository: repository + test_file = if runner.has_test? + repository.find_test(file) + end + + runner.run changed_files: [file], test_files: [test_file] end def diff(branch) raise "Git not installed" unless VersionControl::Git.installed? + test_files = repository.find_tests VersionControl::Git.diff_files(branch) + run_selected(test_files) + end - puts "Tests found:" - test_files.each { |test_file| puts " - #{test_file}" } + def run_all + runner.run_all + end - puts "Running tests..." - runner.run_all_tests command.format_batch(*test_files) + def run_selected(test_files) + runner.run(test_files: test_files) + end + + def clear_terminal + system('clear 2>/dev/null') || system('cls 2>/dev/null') end end end diff --git a/lib/retest/program/forced_selection.rb b/lib/retest/program/forced_selection.rb new file mode 100644 index 00000000..ef42c84f --- /dev/null +++ b/lib/retest/program/forced_selection.rb @@ -0,0 +1,19 @@ +module ForcedSelection + attr_reader :selected_test_files + + def initialize_forced_selection(value = []) + @selected_test_files = value + end + + def forced_selection? + !@selected_test_files.empty? + end + + def reset_selection + @selected_test_files = [] + end + + def force_selection(test_files) + @selected_test_files = Array(test_files) + end +end \ No newline at end of file diff --git a/lib/retest/program/pausable.rb b/lib/retest/program/pausable.rb new file mode 100644 index 00000000..c26e5afa --- /dev/null +++ b/lib/retest/program/pausable.rb @@ -0,0 +1,21 @@ +module Pausable + def initialize_pause(value = false) + @paused = value + end + + def pause + @paused = true + end + + def paused? + @paused + end + + def resume + @paused = false + end + + def running_state + @paused + end +end \ No newline at end of file diff --git a/lib/retest/prompt.rb b/lib/retest/prompt.rb index fc7b15e9..fe2879b1 100644 --- a/lib/retest/prompt.rb +++ b/lib/retest/prompt.rb @@ -9,27 +9,21 @@ def initialize(input: nil, output: nil) @question_asked = false end - def question_asked? - @question_asked - end - def ask_which_test_to_use(path, files) - ask_question do - changed - notify_observers(:question) - options = options(files) + changed + notify_observers(:question) + options = options(files) - output.puts(<<~QUESTION) - We found few tests matching: #{path} + output.puts(<<~QUESTION) + We found few tests matching: #{path} - #{list_options(options.keys)} + #{list_options(options.keys)} - Which file do you want to use? - Enter the file number now: - QUESTION - output.print("> ") - options.values[input.gets.chomp.to_i] - end + Which file do you want to use? + Enter the file number now: + QUESTION + output.print("> ") + options.values[input.gets.to_s.chomp.to_i] end def puts(*args) @@ -40,14 +34,6 @@ def read_output output.tap(&:rewind).read end - def ask_question - old_question_asked = @question_asked - @question_asked = true - yield - ensure - @question_asked = old_question_asked - end - private def options(files, blank_option: 'none') diff --git a/lib/retest/repository.rb b/lib/retest/repository.rb index 494f5b18..dd1d204b 100644 --- a/lib/retest/repository.rb +++ b/lib/retest/repository.rb @@ -31,6 +31,10 @@ def find_tests(paths) .sort end + def test_files + files.select { |file| MatchingOptions::Path.new(file).test? } + end + def sync(added:, removed:) add(added) remove(removed) diff --git a/lib/retest/runner.rb b/lib/retest/runner.rb new file mode 100644 index 00000000..c36990b8 --- /dev/null +++ b/lib/retest/runner.rb @@ -0,0 +1,97 @@ +require_relative "runner/cached_test_file" + +module Retest + class Runner + extend Forwardable + include Observable + include CachedTestFile + + def_delegators :command, :has_changed?, :has_test? + + attr_accessor :command, :stdout, :last_command + def initialize(command, stdout: $stdout) + @stdout = stdout + @command = command + end + + def run_last_command + system_run last_command + end + + def run(changed_files: [], test_files: []) + self.last_command = format_instruction(changed_files: changed_files, test_files: test_files) + system_run last_command + rescue FileNotFound => e + log("FileNotFound - #{e.message}") + rescue Command::MultipleTestsNotSupported => e + log("Command::MultipleTestsNotSupported - #{e.message}") + end + + def run_all + self.last_command = command.clone(all: true).to_s + system_run last_command + end + + def format_instruction(changed_files: [], test_files: []) + if changed_files.empty? && test_files.size >= 1 + instruction = command.clone(all: false).to_s + tests_string = command.format_batch(*test_files) + log("Tests selected:") + test_files.each { |test_file| log(" - #{test_file}") } + return instruction.gsub('', tests_string) + end + + instruction = command.to_s + instruction = format_changed_files(instruction: instruction, files: changed_files) + instruction = format_test_files(instruction: instruction, files: test_files) + end + + def format_test_files(instruction:, files:) + return instruction unless has_test? + + self.cached_test_file = files.first + + if cached_test_file.nil? + raise FileNotFound, "Retest could not find a matching test file to run." + end + + log("Test file: #{cached_test_file}") + instruction.gsub('', cached_test_file) + end + + def format_changed_files(instruction:, files:) + return instruction unless has_changed? + changed_file = files.first + + if changed_file.nil? + raise FileNotFound, "Retest could not find a changed file to run." + end + + log("Changed file: #{changed_file}") + instruction.gsub('', changed_file) + end + + def sync(added:, removed:) + purge_test_file(removed) + end + + private + + def print_test_file_not_found + log(<<~ERROR) + FileNotFound - Retest could not find a matching test file to run. + ERROR + end + + def system_run(command) + log("\n") + result = system(command) ? :tests_pass : :tests_fail + changed + notify_observers(result) + end + + def log(message) + stdout.puts(message) + end + end +end diff --git a/lib/retest/runner/cached_test_file.rb b/lib/retest/runner/cached_test_file.rb new file mode 100644 index 00000000..b2a6f909 --- /dev/null +++ b/lib/retest/runner/cached_test_file.rb @@ -0,0 +1,19 @@ +module Retest + module CachedTestFile + attr_reader :cached_test_file + + def cached_test_file=(value) + @cached_test_file = value || cached_test_file + end + + def purge_test_file(purged) + return if purged.empty? + + if purged.is_a?(Array) && purged.include?(cached_test_file) + @cached_test_file = nil + elsif purged.is_a?(String) && purged == cached_test_file + @cached_test_file = nil + end + end + end +end diff --git a/lib/retest/runners.rb b/lib/retest/runners.rb deleted file mode 100644 index 14cefc71..00000000 --- a/lib/retest/runners.rb +++ /dev/null @@ -1,21 +0,0 @@ -require_relative 'runners/runner' -require_relative 'runners/test_runner' -require_relative 'runners/change_runner' -require_relative 'runners/variable_runner' - -module Retest - module Runners - module_function - - def runner_for(command) - for_test = command.include?('') - for_change = command.include?('') - - if for_test && for_change then VariableRunner - elsif for_test then TestRunner - elsif for_change then ChangeRunner - else Runner - end.new command - end - end -end \ No newline at end of file diff --git a/lib/retest/runners/cached_test_file.rb b/lib/retest/runners/cached_test_file.rb deleted file mode 100644 index 5002a8ce..00000000 --- a/lib/retest/runners/cached_test_file.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Retest - module Runners - module CachedTestFile - def cached_test_file - @cached_test_file - end - - def cached_test_file=(value) - @cached_test_file = value || @cached_test_file - end - - def purge_test_file(purged) - return if purged.empty? - - if purged.is_a?(Array) && purged.include?(cached_test_file) - @cached_test_file = nil - elsif purged.is_a?(String) && purged == cached_test_file - @cached_test_file = nil - end - end - end - end -end diff --git a/lib/retest/runners/change_runner.rb b/lib/retest/runners/change_runner.rb deleted file mode 100644 index d7a110e3..00000000 --- a/lib/retest/runners/change_runner.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Retest - module Runners - class ChangeRunner < Runner - def run(changed_file = nil, repository: nil) - return print_file_not_found unless changed_file - - log("Changed File Selected: #{changed_file}") - system_run command.gsub('', changed_file) - end - - private - - def print_file_not_found - log(<<~ERROR) - 404 - File Not Found - Retest could not find a changed file to run. - ERROR - end - end - end -end \ No newline at end of file diff --git a/lib/retest/runners/runner.rb b/lib/retest/runners/runner.rb deleted file mode 100644 index 6dfb4b55..00000000 --- a/lib/retest/runners/runner.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Retest - module Runners - class Runner - include Observable - - attr_accessor :command, :stdout - def initialize(command, stdout: $stdout) - @stdout = stdout - @command = command - end - - def ==(obj) - command == obj.command && obj.class == self.class - end - - def run(changed_file = nil, repository: nil) - system_run command - end - - def run_all_tests(tests_string) - log("Test File Selected: #{tests_string}") - system_run command.gsub('', tests_string) - end - - def sync(added:, removed:) - end - - private - - def system_run(command) - result = system(command) ? :tests_pass : :tests_fail - changed - notify_observers(result) - end - - def log(message) - stdout.puts(message) - end - end - end -end diff --git a/lib/retest/runners/test_runner.rb b/lib/retest/runners/test_runner.rb deleted file mode 100644 index 03283c71..00000000 --- a/lib/retest/runners/test_runner.rb +++ /dev/null @@ -1,31 +0,0 @@ -require_relative "cached_test_file" - -module Retest - module Runners - class TestRunner < Runner - include CachedTestFile - - def run(changed_file, repository:) - self.cached_test_file = repository.find_test(changed_file) - - return print_file_not_found unless cached_test_file - - log("Test File Selected: #{cached_test_file}") - system_run command.gsub('', cached_test_file) - end - - def sync(added:, removed:) - purge_test_file(removed) - end - - private - - def print_file_not_found - log(<<~ERROR) - 404 - Test File Not Found - Retest could not find a matching test file to run. - ERROR - end - end - end -end diff --git a/lib/retest/runners/variable_runner.rb b/lib/retest/runners/variable_runner.rb deleted file mode 100644 index dd4b9029..00000000 --- a/lib/retest/runners/variable_runner.rb +++ /dev/null @@ -1,39 +0,0 @@ -require_relative "cached_test_file" - -module Retest - module Runners - class VariableRunner < Runner - include CachedTestFile - - def run(changed_file, repository:) - self.cached_test_file = repository.find_test(changed_file) - - return print_file_not_found unless cached_test_file - - log(<<~FILES) - Files Selected: - - changed: #{changed_file} - - test: #{cached_test_file} - - FILES - - system_run command - .gsub('', cached_test_file) - .gsub('', changed_file) - end - - def sync(added:, removed:) - purge_test_file(removed) - end - - private - - def print_file_not_found - log(<<~ERROR) - 404 - Test File Not Found - Retest could not find a matching test file to run. - ERROR - end - end - end -end diff --git a/lib/retest/setup.rb b/lib/retest/setup.rb index 62d6d73d..85737997 100644 --- a/lib/retest/setup.rb +++ b/lib/retest/setup.rb @@ -4,7 +4,7 @@ def type @type ||= begin return :ruby unless has_lock_file? - if rspec? then :rspec + if rspec? then :rspec elsif rails? then :rails elsif rake? then :rake else :ruby diff --git a/lib/retest/version.rb b/lib/retest/version.rb index 074554d2..cfb05244 100644 --- a/lib/retest/version.rb +++ b/lib/retest/version.rb @@ -1,3 +1,3 @@ module Retest - VERSION = "1.13.2" + VERSION = "2.0.0.pre5" end diff --git a/lib/retest/version_control.rb b/lib/retest/version_control.rb index ae0abafd..df48272c 100644 --- a/lib/retest/version_control.rb +++ b/lib/retest/version_control.rb @@ -6,8 +6,8 @@ module VersionControl module_function - def files - [Git, NoVersionControl].find(&:installed?).files + def files(extensions: []) + [Git, NoVersionControl].find(&:installed?).files(extensions: extensions) end end end \ No newline at end of file diff --git a/lib/retest/version_control/git.rb b/lib/retest/version_control/git.rb index 7c6f6590..5aaa32d3 100644 --- a/lib/retest/version_control/git.rb +++ b/lib/retest/version_control/git.rb @@ -12,8 +12,12 @@ def name 'git' end - def files - (untracked_files + tracked_files).sort + def files(extensions: []) + result = (untracked_files + tracked_files).sort + unless extensions.empty? + result.select! { |file| /\.(?:#{extensions.join('|')})$/.match?(file) } + end + result end def diff_files(branch) diff --git a/lib/retest/version_control/no_version_control.rb b/lib/retest/version_control/no_version_control.rb index 58e9a9e8..618cae76 100644 --- a/lib/retest/version_control/no_version_control.rb +++ b/lib/retest/version_control/no_version_control.rb @@ -12,8 +12,14 @@ def name 'default' end - def files - Dir.glob('**/*') - Dir.glob('{tmp,node_modules}/**/*') + def files(extensions: []) + result = if extensions.empty? + Dir.glob('**/*') + else + Dir.glob("**/*.{#{extensions.join(',')}}") + end + + result - Dir.glob('{tmp,node_modules}/**/*') end end end diff --git a/lib/retest/watcher.rb b/lib/retest/watcher.rb new file mode 100644 index 00000000..95a923d6 --- /dev/null +++ b/lib/retest/watcher.rb @@ -0,0 +1,122 @@ +module Retest + module Watcher + def self.for(watcher) + tool = case watcher.to_s + when 'listen' then Default + when 'watchexec' then Watchexec + when '', 'installed' then installed + else raise ArgumentError, "Unknown #{watcher}" + end + + unless tool.installed? + raise ArgumentError, "#{watcher} not installed on machine" + end + + tool + end + + def self.installed + [Watchexec, Default].find(&:installed?) + end + + module Default + def self.installed? + true + end + + def self.watch(dir:, extensions:, polling: false) + Listen.to(dir, only: extensions_regex(extensions), relative: true, polling: polling) do |modified, added, removed| + yield modified, added, removed + end.start + end + + def self.extensions_regex(extensions) + Regexp.new("\\.(?:#{extensions.join("|")})$") + end + end + + module Watchexec + def self.installed? + system "watchexec --version > /dev/null 2>&1" + end + + def self.watch(dir:, extensions:, polling: false) + command = "watchexec --exts #{extensions.join(',')} -w #{dir} --emit-events-to stdio --no-meta --only-emit-events" + files = VersionControl.files(extensions: extensions).zip([]).to_h + + watch_rd, watch_wr = IO.pipe + pid = Process.spawn(command, out: watch_wr) + at_exit do + Process.kill("TERM", pid) if pid + watch_rd.close + watch_wr.close + end + + Thread.new do + loop do + ready = IO.select([watch_rd]) + readable_connections = ready[0] + readable_connections.each do |conn| + data = conn.readpartial(4096) + change = /^(?:create|remove|rename|modify):(?.*)/.match(data.strip) + + next unless change + + path = Pathname(change[:path]).relative_path_from(Dir.pwd).to_s + file_exist = File.exist?(path) + file_cached = files.key?(path) + + modified, added, removed = result = [[], [], []] + if file_exist && file_cached + modified << path + elsif file_exist && !file_cached + added << path + files[path] = nil + elsif !file_exist && file_cached + removed << path + files.delete(path) + end + + yield result + end + end + end + + # require 'open3' + # Thread.new do + # files = VersionControl.files(extensions: extensions).zip([]).to_h + + # Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| + # loop do + # ready = IO.select([stdout]) + # readable_connections = ready[0] + # readable_connections.each do |conn| + # data = conn.readpartial(4096) + # change = /^(?:create|remove|rename|modify):(?.*)/.match(data.strip) + + # next unless change + + # path = Pathname(change[:path]).relative_path_from(Dir.pwd).to_s + # file_exist = File.exist?(path) + # file_cached = files.key?(path) + + # modified, added, removed = result = [[], [], []] + # if file_exist && file_cached + # modified << path + # elsif file_exist && !file_cached + # added << path + # files[path] = nil + # elsif !file_exist && file_cached + # removed << path + # files.delete(path) + # end + + # yield result + # end + # end + # end + # end + end + end + end +end \ No newline at end of file diff --git a/retest.gemspec b/retest.gemspec index acb70e90..529e9931 100644 --- a/retest.gemspec +++ b/retest.gemspec @@ -28,5 +28,6 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "string-similarity", ["~> 2.1"] spec.add_runtime_dependency "listen", ["~> 3.9"] spec.add_runtime_dependency "tty-option", ["~> 0.1"] + spec.add_runtime_dependency "tty-prompt", ["~> 0.1"] spec.add_runtime_dependency "observer", ["~> 0.1"] end diff --git a/test/retest/command/command_interface.rb b/test/retest/command/command_interface.rb index 6eaef6cb..a6d9448e 100644 --- a/test/retest/command/command_interface.rb +++ b/test/retest/command/command_interface.rb @@ -4,6 +4,13 @@ module CommandInterface def test_interface assert_respond_to @subject, :format_batch assert_respond_to @subject, :to_s + assert_respond_to @subject, :has_test? + assert_respond_to @subject, :has_changed? + end + + def test_initializatioin + @subject.class.new + @subject.class.new(all: '', file_system: '', command: '') end end end diff --git a/test/retest/command/hardcoded_test.rb b/test/retest/command/hardcoded_test.rb new file mode 100644 index 00000000..2a2b9d47 --- /dev/null +++ b/test/retest/command/hardcoded_test.rb @@ -0,0 +1,54 @@ +require 'test_helper' +require_relative 'command_interface' + +module Retest + class Command + class HardcodedTest < MiniTest::Test + include CommandInterface + + def setup + @subject = Hardcoded.new(command: 'echo "hello world"') + end + + def test_a_hardcoded_command_status + command = Hardcoded.new(command: 'echo "hello world"') + refute command.has_test? + refute command.has_changed? + end + + def test_a_test_and_changed_command_status + command = Hardcoded.new(command: 'echo & ') + assert command.has_test? + assert command.has_changed? + end + + def test_a_test_command_status + command = Hardcoded.new(command: 'echo ') + assert command.has_test? + refute command.has_changed? + end + + def test_a_changed_command_status + command = Hardcoded.new(command: 'echo ') + refute command.has_test? + assert command.has_changed? + end + + def test_to_s + assert_equal 'echo "hello world"', @subject.to_s + end + + def test_format_with_one_file + assert_raises(Command::MultipleTestsNotSupported) do + @subject.format_batch('a/file/path.rb') + end + end + + def test_format_with_multiple_files + assert_raises(Command::MultipleTestsNotSupported) do + @subject.format_batch('a/file/path.rb', 'another/file/path.rb') + end + end + end + end +end \ No newline at end of file diff --git a/test/retest/command/ruby_test.rb b/test/retest/command/ruby_test.rb index adc0eb14..b0811ace 100644 --- a/test/retest/command/ruby_test.rb +++ b/test/retest/command/ruby_test.rb @@ -23,7 +23,7 @@ def test_to_s end def test_format_with_one_file - assert_equal %Q{-e "require './a/file/path.rb';"}, @subject.format_batch('a/file/path.rb') + assert_equal 'a/file/path.rb', @subject.format_batch('a/file/path.rb') end def test_format_with_multiple_files diff --git a/test/retest/options/help.txt b/test/retest/options/help.txt index 8a47eb7e..0a43a4d4 100644 --- a/test/retest/options/help.txt +++ b/test/retest/options/help.txt @@ -9,21 +9,23 @@ Arguments: Options: - --all Run all the specs of a specificied ruby setup - --diff=git-branch Pipes all matching tests from diffed branch to test - command - --ext=regex Regex of file extensions to listen to (default - "\\.rb$") - -h, --help Print usage - --notify Play a sound when specs pass or fail (macOS only) - --polling Use polling method when listening to file changes - Some filesystems won't work without it - VM/Vagrant Shared folders, NFS, Samba, sshfs... - --rails Shortcut for a standard Rails setup - --rake Shortcut for a standard Rake setup - --rspec Shortcut for a standard RSpec setup - --ruby Shortcut for a Ruby project - -v, --version Print retest version + --all Run all the specs of a specificied ruby setup + --diff=git-branch Pipes all matching tests from diffed branch to test + command + --exts= Comma separated of filenames extensions to filter + to (default "rb") + -h, --help Print usage + --notify Play a sound when specs pass or fail (macOS only) + --polling Use polling method when listening to file changes + Some filesystems won't work without it + VM/Vagrant Shared folders, NFS, Samba, sshfs... + --rails Shortcut for a standard Rails setup + --rake Shortcut for a standard Rake setup + --rspec Shortcut for a standard RSpec setup + --ruby Shortcut for a Ruby project + -v, --version Print retest version + -w, --watcher= Tool used to watch file events (permitted: listen, + watchexec) Examples: Runs a matching rails test after a file change diff --git a/test/retest/options_test.rb b/test/retest/options_test.rb index c21315d0..76181bb9 100644 --- a/test/retest/options_test.rb +++ b/test/retest/options_test.rb @@ -48,5 +48,44 @@ def test_notify? @subject.args = ["--notify"] assert @subject.notify? end + + def test_extensions + @subject.args = ["--exts rb"] + + assert_equal %w[rb], @subject.extensions + + @subject.args = ["--exts rb,js, html,css ,ts"] + + assert_equal %w[rb js html css ts], @subject.extensions + end + + def test_all_version_copy + @subject.args = %w[--notify --rake] + + copy = @subject.merge(%w[--all]) + + assert_equal %w[--notify --rake --all], copy.args + refute_equal copy.object_id, @subject.object_id + end + + def test_listener + @subject.args = %w[--watcher=watchexec] + assert_equal :watchexec, @subject.watcher + + @subject.args = %w[-w watchexec] + assert_equal :watchexec, @subject.watcher + + @subject.args = %w[--watcher=listen] + assert_equal :listen, @subject.watcher + + @subject.args = %w[-w listen] + assert_equal :listen, @subject.watcher + + @subject.args = %w[-w hello] + assert_equal :installed, @subject.watcher + + @subject.args = %w[] # default when no listeners are install by default + assert_equal :installed, @subject.watcher + end end end \ No newline at end of file diff --git a/test/retest/program_test.rb b/test/retest/program_test.rb index cb5ebb9c..138bcf7a 100644 --- a/test/retest/program_test.rb +++ b/test/retest/program_test.rb @@ -1,10 +1,42 @@ require 'test_helper' -# TODO : write missing tests module Retest class ProgramTest < MiniTest::Test - def setup - @subject = Program.new + class PauseTest < Minitest::Test + class RaisingRepository + class NotToBeCalledError < StandardError; end + def find_test(_) + raise NotToBeCalledError + end + end + + def setup + @subject = Program.new(repository: Repository.new, stdout: StringIO.new) + end + + def test_paused? + refute @subject.paused? + + @subject.pause + assert @subject.paused? + + @subject.resume + refute @subject.paused? + end + + def test_no_run_when_paused + @subject.runner = RaisingRunner.new + @subject.pause + @subject.run('file_path') + end + + def test_run_not_trigger_repository_find_test_when_hardcoded + @runner = Runner.new(Command::Hardcoded.new(command: 'echo ')) + @repository = RaisingRepository.new + @subject = Program.new(runner: @runner, repository: @repository) + + capture_subprocess_io { @subject.run('path.rb') } + end end end end diff --git a/test/retest/prompt_test.rb b/test/retest/prompt_test.rb index e1446473..2d6d3eb4 100644 --- a/test/retest/prompt_test.rb +++ b/test/retest/prompt_test.rb @@ -49,34 +49,6 @@ def test_ask_which_test_to_use EXPECTED end - def test_question_asked_when_asking_question - files = %w( - test/models/taxation/holdings_test.rb - test/models/schedule/holdings_test.rb - test/models/holdings_test.rb - test/models/performance/holdings_test.rb - test/lib/csv_report/holdings_test.rb - ) - - rd, wr = IO.pipe - - @subject.input = rd - - th = Thread.new do - @subject.ask_which_test_to_use("app/models/valuation/holdings.rb", files) - end - - wait_until { assert @subject.question_asked? } - - wr.puts("1\n") - - assert_equal "test/models/schedule/holdings_test.rb", th.value - refute @subject.question_asked? - ensure - rd.close - wr.close - end - def test_read_output @subject.output.puts "hello world\n" @@ -102,17 +74,5 @@ def test_observers_receive_correct_update_on_ask_which_test_to_use assert_includes observer.notepad, MethodCall.new(name: :update, args: [:question]) end - - def test_ask_question - refute @subject.question_asked? - @subject.ask_question do - assert @subject.question_asked? - end - refute @subject.question_asked? - end - - def test_question_flag_when_asking_for_file - - end end end diff --git a/test/retest/repository_test.rb b/test/retest/repository_test.rb index 81ffa2ab..37495598 100644 --- a/test/retest/repository_test.rb +++ b/test/retest/repository_test.rb @@ -1,3 +1,4 @@ + require 'test_helper' require_relative 'repository/multiple_test_files_with_user_input.rb' @@ -143,7 +144,7 @@ def test_find_test_similar_files_but_no_exact_match Which file do you want to use? Enter the file number now: - > + >\s EXPECTED end @@ -192,5 +193,58 @@ def test_find_test_return_changed_file assert_equal expected, @subject.find_test(file_changed) end end + + class TestTestFiles < Minitest::Test + def setup + @subject = Repository.new + end + + def test_returns_test_files_only + @subject.files = %w( + exe/retest + lib/retest.rb + lib/bottles.rb + lib/glasses.rb + lib/pints.rb + test/bottles_test.rb + test/glasses_test.rb + test/plates_test.rb + test/test_bottles_test.rb + test/test_glasses_test.rb + test/test_plates_test.rb + spec/bottles_spec.rb + spec/glasses_spec.rb + spec/plates_spec.rb + bottles_spec.rb + glasses_spec.rb + plates_spec.rb + bottles_test.rb + glasses_test.rb + plates_test.rb + program.rb + README.md + Gemfile + Gemfile.lock + ) + + assert_equal %w[ + test/bottles_test.rb + test/glasses_test.rb + test/plates_test.rb + test/test_bottles_test.rb + test/test_glasses_test.rb + test/test_plates_test.rb + spec/bottles_spec.rb + spec/glasses_spec.rb + spec/plates_spec.rb + bottles_spec.rb + glasses_spec.rb + plates_spec.rb + bottles_test.rb + glasses_test.rb + plates_test.rb + ], @subject.test_files + end + end end end \ No newline at end of file diff --git a/test/retest/runners/observable_runner.rb b/test/retest/runner/observable_runner.rb similarity index 97% rename from test/retest/runners/observable_runner.rb rename to test/retest/runner/observable_runner.rb index 1d356dc7..870e9b83 100644 --- a/test/retest/runners/observable_runner.rb +++ b/test/retest/runner/observable_runner.rb @@ -1,5 +1,5 @@ module Retest - module Runners + class Runner module OversableRunnerTests def test_publishes_event_after_running_command observer = MiniTest::Mock.new diff --git a/test/retest/runner/runner_interface.rb b/test/retest/runner/runner_interface.rb new file mode 100644 index 00000000..26139c1a --- /dev/null +++ b/test/retest/runner/runner_interface.rb @@ -0,0 +1,15 @@ +module Retest + class Runner + module RunnerInterfaceTest + def test_behaviour + assert_respond_to @subject, :== + assert_respond_to @subject, :run + assert_respond_to @subject, :sync + end + + def test_run_accepts_the_right_parameter + _, _ = capture_subprocess_io { @subject.run changed_files: ['some-path.rb'], test_files: ['some-test-path.rb'] } + end + end + end +end diff --git a/test/retest/runner_test.rb b/test/retest/runner_test.rb new file mode 100644 index 00000000..e67757c8 --- /dev/null +++ b/test/retest/runner_test.rb @@ -0,0 +1,174 @@ +require 'test_helper' +require_relative 'runner/runner_interface' +require_relative 'runner/observable_runner' + +module Retest + class Runner + class RunnerInterfaceTests < MiniTest::Test + def setup + @command = Command::Hardcoded.new(command: "echo 'hello world'") + @subject = Runner.new(@command) + end + + include RunnerInterfaceTest + include OversableRunnerTests + + private + + def observable_act(subject) + subject.run + end + end + + class RunnerTest < MiniTest::Test + def setup + @command = Command::Hardcoded.new(command: "echo 'hello world'") + @subject = Runner.new(@command) + end + + def test_run + out, _ = capture_subprocess_io { @subject.run(changed_files: ['file_path.rb']) } + + assert_match "hello world", out + + out, _ = capture_subprocess_io { @subject.run } + + assert_match "hello world", out + end + + def test_sync_files + @subject.cached_test_file = 'file_path_test.rb' + + @subject.sync(added: [], removed:['something.rb']) + assert_equal 'file_path_test.rb', @subject.cached_test_file + + @subject.sync(added: nil, removed:'something.rb') + assert_equal 'file_path_test.rb', @subject.cached_test_file + + @subject.sync(added: ['a.rb'], removed:['file_path_test.rb']) + assert_nil @subject.cached_test_file + + @subject.cached_test_file = 'file_path_test.rb' + @subject.sync(added: 'a.rb', removed:'file_path_test.rb') + assert_nil @subject.cached_test_file + end + end + + class VariableRunnerTest < MiniTest::Test + def setup + @command = Command::Hardcoded.new(command: "echo ' & '") + @subject = Runner.new(@command, stdout: StringIO.new) + end + + def output + @subject.stdout.string + end + + def test_files_selected_ouptut + _, _ = capture_subprocess_io { @subject.run(changed_files: ['file_path.rb'], test_files: ['file_path_test.rb']) } + + assert_equal(<<~EXPECTED, output) + Changed file: file_path.rb + Test file: file_path_test.rb + + EXPECTED + end + + def test_run_with_no_match + _, _ = capture_subprocess_io { @subject.run(changed_files: ['another_file_path.rb'], test_files: [nil]) } + + assert_equal(<<~EXPECTED, output) + Changed file: another_file_path.rb + FileNotFound - Retest could not find a matching test file to run. + EXPECTED + end + + def test_run_with_a_file_found + out, _ = capture_subprocess_io { @subject.run(changed_files: ['file_path.rb'], test_files: ['file_path_test.rb']) } + + assert_match "file_path.rb & file_path_test.rb", out + end + + def test_returns_last_command + out, _ = capture_subprocess_io { @subject.run(changed_files: ['file_path.rb'], test_files: ['file_path_test.rb']) } + + assert_match "file_path.rb & file_path_test.rb", out + + out, _ = capture_subprocess_io { @subject.run(changed_files: ['another_file_path.rb'], test_files: ['file_path_test.rb']) } + + assert_match "another_file_path.rb & file_path_test.rb", out + end + end + + class ChangeRunnerTest < MiniTest::Test + def setup + @command = Command::Hardcoded.new(command: "echo ''") + @subject = Runner.new(@command, stdout: StringIO.new) + end + + def output + @subject.stdout.string + end + + def test_run_with_no_file_found + @subject.run + + assert_equal(<<~EXPECTED, output) + FileNotFound - Retest could not find a changed file to run. + EXPECTED + end + + def test_run_with_a_file_found + out, _ = capture_subprocess_io { @subject.run(changed_files: ['file_path.rb']) } + + assert_match "file_path.rb", out + end + end + + class TestRunnerTest < MiniTest::Test + def setup + @command = Command::Hardcoded.new(command: "echo 'touch '") + @subject = Runner.new(@command, stdout: StringIO.new) + end + + def output + @subject.stdout.string + end + + def test_run_with_no_file_found + _, _ = capture_subprocess_io { @subject.run(changed_files: [nil], test_files: [nil]) } + + assert_equal(<<~EXPECTED, output) + FileNotFound - Retest could not find a matching test file to run. + EXPECTED + end + + def test_run_with_a_file_found + out, _ = capture_subprocess_io { @subject.run(changed_files: ['file_path.rb'], test_files: ['file_path_test.rb']) } + + assert_match "touch file_path_test.rb", out + end + + def test_returns_last_command + out, _ = capture_subprocess_io { @subject.run(changed_files: ['file_path.rb'], test_files: ['file_path_test.rb']) } + + assert_match "touch file_path_test.rb", out + + out, _ = capture_subprocess_io { @subject.run(changed_files: ['some-weird-path.rb'], test_files: ['file_path_test.rb']) } + + assert_match "touch file_path_test.rb", out + end + + def test_run_multiple_tests + assert_raises(Command::MultipleTestsNotSupported) do + @subject.command = @command + @subject.format_instruction(test_files: ['file_path.rb', 'file_path_two.rb']) + end + + @subject.command = Command::Rails.new(all: false) + instruction = @subject.format_instruction(test_files: ['file_path.rb', 'file_path_two.rb']) + assert_equal 'bundle exec rails test file_path.rb file_path_two.rb', instruction + end + end + end +end \ No newline at end of file diff --git a/test/retest/runners/change_runner_test.rb b/test/retest/runners/change_runner_test.rb deleted file mode 100644 index b6105ae2..00000000 --- a/test/retest/runners/change_runner_test.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'test_helper' -require_relative 'runner_interface' -require_relative 'observable_runner' - -module Retest - module Runners - class ChangeRunnerInterfaceTests < MiniTest::Test - def setup - @subject = ChangeRunner.new("echo 'touch '") - end - - include RunnerInterfaceTest - include OversableRunnerTests - - private - - def observable_act(subject) - subject.run('file_path.rb') - end - end - - class ChangeRunnerTest < MiniTest::Test - def setup - @subject = ChangeRunner.new("echo 'touch '", stdout: StringIO.new) - end - - def output - @subject.stdout.string - end - - def test_run_with_no_file_found - _, _ = capture_subprocess_io { @subject.run } - - assert_equal(<<~EXPECTED, output) - 404 - File Not Found - Retest could not find a changed file to run. - EXPECTED - end - - def test_run_with_a_file_found - out, _ = capture_subprocess_io { @subject.run('file_path.rb') } - - assert_match "touch file_path.rb", out - end - end - end -end \ No newline at end of file diff --git a/test/retest/runners/runner_interface.rb b/test/retest/runners/runner_interface.rb deleted file mode 100644 index d4162caa..00000000 --- a/test/retest/runners/runner_interface.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Retest - module Runners - module RunnerInterfaceTest - def test_behaviour - assert_respond_to @subject, :== - assert_respond_to @subject, :run - assert_respond_to @subject, :run_all_tests - assert_respond_to @subject, :sync - end - - def test_run_accepts_the_right_parameter - _, _ = capture_subprocess_io { @subject.run 'some-path.rb', repository: Repository.new } - end - - def test_equal - runner1 = @subject.class.new('hello') - runner2 = @subject.class.new('hello') - runner3 = @subject.class.new('world') - - assert_equal runner1, runner2 - refute_equal runner1, runner3 - end - end - end -end diff --git a/test/retest/runners/runner_test.rb b/test/retest/runners/runner_test.rb deleted file mode 100644 index 64b36378..00000000 --- a/test/retest/runners/runner_test.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'test_helper' -require_relative 'runner_interface' -require_relative 'observable_runner' - -module Retest - module Runners - class RunnerInterfaceTests < MiniTest::Test - def setup - @subject = Runner.new("echo 'hello world'") - end - - include RunnerInterfaceTest - include OversableRunnerTests - - private - - def observable_act(subject) - subject.run - end - end - - class RunnerTest < MiniTest::Test - def setup - @subject = Runner.new("echo 'hello world'") - end - - def test_run - out, _ = capture_subprocess_io { @subject.run('file_path.rb') } - - assert_match "hello world", out - - out, _ = capture_subprocess_io { @subject.run } - - assert_match "hello world", out - end - - def test_run_all_tests - runner = Runner.new("echo ''", stdout: StringIO.new) - - out, _ = capture_subprocess_io { runner.run_all_tests('file_path.rb file_path_two.rb') } - - assert_equal(<<~EXPECATIONS, runner.stdout.string) - Test File Selected: file_path.rb file_path_two.rb - EXPECATIONS - - assert_equal(<<~EXPECATIONS, out) - file_path.rb file_path_two.rb - EXPECATIONS - end - end - end -end \ No newline at end of file diff --git a/test/retest/runners/test_runner_test.rb b/test/retest/runners/test_runner_test.rb deleted file mode 100644 index e73341c6..00000000 --- a/test/retest/runners/test_runner_test.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'test_helper' -require_relative 'runner_interface' -require_relative 'observable_runner' - -module Retest - module Runners - class TestRunnerInterfaceTests < MiniTest::Test - def setup - @repository = Repository.new(files: ['file_path_test.rb']) - @subject = TestRunner.new("echo 'touch '") - end - - include RunnerInterfaceTest - include OversableRunnerTests - - private - - def observable_act(subject) - subject.run( - 'file_path.rb', - repository: Repository.new(files: ['file_path_test.rb']) - ) - end - end - - class TestRunnerTest < MiniTest::Test - def setup - @repository = Repository.new(files: ['file_path_test.rb']) - @subject = TestRunner.new("echo 'touch '", stdout: StringIO.new) - end - - def output - @subject.stdout.string - end - - def test_run_with_no_file_found - _, _ = capture_subprocess_io { @subject.run nil, repository: @repository} - - assert_equal(<<~EXPECTED, output) - 404 - Test File Not Found - Retest could not find a matching test file to run. - EXPECTED - end - - def test_run_with_a_file_found - out, _ = capture_subprocess_io { @subject.run('file_path.rb', repository: @repository) } - - assert_match "touch file_path_test.rb", out - end - - def test_returns_last_command - out, _ = capture_subprocess_io { @subject.run('file_path.rb', repository: @repository) } - - assert_match "touch file_path_test.rb", out - - out, _ = capture_subprocess_io { @subject.run 'some-weird-path.rb', repository: @repository} - - assert_match "touch file_path_test.rb", out - end - - def test_sync_files - @subject.cached_test_file = 'file_path_test.rb' - - @subject.sync(added: [], removed:['something.rb']) - assert_equal 'file_path_test.rb', @subject.cached_test_file - - @subject.sync(added: nil, removed:'something.rb') - assert_equal 'file_path_test.rb', @subject.cached_test_file - - @subject.sync(added: ['a.rb'], removed:['file_path_test.rb']) - assert_nil @subject.cached_test_file - - @subject.cached_test_file = 'file_path_test.rb' - @subject.sync(added: 'a.rb', removed:'file_path_test.rb') - assert_nil @subject.cached_test_file - end - end - end -end \ No newline at end of file diff --git a/test/retest/runners/variable_runner_test.rb b/test/retest/runners/variable_runner_test.rb deleted file mode 100644 index 01c2fcde..00000000 --- a/test/retest/runners/variable_runner_test.rb +++ /dev/null @@ -1,90 +0,0 @@ -require 'test_helper' -require_relative 'runner_interface' -require_relative 'observable_runner' - -module Retest - module Runners - class VariableRunnerInterfaceTests < MiniTest::Test - def setup - @repository = Repository.new files: ['file_path_test.rb'] - @subject = VariableRunner.new("echo 'touch & '") - end - - include RunnerInterfaceTest - include OversableRunnerTests - - private - - def observable_act(subject) - subject.run( - 'file_path.rb', - repository: Repository.new(files: ['file_path_test.rb']) - ) - end - end - - class VariableRunnerTest < MiniTest::Test - def setup - @repository = Repository.new files: ['file_path_test.rb'] - @subject = VariableRunner.new("echo 'touch & '", stdout: StringIO.new) - end - - def output - @subject.stdout.string - end - - def test_files_selected_ouptut - _, _ = capture_subprocess_io { @subject.run('file_path.rb', repository: @repository) } - - assert_equal(<<~EXPECTED, output) - Files Selected: - - changed: file_path.rb - - test: file_path_test.rb - - EXPECTED - end - - def test_run_with_no_match - _, _ = capture_subprocess_io { @subject.run('another_file_path.rb', repository: @repository) } - - assert_equal(<<~EXPECTED, output) - 404 - Test File Not Found - Retest could not find a matching test file to run. - EXPECTED - end - - def test_run_with_a_file_found - out, _ = capture_subprocess_io { @subject.run('file_path.rb', repository: @repository) } - - assert_match "touch file_path.rb & file_path_test.rb", out - end - - def test_returns_last_command - out, _ = capture_subprocess_io { @subject.run('file_path.rb', repository: @repository) } - - assert_match "touch file_path.rb & file_path_test.rb", out - - out, _ = capture_subprocess_io { @subject.run('another_file_path.rb', repository: @repository) } - - assert_match "touch another_file_path.rb & file_path_test.rb", out - end - - def test_sync_files - @subject.cached_test_file = 'file_path_test.rb' - - @subject.sync(added: [], removed:['something.rb']) - assert_equal 'file_path_test.rb', @subject.cached_test_file - - @subject.sync(added: nil, removed:'something.rb') - assert_equal 'file_path_test.rb', @subject.cached_test_file - - @subject.sync(added: ['a.rb'], removed:['file_path_test.rb']) - assert_nil @subject.cached_test_file - - @subject.cached_test_file = 'file_path_test.rb' - @subject.sync(added: 'a.rb', removed:'file_path_test.rb') - assert_nil @subject.cached_test_file - end - end - end -end \ No newline at end of file diff --git a/test/retest/runners_test.rb b/test/retest/runners_test.rb deleted file mode 100644 index 1e95c9f2..00000000 --- a/test/retest/runners_test.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'test_helper' - -module Retest - class RunnersTest < MiniTest::Test - def test_runner_for - assert_equal Runners::Runner.new('bundle exec rake test'), Runners.runner_for('bundle exec rake test') - assert_equal Runners::TestRunner.new('echo '), Runners.runner_for('echo ') - assert_equal Runners::ChangeRunner.new('echo '), Runners.runner_for('echo ') - assert_equal Runners::VariableRunner.new('echo & '), Runners.runner_for('echo & ') - end - end -end \ No newline at end of file diff --git a/test/retest/sounds/sounds_interface.rb b/test/retest/sounds/sounds_interface.rb index aede674a..1da52bae 100644 --- a/test/retest/sounds/sounds_interface.rb +++ b/test/retest/sounds/sounds_interface.rb @@ -1,10 +1,6 @@ -require_relative '../runners/observable_runner' - module Retest module Sounds module SoundsInterfaceTests - include Runners::ObserverInterfaceTests - def test_interface assert_respond_to @subject, :play end diff --git a/test/retest/watcher_test.rb b/test/retest/watcher_test.rb new file mode 100644 index 00000000..18952248 --- /dev/null +++ b/test/retest/watcher_test.rb @@ -0,0 +1,46 @@ +require 'test_helper' + +module Retest + class TestWatcher < Minitest::Test + def test_for + assert_equal Watcher::Default, Watcher.for('listen') + assert_equal Watcher::Watchexec, Watcher.for('watchexec') + assert_equal Watcher::Watchexec, Watcher.for(nil) + + assert_raises(ArgumentError) { Watcher.for('fswatch') } + end + end + + module Watcher + class TestDefault < Minitest::Test + def test_installed? + assert Default.installed? + end + + def test_extensions_regex + result = Default.extensions_regex(%w[rb html]) + + assert result.match? "a/file/path.rb" + refute result.match? "a/file/path.js" + assert result.match? "a/file/path.html" + refute result.match? "a/file/path.css" + refute result.match? "a/file/path.ts" + + result = Default.extensions_regex(%w[rb js html css ts]) + + assert result.match? "a/file/path.rb" + assert result.match? "a/file/path.js" + assert result.match? "a/file/path.html" + assert result.match? "a/file/path.css" + assert result.match? "a/file/path.ts" + end + end + + class TestWatchexec < Minitest::Test + def test_installed? + # Installed on the machine + assert Watchexec.installed? + end + end + end +end \ No newline at end of file diff --git a/test/retest_test.rb b/test/retest_test.rb index 9deef2bd..e53e63e3 100644 --- a/test/retest_test.rb +++ b/test/retest_test.rb @@ -13,9 +13,9 @@ class ListenTests < MiniTest::Test def test_listen_default_behaviour listener = MiniTest::Mock.new - expected_options = {relative: true, only: Regexp.new('\\.rb$'), force_polling: false} + expected_options = { dir: '.', extensions: ['rb'], polling: false } - listener.expect(:to, Struct.new(:start).new, ['.', expected_options]) + listener.expect(:watch, true, [expected_options]) Retest.listen(Options.new, listener: listener) @@ -24,9 +24,9 @@ def test_listen_default_behaviour def test_listen_when_polling listener = MiniTest::Mock.new - expected_options = {relative: true, only: Regexp.new('\\.rb$'), force_polling: true} + expected_options = { dir: '.', extensions: ['rb'], polling: true } - listener.expect(:to, Struct.new(:start).new, ['.', expected_options]) + listener.expect(:watch, true, [expected_options]) Retest.listen(Options.new(["--polling"]), listener: listener) diff --git a/test/test_helper.rb b/test/test_helper.rb index 8aa0200c..df5f2d70 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -16,6 +16,20 @@ def exist?(value) end end +class RaisingRunner + class MethodCallError < StandardError; end + + def initialize + end + + def sync(added:, removed:) + end + + def run(file, repository:) + raise MethodCallError, "#{__method__} should not be called" + end +end + def wait_until(max_attempts: 10) attempts = 0 begin @@ -27,3 +41,10 @@ def wait_until(max_attempts: 10) end end +module Retest + # Remove Watchexec when not installed + if defined?(Watcher::Watchexec) && !Watcher::Watchexec.installed? + Watcher.send(:remove_const, :Watchexec) + Watcher::Watchexec = Watcher::Default + end +end