lib/tryouts/expectation_evaluators/output.rb



# lib/tryouts/expectation_evaluators/output.rb

require_relative 'base'

class Tryouts
  module ExpectationEvaluators
    # Evaluator for output expectations using syntax: #=1> content, #=2> content
    #
    # PURPOSE:
    # - Validates that stdout (pipe 1) or stderr (pipe 2) contains expected content
    # - Provides ability to test console output, error messages, and logging
    # - Supports both string contains and regex pattern matching
    #
    # SYNTAX:
    # - #=1> expression  - Test stdout content
    # - #=2> expression  - Test stderr content
    #
    # Examples:
    #   puts "Hello World"           #=1> "Hello"            # String contains check
    #   puts "Hello World"           #=1> /Hello.*World/     # Regex pattern match
    #   $stderr.puts "Error!"        #=2> "Error"            # Stderr contains check
    #   $stderr.puts "Warning: 404"  #=2> /Warning.*\d+/     # Stderr regex match
    #
    # MATCHING BEHAVIOR:
    # - String expectations: Uses String#include? for substring matching
    # - Regex expectations: Uses =~ operator for pattern matching
    # - Auto-detects expectation type based on content (literal string vs regex)
    # - Case-sensitive matching for both strings and regex patterns
    #
    # IMPLEMENTATION DETAILS:
    # - Requires output capture during test execution
    # - Expression has access to `result` and `_` variables (actual_result)
    # - Expected display shows the evaluated expression result
    # - Actual display shows captured output content for the specific pipe
    # - Supports evaluation of dynamic expressions in expectation content
    #
    # DESIGN DECISIONS:
    # - Uses POSIX pipe convention: 1=stdout, 2=stderr
    # - Always captures output for debugging regardless of expectations
    # - Supports both literal strings and regex patterns seamlessly
    # - Part of unified #= prefix convention for all expectation types
    # - Uses #=N> syntax where N is the pipe number (following shell convention)
    #
    # PIPE MAPPING:
    # - Pipe 1: Standard output (stdout) - $stdout, puts, print, p
    # - Pipe 2: Standard error (stderr) - $stderr, warn, STDERR.puts
    # - Future pipes (3+) could support custom streams if needed
    class Output < Base
      def self.handles?(expectation_type)
        expectation_type == :output
      end

      def evaluate(actual_result = nil, stdout_content = nil, stderr_content = nil)
        # Determine which pipe we're testing based on expectation metadata
        pipe_number = @expectation.respond_to?(:pipe) ? @expectation.pipe : 1

        # Get the appropriate captured content
        captured_content = case pipe_number
                          when 1 then stdout_content || ''
                          when 2 then stderr_content || ''
                          else ''
                          end

        # Create result packet for expression evaluation
        expectation_result = ExpectationResult.from_execution_with_output(actual_result, stdout_content, stderr_content)

        # Evaluate the expectation expression (could be string literal or regex)
        expected_pattern = eval_expectation_content(@expectation.content, expectation_result)

        # Determine matching strategy based on expectation type
        matched = case expected_pattern
                  when Regexp
                    # Regex pattern matching
                    !!(captured_content =~ expected_pattern)
                  when String
                    # String contains matching
                    captured_content.include?(expected_pattern)
                  else
                    # Convert to string and do contains check
                    captured_content.include?(expected_pattern.to_s)
                  end

        # Build result with appropriate pipe description
        pipe_name = case pipe_number
                   when 1 then 'stdout'
                   when 2 then 'stderr'
                   else "pipe#{pipe_number}"
                   end

        build_result(
          passed: matched,
          actual: "#{pipe_name}: #{captured_content.inspect}",
          expected: expected_pattern.inspect,
          expectation: @expectation.content,
        )
      rescue StandardError => ex
        handle_evaluation_error(ex, actual_result)
      end
    end
  end
end