

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  This file is part of CMSDK version 2.1.1.260726 (26 July 2026)
//_/_/  
//_/_/  developed by
//_/_/  
//_/_/  Communicative Machines Ltd. 
//_/_/    http://cmlabs.com/
//_/_/  Copyright (c) 2026
//_/_/  
//_/_/  in collaboration with 
//_/_/  
//_/_/  Icelandic Institute for Intelligent Machines ses
//_/_/    Menntavegur 1
//_/_/    101 Reykjavik
//_/_/    ICELAND
//_/_/    http://www.iiim.is/
//_/_/
//_/_/  and is released under the following
//_/_/  
//_/_/  EXTENDED BSD License
//_/_/
//_/_/  Redistribution and use in source and binary forms, with or without
//_/_/  modification, is permitted provided that the following conditions 
//_/_/  are met:
//_/_/
//_/_/  - Redistributions of source code must retain the above copyright 
//_/_/    and collaboration notice, this list of conditions and the following 
//_/_/    disclaimer.
//_/_/
//_/_/  - Redistributions in binary form must reproduce the above copyright 
//_/_/    notice, this list of conditions and the following disclaimer in the 
//_/_/    documentation and/or other materials provided with the distribution.
//_/_/
//_/_/  - Neither the name of its copyright holders nor the names of its 
//_/_/    contributors may be used to endorse or promote products derived from 
//_/_/    this software without specific prior written permission.
//_/_/
//_/_/  - CADIA Clause: The license granted in and to the software under this 
//_/_/    agreement is a limited-use license. The software may not be used in 
//_/_/    furtherance of: (i) intentionally causing bodily injury or severe 
//_/_/    emotional harm to any person; (ii) invading the personal privacy or 
//_/_/    violating the human rights of any person; or (iii) committing or 
//_/_/    preparing for any act of war. 
//_/_/
//_/_/  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
//_/_/  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
//_/_/  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
//_/_/  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 
//_/_/  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
//_/_/  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
//_/_/  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
//_/_/  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
//_/_/  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
//_/_/  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
//_/_/  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//_/_/
//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
/*! @file UnitTestFramework.h
    @brief Small, dependency-free unit test harness used by all CMSDK object tests.

    A self-contained (no external libraries) harness that gives every CMSDK
    object's unit test consistent output, performance recording and
    comparison against previous runs.

    A test is any function `bool Func()` that:
    - calls unittest::progress(pct, action) to drive the live progress line
    - calls unittest::metric(name, value, unit) to record/print performance
    - calls unittest::detail(fmt, ...) for verbose-only diagnostics
    - returns true on success, false on failure (optionally unittest::fail()
      first to set a reason)

    Tests are registered with the runner (see RegisterAllCMSDKTests in
    UnitTest_CMSDK.cpp) and driven by Psyclone:
    @code
    Psyclone test=cmsdk                     run all tests
    Psyclone test=<objectname>              run a single test in isolation
    Psyclone test=cmsdk compare=<oldfile>   show deltas vs a previous run
    Psyclone test=cmsdk verbose=1           show verbose diagnostics
    Psyclone test=cmsdk out=<file>          override perf JSON output path
    @endcode

    @defgroup UnitTest Unit Test Framework
    @brief Registration, execution, progress/metric reporting and perf comparison for CMSDK tests.

    @par Why a built-in framework
    CMSDK is the bottom layer of the stack, built on every platform Psyclone
    supports, so its test harness must be dependency-free (no gtest/catch2 to
    vendor or port) and able to exercise things external frameworks handle
    poorly: tests that fork real processes, attach shared-memory segments or
    open sockets, and that may crash or hang without taking down the suite
    (the runner can execute each test in a forked child). It also records
    per-test performance metrics to JSON and diffs them against a previous
    run, turning the unit tests into a lightweight regression benchmark.

    @par Writing a test
    A test is any free function `bool Func()` that reports progress and
    metrics through the cmlabs::unittest namespace and returns true/false:
    @code
    bool DataMessage_UnitTest() {
        unittest::progress(10, "building messages");
        uint64 t0 = GetTimeNow();
        for (int i = 0; i < 100000; i++) {
            DataMessage msg;
            msg.setInt("i", i);
            if (msg.getInt("i") != i)
                { unittest::fail("roundtrip mismatch at %d", i); return false; }
        }
        unittest::progress(90, "recording metrics");
        double secs = GetTimeAge(t0) / 1000000.0;
        unittest::metric("set/get roundtrips", 100000.0 / secs, "msg/s");
        unittest::detail("only shown with verbose=1");
        return true;
    }
    @endcode

    @par Registering and running
    Register the function with the singleton runner (all CMSDK tests are
    registered in RegisterAllCMSDKTests() in UnitTest_CMSDK.cpp):
    @code
    UnitTestRunner::instance().registerTest("datamessage", DataMessage_UnitTest,
                                            "DataMessage set/get roundtrip", "core");
    @endcode
    and drive it from the Psyclone executable:
    @code
    Psyclone test=cmsdk                     run all tests
    Psyclone test=<objectname>              run a single test in isolation
    Psyclone test=cmsdk compare=<oldfile>   show perf deltas vs a previous run
    Psyclone test=cmsdk verbose=1           show verbose diagnostics
    Psyclone test=cmsdk out=<file>          override perf JSON output path
    @endcode

    @note unittest::progress() draws a single overwriting line on a terminal
    and is suppressed when output is piped, so CI logs stay clean.
    @see UnitTestRunner, UnitTestRecord, cmlabs::unittest
*/
#if !defined(_UNITTESTFRAMEWORK_H_)
#define _UNITTESTFRAMEWORK_H_

