Skip to content

Commit 7bddfa4

Browse files
committed
additional validation
duplicate names not allowed spotter mode requires all spotters
1 parent ee449c5 commit 7bddfa4

4 files changed

Lines changed: 110 additions & 0 deletions

File tree

src/jres_solver_base.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44
* @brief Base class for the JRES Solver.
55
*/
66
#include "jres_solver_base.hpp"
7+
#include <set>
8+
#include <stdexcept>
79

810
JresSolverBase::JresSolverBase(const jres::internal::SolverInput& input, const JresSolverOptions& options)
911
: m_input(input), m_options(options)
1012
{
1113
// Filter Participant Pools
14+
std::set<std::string> seenNames;
1215
for (const auto& member : m_input.teamMembers) {
16+
if (seenNames.count(member.name)) {
17+
throw std::runtime_error("Duplicate team member name: " + member.name);
18+
}
19+
seenNames.insert(member.name);
20+
1321
if (member.isDriver) m_driverPool.push_back(member);
1422
if (member.isSpotter) m_spotterPool.push_back(member);
1523
}

src/jres_standard_solver.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <sstream>
1515
#include <iostream>
1616
#include <iomanip>
17+
#include <set>
1718

1819
#include "Highs.h"
1920

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

147+
// --- Duplicate Name Check ---
148+
std::set<std::string> namesSeen;
149+
for (const auto& m : m_input.teamMembers) {
150+
if (namesSeen.count(m.name)) {
151+
std::string err = "Duplicate team member name: " + m.name;
152+
throw std::runtime_error(err);
153+
}
154+
namesSeen.insert(m.name);
155+
}
156+
146157
// --- Arithmetic Pre-flight Check ---
147158
int totalStints = (int)m_input.stints.size();
148159
auto capAnalysis = jres::internal::CapacityAnalyzer::calculate_max_potential_capacity(m_driverPool, m_input);
@@ -557,6 +568,18 @@ jres::internal::SolverOutput JresStandardSolver::solve()
557568
}
558569
}
559570

571+
// --- Final Validation ---
572+
for (size_t s = 0; s < output.schedule.size(); ++s) {
573+
if (output.schedule[s].driver == "N/A") {
574+
output.diagnosis.push_back("Stint " + std::to_string(s) + " (" + output.schedule[s].startTime + ") has no assigned driver.");
575+
}
576+
577+
bool spotterRequired = (m_options.spotterMode != JRES_SPOTTER_MODE_NONE && !m_options.allowNoSpotter);
578+
if (spotterRequired && output.schedule[s].spotter == "N/A") {
579+
output.diagnosis.push_back("Stint " + std::to_string(s) + " (" + output.schedule[s].startTime + ") has no assigned spotter.");
580+
}
581+
}
582+
560583
output.teamMembers = m_input.teamMembers;
561584
return output;
562585
}

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ add_executable(solver_tests
4141
test_max_busy_defaults.cpp
4242
test_availability_overlap.cpp
4343
test_availability_boundaries.cpp
44+
test_validation.cpp
4445
)
4546

4647
add_dependencies(solver_tests jres_solver_lib)

test/test_validation.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#include "gtest/gtest.h"
2+
#include "jres_solver/jres_solver.hpp"
3+
#include <string>
4+
5+
namespace {
6+
7+
const char* DUPLICATE_NAMES_JSON = R"({
8+
"teamMembers": [
9+
{ "name": "DriverA", "isDriver": true },
10+
{ "name": "DriverA", "isDriver": true }
11+
],
12+
"availability": {},
13+
"stints": [ { "id": 1, "startTime": "2024-01-01T12:00:00.000Z", "endTime": "2024-01-01T13:00:00.000Z" } ]
14+
})";
15+
16+
TEST(ValidationTest, DuplicateNames) {
17+
JresSolverOptions options = {};
18+
options.spotterMode = JRES_SPOTTER_MODE_NONE;
19+
20+
JresSolverInput* input = jres_input_from_json(DUPLICATE_NAMES_JSON);
21+
ASSERT_NE(input, nullptr);
22+
23+
JresSolverOutput* output = solve_race_schedule(input, &options);
24+
ASSERT_NE(output, nullptr);
25+
26+
// Expect diagnosis
27+
ASSERT_GT(output->diagnosis_len, 0);
28+
bool foundDuplicateMsg = false;
29+
for (int i = 0; i < output->diagnosis_len; ++i) {
30+
std::string msg = output->diagnosis[i];
31+
if (msg.find("Duplicate team member name") != std::string::npos) {
32+
foundDuplicateMsg = true;
33+
break;
34+
}
35+
}
36+
EXPECT_TRUE(foundDuplicateMsg) << "Should diagnose duplicate names";
37+
38+
free_jres_solver_input(input);
39+
free_jres_solver_output(output);
40+
}
41+
42+
const char* NO_SPOTTERS_SEQUENTIAL_JSON = R"({
43+
"teamMembers": [
44+
{ "name": "DriverA", "isDriver": true, "isSpotter": false },
45+
{ "name": "SpotterA", "isDriver": false, "isSpotter": false }
46+
],
47+
"availability": {},
48+
"stints": [ { "id": 1, "startTime": "2024-01-01T12:00:00.000Z", "endTime": "2024-01-01T13:00:00.000Z" } ]
49+
})";
50+
51+
TEST(ValidationTest, UnassignedSpotterSequential) {
52+
JresSolverOptions options = {};
53+
options.spotterMode = JRES_SPOTTER_MODE_SEQUENTIAL;
54+
options.allowNoSpotter = false;
55+
56+
JresSolverInput* input = jres_input_from_json(NO_SPOTTERS_SEQUENTIAL_JSON);
57+
ASSERT_NE(input, nullptr);
58+
59+
JresSolverOutput* output = solve_race_schedule(input, &options);
60+
ASSERT_NE(output, nullptr);
61+
62+
// Expect diagnosis about unassigned spotter
63+
ASSERT_GT(output->diagnosis_len, 0);
64+
bool foundSpecificMsg = false;
65+
for (int i = 0; i < output->diagnosis_len; ++i) {
66+
std::string msg = output->diagnosis[i];
67+
if (msg.find("Stint 0") != std::string::npos && msg.find("has no assigned spotter") != std::string::npos) {
68+
foundSpecificMsg = true;
69+
break;
70+
}
71+
}
72+
73+
EXPECT_TRUE(foundSpecificMsg) << "Should diagnose specific unassigned stint for spotter";
74+
75+
free_jres_solver_input(input);
76+
free_jres_solver_output(output);
77+
}
78+
}

0 commit comments

Comments
 (0)