<commit_before>/*	opendatacon
*
*	Copyright (c) 2018:
*
*		DCrip3fJguWgVCLrZFfA7sIGgvx1Ou3fHfCxnrz4svAi
*		yxeOtDhDCXf1Z4ApgXvX5ahqQmzRfJ2DoX8S05SqHA==
*
*	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.
*/
/*
* CBTest.cpp
*
*  Created on: 10/09/2018
*      Author: <NAME> <<EMAIL>>
*/

// Disable excessing data on stack warning for the test file only.
#ifdef _MSC_VER
#pragma warning(disable: 6262)
#endif

#define COMPILE_TESTS

#if defined(COMPILE_TESTS)

// #include <trompeloeil.hpp> Not used at the moment - requires __cplusplus to be defined so the cppcheck works properly.
#include "CBMasterPort.h"
#include "CBOutstationPort.h"
#include "CBUtility.h"
#include <opendatacon/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <array>
#include <cassert>
#include <chrono>
#include <fstream>
#include <thread>
#include <utility>


#if defined(NONVSTESTING)
#include <catch.hpp>
#else
#include "spdlog/sinks/msvc_sink.h"
#include <catchvs.hpp> // This version has the hooks to display the tests in the VS Test Explorer - but also has some problems not destructing objects correctly.
#endif

#define SUITE(name) "CBTests - " name

// To remove GCC warnings
namespace RTUConnectedTests
{
extern const char *CBmasterconffile;
}

extern const char *conffilename1;
extern const char *conffilename2;
extern const char *conffile1;
extern const char *conffile2;

const char *conffilename1 = "CBConfig.conf";
const char *conffilename2 = "CBConfig2.conf";

#ifdef _MSC_VER
#pragma region conffiles
#endif
// We actually have the conf file here to match the tests it is used in below. We write out to a file (overwrite) on each test so it can be read back in.
const char *conffile1 = R"001(
{
	"IP" : "127.0.0.1",
	"Port" : 10000,
	"OutstationAddr" : 9,
	"TCPClientServer" : "SERVER",
	"LinkNumRetry": 4,

	//-------Point conf--------#
	// Conitel/baker switch
	"IsBakerDevice" : false,

	// If a binary event time stamp is outside 30 minutes of current time, replace the timestamp
	"OverrideOldTimeStamps" : false,

	// This flag will have the OutStation respond without waiting for ODC responses - it will still send the ODC commands, just no feedback. Useful for testing and connecting to the sim port.
	// Set to false, the OutStation will set up ODC/timeout callbacks/lambdas for ODC responses. If not found will default to false.
	"StandAloneOutstation" : true,

	// This defaults to 500. Allow setting to a low number so testing full queue is practical.
	"SOEQueueSize" : 15,

	// Maximum time to wait for CB Master responses to a command and number of times to retry a command.
	"CBCommandTimeoutmsec" : 3000,
	"CBCommandRetries" : 1,

	// Master only PollGroups - ignored by outstation
	"PollGroups" : [{"ID" : 1, "PollRate" : 10000, "Group" : 3, "PollType" : "Scan"},
					{"ID" : 2, "PollRate" : 20000, "Group" : 3, "PollType" : "SOEScan"},
					{"ID" : 3, "PollRate" : 120000, "PollType" : "TimeSetCommand"}],


	// The payload location can be 1B, 2A, 2B
	// Where there is a 24 bit result (ACC24) the next payload location will automatically be used. Do not put something else in there!
	// The point table will build a group list with all the data it has to collect for a given group number.
	// We can only use range for Binary and Control. For analog each one has to be defined singularly
	// SOE point definitions are optional. If missing - not an SOE point.

	// Digital IN
	// DIG - 12 bits to a Payload, Channel(bit) 1 to 12. On a range, the Channel is the first Channel in the range.
	// MCA,MCB,MCC - 6 bits fit in one payload

	// Analog IN
	// ANA - 1 Channel per payload
	// ANA6 - 2 Channels per payload

	// Counter IN
	// ACC12 - 1 to a payload,
	// ACC24 - takes two payloads.

	"Binaries" : [	{"Range" : {"Start" : 0, "Stop" : 11}, "Group" : 3, "PayloadLocation": "1B", "Channel" : 1, "Type" : "DIG", "SOE" : { "Index" : 0} },
					{"Index" : 12, "Group" : 3, "PayloadLocation": "2A", "Channel" : 1, "Type" : "MCA", "SOE" : { "Index" : 12}},
					{"Index" : 13, "Group" : 3, "PayloadLocation": "2A", "Channel" : 2, "Type" : "MCB"},
					{"Index" : 14, "Group" : 3, "PayloadLocation": "2A", "Channel" : 3, "Type" : "MCC" },
					{"Index" : 81, "Group" : 6, "PayloadLocation": "2A", "Channel" : 2, "Type" : "MCB","SOE" : { "Index" : 82}},
					{"Index" : 90, "Group" : 6, "PayloadLocation": "2A", "Channel" : 3, "Type" : "MCC","SOE" : { "Index" : 91}} ],

	"Analogs" : [	{"Index" : 0, "Group" : 3, "PayloadLocation": "3A","Channel" : 1, "Type":"ANA"},
					{"Index" : 1, "Group" : 3, "PayloadLocation": "3B","Channel" : 1, "Type":"ANA"},
					{"Index" : 2, "Group" : 3, "PayloadLocation": "4A","Channel" : 1, "Type":"ANA"},
					{"Index" : 3, "Group" : 3, "PayloadLocation": "5B","Channel" : 1, "Type":"ANA6"},
					{"Index" : 4, "Group" : 3, "PayloadLocation": "5B","Channel" : 2, "Type":"ANA6"}],

	// CONTROL up to 12 bits per group address, Channel 1 to 12. Python simulator used dual points one for trip one for close.
	"BinaryControls" : [{"Index": 1,  "Group" : 4, "Channel" : 1, "Type" : "CONTROL"},
						{"Range" : {"Start" : 10, "Stop" : 21}, "Group" : 3, "Channel" : 1, "Type" : "CONTROL"}],

	// Setpoint A/B maps from Channel 1 == A, Channel 2 == B
	"AnalogControls" : [{"Index": 1,  "Group" : 3, "Channel" : 1, "Type" : "CONTROL"}],

	// Special definition, so we know where to find the Remote Status Data in the scanned group.
	"RemoteStatus" : [{"Group":3, "Channel" : 1, "PayloadLocation": "5A"}]

})001";

// We actually have the conf file here to match the tests it is used in below. We write out to a file (overwrite) on each test so it can be read back in.
const char *conffile2 = R"002(
{
	"IP" : "127.0.0.1",
	"Port" : 10000,
	"OutstationAddr" : 10,
	"TCPClientServer" : "SERVER",
	"LinkNumRetry": 4,

	//-------Point conf--------#
	// We have two modes for the digital/binary commands. Can be one or the other - not both!
	"IsBakerDevice" : false,

	// The magic Analog point we use to pass through the CB time set command.
	"StandAloneOutstation" : true,

	// Maximum time to wait for CB Master responses to a command and number of times to retry a command.
	"CBCommandTimeoutmsec" : 4000,
	"CBCommandRetries" : 1,

	"Binaries" : [{"Range" : {"Start" : 0, "Stop" : 11}, "Group" : 3, "PayloadLocation": "1B", "Channel" : 1, "Type" : "DIG"}],

	// CONTROL up to 12 bits per group address, Channel 1 to 12. Simulator used dual points one for trip one for close.
	"BinaryControls" : [{"Index": 20,  "Group" : 5, "Channel" : 1, "Type" : "CONTROL"}]

})002";

#ifdef _MSC_VER
#pragma endregion

#pragma region TEST_HELPERS
#endif

// Write out the conf file information about into a file so that it can be read back in by the code.
void WriteConfFilesToCurrentWorkingDirectory()
{
	std::ofstream ofs(conffilename1);
	if (!ofs)
	{
		FAIL("Could not open conffile1 for writing");
	}

	ofs << conffile1;
	ofs.close();

	std::ofstream ofs2(conffilename2);
	if (!ofs2)
	{
		FAIL("Could not open conffile2 for writing");
	}

	ofs2 << conffile2;
	ofs2.close();
}

