lib/tryouts/failure_collector.rb



# lib/tryouts/failure_collector.rb

require_relative 'console'

class Tryouts
  # Collects and organizes failed test results across files for summary display
  # Similar to RSpec's failure summary at the end of test runs
  class FailureCollector
    # Data structure for a single failure entry
    FailureEntry = Data.define(:file_path, :test_case, :result_packet) do
      def line_number
        # Use first expectation line for consistency with main error display
        test_case.first_expectation_line || test_case.line_range&.first || 0
      end

      def description
        desc = test_case.description.to_s.strip
        desc.empty? ? 'unnamed test' : desc
      end

      def failure_reason
        case result_packet.status
        when :failed
          if result_packet.actual_results.any? && result_packet.expected_results.any?
            "expected #{result_packet.first_expected.inspect}, got #{result_packet.first_actual.inspect}"
          else
            'test failed'
          end
        when :error
          error_msg = result_packet.error&.message || 'unknown error'
          "#{result_packet.error&.class&.name || 'Error'}: #{error_msg}"
        else
          'test did not pass'
        end
      end

      def source_context
        return [] unless test_case.source_lines

        # Show the test code (excluding setup/teardown)
        test_case.source_lines.reject do |line|
          line.strip.empty? || line.strip.start_with?('#')
        end.first(3) # Limit to first 3 relevant lines
      end
    end

    def initialize
      @failures            = []
      @files_with_failures = Set.new
    end

    # Add a failed test result
    def add_failure(file_path, result_packet)
      return unless result_packet.failed? || result_packet.error?

      entry = FailureEntry.new(
        file_path: file_path,
        test_case: result_packet.test_case,
        result_packet: result_packet,
      )

      @failures << entry
      @files_with_failures << file_path
    end

    # Check if any failures were collected
    def any_failures?
      !@failures.empty?
    end

    # Get count of total failures
    def failure_count
      @failures.count { |f| f.result_packet.failed? }
    end

    # Get count of total errors
    def error_count
      @failures.count { |f| f.result_packet.error? }
    end

    # Get total issues (failures + errors)
    def total_issues
      @failures.size
    end

    # Get count of files with failures
    def files_with_failures_count
      @files_with_failures.size
    end

    # Get failures grouped by file for summary display
    def failures_by_file
      @failures.group_by(&:file_path).transform_values do |file_failures|
        file_failures.sort_by(&:line_number)
      end
    end

    # Get all failure entries (for detailed processing)
    def all_failures
      @failures.dup
    end

    # Reset the collector (useful for testing)
    def clear
      @failures.clear
      @files_with_failures.clear
    end
  end
end