diff --git a/bin/test/bundler-app b/bin/test/bundler-app index f5f2bc61..4f4ae60f 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 +# 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 + 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/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 ea08577a..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: 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: 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: 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,18 +53,16 @@ def test_creating_a_new_file launch_retest @command create_file 'lib/bundler_app/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 'test/bundler_app/test_foo.rb' - assert_match "Test file: 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: 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: 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' @@ -68,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", @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..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 2b41621f..47ae6a8d 100644 --- a/features/bundler-app/retest/support/test_helper.rb +++ b/features/bundler-app/retest/support/test_helper.rb @@ -1,59 +1,101 @@ -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_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 launch_sleep_seconds - Float(ENV.fetch('LAUNCH_SLEEP_SECONDS', 1.5)) + def end_retest + @input&.close + @stderr&.close + @output&.close + if @pid + Process.kill('SIGHUP', @pid) + Process.detach(@pid) + end end - def wait(sleep_seconds: default_sleep_seconds) - sleep sleep_seconds + # 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 modify_file(path, sleep_seconds: default_sleep_seconds) + # OUTPUT + def read_output(output = @output) + result = "" + loop do + result += output.read_nonblock(1024) + rescue IO::WaitReadable + 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 + + # 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/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: [])