From 1c58de62ed12f34c059a73b6c0bce4fb62d31385 Mon Sep 17 00:00:00 2001
From: James Turner <zakalawe@mac.com>
Date: Fri, 19 Apr 2019 12:26:07 +0100
Subject: [PATCH] Route-manager unit-tests

Also contains large extensions to the TestPilot helper
---
 test_suite/FGTestApi/TestPilot.cxx            |  74 +++++-
 test_suite/FGTestApi/TestPilot.hxx            |  28 ++-
 test_suite/FGTestApi/globals.cxx              | 172 +++++++++++++-
 test_suite/FGTestApi/globals.hxx              |  23 +-
 test_suite/unit_tests/Navaids/CMakeLists.txt  |   2 +
 test_suite/unit_tests/Navaids/TestSuite.cxx   |   2 +
 .../unit_tests/Navaids/test_routeManager.cxx  | 219 ++++++++++++++++++
 .../unit_tests/Navaids/test_routeManager.hxx  |  62 +++++
 8 files changed, 566 insertions(+), 16 deletions(-)
 create mode 100644 test_suite/unit_tests/Navaids/test_routeManager.cxx
 create mode 100644 test_suite/unit_tests/Navaids/test_routeManager.hxx

diff --git a/test_suite/FGTestApi/TestPilot.cxx b/test_suite/FGTestApi/TestPilot.cxx
index aa4e32695..0cf8b7a89 100644
--- a/test_suite/FGTestApi/TestPilot.cxx
+++ b/test_suite/FGTestApi/TestPilot.cxx
@@ -35,6 +35,21 @@ TestPilot::TestPilot(SGPropertyNode_ptr props) :
         _propRoot = globals->get_props();
     }
     
+    _latProp = _propRoot->getNode("position/latitude-deg", true);
+    _lonProp = _propRoot->getNode("position/longitude-deg", true);
+    _altitudeProp = _propRoot->getNode("position/altitude-ft", true);
+    _headingProp = _propRoot->getNode("orientation/heading-deg", true);
+    _speedKnotsProp = _propRoot->getNode("velocities/speed-knots", true);
+    _verticalFPMProp = _propRoot->getNode("velocities/vertical-fpm", true);
+
+    
+    SGPropertyNode_ptr _latProp;
+    SGPropertyNode_ptr _lonProp;
+    SGPropertyNode_ptr _altitudeProp;
+    SGPropertyNode_ptr _headingProp;
+    SGPropertyNode_ptr _speedKnotsProp;
+    SGPropertyNode_ptr _verticalFPMProp;
+    
     globals->add_subsystem("flight", this, SGSubsystemMgr::FDM);
 }
 
@@ -73,9 +88,48 @@ void TestPilot::turnToCourse(double deg)
     _turnActive = true;
     _targetCourseDeg = deg;
 }