void SetupLoggers(spdlog::level::level_enum log_level)
{
	if (auto log = odc::spdlog_get("CBPort"))
		return; // Already exists

	// So create the log sink first - can be more than one and add to a vector.
	#if defined(NONVSTESTING)
	auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
	#else
	auto console_sink = std::make_shared<spdlog::sinks::msvc_sink_mt>();
	#endif
	auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("cbporttest.log", true);

	std::vector<spdlog::sink_ptr> sinks = { file_sink,console_sink };

	auto pLibLogger = std::make_shared<spdlog::logger>("CBPort", begin(sinks),end(sinks));
	pLibLogger->set_level(log_level);
	odc::spdlog_register_logger(pLibLogger);

	// We need an opendatacon logger to catch config file parsing errors
	auto pODCLogger = std::make_shared<spdlog::logger>("opendatacon", begin(sinks), end(sinks));
	pODCLogger->set_level(log_level);
	odc::spdlog_register_logger(pODCLogger);

}
void WriteStartLoggingMessage(const std::string& TestName)
{
	std::string msg = "Logging for '"+TestName+"' started..";

	if (auto cblogger = odc::spdlog_get("CBPort"))
	{
		cblogger->info("------------------");
		cblogger->info(msg);
	}
	else
		std::cout << "Error CBPort Logger not operational";

/*	if (auto odclogger = odc::spdlog_get("opendatacon"))
                          odclogger->info(msg);
            else
                          std::cout << "Error opendatacon Logger not operational";
                          */
}
void TestSetup(const std::string& TestName, bool writeconffiles = true)
{
	#ifndef NONVSTESTING
	SetupLoggers(spdlog::level::debug);
	#endif
	WriteStartLoggingMessage(TestName);

	if (writeconffiles)
		WriteConfFilesToCurrentWorkingDirectory();
}
void TestTearDown(void)
{
	LOGINFO("Test Finished");
	#ifndef NONVSTESTING

	spdlog::drop_all(); // Un-register loggers, and if no other shared_ptr references exist, they will be destroyed.
	#endif
}
// Used for command line test setup
void CommandLineLoggingSetup(spdlog::level::level_enum log_level)
{
	SetupLoggers(log_level);
	WriteStartLoggingMessage("All Tests");
}
void CommandLineLoggingCleanup()
{
	odc::spdlog_flush_all();
	odc::spdlog_drop_all(); // Un-register loggers, and if no other shared_ptr references exist, they will be destroyed.
}
void WaitIOS(odc::asio_service &IOS, int seconds)
{
	auto timer = IOS.make_steady_timer();
	timer->expires_from_now(std::chrono::seconds(seconds));
	timer->wait();
}


// Don't like using macros, but we use the same test set up almost every time.
#define STANDARD_TEST_SETUP()\
	TestSetup(Catch::getResultCapture().getCurrentTestName());\
	auto IOS = odc::asio_service::Get()

// Used for tests that dont need IOS
#define SIMPLE_TEST_SETUP()\
	TestSetup(Catch::getResultCapture().getCurrentTestName())

#define STANDARD_TEST_TEARDOWN()\
	TestTearDown()

#define START_IOS(ThreadCount) \
	LOGINFO("Starting ASIO Threads"); \
	auto work = IOS->make_work(); /* To keep run - running!*/\
	std::vector<std::thread> threads; \
	for (int i = 0; i < (ThreadCount); i++) threads.emplace_back([IOS] { IOS->run(); })

#define STOP_IOS() \
	LOGINFO("Shutting Down ASIO Threads");    \
	work.reset();     \
	for (auto& t : threads) t.join()

#define TEST_CBMAPort(overridejson)\
	auto CBMAPort = std::make_shared<CBMasterPort>("TestMaster", conffilename1, overridejson); \
	CBMAPort->Build()

#define TEST_CBMAPort2(overridejson)\
	auto CBMAPort2 = std::make_shared<CBMasterPort>("TestMaster", conffilename2, overridejson); \
	CBMAPort2->Build()


#define TEST_CBOSPort(overridejson)      \
	auto CBOSPort = std::make_shared<CBOutstationPort>("TestOutStation", conffilename1, overridejson);   \
	CBOSPort->Build()

#define TEST_CBOSPort2(overridejson)     \
	auto CBOSPort2 = std::make_shared<CBOutstationPort>("TestOutStation2", conffilename2, overridejson); \
	CBOSPort2->Build()

#ifdef _MSC_VER
#pragma endregion TEST_HELPERS
#endif

