xt 0.20.0

Translate between serialized data formats
Documentation
//! xt's integration test suite.
//!
//! Most of xt's integration tests are based on the philosophy that xt should
//! produce consistent output for a given input regardless of how it consumes
//! that input. That is, xt should always work the same way whether it reads
//! from a file or a stream, or whether it auto-detects the input format or
//! knows it in advance.
//!
//! The test suite looks at sets of documents containing the same serialized
//! content as translated and output by xt itself, and exhaustively checks all
//! possible xt invocations—yes, all O(n²) of them—for translating one of those
//! documents to another (including itself). Besides generating a quadratic
//! blow-up of test cases, this approach imposes limitations on the structure
//! and values of the test inputs within a given set, and can cause some
//! annoyance when the specific formatting of a given output changes. However,
//! it does provide broad coverage with relatively little effort.

#![allow(clippy::items_after_test_module)]

use std::io;
use std::str::from_utf8;

use rstest::rstest;

use xt::Format;

macro_rules! xt_assert_translation {
	(
		input_source = $input_source:path;
		translator = $translator:path;
		translation = $from:expr => $to:expr;
		source_format = $source_format:expr;
	) => {
		let input = $input_source($from);
		let expected = $input_source($to);
		let mut output = Vec::with_capacity(expected.len());
		$translator(input, $source_format, $to, &mut output).unwrap();

		if let (Ok(expected), Ok(output)) = (from_utf8(expected), from_utf8(&output)) {
			// Try to print out readable representations of these values if we
			// can, instead of just arrays of bytes...
			similar_asserts::assert_eq!(expected, output);
		} else {
			// ...but always make sure we at least print *something*.
			similar_asserts::assert_eq!(expected, output);
		}
	};
}

#[rstest]
fn translate_single_slice_detected(
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_single_document_input;
		translator = xt::translate_slice;
		translation = from => to;
		source_format = None;
	}
}

#[rstest]
fn translate_single_slice_explicit(
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_single_document_input;
		translator = xt::translate_slice;
		translation = from => to;
		source_format = Some(from);
	}
}

#[rstest]
fn translate_single_reader_detected(
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_single_document_input;
		translator = xt::translate_reader;
		translation = from => to;
		source_format = None;
	}
}

#[rstest]
fn translate_single_reader_explicit(
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Toml, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_single_document_input;
		translator = xt::translate_reader;
		translation = from => to;
		source_format = Some(from);
	}
}

#[rstest]
fn translate_multi_slice_detected(
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_multi_document_input;
		translator = xt::translate_slice;
		translation = from => to;
		source_format = None;
	}
}

#[rstest]
fn translate_multi_slice_explicit(
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_multi_document_input;
		translator = xt::translate_slice;
		translation = from => to;
		source_format = Some(from);
	}
}

#[rstest]
fn translate_multi_reader_detected(
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_multi_document_input;
		translator = xt::translate_reader;
		translation = from => to;
		source_format = None;
	}
}

#[rstest]
fn translate_multi_reader_explicit(
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] from: Format,
	#[values(Format::Json, Format::Msgpack, Format::Yaml)] to: Format,
) {
	xt_assert_translation! {
		input_source = get_multi_document_input;
		translator = xt::translate_reader;
		translation = from => to;
		source_format = Some(from);
	}
}

/// Returns the single-document test input for a given format.
///
/// TOML's limitations impose several restrictions on these inputs:
///
/// 1. No null values.
/// 2. The root of each input must be a map.
/// 3. The values of the map must be ordered such that all non-map values appear
///    before any maps at a given depth.
fn get_single_document_input(fmt: Format) -> &'static [u8] {
	match fmt {
		Format::Json => include_bytes!("single.json"),
		Format::Msgpack => include_bytes!("single.msgpack"),
		Format::Toml => include_bytes!("single.toml"),
		Format::Yaml => include_bytes!("single.yaml"),
		fmt => panic!("{fmt} does not have a single-document test case"),
	}
}