+
+void TestPilot::flyHeading(double hdg)
+{
+    _lateralMode = LateralMode::Heading;
+    _turnActive = true;
+    _targetCourseDeg = hdg;
+}
+    
+void TestPilot::flyGPSCourse(GPS *gps)
+{
+    _gps = gps;
+    _gpsNode = globals->get_props()->getNode("instrumentation/gps");
+    _lateralMode = LateralMode::GPSCourse;
+    _turnActive = false;
+}
+
+void TestPilot::flyDirectTo(const SGGeod& target)
+{
+    _lateralMode = LateralMode::Direct;
+    _targetPos = target;
+}
     
 void TestPilot::updateValues(double dt)
 {
+    if (_gps && (_lateralMode == LateralMode::GPSCourse)) {
+        const double deviationDeg = _gpsNode->getDoubleValue("wp/wp[1]/course-deviation-deg");
+        _targetCourseDeg = _gpsNode->getDoubleValue("wp/leg-true-course-deg");
+        _targetCourseDeg -= deviationDeg;
+        SG_NORMALIZE_RANGE(_targetCourseDeg, 0.0, 360.0);
+        if (!_turnActive &&(fabs(_trueCourseDeg - _targetCourseDeg) > 0.5)) {
+            _turnActive = true;
+        }
+    }
+    
+    if (_lateralMode == LateralMode::Direct) {
+        _targetCourseDeg = SGGeodesy::courseDeg(globals->get_aircraft_position(), _targetPos);
+        SG_NORMALIZE_RANGE(_targetCourseDeg, 0.0, 360.0);
+        if (!_turnActive && (fabs(_trueCourseDeg - _targetCourseDeg) > 0.5)) {
+            _turnActive = true;
+        }
+    }
+    
     if (_turnActive) {
         if (fabs(_targetCourseDeg - _trueCourseDeg) < 0.1) {
             _trueCourseDeg = _targetCourseDeg;
@@ -84,13 +138,16 @@ void TestPilot::updateValues(double dt)
             // standard 2-minute turn, 180-deg min, thus 3-degrees per second
             double turnDeg = 3.0 * dt;
             double errorDeg = _targetCourseDeg - _trueCourseDeg;
-            if (fabs(errorDeg) > 180.0) {
-                errorDeg = 360.0 - errorDeg;
+            if (errorDeg > 180.0) {
+                errorDeg = errorDeg -= 360;
+            } else if (errorDeg < -180) {
+                errorDeg += 360.0;
             }
             
             turnDeg = copysign(turnDeg, errorDeg);
             // simple integral
             _trueCourseDeg += std::min(turnDeg, errorDeg);
+            SG_NORMALIZE_RANGE(_trueCourseDeg, 0.0, 360.0);
         }
     }
     
@@ -115,14 +172,13 @@ void TestPilot::updateValues(double dt)
 
 void TestPilot::setPosition(const SGGeod& pos)
 {
-    _propRoot->setDoubleValue("position/latitude-deg", pos.getLatitudeDeg());
-    _propRoot->setDoubleValue("position/longitude-deg", pos.getLongitudeDeg());
-    _propRoot->setDoubleValue("position/altitude-ft", pos.getElevationFt());
+    _latProp->setDoubleValue(pos.getLatitudeDeg());
+    _lonProp->setDoubleValue(pos.getLongitudeDeg());
+    _altitudeProp->setDoubleValue(pos.getElevationFt());
     
-    _propRoot->setDoubleValue("orientation/heading-deg", _trueCourseDeg);
-    _propRoot->setDoubleValue("velocities/speed-knots", _speedKnots);
-    _propRoot->setDoubleValue("velocities/vertical-fpm", _vspeedFPM);
-
+    _headingProp->setDoubleValue(_trueCourseDeg);
+    _speedKnotsProp->setDoubleValue(_speedKnots);
+    _verticalFPMProp->setDoubleValue(_vspeedFPM);
 }
     
 void TestPilot::setTargetAltitudeFtMSL(double altFt)
diff --git a/test_suite/FGTestApi/TestPilot.hxx b/test_suite/FGTestApi/TestPilot.hxx
index c15b443c7..f4040aa7a 100644
--- a/test_suite/FGTestApi/TestPilot.hxx
+++ b/test_suite/FGTestApi/TestPilot.hxx
@@ -23,6 +23,9 @@
 #include <simgear/props/propsfwd.hxx>
 #include <simgear/math/SGMathFwd.hxx>
 #include <simgear/structure/subsystem_mgr.hxx>
+#include <simgear/math/SGGeod.hxx>
+
+class GPS;
 
 namespace FGTestApi {
 
@@ -47,14 +50,22 @@ public:
     void setVerticalFPM(double fpm);
     void setTargetAltitudeFtMSL(double altFt);
 
-    // flyGreatCircle ...
-
     // void setTurnRateDegSec();
 
     void init() override;
     void update(double dT) override;
 
+    void flyHeading(double hdg);
+    void flyDirectTo(const SGGeod& target);
+    void flyGPSCourse(GPS *gps);
 private:
+    enum class LateralMode
+    {
+        Heading,
+        Direct,
+        GPSCourse
+    };
+    
     void updateValues(double dt);
     void setPosition(const SGGeod& pos);
 
@@ -68,6 +79,19 @@ private:
     bool _altActive = false;
     double _targetCourseDeg = 0.0;
     double _targetAltitudeFt = 0.0;
+  
+    LateralMode _lateralMode = LateralMode::Heading;
+    SGGeod _targetPos;
+    GPS* _gps = nullptr;
+    
+    SGPropertyNode_ptr _latProp;
+    SGPropertyNode_ptr _lonProp;
+    SGPropertyNode_ptr _altitudeProp;
+    SGPropertyNode_ptr _headingProp;
+    SGPropertyNode_ptr _speedKnotsProp;
+    SGPropertyNode_ptr _verticalFPMProp;
+    
+    SGPropertyNode_ptr _gpsNode;
 };
 
 } // of namespace FGTestApi
diff --git a/test_suite/FGTestApi/globals.cxx b/test_suite/FGTestApi/globals.cxx
index d66da0d47..b4d73972a 100644
--- a/test_suite/FGTestApi/globals.cxx
+++ b/test_suite/FGTestApi/globals.cxx
@@ -4,12 +4,16 @@
 
 #include "globals.hxx"
 
+#include <simgear/io/iostreams/sgstream.hxx>
+
 #if defined(HAVE_QT)
 #include <GUI/QtLauncher.hxx>
 #endif
 
 #include <Main/globals.hxx>
 #include <Main/options.hxx>
+#include <Main/util.hxx>
+#include <Main/FGInterpolator.hxx>
 
 #include <Time/TimeManager.hxx>
 
@@ -21,14 +25,21 @@
 #include <Navaids/FlightPlan.hxx>
 #include <Navaids/waypoint.hxx>
 
+#include <Scripting/NasalSys.hxx>
+
 using namespace flightgear;
 
 namespace FGTestApi {
 
+bool global_loggingToKML = false;
+sg_ofstream global_kmlStream;
+bool global_lineStringOpen = false;
+    
 namespace setUp {
 
 void initTestGlobals(const std::string& testName)
 {
+    assert(globals == nullptr);
     globals = new FGGlobals;
 
     DataStore &data = DataStore::get();
@@ -44,6 +55,18 @@ void initTestGlobals(const std::string& testName)
     }
 
     globals->set_fg_home(homePath);
+    
+    auto props = globals->get_props();
+    props->setStringValue("sim/fg-home", homePath.utf8Str());
+    props->setStringValue("sim/flight-model", "null");
+    props->setStringValue("sim/aircraft", "test-suite-aircraft");
+    
+    props->setDoubleValue("sim/current-view/config/default-field-of-view-deg", 90.0);
+    // ensure /sim/view/config exists
+    props->setBoolValue("sim/view/config/foo", false);
+    
+    props->setBoolValue("sim/rendering/precipitation-gui-enable", false);
+    props->setBoolValue("sim/rendering/precipitation-aircraft-enable", false);
 
     // Activate headless mode.
     globals->set_headless(true);
@@ -61,9 +84,50 @@ void initTestGlobals(const std::string& testName)
      * destroyed via the subsystem manager.
      */
     globals->add_subsystem("events", globals->get_event_mgr(), SGSubsystemMgr::DISPLAY);
+
+    // Nasal needs the interpolator running
+    globals->add_subsystem("prop-interpolator", new FGInterpolator, SGSubsystemMgr::INIT);
+
+}
+    
+bool logPositionToKML(const std::string& testName)
+{
+    SGPath p = SGPath::desktop() / (testName + ".kml");
+    global_kmlStream.open(p);
+    if (!global_kmlStream.is_open()) {
+        SG_LOG(SG_GENERAL, SG_WARN, "unable to open:" << p);
+        return false;
+    }
+    
+    // pre-amble
+    global_kmlStream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+        "<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n"
+        "<Document>\n";
+    // need more precision for doubles when specifying lat/lon, see
+    // https://xkcd.com/2170/  :)
+    global_kmlStream.precision(12);
+    
+    global_loggingToKML = true;
+    return true;
+}
+    
+void initStandardNasal()
+{
+    fgInitAllowedPaths();
+    
+    auto nasalNode = globals->get_props()->getNode("nasal", true);
+    
+// disable various larger modules
+    nasalNode->setBoolValue("canvas/enabled", false);
+    nasalNode->setBoolValue("jetways/enabled", false);
+    nasalNode->setBoolValue("jetways_edit/enabled", false);
+    nasalNode->setBoolValue("local_weather/enabled", false);
+
+    // will be inited, since we already did that
+    globals->add_new_subsystem<FGNasalSys>(SGSubsystemMgr::INIT);
 }
 
-void populateFP(flightgear::FlightPlanRef f,
+void populateFPWithoutNasal(flightgear::FlightPlanRef f,
                  const std::string& depICAO, const std::string& depRunway,
                  const std::string& destICAO, const std::string& destRunway,
                  const std::string& waypoints)
@@ -91,12 +155,70 @@ void populateFP(flightgear::FlightPlanRef f,
                                          destRwy->ident() + "-8", f), -1);
     f->insertWayptAtIndex(new RunwayWaypt(destRwy, f), -1);
 }
+    
+void populateFPWithNasal(flightgear::FlightPlanRef f,
+                       const std::string& depICAO, const std::string& depRunway,
+                       const std::string& destICAO, const std::string& destRunway,
+                       const std::string& waypoints)
+{
+    FGAirportRef depApt = FGAirport::getByIdent(depICAO);
+    f->setDeparture(depApt->getRunwayByIdent(depRunway));
+    
+    FGAirportRef destApt = FGAirport::getByIdent(destICAO);
+    f->setDestination(destApt->getRunwayByIdent(destRunway));
+    
+    // insert after the last departure waypoint
+    int insertIndex = 1;
+    
+    for (auto ws : simgear::strutils::split(waypoints)) {
+        WayptRef wpt = f->waypointFromString(ws);
+        f->insertWayptAtIndex(wpt, insertIndex++);
+    }
+}
 
 
 }  // End of namespace setUp.