namespace SimpleUnitTestsCB
{
TEST_CASE("Util - HexStringTest")
{
	SIMPLE_TEST_SETUP();
	std::string ts = "c406400f0b00"  "0000fffe9000";
	std::string w1 = { ToChar(0xc4),0x06,0x40,0x0f,0x0b,0x00 };
	std::string w2 = { 0x00,0x00,ToChar(0xff),ToChar(0xfe),ToChar(0x90),0x00 };

	std::string res = BuildBinaryStringFromASCIIHexString(ts);
	REQUIRE(res == (w1 + w2));
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - CBBCHTest")
{
	SIMPLE_TEST_SETUP();
	CBBlockData res = CBBlockData(0x09200028);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x000c0020);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x0009111e);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x00291106);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x22290030);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x22280126);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x08080029);
	REQUIRE(res.BCHPasses());
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - ParsePayloadString")
{
	SIMPLE_TEST_SETUP();
	PayloadLocationType payloadlocation;

	bool res = CBPointConf::ParsePayloadString("16B", payloadlocation);
	REQUIRE(res == true);
	REQUIRE(payloadlocation.Packet == 16);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionB);

	res = CBPointConf::ParsePayloadString("1B", payloadlocation);
	REQUIRE(res == true);
	REQUIRE(payloadlocation.Packet == 1);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionB);

	res = CBPointConf::ParsePayloadString("16A", payloadlocation);
	REQUIRE(res == true);
	REQUIRE(payloadlocation.Packet == 16);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionA);
	LOGINFO("Ignore Next Three LOGGGED Errors");

	res = CBPointConf::ParsePayloadString("1A", payloadlocation);
	REQUIRE(res == false);
	REQUIRE(payloadlocation.Packet == 1);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionA);

	res = CBPointConf::ParsePayloadString("0A", payloadlocation);
	REQUIRE(res == false);
	REQUIRE(payloadlocation.Position == PayloadABType::Error);

	res = CBPointConf::ParsePayloadString("123", payloadlocation);
	REQUIRE(res == false);
	REQUIRE(payloadlocation.Position == PayloadABType::Error);
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - MakeChannelID")
{
	SIMPLE_TEST_SETUP();
	std::string IP = "192.168.1.23";
	std::string Port = "34567";
	bool isaserver = true;

	std::string res = CBConnection::MakeChannelID(IP, Port, isaserver);
	REQUIRE(res == "192.168.1.23:34567:1");

	isaserver = false;
	IP = "127.0.0.1";
	Port = "10000";
	res = CBConnection::MakeChannelID(IP, Port, isaserver);
	REQUIRE(res == "127.0.0.1:10000:0");
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - CBIndexTest")
{
	SIMPLE_TEST_SETUP();
	uint8_t group = 1;
	uint8_t channel = 1;
	PayloadLocationType payloadlocation(1, PayloadABType::PositionA);

	// Top 4 bits group (0-15), Next 4 bits channel (1-12), next 4 bits payload packet number (0-15), next 4 bits 0(A) or 1(B)
	uint16_t CBIndex = CBPointTableAccess::GetCBPointMapIndex(group, channel, payloadlocation);
	REQUIRE(CBIndex == 0x1100);

	group = 15;
	channel = 12;
	payloadlocation = PayloadLocationType(16, PayloadABType::PositionB);
	CBIndex = CBPointTableAccess::GetCBPointMapIndex(group, channel, payloadlocation);
	REQUIRE(CBIndex == 0xFCF1);
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("Util - CBPort::BuildUpdateTimeMessage")
{
	SIMPLE_TEST_SETUP();
	uint8_t address = 1;
	auto cbtime = static_cast<CBTime>(0x0000016338b6d4fb); // A value around June 2018

	CBMessage_t CompleteCBMessage;

	CBMasterPort::BuildUpdateTimeMessage(address, cbtime, CompleteCBMessage);

	REQUIRE(CompleteCBMessage.size() == 2);

	uint16_t B0Data = CompleteCBMessage[0].GetB();
	uint16_t A1Data = CompleteCBMessage[1].GetA();
	uint16_t B1Data = CompleteCBMessage[1].GetB();

	REQUIRE(B0Data == 0x0020);
	REQUIRE(A1Data == 0x0f04);
	REQUIRE(B1Data == 0x00fb);
	REQUIRE(CompleteCBMessage[0].IsEndOfMessageBlock() == false);
	REQUIRE(CompleteCBMessage[1].IsEndOfMessageBlock() == true);

	uint8_t hhin;
	uint8_t mmin;
	uint8_t ssin;
	uint16_t msecin;

	DecodeTimePayload(B0Data, A1Data, B1Data, hhin, mmin, ssin, msecin);

	uint64_t rxdmsec = (((uint64_t)hhin * 60 + (uint64_t)mmin) * 60 + (uint64_t)ssin)*1000 + (uint64_t)msecin;

	// We have to remove (mod) the cbtime by msec in a day to get the expected value.
	cbtime = cbtime % (1000 * 60 * 60 * 24);
	REQUIRE(rxdmsec == cbtime);

	// Decode an actual packet.
	// The Ethernet Epoch Frame time is 1560896915.562135000 seconds
	// The packet data is 1B - 0x022, 2A - 0x1c9, 2B - 0x121
	// Arrival Time : Jun 19, 2019 08 : 28 : 35.562135000 AUS Eastern Standard Time

	DecodeTimePayload(0x022, 0x1c9, 0x121, hhin, mmin, ssin, msecin);
	uint64_t epochtimemsecsfull = 1560896915562;
	uint64_t epochtimemsecs = epochtimemsecsfull % (1000 * 60 * 60 * 24);

	rxdmsec = (((uint64_t)hhin * 60 + (uint64_t)mmin) * 60 + (uint64_t)ssin) * 1000 + (uint64_t)msecin;

	LOGDEBUG("Received Time Set Command, Decoded {}, {} msec, PacketStamp {}, {} msec", to_stringfromhhmmssmsec(hhin, mmin, ssin, msecin), rxdmsec, to_stringfromCBtime(epochtimemsecsfull),epochtimemsecs);

	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - SOEEventFormat")
{
	SIMPLE_TEST_SETUP();
	SOEEventFormat S;

	//auto cbtime = static_cast<CBTime>(0x0000016338b6d4fb); // A value around June 2018 //Not used - what was it going to be used for?

	S.Group = 5;     // 3 bits b101
	S.Number = 0x41; // 7 bits b1000001 - 0x41
	S.ValueBit = true;
	S.QualityBit = false;
	S.TimeFormatBit = true; // Long format

	S.Hour = 0x11;         // 5 bits	b10001 - 0x11
	S.Minute = 0x21;       // 6 bits b10 0001 - 0x21
	S.Second = 0x21;       // 6 bits b10 0001 - 0x21
	S.Millisecond = 0x201; // 10 bits b10 0000 0001 - 0x201
	S.LastEventFlag = false;

	uint64_t res = S.GetFormattedData();

	REQUIRE(res == 0xB06C618601000000);

	// Now test our BitArray handling.
	std::array<bool, MaxSOEBits> BitArray{};
	for (int i = 0; i < 64; i++)
		BitArray[i] = ((res >> (63 - i)) & 0x01) == 0x01;

	uint32_t newstartbit = 0;
	bool Success = false;
	SOEEventFormat SOE(BitArray, 0, 64, newstartbit, 0, Success); // Build a new SOE record from the BitArray

	REQUIRE(newstartbit == 41);
	REQUIRE(SOE.GetResultBitLength() == 41); // Should be long...
	REQUIRE(SOE.GetFormattedData() == 0xB06C618601000000);
	REQUIRE(SOE.Group == 5);
	REQUIRE(SOE.Number == 0x41);
	REQUIRE(SOE.ValueBit == true);
	REQUIRE(SOE.TimeFormatBit == true);
	REQUIRE(SOE.Hour == 0x11);
	REQUIRE(SOE.Minute == 0x21);
	REQUIRE(SOE.Second == 0x21);
	REQUIRE(SOE.Millisecond == 0x201);
	REQUIRE(SOE.LastEventFlag == false);
	REQUIRE(Success);


	S.TimeFormatBit = false; // Short format
	S.LastEventFlag = true;

	res = S.GetFormattedData();

	REQUIRE(res == 0xb064300c00000000);

	// Now test our BitArray handling.
	std::array<bool, MaxSOEBits> BitArray2{};
	for (int i = 0; i < 64; i++)
		BitArray2[i] = ((res >> (63 - i)) & 0x01) == 0x01;

	newstartbit = 0;
	SOEEventFormat SOE2(BitArray2, 0, 64, newstartbit, 0, Success); // Build a new SOE record from the BitArray

	REQUIRE(newstartbit == 30);
	REQUIRE(SOE2.GetResultBitLength() == 41); // Always long
	REQUIRE(SOE2.GetFormattedData() == 0xb068008601800000);
	REQUIRE(SOE2.Group == 5);
	REQUIRE(SOE2.Number == 0x41);
	REQUIRE(SOE2.ValueBit == true);
	REQUIRE(SOE2.TimeFormatBit == true);
	REQUIRE(SOE2.Second == 0x21);
	REQUIRE(SOE2.Millisecond == 0x201);
	REQUIRE(SOE2.LastEventFlag == true);
	REQUIRE(Success);

	uint64_t payload = 0x9945455800000000; // From a packet capture
	// Now test our BitArray handling.
	std::array<bool, MaxSOEBits> BitArray3{};
	for (int i = 0; i < 64; i++)
		BitArray3[i] = ((payload >> (63 - i)) & 0x01) == 0x01;

	newstartbit = 0;
	SOEEventFormat SOE3(BitArray3, 0, 64, newstartbit, 0, Success); // Build a new SOE record from the BitArray

	REQUIRE(SOE3.Group == 4);
	REQUIRE(SOE3.Number == 0x65);

	REQUIRE(Success);

	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("Util - ConfigFileLoadTest")
{
	//This is a test to load the autogenerated config file from the Mosaic database output.
/*	STANDARD_TEST_SETUP();
      auto CBOSPort = std::make_unique<CBOutstationPort>("TestOutStation", "CBAutoGenConfig.conf", Json::nullValue);

      CBOSPort->SetIOS(IOS);
      CBOSPort->Build();

      STANDARD_TEST_TEARDOWN();
      */
}

#ifdef _MSC_VER
#pragma region Block Tests
#endif

TEST_CASE("CBBlock - ClassConstructor1")
{
	SIMPLE_TEST_SETUP();
	CBBlockArray msg = { 0x09,0x20,0x00,0x28}; // From a packet capture - this is the address packet

	CBBlockData b(msg);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor2")
{
	SIMPLE_TEST_SETUP();
	CBBlockData b2("22290030");
	REQUIRE(b2.GetData() == 0x22290030);
	REQUIRE(b2.GetA() == 0x222);
	REQUIRE(b2.GetB() == 0x200);
	REQUIRE(b2.IsEndOfMessageBlock() == false);
	REQUIRE(b2.IsDataBlock());
	REQUIRE(b2.BCHPasses());
	REQUIRE(b2.CheckBBitIsZero());

	CBBlockData b3("08080029");
	REQUIRE(b3.GetData() == 0x08080029);
	REQUIRE(b3.GetA() == 0x080);
	REQUIRE(b3.GetB() == 0x000);
	REQUIRE(b3.IsEndOfMessageBlock() == true);
	REQUIRE(b3.IsDataBlock());
	REQUIRE(b3.BCHPasses());
	REQUIRE(b3.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor3")
{
	SIMPLE_TEST_SETUP();
	uint8_t d[] = { 0x09, 0x20, 0x00,0x28 };
	CBBlockData b(d[0], d[1], d[2], d[3]);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	CBBlockData c(numeric_cast<char>(d[0]), numeric_cast<char>(d[1]), numeric_cast<char>(d[2]), numeric_cast<char>(d[3]));

	REQUIRE(c.GetStationAddress() == 9);
	REQUIRE(c.GetGroup() == 2);
	REQUIRE(c.GetFunctionCode() == 0);
	REQUIRE(c.GetB() == 0);
	REQUIRE(c.IsEndOfMessageBlock() == false);
	REQUIRE(c.IsAddressBlock());
	REQUIRE(c.BCHPasses());
	REQUIRE(c.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor4")
{
	SIMPLE_TEST_SETUP();
	uint16_t AData = 0xF23;
	uint16_t BData = 0x102;
	bool lastblock = false;

	CBBlockData b(AData, BData, lastblock);

	REQUIRE(b.GetA() == AData);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock() == false);
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	AData = 0x023;

	b.SetA(AData);
	REQUIRE(b.GetA() == AData);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock() == false);
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	BData = 0xF01;

	b.SetB(BData);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock() == false);
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor5")
{
	SIMPLE_TEST_SETUP();
	// Need to test the construction of an address block. What is B set to??
	uint8_t StationAddress = 4;
	uint8_t Group = 2;
	uint8_t FunctionCode = 5;
	uint16_t BData = 0x040;
	bool lastblock = false;

	CBBlockData b(StationAddress, Group, FunctionCode, BData, lastblock);
	REQUIRE(b.GetStationAddress() == StationAddress);
	REQUIRE(b.GetGroup() == Group);
	REQUIRE(b.GetFunctionCode() == FunctionCode);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	b.DoBakerConitelSwap(); // Swap Group and Station

	REQUIRE(b.GetStationAddress() == Group);
	REQUIRE(b.GetGroup() == StationAddress);
	REQUIRE(b.GetFunctionCode() == FunctionCode);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("CBBlock - BlockBuilding")
{
	SIMPLE_TEST_SETUP();

	CBBlockData b;

	// From a packet capture - this is the address packet
	// Test the block building code.
	b.AddByteToBlock(0x09);
	b.AddByteToBlock(0x20);
	b.AddByteToBlock(0x00);
	b.AddByteToBlock(0x28);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.IsValidBlock());

	// Now add some more bytes, check that they block is invalid until we should have a valid block again...
	b.AddByteToBlock(0x09);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x09);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x20);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x00);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x28);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.IsValidBlock());

	b.Clear();
	b.AddByteToBlock(0x09);
	b.AddByteToBlock(0x20);
	b.AddByteToBlock(0x00);
	b.AddByteToBlock(0x28);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.IsValidBlock());

	STANDARD_TEST_TEARDOWN();
}


#ifdef _MSC_VER
#pragma endregion Block Tests
#endif
}

