From c7b28a196036edf66f98b3a8ef6596436c288975 Mon Sep 17 00:00:00 2001
From: James Turner <zakalawe@mac.com>
Date: Wed, 10 Oct 2018 09:26:06 +0100
Subject: [PATCH] UNit-tests for the NavRadio

Still needs ILS and GS tests, but starting to be useful already.
---
 src/Sound/audioident.cxx                      |  19 +-
 test_suite/FGTestApi/globals.cxx              |   8 +-
 test_suite/FGTestApi/globals.hxx              |   3 +
 test_suite/unit_tests/CMakeLists.txt          |   1 +
 test_suite/unit_tests/Input/test_hidinput.cxx |   2 +-
 .../unit_tests/Instrumentation/CMakeLists.txt |  12 +
 .../unit_tests/Instrumentation/TestSuite.cxx  |  23 ++
 .../Instrumentation/test_navRadio.cxx         | 206 ++++++++++++++++++
 .../Instrumentation/test_navRadio.hxx         |  67 ++++++
 9 files changed, 335 insertions(+), 6 deletions(-)
 create mode 100644 test_suite/unit_tests/Instrumentation/CMakeLists.txt
 create mode 100644 test_suite/unit_tests/Instrumentation/TestSuite.cxx
 create mode 100644 test_suite/unit_tests/Instrumentation/test_navRadio.cxx
 create mode 100644 test_suite/unit_tests/Instrumentation/test_navRadio.hxx

