surrealkit 1.0.0-beta.2

Manage migrations, seeding, typegen and tests for SurrealDB via CLI
use std::path::Path;
use std::{fs, io::Write};

use super::types::RunReport;
use anyhow::{Context, Result};
use indoc::writedoc;

fn indent_block(s: &str) -> String {
	s.lines().map(|line| format!("  {line}")).collect::<Vec<_>>().join("\n")
}

pub fn print_human_report<W: Write>(out: &mut W, report: &RunReport) -> Result<()> {
	let has_failure = report.suites_failed > 0 || report.cases_failed > 0;
	writedoc!(
		out,
		"
		Test run: {status} ({duration_ms}ms)

		Summary
		- suites: {suites_total} total, {suites_failed} failed
		- cases : {cases_total} total, {cases_passed} passed, {cases_failed} failed
		",
		status = if has_failure {
			"FAIL"
		} else {
			"PASS"
		},
		duration_ms = report.duration_ms,
		suites_total = report.suites_total,
		suites_failed = report.suites_failed,
		cases_total = report.cases_total,
		cases_passed = report.cases_passed,
		cases_failed = report.cases_failed
	)?;

	if report.suites_failed > 0 {
		for suite in &report.suites {
			if suite.cases_failed == 0 {
				continue;
			}
			writedoc!(
				out,
				"

				Suite: {suite_name}
				- file     : {suite_file}
				- namespace: {namespace}
				- database : {database}
				- result   : {cases_passed} passed, {cases_failed} failed
				",
				suite_name = suite.suite_name,
				suite_file = suite.suite_file,
				namespace = suite.namespace,
				database = suite.database,
				cases_passed = suite.cases_passed,
				cases_failed = suite.cases_failed
			)?;

			for (index, case) in suite.cases.iter().enumerate() {
				if case.passed {
					continue;
				}
				writedoc!(
					out,
					"

					Case {n}: {case_name}
					",
					n = index + 1,
					case_name = case.name
				)?;
				if let Some(message) = &case.message {
					writeln!(out, "\n{}\n", message.trim())?;
				}

				for assertion in &case.assertions {
					if assertion.passed {
						continue;
					}
					let header = format!("- {}", assertion.name);
					let line_count = assertion.message.lines().count();
					match line_count {
						0 => writeln!(out, "{}", header)?,
						1 => writeln!(out, "{}: {}", header, assertion.message)?,
						_ => {
							writeln!(out, "{}:", header)?;
							writeln!(out, "{}", indent_block(&assertion.message))?;
						}
					}
				}
			}
		}
	}

	Ok(())
}