namespace StationTests
{
void SendBinaryEvent(std::shared_ptr<CBOutstationPort> &CBOSPort, int ODCIndex, bool val, QualityFlags qual = QualityFlags::ONLINE, msSinceEpoch_t time = msSinceEpoch())
{
	auto event = std::make_shared<EventInfo>(EventType::Binary, ODCIndex, "Testing", qual, time);
	event->SetPayload<EventType::Binary>(std::move(val));

	CommandStatus res = CommandStatus::NOT_AUTHORIZED;
	auto pStatusCallback = std::make_shared<std::function<void(CommandStatus)>>([=, &res](CommandStatus command_stat)
		{
			res = command_stat;
		});

	CBOSPort->Event(event, "TestHarness", pStatusCallback);
	REQUIRE(res == CommandStatus::SUCCESS); // The Get will Wait for the result to be set.
}

TEST_CASE("Station - ScanRequest F0")
{
	// So we send a Scan F0 packet to the Outstation, it responds with the data in the point table.
	// Then we update the data in the point table, scan again and check the data we get back.
	STANDARD_TEST_SETUP();
	TEST_CBOSPort(Json::nullValue);

	START_IOS(2);

	CBOSPort->Enable();

	uint8_t station = 9;
	uint8_t group = 3;
	CBBlockData commandblock(station, group, FUNC_SCAN_DATA, 0, true);
	asio::streambuf write_buffer;
	std::ostream output(&write_buffer);


	CommandStatus res = CommandStatus::NOT_AUTHORIZED;
	auto pStatusCallback = std::make_shared<std::function<void(CommandStatus)>>([=, &res](CommandStatus command_stat)
		{
			res = command_stat;
		});

	// Hook the output function with a lambda
	std::string Response = "Not Set";
	std::atomic_bool done_flag(false);
	CBOSPort->SetSendTCPDataFn([&Response,&done_flag](std::string CBMessage) { Response = std::move(CBMessage); done_flag = true; });

	// Send the commands in as if came from TCP channel
	output << commandblock.ToBinaryString();
	CBOSPort->InjectSimulatedTCPMessage(write_buffer);

	// Check the command is formatted correctly
	std::string DesiredResult = "0937ffaa" // Echoed block plus data 1B
	                            "14080022" // Data 2A and 2B
	                            "00080006"
	                            "00080006"
	                            "00080007";

	while(!done_flag)
		IOS->poll_one();
	done_flag = false;

	// No need to delay to process result, all done in the InjectCommand at call time.
	REQUIRE(BuildASCIIHexStringfromBinaryString(Response) == DesiredResult);


	// Call the Event functions to set the CB table data to what we are expecting to get back.
	// Write to the analog registers that we are going to request the values for.
	for (int ODCIndex = 0; ODCIndex < 3; ODCIndex++)
	{
		auto event = std::make_shared<EventInfo>(EventType::Analog, ODCIndex);
		event->SetPayload<EventType::Analog>(1024 + ODCIndex);

		CBOSPort->Event(event, "TestHarness", pStatusCallback);
		REQUIRE((res == CommandStatus::SUCCESS)); // The Get will Wait for the result to be set.
	}
	for (int ODCIndex = 3; ODCIndex < 5; ODCIndex++)
	{
		auto event = std::make_shared<EventInfo>(EventType::Analog, ODCIndex);
		event->SetPayload<EventType::Analog>(1 + ODCIndex);

		CBOSPort->Event(event, "TestHarness", pStatusCallback);
		REQUIRE((res == CommandStatus::SUCCESS)); // The Get will Wait for the result to be set.
	}

	for (int ODCIndex = 0; ODCIndex < 12; ODCIndex++)
	{
		SendBinaryEvent(CBOSPort, ODCIndex, ((ODCIndex % 2) == 0));
	}

	// MCA,MCB,MCC Set to starting values
	SendBinaryEvent(CBOSPort, 12, true);  //CLOSED // MCA inverted on the wire!!
	SendBinaryEvent(CBOSPort, 13, false); //OPEN
	SendBinaryEvent(CBOSPort, 14, true);  //CLOSED

	WaitIOS(*IOS, 1);

	Response = "Not Set";
	output << commandblock.ToBinaryString();
	CBOSPort->InjectSimulatedTCPMessage(write_buffer); // Scan Data Group 3 - analog values and digitals have now changed. No SOE overflow, SOE Data Available in RSW

	// Should now get different data!
	DesiredResult = "09355516" // Echoed block plus data 1B
	                "04080034" // Data 2A and 2B
	                "400a00b6"
	                "4028000c"
	                "80088297";

	CBMessage_t Msg = BuildCBMessageFromASCIIHexString(DesiredResult);
	assert(Msg[2].GetA() == 1024, "Checking payload values");
	assert(Msg[2].GetB() == 1025, "Checking payload values");
	assert(Msg[3].GetA() == 1026, "Checking payload values");
	assert(Msg[4].GetB() == (4 << 6) + 5, "Checking payload values");	// The require below does the whole check in one go!

	while(!done_flag)
		IOS->poll_one();
	done_flag = false;

	REQUIRE(BuildASCIIHexStringfromBinaryString(Response) == DesiredResult);<commit_msg>Swapping between perl, python and c++ too much!<commit_after>/*	opendatacon
*
*	Copyright (c) 2018:
*
*		DCrip3fJguWgVCLrZFfA7sIGgvx1Ou3fHfCxnrz4svAi
*		yxeOtDhDCXf1Z4ApgXvX5ahqQmzRfJ2DoX8S05SqHA==
*
*	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.
*/
/*
* CBTest.cpp
*
*  Created on: 10/09/2018
*      Author: <NAME> <<EMAIL>>
*/

// Disable excessing data on stack warning for the test file only.
#ifdef _MSC_VER
#pragma warning(disable: 6262)
#endif

#define COMPILE_TESTS

#if defined(COMPILE_TESTS)

// #include <trompeloeil.hpp> Not used at the moment - requires __cplusplus to be defined so the cppcheck works properly.
#include "CBMasterPort.h"
#include "CBOutstationPort.h"
#include "CBUtility.h"
#include <opendatacon/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <array>
#include <cassert>
#include <chrono>
#include <fstream>
#include <thread>
#include <utility>


#if defined(NONVSTESTING)
#include <catch.hpp>
#else
#include "spdlog/sinks/msvc_sink.h"
#include <catchvs.hpp> // This version has the hooks to display the tests in the VS Test Explorer - but also has some problems not destructing objects correctly.
#endif

#define SUITE(name) "CBTests - " name

// To remove GCC warnings
namespace RTUConnectedTests
{
extern const char *CBmasterconffile;
}

extern const char *conffilename1;
extern const char *conffilename2;
extern const char *conffile1;
extern const char *conffile2;

const char *conffilename1 = "CBConfig.conf";
const char *conffilename2 = "CBConfig2.conf";

#ifdef _MSC_VER
#pragma region conffiles
#endif
// We actually have the conf file here to match the tests it is used in below. We write out to a file (overwrite) on each test so it can be read back in.
const char *conffile1 = R"001(
{
	"IP" : "127.0.0.1",
	"Port" : 10000,
	"OutstationAddr" : 9,
	"TCPClientServer" : "SERVER",
	"LinkNumRetry": 4,

	//-------Point conf--------#
	// Conitel/baker switch
	"IsBakerDevice" : false,

	// If a binary event time stamp is outside 30 minutes of current time, replace the timestamp
	"OverrideOldTimeStamps" : false,

	// This flag will have the OutStation respond without waiting for ODC responses - it will still send the ODC commands, just no feedback. Useful for testing and connecting to the sim port.
	// Set to false, the OutStation will set up ODC/timeout callbacks/lambdas for ODC responses. If not found will default to false.
	"StandAloneOutstation" : true,

	// This defaults to 500. Allow setting to a low number so testing full queue is practical.
	"SOEQueueSize" : 15,

	// Maximum time to wait for CB Master responses to a command and number of times to retry a command.
	"CBCommandTimeoutmsec" : 3000,
	"CBCommandRetries" : 1,

	// Master only PollGroups - ignored by outstation
	"PollGroups" : [{"ID" : 1, "PollRate" : 10000, "Group" : 3, "PollType" : "Scan"},
					{"ID" : 2, "PollRate" : 20000, "Group" : 3, "PollType" : "SOEScan"},
					{"ID" : 3, "PollRate" : 120000, "PollType" : "TimeSetCommand"}],


	// The payload location can be 1B, 2A, 2B
	// Where there is a 24 bit result (ACC24) the next payload location will automatically be used. Do not put something else in there!
	// The point table will build a group list with all the data it has to collect for a given group number.
	// We can only use range for Binary and Control. For analog each one has to be defined singularly
	// SOE point definitions are optional. If missing - not an SOE point.

	// Digital IN
	// DIG - 12 bits to a Payload, Channel(bit) 1 to 12. On a range, the Channel is the first Channel in the range.
	// MCA,MCB,MCC - 6 bits fit in one payload

	// Analog IN
	// ANA - 1 Channel per payload
	// ANA6 - 2 Channels per payload

	// Counter IN
	// ACC12 - 1 to a payload,
	// ACC24 - takes two payloads.

	"Binaries" : [	{"Range" : {"Start" : 0, "Stop" : 11}, "Group" : 3, "PayloadLocation": "1B", "Channel" : 1, "Type" : "DIG", "SOE" : { "Index" : 0} },
					{"Index" : 12, "Group" : 3, "PayloadLocation": "2A", "Channel" : 1, "Type" : "MCA", "SOE" : { "Index" : 12}},
					{"Index" : 13, "Group" : 3, "PayloadLocation": "2A", "Channel" : 2, "Type" : "MCB"},
					{"Index" : 14, "Group" : 3, "PayloadLocation": "2A", "Channel" : 3, "Type" : "MCC" },
					{"Index" : 81, "Group" : 6, "PayloadLocation": "2A", "Channel" : 2, "Type" : "MCB","SOE" : { "Index" : 82}},
					{"Index" : 90, "Group" : 6, "PayloadLocation": "2A", "Channel" : 3, "Type" : "MCC","SOE" : { "Index" : 91}} ],

	"Analogs" : [	{"Index" : 0, "Group" : 3, "PayloadLocation": "3A","Channel" : 1, "Type":"ANA"},
					{"Index" : 1, "Group" : 3, "PayloadLocation": "3B","Channel" : 1, "Type":"ANA"},
					{"Index" : 2, "Group" : 3, "PayloadLocation": "4A","Channel" : 1, "Type":"ANA"},
					{"Index" : 3, "Group" : 3, "PayloadLocation": "5B","Channel" : 1, "Type":"ANA6"},
					{"Index" : 4, "Group" : 3, "PayloadLocation": "5B","Channel" : 2, "Type":"ANA6"}],

	// CONTROL up to 12 bits per group address, Channel 1 to 12. Python simulator used dual points one for trip one for close.
	"BinaryControls" : [{"Index": 1,  "Group" : 4, "Channel" : 1, "Type" : "CONTROL"},
						{"Range" : {"Start" : 10, "Stop" : 21}, "Group" : 3, "Channel" : 1, "Type" : "CONTROL"}],

	// Setpoint A/B maps from Channel 1 == A, Channel 2 == B
	"AnalogControls" : [{"Index": 1,  "Group" : 3, "Channel" : 1, "Type" : "CONTROL"}],

	// Special definition, so we know where to find the Remote Status Data in the scanned group.
	"RemoteStatus" : [{"Group":3, "Channel" : 1, "PayloadLocation": "5A"}]

})001";

// We actually have the conf file here to match the tests it is used in below. We write out to a file (overwrite) on each test so it can be read back in.
const char *conffile2 = R"002(
{
	"IP" : "127.0.0.1",
	"Port" : 10000,
	"OutstationAddr" : 10,
	"TCPClientServer" : "SERVER",
	"LinkNumRetry": 4,

	//-------Point conf--------#
	// We have two modes for the digital/binary commands. Can be one or the other - not both!
	"IsBakerDevice" : false,

	// The magic Analog point we use to pass through the CB time set command.
	"StandAloneOutstation" : true,

	// Maximum time to wait for CB Master responses to a command and number of times to retry a command.
	"CBCommandTimeoutmsec" : 4000,
	"CBCommandRetries" : 1,

	"Binaries" : [{"Range" : {"Start" : 0, "Stop" : 11}, "Group" : 3, "PayloadLocation": "1B", "Channel" : 1, "Type" : "DIG"}],

	// CONTROL up to 12 bits per group address, Channel 1 to 12. Simulator used dual points one for trip one for close.
	"BinaryControls" : [{"Index": 20,  "Group" : 5, "Channel" : 1, "Type" : "CONTROL"}]

})002";

