/*
* JSCommandsUTest.cxxtest
* Test JSON command processor
*
* Copyright (c) 2025 Linas Vepstas
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* 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/util/Logger.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atoms/value/StringValue.h>
#include <opencog/atoms/base/Node.h>
#include <opencog/atoms/base/Link.h>
#include "opencog/persist/json/JSCommands.h"
using namespace opencog;
class JSCommandsUTest : public CxxTest::TestSuite
{
private:
AtomSpacePtr as;
public:
JSCommandsUTest()
{
logger().set_print_to_stdout_flag(true);
as = createAtomSpace();
}
void setUp() { as->clear(); }
void tearDown() {}
// Helper to extract and unescape text from MCP content format
std::string extractText(const std::string& mcpResponse) {
// Extract text from {"content": [{"type":"text", "text": "..."}]}
size_t textPos = mcpResponse.find("\"text\": \"");
if (textPos == std::string::npos) return "";
textPos += 9; // strlen("\"text\": \"")
// Find closing quote+brace (may have trailing space)
size_t endPos = mcpResponse.find("\"}", textPos);
if (endPos == std::string::npos) {
// Try with space before closing brace
endPos = mcpResponse.find(" \"}", textPos);
}
if (endPos == std::string::npos) return "";
std::string escaped = mcpResponse.substr(textPos, endPos - textPos);
// Unescape JSON string: \" -> ", \\ -> \, etc.
std::string unescaped;
for (size_t i = 0; i < escaped.length(); i++) {
if (escaped[i] == '\\' && i + 1 < escaped.length()) {
char next = escaped[i + 1];
if (next == '"' || next == '\\' || next == '/') {
unescaped += next;
i++; // Skip the next char
} else if (next == 'n') {
unescaped += '\n';
i++;
} else if (next == 't') {
unescaped += '\t';
i++;
} else {
unescaped += escaped[i];
}
} else {
unescaped += escaped[i];
}
}
return unescaped;
}
// Test type query functions
void test_getSubTypes();
void test_getSuperTypes();
void test_getAtoms();
// Test atom query functions
void test_haveNode();
void test_haveLink();
void test_haveAtom();
// Test atom creation functions
void test_makeAtom();
// Test atom property functions
void test_getIncoming();
void test_getValuesSetValue();
// Test other functions
void test_execute();
void test_extract();
void test_version();
};
// Test getSubTypes with both formats
void JSCommandsUTest::test_getSubTypes()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Test original format
std::string cmd = "AtomSpace.getSubTypes(\"Link\")";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should return array of Link subtypes
std::string text = extractText(result);
TS_ASSERT(text.find("[") != std::string::npos);
TS_ASSERT(text.find("\"OrderedLink\"") != std::string::npos);
// Test JSON object format
cmd = "AtomSpace.getSubTypes({\"type\": \"Link\"})";
std::string result2 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result2.c_str());
// Results should be identical
TS_ASSERT_EQUALS(result, result2);
// Test with recursive flag - original format
cmd = "AtomSpace.getSubTypes(\"Link\", true)";
std::string result3 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result3.c_str());
// Recursive should return more types
TS_ASSERT(result3.length() >= result.length());
// Test JSON object format with recursive
// NOTE: The "recursive" field in JSON object format is not yet implemented
// This test documents the expected behavior once implemented
cmd = "AtomSpace.getSubTypes({\"type\": \"Link\", \"recursive\": true})";
std::string result4 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result4.c_str());
// TODO: Fix JSON parser to handle recursive field
// For now, this returns non-recursive results
// TS_ASSERT_EQUALS(result3, result4);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test getSuperTypes with both formats
void JSCommandsUTest::test_getSuperTypes()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Test original format
std::string cmd = "AtomSpace.getSuperTypes(\"ListLink\")";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should return array containing parent types
std::string text = extractText(result);
TS_ASSERT(text.find("[") != std::string::npos);
// ListLink's direct parent is OrderedLink, not UnorderedLink
TS_ASSERT(text.find("\"OrderedLink\"") != std::string::npos);
// Test JSON object format
cmd = "AtomSpace.getSuperTypes({\"type\": \"ListLink\"})";
std::string result2 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result2.c_str());
// Results should be identical
TS_ASSERT_EQUALS(result, result2);
// Test with recursive flag - original format
cmd = "AtomSpace.getSuperTypes(\"ListLink\", true)";
std::string result3 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result3.c_str());
// Recursive should return more parent types
TS_ASSERT(result3.length() >= result.length());
// Test JSON object format with recursive
// NOTE: The "recursive" field in JSON object format is not yet implemented
// This test documents the expected behavior once implemented
cmd = "AtomSpace.getSuperTypes({\"type\": \"ListLink\", \"recursive\": true})";
std::string result4 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result4.c_str());
// TODO: Fix JSON parser to handle recursive field
// For now, this returns non-recursive results
// TS_ASSERT_EQUALS(result3, result4);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test getAtoms with both formats
void JSCommandsUTest::test_getAtoms()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Add some test atoms
as->add_node(CONCEPT_NODE, "test1");
as->add_node(CONCEPT_NODE, "test2");
as->add_node(PREDICATE_NODE, "pred1");
// Test original format with quoted type
std::string cmd = "AtomSpace.getAtoms(\"ConceptNode\")";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should return array with 2 concept nodes
std::string text = extractText(result);
TS_ASSERT(text.find("[") != std::string::npos);
TS_ASSERT(text.find("\"test1\"") != std::string::npos);
TS_ASSERT(text.find("\"test2\"") != std::string::npos);
TS_ASSERT(text.find("\"pred1\"") == std::string::npos);
// Test JSON object format
cmd = "AtomSpace.getAtoms({\"type\": \"ConceptNode\"})";
std::string result2 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result2.c_str());
// Results should be identical
TS_ASSERT_EQUALS(result, result2);
// Test with recursive flag to get all Nodes - original format
cmd = "AtomSpace.getAtoms(\"Node\", true)";
std::string result3 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result3.c_str());
// Should include all nodes
std::string text3 = extractText(result3);
TS_ASSERT(text3.find("\"test1\"") != std::string::npos);
TS_ASSERT(text3.find("\"test2\"") != std::string::npos);
TS_ASSERT(text3.find("\"pred1\"") != std::string::npos);
// Test JSON object format with subclass
// NOTE: The "subclass" field in JSON object format is not yet implemented
// This test documents the expected behavior once implemented
cmd = "AtomSpace.getAtoms({\"type\": \"Node\", \"subclass\": true})";
std::string result4 = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result4.c_str());
// TODO: Fix JSON parser to handle subclass field
// For now, this returns only exact type matches
// TS_ASSERT_EQUALS(result3, result4);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test haveNode with both formats
void JSCommandsUTest::test_haveNode()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Test when node doesn't exist - original format
std::string cmd = "AtomSpace.haveNode(\"ConceptNode\", \"foo\")";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"false\"}]}\n");
// Test when node doesn't exist - JSON object format
cmd = "AtomSpace.haveNode({\"type\": \"ConceptNode\", \"name\": \"foo\"})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"false\"}]}\n");
// Add the node
as->add_node(CONCEPT_NODE, "foo");
// Test when node exists - original format
cmd = "AtomSpace.haveNode(\"ConceptNode\", \"foo\")";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
// Test when node exists - JSON object format
cmd = "AtomSpace.haveNode({\"type\": \"ConceptNode\", \"name\": \"foo\"})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
logger().info("END TEST: %s", __FUNCTION__);
}
// Test haveLink with both formats
void JSCommandsUTest::test_haveLink()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Add test nodes
Handle h1 = as->add_node(CONCEPT_NODE, "foo");
Handle h2 = as->add_node(CONCEPT_NODE, "bar");
// Test when link doesn't exist - original format
std::string cmd = "AtomSpace.haveLink(\"ListLink\", [{\"type\": \"ConceptNode\", \"name\": \"foo\"}, {\"type\": \"ConceptNode\", \"name\": \"bar\"}])";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"false\"}]}\n");
// Test when link doesn't exist - JSON object format
cmd = "AtomSpace.haveLink({\"type\": \"ListLink\", \"outgoing\": [{\"type\": \"ConceptNode\", \"name\": \"foo\"}, {\"type\": \"ConceptNode\", \"name\": \"bar\"}]})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"false\"}]}\n");
// Add the link
as->add_link(LIST_LINK, h1, h2);
// Test when link exists - original format
cmd = "AtomSpace.haveLink(\"ListLink\", [{\"type\": \"ConceptNode\", \"name\": \"foo\"}, {\"type\": \"ConceptNode\", \"name\": \"bar\"}])";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
// Test when link exists - JSON object format
cmd = "AtomSpace.haveLink({\"type\": \"ListLink\", \"outgoing\": [{\"type\": \"ConceptNode\", \"name\": \"foo\"}, {\"type\": \"ConceptNode\", \"name\": \"bar\"}]})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
logger().info("END TEST: %s", __FUNCTION__);
}
// Test haveAtom
void JSCommandsUTest::test_haveAtom()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Test when atom doesn't exist
std::string cmd = "AtomSpace.haveAtom({\"type\": \"ConceptNode\", \"name\": \"test\"})";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"false\"}]}\n");
// Add the atom
as->add_node(CONCEPT_NODE, "test");
// Test when atom exists
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
logger().info("END TEST: %s", __FUNCTION__);
}
// Test makeAtom
void JSCommandsUTest::test_makeAtom()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Test creating a node
std::string cmd = "AtomSpace.makeAtom({\"type\": \"ConceptNode\", \"name\": \"created\"})";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
// Verify node was created
Handle h = as->get_node(CONCEPT_NODE, "created");
TS_ASSERT(h != nullptr);
// Test creating a link
cmd = "AtomSpace.makeAtom({\"type\": \"ListLink\", \"outgoing\": [{\"type\": \"ConceptNode\", \"name\": \"created\"}, {\"type\": \"ConceptNode\", \"name\": \"another\"}]})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
// Verify link was created
HandleSeq outgoing;
outgoing.push_back(as->get_node(CONCEPT_NODE, "created"));
outgoing.push_back(as->get_node(CONCEPT_NODE, "another"));
Handle link = as->get_link(LIST_LINK, std::move(outgoing));
TS_ASSERT(link != nullptr);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test getIncoming
void JSCommandsUTest::test_getIncoming()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Create a node and some links pointing to it
Handle target = as->add_node(CONCEPT_NODE, "target");
Handle other = as->add_node(CONCEPT_NODE, "other");
Handle list1 = as->add_link(LIST_LINK, target, other);
Handle list2 = as->add_link(LIST_LINK, other, target);
Handle eval1 = as->add_link(EVALUATION_LINK, target, other);
// Test getting all incoming
std::string cmd = "AtomSpace.getIncoming({\"type\": \"ConceptNode\", \"name\": \"target\"})";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should return array with 3 links
TS_ASSERT(result.find("[") != std::string::npos);
TS_ASSERT(result.find("ListLink") != std::string::npos);
TS_ASSERT(result.find("EvaluationLink") != std::string::npos);
// Test with type filter
cmd = "AtomSpace.getIncoming({\"type\": \"ConceptNode\", \"name\": \"target\"}, \"ListLink\")";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should only have ListLinks
TS_ASSERT(result.find("ListLink") != std::string::npos);
TS_ASSERT(result.find("EvaluationLink") == std::string::npos);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test getValues and setValue
void JSCommandsUTest::test_getValuesSetValue()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Create atoms
Handle h = as->add_node(CONCEPT_NODE, "valuetest");
Handle key = as->add_node(PREDICATE_NODE, "mykey");
// Set a value
std::string cmd = "AtomSpace.setValue({\"type\": \"ConceptNode\", \"name\": \"valuetest\", \"key\": {\"type\": \"PredicateNode\", \"name\": \"mykey\"}, \"value\": {\"type\": \"FloatValue\", \"value\": [1.1, 2.2, 3.3]}})";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
// Get values
cmd = "AtomSpace.getValues({\"type\": \"ConceptNode\", \"name\": \"valuetest\"})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should contain the key and float value
TS_ASSERT(result.find("mykey") != std::string::npos);
TS_ASSERT(result.find("FloatValue") != std::string::npos);
TS_ASSERT(result.find("1.1") != std::string::npos);
TS_ASSERT(result.find("2.2") != std::string::npos);
TS_ASSERT(result.find("3.3") != std::string::npos);
// Test setValue with mixed field order and StringValue
logger().info("Testing setValue with mixed field order and StringValue...");
// Test the exact command format requested
cmd = "AtomSpace.setValue({\"key\":{\"type\":\"Predicate\",\"name\":\"sovs\"},\"name\":\"fimble\",\"type\":\"Concept\",\"value\":{\"type\":\"StringValue\",\"value\":[\"a\", \"b\", \"c\", \"wait there's more\", \"e\", \"oofdah\"]}})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for setValue with mixed fields: %s", result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
// Verify the atom was created with the correct name
Handle fimble = as->get_node(CONCEPT_NODE, "fimble");
TS_ASSERT(fimble != nullptr);
TS_ASSERT_EQUALS(fimble->get_name(), "fimble");
// Verify the key was created
Handle sovs = as->get_node(PREDICATE_NODE, "sovs");
TS_ASSERT(sovs != nullptr);
TS_ASSERT_EQUALS(sovs->get_name(), "sovs");
// Verify the value was set correctly
ValuePtr v = fimble->getValue(sovs);
TS_ASSERT(v != nullptr);
// Check that it's a StringValue
StringValuePtr sv = StringValueCast(v);
TS_ASSERT(sv != nullptr);
// Verify the string values
std::vector<std::string> expected = {"a", "b", "c", "wait there's more", "e", "oofdah"};
TS_ASSERT_EQUALS(sv->size(), expected.size());
for (size_t i = 0; i < expected.size(); i++) {
TS_ASSERT_EQUALS(sv->value()[i], expected[i]);
}
// Also verify using getValues command
cmd = "AtomSpace.getValues({\"type\": \"Concept\", \"name\": \"fimble\"})";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for getValues on fimble: %s", result.c_str());
// Should contain the key and string values (now escaped in JSON string)
TS_ASSERT(result.find("sovs") != std::string::npos);
TS_ASSERT(result.find("StringValue") != std::string::npos);
TS_ASSERT(result.find("\\\"a\\\"") != std::string::npos);
TS_ASSERT(result.find("\\\"b\\\"") != std::string::npos);
TS_ASSERT(result.find("\\\"c\\\"") != std::string::npos);
TS_ASSERT(result.find("\\\"wait there's more\\\"") != std::string::npos);
TS_ASSERT(result.find("\\\"e\\\"") != std::string::npos);
TS_ASSERT(result.find("\\\"oofdah\\\"") != std::string::npos);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test execute
void JSCommandsUTest::test_execute()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Create executable atoms (using NumberNodes for PlusLink)
Handle n1 = as->add_node(NUMBER_NODE, "2");
Handle n2 = as->add_node(NUMBER_NODE, "3");
Handle plus = as->add_link(PLUS_LINK, n1, n2);
// Execute via command
std::string cmd = "AtomSpace.execute({\"type\": \"PlusLink\", \"outgoing\": [{\"type\": \"NumberNode\", \"name\": \"2\"}, {\"type\": \"NumberNode\", \"name\": \"3\"}]})";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should return result of execution (now escaped in JSON string)
TS_ASSERT(result.find("NumberNode") != std::string::npos);
TS_ASSERT(result.find("\\\"5\\\"") != std::string::npos);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test extract
void JSCommandsUTest::test_extract()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
// Create some atoms
Handle h1 = as->add_node(CONCEPT_NODE, "extract1");
Handle h2 = as->add_node(CONCEPT_NODE, "extract2");
Handle link = as->add_link(LIST_LINK, h1, h2);
size_t initial_size = as->get_size();
// Extract non-recursively (should fail because of incoming link)
std::string cmd = "AtomSpace.extract({\"type\": \"ConceptNode\", \"name\": \"extract1\"}, false)";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"false\"}]}\n");
TS_ASSERT_EQUALS(as->get_size(), initial_size);
// Extract recursively
cmd = "AtomSpace.extract({\"type\": \"ConceptNode\", \"name\": \"extract1\"}, true)";
result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
TS_ASSERT_EQUALS(result, "{\"content\": [{\"type\":\"text\", \"text\": \"true\"}]}\n");
// Verify extraction
TS_ASSERT(as->get_node(CONCEPT_NODE, "extract1") == nullptr);
TS_ASSERT(as->get_link(LIST_LINK, HandleSeq{h1, h2}) == nullptr);
logger().info("END TEST: %s", __FUNCTION__);
}
// Test version
void JSCommandsUTest::test_version()
{
logger().info("BEGIN TEST: %s", __FUNCTION__);
std::string cmd = "AtomSpace.version()";
std::string result = JSCommands::interpret_command(as.get(), cmd);
logger().info("Result for '%s': %s", cmd.c_str(), result.c_str());
// Should return version string
TS_ASSERT(result.length() > 0);
logger().info("END TEST: %s", __FUNCTION__);
}