|
| 1 | +#include "gtest/gtest.h" |
| 2 | +#include "jres_solver/jres_solver.hpp" |
| 3 | +#include "nlohmann/json.hpp" |
| 4 | +#include <vector> |
| 5 | +#include <string> |
| 6 | +#include <iomanip> |
| 7 | +#include <sstream> |
| 8 | + |
| 9 | +using json = nlohmann::json; |
| 10 | + |
| 11 | +// Helper to format ISO8601 time |
| 12 | +std::string format_time(int hour) { |
| 13 | + std::ostringstream oss; |
| 14 | + int day = 17 + (hour / 24); |
| 15 | + oss << "2026-01-" << std::setfill('0') << std::setw(2) << day << "T" |
| 16 | + << std::setfill('0') << std::setw(2) << (hour % 24) << ":00:00"; |
| 17 | + return oss.str(); |
| 18 | +} |
| 19 | + |
| 20 | +TEST(LongRaceTest, Solves48HourRace) { |
| 21 | + // Scenario: 4 Team Members, 48 Stints (1 hour each). |
| 22 | + // This tests if the solver can handle a larger problem size (longer duration). |
| 23 | + |
| 24 | + json j; |
| 25 | + j["success"] = true; |
| 26 | + j["consecutiveStints"] = 2; // Encourage double stints |
| 27 | + j["minimumRestHours"] = 6; // Mandatory rest |
| 28 | + |
| 29 | + json members = json::array(); |
| 30 | + std::vector<std::string> names = {"Driver A", "Driver B", "Driver C", "Driver D"}; |
| 31 | + for (const auto& name : names) { |
| 32 | + members.push_back({ |
| 33 | + {"name", name}, |
| 34 | + {"isDriver", true}, |
| 35 | + {"isSpotter", true} |
| 36 | + }); |
| 37 | + } |
| 38 | + j["teamMembers"] = members; |
| 39 | + |
| 40 | + json stints = json::array(); |
| 41 | + int num_stints = 48; |
| 42 | + for (int i = 0; i < num_stints; ++i) { |
| 43 | + stints.push_back({ |
| 44 | + {"id", i + 1}, |
| 45 | + {"startTime", format_time(i)}, |
| 46 | + {"endTime", format_time(i + 1)} |
| 47 | + }); |
| 48 | + } |
| 49 | + j["stints"] = stints; |
| 50 | + |
| 51 | + // Everyone available all the time |
| 52 | + j["availability"] = json::object(); |
| 53 | + for (const auto& name : names) { |
| 54 | + json member_avail = json::object(); |
| 55 | + for (int i = 0; i < num_stints; ++i) { |
| 56 | + member_avail[format_time(i)] = "Available"; |
| 57 | + } |
| 58 | + // Also add the end time of the last stint as an availability point? |
| 59 | + // The solver typically looks at stint start times or specific intervals. |
| 60 | + // Based on other tests, it seems to be keyed by time. |
| 61 | + // Let's just ensure we cover the stint start times. |
| 62 | + j["availability"][name] = member_avail; |
| 63 | + } |
| 64 | + |
| 65 | + j["firstStintDriver"] = nullptr; |
| 66 | + |
| 67 | + std::string json_str = j.dump(); |
| 68 | + JresSolverInput* input = jres_input_from_json(json_str.c_str()); |
| 69 | + ASSERT_NE(input, nullptr); |
| 70 | + |
| 71 | + JresSolverOptions options = {}; |
| 72 | + options.timeLimit = 30; // Give it a bit more time for a larger problem |
| 73 | + options.spotterMode = JRES_SPOTTER_MODE_INTEGRATED; |
| 74 | + options.allowNoSpotter = false; |
| 75 | + options.optimalityGap = 0.05; // Allow 5% gap to speed it up |
| 76 | + |
| 77 | + JresSolverOutput* output = solve_race_schedule(input, &options); |
| 78 | + |
| 79 | + // Check if we got a solution |
| 80 | + ASSERT_NE(output, nullptr); |
| 81 | + EXPECT_EQ(output->schedule_len, num_stints); |
| 82 | + EXPECT_EQ(output->diagnosis_len, 0); |
| 83 | + |
| 84 | + // Basic validation: ensure no one drives > 4 hours in 6 hours (implicit check by solver, but good to know it solved) |
| 85 | + |
| 86 | + free_jres_solver_output(output); |
| 87 | + free_jres_solver_input(input); |
| 88 | +} |
| 89 | + |
| 90 | +TEST(LongRaceTest, DaySpecificAvailability) { |
| 91 | + // Scenario: Verify that availability at 10:00 Day 1 is distinct from 10:00 Day 2. |
| 92 | + // Stint 10 starts at 10:00 on Day 1. |
| 93 | + // Stint 34 starts at 10:00 on Day 2 (10 + 24 = 34). |
| 94 | + |
| 95 | + // We will restrict availability such that: |
| 96 | + // - Stint 10 MUST be driven by Driver A (Driver B is unavailable). |
| 97 | + // - Stint 34 MUST be driven by Driver B (Driver A is unavailable). |
| 98 | + // If the solver conflates days, it might think Driver A is unavailable at Stint 10 (if Day 2 overwrites Day 1) |
| 99 | + // or Driver A is available at Stint 34 (if Day 1 overwrites Day 2). |
| 100 | + |
| 101 | + json j; |
| 102 | + j["success"] = true; |
| 103 | + j["consecutiveStints"] = 1; |
| 104 | + j["minimumRestHours"] = 0; |
| 105 | + |
| 106 | + json members = json::array(); |
| 107 | + members.push_back({{"name", "Driver A"}, {"isDriver", true}, {"isSpotter", false}}); |
| 108 | + members.push_back({{"name", "Driver B"}, {"isDriver", true}, {"isSpotter", false}}); |
| 109 | + members.push_back({{"name", "Driver C"}, {"isDriver", true}, {"isSpotter", false}}); |
| 110 | + j["teamMembers"] = members; |
| 111 | + |
| 112 | + json stints = json::array(); |
| 113 | + int num_stints = 48; |
| 114 | + for (int i = 0; i < num_stints; ++i) { |
| 115 | + stints.push_back({ |
| 116 | + {"id", i + 1}, |
| 117 | + {"startTime", format_time(i)}, |
| 118 | + {"endTime", format_time(i + 1)} |
| 119 | + }); |
| 120 | + } |
| 121 | + j["stints"] = stints; |
| 122 | + |
| 123 | + j["availability"] = json::object(); |
| 124 | + |
| 125 | + // Default: Everyone available everywhere |
| 126 | + json avail_A = json::object(); |
| 127 | + json avail_B = json::object(); |
| 128 | + json avail_C = json::object(); |
| 129 | + for (int i = 0; i < num_stints; ++i) { |
| 130 | + avail_A[format_time(i)] = "Available"; |
| 131 | + avail_B[format_time(i)] = "Available"; |
| 132 | + avail_C[format_time(i)] = "Available"; |
| 133 | + } |
| 134 | + |
| 135 | + // Constraint for Stint 10 (Hour 10) |
| 136 | + // Driver B and C are unavailable, so A must drive |
| 137 | + avail_B[format_time(10)] = "Unavailable"; |
| 138 | + avail_C[format_time(10)] = "Unavailable"; |
| 139 | + |
| 140 | + // Constraint for Stint 34 (Hour 34 = 10 + 24) |
| 141 | + // Driver A and C are unavailable, so B must drive |
| 142 | + avail_A[format_time(34)] = "Unavailable"; |
| 143 | + avail_C[format_time(34)] = "Unavailable"; |
| 144 | + |
| 145 | + j["availability"]["Driver A"] = avail_A; |
| 146 | + j["availability"]["Driver B"] = avail_B; |
| 147 | + j["availability"]["Driver C"] = avail_C; |
| 148 | + |
| 149 | + j["firstStintDriver"] = nullptr; |
| 150 | + |
| 151 | + std::string json_str = j.dump(); |
| 152 | + JresSolverInput* input = jres_input_from_json(json_str.c_str()); |
| 153 | + ASSERT_NE(input, nullptr); |
| 154 | + |
| 155 | + JresSolverOptions options = {}; |
| 156 | + options.timeLimit = 30; |
| 157 | + options.spotterMode = JRES_SPOTTER_MODE_NONE; |
| 158 | + options.allowNoSpotter = true; // No spotters needed for this test |
| 159 | + options.optimalityGap = 0.0; |
| 160 | + |
| 161 | + JresSolverOutput* output = solve_race_schedule(input, &options); |
| 162 | + |
| 163 | + ASSERT_NE(output, nullptr); |
| 164 | + if (output->diagnosis_len > 0) { |
| 165 | + for(int i=0; i<output->diagnosis_len; ++i) { |
| 166 | + std::cout << "Diagnosis: " << output->diagnosis[i] << std::endl; |
| 167 | + } |
| 168 | + } |
| 169 | + EXPECT_EQ(output->diagnosis_len, 0); |
| 170 | + EXPECT_EQ(output->schedule_len, num_stints); |
| 171 | + |
| 172 | + // Verify Stint 10 |
| 173 | + // schedule[10] corresponds to stint with id 11 (index 10) |
| 174 | + // startTime is format_time(10) |
| 175 | + EXPECT_STREQ(output->schedule[10].driver, "Driver A") |
| 176 | + << "Stint 10 (Day 1 10:00) should be Driver A. Driver B was unavailable."; |
| 177 | + |
| 178 | + // Verify Stint 34 |
| 179 | + // schedule[34] corresponds to stint with id 35 (index 34) |
| 180 | + // startTime is format_time(34) |
| 181 | + EXPECT_STREQ(output->schedule[34].driver, "Driver B") |
| 182 | + << "Stint 34 (Day 2 10:00) should be Driver B. Driver A was unavailable."; |
| 183 | + |
| 184 | + free_jres_solver_output(output); |
| 185 | + free_jres_solver_input(input); |
| 186 | +} |
0 commit comments