Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/jres_solver_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@
* @brief Base class for the JRES Solver.
*/
#include "jres_solver_base.hpp"
#include <set>
#include <stdexcept>

JresSolverBase::JresSolverBase(const jres::internal::SolverInput& input, const JresSolverOptions& options)
: m_input(input), m_options(options)
{
// Filter Participant Pools
std::set<std::string> seenNames;
for (const auto& member : m_input.teamMembers) {
if (seenNames.count(member.name)) {
throw std::runtime_error("Duplicate team member name: " + member.name);
}
seenNames.insert(member.name);

if (member.isDriver) m_driverPool.push_back(member);
if (member.isSpotter) m_spotterPool.push_back(member);
}
Expand Down
23 changes: 23 additions & 0 deletions src/jres_standard_solver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <sstream>
#include <iostream>
#include <iomanip>
#include <set>

#include "Highs.h"

Expand Down Expand Up @@ -143,6 +144,16 @@ jres::internal::SolverOutput JresStandardSolver::solve()
output.config.maximumBusyHours = m_input.maximumBusyHours;
output.config.firstStintDriver = m_input.firstStintDriver;

// --- Duplicate Name Check ---
std::set<std::string> namesSeen;
for (const auto& m : m_input.teamMembers) {
if (namesSeen.count(m.name)) {
std::string err = "Duplicate team member name: " + m.name;
throw std::runtime_error(err);
}
namesSeen.insert(m.name);
}

// --- Arithmetic Pre-flight Check ---
int totalStints = (int)m_input.stints.size();
auto capAnalysis = jres::internal::CapacityAnalyzer::calculate_max_potential_capacity(m_driverPool, m_input);
Expand Down Expand Up @@ -557,6 +568,18 @@ jres::internal::SolverOutput JresStandardSolver::solve()
}
}

// --- Final Validation ---
for (size_t s = 0; s < output.schedule.size(); ++s) {
if (output.schedule[s].driver == "N/A") {
output.diagnosis.push_back("Stint " + std::to_string(s) + " (" + output.schedule[s].startTime + ") has no assigned driver.");
}

bool spotterRequired = (m_options.spotterMode != JRES_SPOTTER_MODE_NONE && !m_options.allowNoSpotter);
if (spotterRequired && output.schedule[s].spotter == "N/A") {
output.diagnosis.push_back("Stint " + std::to_string(s) + " (" + output.schedule[s].startTime + ") has no assigned spotter.");
}
}

output.teamMembers = m_input.teamMembers;
return output;
}
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ add_executable(solver_tests
test_max_busy_defaults.cpp
test_availability_overlap.cpp
test_availability_boundaries.cpp
test_validation.cpp
)

add_dependencies(solver_tests jres_solver_lib)
Expand Down
78 changes: 78 additions & 0 deletions test/test_validation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#include "gtest/gtest.h"
#include "jres_solver/jres_solver.hpp"
#include <string>

namespace {

const char* DUPLICATE_NAMES_JSON = R"({
"teamMembers": [
{ "name": "DriverA", "isDriver": true },
{ "name": "DriverA", "isDriver": true }
],
"availability": {},
"stints": [ { "id": 1, "startTime": "2024-01-01T12:00:00.000Z", "endTime": "2024-01-01T13:00:00.000Z" } ]
})";

TEST(ValidationTest, DuplicateNames) {
JresSolverOptions options = {};
options.spotterMode = JRES_SPOTTER_MODE_NONE;

JresSolverInput* input = jres_input_from_json(DUPLICATE_NAMES_JSON);
ASSERT_NE(input, nullptr);

JresSolverOutput* output = solve_race_schedule(input, &options);
ASSERT_NE(output, nullptr);

// Expect diagnosis
ASSERT_GT(output->diagnosis_len, 0);
bool foundDuplicateMsg = false;
for (int i = 0; i < output->diagnosis_len; ++i) {
std::string msg = output->diagnosis[i];
if (msg.find("Duplicate team member name") != std::string::npos) {
foundDuplicateMsg = true;
break;
}
}
EXPECT_TRUE(foundDuplicateMsg) << "Should diagnose duplicate names";

free_jres_solver_input(input);
free_jres_solver_output(output);
}

const char* NO_SPOTTERS_SEQUENTIAL_JSON = R"({
"teamMembers": [
{ "name": "DriverA", "isDriver": true, "isSpotter": false },
{ "name": "SpotterA", "isDriver": false, "isSpotter": false }
],
"availability": {},
"stints": [ { "id": 1, "startTime": "2024-01-01T12:00:00.000Z", "endTime": "2024-01-01T13:00:00.000Z" } ]
})";

TEST(ValidationTest, UnassignedSpotterSequential) {
JresSolverOptions options = {};
options.spotterMode = JRES_SPOTTER_MODE_SEQUENTIAL;
options.allowNoSpotter = false;

JresSolverInput* input = jres_input_from_json(NO_SPOTTERS_SEQUENTIAL_JSON);
ASSERT_NE(input, nullptr);

JresSolverOutput* output = solve_race_schedule(input, &options);
ASSERT_NE(output, nullptr);

// Expect diagnosis about unassigned spotter
ASSERT_GT(output->diagnosis_len, 0);
bool foundSpecificMsg = false;
for (int i = 0; i < output->diagnosis_len; ++i) {
std::string msg = output->diagnosis[i];
if (msg.find("Stint 0") != std::string::npos && msg.find("has no assigned spotter") != std::string::npos) {
foundSpecificMsg = true;
break;
}
}

EXPECT_TRUE(foundSpecificMsg) << "Should diagnose specific unassigned stint for spotter";

free_jres_solver_input(input);
free_jres_solver_output(output);
}
}