/// Returns the multi-document test input for a given format.
///
/// The YAML and MessagePack format detection logic imposes a restriction on
/// these inputs: the first input in the stream must be a map or sequence.
/// Subsequent values may be of any supported type.
///
/// TOML does not support multi-document transcoding.
fn get_multi_document_input(fmt: Format) -> &'static [u8] {
	match fmt {
		Format::Json => include_bytes!("multi.json"),
		Format::Msgpack => include_bytes!("multi.msgpack"),
		Format::Yaml => include_bytes!("multi.yaml"),
		fmt => panic!("{fmt} does not have a multi-document test case"),
	}
}

/// Tests the translation of YAML documents from various text encodings.
///
/// YAML 1.2 requires support for the UTF-8, UTF-16, and UTF-32 character
/// encodings. Because serde_yaml only supports UTF-8 as of this writing, xt
/// takes care of re-encoding inputs where necessary. The test inputs cover a
/// reasonable subset of combinations of code unit size, endianness, and
/// presence or lack of a BOM.
#[rstest]
fn yaml_encoding(
	#[values("utf16be", "utf16le", "utf32be", "utf32le", "utf16bebom", "utf32lebom")] name: &str,
) {
	let input = get_yaml_encoding_input(name);
	let mut output = Vec::with_capacity(YAML_ENCODING_RESULT.len());
	xt::translate_slice(input, Some(Format::Yaml), Format::Json, &mut output).unwrap();
	assert_eq!(std::str::from_utf8(&output), Ok(YAML_ENCODING_RESULT));
}

const YAML_ENCODING_RESULT: &str = concat!(r#"{"xt":"🧑‍💻"}"#, "\n");

fn get_yaml_encoding_input(name: &str) -> &'static [u8] {
	match name {
		"utf16be" => include_bytes!("utf16be.yaml"),
		"utf16le" => include_bytes!("utf16le.yaml"),
		"utf32be" => include_bytes!("utf32be.yaml"),
		"utf32le" => include_bytes!("utf32le.yaml"),
		"utf16bebom" => include_bytes!("utf16bebom.yaml"),
		"utf32lebom" => include_bytes!("utf32lebom.yaml"),
		name => panic!("{name} is not a known YAML encoding input"),
	}
}

/// Tests that TOML output re-orders inputs as needed to meet TOML-specific
/// requirements, in particular that all non-table values must appear before any
/// tables at the same level.
#[test]
fn toml_reordering() {
	const INPUT: &[u8] = include_bytes!("single_reordered.json");
	const EXPECTED: &str = include_str!("single.toml");
	let mut output = Vec::with_capacity(EXPECTED.len());
	xt::translate_slice(INPUT, Some(Format::Json), Format::Toml, &mut output).unwrap();
	assert_eq!(std::str::from_utf8(&output), Ok(EXPECTED));
}

/// Tests that a TOML input that starts with a table is not accidentally
/// mis-detected as YAML. This happened with an early version of streaming YAML
/// input support, since a YAML parser can successfully parse a TOML table
/// header as a valid document containing a flow sequence, and not actually fail
/// until later in the stream.
#[test]
fn toml_initial_table_detection() {
	const INPUT: &[u8] = include_bytes!("initial_table.toml");
	xt::translate_reader(INPUT, None, Format::Json, io::sink()).unwrap();
}

/// Tests that halting transcoding in the middle of a YAML input does not panic
/// and crash.
///
/// The particular example involves translating a YAML input with a null key to
/// JSON, which refuses to accept the non-string key. Past versions of xt's
/// transcoder broke internal YAML deserializer variants when this happened.
#[test]
fn yaml_halting_without_panic() {
	const INPUT: &[u8] = include_bytes!("nullkey.yaml");
	let _ = xt::translate_slice(INPUT, Some(Format::Yaml), Format::Json, std::io::sink());
}