/*
* 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============================
// =========================================================