From c2744b94ee49d20741c2334e4b1782320fea6d3d Mon Sep 17 00:00:00 2001 From: Alexandre Barret Date: Mon, 9 Dec 2024 10:04:19 +1300 Subject: [PATCH] V2/refactor test helper (#235) * Enable new test helper for all feature specs * Refactor all feature specs --- bin/test/bundler-app | 2 +- bin/test/ruby-app | 2 +- .../bundler-app/retest/support/test_helper.rb | 11 +- features/git-ruby/retest/retest_test.rb | 33 +++--- .../git-ruby/retest/support/output_file.rb | 21 ---- .../git-ruby/retest/support/test_helper.rb | 101 ++++++++++++------ features/hanami-app/retest/retest_test.rb | 23 ++-- .../hanami-app/retest/support/output_file.rb | 21 ---- .../hanami-app/retest/support/test_helper.rb | 101 ++++++++++++------ features/rails-app/retest/retest_test.rb | 33 +++--- .../rails-app/retest/support/output_file.rb | 21 ---- .../rails-app/retest/support/test_helper.rb | 101 ++++++++++++------ features/rspec-rails/retest/retest_test.rb | 25 +++-- .../rspec-rails/retest/support/output_file.rb | 21 ---- .../rspec-rails/retest/support/test_helper.rb | 101 ++++++++++++------ features/rspec-ruby/retest/retest_test.rb | 2 - .../retest/retest_test/file_changes_test.rb | 30 +++--- .../retest/retest_test/flags_test.rb | 6 +- .../rspec-ruby/retest/support/output_file.rb | 21 ---- .../rspec-ruby/retest/support/test_helper.rb | 101 ++++++++++++------ features/ruby-app/retest/retest_test.rb | 29 ++--- .../retest/shared/explicit_matching.rb | 12 +-- .../ruby-app/retest/shared/file_changes.rb | 32 +++--- .../ruby-app/retest/support/test_helper.rb | 95 +++++++++------- features/ruby-bare/retest/flags/help.rb | 6 +- features/ruby-bare/retest/flags/version.rb | 6 +- features/ruby-bare/retest/retest_test.rb | 32 +++--- .../ruby-bare/retest/scenarios/auto_flag.rb | 6 +- .../changed_and_test_placeholders.rb | 6 +- .../retest/scenarios/changed_placeholder.rb | 6 +- .../retest/scenarios/custom_extensions.rb | 12 ++- .../retest/scenarios/force_polling.rb | 32 +++--- .../retest/scenarios/interruptions.rb | 11 +- .../retest/scenarios/multiple_commands.rb | 6 +- .../ruby-bare/retest/support/output_file.rb | 21 ---- .../ruby-bare/retest/support/test_helper.rb | 101 ++++++++++++------ features/support/output_file.rb | 21 ---- features/support/test_helper.rb | 98 +++++++++++------ 38 files changed, 732 insertions(+), 577 deletions(-) delete mode 100644 features/git-ruby/retest/support/output_file.rb delete mode 100644 features/hanami-app/retest/support/output_file.rb delete mode 100644 features/rails-app/retest/support/output_file.rb delete mode 100644 features/rspec-rails/retest/support/output_file.rb delete mode 100644 features/rspec-ruby/retest/support/output_file.rb delete mode 100644 features/ruby-bare/retest/support/output_file.rb delete mode 100644 features/support/output_file.rb diff --git a/bin/test/bundler-app b/bin/test/bundler-app index 4f4ae60f..1a49ef40 100755 --- a/bin/test/bundler-app +++ b/bin/test/bundler-app @@ -4,7 +4,7 @@ FOLDER="features/bundler-app" bundle install bundle exec rake build -# cp -R features/support features/bundler-app/retest +cp -R features/support features/bundler-app/retest ls -t pkg | head -n1 | xargs -I {} mv pkg/{} "$FOLDER/retest.gem" if [[ "$1" == "--no-build" ]]; then diff --git a/bin/test/ruby-app b/bin/test/ruby-app index 5b314ad1..ad84d862 100755 --- a/bin/test/ruby-app +++ b/bin/test/ruby-app @@ -2,6 +2,6 @@ bundle install bundle exec rake build -# cp -R features/support features/ruby-app/retest +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 diff --git a/features/bundler-app/retest/support/test_helper.rb b/features/bundler-app/retest/support/test_helper.rb index 47ae6a8d..57431346 100644 --- a/features/bundler-app/retest/support/test_helper.rb +++ b/features/bundler-app/retest/support/test_helper.rb @@ -3,21 +3,18 @@ module RetestHelper # COMMAND - def launch_retest(command, sleep_seconds: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) + 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_seconds + sleep sleep_for end def end_retest @input&.close @stderr&.close @output&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) - end + @wait_thr.exit end # ASSERTIONS @@ -42,7 +39,7 @@ def read_output(output = @output) result = "" loop do result += output.read_nonblock(1024) - rescue IO::WaitReadable + rescue IO::WaitReadable, EOFError break end diff --git a/features/git-ruby/retest/retest_test.rb b/features/git-ruby/retest/retest_test.rb index ca9244e7..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 selected: - - 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 8b0709d5..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: 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,7 +74,7 @@ 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... @@ -80,7 +85,7 @@ 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... 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 3fe1af52..664d7113 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: 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,7 +74,7 @@ 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... @@ -80,7 +85,7 @@ 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... @@ -96,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"` @@ -108,7 +115,6 @@ def setup end def teardown - @output.delete `git checkout -` `git clean -fd .` `git checkout .` @@ -124,9 +130,8 @@ 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 Setup identified: [RAILS]. Using command: 'bin/rails test ' Tests selected: - test/controllers/books_controller_test.rb @@ -134,8 +139,6 @@ def test_diffs_from_other_branch - test/system/books_test.rb EXPECTED - assert_match <<~EXPECTED, @output.read - 7 runs, 9 assertions, 0 failures, 0 errors, 0 skips - EXPECTED + assert_output_matches "7 runs, 9 assertions, 0 failures, 0 errors, 0 skips" 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 8035ad0b..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: 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,7 +74,7 @@ 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... @@ -80,7 +85,7 @@ 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... 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 52668e80..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: 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: 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: foo_spec.rb", @output.read + assert_output_matches "Test file: foo_spec.rb" ensure delete_file 'foo_spec.rb' @@ -49,18 +53,18 @@ def test_creating_a_new_file launch_retest @command create_file 'foo.rb' - assert_match <<~EXPECTED, @output.read + 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: foo_spec.rb", @output.read + assert_output_matches "Test file: foo_spec.rb" modify_file('lib/bottles.rb') - assert_match "Test file: spec/bottles_spec.rb", @output.read + assert_output_matches "Test file: spec/bottles_spec.rb" modify_file('foo.rb') - assert_match "Test file: foo_spec.rb", @output.read + assert_output_matches "Test file: foo_spec.rb" ensure delete_file 'foo.rb' @@ -68,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: 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 1dab0d83..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,7 +11,7 @@ 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... @@ -18,6 +20,6 @@ def test_with_no_command modify_file('lib/bottles.rb') - assert_match "Test file: 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/retest/retest_test.rb b/features/ruby-app/retest/retest_test.rb index 7c34966b..dd99d5e3 100644 --- a/features/ruby-app/retest/retest_test.rb +++ b/features/ruby-app/retest/retest_test.rb @@ -9,15 +9,7 @@ $stdout.sync = true class TestListenWatcher < Minitest::Test - # Helpers - include FileHelper - include OutputHelper - include CommandHelper - - # Assertions - include Setup - include FileChanges - include ExplicitMatching + include RetestHelper def setup @command = 'retest -w listen' @@ -26,7 +18,7 @@ def setup def test_start_retest launch_retest(@command) - assert_match <<~EXPECTED, read_output + assert_output_matches <<~EXPECTED Setup identified: [RUBY]. Using command: 'bundle exec ruby ' Watcher: [LISTEN] Launching Retest... @@ -36,15 +28,7 @@ def test_start_retest end class TestWatchexecWatcher < Minitest::Test - # Helpers - include FileHelper - include OutputHelper - include CommandHelper - - # Assertions - include Setup - include FileChanges - include ExplicitMatching + include RetestHelper def setup @command = 'retest -w watchexec' @@ -53,7 +37,7 @@ def setup def test_start_retest launch_retest(@command) - assert_match <<~EXPECTED, read_output + assert_output_matches <<~EXPECTED Setup identified: [RUBY]. Using command: 'bundle exec ruby ' Watcher: [WATCHEXEC] Launching Retest... @@ -63,8 +47,7 @@ def test_start_retest end class TestDefaultWatcher < Minitest::Test - include OutputHelper - include CommandHelper + include RetestHelper def setup @command = 'retest' @@ -73,7 +56,7 @@ def setup def test_uses_watchexec_when_installed launch_retest(@command) - assert_match <<~EXPECTED, read_output + assert_output_matches <<~EXPECTED Setup identified: [RUBY]. Using command: 'bundle exec ruby ' Watcher: [WATCHEXEC] Launching Retest... diff --git a/features/ruby-app/retest/shared/explicit_matching.rb b/features/ruby-app/retest/shared/explicit_matching.rb index 1bff62f1..b0c457ed 100644 --- a/features/ruby-app/retest/shared/explicit_matching.rb +++ b/features/ruby-app/retest/shared/explicit_matching.rb @@ -4,15 +4,15 @@ def teardown end def test_displaying_options_on_matching_command - create_file('test/other_bottles_test.rb', should_sleep: false) + create_file('test/other_bottles_test.rb', sleep_for: 0) launch_retest(@command) create_file 'foo_test.rb' - assert_match "Test file: foo_test.rb", read_output + assert_output_matches "Test file: foo_test.rb" modify_file('lib/bottles.rb') - assert_match <<~EXPECTED.chomp, read_output + assert_output_matches <<~EXPECTED.chomp We found few tests matching: lib/bottles.rb [0] - test/bottles_test.rb @@ -24,10 +24,8 @@ def test_displaying_options_on_matching_command > EXPECTED - @input.write "2\n" - wait - - assert_match "Test file: foo_test.rb", read_output + write_input("2\n") + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo_test.rb' diff --git a/features/ruby-app/retest/shared/file_changes.rb b/features/ruby-app/retest/shared/file_changes.rb index 2ea0115d..94c41790 100644 --- a/features/ruby-app/retest/shared/file_changes.rb +++ b/features/ruby-app/retest/shared/file_changes.rb @@ -1,4 +1,6 @@ module FileChanges + include RetestHelper + def teardown end_retest end @@ -8,10 +10,9 @@ def test_modifying_existing_file modify_file('lib/bottles.rb') - read_output do |output| - assert_match "Test file: test/bottles_test.rb", output - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", output - end + 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 @@ -19,10 +20,9 @@ def test_modifying_existing_test_file modify_file('test/bottles_test.rb') - read_output do |output| - assert_match "Test file: test/bottles_test.rb", output - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", output - end + 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 @@ -30,7 +30,7 @@ def test_creating_a_new_test_file create_file 'foo_test.rb' - assert_match "Test file: foo_test.rb", read_output + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo_test.rb' @@ -40,18 +40,18 @@ def test_creating_a_new_file launch_retest(@command) create_file 'foo.rb' - assert_match <<~EXPECTED, read_output + 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: foo_test.rb", read_output + assert_output_matches "Test file: foo_test.rb" modify_file('lib/bottles.rb') - assert_match "Test file: test/bottles_test.rb", read_output + assert_output_matches "Test file: test/bottles_test.rb" modify_file('foo.rb') - assert_match "Test file: foo_test.rb", read_output + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo.rb' @@ -59,13 +59,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: foo_test.rb", read_output + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo.rb' diff --git a/features/ruby-app/retest/support/test_helper.rb b/features/ruby-app/retest/support/test_helper.rb index d50ea30a..57431346 100644 --- a/features/ruby-app/retest/support/test_helper.rb +++ b/features/ruby-app/retest/support/test_helper.rb @@ -1,12 +1,45 @@ # Can be updated to all feature repositories with # $ bin/test/reset_helpers -module OutputHelper +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 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 + + # OUTPUT def read_output(output = @output) result = "" loop do result += output.read_nonblock(1024) - rescue IO::WaitReadable + rescue IO::WaitReadable, EOFError break end @@ -16,64 +49,50 @@ def read_output(output = @output) result end end -end - -module FileHelper - 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 wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # 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 -module CommandHelper - def launch_retest(command, sleep_seconds: Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5))) - require 'open3' - @input, @output, @stderr, @wait_thr = Open3.popen3(command) - @pid = @wait_thr[:pid] - sleep sleep_seconds + def default_sleep_seconds + Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) end - def end_retest - @input&.close - @stderr&.close - @output&.close - if @pid - Process.kill('SIGHUP', @pid) - Process.detach(@pid) - end + def launch_sleep_seconds + Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + end + + 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 2c8eb9c4..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 /^2\.\d+\.\d+/, @output.read + assert_output_matches /^2\.\d+\.\d+/ end def test_version_short_flag launch_retest 'retest -v' - assert_match /^2\.\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 a4612478..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: 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: 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: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" delete_file 'foo_test.rb' end @@ -73,31 +75,31 @@ def test_creating_a_new_file launch_retest @command create_file 'foo.rb' - assert_match <<~EXPECTED, @output.read + 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: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" modify_file('program.rb') - assert_match "Test file: program_test.rb", @output.read + assert_output_matches "Test file: program_test.rb" modify_file('foo.rb') - assert_match "Test file: 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: 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 95185c25..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,7 +8,7 @@ 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... @@ -15,6 +17,6 @@ def test_start_retest modify_file('program.rb') - assert_match "Test file: 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 0ab62ff6..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,14 +12,14 @@ 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 + assert_output_matches <<~EXPECTED Changed file: program.rb Test file: program_test.rb diff --git a/features/ruby-bare/retest/scenarios/changed_placeholder.rb b/features/ruby-bare/retest/scenarios/changed_placeholder.rb index c8c5e0e5..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,14 +12,14 @@ 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 + assert_output_matches <<~EXPECTED Changed file: program.rb file modified: program.rb diff --git a/features/ruby-bare/retest/scenarios/custom_extensions.rb b/features/ruby-bare/retest/scenarios/custom_extensions.rb index c8386850..db0f42a0 100644 --- a/features/ruby-bare/retest/scenarios/custom_extensions.rb +++ b/features/ruby-bare/retest/scenarios/custom_extensions.rb @@ -1,4 +1,6 @@ class CustomExtensionTest < Minitest::Test + include RetestHelper + def setup @command = %Q{retest "echo 'I captured a change'" --exts="txt"} end @@ -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 eb3dbfa4..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: 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: 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: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo_test.rb' @@ -49,18 +53,16 @@ def test_creating_a_new_file launch_retest @command create_file 'foo.rb' - assert_match <<~EXPECTED, @output.read - FileNotFound - 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: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" modify_file('program.rb') - assert_match "Test file: program_test.rb", @output.read + assert_output_matches "Test file: program_test.rb" modify_file('foo.rb') - assert_match "Test file: foo_test.rb", @output.read + assert_output_matches "Test file: foo_test.rb" ensure delete_file 'foo.rb' @@ -68,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: 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 3786c892..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,14 +12,14 @@ 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 + assert_output_matches <<~EXPECTED Changed file: program.rb Test file: program_test.rb 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 610a8f4b..57431346 100644 --- a/features/support/test_helper.rb +++ b/features/support/test_helper.rb @@ -1,62 +1,98 @@ # Can be updated to all feature repositories with # $ bin/test/reset_helpers -require_relative 'output_file' +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 -module FileHelper - def default_sleep_seconds - Float(ENV.fetch('DEFAULT_SLEEP_SECONDS', 1)) + def end_retest + @input&.close + @stderr&.close + @output&.close + @wait_thr.exit end - def launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + # 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 + + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) end - def modify_file(path, sleep_seconds: default_sleep_seconds) + # 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