lib/tryouts/translators/minitest_translator.rb
# lib/tryouts/translators/minitest_translator.rb class Tryouts module Translators # Translates Tryouts test files to Minitest format # # IMPORTANT: Context Mode Differences # ================================== # # Tryouts supports two context modes that behave differently than Minitest: # # 1. Tryouts Shared Context (default): # - Setup runs once, all tests share the same context object # - Tests can modify variables/state and affect subsequent tests # - Behaves like a Ruby script executing top-to-bottom # - Designed for documentation-style tests where examples build on each other # # 2. Tryouts Fresh Context (--no-shared-context): # - Setup @instance_variables are copied to each test's fresh context # - Tests are isolated but inherit setup state # - Similar to Minitest's setup method but with setup state inheritance # # Minitest Translation Behavior: # ============================== # - Uses setup method which runs before each test (Minitest standard) # - Each test method gets fresh context (Minitest standard) # - Tests that rely on shared state between test cases WILL FAIL # - This is intentional and reveals inappropriate test dependencies # # Example that works in Tryouts shared mode but fails in Minitest: # ## TEST 1 # @counter = 1 # @counter # #=> 1 # # ## TEST 2 # @counter += 1 # Will be reset to 1 by setup, then fail # @counter # #=> 2 # # Recommendation: Write tryouts tests that work in fresh context mode # if you plan to use Minitest translation. class MinitestTranslator def initialize require 'minitest/test' rescue LoadError raise 'Minitest gem is required for Minitest translation' end def translate(testrun) file_basename = File.basename(testrun.source_file, '.rb') class_name = "Test#{file_basename.gsub(/[^A-Za-z0-9]/, '')}" test_class = Class.new(Minitest::Test) do # Setup method if testrun.setup && !testrun.setup.empty? define_method(:setup) do instance_eval(testrun.setup.code) end end # Generate test methods testrun.test_cases.each_with_index do |test_case, index| next if test_case.empty? || !test_case.expectations? method_name = "test_#{index.to_s.rjust(3, '0')}_#{parameterize(test_case.description)}" define_method(method_name) do if test_case.exception_expectations? # Handle exception expectations assert_raises(StandardError) do instance_eval(test_case.code) unless test_case.code.strip.empty? end test_case.exception_expectations.each do |expectation| result = instance_eval(expectation.content) assert result, "Exception expectation failed: #{expectation.content}" end else # Handle regular expectations result = instance_eval(test_case.code) unless test_case.code.strip.empty? test_case.regular_expectations.each do |expectation| expected_value = instance_eval(expectation.content) assert_equal expected_value, result end end end end # Teardown method if testrun.teardown && !testrun.teardown.empty? define_method(:teardown) do instance_eval(testrun.teardown.code) end end end # Set the class name dynamically Object.const_set(class_name, test_class) unless Object.const_defined?(class_name) test_class end def generate_code(testrun) file_basename = File.basename(testrun.source_file, '.rb') class_name = "Test#{file_basename.gsub(/[^A-Za-z0-9]/, '')}" lines = [] lines << '' lines << "require 'minitest/test'" lines << "require 'minitest/autorun'" lines << '' lines << "class #{class_name} < Minitest::Test" if testrun.setup && !testrun.setup.empty? lines << ' def setup' testrun.setup.code.lines.each { |line| lines << " #{line.chomp}" } lines << ' end' lines << '' end testrun.test_cases.each_with_index do |test_case, index| next if test_case.empty? || !test_case.expectations? method_name = "test_#{index.to_s.rjust(3, '0')}_#{parameterize(test_case.description)}" lines << " def #{method_name}" if test_case.exception_expectations? # Handle exception expectations lines << ' error = assert_raises(StandardError) do' unless test_case.code.strip.empty? test_case.code.lines.each { |line| lines << " #{line.chomp}" } end lines << ' end' test_case.exception_expectations.each do |expectation| lines << " assert #{expectation}, \"Exception expectation failed: #{expectation}\"" end else # Handle regular expectations unless test_case.code.strip.empty? lines << ' result = begin' test_case.code.lines.each { |line| lines << " #{line.chomp}" } lines << ' end' end test_case.expectations.each do |expectation| lines << " assert_equal #{expectation}, result" end end lines << ' end' lines << '' end if testrun.teardown && !testrun.teardown.empty? lines << ' def teardown' testrun.teardown.code.lines.each { |line| lines << " #{line.chomp}" } lines << ' end' end lines << 'end' lines.join("\n") end private # Simple string parameterization for method names def parameterize(string) string.downcase.gsub(/[^a-z0-9]+/, '_').gsub(/^_|_$/, '') end end end end