+    
+    void beginLineString(const std::string& ident)
+    {
+        global_lineStringOpen = true;
+        global_kmlStream << "<Placemark>\n";
+        if (!ident.empty()) {
+            global_kmlStream << "<name>" << ident << "</name>\n";
+        }
+        global_kmlStream << "<LineString>\n";
+        global_kmlStream << "<tessellate>1</tessellate>\n";
+        global_kmlStream << "<coordinates>\n";
+    }
+    
+    void logCoordinate(const SGGeod& pos)
+    {
+        if (!global_lineStringOpen) {
+            beginLineString({});
+        }
+        
+        global_kmlStream << pos.getLongitudeDeg() << "," << pos.getLatitudeDeg() << " " << endl;
+    }
 
+    void endCurrentLineString()
+    {
+        global_lineStringOpen = false;
+        global_kmlStream <<
+            "</coordinates>\n"
+            "</LineString>\n"
+            "</Placemark>\n" << endl;
+    }
+    
 void setPosition(const SGGeod& g)
 {
+    if (global_loggingToKML) {
+        if (global_lineStringOpen) {
+            endCurrentLineString();
+        }
+        
+        logCoordinate(g);
+    }
+    
     globals->get_props()->setDoubleValue("position/latitude-deg", g.getLatitudeDeg());
     globals->get_props()->setDoubleValue("position/longitude-deg", g.getLongitudeDeg());
     globals->get_props()->setDoubleValue("position/altitude-ft", g.getElevationFt());
@@ -109,9 +231,46 @@ void runForTime(double t)
 
     for (int t = 0; t < ticks; ++t) {
         globals->get_subsystem_mgr()->update(1 / 120.0);
+        if (global_loggingToKML) {
+            logCoordinate(globals->get_aircraft_position());
+        }
     }
 }
 
