lib/tryouts/test_case.rb



# lib/tryouts/testcase.rb

# Modern data structures using Ruby 3.2+ Data classes
class Tryouts
  # Core data structures
  TestCase = Data.define(:description, :code, :expectations, :line_range, :path, :source_lines, :first_expectation_line) do
    def empty?
      code.empty?
    end

    def expectations?
      !expectations.empty?
    end

    def exception_expectations?
      expectations.any?(&:exception?)
    end

    def regular_expectations
      expectations.filter(&:regular?)
    end

    def exception_expectations
      expectations.filter(&:exception?)
    end
  end

  Expectation = Data.define(:content, :type) do
    def regular? = type == :regular
    def exception? = type == :exception
    def boolean? = type == :boolean
    def true? = type == :true
    def false? = type == :false
    def result_type? = type == :result_type
    def regex_match? = type == :regex_match
    def performance_time? = type == :performance_time
    def intentional_failure? = type == :intentional_failure
    def output? = type == :output
  end

  # Special expectation type for output capturing with pipe information
  OutputExpectation = Data.define(:content, :type, :pipe) do
    def regular? = type == :regular
    def exception? = type == :exception
    def boolean? = type == :boolean
    def true? = type == :true
    def false? = type == :false
    def result_type? = type == :result_type
    def regex_match? = type == :regex_match
    def performance_time? = type == :performance_time
    def intentional_failure? = type == :intentional_failure
    def output? = type == :output

    def stdout? = pipe == 1
    def stderr? = pipe == 2
  end

  Setup = Data.define(:code, :line_range, :path) do
    def empty?
      code.empty?
    end
  end

  Teardown = Data.define(:code, :line_range, :path) do
    def empty?
      code.empty?
    end
  end

  Testrun = Data.define(:setup, :test_cases, :teardown, :source_file, :metadata) do
    def total_tests
      test_cases.size
    end

    def empty?
      test_cases.empty?
    end
  end

  # Test case result packet for formatters
  # Replaces the simple Hash aggregation with a rich, immutable data structure
  # containing all execution context and results needed by formatters
  TestCaseResultPacket = Data.define(
    :test_case,          # TestCase object
    :status,             # :passed, :failed, :error
    :result_value,       # Actual execution result
    :actual_results,     # Array of actual values from expectations
    :expected_results,   # Array of expected values from expectations
    :error,              # Exception object (if any)
    :captured_output,    # Captured stdout/stderr content
    :elapsed_time,       # Execution timing (future use)
    :metadata,            # Hash for future extensibility
  ) do
    def passed?
      status == :passed
    end

    def failed?
      status == :failed
    end

    def error?
      status == :error
    end

    def has_output?
      captured_output && !captured_output.empty?
    end

    def has_error?
      !error.nil?
    end

    # Helper for formatter access to first actual/expected values
    def first_actual
      actual_results&.first
    end

    def first_expected
      expected_results&.first
    end

    # Create a basic result packet for successful tests
    def self.from_success(test_case, result_value, actual_results, expected_results, captured_output: nil, elapsed_time: nil, metadata: {})
      new(
        test_case: test_case,
        status: :passed,
        result_value: result_value,
        actual_results: actual_results,
        expected_results: expected_results,
        error: nil,
        captured_output: captured_output,
        elapsed_time: elapsed_time,
        metadata: metadata,
      )
    end

    # Create a result packet for failed tests
    def self.from_failure(test_case, result_value, actual_results, expected_results, captured_output: nil, elapsed_time: nil, metadata: {})
      new(
        test_case: test_case,
        status: :failed,
        result_value: result_value,
        actual_results: actual_results,
        expected_results: expected_results,
        error: nil,
        captured_output: captured_output,
        elapsed_time: elapsed_time,
        metadata: metadata,
      )
    end

    # Create a result packet for error cases
    def self.from_error(test_case, error, captured_output: nil, elapsed_time: nil, metadata: {})
      error_message = error ? error.message : '<exception is nil>'

      # Include backtrace in error message when in debug/verbose mode
      error_display = if error && Tryouts.debug?
        backtrace_preview = error.backtrace&.first(3)&.join("\n    ")
        "(#{error.class}) #{error_message}\n    #{backtrace_preview}"
      else
        "(#{error.class}) #{error_message}"
      end

      new(
        test_case: test_case,
        status: :error,
        result_value: nil,
        actual_results: [error_display],
        expected_results: [],
        error: error,
        captured_output: captured_output,
        elapsed_time: elapsed_time,
        metadata: metadata,
      )
    end
  end

  # Enhanced error with context
  class TryoutSyntaxError < StandardError
    attr_reader :line_number, :context, :source_file

    def initialize(message, line_number:, context:, source_file: nil)
      @line_number = line_number
      @context     = context
      @source_file = source_file

      location = source_file ? "#{source_file}:#{line_number}" : "line #{line_number}"
      super("#{message} at #{location}: #{context}")
    end
  end
end