defmodule Reporter do @moduledoc """ Generates test reports in both summary and detailed formats. """ @doc """ Generates and saves test reports. ## Parameters - results: List of test results - summary: Test suite summary - config: Configuration with report path """ def generate_reports(results, summary, config) do timestamp = DateTime.utc_now() |> DateTime.to_iso8601(:basic) # Generate both reports summary_report = generate_summary_report(results, summary) detailed_report = generate_detailed_report(results, summary) # Save reports reports_dir = Map.get(config, :reports_dir, "reports") File.mkdir_p!(reports_dir) summary_path = Path.join(reports_dir, "summary_#{timestamp}.txt") detailed_path = Path.join(reports_dir, "detailed_#{timestamp}.txt") File.write!(summary_path, summary_report) File.write!(detailed_path, detailed_report) IO.puts("\nšŸ“Š Reports generated:") IO.puts(" Summary: #{summary_path}") IO.puts(" Detailed: #{detailed_path}") {summary_path, detailed_path} end @doc """ Generates a summary report with overall statistics and brief test results. """ def generate_summary_report(results, summary) do lines = [] # Header lines = lines ++ [ String.duplicate("=", 80), "ISO8583 TRANSACTION TEST SUITE - SUMMARY REPORT", String.duplicate("=", 80), "" ] # Summary statistics lines = lines ++ [ "Test Execution Summary", String.duplicate("-", 80), "Start Time: #{DateTime.to_string(summary.start_time)}", "End Time: #{DateTime.to_string(summary.end_time)}", "Duration: #{summary.duration_ms}ms (#{Float.round(summary.duration_ms / 1000, 2)}s)", "", "Total Tests: #{summary.total}", "Passed: #{summary.passed} āœ“", "Failed: #{summary.failed} āœ—", "Errors: #{summary.errors} ⚠", "Success Rate: #{calculate_success_rate(summary)}%", "" ] # Test results table lines = lines ++ [ "Test Results", String.duplicate("-", 80), format_table_header(), String.duplicate("-", 80) ] lines = lines ++ Enum.map(results, &format_result_row/1) lines = lines ++ [ String.duplicate("-", 80), "" ] # Failures summary (if any) failed_tests = Enum.filter(results, &(&1.status == :fail)) if failed_tests != [] do lines = lines ++ [ "Failed Tests Summary", String.duplicate("-", 80) ] lines = lines ++ Enum.flat_map(failed_tests, fn result -> [ "Test: #{result.test_id} - #{result.scenario}", " Expected: MTI=#{result.expected_mti}, RC=#{result.expected_response_code}", " Actual: MTI=#{result.actual_mti}, RC=#{result.actual_response_code}", "" ] end) end # Error summary (if any) error_tests = Enum.filter(results, &(&1.status == :error)) if error_tests != [] do lines = lines ++ [ "Errors Summary", String.duplicate("-", 80) ] lines = lines ++ Enum.flat_map(error_tests, fn result -> [ "Test: #{result.test_id} - #{result.scenario}", " Error: #{result.error}", "" ] end) end lines = lines ++ [ String.duplicate("=", 80), "END OF SUMMARY REPORT", String.duplicate("=", 80) ] Enum.join(lines, "\n") end @doc """ Generates a detailed report with complete test information including hex dumps. """ def generate_detailed_report(results, summary) do lines = [] # Header lines = lines ++ [ String.duplicate("=", 80), "ISO8583 TRANSACTION TEST SUITE - DETAILED REPORT", String.duplicate("=", 80), "", "Test Execution Summary", String.duplicate("-", 80), "Start Time: #{DateTime.to_string(summary.start_time)}", "End Time: #{DateTime.to_string(summary.end_time)}", "Duration: #{summary.duration_ms}ms", "Total Tests: #{summary.total}", "Passed: #{summary.passed}", "Failed: #{summary.failed}", "Errors: #{summary.errors}", "", String.duplicate("=", 80), "DETAILED TEST RESULTS", String.duplicate("=", 80), "" ] # Detailed results for each test lines = lines ++ Enum.flat_map(results, &format_detailed_result/1) lines = lines ++ [ String.duplicate("=", 80), "END OF DETAILED REPORT", String.duplicate("=", 80) ] Enum.join(lines, "\n") end # Helper functions defp calculate_success_rate(summary) do if summary.total == 0 do 0.0 else Float.round(summary.passed / summary.total * 100, 2) end end defp format_table_header do String.pad_trailing("Test ID", 15) <> String.pad_trailing("Scenario", 30) <> String.pad_trailing("Status", 10) <> String.pad_trailing("Exp RC", 8) <> "Act RC" end defp format_result_row(result) do status_icon = case result.status do :pass -> "āœ“ PASS" :fail -> "āœ— FAIL" :error -> "⚠ ERROR" end exp_rc = Map.get(result, :expected_response_code, "N/A") act_rc = Map.get(result, :actual_response_code, "N/A") String.pad_trailing(result.test_id, 15) <> String.pad_trailing(String.slice(result.scenario, 0, 29), 30) <> String.pad_trailing(status_icon, 10) <> String.pad_trailing(exp_rc, 8) <> act_rc end defp format_detailed_result(result) do lines = [ String.duplicate("-", 80), "Test ID: #{result.test_id}", "Scenario: #{result.scenario}", "Description: #{result.description}", "Status: #{format_status(result.status)}", "" ] # Test times if Map.has_key?(result, :start_time) && Map.has_key?(result, :end_time) do duration = DateTime.diff(result.end_time, result.start_time, :millisecond) lines = lines ++ [ "Execution Time: #{duration}ms", "" ] end # Expected vs Actual if result.status != :error do lines = lines ++ [ "Expected:", " MTI: #{Map.get(result, :expected_mti, "N/A")}", " Response Code: #{Map.get(result, :expected_response_code, "N/A")}", "", "Actual:", " MTI: #{Map.get(result, :actual_mti, "N/A")}", " Response Code: #{Map.get(result, :actual_response_code, "N/A")}", "" ] # Validation details if Map.has_key?(result, :validation) do lines = lines ++ format_validation_details(result.validation) end # Parsed response if Map.has_key?(result, :parsed_response) do lines = lines ++ format_parsed_response(result.parsed_response) end # Hex dumps if Map.has_key?(result, :packet_hex) do lines = lines ++ [ "Request Packet (hex):", format_hex_dump(result.packet_hex), "" ] end if Map.has_key?(result, :response_hex) do lines = lines ++ [ "Response Packet (hex):", format_hex_dump(result.response_hex), "" ] end else # Error details lines = lines ++ [ "Error: #{result.error}", "" ] end lines ++ [""] end defp format_status(:pass), do: "āœ“ PASSED" defp format_status(:fail), do: "āœ— FAILED" defp format_status(:error), do: "⚠ ERROR" defp format_validation_details(validation) do lines = [ "Validation Results:", " Total Checks: #{validation.total_checks}", " Failed Checks: #{validation.failed_checks}", "" ] if validation.checks != [] do lines = lines ++ [" Individual Checks:"] lines = lines ++ Enum.map(validation.checks, fn check -> status = if check.status == :pass, do: "āœ“", else: "āœ—" " #{status} #{check.name}: Expected=#{check.expected}, Actual=#{check.actual}" end) lines = lines ++ [""] end if validation.failures != [] do lines = lines ++ [" Failures:"] lines = lines ++ Enum.map(validation.failures, fn failure -> " - #{failure}" end) lines = lines ++ [""] end lines end defp format_parsed_response(parsed) do [ "Parsed Response:", " MTI: #{parsed.mti}", " TPDU: #{parsed.tpdu}", " Present Fields: #{inspect(parsed.present_fields)}", " Field Values:", format_field_values(parsed.fields), "" ] end defp format_field_values(fields) do fields |> Enum.sort() |> Enum.map(fn {field_num, value} -> " DE#{String.pad_leading(Integer.to_string(field_num), 3, "0")}: #{value}" end) |> Enum.join("\n") end defp format_hex_dump(hex_string) do # Format hex in lines of 32 chars (16 bytes) hex_string |> String.graphemes() |> Enum.chunk_every(32) |> Enum.map(fn chunk -> " " <> Enum.join(chunk) end) |> Enum.join("\n") end end