+bool runForTimeWithCheck(double t, RunCheck check)
+{
+    const int tickHz = 30;
+    const double tickDuration = 1.0 / tickHz;
+    
+    int ticks = static_cast<int>(t * tickHz);
+    assert(ticks > 0);
+    const int logInterval = 2 * tickHz; // every two seconds
+    int nextLog = 0;
+    
+    for (int t = 0; t < ticks; ++t) {
+        globals->get_subsystem_mgr()->update(tickDuration);
+        
+        if (global_loggingToKML) {
+            if (nextLog == 0) {
+                logCoordinate(globals->get_aircraft_position());
+                nextLog = logInterval;
+            } else {
+                nextLog--;
+            }
+        }
+        
+        bool done = check();
+        if (done) {
+            if (global_loggingToKML) {
+                endCurrentLineString();
+            }
+            return true;
+        }
+    }
+    
+    return false;
+}
+    
 namespace tearDown {
 
 void shutdownTestGlobals()
@@ -123,6 +282,17 @@ void shutdownTestGlobals()
 #endif
 
     delete globals;
+    globals = nullptr;
+    
+    if (global_loggingToKML) {
+        if (global_lineStringOpen) {
+            endCurrentLineString();
+        }
+        // post-amble
+        global_kmlStream << "</Document>\n"
+        "</kml>" << endl;
+        global_kmlStream.close();
+    }
 }
 
 }  // End of namespace tearDown.