#include "Standard.h"
#include "Types.h"
#include "Utils.h"

#include <string>
#include <vector>
#include <map>

namespace cmlabs {

/*! @brief Signature every unit test uses.
    @ingroup UnitTest
    @return true on success, false on failure. */
typedef bool (*UnitTestFunc)();

/*! @namespace cmlabs::unittest
    @brief API used by the body of a unit test.
    @ingroup UnitTest
    These free functions operate on the test the runner is currently
    executing (dispatched via UnitTestRunner's hook* methods). */
namespace unittest {

	/*! @brief Report progress with a short description of the current action.
	    @param percent Progress 0..100.
	    @param action Short description of the current phase.
	    On a terminal this redraws a single overwriting line; when output is not
	    a terminal (piped/redirected) it is suppressed so logs stay clean. */
	void progress(int percent, const char* action);
	//! printf-style variant of progress(). @see progress()
	void progressf(int percent, const char* fmt, ...);

	/*! @brief Record a performance metric.
	    @param name Metric name (unique within the test).
	    @param value Measured value.
	    @param unit Unit label, e.g. "msg/s" or "us".
	    @param higherIsBetter Whether a larger value is an improvement (e.g.
	           throughput) or a smaller one is (e.g. latency) - used only to
	           label the delta direction in compare mode.
	    Printed as an indented subline under the test's PASS line and written to
	    the performance JSON file. In compare mode the previous value and the
	    %-change are appended. */
	void metric(const char* name, double value, const char* unit = "", bool higherIsBetter = true);

	//! Verbose-only indented diagnostic line (shown only when verbose=1).
	void detail(const char* fmt, ...);

	/*! @brief Set an explanatory reason shown on the FAIL line.
	    Optional - a test may simply return false without a reason. */
	void fail(const char* fmt, ...);

	//! @return true when verbose diagnostics are enabled.
	bool verbose();

} // namespace unittest

/*! @brief One recorded performance metric of a test run.
    @ingroup UnitTest */
struct UnitTestMetric {
	std::string name;			//!< Metric name (unique within the test)
	double      value;			//!< Measured value
	std::string unit;			//!< Unit label (e.g. "msg/s", "us")
	bool        higherIsBetter;	//!< Delta-direction hint for compare mode
};

/*! @brief Registration and result record for a single unit test.
    @ingroup UnitTest */
struct UnitTestRecord {
	std::string  name;          //!< Canonical name, used by test=&lt;name&gt;
	std::string  description;   //!< One-line human description
	std::string  category;      //!< Grouping label (e.g. "core")
	UnitTestFunc func;          //!< The test function itself
	bool         inDefaultRun;  //!< Included when running the whole suite (test=cmsdk)

	// Filled in at run time
	bool         ran;           //!< True once the runner attempted this test
	bool         passed;        //!< Outcome (only meaningful when @c ran)
	uint64       durationUs;    //!< Wall-clock duration in microseconds
	std::string  failReason;    //!< Reason set by the test via unittest::fail() (printed immediately)
	std::string  endNote;       //!< Framework outcome note (timed out / crashed / no test function)
	std::vector<UnitTestMetric> metrics; //!< Metrics recorded via unittest::metric()

	//! Default: unregistered, not run, in the default suite.
	UnitTestRecord() : func(NULL), inDefaultRun(true), ran(false), passed(false), durationUs(0) {}
};

/*! @brief The test runner (process-wide singleton).
    @ingroup UnitTest

    Holds the registry of tests, executes them (optionally each in a forked
    child process so a crash or hang cannot take down the suite), renders the
    console output, writes the perf JSON and computes compare-mode deltas.
    @note Thread-safe for the hook* callbacks via an internal mutex;
          registration and execution are expected from a single thread. */
class UnitTestRunner {
public:
	//! Access the singleton (created on first use).
	static UnitTestRunner& instance();

