// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include #include "../common/JFJochException.h" #include "../reader/SweepLayout.h" namespace { // A series of frames all taken at the same instrument setting, at the angles given. std::vector Series(const std::vector &angles, double increment) { std::vector out; out.reserve(angles.size()); for (size_t i = 0; i < angles.size(); i++) out.push_back({"f" + std::to_string(i) + ".cbf", angles[i], increment, 0.2, 1000, 1000, 1.0}); return out; } } // namespace TEST_CASE("SweepLayout_Contiguous") { std::vector angles; for (int i = 0; i < 100; i++) angles.push_back(20.0 + 0.1 * i); const auto l = sweep::Place(Series(angles, 0.1), "test"); CHECK(l.files.size() == 100); CHECK(l.present == 100); CHECK(l.start_deg == Catch::Approx(20.0)); CHECK(l.increment_deg == Catch::Approx(0.1)); for (size_t i = 0; i < l.files.size(); i++) CHECK(l.files[i] == "f" + std::to_string(i) + ".cbf"); } TEST_CASE("SweepLayout_Gapped") { // Frames 3, 4 and 7 of a ten-frame sweep never made it into the archive. The sweep is still ten // steps wide and every frame keeps its own angle. const auto l = sweep::Place(Series({0.0, 0.1, 0.2, 0.5, 0.6, 0.8, 0.9}, 0.1), "test"); REQUIRE(l.files.size() == 10); CHECK(l.present == 7); CHECK(l.increment_deg == Catch::Approx(0.1)); CHECK(l.files[2] == "f2.cbf"); CHECK(l.files[3].empty()); CHECK(l.files[4].empty()); CHECK(l.files[5] == "f3.cbf"); CHECK(l.files[7].empty()); CHECK(l.files[9] == "f6.cbf"); } TEST_CASE("SweepLayout_PastFullTurn") { // A writer that starts over at 0 rather than counting on past 360. const auto l = sweep::Place(Series({359.7, 359.8, 359.9, 0.0, 0.1}, 0.1), "test"); CHECK(l.files.size() == 5); CHECK(l.present == 5); CHECK(l.increment_deg == Catch::Approx(0.1)); } TEST_CASE("SweepLayout_Reversed") { const auto l = sweep::Place(Series({10.0, 9.5, 9.0, 8.0}, 0.5), "test"); REQUIRE(l.files.size() == 5); CHECK(l.increment_deg == Catch::Approx(-0.5)); CHECK(l.files[3].empty()); CHECK(l.files[4] == "f3.cbf"); } TEST_CASE("SweepLayout_Stills") { // Nothing turns: the files are the images, and the header's nominal increment stands. const auto l = sweep::Place(Series({45.0, 45.0, 45.0}, 0.1), "test"); CHECK(l.files.size() == 3); CHECK(l.present == 3); CHECK(l.start_deg == Catch::Approx(45.0)); CHECK(l.increment_deg == Catch::Approx(0.1)); } TEST_CASE("SweepLayout_ScreeningImagesRefused") { // Five shots at scattered angles are not a sweep, and must not be laid out as one. CHECK_THROWS_AS(sweep::Place(Series({0.0, 90.0, 45.0, 300.0, 270.0}, 0.5), "test"), JFJochException); } TEST_CASE("SweepLayout_MovedDetectorRefused") { auto frames = Series({0.0, 0.1, 0.2, 0.3}, 0.1); frames[2].distance_m = 0.3; CHECK_THROWS_AS(sweep::Place(frames, "test"), JFJochException); } TEST_CASE("SweepLayout_SecondWavelengthRefused") { auto frames = Series({0.0, 0.1, 0.2, 0.3}, 0.1); frames[3].wavelength_A = 1.9; CHECK_THROWS_AS(sweep::Place(frames, "test"), JFJochException); } TEST_CASE("SweepLayout_RepeatedAngleRefused") { // Two sweeps of the same crystal concatenated: the second covers angles the first already has. CHECK_THROWS_AS(sweep::Place(Series({0.0, 0.1, 0.2, 0.1, 0.2}, 0.1), "test"), JFJochException); }