Add support for config file parsing and watching

This change introduces the ability to load and parse
the json xapp descriptor (config) file. It also provides
the ability for the xAPP to register a callback function
which is executed when the descriptor is changed.

Issue-ID: RIC-428

Signed-off-by: E. Scott Daniels <daniels@research.att.com>
Change-Id: I7a1147aa8055599ef4b36ab17960e32ccb5b741f
diff --git a/test/Makefile b/test/Makefile
index b7c843b..cc47990 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -1,11 +1,11 @@
 
 coverage_opts = -ftest-coverage -fprofile-arcs
 
-binaries = unit_test
+binaries = unit_test jhash_test config_test metrics_test
+include = -I ../src/xapp -I ../src/alarm -I ../src/messaging  -I  ../src/config -I ../ext/jsmn  -I  ../src/json -I ../src/metrics
+ld_path = LD_LIBRARY_PATH=../src/.build
 
-tests::	unit_test jhash_test metrics_test
-
-include_list = -I ../src/metrics -I ../src/alarm -I ../src/messaging
+tests::	$(binaries)
 
 # RMR emulation
 rmr_em.o::	rmr_em.c
@@ -15,7 +15,7 @@
 # emulate (and don't need to)
 #
 unit_test:: unit_test.cpp rmr_em.o
-	g++ -g $(coverage_opts) $(include_list) unit_test.cpp -o unit_test -L../.build -lricxfcpp rmr_em.o  -lrmr_si -lpthread
+	g++ -g $(coverage_opts) $(include) unit_test.cpp -o unit_test -L../.build -lricxfcpp rmr_em.o  -lrmr_si -lpthread
 
 # build a special jwrapper object with coverage settings
 jwrapper_test.o:: ../src/json/jwrapper.c ../src/json/jwrapper.h
@@ -26,8 +26,10 @@
 	g++ -g $(coverage_opts) -I ../src/json -I ../ext/jsmn jhash_test.cpp -o jhash_test jwrapper_test.o -lrmr_si -lpthread
 
 metrics_test:: metrics_test.cpp rmr_em.o
-	# do NOT link the xapp lib; we include all modules in the test programme
-	g++ -g $(coverage_opts) $(include_list) metrics_test.cpp -o metrics_test -L../.build -lricxfcpp rmr_em.o -l rmr_si -lpthread
+	g++ -g $(coverage_opts) $(include) metrics_test.cpp -o metrics_test -L../.build -lricxfcpp rmr_em.o -l rmr_si -lpthread
+
+config_test:: config_test.cpp jwrapper_test.o
+	g++ -g $(coverage_opts) $(include) config_test.cpp -o config_test jwrapper_test.o -lricxfcpp -lrmr_si -lpthread
 
 # prune gcov files generated by system include files
 clean::