diff --git a/test_suite/FGTestApi/globals.hxx b/test_suite/FGTestApi/globals.hxx
index 933cdaf05..032ae7099 100644
--- a/test_suite/FGTestApi/globals.hxx
+++ b/test_suite/FGTestApi/globals.hxx
@@ -2,6 +2,8 @@
 #define FG_TEST_GLOBALS_HELPERS_HXX
 
 #include <string>
+#include <functional>
+
 #include <simgear/structure/SGSharedPtr.hxx>
 
 class SGGeod;
@@ -18,10 +20,19 @@ namespace setUp {
 
 void initTestGlobals(const std::string& testName);
 
-    void populateFP(flightgear::FlightPlanRef f,
-                const std::string& depICAO, const std::string& depRunway,
-                const std::string& destICAO, const std::string& destRunway,
-                const std::string& waypoints);
+bool logPositionToKML(const std::string& testName);
+
+void initStandardNasal();
+
+void populateFPWithoutNasal(flightgear::FlightPlanRef f,
+                         const std::string& depICAO, const std::string& depRunway,
+                         const std::string& destICAO, const std::string& destRunway,
+                         const std::string& waypoints);
+    
+void populateFPWithNasal(flightgear::FlightPlanRef f,
+            const std::string& depICAO, const std::string& depRunway,
+            const std::string& destICAO, const std::string& destRunway,
+            const std::string& waypoints);
     
 }  // End of namespace setUp.
 
@@ -29,6 +40,10 @@ void setPosition(const SGGeod& g);
 
 void runForTime(double t);
 
