diff --git a/build/cmake/create_tests.cmake b/build/cmake/create_tests.cmake index dbe56a817..388737f92 100644 --- a/build/cmake/create_tests.cmake +++ b/build/cmake/create_tests.cmake @@ -69,7 +69,7 @@ foreach(TEST_TARGET ${ALL_SHAREDLIB_TARGETS}) # Create test exe: set(TEST_EXE_NAME "${TEST_TARGET}-tester") - set(TEST_SOURCES "test/lib_smoke_test/main.cpp" "test/lib_smoke_test/test.amlg") + set(TEST_SOURCES "test/lib_smoke_test/main.cpp" "test/lib_smoke_test/test.amlg" "test/lib_smoke_test/counter.amlg") source_group(TREE ${CMAKE_SOURCE_DIR} FILES ${TEST_SOURCES}) add_executable(${TEST_EXE_NAME} ${TEST_SOURCES}) set_target_properties(${TEST_EXE_NAME} PROPERTIES FOLDER "Testing") @@ -78,10 +78,9 @@ foreach(TEST_TARGET ${ALL_SHAREDLIB_TARGETS}) # Test for test exe: set(TEST_NAME "Lib.SmokeTest.${TEST_EXE_NAME}") add_test(NAME ${TEST_NAME} - COMMAND ${TEST_RUNNER} "$" test.amlg + COMMAND ${TEST_RUNNER} "$" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test/lib_smoke_test ) - set_tests_properties(${TEST_NAME} PROPERTIES PASS_REGULAR_EXPRESSION "${AMALGAM_VERSION_FULL_ESCAPED}") list(APPEND ALL_TEST_TARGETS ${TEST_NAME}) endforeach() diff --git a/src/Amalgam/Amalgam.h b/src/Amalgam/Amalgam.h index eede83080..eb44359c8 100644 --- a/src/Amalgam/Amalgam.h +++ b/src/Amalgam/Amalgam.h @@ -24,6 +24,13 @@ extern "C" char *version; }; + //output from ExecuteEntityJsonPtrLogged + struct ResultWithLog + { + char *json; + char *log; + }; + //loads the entity specified into handle AMALGAM_EXPORT LoadEntityStatus LoadEntity(char *handle, char *path, char *file_type, bool persistent, char *json_file_params, char *write_log_filename, char *print_log_filename); @@ -59,6 +66,7 @@ extern "C" AMALGAM_EXPORT wchar_t *ExecuteEntityJsonPtrWide(char *handle, char *label, char *json); AMALGAM_EXPORT char *ExecuteEntityJsonPtr(char *handle, char *label, char *json); + AMALGAM_EXPORT ResultWithLog ExecuteEntityJsonPtrLogged(char *handle, char *label, char *json); AMALGAM_EXPORT char *EvalOnEntity(char *handle, char *amlg); diff --git a/src/Amalgam/AmalgamAPI.cpp b/src/Amalgam/AmalgamAPI.cpp index d317a9f2f..3bc66fa25 100644 --- a/src/Amalgam/AmalgamAPI.cpp +++ b/src/Amalgam/AmalgamAPI.cpp @@ -194,6 +194,18 @@ extern "C" return StringToCharPtr(ret); } + ResultWithLog ExecuteEntityJsonPtrLogged(char *handle, char *label, char *json) + { + std::string h(handle); + std::string l(label); + std::string_view j(json); + std::pair ret = entint.ExecuteEntityJSONLogged(h, l, j); + ResultWithLog rwl; + rwl.json = StringToCharPtr(ret.first); + rwl.log = StringToCharPtr(ret.second); + return rwl; + } + void ExecuteEntity(char *handle, char *label) { std::string h(handle); diff --git a/src/Amalgam/AmalgamTrace.cpp b/src/Amalgam/AmalgamTrace.cpp index 49d3047c4..5d612968b 100644 --- a/src/Amalgam/AmalgamTrace.cpp +++ b/src/Amalgam/AmalgamTrace.cpp @@ -189,6 +189,14 @@ int32_t RunAmalgamTrace(std::istream *in_stream, std::ostream *out_stream, std:: json_payload = input; // json data response = entint.ExecuteEntityJSON(handle, label, json_payload); } + else if(command == "EXECUTE_ENTITY_JSON_LOGGED") + { + handle = StringManipulation::RemoveFirstToken(input); + label = StringManipulation::RemoveFirstToken(input); + json_payload = input; // json data + auto [json_response, log] = entint.ExecuteEntityJSONLogged(handle, label, json_payload); + response = json_response + "\n# " + log; + } else if(command == "EVAL_ON_ENTITY") { handle = StringManipulation::RemoveFirstToken(input); diff --git a/src/Amalgam/entity/Entity.h b/src/Amalgam/entity/Entity.h index 67897ee17..40da33126 100644 --- a/src/Amalgam/entity/Entity.h +++ b/src/Amalgam/entity/Entity.h @@ -240,7 +240,7 @@ class Entity } //same as Execute but accepts a string for label name - inline EvaluableNodeReference Execute(std::string &label_name, + inline EvaluableNodeReference Execute(const std::string &label_name, EvaluableNode *call_stack, bool on_self = false, Interpreter *calling_interpreter = nullptr, std::vector *write_listeners = nullptr, PrintListener *print_listener = nullptr, PerformanceConstraints *performance_constraints = nullptr diff --git a/src/Amalgam/entity/EntityExternalInterface.cpp b/src/Amalgam/entity/EntityExternalInterface.cpp index ae7994519..652e09e01 100644 --- a/src/Amalgam/entity/EntityExternalInterface.cpp +++ b/src/Amalgam/entity/EntityExternalInterface.cpp @@ -257,6 +257,43 @@ std::string EntityExternalInterface::ExecuteEntityJSON(std::string &handle, std: return (converted ? result : string_intern_pool.GetStringFromID(string_intern_pool.NOT_A_STRING_ID)); } +std::pair EntityExternalInterface::ExecuteEntityJSONLogged(const std::string &handle, const std::string &label, std::string_view json) +{ + auto bundle = FindEntityBundle(handle); + if(bundle == nullptr) + return std::pair("", ""); + + EntityWriteListener logger(bundle->entity, true); + std::vector listeners(bundle->writeListeners); + listeners.push_back(&logger); + + EvaluableNodeManager &enm = bundle->entity->evaluableNodeManager; +#ifdef MULTITHREAD_SUPPORT + //lock memory before allocating call stack + Concurrency::ReadLock enm_lock(enm.memoryModificationMutex); +#endif + EvaluableNodeReference args = EvaluableNodeReference(EvaluableNodeJSONTranslation::JsonToEvaluableNode(&enm, json), true); + + auto call_stack = Interpreter::ConvertArgsToCallStack(args, enm); + + EvaluableNodeReference returned_value = bundle->entity->Execute(label, call_stack, false, nullptr, + &listeners, bundle->printListener, nullptr +#ifdef MULTITHREAD_SUPPORT + , &enm_lock +#endif + ); + enm.FreeNode(call_stack->GetOrderedChildNodesReference()[0]); + enm.FreeNode(call_stack); + + auto [result, converted] = EvaluableNodeJSONTranslation::EvaluableNodeToJson(returned_value); + enm.FreeNodeTreeIfPossible(returned_value); + std::string json_out = converted ? result : string_intern_pool.GetStringFromID(string_intern_pool.NOT_A_STRING_ID); + + std::string log = Parser::Unparse(logger.GetWrites(), false, false); + + return std::pair(json_out, log); +} + std::string EntityExternalInterface::EvalOnEntity(const std::string &handle, const std::string &amlg) { auto bundle = FindEntityBundle(handle); diff --git a/src/Amalgam/entity/EntityExternalInterface.h b/src/Amalgam/entity/EntityExternalInterface.h index 8db200e2f..63d0dcdf3 100644 --- a/src/Amalgam/entity/EntityExternalInterface.h +++ b/src/Amalgam/entity/EntityExternalInterface.h @@ -58,6 +58,7 @@ class EntityExternalInterface bool SetJSONToLabel(std::string &handle, std::string &label, std::string_view json); std::string GetJSONFromLabel(std::string &handle, std::string &label); std::string ExecuteEntityJSON(std::string &handle, std::string &label, std::string_view json); + std::pair ExecuteEntityJSONLogged(const std::string &handle, const std::string &label, std::string_view json); std::string EvalOnEntity(const std::string &handle, const std::string &amlg); protected: diff --git a/src/Amalgam/interpreter/InterpreterOpcodesEntityAccess.cpp b/src/Amalgam/interpreter/InterpreterOpcodesEntityAccess.cpp index f5fd2e9d1..f32361444 100644 --- a/src/Amalgam/interpreter/InterpreterOpcodesEntityAccess.cpp +++ b/src/Amalgam/interpreter/InterpreterOpcodesEntityAccess.cpp @@ -487,7 +487,7 @@ EvaluableNodeReference Interpreter::InterpretNode_ENT_CALL_ENTITY_and_CALL_ENTIT memoryModificationLock.unlock(); #endif - EvaluableNodeReference result = called_entity->Execute(entity_label_sid, + EvaluableNodeReference result = called_entity->Execute(StringInternPool::StringID(entity_label_sid), call_stack, called_entity == curEntity, this, cur_write_listeners, printListener, perf_constraints_ptr #ifdef MULTITHREAD_SUPPORT , &enm_lock diff --git a/test/lib_smoke_test/counter.amlg b/test/lib_smoke_test/counter.amlg new file mode 100644 index 000000000..0c12b5866 --- /dev/null +++ b/test/lib_smoke_test/counter.amlg @@ -0,0 +1,24 @@ +; An entity that's a trivial counter. +(null + #!value + 0 + + #initialize + (assign_to_entities (assoc !value 0)) + + #get_value + (retrieve_from_entity "!value") + + #increment + (seq + (accum_to_entities (assoc !value 1)) + (call get_value) + ) + + #add + (declare + (assoc count 1) + (accum_to_entities (assoc !value count)) + (call get_value) + ) +) \ No newline at end of file diff --git a/test/lib_smoke_test/main.cpp b/test/lib_smoke_test/main.cpp index ef3b814ba..25a721d26 100644 --- a/test/lib_smoke_test/main.cpp +++ b/test/lib_smoke_test/main.cpp @@ -6,37 +6,293 @@ #include "Amalgam.h" //system headers: +#include #include #include -int main(int argc, char* argv[]) +// A wrapper around a C string that DeleteString() on exit. +class ApiString +{ + char *p; + +public: + ApiString(char *p) : p(p) {} + ApiString(const ApiString &a) = delete; + ApiString(ApiString &&a) : p(a.p) + { + a.p = nullptr; + } + + ~ApiString() + { + if(p != nullptr) + DeleteString(p); + } + + operator std::string() const + { + std::string s(p); + return s; + } +}; + +// A wrapper around an entity that DestroyEntity() on exit. +class LoadedEntity +{ + std::string h; + +public: + LoadedEntity(const std::string &handle) : h(handle) {} + + ~LoadedEntity() + { + DestroyEntity(h.data()); + } + + const std::string &handle() const + { + return h; + } +}; + +class TestResult { - // Print version: - std::cout << std::string(GetVersionString()) << std::endl; + std::string test; + bool successful; + +public: + TestResult(const std::string &test) : test(test), successful(true) {} + + operator bool() const { + return successful; + } + + void check(const std::string &action, const std::string &actual, const std::string &expected) + { + if(actual != expected) + { + std::cerr << test << ": " << action << " produced " << actual << " but expected " << expected << std::endl; + successful = false; + } + } + + void require(const std::string &action, bool actual) + { + if(!actual) + { + std::cerr << test << ": Failed to " << action << std::endl; + successful = false; + } + } +}; + +class SuiteResult +{ + bool verbose; + bool successful; + +public: + SuiteResult(bool verbose) : verbose(verbose), successful(true) {} + + operator bool() const + { + return successful; + } + + void run(const std::string &test, std::function f) + { + TestResult test_result(test); + if(verbose) + std::cout << test << std::endl; + f(test_result); + successful = successful && test_result; + } +}; + +static void dumpVersion(TestResult &test_result) +{ + ApiString version(GetVersionString()); + std::cout << static_cast(version) << std::endl; +} +static void loadAndEval(TestResult &test_result) +{ // Load+execute+delete entity: char handle[] = "1"; - char* file = (argc > 1) ? argv[1] : (char*)"test.amlg"; + char* file = (char*)"test.amlg"; char file_type[] = ""; char json_file_params[] = ""; char write_log[] = ""; char print_log[] = ""; auto status = LoadEntity(handle, file, file_type, false, json_file_params, write_log, print_log); - if(!status.loaded) - return 1; + test_result.require("LoadEntity", status.loaded); + if(test_result) + { + LoadedEntity loaded_entity(handle); + + char label[] = "test"; + ExecuteEntity(handle, label); + + std::string amlg("(size (contained_entities))"); + ApiString result(EvalOnEntity(handle, amlg.data())); + test_result.check("EvalOnEntity", result, "24"); + } +} + +static std::string handle("handle"); +static std::string filename("counter.amlg"); +static std::string empty(""); +static std::string initialize("initialize"); +static std::string add("add"); +static std::string get_value("get_value"); +static std::string increment("increment"); + +static void initializeCounter(TestResult &test_result) +{ + LoadEntityStatus status = LoadEntity(handle.data(), filename.data(), empty.data(), false, empty.data(), empty.data(), empty.data()); + test_result.require("LoadEntity", status.loaded); + if(test_result) + { + LoadedEntity loaded_entity(handle); + ExecuteEntity(handle.data(), initialize.data()); + ApiString result(ExecuteEntityJsonPtr(handle.data(), get_value.data(), empty.data())); + test_result.check("ExecuteEntityJsonPtr", result, "0"); + } +} + +static void executeEntityJsonWithValue(TestResult &test_result) { + LoadEntityStatus status = LoadEntity(handle.data(), filename.data(), empty.data(), false, empty.data(), empty.data(), empty.data()); + test_result.require("LoadEntity", status.loaded); + if(test_result) + { + LoadedEntity loaded_entity(handle); + ExecuteEntity(handle.data(), initialize.data()); + std::string json("{\"count\":2}"); + ApiString result(ExecuteEntityJsonPtr(handle.data(), add.data(), json.data())); + test_result.check("ExecuteEntityJsonPtr", result, "2"); + } +} + +static void executeEntityJsonLogged(TestResult &test_result) { + LoadEntityStatus status = LoadEntity(handle.data(), filename.data(), empty.data(), false, empty.data(), empty.data(), empty.data()); + test_result.require("LoadEntity", status.loaded); + if(test_result) + { + LoadedEntity loaded_entity(handle); + ExecuteEntity(handle.data(), initialize.data()); + ResultWithLog result = ExecuteEntityJsonPtrLogged(handle.data(), increment.data(), empty.data()); + ApiString json(result.json); + ApiString log(result.log); + test_result.check("ExecuteEntityJsonPtrLogged json", json, "1"); + test_result.check("ExecuteEntityJsonPtrLogged log", log, "(seq (accum_to_entities {!value 1}))"); + } +} + +static void executeEntityJsonLoggedUpdating(TestResult &test_result) { + LoadEntityStatus status = LoadEntity(handle.data(), filename.data(), empty.data(), false, empty.data(), empty.data(), empty.data()); + test_result.require("LoadEntity", status.loaded); + if(test_result) + { + LoadedEntity loaded_entity(handle); + ExecuteEntity(handle.data(), initialize.data()); + + ApiString one(ExecuteEntityJsonPtr(handle.data(), increment.data(), empty.data())); + test_result.check("ExecuteEntityJson", one, "1"); - int retval = 0; - char label[] = "test"; - ExecuteEntity(handle, label); + ResultWithLog result = ExecuteEntityJsonPtrLogged(handle.data(), increment.data(), empty.data()); + ApiString json(result.json); + ApiString log(result.log); + test_result.check("ExecuteEntityJsonPtrLogged json", json, "2"); + test_result.check("ExecuteEntityJsonPtrLogged log", log, "(seq (accum_to_entities {!value 1}))"); + } +} - std::string amlg("(size (contained_entities))"); - std::string result = EvalOnEntity(handle, amlg.data()); - if(result != std::string("24")) +static void executeEntityJsonLoggedRoundTrip(TestResult &test_result) { + LoadEntityStatus status = LoadEntity(handle.data(), filename.data(), empty.data(), false, empty.data(), empty.data(), empty.data()); + test_result.require("LoadEntity", status.loaded); + if(test_result) { - std::cerr << "EvalOnEntity produced " << result << " but expected 24"; - retval = 1; + LoadedEntity loaded_entity(handle); + ExecuteEntity(handle.data(), initialize.data()); + + // Increment the counter, getting a log. + ResultWithLog result = ExecuteEntityJsonPtrLogged(handle.data(), increment.data(), empty.data()); + ApiString json(result.json); + ApiString log(result.log); + test_result.check("ExecuteEntityJsonPtrLogged json", json, "1"); + + // Reset the entity and replay the log. We should get the same result back from the state. + ExecuteEntity(handle.data(), initialize.data()); + EvalOnEntity(handle.data(), result.log); + ApiString gotten(ExecuteEntityJsonPtr(handle.data(), get_value.data(), empty.data())); + test_result.check("ExecuteEntityJsonPtr get_value", gotten, "1"); } +} + +static void executeEntityJsonLoggedTwice(TestResult &test_result) { + LoadEntityStatus status = LoadEntity(handle.data(), filename.data(), empty.data(), false, empty.data(), empty.data(), empty.data()); + test_result.require("LoadEntity", status.loaded); + if(test_result) + { + LoadedEntity loaded_entity(handle); + ExecuteEntity(handle.data(), initialize.data()); + + // Increment the counter, getting a log. + ResultWithLog result1 = ExecuteEntityJsonPtrLogged(handle.data(), increment.data(), empty.data()); + ApiString json1(result1.json); + ApiString log1(result1.log); + test_result.check("ExecuteEntityJsonPtrLogged json", json1, "1"); + + // Again. + ResultWithLog result2 = ExecuteEntityJsonPtrLogged(handle.data(), increment.data(), empty.data()); + ApiString json2(result2.json); + ApiString log2(result2.log); + test_result.check("ExecuteEntityJsonPtrLogged json", json2, "2"); + + // Reset the entity and replay both logs. We should get the same result back from the state. + ExecuteEntity(handle.data(), initialize.data()); + EvalOnEntity(handle.data(), result1.log); + EvalOnEntity(handle.data(), result2.log); + ApiString gotten(ExecuteEntityJsonPtr(handle.data(), get_value.data(), empty.data())); + test_result.check("ExecuteEntityJsonPtr get_value", gotten, "2"); + } +} + +int main(int argc, char* argv[]) +{ + bool verbose = false; + + for(int i = 1; i < argc; i++) + { + if(std::string("--help") == argv[i] || std::string("-h") == argv[i]) + { + std::cout << "Usage: " << argv[0] << " [-h] [-v]" << std::endl + << std::endl + << "Options:" << std::endl + << " --help, -h Print this help message" << std::endl + << " --verbose, -v Print each test name as it executes" << std::endl; + return 0; + } + else if(std::string("--verbose") == argv[i] || std::string("-v") == argv[i]) + { + verbose = true; + } + else + { + std::cerr << argv[0] << ": unrecognized option " << argv[i] << std::endl; + return 1; + } + } + + SuiteResult suite(verbose); + suite.run("dumpVersion", dumpVersion); + suite.run("loadAndEval", loadAndEval); + suite.run("initializeCounter", initializeCounter); + suite.run("executeEntityJsonWithValue", executeEntityJsonWithValue); + suite.run("executeEntityJsonLogged", executeEntityJsonLogged); + suite.run("executeEntityJsonLoggedUpdating", executeEntityJsonLoggedUpdating); + suite.run("executeEntityJsonLoggedRoundTrip", executeEntityJsonLoggedRoundTrip); + suite.run("executeEntityJsonLoggedTwice", executeEntityJsonLoggedTwice); - DestroyEntity(handle); - return retval; + return suite ? 0 : 1; }