pub fn write_json_report(path: &Path, report: &RunReport) -> Result<()> {
	if let Some(parent) = path.parent() {
		fs::create_dir_all(parent)
			.with_context(|| format!("creating report directory {}", parent.display()))?;
	}
	let raw = serde_json::to_string_pretty(report).context("serializing report json")?;
	fs::write(path, format!("{raw}\n"))
		.with_context(|| format!("writing report file {}", path.display()))?;
	Ok(())
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::tester::types::{AssertionReport, CaseReport, RunReport, SuiteReport};

	#[test]
	fn human_report_prints_summary() -> Result<()> {
		let report = make_report_successful();
		let mut output = Vec::new();
		print_human_report(&mut output, &report)?;
		let text = String::from_utf8(output)?;
		insta::assert_snapshot!(text, @"
		Test run: PASS (1000ms)

		Summary
		- suites: 1 total, 0 failed
		- cases : 1 total, 1 passed, 0 failed
		");
		Ok(())
	}

	#[test]
	fn human_report_prints_failure() -> Result<()> {
		let report = make_report_failure();
		let mut output = Vec::new();
		print_human_report(&mut output, &report)?;
		let text = String::from_utf8(output)?;
		insta::assert_snapshot!(text, @"
		Test run: FAIL (1000ms)

		Summary
		- suites: 1 total, 1 failed
		- cases : 1 total, 0 passed, 1 failed

		Suite: test_suite_name
		- file     : test_suite_file.yaml
		- namespace: test_ns
		- database : test_db
		- result   : 0 passed, 1 failed

		Case 1: test_case_name

		Case mutli-line
		message with some details

		- test_no_message_assertion
		- test_short_assertion: assertion failed for some reasons
		- test_multi_line_assertion:
		  multi line assertion failed for some reasons
		  second line
		    third line
		  
		  fourth line
		");
		Ok(())
	}

	#[test]
	fn json_report_is_serializable() {
		let report = make_report_successful();
		let encoded = serde_json::to_string(&report).expect("serialization should work");
		let decoded: serde_json::Value =
			serde_json::from_str(&encoded).expect("deserialization should work");

		insta::assert_json_snapshot!(
			decoded,
			@r#"
		{
		  "cases_failed": 0,
		  "cases_passed": 1,
		  "cases_total": 1,
		  "duration_ms": 1000,
		  "finished_at": "2020-01-01T00:00:01Z",
		  "started_at": "2020-01-01T00:00:00Z",
		  "suites": [
		    {
		      "cases": [
		        {
		          "assertions": [
		            {
		              "message": "assertion passed",
		              "name": "test_assertion_name",
		              "passed": true
		            }
		          ],
		          "duration_ms": 1000,
		          "kind": "sql_expect",
		          "message": null,
		          "name": "test_case_name",
		          "passed": true
		        }
		      ],
		      "cases_failed": 0,
		      "cases_passed": 1,
		      "cases_total": 1,
		      "database": "test_db",
		      "duration_ms": 1000,
		      "namespace": "test_ns",
		      "suite_file": "test_suite_file.yaml",
		      "suite_name": "test_suite_name"
		    }
		  ],
		  "suites_failed": 0,
		  "suites_total": 1
		}
		"#
		);
	}

	// ---------

	fn make_report_successful() -> RunReport {
		make_report(vec![CaseReport {
			name: "test_case_name".into(),
			passed: true,
			kind: "sql_expect".into(),
			duration_ms: 1000,
			message: None,
			assertions: vec![AssertionReport {
				name: "test_assertion_name".into(),
				passed: true,
				message: "assertion passed".into(),
			}],
		}])
	}

	fn make_report_failure() -> RunReport {
		make_report(vec![CaseReport {
			name: "test_case_name".into(),
			passed: false,
			kind: "sql_expect".into(),
			duration_ms: 1000,
			message: Some(
				indoc::indoc!(
					"
				Case mutli-line
				message with some details
				"
				)
				.into(),
			),
			assertions: vec![
				AssertionReport {
					name: "test_no_message_assertion".into(),
					passed: false,
					message: "".into(),
				},
				AssertionReport {
					name: "test_short_assertion".into(),
					passed: false,
					message: "assertion failed for some reasons".into(),
				},
				AssertionReport {
					name: "test_multi_line_assertion".into(),
					passed: false,
					message: indoc::indoc!(
						"
						multi line assertion failed for some reasons
						second line
						  third line

						fourth line
					"
					)
					.into(),
				},
			],
		}])
	}

	fn make_report(cases: Vec<CaseReport>) -> RunReport {
		let cases_total = cases.len();
		let cases_passed = cases.iter().filter(|c| c.passed).count();
		let cases_failed = cases.iter().filter(|c| !c.passed).count();
		let suites_failed = if cases_failed > 0 {
			1
		} else {
			0
		};
		RunReport {
			started_at: "2020-01-01T00:00:00Z".into(),
			finished_at: "2020-01-01T00:00:01Z".into(),
			duration_ms: 1000,
			suites_total: 1,
			suites_failed,
			cases_total,
			cases_passed,
			cases_failed,
			suites: vec![SuiteReport {
				suite_file: "test_suite_file.yaml".into(),
				suite_name: "test_suite_name".into(),
				namespace: "test_ns".into(),
				database: "test_db".into(),
				duration_ms: 1000,
				cases_total,
				cases_passed,
				cases_failed,
				cases,
			}],
		}
	}
}