+    using RunCheck = std::function<bool(void)>;
+    
+bool runForTimeWithCheck(double t, RunCheck check);
+    
 namespace tearDown {
 
 void shutdownTestGlobals();
diff --git a/test_suite/unit_tests/Navaids/CMakeLists.txt b/test_suite/unit_tests/Navaids/CMakeLists.txt
index bf5556a73..bc0bfc66d 100644
--- a/test_suite/unit_tests/Navaids/CMakeLists.txt
+++ b/test_suite/unit_tests/Navaids/CMakeLists.txt
@@ -4,6 +4,7 @@ set(TESTSUITE_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/test_flightplan.cxx
     ${CMAKE_CURRENT_SOURCE_DIR}/test_navaids2.cxx
     ${CMAKE_CURRENT_SOURCE_DIR}/test_aircraftPerformance.cxx
+    ${CMAKE_CURRENT_SOURCE_DIR}/test_routeManager.cxx
     PARENT_SCOPE
 )
 
@@ -11,5 +12,6 @@ set(TESTSUITE_HEADERS
     ${TESTSUITE_HEADERS}
     ${CMAKE_CURRENT_SOURCE_DIR}/test_flightplan.hxx
     ${CMAKE_CURRENT_SOURCE_DIR}/test_aircraftPerformance.hxx
+    ${CMAKE_CURRENT_SOURCE_DIR}/test_routeManager.hxx
     PARENT_SCOPE
 )
diff --git a/test_suite/unit_tests/Navaids/TestSuite.cxx b/test_suite/unit_tests/Navaids/TestSuite.cxx
index e4a1f93a5..9e2af8918 100644
--- a/test_suite/unit_tests/Navaids/TestSuite.cxx
+++ b/test_suite/unit_tests/Navaids/TestSuite.cxx
@@ -20,8 +20,10 @@
 #include "test_flightplan.hxx"
 #include "test_navaids2.hxx"
 #include "test_aircraftPerformance.hxx"
+#include "test_routeManager.hxx"
 
 // Set up the unit tests.
 CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(FlightplanTests, "Unit tests");
 CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(NavaidsTests, "Unit tests");
 CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(AircraftPerformanceTests, "Unit tests");
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(RouteManagerTests, "Unit tests");
diff --git a/test_suite/unit_tests/Navaids/test_routeManager.cxx b/test_suite/unit_tests/Navaids/test_routeManager.cxx
new file mode 100644
index 000000000..af15ed8da
--- /dev/null
+++ b/test_suite/unit_tests/Navaids/test_routeManager.cxx
@@ -0,0 +1,219 @@
+#include "test_routeManager.hxx"
+
+#include <memory>
+#include <cstring>
+
+#include "test_suite/FGTestApi/globals.hxx"
+#include "test_suite/FGTestApi/NavDataCache.hxx"
+#include "test_suite/FGTestApi/TestPilot.hxx"
+
+#include <Navaids/FlightPlan.hxx>
+#include <Navaids/NavDataCache.hxx>
+#include <Navaids/navrecord.hxx>
+#include <Navaids/navlist.hxx>
+
+// we need a default GPS instrument, hard to test seperately for now
+#include <Instrumentation/gps.hxx>
+
+#include <Autopilot/route_mgr.hxx>
+
+using namespace flightgear;
+
+static FlightPlanRef makeTestFP(const std::string& depICAO, const std::string& depRunway,
+                         const std::string& destICAO, const std::string& destRunway,
+                         const std::string& waypoints)
+{
+    FlightPlanRef f = new FlightPlan;
+    FGTestApi::setUp::populateFPWithNasal(f, depICAO, depRunway, destICAO, destRunway, waypoints);
+    return f;
+}
+
+
+// Set up function for each test.
+void RouteManagerTests::setUp()
+{
+    FGTestApi::setUp::initTestGlobals("routemanager");
+    FGTestApi::setUp::initNavDataCache();
+    
+    globals->add_new_subsystem<FGRouteMgr>();
+    
+// setup the default GPS, which is needed for waypoint
+// sequencing to work
+    SGPropertyNode_ptr configNode(new SGPropertyNode);
+    configNode->setStringValue("name", "gps");
+    configNode->setIntValue("number", 0);
+    GPS* gps(new GPS(configNode, true /* default mode */));
+    m_gps = gps;
+    
+    SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation", true)->getChild("gps", 0, true);
+  //  node->setBoolValue("serviceable", true);
+   // globals->get_props()->setDoubleValue("systems/electrical/outputs/gps", 6.0);
+    globals->add_subsystem("gps", gps, SGSubsystemMgr::POST_FDM);
+    
+    globals->get_subsystem_mgr()->bind();
+    globals->get_subsystem_mgr()->init();
+    
+    FGTestApi::setUp::initStandardNasal();
+    globals->get_subsystem_mgr()->postinit();
+}
+
+
+// Clean up after each test.
+void RouteManagerTests::tearDown()
+{
+    m_gps = nullptr;
+    FGTestApi::tearDown::shutdownTestGlobals();
+}
+
+ void RouteManagerTests::setPositionAndStabilise(const SGGeod& g)
+ {
+     FGTestApi::setPosition(g);
+     for (int i=0; i<60; ++i) {
+         globals->get_subsystem_mgr()->update(0.02);
+     }
+ }
+
+void RouteManagerTests::testBasic()
+{
+    FGTestApi::setUp::logPositionToKML("rm_basic");
+    
+    FlightPlanRef fp1 = makeTestFP("EGLC", "27", "EHAM", "06",
+                                   "CLN IDESI RINIS VALKO RIVER RTM EKROS");
+    fp1->setIdent("testplan");
+    fp1->setCruiseFlightLevel(360);
+    
+    auto rm = globals->get_subsystem<FGRouteMgr>();
+    rm->setFlightPlan(fp1);
+    
+    auto gpsNode = globals->get_props()->getNode("instrumentation/gps", true);
+    CPPUNIT_ASSERT(!strcmp("obs", gpsNode->getStringValue("mode")));
+
+    rm->activate();
+    
+    CPPUNIT_ASSERT(fp1->isActive());
+    
+    // Nasal deleagte should have placed GPS into leg mode
+    auto rmNode = globals->get_props()->getNode("autopilot/route-manager", true);
+    
+    CPPUNIT_ASSERT(!strcmp("leg", gpsNode->getStringValue("mode")));
+
+    CPPUNIT_ASSERT(!strcmp("EGLC", rmNode->getStringValue("departure/airport")));
+    CPPUNIT_ASSERT(!strcmp("27", rmNode->getStringValue("departure/runway")));
+    CPPUNIT_ASSERT(!strcmp("", rmNode->getStringValue("departure/sid")));
+    CPPUNIT_ASSERT(!strcmp("London City", rmNode->getStringValue("departure/name")));
+
+    CPPUNIT_ASSERT(!strcmp("EHAM", rmNode->getStringValue("destination/airport")));
+    CPPUNIT_ASSERT(!strcmp("06", rmNode->getStringValue("destination/runway")));
+    
+    CPPUNIT_ASSERT_EQUAL(360, rmNode->getIntValue("cruise/flight-level"));
+    CPPUNIT_ASSERT_EQUAL(false, rmNode->getBoolValue("airborne"));
+
+    CPPUNIT_ASSERT_EQUAL(0, rmNode->getIntValue("current-wp"));
+    auto wp0Node = rmNode->getNode("wp");
+    CPPUNIT_ASSERT(!strcmp("EGLC-27", wp0Node->getStringValue("id")));
+    
+    auto wp1Node = rmNode->getNode("wp[1]");
+    CPPUNIT_ASSERT(!strcmp("CLN", wp1Node->getStringValue("id")));
+
+    FGPositioned::TypeFilter f{FGPositioned::VOR};
+    auto clactonVOR = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent("CLN", SGGeod::fromDeg(0.0, 51.0), &f));
+    
+    // verify hold entry course
+    auto pilot = SGSharedPtr<FGTestApi::TestPilot>(new FGTestApi::TestPilot);
+    FGTestApi::setPosition(fp1->departureRunway()->geod());
+    pilot->resetAtPosition(fp1->departureRunway()->geod());
+    
+    pilot->setSpeedKts(220);
+    pilot->setCourseTrue(fp1->departureRunway()->headingDeg());
+    pilot->setTargetAltitudeFtMSL(10000);
+    
+    bool ok = FGTestApi::runForTimeWithCheck(30.0, [rmNode] () {
+        if (rmNode->getIntValue("current-wp") == 1) return true;
+        return false;
+    });
+    
+    CPPUNIT_ASSERT(ok);
+    
+    // continue outbound for some time
+    pilot->setSpeedKts(250);
+    FGTestApi::runForTime(30.0);
+    
+    // turn towards Clacton VOR
+    pilot->flyDirectTo(clactonVOR->geod());
+    pilot->setTargetAltitudeFtMSL(30000);
+    pilot->setSpeedKts(280);
+    ok = FGTestApi::runForTimeWithCheck(6000.0, [rmNode] () {
+        if (rmNode->getIntValue("current-wp") == 2) return true;
+        return false;
+    });
+    CPPUNIT_ASSERT(ok);
+
+    // short straight leg for testing
+    pilot->flyHeading(90.0);
+    pilot->setSpeedKts(330);
+    FGTestApi::runForTime(30.0);
+
+    // let's engage LNAV mode :)
+    pilot->flyGPSCourse(m_gps);
+    
+    ok = FGTestApi::runForTimeWithCheck(6000.0, [rmNode] () {
+        if (rmNode->getIntValue("current-wp") == 5) return true;
+        return false;
+    });
+    CPPUNIT_ASSERT(ok);
+    
+    // check where we are - should be heading to RIVER from VALKO
+    CPPUNIT_ASSERT_EQUAL(5, rmNode->getIntValue("current-wp"));
+    CPPUNIT_ASSERT(!strcmp("RIVER", wp0Node->getStringValue("id")));
+    CPPUNIT_ASSERT(!strcmp("RTM", wp1Node->getStringValue("id")));
+    // slightly rapid descent 
+    pilot->setTargetAltitudeFtMSL(3000);
+
+    // temporary until GPS course / deviation fixes are merged
+    return;
+    
+    ok = FGTestApi::runForTimeWithCheck(6000.0, [rmNode] () {
+        if (rmNode->getIntValue("current-wp") == 7) return true;
+        return false;
+    });
+    CPPUNIT_ASSERT(ok);
+    
+    // run until the GPS reverts to OBS mode at the end of the flight plan
+    ok = FGTestApi::runForTimeWithCheck(6000.0, [gpsNode] () {
+        std::string mode = gpsNode->getStringValue("mode");
+        if (mode == "obs") return true;
+        return false;
+    });
+    
+    CPPUNIT_ASSERT(ok);
+    CPPUNIT_ASSERT_EQUAL(-1, fp1->currentIndex());
+    CPPUNIT_ASSERT(!fp1->isActive());
+    CPPUNIT_ASSERT_EQUAL(-1, rmNode->getIntValue("current-wp"));
+    CPPUNIT_ASSERT_EQUAL(false, rmNode->getBoolValue("active"));
+}
+
+void RouteManagerTests::testDefaultSID()
+{
+    FlightPlanRef fp1 = makeTestFP("EGLC", "27", "EHAM", "24",
+                                   "CLN IDRID VALKO");
+    fp1->setIdent("testplan");
+    
+    auto rm = globals->get_subsystem<FGRouteMgr>();
+    rm->setFlightPlan(fp1);
+    
+    auto gpsNode = globals->get_props()->getNode("instrumentation/gps", true);
+    auto rmNode = globals->get_props()->getNode("autopilot/route-manager", true);
+    rmNode->setStringValue("departure/sid", "DEFAULT");
+    
+    // let's see what we got :)
+    
+    rm->activate();
+    
+    CPPUNIT_ASSERT(fp1->isActive());
+}
+
+void RouteManagerTests::testDefaultApproach()
+{
+    
+}
+
diff --git a/test_suite/unit_tests/Navaids/test_routeManager.hxx b/test_suite/unit_tests/Navaids/test_routeManager.hxx
new file mode 100644
index 000000000..e143ab466
--- /dev/null
+++ b/test_suite/unit_tests/Navaids/test_routeManager.hxx
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2019 James Turner
+ *
+ * This file is part of the program FlightGear.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#ifndef _FG_ROUTE_MANAGER_UNIT_TESTS_HXX
+#define _FG_ROUTE_MANAGER_UNIT_TESTS_HXX
+
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/TestFixture.h>
+
+class SGGeod;
+class GPS;
+
+// The flight plan unit tests.
+class RouteManagerTests : public CppUnit::TestFixture
+{
+    // Set up the test suite.
+    CPPUNIT_TEST_SUITE(RouteManagerTests);
+    CPPUNIT_TEST(testBasic);
+    CPPUNIT_TEST(testDefaultSID);
+    CPPUNIT_TEST(testDefaultApproach);
+
+    CPPUNIT_TEST_SUITE_END();
+
+   // void setPositionAndStabilise(FGNavRadio* r, const SGGeod& g);
+
+public:
+    // Set up function for each test.
+    void setUp();
+
+    // Clean up after each test.
+    void tearDown();
+
+    void setPositionAndStabilise(const SGGeod& g);
+    
+    // The tests.
+    void testBasic();
+    void testDefaultSID();
+    void testDefaultApproach();
+    
+private:
+    GPS* m_gps = nullptr;
+};
+
+#endif  // _FG_ROUTE_MANAGER_UNIT_TESTS_HXX