lib/tryouts/expectation_evaluators/base.rb



# lib/tryouts/expectation_evaluators/base.rb

class Tryouts
  module ExpectationEvaluators
    # Base class for all expectation evaluators
    class Base
      attr_reader :expectation, :test_case, :context

      # @param expectation_type [Symbol] the type of expectation to check
      # @return [Boolean] whether this evaluator can handle the given expectation type
      def self.handles?(expectation_type)
        raise NotImplementedError, "#{self} must implement handles? class method"
      end

      # @param expectation [Object] the expectation object containing content and metadata
      # @param test_case [Object] the test case being evaluated
      # @param context [Object] the context in which to evaluate expectations
      def initialize(expectation, test_case, context)
        @expectation = expectation
        @test_case   = test_case
        @context     = context
      end

      # Evaluates the expectation against the actual result
      # @param actual_result [Object] the result to evaluate against the expectation
      # @return [Hash] evaluation result with passed status and details
      def evaluate(actual_result = nil)
        raise NotImplementedError, "#{self.class} must implement evaluate method"
      end

      protected

      # Evaluates expectation content in the test context with predefined variables
      #
      # This method is the core of the expectation evaluation system, providing context-aware
      # variable access for different expectation types:
      #
      # VARIABLE AVAILABILITY:
      # - `result`: contains actual_result (regular) or timing_ms (performance)
      # - `_`: shorthand alias for the same data as result
      #
      # DESIGN DECISIONS:
      # - Add new values to ExpectationResult to avoid method signature changes
      # - Use define_singleton_method for clean variable injection
      # - Using instance_eval for evaluation provides:
      #     - Full access to test context (instance variables, methods)
      #     - Clean variable injection (result, _)
      #     - Proper file/line reporting for debugging
      #     - Support for complex Ruby expressions in expectations
      #
      #   Potential enhancements (without breaking changes):
      #     - Add more variables to ExpectationResult (memory usage, etc.)
      #     - Provide additional helper methods in evaluation context
      #     - Enhanced error reporting with better stack traces
      #
      # @param content [String] the expectation code to evaluate
      # @param expectation_result [ExpectationResult] container with actual_result and timing data
      # @return [Object] the result of evaluating the content
      def eval_expectation_content(content, expectation_result = nil)
        path  = @test_case.path
        range = @test_case.line_range

        if expectation_result
          # For performance expectations, timing data takes precedence for result/_
          if expectation_result.execution_time_ns
            timing_ms = expectation_result.execution_time_ms
            @context.define_singleton_method(:result) { timing_ms }
            @context.define_singleton_method(:_) { timing_ms }
          elsif expectation_result.actual_result
            # For regular expectations, use actual_result
            @context.define_singleton_method(:result) { expectation_result.actual_result }
            @context.define_singleton_method(:_) { expectation_result.actual_result }
          end
        end

        @context.instance_eval(content, path, range.first + 1)
      end

      def build_result(passed:, actual:, expected:, expectation: @expectation.content, error: nil)
        result         = {
          passed: passed,
          actual: actual,
          expected: expected,
          expectation: expectation,
        }
        result[:error] = error if error
        result
      end

      def handle_evaluation_error(error, actual_result)
        build_result(
          passed: false,
          actual: actual_result,
          expected: "EXPECTED: #{error.message}",
          expectation: @expectation.content,
          error: error,
        )
      end
    end
  end
end