diff --git a/test/config1.json b/test/config1.json
new file mode 100644
index 0000000..8a09e5c
--- /dev/null
+++ b/test/config1.json
@@ -0,0 +1,103 @@
+{
+   "comment":  "test config; this one is complete and legit",
+   "xapp_name": "mcxapp",
+   "version": "1.0.11",
+   "containers": [
+        {
+            "name": "mcxapp",
+            "image": {
+                "registry": "ranco-dev-tools.eastus.cloudapp.azure.com:10001",
+                "name": "ric-app-mc",
+                "tag": "1.1.3"
+            },
+            "command": [ "/bin/bash", "-c", "--" ],
+        "args": [ "/playpen/bin/container_start.sh" ]
+        }
+    ],
+    "messaging": {
+        "ports": [
+            {
+                "name": "rmr-data-in",
+                "container": "mcxapp",
+                "port": 4560,
+                "rxMessages": [
+                    "RIC_UE_CONTEXT_RELEASE",
+                    "RIC_SGNB_ADDITION_REQ",
+                    "RIC_SGNB_ADDITION_ACK",
+                    "RIC_SGNB_ADDITION_REJECT",
+                    "RIC_SGNB_RECONF_COMPLETE",
+                    "RIC_RRC_TRANSFER",
+                    "RIC_SGNB_MOD_REQUEST",
+                    "RIC_SGNB_MOD_REQUEST_ACK",
+                    "RIC_SGNB_MOD_REQUEST_REJ",
+                    "RIC_SGNB_MOD_REQUIRED",
+                    "RIC_SGNB_MOD_CONFIRM",
+                    "RIC_SGNB_MOD_REFUSE",
+                    "RIC_SGNB_RELEASE_REQUEST",
+                    "RIC_SGNB_RELEASE_REQUEST_ACK",
+                    "RIC_SGNB_RELEASE_REQUIRED",
+                    "RIC_SGNB_RELEASE_CONFIRM",
+                    "RIC_SECONDARY_RAT_DATA_USAGE_REPORT"
+                ],
+                "txMessages": [],
+                "policies": [],
+                "description": "rmr receive data port for mcxapp"
+            },
+            {
+                "name": "rmr-data-out",
+                "container": "mcxapp",
+                "port": 4562,
+                "txMessages": [
+                    "MC_REPORT"
+                ],
+                "rxMessages": [],
+                "policies": [],
+                "description": "rmr send data port for mcxapp"
+            },
+            {
+                "name": "rmr-route",
+                "container": "mcxapp",
+                "port": 4561,
+                "description": "rmr route port for mcxapp"
+            }
+        ]
+    },
+
+    "controls": {
+        "ves_collector_address": "10.53.183.214:8888",
+        "measurement_interval": 1000,
+        "debug_mode": true,
+        "tbool": true,
+        "fbool": false,
+        "simulator_mode": true
+    },
+
+    "rmr": {
+        "protPort": "tcp:4560",
+        "maxSize": 2072,
+        "numWorkers": 1,
+        "txMessages": [
+            "MC_REPORT"
+        ],
+        "rxMessages": [
+            "RIC_UE_CONTEXT_RELEASE",
+            "RIC_SGNB_ADDITION_REQ",
+            "RIC_SGNB_ADDITION_ACK",
+            "RIC_SGNB_ADDITION_REJECT",
+            "RIC_SGNB_RECONF_COMPLETE",
+            "RIC_RRC_TRANSFER",
+            "RIC_SGNB_MOD_REQUEST",
+            "RIC_SGNB_MOD_REQUEST_ACK",
+            "RIC_SGNB_MOD_REQUEST_REJ",
+            "RIC_SGNB_MOD_REQUIRED",
+            "RIC_SGNB_MOD_CONFIRM",
+            "RIC_SGNB_MOD_REFUSE",
+            "RIC_SGNB_RELEASE_REQUEST",
+            "RIC_SGNB_RELEASE_REQUEST_ACK",
+            "RIC_SGNB_RELEASE_REQUIRED",
+            "RIC_SGNB_RELEASE_CONFIRM",
+            "RIC_SECONDARY_RAT_DATA_USAGE_REPORT"
+        ]
+    }
+}
+
diff --git a/test/config_test.cpp b/test/config_test.cpp
new file mode 100644
index 0000000..68506c1
--- /dev/null
+++ b/test/config_test.cpp
@@ -0,0 +1,186 @@
+// vim: ts=4 sw=4 noet :
+/*
+==================================================================================
+       Copyright (c) 2020 Nokia
+       Copyright (c) 2020 AT&T Intellectual Property.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================================
+*/
+
+/*
+	Mnemonic:	config_test.cpp
+	Abstract:	Unit test to drive the config functions.
+
+	Date:		28 July 2020
+	Author:		E. Scott Daniels
+*/
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <time.h>
+
+#include <string>
+#include <memory>
+
+#include "../src/xapp/xapp.hpp"
+
+//#include "../src/json/jhash.hpp"
+//#include "../src/json/jhash.cpp"
+
+#include "../src/config/config.hpp"		// include things directly under test
+#include "../src/config/config_cb.hpp"
+#include "../src/config/config.cpp"
+#include "../src/config/config_cb.cpp"
+
+#include "ut_support.cpp"
+
+char* data = (char *) "some data to validate in callback";
+bool	callback_driven = false;
+int		cb_errors = 0;
+
+
+/*
+	Notification callback to see that it is driven on change.
+*/
+void ncb( xapp::Config& c, void* data ) {
+	cb_errors += fail_if( data == NULL, "callback function did not get a good pointer" );
+
+	auto v = c.Get_control_value( "measurement_interval" );
+	cb_errors += fail_if( v != 1000, "measurement value in new file wasn't different" );
+
+	callback_driven = true;
+}
+
+int main( int argc, char** argv ) {
+	int		errors = 0;
+
+	set_test_name( "config_test" );
+
+
+	auto x = new Xapp( (char*) "43086", false );
+	if(	fail_if( x == NULL, "could not allocate xapp" )  > 0 ) {
+		announce_results( 1 );
+		exit( 1 );
+	}
+
+	fprintf( stderr, "<INFO> sussing info from config1.json\n" );
+	auto c = new xapp::Config( "config1.json" );
+	errors += fail_if( c == NULL, "unable to allocate a config with alternate name" );
+
+	auto s = c->Get_control_str( "ves_collector_address" );
+	errors += fail_if( s.empty(), "expected control string not found" );
+	fprintf( stderr, "<INFO> collector address string var: %s\n", s.c_str() );
+
+	s = c->Get_port( "rmr-data-out" );
+	errors += fail_if( s.empty(), "expected port string not found" );
+	fprintf( stderr, "<INFO> port string var: %s\n", s.c_str() );
+
+	s = c->Get_port( "no-interface" );
+	errors += fail_if( ! s.empty(), "did not return empty when get port given an invalid name" );
+
+	s = c->Get_control_str( "no-such-control" );
+	errors += fail_if( ! s.empty(), "expected empty string for missing control got a string" );
+	if( ! s.empty() ) {
+		fprintf( stderr, "<INFO> unexpected string for no such control name:  %s\n", s.c_str() );
+	}
+
+	auto v = c->Get_control_value( "measurement_interval" );
+	errors += fail_if( v == 0.0, "epxected measurement interval control value not found" );
+
+	auto b = c->Get_control_bool( "debug_mode" );
+	errors += fail_if( b == false, "epxected debug mode control boolean not found or had wrong value" );
+
+
+	// ----- test sussing path and using default name ----------------------------------
+	fprintf( stderr, "<INFO> sussing info from default (no name)\n" );
+	c = new xapp::Config(  );							// drive for coverage
+
+	fprintf( stderr, "<INFO> sussing info from default (env var == ./config1.json)\n" );
+	setenv( (char *) "XAPP_DESCRIPTOR_PATH", "./config1.json", 1 );				// this var name is bad; it's not a path, but fname
+	c = new xapp::Config(  );
+
+	s = c->Get_control_str( "ves_collector_address" );
+	errors += fail_if( s.empty(), "expected collector address control string not found" );
+	fprintf( stderr, "<INFO> string var: %s\n", s.c_str() );
+
+	v = c->Get_control_value( "measurement_interval" );
+	errors += fail_if( v == 0.0, "expected measurement interval control value not found" );
+
+	b = c->Get_control_bool( "debug_mode" );
+	errors += fail_if( b == false, "expected debug mode control boolean not found" );
+
+
+	auto cs = c->Get_contents();
+	if( fail_if( cs.empty(), "get contents returned an empty string" ) == 0 ) {
+		fprintf( stderr, "<INFO> contents from file: %s\n", cs.c_str() );
+		fprintf( stderr, "<INFO> ---------------------\n" );
+	} else {
+		errors++;
+	}
+
+
+	// -------------- force callback to drive and test ---------------------------------
+
+	fprintf( stderr, "<INFO> load config-file.json for listener coverage testing\n" );
+	c = new xapp::Config( "config-file.json" );			// set filname with out leading path
+	c->Set_callback( ncb, data );						// for coverage in listener
+
+	fprintf( stderr, "<INFO> load ./config-file.json for callback testing\n" );
+	c = new xapp::Config( "./config-file.json" );
+	c->Set_callback( ncb, data );
+
+	fprintf( stderr, "<INFO> sleeping to give callback time to be initialsed\n" );
+	sleep( 4 );
+	if( rename( (char *) "./config-file.json", (char *) "./tmp-config.json" ) == 0 ) {		// rename (should not cause callback)
+		fprintf( stderr, "<INFO> file moved; sleeping a bit\n" );
+		sleep( 3 );
+		errors += fail_if( callback_driven, "callback was driven when file was deleted/moved away" );
+
+		if( rename( (char *) "./tmp-config.json", (char *) "./config-file.json" ) == 0 ) {		// put it back to drive callback
+			fprintf( stderr, "<INFO> sleeping to give callback time to be driven\n" );
+			sleep( 3 );
+
+			errors += fail_if( ! callback_driven, "callback was never executed" );
+		} else {
+			fprintf( stderr, "<WARN> attempt to move config file back failed: %s\n", strerror( errno ) );
+		}
+	} else {
+		fprintf( stderr, "<WARN> attempt to move config file away failed: %s\n", strerror( errno ) );
+	}
+
+
+	// ----- force errors where we can -----------------------------------------
+	fprintf( stderr, "<INFO> json parse errors expected to be reported now\n" );
+	c = new xapp::Config( "not-there.json" );						// json parse will fail
+
+	v = c->Get_control_value( "measurement_interval", 999 );
+	errors += fail_if( v != 999.0, "value from non-existant file wasn't default" );
+
+	s = c->Get_control_str( "ves_collector_address", "no-value" );
+	errors += fail_if( s.compare( "no-value" ) != 0, "string from non-existant file wasn't default" );
+
+	b = c->Get_control_bool( "debug_mode", false );
+	errors += fail_if( b, "boolean from non-existant file wasn't default" );
+
+	s = c->Get_port( "rmr-data-out" );
+	errors += fail_if( !s.empty(), "get port from bad jsonfile returned value" );
+
+	// ---------------------------- end housekeeping ---------------------------
+	announce_results( cb_errors + errors );
+	return !!errors;
+}
diff --git a/test/metrics_test.cpp b/test/metrics_test.cpp
index 3b939e3..120bbda 100644
--- a/test/metrics_test.cpp
+++ b/test/metrics_test.cpp
@@ -45,6 +45,7 @@
 #include "../src/xapp/xapp.hpp"
 
 #include "../src/metrics/metrics.hpp"		// overtly pull the code under test to get coverage opts