diff --git a/src/Sound/audioident.cxx b/src/Sound/audioident.cxx
index aa8a97ac7..bb924c26b 100644
--- a/src/Sound/audioident.cxx
+++ b/src/Sound/audioident.cxx
@@ -37,22 +37,29 @@ AudioIdent::AudioIdent( const std::string & fx_name, const double interval_secs,
 
 void AudioIdent::init()
 {
+    auto soundManager = globals->get_subsystem<SGSoundMgr>();
+    if (!soundManager)
+        return; // sound disabled
+    
     _timer = 0.0;
     _ident = "";
     _running = false;
-    _sgr = globals->get_subsystem<SGSoundMgr>()->find("avionics", true);
+    _sgr = soundManager->find("avionics", true);
     _sgr->tie_to_listener();
 }
 
 void AudioIdent::stop()
 {
-    if( _sgr->exists( _fx_name ) )
+    if (_sgr && _sgr->exists( _fx_name ) )
         _sgr->stop( _fx_name );
     _running = false;
 }
 
 void AudioIdent::start()
 {
+    if (!_sgr)
+        return;
+    
     _timer = _interval;
     _sgr->play_once(_fx_name);
     _running = true;
@@ -60,10 +67,11 @@ void AudioIdent::start()
 
 void AudioIdent::setVolumeNorm( double volumeNorm )
 {
+    if (!_sgr)
+        return;
+    
     SG_CLAMP_RANGE(volumeNorm, 0.0, 1.0);
-
     SGSoundSample *sound = _sgr->find( _fx_name );
-
     if ( sound != NULL ) {
         sound->set_volume( volumeNorm );
     }
@@ -71,6 +79,9 @@ void AudioIdent::setVolumeNorm( double volumeNorm )
 
 void AudioIdent::setIdent( const std::string & ident, double volumeNorm )
 {
+    if (!_sgr)
+        return;
+    
     // Signal may flicker very frequently (due to our realistic newnavradio...).
     // Avoid recreating identical sound samples all the time, instead turn off
     // volume when signal is lost, and save the most recent sample.
diff --git a/test_suite/FGTestApi/globals.cxx b/test_suite/FGTestApi/globals.cxx
index cb0257727..4b8bf0972 100644
--- a/test_suite/FGTestApi/globals.cxx
+++ b/test_suite/FGTestApi/globals.cxx
@@ -15,7 +15,7 @@
 
 #include <simgear/structure/event_mgr.hxx>
 #include <simgear/timing/timestamp.hxx>
-
+#include <simgear/math/sg_geodesy.hxx>
 
 namespace FGTestApi {
 
@@ -59,6 +59,12 @@ void initTestGlobals(const std::string& testName)
 
 }  // End of namespace setUp.
 
+void setPosition(const SGGeod& 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());
+}
 
 namespace tearDown {
 
diff --git a/test_suite/FGTestApi/globals.hxx b/test_suite/FGTestApi/globals.hxx
index 14ae13dbf..947e5eb08 100644
--- a/test_suite/FGTestApi/globals.hxx
+++ b/test_suite/FGTestApi/globals.hxx
@@ -3,6 +3,8 @@
 
 #include <string>
 
+class SGGeod;
+
 namespace FGTestApi {
 
 namespace setUp {
@@ -11,6 +13,7 @@ void initTestGlobals(const std::string& testName);
 
 }  // End of namespace setUp.
 
+void setPosition(const SGGeod& g);
 
 namespace tearDown {
 
diff --git a/test_suite/unit_tests/CMakeLists.txt b/test_suite/unit_tests/CMakeLists.txt
index 8d2958847..da57f3367 100644
--- a/test_suite/unit_tests/CMakeLists.txt
+++ b/test_suite/unit_tests/CMakeLists.txt
@@ -6,6 +6,7 @@ foreach( unit_test_category
         Input
         Main
         Navaids
+        Instrumentation
         Scripting
     )
 
diff --git a/test_suite/unit_tests/Input/test_hidinput.cxx b/test_suite/unit_tests/Input/test_hidinput.cxx
index 2a586844e..0a58d1143 100644
--- a/test_suite/unit_tests/Input/test_hidinput.cxx
+++ b/test_suite/unit_tests/Input/test_hidinput.cxx
@@ -20,7 +20,7 @@
 
 #include "test_hidinput.hxx"
 
-#include "test_suite/helpers/globals.hxx"
+#include "test_suite/FGTestApi/globals.hxx"
 
 #include <simgear/misc/test_macros.hxx>
 
diff --git a/test_suite/unit_tests/Instrumentation/CMakeLists.txt b/test_suite/unit_tests/Instrumentation/CMakeLists.txt
new file mode 100644
index 000000000..610ed2867
--- /dev/null
+++ b/test_suite/unit_tests/Instrumentation/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(TESTSUITE_SOURCES
+    ${TESTSUITE_SOURCES}
+    ${CMAKE_CURRENT_SOURCE_DIR}/TestSuite.cxx
+    ${CMAKE_CURRENT_SOURCE_DIR}/test_navRadio.cxx
+    PARENT_SCOPE
+)
+
+set(TESTSUITE_HEADERS
+    ${TESTSUITE_HEADERS}
+    ${CMAKE_CURRENT_SOURCE_DIR}/test_navRadio.hxx
+    PARENT_SCOPE
+)
diff --git a/test_suite/unit_tests/Instrumentation/TestSuite.cxx b/test_suite/unit_tests/Instrumentation/TestSuite.cxx
new file mode 100644
index 000000000..e51d4c35c
--- /dev/null
+++ b/test_suite/unit_tests/Instrumentation/TestSuite.cxx
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 Edward d'Auvergne
+ *
+ * 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/>.
+ */
+
+#include "test_navRadio.hxx"
+
+// Set up the unit tests.
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(NavRadioTests, "Unit tests");
diff --git a/test_suite/unit_tests/Instrumentation/test_navRadio.cxx b/test_suite/unit_tests/Instrumentation/test_navRadio.cxx
new file mode 100644
index 000000000..7ca2f6b3c
--- /dev/null
+++ b/test_suite/unit_tests/Instrumentation/test_navRadio.cxx
@@ -0,0 +1,206 @@
+#include "test_navRadio.hxx"
+
+#include <memory>
+#include <cstring>
+
+#include "test_suite/FGTestApi/globals.hxx"
+#include "test_suite/FGTestApi/NavDataCache.hxx"
+
+#include <Navaids/NavDataCache.hxx>
+#include <Navaids/navrecord.hxx>
+#include <Navaids/navlist.hxx>
+
+#include <Instrumentation/navradio.hxx>
+
+// Set up function for each test.
+void NavRadioTests::setUp()
+{
+    FGTestApi::setUp::initTestGlobals("navradio");
+    FGTestApi::setUp::initNavDataCache();
+}
+
+
+// Clean up after each test.
+void NavRadioTests::tearDown()
+{
+    FGTestApi::tearDown::shutdownTestGlobals();
+}
+
+void NavRadioTests::setPositionAndStabilise(FGNavRadio* r, const SGGeod& g)
+{
+    FGTestApi::setPosition(g);
+    for (int i=0; i<60; ++i) {
+        r->update(0.1);
+    }
+}
+
+void NavRadioTests::testBasic()
+{
+    SGPropertyNode_ptr configNode(new SGPropertyNode);
+    configNode->setStringValue("name", "navtest");
+    configNode->setIntValue("number", 2);
+    std::unique_ptr<FGNavRadio> r(new FGNavRadio(configNode));
+    
+    r->bind();
+    r->init();
+    
+    SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation/navtest[2]");
+    node->setBoolValue("serviceable", true);
+    // needed for the radio to power up
+    globals->get_props()->setDoubleValue("systems/electrical/outputs/navtest", 6.0);
+    node->setDoubleValue("frequencies/selected-mhz", 113.8);
+
+    SGGeod pos = SGGeod::fromDegFt(-3.352780, 55.499199, 20000);
+    setPositionAndStabilise(r.get(), pos);
+    
+    CPPUNIT_ASSERT(!strcmp("TLA", node->getStringValue("nav-id")));
+    CPPUNIT_ASSERT_EQUAL(true, node->getBoolValue("in-range"));
+}
+
+void NavRadioTests::testCDIDeflection()
+{
+    SGPropertyNode_ptr configNode(new SGPropertyNode);
+    configNode->setStringValue("name", "navtest");
+    configNode->setIntValue("number", 2);
+    std::unique_ptr<FGNavRadio> r(new FGNavRadio(configNode));
+    
+    r->bind();
+    r->init();
+    
+    SGPropertyNode_ptr node = globals->get_props()->getNode("instrumentation/navtest[2]");
+    node->setBoolValue("serviceable", true);
+    // needed for the radio to power up
+    globals->get_props()->setDoubleValue("systems/electrical/outputs/navtest", 6.0);
+    node->setDoubleValue("frequencies/selected-mhz", 113.55);
+    
+    node->setDoubleValue("radials/selected-deg", 25);
+    
+    FGPositioned::TypeFilter f{FGPositioned::VOR};
+    FGNavRecordRef nav = fgpositioned_cast<FGNavRecord>(FGPositioned::findClosestWithIdent("MCT", SGGeod::fromDeg(-2.26, 53.3), &f));
+
+    // twist of MCT is 5.0, so we use a bearing of 20 here, not 25
+    SGGeod posOnRadial = SGGeodesy::direct(nav->geod(), 20.0, 10 * SG_NM_TO_METER);
+    posOnRadial.setElevationFt(10000);
+    setPositionAndStabilise(r.get(), posOnRadial);
+
+    CPPUNIT_ASSERT(!strcmp("MCT", node->getStringValue("nav-id")));
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("crosstrack-error-m"), 0.01);
+    CPPUNIT_ASSERT(node->getBoolValue("from-flag"));
+    CPPUNIT_ASSERT(!node->getBoolValue("to-flag"));
+
+    
+    // move off course
+    SGGeod posOffRadial = SGGeodesy::direct(nav->geod(), 15.0, 20 * SG_NM_TO_METER); // 5 degress off
+    posOffRadial.setElevationFt(12000);
+    setPositionAndStabilise(r.get(), posOffRadial);
+    
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(5.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.5, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+    
+    double xtkE = sin(5.0 * SG_DEGREES_TO_RADIANS) * 20 * SG_NM_TO_METER;
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+    CPPUNIT_ASSERT(node->getBoolValue("from-flag"));
+    CPPUNIT_ASSERT(!node->getBoolValue("to-flag"));
+    
+    posOffRadial = SGGeodesy::direct(nav->geod(), 28.0, 30 * SG_NM_TO_METER); // 8 degress off
+    posOffRadial.setElevationFt(16000);
+    setPositionAndStabilise(r.get(), posOffRadial);
+
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(-8.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(-0.8, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+    
+    xtkE = sin(-8.0 * SG_DEGREES_TO_RADIANS) * 30 * SG_NM_TO_METER;
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+
+    // move more than ten degrees off course
+    posOffRadial = SGGeodesy::direct(nav->geod(), 33.0, 40 * SG_NM_TO_METER); // 13 degress off
+    posOffRadial.setElevationFt(16000);
+    setPositionAndStabilise(r.get(), posOffRadial);
+    
+    // pegged to full deflection
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(-10.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(-1.0, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+    
+    // cross track error is computed based on true deflection, not clamped
+    xtkE = sin(-13.0 * SG_DEGREES_TO_RADIANS) * 40 * SG_NM_TO_METER;
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+    CPPUNIT_ASSERT(node->getBoolValue("from-flag"));
+    CPPUNIT_ASSERT(!node->getBoolValue("to-flag"));
+    
+// try on the TO side of the station
+    // let's use Perth VOR, but the Australian one to check southern
+    // hemisphere operation
+    node->setDoubleValue("frequencies/selected-mhz", 113.7);
+    node->setDoubleValue("radials/selected-deg", 42.0); // twist is -2.0
+    CPPUNIT_ASSERT(!strcmp("113.70", node->getStringValue("frequencies/selected-mhz-fmt")));
+
+    auto perthVOR = fgpositioned_cast<FGNavRecord>(
+        FGPositioned::findClosestWithIdent("PH", SGGeod::fromDeg(115.95, -31.9), &f));
+    
+    SGGeod p = SGGeodesy::direct(perthVOR->geod(), 220.0, 20 * SG_NM_TO_METER); // on the reciprocal radial
+    p.setElevationFt(12000);
+    setPositionAndStabilise(r.get(), p);
+    
+    CPPUNIT_ASSERT(!strcmp("PH", node->getStringValue("nav-id")));
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(40.0, node->getDoubleValue("heading-deg"), 0.5);
+    
+    // actual radial has twist subtracted
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(222.0, node->getDoubleValue("radials/actual-deg"), 0.01);
+    
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.0, node->getDoubleValue("crosstrack-error-m"), 50.0);
+    CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+    CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+    
+// off course on the TO side
+    p = SGGeodesy::direct(perthVOR->geod(), 227.0, 100 * SG_NM_TO_METER);
+    p.setElevationFt(18000);
+    setPositionAndStabilise(r.get(), p);
+    
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(1.0, node->getDoubleValue("signal-quality-norm"), 0.01);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(47.0, node->getDoubleValue("heading-deg"), 1);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(229.0, node->getDoubleValue("radials/actual-deg"), 0.01);
+    
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(7.0, node->getDoubleValue("heading-needle-deflection"), 0.1);
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(0.7, node->getDoubleValue("heading-needle-deflection-norm"), 0.01);
+    
+    xtkE = sin(7.0 * SG_DEGREES_TO_RADIANS) * 100 * SG_NM_TO_METER;
+    CPPUNIT_ASSERT_DOUBLES_EQUAL(xtkE, node->getDoubleValue("crosstrack-error-m"), 50.0);
+    CPPUNIT_ASSERT(!node->getBoolValue("from-flag"));
+    CPPUNIT_ASSERT(node->getBoolValue("to-flag"));
+}
+
+void NavRadioTests::testILSBasic()
+{
+    
+    // also check ILS back course
+}
+
+void NavRadioTests::testGS()
+{
+    
+}
+
+void NavRadioTests::testILSFalseCourse()
+{
+    
+    // also GS false lobes
+}
+
+void NavRadioTests::testILSPaired()
+{
+    // EGPH and countless more
+}
+
+void NavRadioTests::testILSAdjacentPaired()
+{
+    // eg KJFK
+}
diff --git a/test_suite/unit_tests/Instrumentation/test_navRadio.hxx b/test_suite/unit_tests/Instrumentation/test_navRadio.hxx
new file mode 100644
index 000000000..200653647
--- /dev/null
+++ b/test_suite/unit_tests/Instrumentation/test_navRadio.hxx
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 Edward d'Auvergne
+ *
+ * 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_NAVRADIO_UNIT_TESTS_HXX
+#define _FG_NAVRADIO_UNIT_TESTS_HXX
+
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/TestFixture.h>
+
+class FGNavRadio;
+class SGGeod;
+
+// The flight plan unit tests.
+class NavRadioTests : public CppUnit::TestFixture
+{
+    // Set up the test suite.
+    CPPUNIT_TEST_SUITE(NavRadioTests);
+    CPPUNIT_TEST(testBasic);
+    CPPUNIT_TEST(testCDIDeflection);
+    
+    CPPUNIT_TEST(testILSBasic);
+    CPPUNIT_TEST(testGS);
+    CPPUNIT_TEST(testILSFalseCourse);
+    CPPUNIT_TEST(testILSPaired);
+    CPPUNIT_TEST(testILSAdjacentPaired);
+
+    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();
+
+    // The tests.
+    void testBasic();
+    void testCDIDeflection();
+    
+    void testILSBasic();
+    void testGS();
+    void testILSFalseCourse();
+    void testILSPaired();
+    void testILSAdjacentPaired();
+};
+
+#endif  // _FG_NAVRADIO_UNIT_TESTS_HXX