From bcd2443984cf0cc7b57439b0f6ff34abeacc7002 Mon Sep 17 00:00:00 2001 From: Alexandre Barret Date: Mon, 9 Dec 2024 08:46:43 +1300 Subject: [PATCH] Add interactive feature tests * Pause/Unpause * Run all * Force selection * Start / Help * Run last command --- features/bundler-app/lib/bundler_app.rb | 1 + .../bundler-app/lib/bundler_app/fibonacci.rb | 11 ++ features/bundler-app/retest/retest_test.rb | 1 + .../retest/retest_test/file_changes_test.rb | 38 ++--- .../retest_test/interactive_commands_test.rb | 141 ++++++++++++++++++ .../bundler-app/retest/support/output_file.rb | 21 --- .../bundler-app/retest/support/test_helper.rb | 96 +++++++----- .../test/bundler_app/test_fibonacci.rb | 40 +++++ lib/retest/runner.rb | 3 +- 9 files changed, 271 insertions(+), 81 deletions(-) create mode 100644 features/bundler-app/lib/bundler_app/fibonacci.rb create mode 100644 features/bundler-app/retest/retest_test/interactive_commands_test.rb delete mode 100644 features/bundler-app/retest/support/output_file.rb create mode 100644 features/bundler-app/test/bundler_app/test_fibonacci.rb 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 d9af287a..0f4b5c32 100644 --- a/features/bundler-app/retest/retest_test.rb +++ b/features/bundler-app/retest/retest_test.rb @@ -2,5 +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 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 5053b107..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,7 +1,5 @@ class FileChangesTest < Minitest::Test - include FileHelper - include OutputHelper - include CommandHelper + include RetestHelper def setup @command = 'retest' @@ -14,7 +12,7 @@ def teardown def test_start_retest launch_retest @command - assert_match <<~EXPECTED, read_output + assert_output_matches <<~EXPECTED Launching Retest... Ready to refactor! You can make file changes now EXPECTED @@ -25,10 +23,9 @@ def test_modifying_existing_file modify_file('lib/bundler_app/bottles.rb') - read_output do |output| - assert_match "Test file: test/bundler_app/test_bottles.rb", output - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", output - end + 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 @@ -36,10 +33,9 @@ def test_modifying_existing_test_file modify_file('test/bundler_app/test_bottles.rb') - read_output do |output| - assert_match "Test file: test/bundler_app/test_bottles.rb", output - assert_match "12 runs, 12 assertions, 0 failures, 0 errors, 0 skips", output - end + 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 @@ -47,7 +43,7 @@ def test_creating_a_new_test_file create_file 'test/bundler_app/test_foo.rb' - assert_match "Test file: test/bundler_app/test_foo.rb", read_output + assert_output_matches 'Test file: test/bundler_app/test_foo.rb' ensure delete_file 'test/bundler_app/test_foo.rb' @@ -57,18 +53,16 @@ def test_creating_a_new_file launch_retest @command create_file 'lib/bundler_app/foo.rb' - assert_match <<~EXPECTED, read_output - 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 'test/bundler_app/test_foo.rb' - assert_match "Test file: test/bundler_app/test_foo.rb", read_output + assert_output_matches 'Test file: test/bundler_app/test_foo.rb' modify_file('lib/bundler_app/bottles.rb') - assert_match "Test file: test/bundler_app/test_bottles.rb", read_output + assert_output_matches 'Test file: test/bundler_app/test_bottles.rb' modify_file('lib/bundler_app/foo.rb') - assert_match "Test file: test/bundler_app/test_foo.rb", read_output + assert_output_matches 'Test file: test/bundler_app/test_foo.rb' ensure delete_file 'lib/bundler_app/foo.rb' @@ -76,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: test/bundler_app/test_foo.rb", read_output + 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..46d7d93e --- /dev/null +++ b/features/bundler-app/retest/retest_test/interactive_commands_test.rb @@ -0,0 +1,141 @@ +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. + * '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 d50ea30a..47ae6a8d 100644 --- a/features/bundler-app/retest/support/test_helper.rb +++ b/features/bundler-app/retest/support/test_helper.rb @@ -1,7 +1,43 @@ # Can be updated to all feature repositories with # $ bin/test/reset_helpers -module OutputHelper +module RetestHelper + # COMMAND + 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 + end + + def end_retest + @input&.close + @stderr&.close + @output&.close + if @pid + Process.kill('SIGHUP', @pid) + Process.detach(@pid) + end + 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 @@ -16,64 +52,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)) + # INPUT + def write_input(command, input: @input, sleep_for: 0.1) + input.write(command) + wait(sleep_for) end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds - 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/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/lib/retest/runner.rb b/lib/retest/runner.rb index aa26f4ab..825e8384 100644 --- a/lib/retest/runner.rb +++ b/lib/retest/runner.rb @@ -30,7 +30,8 @@ def run(changed_files: [], test_files: []) end def run_all - system_run command.clone(all: true).to_s + self.last_command = command.clone(all: true).to_s + system_run last_command end def format_instruction(changed_files: [], test_files: [])