#ifdef _MSC_VER
#pragma endregion

#pragma region TEST_HELPERS
#endif

// Write out the conf file information about into a file so that it can be read back in by the code.
void WriteConfFilesToCurrentWorkingDirectory()
{
	std::ofstream ofs(conffilename1);
	if (!ofs)
	{
		FAIL("Could not open conffile1 for writing");
	}

	ofs << conffile1;
	ofs.close();

	std::ofstream ofs2(conffilename2);
	if (!ofs2)
	{
		FAIL("Could not open conffile2 for writing");
	}

	ofs2 << conffile2;
	ofs2.close();
}

void SetupLoggers(spdlog::level::level_enum log_level)
{
	if (auto log = odc::spdlog_get("CBPort"))
		return; // Already exists

	// So create the log sink first - can be more than one and add to a vector.
	#if defined(NONVSTESTING)
	auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
	#else
	auto console_sink = std::make_shared<spdlog::sinks::msvc_sink_mt>();
	#endif
	auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("cbporttest.log", true);

	std::vector<spdlog::sink_ptr> sinks = { file_sink,console_sink };

	auto pLibLogger = std::make_shared<spdlog::logger>("CBPort", begin(sinks),end(sinks));
	pLibLogger->set_level(log_level);
	odc::spdlog_register_logger(pLibLogger);

	// We need an opendatacon logger to catch config file parsing errors
	auto pODCLogger = std::make_shared<spdlog::logger>("opendatacon", begin(sinks), end(sinks));
	pODCLogger->set_level(log_level);
	odc::spdlog_register_logger(pODCLogger);

}
void WriteStartLoggingMessage(const std::string& TestName)
{
	std::string msg = "Logging for '"+TestName+"' started..";

	if (auto cblogger = odc::spdlog_get("CBPort"))
	{
		cblogger->info("------------------");
		cblogger->info(msg);
	}
	else
		std::cout << "Error CBPort Logger not operational";

/*	if (auto odclogger = odc::spdlog_get("opendatacon"))
                          odclogger->info(msg);
            else
                          std::cout << "Error opendatacon Logger not operational";
                          */
}
void TestSetup(const std::string& TestName, bool writeconffiles = true)
{
	#ifndef NONVSTESTING
	SetupLoggers(spdlog::level::debug);
	#endif
	WriteStartLoggingMessage(TestName);

	if (writeconffiles)
		WriteConfFilesToCurrentWorkingDirectory();
}
void TestTearDown(void)
{
	LOGINFO("Test Finished");
	#ifndef NONVSTESTING

	spdlog::drop_all(); // Un-register loggers, and if no other shared_ptr references exist, they will be destroyed.
	#endif
}
// Used for command line test setup
void CommandLineLoggingSetup(spdlog::level::level_enum log_level)
{
	SetupLoggers(log_level);
	WriteStartLoggingMessage("All Tests");
}
void CommandLineLoggingCleanup()
{
	odc::spdlog_flush_all();
	odc::spdlog_drop_all(); // Un-register loggers, and if no other shared_ptr references exist, they will be destroyed.
}
void WaitIOS(odc::asio_service &IOS, int seconds)
{
	auto timer = IOS.make_steady_timer();
	timer->expires_from_now(std::chrono::seconds(seconds));
	timer->wait();
}


