diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt index 84ab34293..e85797d04 100644 --- a/src/plugin/CMakeLists.txt +++ b/src/plugin/CMakeLists.txt @@ -484,7 +484,7 @@ set(src__intention "intention/SectorExitRepositoryFactory.h" "intention/ShannonAirfieldGroup.cpp" "intention/ShannonAirfieldGroup.h" -) + intention/FirExitPointCollection.cpp intention/FirExitPointCollection.h intention/FirExitPointCollectionFactory.cpp intention/FirExitPointCollectionFactory.h) source_group("src\\intention" FILES ${src__intention}) set(src__list diff --git a/src/plugin/flightplan/FlightplanPoint.cpp b/src/plugin/flightplan/FlightplanPoint.cpp index e23323be1..0e9078d82 100644 --- a/src/plugin/flightplan/FlightplanPoint.cpp +++ b/src/plugin/flightplan/FlightplanPoint.cpp @@ -6,6 +6,7 @@ namespace UKControllerPlugin::Flightplan { int index, std::string identifier, std::shared_ptr position) : index(index), identifier(std::move(identifier)), position(std::move(position)) { + assert(!position && "No position in flightplan point"); } auto FlightplanPoint::Index() const -> int diff --git a/src/plugin/intention/CachedAircraftFirExitGenerator.cpp b/src/plugin/intention/CachedAircraftFirExitGenerator.cpp index 13396ca87..c35ad2955 100644 --- a/src/plugin/intention/CachedAircraftFirExitGenerator.cpp +++ b/src/plugin/intention/CachedAircraftFirExitGenerator.cpp @@ -1,10 +1,21 @@ #include "AircraftFirExit.h" #include "CachedAircraftFirExitGenerator.h" +#include "ExitDetermination.h" +#include "FirExitPoint.h" +#include "FirExitPointCollection.h" #include "euroscope/EuroScopeCFlightPlanInterface.h" #include "flightplan/FlightplanPoint.h" #include "flightplan/ParsedFlightplan.h" namespace UKControllerPlugin::IntentionCode { + + CachedAircraftFirExitGenerator::CachedAircraftFirExitGenerator( + std::shared_ptr firExitPoints) + : firExitPoints(firExitPoints) + { + assert(firExitPoints && "FIR exit point collection not set"); + } + void CachedAircraftFirExitGenerator::AddCacheEntry(const std::shared_ptr& entry) { cache[entry->callsign] = entry; @@ -29,18 +40,58 @@ namespace UKControllerPlugin::IntentionCode { auto CachedAircraftFirExitGenerator::Generate(Euroscope::EuroScopeCFlightPlanInterface& flightplan) -> std::shared_ptr { + auto cachedEntry = GetCacheEntryForCallsign(flightplan.GetCallsign()); + if (cachedEntry) { + return cachedEntry; + } + auto exit = AircraftFirExit{}; exit.callsign = flightplan.GetCallsign(); const auto parsedFlightplan = flightplan.GetParsedFlightplan(); - int i = 0; - while (true) { - break; + for (size_t i = 0; i < parsedFlightplan->CountPoints(); i++) { + const auto flightplanPoint = parsedFlightplan->PointByIndex(i); - i++; + // If we've reached the end of the flightplan, stop. + if (!flightplanPoint) { + break; + } + + const auto firExitPoint = firExitPoints->PointByIdentifier(flightplanPoint->Identifier()); + if (!firExitPoint) { + continue; + } + + /* + * If the aircraft is exiting between the two FIRs, we only care about recording this if we don't already + * have an internal exit point and the aircraft is exiting. + */ + if (firExitPoint->IsInternal()) { + if (exit.internalExitPoint) { + continue; + } + + if (firExitPoint->FirExitDetermination().AircraftIsExiting(*flightplanPoint, flightplan)) { + exit.internalExitPoint = firExitPoint; + } + + continue; + } + + /* + * If the aircraft is not exiting between the two FIRs, then we have reached the UK exit and don't need + * to check this any further. + */ + if (firExitPoint->FirExitDetermination().AircraftIsExiting(*flightplanPoint, flightplan)) { + exit.ukExitPoint = firExitPoint; + break; + } } - return std::make_shared(exit); + const auto cacheItem = std::make_shared(exit); + AddCacheEntry(cacheItem); + + return cacheItem; } } // namespace UKControllerPlugin::IntentionCode diff --git a/src/plugin/intention/CachedAircraftFirExitGenerator.h b/src/plugin/intention/CachedAircraftFirExitGenerator.h index d75c7605a..e47853780 100644 --- a/src/plugin/intention/CachedAircraftFirExitGenerator.h +++ b/src/plugin/intention/CachedAircraftFirExitGenerator.h @@ -3,6 +3,7 @@ namespace UKControllerPlugin::IntentionCode { struct AircraftFirExit; + class FirExitPointCollection; /* Generates aircraft FIR exit data and caches the result for future use. @@ -10,6 +11,7 @@ namespace UKControllerPlugin::IntentionCode { class CachedAircraftFirExitGenerator : public AircraftFirExitGenerator { public: + CachedAircraftFirExitGenerator(std::shared_ptr firExitPoints); void AddCacheEntry(const std::shared_ptr& entry); [[nodiscard]] auto GetCacheEntryForCallsign(const std::string& callsign) const -> std::shared_ptr; @@ -22,6 +24,9 @@ namespace UKControllerPlugin::IntentionCode { -> std::shared_ptr override; private: + // The FIR exit points + std::shared_ptr firExitPoints; + // The cache std::map> cache; }; diff --git a/src/plugin/intention/ExitCone.cpp b/src/plugin/intention/ExitCone.cpp index ec1ed8fc6..a19f01531 100644 --- a/src/plugin/intention/ExitCone.cpp +++ b/src/plugin/intention/ExitCone.cpp @@ -11,7 +11,7 @@ namespace UKControllerPlugin::IntentionCode { } auto ExitCone::AircraftIsExiting( - const Flightplan::FlightplanPoint& flightplanPoint, Euroscope::EuroScopeCFlightPlanInterface& flightplan) + const Flightplan::FlightplanPoint& flightplanPoint, Euroscope::EuroScopeCFlightPlanInterface& flightplan) const -> bool { const auto nextPoint = flightplan.GetParsedFlightplan()->PointByIndex(flightplanPoint.Index() + 1); @@ -25,4 +25,14 @@ namespace UKControllerPlugin::IntentionCode { return endDirection <= startDirection ? exitDirection >= startDirection || exitDirection <= endDirection : exitDirection >= startDirection && exitDirection <= endDirection; } + + auto ExitCone::StartDirection() const -> int + { + return startDirection; + } + + auto ExitCone::EndDirection() const -> int + { + return endDirection; + } } // namespace UKControllerPlugin::IntentionCode diff --git a/src/plugin/intention/ExitCone.h b/src/plugin/intention/ExitCone.h index 258a4c7be..4f89a32e4 100644 --- a/src/plugin/intention/ExitCone.h +++ b/src/plugin/intention/ExitCone.h @@ -7,8 +7,10 @@ namespace UKControllerPlugin::IntentionCode { public: ExitCone(int startDirection, int endDirection); [[nodiscard]] auto AircraftIsExiting( - const Flightplan::FlightplanPoint& flightplanPoint, Euroscope::EuroScopeCFlightPlanInterface& flightplan) - -> bool override; + const Flightplan::FlightplanPoint& flightplanPoint, + Euroscope::EuroScopeCFlightPlanInterface& flightplan) const -> bool override; + [[nodiscard]] auto StartDirection() const -> int; + [[nodiscard]] auto EndDirection() const -> int; private: // The start of the exit direction cone diff --git a/src/plugin/intention/ExitDetermination.h b/src/plugin/intention/ExitDetermination.h index 6b4145c21..48c75371e 100644 --- a/src/plugin/intention/ExitDetermination.h +++ b/src/plugin/intention/ExitDetermination.h @@ -19,7 +19,7 @@ namespace UKControllerPlugin::IntentionCode { public: virtual ~ExitDetermination() = default; [[nodiscard]] virtual auto AircraftIsExiting( - const Flightplan::FlightplanPoint& flightplanPoint, Euroscope::EuroScopeCFlightPlanInterface& flightplan) - -> bool = 0; + const Flightplan::FlightplanPoint& flightplanPoint, + Euroscope::EuroScopeCFlightPlanInterface& flightplan) const -> bool = 0; }; } // namespace UKControllerPlugin::IntentionCode diff --git a/src/plugin/intention/FirExitPointCollection.cpp b/src/plugin/intention/FirExitPointCollection.cpp new file mode 100644 index 000000000..554248af8 --- /dev/null +++ b/src/plugin/intention/FirExitPointCollection.cpp @@ -0,0 +1,25 @@ +#include "FirExitPoint.h" +#include "FirExitPointCollection.h" + +namespace UKControllerPlugin::IntentionCode { + + void FirExitPointCollection::Add(const std::shared_ptr& point) + { + if (points.contains(point->Identifier())) { + LogWarning("Duplicate FIR exit point added"); + return; + } + + points[point->Identifier()] = point; + } + + auto FirExitPointCollection::CountPoints() const -> size_t + { + return points.size(); + } + + auto FirExitPointCollection::PointByIdentifier(const std::string& identifier) const -> std::shared_ptr + { + return points.contains(identifier) ? points.at(identifier) : nullptr; + } +} // namespace UKControllerPlugin::IntentionCode diff --git a/src/plugin/intention/FirExitPointCollection.h b/src/plugin/intention/FirExitPointCollection.h new file mode 100644 index 000000000..8ecb1c28d --- /dev/null +++ b/src/plugin/intention/FirExitPointCollection.h @@ -0,0 +1,22 @@ +#pragma once + +namespace UKControllerPlugin::IntentionCode { + + class FirExitPoint; + + /** + * All the FIR exit points in one handy place + */ + class FirExitPointCollection + { + public: + void Add(const std::shared_ptr& point); + [[nodiscard]] auto CountPoints() const -> size_t; + [[nodiscard]] auto PointByIdentifier(const std::string& identifier) const -> std::shared_ptr; + + private: + // All the points, indexed by identifier + std::map> points; + }; + +} // namespace UKControllerPlugin::IntentionCode diff --git a/src/plugin/intention/FirExitPointCollectionFactory.cpp b/src/plugin/intention/FirExitPointCollectionFactory.cpp new file mode 100644 index 000000000..4d13d5968 --- /dev/null +++ b/src/plugin/intention/FirExitPointCollectionFactory.cpp @@ -0,0 +1,55 @@ +#include "ExitCone.h" +#include "FirExitPoint.h" +#include "FirExitPointCollection.h" +#include "FirExitPointCollectionFactory.h" + +namespace UKControllerPlugin::IntentionCode { + + auto MakeFirExitPointCollection(const nlohmann::json& exitPointData) -> std::shared_ptr + { + auto collection = std::make_shared(); + if (!exitPointData.is_array()) { + return collection; + } + + for (const auto& exitPoint : exitPointData) { + if (!ExitPointValid(exitPoint)) { + LogError("Invalid FIR exit point: " + exitPoint.dump()); + continue; + } + + collection->Add(std::make_shared( + exitPoint.at("id").get(), + exitPoint.at("exit_point").get(), + exitPoint.at("internal").get(), + std::make_shared( + exitPoint.at("exit_direction_start").get(), exitPoint.at("exit_direction_end").get()))); + } + + LogInfo("Created FIR Exit Points collection with " + std::to_string(collection->CountPoints()) + " points"); + return collection; + } + + auto ExitPointValid(const nlohmann::json& exitPointData) -> bool + { + return exitPointData.is_object() && exitPointData.contains("id") && + exitPointData.at("id").is_number_integer() && exitPointData.contains("exit_point") && + exitPointData.at("exit_point").is_string() && exitPointData.contains("internal") && + exitPointData.at("internal").is_boolean() && HeadingValid("exit_direction_start", exitPointData) && + HeadingValid("exit_direction_end", exitPointData); + } + + auto HeadingValid(const std::string& key, const nlohmann::json& exitPointData) -> bool + { + if (!exitPointData.contains(key)) { + return false; + } + + if (!exitPointData.at(key).is_number_integer()) { + return false; + } + + const auto heading = exitPointData.at(key).get(); + return heading >= 0 && heading <= 360; + } +} // namespace UKControllerPlugin::IntentionCode diff --git a/src/plugin/intention/FirExitPointCollectionFactory.h b/src/plugin/intention/FirExitPointCollectionFactory.h new file mode 100644 index 000000000..79b6b3379 --- /dev/null +++ b/src/plugin/intention/FirExitPointCollectionFactory.h @@ -0,0 +1,11 @@ +#pragma once + +namespace UKControllerPlugin::IntentionCode { + + class FirExitPointCollection; + + [[nodiscard]] auto MakeFirExitPointCollection(const nlohmann::json& exitPointData) + -> std::shared_ptr; + [[nodiscard]] auto ExitPointValid(const nlohmann::json& exitPointData) -> bool; + [[nodiscard]] auto HeadingValid(const std::string& key, const nlohmann::json& exitPointData) -> bool; +} // namespace UKControllerPlugin::IntentionCode diff --git a/test/plugin/CMakeLists.txt b/test/plugin/CMakeLists.txt index c1074800f..3f688bd24 100644 --- a/test/plugin/CMakeLists.txt +++ b/test/plugin/CMakeLists.txt @@ -254,7 +254,7 @@ set(test__intention "intention/SectorExitPointVeuleTest.cpp" "intention/SectorExitRepositoryTest.cpp" "intention/ShannonAirfieldGroupTest.cpp" -) + intention/FirExitPointCollectionTest.cpp intention/CachedAircraftFirExitGeneratorTest.cpp intention/FirExitPointCollectionFactoryTest.cpp) source_group("test\\intention" FILES ${test__intention}) set(test__list diff --git a/test/plugin/intention/CachedAircraftFirExitGeneratorTest.cpp b/test/plugin/intention/CachedAircraftFirExitGeneratorTest.cpp new file mode 100644 index 000000000..db145510e --- /dev/null +++ b/test/plugin/intention/CachedAircraftFirExitGeneratorTest.cpp @@ -0,0 +1,183 @@ +#include "flightplan/FlightplanPoint.h" +#include "flightplan/ParsedFlightplan.h" +#include "intention/AircraftFirExit.h" +#include "intention/CachedAircraftFirExitGenerator.h" +#include "intention/FirExitPoint.h" +#include "intention/FirExitPointCollection.h" + +using UKControllerPlugin::Flightplan::FlightplanPoint; +using UKControllerPlugin::Flightplan::ParsedFlightplan; +using UKControllerPlugin::IntentionCode::AircraftFirExit; +using UKControllerPlugin::IntentionCode::CachedAircraftFirExitGenerator; +using UKControllerPlugin::IntentionCode::FirExitPoint; +using UKControllerPlugin::IntentionCode::FirExitPointCollection; + +namespace UKControllerPluginTest::IntentionCode { + class CachedAircraftFirExitGeneratorTest : public testing::Test + { + public: + CachedAircraftFirExitGeneratorTest() + : coordinate(std::make_shared>()), + parsedFlightplan(std::make_shared()), + exitDetermination1(std::make_shared>()), + exitDetermination2(std::make_shared>()), + exitDetermination3(std::make_shared>()), + exitDetermination4(std::make_shared>()), + exitDetermination5(std::make_shared>()), + exitDetermination6(std::make_shared>()), + collection(std::make_shared()), + point1(std::make_shared(1, "FOO", false, exitDetermination1)), + point2(std::make_shared(2, "WOO", false, exitDetermination2)), + point3(std::make_shared(3, "DOO", false, exitDetermination3)), + point4(std::make_shared(4, "MOO", true, exitDetermination4)), + point5(std::make_shared(5, "BOO", true, exitDetermination5)), + point6(std::make_shared(6, "LOO", true, exitDetermination6)), generator(collection) + { + ON_CALL(flightplan, GetCallsign).WillByDefault(testing::Return("BAW123")); + ON_CALL(flightplan, GetParsedFlightplan).WillByDefault(testing::Return(parsedFlightplan)); + + collection->Add(point1); + collection->Add(point2); + collection->Add(point3); + collection->Add(point4); + collection->Add(point5); + collection->Add(point6); + } + + std::shared_ptr> coordinate; + std::shared_ptr parsedFlightplan; + testing::NiceMock flightplan; + std::shared_ptr> exitDetermination1; + std::shared_ptr> exitDetermination2; + std::shared_ptr> exitDetermination3; + std::shared_ptr> exitDetermination4; + std::shared_ptr> exitDetermination5; + std::shared_ptr> exitDetermination6; + std::shared_ptr collection; + std::shared_ptr point1; + std::shared_ptr point2; + std::shared_ptr point3; + std::shared_ptr point4; + std::shared_ptr point5; + std::shared_ptr point6; + CachedAircraftFirExitGenerator generator; + }; + + TEST_F(CachedAircraftFirExitGeneratorTest, ItReturnsCachedEntry) + { + const auto entry = std::make_shared(); + entry->callsign = "BAW123"; + generator.AddCacheEntry(entry); + + EXPECT_EQ(entry, generator.GetCacheEntryForCallsign("BAW123")); + EXPECT_EQ(entry, generator.Generate(flightplan)); + } + + TEST_F(CachedAircraftFirExitGeneratorTest, ItReturnsEmptyEntryIfNoFlightplan) + { + const auto firExit = generator.Generate(flightplan); + EXPECT_EQ("BAW123", firExit->callsign); + EXPECT_EQ(nullptr, firExit->internalExitPoint); + EXPECT_EQ(nullptr, firExit->ukExitPoint); + EXPECT_EQ(firExit, generator.GetCacheEntryForCallsign("BAW123")); + } + + TEST_F(CachedAircraftFirExitGeneratorTest, ItReturnsOnlyTheUkExitPointIfSpecified) + { + parsedFlightplan->AddPoint(std::make_shared(0, "NOTTHIS", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(1, "FOO", coordinate)); + + EXPECT_CALL( + *exitDetermination1, + AircraftIsExiting(testing::Ref(*parsedFlightplan->PointByIndex(1)), testing::Ref(flightplan))) + .Times(1) + .WillOnce(testing::Return(true)); + + const auto firExit = generator.Generate(flightplan); + EXPECT_EQ("BAW123", firExit->callsign); + EXPECT_EQ(nullptr, firExit->internalExitPoint); + EXPECT_EQ(1, firExit->ukExitPoint->Id()); + EXPECT_EQ(firExit, generator.GetCacheEntryForCallsign("BAW123")); + } + + TEST_F(CachedAircraftFirExitGeneratorTest, ItReturnsTheFirstExitingUkExitPoint) + { + parsedFlightplan->AddPoint(std::make_shared(0, "NOTTHIS", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(1, "WOO", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(2, "FOO", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(3, "DOO", coordinate)); + + EXPECT_CALL( + *exitDetermination1, + AircraftIsExiting(testing::Ref(*parsedFlightplan->PointByIndex(2)), testing::Ref(flightplan))) + .Times(1) + .WillOnce(testing::Return(true)); + + EXPECT_CALL( + *exitDetermination2, + AircraftIsExiting(testing::Ref(*parsedFlightplan->PointByIndex(1)), testing::Ref(flightplan))) + .Times(1) + .WillOnce(testing::Return(false)); + + EXPECT_CALL(*exitDetermination3, AircraftIsExiting).Times(0); + + const auto firExit = generator.Generate(flightplan); + EXPECT_EQ("BAW123", firExit->callsign); + EXPECT_EQ(nullptr, firExit->internalExitPoint); + EXPECT_EQ(1, firExit->ukExitPoint->Id()); + EXPECT_EQ(firExit, generator.GetCacheEntryForCallsign("BAW123")); + } + + TEST_F(CachedAircraftFirExitGeneratorTest, ItReturnsInternalAndUkExitPoints) + { + parsedFlightplan->AddPoint(std::make_shared(0, "NOTTHIS", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(1, "MOO", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(2, "FOO", coordinate)); + + EXPECT_CALL( + *exitDetermination1, + AircraftIsExiting(testing::Ref(*parsedFlightplan->PointByIndex(2)), testing::Ref(flightplan))) + .Times(1) + .WillOnce(testing::Return(true)); + + EXPECT_CALL( + *exitDetermination4, + AircraftIsExiting(testing::Ref(*parsedFlightplan->PointByIndex(1)), testing::Ref(flightplan))) + .Times(1) + .WillOnce(testing::Return(true)); + + const auto firExit = generator.Generate(flightplan); + EXPECT_EQ("BAW123", firExit->callsign); + EXPECT_EQ(4, firExit->internalExitPoint->Id()); + EXPECT_EQ(1, firExit->ukExitPoint->Id()); + EXPECT_EQ(firExit, generator.GetCacheEntryForCallsign("BAW123")); + } + + TEST_F(CachedAircraftFirExitGeneratorTest, ItReturnsTheFirstInternalExitPointFound) + { + parsedFlightplan->AddPoint(std::make_shared(0, "NOTTHIS", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(1, "MOO", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(2, "BOO", coordinate)); + parsedFlightplan->AddPoint(std::make_shared(3, "LOO", coordinate)); + + EXPECT_CALL( + *exitDetermination4, + AircraftIsExiting(testing::Ref(*parsedFlightplan->PointByIndex(1)), testing::Ref(flightplan))) + .Times(1) + .WillOnce(testing::Return(false)); + + EXPECT_CALL( + *exitDetermination5, + AircraftIsExiting(testing::Ref(*parsedFlightplan->PointByIndex(2)), testing::Ref(flightplan))) + .Times(1) + .WillOnce(testing::Return(true)); + + EXPECT_CALL(*exitDetermination6, AircraftIsExiting).Times(0); + + const auto firExit = generator.Generate(flightplan); + EXPECT_EQ("BAW123", firExit->callsign); + EXPECT_EQ(5, firExit->internalExitPoint->Id()); + EXPECT_EQ(nullptr, firExit->ukExitPoint); + EXPECT_EQ(firExit, generator.GetCacheEntryForCallsign("BAW123")); + } +} // namespace UKControllerPluginTest::IntentionCode diff --git a/test/plugin/intention/FirExitPointCollectionFactoryTest.cpp b/test/plugin/intention/FirExitPointCollectionFactoryTest.cpp new file mode 100644 index 000000000..95cd00f04 --- /dev/null +++ b/test/plugin/intention/FirExitPointCollectionFactoryTest.cpp @@ -0,0 +1,157 @@ +#include "intention/ExitCone.h" +#include "intention/FirExitPointCollectionFactory.h" +#include "intention/FirExitPointCollection.h" +#include "intention/FirExitPoint.h" + +using UKControllerPlugin::IntentionCode::ExitCone; +using UKControllerPlugin::IntentionCode::ExitPointValid; +using UKControllerPlugin::IntentionCode::MakeFirExitPointCollection; + +namespace UKControllerPluginTest::IntentionCode { + class FirExitPointCollectionFactoryTest : public testing::Test + { + public: + static auto MakeExitPoint( + const nlohmann::json& overridingData = nlohmann::json::object(), const std::string& keyToRemove = "") + -> nlohmann::json + { + nlohmann::json exitPoint{ + {"id", 1}, + {"exit_point", "INKOB"}, + {"internal", false}, + {"exit_direction_start", 123}, + {"exit_direction_end", 234}}; + exitPoint.update(overridingData); + + if (!keyToRemove.empty()) { + exitPoint.erase(exitPoint.find(keyToRemove)); + } + + return exitPoint; + }; + }; + + TEST_F(FirExitPointCollectionFactoryTest, ItReturnsEmptyCollectionIfDataIsNotArray) + { + EXPECT_EQ(0, MakeFirExitPointCollection(nlohmann::json::object())->CountPoints()); + } + + TEST_F(FirExitPointCollectionFactoryTest, ItSkipsInvalidPoints) + { + const auto collection = MakeFirExitPointCollection(nlohmann::json::array( + {MakeExitPoint(nlohmann::json::object(), "id"), MakeExitPoint(nlohmann::json::object(), "exit_point")})); + EXPECT_EQ(0, collection->CountPoints()); + } + + TEST_F(FirExitPointCollectionFactoryTest, ItBuildsCollection) + { + const auto collection = MakeFirExitPointCollection(nlohmann::json::array( + {MakeExitPoint(), MakeExitPoint(nlohmann::json{{"id", 2}, {"exit_point", "SOSIM"}, {"internal", true}})})); + EXPECT_EQ(2, collection->CountPoints()); + + // Check the first point + const auto point1 = collection->PointByIdentifier("INKOB"); + EXPECT_EQ(1, point1->Id()); + EXPECT_EQ("INKOB", point1->Identifier()); + EXPECT_FALSE(point1->IsInternal()); + + const auto& exitCone1 = static_cast(point1->FirExitDetermination()); + EXPECT_EQ(123, exitCone1.StartDirection()); + EXPECT_EQ(234, exitCone1.EndDirection()); + + // Check the second point + const auto point2 = collection->PointByIdentifier("SOSIM"); + EXPECT_EQ(2, point2->Id()); + EXPECT_EQ("SOSIM", point2->Identifier()); + EXPECT_TRUE(point2->IsInternal()); + + const auto& exitCone2 = static_cast(point2->FirExitDetermination()); + EXPECT_EQ(123, exitCone2.StartDirection()); + EXPECT_EQ(234, exitCone2.EndDirection()); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsValid) + { + EXPECT_TRUE(ExitPointValid(MakeExitPoint())); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsValidWhenInternal) + { + EXPECT_TRUE(ExitPointValid(MakeExitPoint(nlohmann::json{{"internal", true}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidNotObject) + { + EXPECT_FALSE(ExitPointValid(nlohmann::json::array())); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidIdMissing) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json::object(), "id"))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidIdNotAnInteger) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"id", "abc"}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidExitPointMissing) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json::object(), "exit_point"))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidExitPointNotAString) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"exit_point", 123}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidInternalMissing) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json::object(), "internal"))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInternalNotABoolean) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"internal", "abc"}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidStartDirectionMissing) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json::object(), "exit_direction_start"))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidStartDirectionNotAnInteger) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"exit_direction_start", "abc"}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidStartDirectionTooSmall) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"exit_direction_start", -1}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidStartDirectionTooLarge) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"exit_direction_start", 361}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidEndDirectionMissing) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json::object(), "exit_direction_end"))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidEndDirectionNotAnInteger) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"exit_direction_end", "abc"}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidEndDirectionTooSmall) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"exit_direction_end", -1}}))); + } + + TEST_F(FirExitPointCollectionFactoryTest, ExitPointIsInvalidEndDirectionTooLarge) + { + EXPECT_FALSE(ExitPointValid(MakeExitPoint(nlohmann::json{{"exit_direction_end", 361}}))); + } +} // namespace UKControllerPluginTest::IntentionCode diff --git a/test/plugin/intention/FirExitPointCollectionTest.cpp b/test/plugin/intention/FirExitPointCollectionTest.cpp new file mode 100644 index 000000000..0d4822b53 --- /dev/null +++ b/test/plugin/intention/FirExitPointCollectionTest.cpp @@ -0,0 +1,65 @@ +#include "intention/FirExitPoint.h" +#include "intention/FirExitPointCollection.h" + +using UKControllerPlugin::IntentionCode::FirExitPoint; +using UKControllerPlugin::IntentionCode::FirExitPointCollection; + +namespace UKControllerPluginTest::IntentionCode { + class FirExitPointCollectionTest : public testing::Test + { + public: + FirExitPointCollectionTest() + : exitDetermination(std::make_shared>()), + point1(std::make_shared(1, "FOO", false, exitDetermination)), + point2(std::make_shared(2, "WOO", false, exitDetermination)), + point3(std::make_shared(3, "DOO", false, exitDetermination)) + { + } + + std::shared_ptr> exitDetermination; + std::shared_ptr point1; + std::shared_ptr point2; + std::shared_ptr point3; + FirExitPointCollection collection; + }; + + TEST_F(FirExitPointCollectionTest, ItStartsEmpty) + { + EXPECT_EQ(0, collection.CountPoints()); + } + + TEST_F(FirExitPointCollectionTest, ItAddsPoints) + { + collection.Add(point1); + collection.Add(point2); + collection.Add(point3); + EXPECT_EQ(3, collection.CountPoints()); + } + + TEST_F(FirExitPointCollectionTest, ItDoesntAddDuplicatePoints) + { + collection.Add(point1); + collection.Add(point1); + collection.Add(point2); + collection.Add(point2); + collection.Add(point3); + collection.Add(point3); + EXPECT_EQ(3, collection.CountPoints()); + } + + TEST_F(FirExitPointCollectionTest, ItReturnsNullptrIfPointNotFoundByIdentifier) + { + collection.Add(point1); + collection.Add(point2); + collection.Add(point3); + EXPECT_EQ(nullptr, collection.PointByIdentifier("NOPE")); + } + + TEST_F(FirExitPointCollectionTest, ItReturnsPointIfFoundByIdentifier) + { + collection.Add(point1); + collection.Add(point2); + collection.Add(point3); + EXPECT_EQ(point1, collection.PointByIdentifier("FOO")); + } +} // namespace UKControllerPluginTest::IntentionCode diff --git a/test/plugin/mock/MockExitDetermination.h b/test/plugin/mock/MockExitDetermination.h index 4d7be2f47..0d81b9772 100644 --- a/test/plugin/mock/MockExitDetermination.h +++ b/test/plugin/mock/MockExitDetermination.h @@ -10,6 +10,6 @@ namespace UKControllerPluginTest::IntentionCode { AircraftIsExiting, (const UKControllerPlugin::Flightplan::FlightplanPoint&, UKControllerPlugin::Euroscope::EuroScopeCFlightPlanInterface&), - (override)); + (const, override)); }; } // namespace UKControllerPluginTest::IntentionCode