// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include #include #include #include "../common/Logger.h" #include "../rugnux/ModelValidation.h" namespace { // A synthetic P1 cell with two carbon atoms - enough for a reader to produce a Structure with // atoms, a cell and a space group, which is all these tests are about. Neutral by construction: // no real specimen's parameters are involved. const char *kMmcif = R"(data_test _cell.length_a 40.000 _cell.length_b 50.000 _cell.length_c 60.000 _cell.angle_alpha 90.000 _cell.angle_beta 90.000 _cell.angle_gamma 90.000 _symmetry.space_group_name_H-M 'P 1' loop_ _atom_site.group_PDB _atom_site.id _atom_site.type_symbol _atom_site.label_alt_id _atom_site.label_atom_id _atom_site.label_comp_id _atom_site.label_asym_id _atom_site.label_seq_id _atom_site.Cartn_x _atom_site.Cartn_y _atom_site.Cartn_z _atom_site.occupancy _atom_site.B_iso_or_equiv ATOM 1 C . CA GLY A 1 10.000 12.000 14.000 1.00 20.00 ATOM 2 C . CB GLY A 1 12.000 14.000 16.000 1.00 20.00 )"; const char *kPdb = "CRYST1 40.000 50.000 60.000 90.00 90.00 90.00 P 1 1\n" "ATOM 1 CA GLY A 1 10.000 12.000 14.000 1.00 20.00 C\n" "ATOM 2 CB GLY A 1 12.000 14.000 16.000 1.00 20.00 C\n" "END\n"; std::string WriteTemp(const std::string &name, const char *content) { std::ofstream f(name); f << content; f.close(); return name; } } // --model used to call gemmi::read_pdb unconditionally, so a deposited model handed over as mmCIF - // which is how the PDB serves coordinates by default - was refused outright. Both formats now go // through the same reader, which decides on the file's content. TEST_CASE("ModelValidation_ReadsPdbAndMmcif", "[ModelValidation]") { Logger logger("ModelValidation_ReadsPdbAndMmcif"); const auto pdb = WriteTemp("model_validation_test.pdb", kPdb); const auto cif = WriteTemp("model_validation_test.cif", kMmcif); const auto from_pdb = ModelReferenceIntensities(pdb, {}, {}, 4.0, logger); const auto from_cif = ModelReferenceIntensities(cif, {}, {}, 4.0, logger); REQUIRE_FALSE(from_pdb.empty()); REQUIRE_FALSE(from_cif.empty()); // The same structure either way, so the same reflections come out of it. CHECK(from_cif.size() == from_pdb.size()); // The extension is not what decides: the same mmCIF under a .pdb name still reads. const auto misnamed = WriteTemp("model_validation_test_misnamed.pdb", kMmcif); CHECK_FALSE(ModelReferenceIntensities(misnamed, {}, {}, 4.0, logger).empty()); std::filesystem::remove(pdb); std::filesystem::remove(cif); std::filesystem::remove(misnamed); } // A model that cannot be used must say why. Returning an empty result and logging was enough to lose // the fact entirely: the run finished successfully with no R-free and no maps, which is exactly what // a run without --model looks like. TEST_CASE("ModelValidation_UnusableModelGivesAReason", "[ModelValidation]") { Logger logger("ModelValidation_UnusableModelGivesAReason"); const auto empty_file = WriteTemp("model_validation_test_bogus.pdb", "not a coordinate file\n"); const auto result = ValidateAgainstModel({}, UnitCell{.a = 40, .b = 50, .c = 60, .alpha = 90, .beta = 90, .gamma = 90}, empty_file, "", logger); CHECK_FALSE(result.ok); CHECK_FALSE(result.failure_reason.empty()); CHECK_THAT(result.failure_reason, Catch::Matchers::ContainsSubstring(empty_file)); const auto missing = ValidateAgainstModel({}, UnitCell{.a = 40, .b = 50, .c = 60, .alpha = 90, .beta = 90, .gamma = 90}, "model_validation_test_does_not_exist.pdb", "", logger); CHECK_FALSE(missing.ok); CHECK_FALSE(missing.failure_reason.empty()); std::filesystem::remove(empty_file); }