// Don't like using macros, but we use the same test set up almost every time.
#define STANDARD_TEST_SETUP()\
	TestSetup(Catch::getResultCapture().getCurrentTestName());\
	auto IOS = odc::asio_service::Get()

// Used for tests that dont need IOS
#define SIMPLE_TEST_SETUP()\
	TestSetup(Catch::getResultCapture().getCurrentTestName())

#define STANDARD_TEST_TEARDOWN()\
	TestTearDown()

#define START_IOS(ThreadCount) \
	LOGINFO("Starting ASIO Threads"); \
	auto work = IOS->make_work(); /* To keep run - running!*/\
	std::vector<std::thread> threads; \
	for (int i = 0; i < (ThreadCount); i++) threads.emplace_back([IOS] { IOS->run(); })

#define STOP_IOS() \
	LOGINFO("Shutting Down ASIO Threads");    \
	work.reset();     \
	for (auto& t : threads) t.join()

#define TEST_CBMAPort(overridejson)\
	auto CBMAPort = std::make_shared<CBMasterPort>("TestMaster", conffilename1, overridejson); \
	CBMAPort->Build()

#define TEST_CBMAPort2(overridejson)\
	auto CBMAPort2 = std::make_shared<CBMasterPort>("TestMaster", conffilename2, overridejson); \
	CBMAPort2->Build()


#define TEST_CBOSPort(overridejson)      \
	auto CBOSPort = std::make_shared<CBOutstationPort>("TestOutStation", conffilename1, overridejson);   \
	CBOSPort->Build()

#define TEST_CBOSPort2(overridejson)     \
	auto CBOSPort2 = std::make_shared<CBOutstationPort>("TestOutStation2", conffilename2, overridejson); \
	CBOSPort2->Build()

#ifdef _MSC_VER
#pragma endregion TEST_HELPERS
#endif

