OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / tests / atoms / value / SortedValueUTest.cxxtest

407 lines · cxxtest ref:
/*
 * tests/atoms/value/SortedValueUTest.cxxtest
 *
 * Copyright (C) 2025 BrainyBlaze Dynamics, Inc.
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * 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 Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <opencog/atoms/value/SortedValue.h>
#include <opencog/atoms/value/QueueValue.h>
#include <opencog/guile/SchemeEval.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/util/Logger.h>

#include <cxxtest/TestSuite.h>
#include <vector>
#include <thread>
#include <chrono>

using namespace opencog;

class SortedValueUTest : public CxxTest::TestSuite
{
private:
	AtomSpacePtr _asp;

public:
	SortedValueUTest(void);

	void setUp(void);
	void test_basic_sorted(void);
	void test_bare_greater_than(void);
	void test_deduplication(void);
	void test_queue_source(void);
};

SortedValueUTest::SortedValueUTest(void)
	: _asp(createAtomSpace())
{
	logger().set_level(Logger::DEBUG);
	logger().set_print_to_stdout_flag(true);
	logger().set_sync_flag(true);
	logger().set_timestamp_flag(false);
}

void SortedValueUTest::setUp(void)
{
}

// =========================================================

void SortedValueUTest::test_basic_sorted(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	SchemeEval* eval = SchemeEval::get_scheme_evaluator(_asp);

	// Perform numerical compare. The default sort order that's
	// built into Handles will sort this smallest first; so use
	// GreaterThanLink to swap the order.
	ValuePtr sorted_val = eval->eval_v(R"(
		(SortedValue
			(Lambda
				(VariableList (Variable "$A") (Variable "$B"))
				(GreaterThan (Variable "$A") (Variable "$B"))))
	)");

	SortedValuePtr svp = SortedValueCast(sorted_val);
	TS_ASSERT(svp != nullptr);

	// Add numbers in unsorted order: 3, 2, 4
	Handle num3 = eval->eval_h("(Number 3 3 3)");
	Handle num2 = eval->eval_h("(Number 2 2)");
	Handle num4 = eval->eval_h("(Number 4 4 4 4)");

	svp->add(num3);
	svp->add(num2);
	svp->add(num4);

	// Close the SortedValue
	svp->close();

	// Remove items one at a time using the new streaming API
	ValuePtr item1 = svp->remove();
	ValuePtr item2 = svp->remove();
	ValuePtr item3 = svp->remove();
	ValuePtr end_marker = svp->remove();

	// Verify we got 3 items plus VoidValue end marker
	TS_ASSERT(item1 != nullptr);
	TS_ASSERT(item2 != nullptr);
	TS_ASSERT(item3 != nullptr);
	TS_ASSERT(end_marker->is_type(VOID_VALUE));

	// Verify sorted order is 4, 3, 2 (descending)
	TS_ASSERT_EQUALS(item1, num4);
	TS_ASSERT_EQUALS(item2, num3);
	TS_ASSERT_EQUALS(item3, num2);

	// Print the sorted contents
	printf("SortedValue contents (3 items):\n");
	printf("  [0]: %s\n", item1->to_string().c_str());
	printf("  [1]: %s\n", item2->to_string().c_str());
	printf("  [2]: %s\n", item3->to_string().c_str());

	logger().debug("END TEST: %s", __FUNCTION__);
}

// =========================================================

void SortedValueUTest::test_bare_greater_than(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	SchemeEval* eval = SchemeEval::get_scheme_evaluator(_asp);

	// Use bare GreaterThan without Lambda wrapper
	ValuePtr sorted_val = eval->eval_v(R"(
		(SortedValue (GreaterThan))
	)");

	SortedValuePtr svp = SortedValueCast(sorted_val);
	TS_ASSERT(svp != nullptr);

	// Add numbers in unsorted order: 3, 2, 4
	Handle num3 = eval->eval_h("(Number 3 3 3)");
	Handle num2 = eval->eval_h("(Number 2 2)");
	Handle num4 = eval->eval_h("(Number 4 4 4 4)");

	svp->add(num3);
	svp->add(num2);
	svp->add(num4);

	// Close the SortedValue
	svp->close();

	// Remove items one at a time using the new streaming API
	ValuePtr item1 = svp->remove();
	ValuePtr item2 = svp->remove();
	ValuePtr item3 = svp->remove();
	ValuePtr end_marker = svp->remove();

	// Verify we got 3 items plus VoidValue end marker
	TS_ASSERT(item1 != nullptr);
	TS_ASSERT(item2 != nullptr);
	TS_ASSERT(item3 != nullptr);
	TS_ASSERT(end_marker->is_type(VOID_VALUE));

	// Verify sorted order is 4, 3, 2 (descending)
	TS_ASSERT_EQUALS(item1, num4);
	TS_ASSERT_EQUALS(item2, num3);
	TS_ASSERT_EQUALS(item3, num2);

	// Print the sorted contents
	printf("SortedValue contents (3 items):\n");
	printf("  [0]: %s\n", item1->to_string().c_str());
	printf("  [1]: %s\n", item2->to_string().c_str());
	printf("  [2]: %s\n", item3->to_string().c_str());

	logger().debug("END TEST: %s", __FUNCTION__);
}

// =========================================================

void SortedValueUTest::test_deduplication(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	SchemeEval* eval = SchemeEval::get_scheme_evaluator(_asp);

	// Use bare GreaterThan for descending sort
	ValuePtr sorted_val = eval->eval_v(R"(
		(SortedValue (GreaterThan))
	)");

	SortedValuePtr svp = SortedValueCast(sorted_val);
	TS_ASSERT(svp != nullptr);

	ValuePtr num2 = eval->eval_v("(FloatValue 2 2)");
	ValuePtr num3 = eval->eval_v("(FloatValue 3 3 3)");
	ValuePtr num4 = eval->eval_v("(FloatValue 4 4 4 4)");
	ValuePtr num5 = eval->eval_v("(FloatValue 5 5 5 5 5)");
	ValuePtr num6 = eval->eval_v("(FloatValue 6)");
	ValuePtr voiv = eval->eval_v("(VoidValue)");

	// Add 12 items in "random" order.
	svp->add(num2);
	svp->add(num6);
	svp->add(num3);
	svp->add(num5);
	svp->add(num4);
	svp->add(num5); // duplicate
	svp->add(num5); // duplicate
	svp->add(num3); // duplicate
	svp->add(num2); // duplicate
	svp->add(num6); // duplicate
	svp->add(num4); // duplicate
	svp->add(num5); // duplicate
	svp->add(voiv); // closes stream

	// Remove items one at a time using the new streaming API
	// Collect all items until we get VoidValue
	std::vector<ValuePtr> contents;
	while (true)
	{
		ValuePtr item = svp->remove();
		if (item->is_type(VOID_VALUE))
			break;
		contents.push_back(item);
	}

	// Deduplication works just like UnisetValue:
	// should have only 5 unique items
	TS_ASSERT_EQUALS(contents.size(), 5);

	// Verify sorted order is 6, 5, 4, 3, 2 (descending)
	TS_ASSERT_EQUALS(contents[0], num6);
	TS_ASSERT_EQUALS(contents[1], num5);
	TS_ASSERT_EQUALS(contents[2], num4);
	TS_ASSERT_EQUALS(contents[3], num3);
	TS_ASSERT_EQUALS(contents[4], num2);

	printf("SortedValue contents (%zu items, should be 5):\n", contents.size());
	for (size_t i = 0; i < contents.size(); i++)
		printf("  [%zu]: %s\n", i, contents[i]->to_string().c_str());

	logger().debug("END TEST: %s", __FUNCTION__);
}

// =========================================================

void SortedValueUTest::test_queue_source(void)
{
	logger().debug("BEGIN TEST: %s", __FUNCTION__);

	SchemeEval* eval = SchemeEval::get_scheme_evaluator(_asp);

	// Create the greater-or-equal comparison relation
	// This is the same as in sorting-test.scm
	Handle greater_or_equal = eval->eval_h(R"(
		(Lambda
			(VariableList (Variable "$left") (Variable "$right"))
			(Or
				(GreaterThan
					(SizeOf (Variable "$left"))
					(SizeOf (Variable "$right")))
				(Equal
					(SizeOf (Variable "$left"))
					(SizeOf (Variable "$right")))))
	)");

	// Create a QueueValue as the data source
	QueueValuePtr queue = createQueueValue();
	queue->open();

	// Attach the queue to an anchor so we can pass it to SortedValue
	Handle queue_anchor = eval->eval_h("(Anchor \"queue-source\")");
	Handle queue_key = eval->eval_h("(Predicate \"queue-data\")");
	queue_anchor->setValue(queue_key, queue);

	// Provide accessor to the Queue
	Handle queue_source = eval->eval_h(R"(
		(ValueOf (Anchor "queue-source") (Predicate "queue-data"))
	)");

	// Create the SortedValue with the queue source
	HandleSeq args{greater_or_equal, queue_source};
	ValuePtr sorter = createSortedValue(args);
	SortedValuePtr sorted = SortedValueCast(sorter);
	TS_ASSERT(sorted != nullptr);

	// Create the items to add (same as in sorting-test.scm)
	Handle item_a = eval->eval_h("(Item \"a\")");
	Handle item_b = eval->eval_h("(Item \"b\")");
	Handle edge1 = eval->eval_h(R"(
		(Edge
			(Predicate "relation")
			(List (Item "c") (Item "d")))
	)");
	Handle item_e = eval->eval_h("(Item \"e\")");
	Handle item_f = eval->eval_h("(Item \"f\")");
	Handle link3 = eval->eval_h(R"(
		(Link
			(Item "p")
			(Predicate "q")
			(TagNode "z"))
	)");
	Handle edge2 = eval->eval_h(R"(
		(Edge
			(Predicate "relation")
			(List (Item "g") (Item "h")))
	)");

	// Add items one at a time with 500ms sleep between each
	printf("Adding items to queue with delays...\n");

	queue->add(item_a);
	printf("  Added item_a (size 1)\n");
	std::this_thread::sleep_for(std::chrono::milliseconds(500));

	queue->add(item_b);
	printf("  Added item_b (size 1)\n");
	std::this_thread::sleep_for(std::chrono::milliseconds(500));

	queue->add(edge1);
	printf("  Added edge1 (size 2)\n");
	std::this_thread::sleep_for(std::chrono::milliseconds(500));

	queue->add(item_e);
	printf("  Added item_e (size 1)\n");
	std::this_thread::sleep_for(std::chrono::milliseconds(500));

	queue->add(item_f);
	printf("  Added item_f (size 1)\n");
	std::this_thread::sleep_for(std::chrono::milliseconds(500));

	queue->add(link3);
	printf("  Added link3 (size 3)\n");
	std::this_thread::sleep_for(std::chrono::milliseconds(500));

	queue->add(edge2);
	printf("  Added edge2 (size 2)\n");
	std::this_thread::sleep_for(std::chrono::milliseconds(500));

	// Close the queue after the last add
	queue->close();
	printf("Queue closed\n");

	// Now retrieve items from the SortedValue and verify they're in sorted order
	// Expected order: size 3, then 2, 2, then 1, 1, 1, 1 (descending)
	printf("Retrieving items from SortedValue...\n");

	ValuePtr item1 = sorted->remove();
	TS_ASSERT(item1 != nullptr);
	TS_ASSERT_EQUALS(item1, link3);
	printf("  Item 1: size 3 (Link) - CORRECT\n");

	ValuePtr item2 = sorted->remove();
	TS_ASSERT(item2 != nullptr);
	// Could be either edge1 or edge2 (both size 2)
	TS_ASSERT(item2 == edge1 || item2 == edge2);
	printf("  Item 2: size 2 (Edge) - CORRECT\n");

	ValuePtr item3 = sorted->remove();
	TS_ASSERT(item3 != nullptr);
	// The other edge (size 2)
	TS_ASSERT(item3 == edge1 || item3 == edge2);
	TS_ASSERT(item3 != item2);  // Should be different from item2
	printf("  Item 3: size 2 (Edge) - CORRECT\n");

	// Next 4 items should all be size 1
	ValuePtr item4 = sorted->remove();
	TS_ASSERT(item4 != nullptr);
	TS_ASSERT(item4 == item_a || item4 == item_b ||
	          item4 == item_e || item4 == item_f);
	printf("  Item 4: size 1 - CORRECT\n");

	ValuePtr item5 = sorted->remove();
	TS_ASSERT(item5 != nullptr);
	TS_ASSERT(item5 == item_a || item5 == item_b ||
	          item5 == item_e || item5 == item_f);
	printf("  Item 5: size 1 - CORRECT\n");

	ValuePtr item6 = sorted->remove();
	TS_ASSERT(item6 != nullptr);
	TS_ASSERT(item6 == item_a || item6 == item_b ||
	          item6 == item_e || item6 == item_f);
	printf("  Item 6: size 1 - CORRECT\n");

	ValuePtr item7 = sorted->remove();
	TS_ASSERT(item7 != nullptr);
	TS_ASSERT(item7 == item_a || item7 == item_b ||
	          item7 == item_e || item7 == item_f);
	printf("  Item 7: size 1 - CORRECT\n");

	// Next remove should return VoidValue (end of stream)
	ValuePtr end_marker = sorted->remove();
	TS_ASSERT(end_marker != nullptr);
	TS_ASSERT(end_marker->is_type(VOID_VALUE));
	printf("  End of stream marker received - CORRECT\n");

	// Test proper destruction by setting to nullptr
	printf("Testing destructor...\n");
	sorter = nullptr;
	printf("Destructor called successfully\n");

	logger().debug("END TEST: %s", __FUNCTION__);
}

// ======================THE END============================
// =========================================================