	/*! @brief Register a test with the runner.
	    @param name Canonical test name (used by test=&lt;name&gt;); duplicates are ignored.
	    @param func The test function.
	    @param description One-line human description.
	    @param category Grouping label shown in listings.
	    @param inDefaultRun Include in the full-suite run (test=cmsdk). */
	void registerTest(const char* name, UnitTestFunc func,
	                   const char* description = "", const char* category = "",
	                   bool inDefaultRun = true);

	//! Enable/disable verbose diagnostics (unittest::detail output).
	void setVerbose(bool v);
	//! Set a previous perf JSON file to compare metrics against.
	void setCompareFile(const char* path);
	//! Set an explicit perf JSON output path (overrides the timestamped default).
	void setOutputFile(const char* path);
	//! Set the directory for the default (timestamped + latest) output files.
	void setOutputDir(const char* dir);
	//! Enable/disable writing the perf JSON file at all.
	void setWriteJSON(bool v);
	//! Run each test in its own process (default: on where supported) so crashes/hangs are isolated.
	void setForkPerTest(bool v);

	//! @return true if @a name is a registered test.
	bool hasTest(const char* name) const;
	//! @return the names of all registered tests.
	std::vector<std::string> testNames() const;
	//! Print the registered tests (name, category, description) to stdout.
	void listTests() const;

	/*! @brief Run every test marked @c inDefaultRun.
	    @return true only if every test that ran passed. */
	bool runAll();
	/*! @brief Run a single named test in isolation.
	    @return true only if it passed. */
	bool runOne(const char* name);

	//! Hook behind unittest::progress()/progressf() for the current test.
	void hookProgress(int percent, const char* action);
	//! Hook behind unittest::metric() for the current test.
	void hookMetric(const char* name, double value, const char* unit, bool higherIsBetter);
	//! Hook behind unittest::detail() for the current test.
	void hookDetail(const char* text);
	//! Hook behind unittest::fail() for the current test.
	void hookFail(const char* text);
	//! Hook behind unittest::verbose().
	bool hookVerbose() const;

private:
	UnitTestRunner();

	//! Execute a list of tests and print the summary; true if all passed.
	bool runRecords(std::vector<UnitTestRecord*>& records);
	//! Execute one test (in-process or forked, per @c forkPerTest).
	bool executeRecord(UnitTestRecord* rec);
	//! Set up per-test state and print the initial status line.
	void beginTest(UnitTestRecord* rec);
	//! Record duration/outcome and print PASS/FAIL + metric lines.
	void finishTest(UnitTestRecord* rec);
	//! Erase the transient in-place progress line from the terminal.
	void clearProgressLine();
	//! Print the PASS/FAIL status line for a finished test.
	void printStatusLine(const UnitTestRecord* rec);
	//! Print the indented metric sublines (with compare deltas when loaded).
	void printMetricLines(const UnitTestRecord* rec);
	//! Write the perf JSON result file(s) for a completed run.
	void writeResults(const std::vector<UnitTestRecord*>& records,
	                  uint64 totalUs, int passed, int failed);
	//! Lazily parse the compare file into @c compareMetrics.
	void loadComparison();
	//! Look up a "test/metric" key in the loaded comparison data.
	bool comparisonValue(const std::string& key, double& out) const;
	//! Format a timestamp for use in the default output filename.
	static std::string timestampForFilename(uint64 t);

	std::vector<UnitTestRecord> tests;	//!< Registry of all registered tests
	UnitTestRecord* current;			//!< Test currently executing (target of the hook* calls)

	bool   verboseFlag;			//!< Verbose diagnostics enabled
	bool   writeJSONFlag;		//!< Write perf JSON output
	bool   forkPerTest;			//!< Isolate each test in its own process
	bool   isTTY;				//!< stdout is a terminal (enables in-place progress line)
	int    lastProgressLen;		//!< Length of the last progress line (for clean overwrite)
	std::string progressPath;   //!< Forked child writes its current phase here so a hung
	                            //!< test can be diagnosed by the parent on timeout

	std::string compareFile;	//!< Path of the previous perf JSON to compare against
	std::string outputFile;		//!< Explicit output path (empty = timestamped default)
	std::string outputDir;		//!< Directory for default output files

	bool compareLoaded;			//!< Comparison file has been parsed
	std::map<std::string, double> compareMetrics; //!< "test/metric" -> previous value

	utils::Mutex lock;			//!< Protects hook dispatch and shared runner state
};

} // namespace cmlabs

#endif // _UNITTESTFRAMEWORK_H_