namespace SimpleUnitTestsCB
{
TEST_CASE("Util - HexStringTest")
{
	SIMPLE_TEST_SETUP();
	std::string ts = "c406400f0b00"  "0000fffe9000";
	std::string w1 = { ToChar(0xc4),0x06,0x40,0x0f,0x0b,0x00 };
	std::string w2 = { 0x00,0x00,ToChar(0xff),ToChar(0xfe),ToChar(0x90),0x00 };

	std::string res = BuildBinaryStringFromASCIIHexString(ts);
	REQUIRE(res == (w1 + w2));
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - CBBCHTest")
{
	SIMPLE_TEST_SETUP();
	CBBlockData res = CBBlockData(0x09200028);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x000c0020);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x0009111e);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x00291106);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x22290030);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x22280126);
	REQUIRE(res.BCHPasses());

	res = CBBlockData(0x08080029);
	REQUIRE(res.BCHPasses());
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - ParsePayloadString")
{
	SIMPLE_TEST_SETUP();
	PayloadLocationType payloadlocation;

	bool res = CBPointConf::ParsePayloadString("16B", payloadlocation);
	REQUIRE(res == true);
	REQUIRE(payloadlocation.Packet == 16);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionB);

	res = CBPointConf::ParsePayloadString("1B", payloadlocation);
	REQUIRE(res == true);
	REQUIRE(payloadlocation.Packet == 1);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionB);

	res = CBPointConf::ParsePayloadString("16A", payloadlocation);
	REQUIRE(res == true);
	REQUIRE(payloadlocation.Packet == 16);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionA);
	LOGINFO("Ignore Next Three LOGGGED Errors");

	res = CBPointConf::ParsePayloadString("1A", payloadlocation);
	REQUIRE(res == false);
	REQUIRE(payloadlocation.Packet == 1);
	REQUIRE(payloadlocation.Position == PayloadABType::PositionA);

	res = CBPointConf::ParsePayloadString("0A", payloadlocation);
	REQUIRE(res == false);
	REQUIRE(payloadlocation.Position == PayloadABType::Error);

	res = CBPointConf::ParsePayloadString("123", payloadlocation);
	REQUIRE(res == false);
	REQUIRE(payloadlocation.Position == PayloadABType::Error);
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - MakeChannelID")
{
	SIMPLE_TEST_SETUP();
	std::string IP = "192.168.1.23";
	std::string Port = "34567";
	bool isaserver = true;

	std::string res = CBConnection::MakeChannelID(IP, Port, isaserver);
	REQUIRE(res == "192.168.1.23:34567:1");

	isaserver = false;
	IP = "127.0.0.1";
	Port = "10000";
	res = CBConnection::MakeChannelID(IP, Port, isaserver);
	REQUIRE(res == "127.0.0.1:10000:0");
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - CBIndexTest")
{
	SIMPLE_TEST_SETUP();
	uint8_t group = 1;
	uint8_t channel = 1;
	PayloadLocationType payloadlocation(1, PayloadABType::PositionA);

	// Top 4 bits group (0-15), Next 4 bits channel (1-12), next 4 bits payload packet number (0-15), next 4 bits 0(A) or 1(B)
	uint16_t CBIndex = CBPointTableAccess::GetCBPointMapIndex(group, channel, payloadlocation);
	REQUIRE(CBIndex == 0x1100);

	group = 15;
	channel = 12;
	payloadlocation = PayloadLocationType(16, PayloadABType::PositionB);
	CBIndex = CBPointTableAccess::GetCBPointMapIndex(group, channel, payloadlocation);
	REQUIRE(CBIndex == 0xFCF1);
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("Util - CBPort::BuildUpdateTimeMessage")
{
	SIMPLE_TEST_SETUP();
	uint8_t address = 1;
	auto cbtime = static_cast<CBTime>(0x0000016338b6d4fb); // A value around June 2018

	CBMessage_t CompleteCBMessage;

	CBMasterPort::BuildUpdateTimeMessage(address, cbtime, CompleteCBMessage);

	REQUIRE(CompleteCBMessage.size() == 2);

	uint16_t B0Data = CompleteCBMessage[0].GetB();
	uint16_t A1Data = CompleteCBMessage[1].GetA();
	uint16_t B1Data = CompleteCBMessage[1].GetB();

	REQUIRE(B0Data == 0x0020);
	REQUIRE(A1Data == 0x0f04);
	REQUIRE(B1Data == 0x00fb);
	REQUIRE(CompleteCBMessage[0].IsEndOfMessageBlock() == false);
	REQUIRE(CompleteCBMessage[1].IsEndOfMessageBlock() == true);

	uint8_t hhin;
	uint8_t mmin;
	uint8_t ssin;
	uint16_t msecin;

	DecodeTimePayload(B0Data, A1Data, B1Data, hhin, mmin, ssin, msecin);

	uint64_t rxdmsec = (((uint64_t)hhin * 60 + (uint64_t)mmin) * 60 + (uint64_t)ssin)*1000 + (uint64_t)msecin;

	// We have to remove (mod) the cbtime by msec in a day to get the expected value.
	cbtime = cbtime % (1000 * 60 * 60 * 24);
	REQUIRE(rxdmsec == cbtime);

	// Decode an actual packet.
	// The Ethernet Epoch Frame time is 1560896915.562135000 seconds
	// The packet data is 1B - 0x022, 2A - 0x1c9, 2B - 0x121
	// Arrival Time : Jun 19, 2019 08 : 28 : 35.562135000 AUS Eastern Standard Time

	DecodeTimePayload(0x022, 0x1c9, 0x121, hhin, mmin, ssin, msecin);
	uint64_t epochtimemsecsfull = 1560896915562;
	uint64_t epochtimemsecs = epochtimemsecsfull % (1000 * 60 * 60 * 24);

	rxdmsec = (((uint64_t)hhin * 60 + (uint64_t)mmin) * 60 + (uint64_t)ssin) * 1000 + (uint64_t)msecin;

	LOGDEBUG("Received Time Set Command, Decoded {}, {} msec, PacketStamp {}, {} msec", to_stringfromhhmmssmsec(hhin, mmin, ssin, msecin), rxdmsec, to_stringfromCBtime(epochtimemsecsfull),epochtimemsecs);

	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("Util - SOEEventFormat")
{
	SIMPLE_TEST_SETUP();
	SOEEventFormat S;

	//auto cbtime = static_cast<CBTime>(0x0000016338b6d4fb); // A value around June 2018 //Not used - what was it going to be used for?

	S.Group = 5;     // 3 bits b101
	S.Number = 0x41; // 7 bits b1000001 - 0x41
	S.ValueBit = true;
	S.QualityBit = false;
	S.TimeFormatBit = true; // Long format

	S.Hour = 0x11;         // 5 bits	b10001 - 0x11
	S.Minute = 0x21;       // 6 bits b10 0001 - 0x21
	S.Second = 0x21;       // 6 bits b10 0001 - 0x21
	S.Millisecond = 0x201; // 10 bits b10 0000 0001 - 0x201
	S.LastEventFlag = false;

	uint64_t res = S.GetFormattedData();

	REQUIRE(res == 0xB06C618601000000);

	// Now test our BitArray handling.
	std::array<bool, MaxSOEBits> BitArray{};
	for (int i = 0; i < 64; i++)
		BitArray[i] = ((res >> (63 - i)) & 0x01) == 0x01;

	uint32_t newstartbit = 0;
	bool Success = false;
	SOEEventFormat SOE(BitArray, 0, 64, newstartbit, 0, Success); // Build a new SOE record from the BitArray

	REQUIRE(newstartbit == 41);
	REQUIRE(SOE.GetResultBitLength() == 41); // Should be long...
	REQUIRE(SOE.GetFormattedData() == 0xB06C618601000000);
	REQUIRE(SOE.Group == 5);
	REQUIRE(SOE.Number == 0x41);
	REQUIRE(SOE.ValueBit == true);
	REQUIRE(SOE.TimeFormatBit == true);
	REQUIRE(SOE.Hour == 0x11);
	REQUIRE(SOE.Minute == 0x21);
	REQUIRE(SOE.Second == 0x21);
	REQUIRE(SOE.Millisecond == 0x201);
	REQUIRE(SOE.LastEventFlag == false);
	REQUIRE(Success);


	S.TimeFormatBit = false; // Short format
	S.LastEventFlag = true;

	res = S.GetFormattedData();

	REQUIRE(res == 0xb064300c00000000);

	// Now test our BitArray handling.
	std::array<bool, MaxSOEBits> BitArray2{};
	for (int i = 0; i < 64; i++)
		BitArray2[i] = ((res >> (63 - i)) & 0x01) == 0x01;

	newstartbit = 0;
	SOEEventFormat SOE2(BitArray2, 0, 64, newstartbit, 0, Success); // Build a new SOE record from the BitArray

	REQUIRE(newstartbit == 30);
	REQUIRE(SOE2.GetResultBitLength() == 41); // Always long
	REQUIRE(SOE2.GetFormattedData() == 0xb068008601800000);
	REQUIRE(SOE2.Group == 5);
	REQUIRE(SOE2.Number == 0x41);
	REQUIRE(SOE2.ValueBit == true);
	REQUIRE(SOE2.TimeFormatBit == true);
	REQUIRE(SOE2.Second == 0x21);
	REQUIRE(SOE2.Millisecond == 0x201);
	REQUIRE(SOE2.LastEventFlag == true);
	REQUIRE(Success);

	uint64_t payload = 0x9945455800000000; // From a packet capture
	// Now test our BitArray handling.
	std::array<bool, MaxSOEBits> BitArray3{};
	for (int i = 0; i < 64; i++)
		BitArray3[i] = ((payload >> (63 - i)) & 0x01) == 0x01;

	newstartbit = 0;
	SOEEventFormat SOE3(BitArray3, 0, 64, newstartbit, 0, Success); // Build a new SOE record from the BitArray

	REQUIRE(SOE3.Group == 4);
	REQUIRE(SOE3.Number == 0x65);

	REQUIRE(Success);

	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("Util - ConfigFileLoadTest")
{
	//This is a test to load the autogenerated config file from the Mosaic database output.
/*	STANDARD_TEST_SETUP();
      auto CBOSPort = std::make_unique<CBOutstationPort>("TestOutStation", "CBAutoGenConfig.conf", Json::nullValue);

      CBOSPort->SetIOS(IOS);
      CBOSPort->Build();

      STANDARD_TEST_TEARDOWN();
      */
}

#ifdef _MSC_VER
#pragma region Block Tests
#endif

TEST_CASE("CBBlock - ClassConstructor1")
{
	SIMPLE_TEST_SETUP();
	CBBlockArray msg = { 0x09,0x20,0x00,0x28}; // From a packet capture - this is the address packet

	CBBlockData b(msg);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor2")
{
	SIMPLE_TEST_SETUP();
	CBBlockData b2("22290030");
	REQUIRE(b2.GetData() == 0x22290030);
	REQUIRE(b2.GetA() == 0x222);
	REQUIRE(b2.GetB() == 0x200);
	REQUIRE(b2.IsEndOfMessageBlock() == false);
	REQUIRE(b2.IsDataBlock());
	REQUIRE(b2.BCHPasses());
	REQUIRE(b2.CheckBBitIsZero());

	CBBlockData b3("08080029");
	REQUIRE(b3.GetData() == 0x08080029);
	REQUIRE(b3.GetA() == 0x080);
	REQUIRE(b3.GetB() == 0x000);
	REQUIRE(b3.IsEndOfMessageBlock() == true);
	REQUIRE(b3.IsDataBlock());
	REQUIRE(b3.BCHPasses());
	REQUIRE(b3.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor3")
{
	SIMPLE_TEST_SETUP();
	uint8_t d[] = { 0x09, 0x20, 0x00,0x28 };
	CBBlockData b(d[0], d[1], d[2], d[3]);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	CBBlockData c(numeric_cast<char>(d[0]), numeric_cast<char>(d[1]), numeric_cast<char>(d[2]), numeric_cast<char>(d[3]));

	REQUIRE(c.GetStationAddress() == 9);
	REQUIRE(c.GetGroup() == 2);
	REQUIRE(c.GetFunctionCode() == 0);
	REQUIRE(c.GetB() == 0);
	REQUIRE(c.IsEndOfMessageBlock() == false);
	REQUIRE(c.IsAddressBlock());
	REQUIRE(c.BCHPasses());
	REQUIRE(c.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor4")
{
	SIMPLE_TEST_SETUP();
	uint16_t AData = 0xF23;
	uint16_t BData = 0x102;
	bool lastblock = false;

	CBBlockData b(AData, BData, lastblock);

	REQUIRE(b.GetA() == AData);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock() == false);
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	AData = 0x023;

	b.SetA(AData);
	REQUIRE(b.GetA() == AData);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock() == false);
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	BData = 0xF01;

	b.SetB(BData);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock() == false);
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}

TEST_CASE("CBBlock - ClassConstructor5")
{
	SIMPLE_TEST_SETUP();
	// Need to test the construction of an address block. What is B set to??
	uint8_t StationAddress = 4;
	uint8_t Group = 2;
	uint8_t FunctionCode = 5;
	uint16_t BData = 0x040;
	bool lastblock = false;

	CBBlockData b(StationAddress, Group, FunctionCode, BData, lastblock);
	REQUIRE(b.GetStationAddress() == StationAddress);
	REQUIRE(b.GetGroup() == Group);
	REQUIRE(b.GetFunctionCode() == FunctionCode);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());

	b.DoBakerConitelSwap(); // Swap Group and Station

	REQUIRE(b.GetStationAddress() == Group);
	REQUIRE(b.GetGroup() == StationAddress);
	REQUIRE(b.GetFunctionCode() == FunctionCode);
	REQUIRE(b.GetB() == BData);
	REQUIRE(b.IsEndOfMessageBlock() == lastblock);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.BCHPasses());
	REQUIRE(b.CheckBBitIsZero());
	STANDARD_TEST_TEARDOWN();
}
TEST_CASE("CBBlock - BlockBuilding")
{
	SIMPLE_TEST_SETUP();

	CBBlockData b;

	// From a packet capture - this is the address packet
	// Test the block building code.
	b.AddByteToBlock(0x09);
	b.AddByteToBlock(0x20);
	b.AddByteToBlock(0x00);
	b.AddByteToBlock(0x28);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.IsValidBlock());

	// Now add some more bytes, check that they block is invalid until we should have a valid block again...
	b.AddByteToBlock(0x09);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x09);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x20);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x00);
	REQUIRE(!b.IsValidBlock());

	b.AddByteToBlock(0x28);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.IsValidBlock());

	b.Clear();
	b.AddByteToBlock(0x09);
	b.AddByteToBlock(0x20);
	b.AddByteToBlock(0x00);
	b.AddByteToBlock(0x28);

	REQUIRE(b.GetStationAddress() == 9);
	REQUIRE(b.GetGroup() == 2);
	REQUIRE(b.GetFunctionCode() == 0);
	REQUIRE(b.GetB() == 0);
	REQUIRE(b.IsEndOfMessageBlock() == false);
	REQUIRE(b.IsAddressBlock());
	REQUIRE(b.IsValidBlock());

	STANDARD_TEST_TEARDOWN();
}