+#include "../src/messaging/messenger.cpp"
 #include "../src/metrics/metrics.cpp"
 
 #include "ut_support.cpp"
@@ -54,7 +55,7 @@
 	std::shared_ptr<Xapp> x;
 	std::shared_ptr<xapp::Metrics> m;
 
-	set_test_name( "jhash_test" );
+	set_test_name( "metrics_test" );
 
 	x = std::shared_ptr<Xapp>( new Xapp( "4560", true ) );
 	if( x == NULL ) {
diff --git a/test/unit_test.cpp b/test/unit_test.cpp
index 3e3c5bc..6c9cf31 100644
--- a/test/unit_test.cpp
+++ b/test/unit_test.cpp
@@ -365,7 +365,6 @@
 	b = std::move( c );						// move = operator
 	xapp::Alarm d = std::move( b );			// move constructor
 
-
 	// ------ minimal metrics to prove coverage ------------------------------------------------------------
 	metrics( x );
 
diff --git a/test/unit_test.sh b/test/unit_test.sh
index 3e06a53..c845553 100755
--- a/test/unit_test.sh
+++ b/test/unit_test.sh
@@ -1,6 +1,5 @@
 #!/usr/bin/env bash
 # vim: ts=4 sw=4 noet:
-
 #==================================================================================
 #       Copyright (c) 2020 Nokia
 #       Copyright (c) 2020 AT&T Intellectual Property.
@@ -99,6 +98,8 @@
 export LD_LIBRARY_PATH=$build_dir:/usr/local/lib:$LD_LIBRARY_PATH
 export LIBRARY_PATH=$build_dir:/usr/local/lib:$LIBRARY_PATH
 
+cp config1.json config-file.json						# ensure default named file is there too
+
 make nuke >/dev/null
 make tests >/tmp/PID$$.log 2>&1
 abort_if_error $? "unable to make"
@@ -106,10 +107,18 @@
 
 spew="cat"
 
-for x in unit_test jhash_test metrics_test
+#run everything, then generate coverage stats after all have run
+for x in metrics_test jhash_test config_test  unit_test
 do
 	./$x >/tmp/PID$$.log 2>&1
 	abort_if_error $? "test failed: $x"
+done
+
+# it seems that we loose coverage reporting if metrics_test's gcov file is generated
+# after unit test.  Very strange. To be safe, run unit_test last.
+#
+for x in metrics_test jhash_test config_test unit_test
+do
 	gcov $x.c >/dev/null 2>&1
 done