diff --git a/test/e2e/CMakeLists.txt b/test/e2e/CMakeLists.txt index 5fc3bd09..52b059f1 100644 --- a/test/e2e/CMakeLists.txt +++ b/test/e2e/CMakeLists.txt @@ -4,6 +4,8 @@ target_link_libraries(jsonbinpack_e2e_test_runner PRIVATE sourcemeta::core::json target_link_libraries(jsonbinpack_e2e_test_runner PRIVATE sourcemeta::jsonbinpack::compiler) target_link_libraries(jsonbinpack_e2e_test_runner PRIVATE sourcemeta::jsonbinpack::runtime) target_link_libraries(jsonbinpack_e2e_test_runner PRIVATE sourcemeta::core::jsonschema) +target_link_libraries(jsonbinpack_e2e_test_runner PRIVATE sourcemeta::core::io) +target_link_libraries(jsonbinpack_e2e_test_runner PRIVATE sourcemeta::core::options) set_target_properties(jsonbinpack_e2e_test_runner PROPERTIES FOLDER "JSON BinPack/E2E") diff --git a/test/e2e/runner.cc b/test/e2e/runner.cc index 3bb45cfc..28a31f7e 100644 --- a/test/e2e/runner.cc +++ b/test/e2e/runner.cc @@ -1,31 +1,152 @@ #include #include +#include #include #include +#include -#include // assert -#include // EXIT_SUCCESS, EXIT_FAILURE -#include // std::filesystem -#include // std::ifstream, std::ofstream -#include // std::ios_base, std::ios::binary -#include // std::cerr -#include // std::to_string +#include // std::min +#include // assert +#include // std::from_chars +#include // std::int64_t +#include // EXIT_SUCCESS, EXIT_FAILURE +#include // std::filesystem +#include // std::cerr +#include // std::optional, std::nullopt +#include // std::istringstream, std::ostringstream +#include // std::string, std::to_string +#include // std::string_view +#include // std::errc constexpr auto DEFAULT_METASCHEMA = "https://json-schema.org/draft/2020-12/schema"; +namespace { + +auto prettify_to_string(const sourcemeta::core::JSON &document) -> std::string { + std::ostringstream stream; + sourcemeta::core::prettify(document, stream); + stream << "\n"; + return stream.str(); +} + +auto report_missing_artifact(const std::filesystem::path &path) -> void { + std::cerr << "The artifact does not exist: " << path.string() << "\n"; +} + +auto report_artifact_mismatch(const std::filesystem::path &path) -> void { + std::cerr << "The artifact does not match: " << path.string() << "\n"; +} + +auto first_difference(const std::string_view left, const std::string_view right) + -> std::string_view::size_type { + const auto bound{std::min(left.size(), right.size())}; + for (std::string_view::size_type index{0}; index < bound; index++) { + if (left[index] != right[index]) { + return index; + } + } + + return bound; +} + +auto check_text_artifact(const std::filesystem::path &path, + const std::string_view actual) -> bool { + if (!std::filesystem::is_regular_file(path)) { + report_missing_artifact(path); + return false; + } + + const auto expected{sourcemeta::core::read_file_to_string(path)}; + if (expected == actual) { + return true; + } + + report_artifact_mismatch(path); + std::cerr << "GOT:\n" << actual << "EXPECTED:\n" << expected; + return false; +} + +auto check_binary_artifact(const std::filesystem::path &path, + const std::string_view actual) -> bool { + if (!std::filesystem::is_regular_file(path)) { + report_missing_artifact(path); + return false; + } + + const auto expected{sourcemeta::core::read_file_to_string(path)}; + if (expected == actual) { + return true; + } + + report_artifact_mismatch(path); + std::cerr << "GOT: " << actual.size() << " bytes\n" + << "EXPECTED: " << expected.size() << " bytes\n" + << "FIRST DIFFERENCE AT BYTE: " + << first_difference(actual, expected) << "\n"; + return false; +} + +auto parse_size_artifact(const std::string_view contents) + -> std::optional { + std::string::size_type result{0}; + const auto *const end{contents.data() + contents.size()}; + const auto outcome{std::from_chars(contents.data(), end, result)}; + if (outcome.ec != std::errc{} || std::string_view{outcome.ptr, end} != "\n") { + return std::nullopt; + } + + return result; +} + +auto check_size_artifact(const std::filesystem::path &path, + const std::string::size_type actual) -> bool { + if (!std::filesystem::is_regular_file(path)) { + report_missing_artifact(path); + return false; + } + + const auto contents{std::to_string(actual) + "\n"}; + const auto expected{sourcemeta::core::read_file_to_string(path)}; + if (expected == contents) { + return true; + } + + report_artifact_mismatch(path); + const auto previous{parse_size_artifact(expected)}; + if (previous.has_value()) { + const auto difference{static_cast(actual) - + static_cast(previous.value())}; + std::cerr << "GOT: " << actual << " bytes\n" + << "EXPECTED: " << previous.value() << " bytes\n" + << "DIFFERENCE: " << (difference > 0 ? "+" : "") << difference + << " bytes\n"; + } else { + std::cerr << "GOT:\n" << contents << "EXPECTED:\n" << expected; + } + + return false; +} + +} // namespace + auto main(int argc, char *argv[]) -> int { - if (argc <= 2) { - std::cerr << "Usage: " << argv[0] << " \n"; + sourcemeta::core::Options application; + application.flag("write", {"w"}); + application.parse(argc, argv); + + if (application.positional().size() < 2) { + std::cerr << "Usage: " << argv[0] + << " [--write]\n"; return EXIT_FAILURE; } - const std::filesystem::path instance_path{argv[1]}; + const std::filesystem::path instance_path{application.positional().at(0)}; assert(std::filesystem::is_regular_file(instance_path)); const sourcemeta::core::JSON instance = sourcemeta::core::read_json(instance_path); - const std::filesystem::path directory{argv[2]}; + const std::filesystem::path directory{application.positional().at(1)}; assert(std::filesystem::is_directory(directory)); // Schema @@ -38,59 +159,34 @@ auto main(int argc, char *argv[]) -> int { sourcemeta::core::schema_resolver, DEFAULT_METASCHEMA); - std::ofstream canonical_output_stream(directory / "canonical.json", - std::ios::binary); - canonical_output_stream.exceptions(std::ios_base::badbit); - const sourcemeta::core::SchemaFrame frame{ sourcemeta::core::SchemaFrame::Mode::Locations, schema, sourcemeta::core::schema_walker, sourcemeta::core::schema_resolver, DEFAULT_METASCHEMA}; sourcemeta::core::schema_format(schema, frame); - sourcemeta::core::prettify(schema, canonical_output_stream); - canonical_output_stream << "\n"; - canonical_output_stream.flush(); - canonical_output_stream.close(); + const auto canonical{prettify_to_string(schema)}; // Compile sourcemeta::jsonbinpack::compile(schema, sourcemeta::core::schema_walker, sourcemeta::core::schema_resolver, DEFAULT_METASCHEMA); - std::ofstream encoding_output_stream(directory / "encoding.json", - std::ios::binary); - encoding_output_stream.exceptions(std::ios_base::badbit); - sourcemeta::core::prettify(schema, encoding_output_stream); - encoding_output_stream << "\n"; - encoding_output_stream.flush(); - encoding_output_stream.close(); + const auto encoding_document{prettify_to_string(schema)}; // Encoder const sourcemeta::jsonbinpack::Encoding encoding{ sourcemeta::jsonbinpack::load(schema)}; - std::ofstream output_stream(directory / "output.bin", std::ios::binary); - output_stream.exceptions(std::ios_base::badbit); + std::ostringstream output_stream; sourcemeta::jsonbinpack::Encoder encoder{output_stream}; encoder.write(instance, encoding); - output_stream.flush(); - const auto size{output_stream.tellp()}; - output_stream.close(); - std::ofstream size_stream(directory / "size.txt", std::ios::binary); - size_stream.exceptions(std::ios_base::badbit); - size_stream << std::to_string(size) << "\n"; - size_stream.flush(); - size_stream.close(); + const auto output{output_stream.str()}; // Decoder - std::ifstream data_stream{directory / "output.bin", std::ios::binary}; - data_stream.exceptions(std::ios_base::badbit); + std::istringstream data_stream{output}; sourcemeta::jsonbinpack::Decoder decoder{data_stream}; const sourcemeta::core::JSON result = decoder.read(encoding); - // Report results - if (result == instance) { - return EXIT_SUCCESS; - } else { + if (result != instance) { std::cerr << "GOT:\n"; sourcemeta::core::prettify(result, std::cerr); std::cerr << "\nEXPECTED:\n"; @@ -98,4 +194,33 @@ auto main(int argc, char *argv[]) -> int { std::cerr << "\n"; return EXIT_FAILURE; } + + const std::filesystem::path canonical_path{directory / "canonical.json"}; + const std::filesystem::path encoding_path{directory / "encoding.json"}; + const std::filesystem::path output_path{directory / "output.bin"}; + const std::filesystem::path size_path{directory / "size.txt"}; + + if (application.contains("write")) { + sourcemeta::core::write_file(canonical_path, canonical); + sourcemeta::core::write_file(encoding_path, encoding_document); + sourcemeta::core::write_file(output_path, output); + sourcemeta::core::write_file(size_path, + std::to_string(output.size()) + "\n"); + return EXIT_SUCCESS; + } + + auto matches{check_text_artifact(canonical_path, canonical)}; + matches = check_text_artifact(encoding_path, encoding_document) && matches; + matches = check_binary_artifact(output_path, output) && matches; + matches = check_size_artifact(size_path, output.size()) && matches; + + if (!matches) { + std::cerr << "\nIf these changes are expected, re-run as follows to " + "update the artifacts:\n " + << argv[0] << " " << instance_path.string() << " " + << directory.string() << " --write\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; }