#ifdef _MSC_VER
#pragma endregion Block Tests
#endif
}

namespace StationTests
{
void SendBinaryEvent(std::shared_ptr<CBOutstationPort> &CBOSPort, int ODCIndex, bool val, QualityFlags qual = QualityFlags::ONLINE, msSinceEpoch_t time = msSinceEpoch())
{
	auto event = std::make_shared<EventInfo>(EventType::Binary, ODCIndex, "Testing", qual, time);
	event->SetPayload<EventType::Binary>(std::move(val));

	CommandStatus res = CommandStatus::NOT_AUTHORIZED;
	auto pStatusCallback = std::make_shared<std::function<void(CommandStatus)>>([=, &res](CommandStatus command_stat)
		{
			res = command_stat;
		});

	CBOSPort->Event(event, "TestHarness", pStatusCallback);
	REQUIRE(res == CommandStatus::SUCCESS); // The Get will Wait for the result to be set.
}

TEST_CASE("Station - ScanRequest F0")
{
	// So we send a Scan F0 packet to the Outstation, it responds with the data in the point table.
	// Then we update the data in the point table, scan again and check the data we get back.
	STANDARD_TEST_SETUP();
	TEST_CBOSPort(Json::nullValue);

	START_IOS(2);

	CBOSPort->Enable();

	uint8_t station = 9;
	uint8_t group = 3;
	CBBlockData commandblock(station, group, FUNC_SCAN_DATA, 0, true);
	asio::streambuf write_buffer;
	std::ostream output(&write_buffer);


	CommandStatus res = CommandStatus::NOT_AUTHORIZED;
	auto pStatusCallback = std::make_shared<std::function<void(CommandStatus)>>([=, &res](CommandStatus command_stat)
		{
			res = command_stat;
		});

	// Hook the output function with a lambda
	std::string Response = "Not Set";
	std::atomic_bool done_flag(false);
	CBOSPort->SetSendTCPDataFn([&Response,&done_flag](std::string CBMessage) { Response = std::move(CBMessage); done_flag = true; });

	// Send the commands in as if came from TCP channel
	output << commandblock.ToBinaryString();
	CBOSPort->InjectSimulatedTCPMessage(write_buffer);

	// Check the command is formatted correctly
	std::string DesiredResult = "0937ffaa" // Echoed block plus data 1B
	                            "14080022" // Data 2A and 2B
	                            "00080006"
	                            "00080006"
	                            "00080007";

	while(!done_flag)
		IOS->poll_one();
	done_flag = false;

	// No need to delay to process result, all done in the InjectCommand at call time.
	REQUIRE(BuildASCIIHexStringfromBinaryString(Response) == DesiredResult);


	// Call the Event functions to set the CB table data to what we are expecting to get back.
	// Write to the analog registers that we are going to request the values for.
	for (int ODCIndex = 0; ODCIndex < 3; ODCIndex++)
	{
		auto event = std::make_shared<EventInfo>(EventType::Analog, ODCIndex);
		event->SetPayload<EventType::Analog>(1024 + ODCIndex);

		CBOSPort->Event(event, "TestHarness", pStatusCallback);
		REQUIRE((res == CommandStatus::SUCCESS)); // The Get will Wait for the result to be set.
	}
	for (int ODCIndex = 3; ODCIndex < 5; ODCIndex++)
	{
		auto event = std::make_shared<EventInfo>(EventType::Analog, ODCIndex);
		event->SetPayload<EventType::Analog>(1 + ODCIndex);

		CBOSPort->Event(event, "TestHarness", pStatusCallback);
		REQUIRE((res == CommandStatus::SUCCESS)); // The Get will Wait for the result to be set.
	}

	for (int ODCIndex = 0; ODCIndex < 12; ODCIndex++)
	{
		SendBinaryEvent(CBOSPort, ODCIndex, ((ODCIndex % 2) == 0));
	}

	// MCA,MCB,MCC Set to starting values
	SendBinaryEvent(CBOSPort, 12, true);  //CLOSED // MCA inverted on the wire!!
	SendBinaryEvent(CBOSPort, 13, false); //OPEN
	SendBinaryEvent(CBOSPort, 14, true);  //CLOSED

	WaitIOS(*IOS, 1);

	Response = "Not Set";
	output << commandblock.ToBinaryString();
	CBOSPort->InjectSimulatedTCPMessage(write_buffer); // Scan Data Group 3 - analog values and digitals have now changed. No SOE overflow, SOE Data Available in RSW

	// Should now get different data!
	DesiredResult = "09355516" // Echoed block plus data 1B
	                "04080034" // Data 2A and 2B
	                "400a00b6"
	                "4028000c"
	                "80088297";

	CBMessage_t Msg = BuildCBMessageFromASCIIHexString(DesiredResult);
	assert(Msg[2].GetA() == 1024);	// Checking payload values
	assert(Msg[2].GetB() == 1025);	// Checking payload values
	assert(Msg[3].GetA() == 1026);	// Checking payload values
	assert(Msg[4].GetB() == (4 << 6) + 5);	// Checking payload values	// The require below does the whole check in one go!

	while(!done_flag)
		IOS->poll_one();
	done_flag = false;

	REQUIRE(BuildASCIIHexStringfromBinaryString(Response) == DesiredResult);
{'fraction_non_alphanumeric': 0.1482385650832401, 'fraction_numerical': 0.04323059709083511, 'mean_word_length': 4.699227053140096, 'pattern_counts': {'":': 48, '<': 139, '<?xml version=': 0, '>': 163, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 12, 'substrings_counts': 4, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7414550', 'n_tokens_mistral': 23641, 'n_tokens_neox': 21794, 'n_words': 5296}