From 273a6c2382716b7c5109311243e690d14f76af89 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:41:32 +0100 Subject: [PATCH] Add ScriptBuilder class --- CMakeLists.txt | 1 + include/scratchcpp/dev/test/scriptbuilder.h | 58 ++++++ src/dev/CMakeLists.txt | 1 + src/dev/test/CMakeLists.txt | 6 + src/dev/test/scriptbuilder.cpp | 198 ++++++++++++++++++++ src/dev/test/scriptbuilder_p.cpp | 12 ++ src/dev/test/scriptbuilder_p.h | 35 ++++ test/dev/CMakeLists.txt | 1 + test/dev/test_api/CMakeLists.txt | 25 +++ test/dev/test_api/scriptbuilder_test.cpp | 131 +++++++++++++ test/dev/test_api/testextension.cpp | 80 ++++++++ test/dev/test_api/testextension.h | 25 +++ 12 files changed, 573 insertions(+) create mode 100644 include/scratchcpp/dev/test/scriptbuilder.h create mode 100644 src/dev/test/CMakeLists.txt create mode 100644 src/dev/test/scriptbuilder.cpp create mode 100644 src/dev/test/scriptbuilder_p.cpp create mode 100644 src/dev/test/scriptbuilder_p.h create mode 100644 test/dev/test_api/CMakeLists.txt create mode 100644 test/dev/test_api/scriptbuilder_test.cpp create mode 100644 test/dev/test_api/testextension.cpp create mode 100644 test/dev/test_api/testextension.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b7723d88..27fc54d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ if (LIBSCRATCHCPP_USE_LLVM) include/scratchcpp/dev/executablecode.h include/scratchcpp/dev/executioncontext.h include/scratchcpp/dev/promise.h + include/scratchcpp/dev/test/scriptbuilder.h ) if(LIBSCRATCHCPP_PRINT_LLVM_IR) diff --git a/include/scratchcpp/dev/test/scriptbuilder.h b/include/scratchcpp/dev/test/scriptbuilder.h new file mode 100644 index 00000000..532f5a67 --- /dev/null +++ b/include/scratchcpp/dev/test/scriptbuilder.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "../../global.h" +#include "../../spimpl.h" + +namespace libscratchcpp +{ + +class IExtension; +class IEngine; +class Target; +class List; + +} // namespace libscratchcpp + +namespace libscratchcpp::test +{ + +class ScriptBuilderPrivate; + +/*! \brief The ScriptBuilder class is used to build Scratch scripts in unit tests. */ +class LIBSCRATCHCPP_EXPORT ScriptBuilder +{ + public: + ScriptBuilder(IExtension *extension, IEngine *engine, std::shared_ptr target); + ScriptBuilder(const ScriptBuilder &) = delete; + + ~ScriptBuilder(); + + void addBlock(const std::string &opcode); + void addReporterBlock(const std::string &opcode); + void captureBlockReturnValue(); + + void addValueInput(const std::string &name, const Value &value); + void addNullInput(const std::string &name); + + void addObscuredInput(const std::string &name, std::shared_ptr valueBlock); + void addNullObscuredInput(const std::string &name); + + void addDropdownInput(const std::string &name, const std::string &selectedValue); + void addDropdownField(const std::string &name, const std::string &selectedValue); + + void build(); + void run(); + + List *capturedValues() const; + + private: + void addBlock(std::shared_ptr block); + + spimpl::unique_impl_ptr impl; +}; + +} // namespace libscratchcpp::test diff --git a/src/dev/CMakeLists.txt b/src/dev/CMakeLists.txt index 696178cd..4c9b3cd6 100644 --- a/src/dev/CMakeLists.txt +++ b/src/dev/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(blocks) add_subdirectory(engine) +add_subdirectory(test) diff --git a/src/dev/test/CMakeLists.txt b/src/dev/test/CMakeLists.txt new file mode 100644 index 00000000..d447cb2a --- /dev/null +++ b/src/dev/test/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources(scratchcpp + PRIVATE + scriptbuilder.cpp + scriptbuilder_p.cpp + scriptbuilder_p.h +) diff --git a/src/dev/test/scriptbuilder.cpp b/src/dev/test/scriptbuilder.cpp new file mode 100644 index 00000000..8cd1e037 --- /dev/null +++ b/src/dev/test/scriptbuilder.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scriptbuilder_p.h" + +using namespace libscratchcpp; +using namespace libscratchcpp::test; + +static std::unordered_map> captureLists; + +/*! Constructs ScriptBuilder. */ +ScriptBuilder::ScriptBuilder(IExtension *extension, IEngine *engine, std::shared_ptr target) : + impl(spimpl::make_unique_impl(engine, target)) +{ + // Create capture list + if (captureLists.find(engine) != captureLists.cend()) { + std::cerr << "error: only one ScriptBuilder can be created for each engine" << std::endl; + return; + } + + captureLists[engine] = std::make_shared("", ""); + + // Add start hat block + auto block = std::make_shared(std::to_string(impl->blockId++), "script_builder_init"); + engine->addCompileFunction(extension, block->opcode(), [](Compiler *compiler) -> CompilerValue * { + compiler->engine()->addGreenFlagScript(compiler->block()); + return nullptr; + }); + addBlock(block); + + // Add compile function for return value capture block + engine->addCompileFunction(extension, "script_builder_capture", [](Compiler *compiler) -> CompilerValue * { + CompilerValue *input = compiler->addInput("VALUE"); + compiler->createListAppend(captureLists[compiler->engine()].get(), input); + return nullptr; + }); +} + +/*! Destroys ScriptBuilder. */ +ScriptBuilder::~ScriptBuilder() +{ + captureLists.erase(impl->engine); +} + +/*! Adds a block with the given opcode to the script. */ +void ScriptBuilder::addBlock(const std::string &opcode) +{ + impl->lastBlock = std::make_shared(std::to_string(impl->blockId++), opcode); + addBlock(impl->lastBlock); +} + +/*! Creates a reporter block with the given opcode to be used with captureBlockReturnValue() later. */ +void ScriptBuilder::addReporterBlock(const std::string &opcode) +{ + impl->lastBlock = std::make_shared(std::to_string(impl->blockId++), opcode); +} + +/*! Captures the return value of the created reporter block. It can be retrieved using capturedValues() later. */ +void ScriptBuilder::captureBlockReturnValue() +{ + if (!impl->lastBlock) + return; + + auto valueBlock = impl->lastBlock; + addBlock("script_builder_capture"); + addObscuredInput("VALUE", valueBlock); +} + +/*! Adds a simple input with a value to the current block. */ +void ScriptBuilder::addValueInput(const std::string &name, const Value &value) +{ + if (!impl->lastBlock) + return; + + auto input = std::make_shared(name, Input::Type::Shadow); + input->setPrimaryValue(value); + impl->lastBlock->addInput(input); +} + +/*! Adds a null input (zero) to the current block. */ +void ScriptBuilder::addNullInput(const std::string &name) +{ + if (!impl->lastBlock) + return; + + auto input = std::make_shared(name, Input::Type::Shadow); + impl->lastBlock->addInput(input); +} + +/*! Adds an input obscured by the given block to the current block. */ +void ScriptBuilder::addObscuredInput(const std::string &name, std::shared_ptr valueBlock) +{ + if (!impl->lastBlock) + return; + + valueBlock->setId(std::to_string(impl->blockId++)); + impl->inputBlocks.push_back(valueBlock); + + auto input = std::make_shared(name, Input::Type::ObscuredShadow); + input->setValueBlock(valueBlock); + impl->lastBlock->addInput(input); +} + +/*! Adds an input obscured by a block which returns zero to the current block. */ +void ScriptBuilder::addNullObscuredInput(const std::string &name) +{ + if (!impl->lastBlock) + return; + + auto input = std::make_shared(name, Input::Type::ObscuredShadow); + auto block = std::make_shared(std::to_string(impl->blockId++), ""); + block->setCompileFunction([](Compiler *compiler) -> CompilerValue * { return compiler->addConstValue(Value()); }); + input->setValueBlock(block); + impl->inputBlocks.push_back(block); + impl->blocks.back()->addInput(input); +} + +/*! Adds a dropdown menu input to the current block. */ +void ScriptBuilder::addDropdownInput(const std::string &name, const std::string &selectedValue) +{ + if (!impl->lastBlock) + return; + + auto block = impl->blocks.back(); + auto input = std::make_shared(name, Input::Type::Shadow); + block->addInput(input); + + auto menu = std::make_shared(std::to_string(impl->blockId++), block->opcode() + "_menu"); + menu->setShadow(true); + impl->inputBlocks.push_back(menu); + input->setValueBlock(menu); + + auto field = std::make_shared(name, selectedValue); + menu->addField(field); +} + +/*! Adds a dropdown field to the current block. */ +void ScriptBuilder::addDropdownField(const std::string &name, const std::string &selectedValue) +{ + if (!impl->lastBlock) + return; + + auto field = std::make_shared(name, selectedValue); + impl->blocks.back()->addField(field); +} + +/*! Builds and compiles the script. */ +void ScriptBuilder::build() +{ + if (impl->target->blocks().empty()) { + for (auto block : impl->blocks) + impl->target->addBlock(block); + + for (auto block : impl->inputBlocks) + impl->target->addBlock(block); + } + + std::vector> targets = impl->engine->targets(); + + if (std::find(targets.begin(), targets.end(), impl->target) == targets.end()) { + targets.push_back(impl->target); + impl->engine->setTargets({ impl->target }); + } + + impl->engine->compile(); +} + +/*! Runs the built script. */ +void ScriptBuilder::run() +{ + impl->engine->run(); +} + +/*! Returns the list of captured block return values. */ +List *ScriptBuilder::capturedValues() const +{ + return captureLists[impl->engine].get(); +} + +void ScriptBuilder::addBlock(std::shared_ptr block) +{ + if (!impl->blocks.empty()) { + auto lastBlock = impl->blocks.back(); + lastBlock->setNext(block); + block->setParent(lastBlock); + } + + impl->blocks.push_back(block); +} diff --git a/src/dev/test/scriptbuilder_p.cpp b/src/dev/test/scriptbuilder_p.cpp new file mode 100644 index 00000000..f2d3fa29 --- /dev/null +++ b/src/dev/test/scriptbuilder_p.cpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "scriptbuilder_p.h" + +using namespace libscratchcpp; +using namespace libscratchcpp::test; + +ScriptBuilderPrivate::ScriptBuilderPrivate(IEngine *engine, std::shared_ptr target) : + engine(engine), + target(target) +{ +} diff --git a/src/dev/test/scriptbuilder_p.h b/src/dev/test/scriptbuilder_p.h new file mode 100644 index 00000000..9b332b54 --- /dev/null +++ b/src/dev/test/scriptbuilder_p.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +namespace libscratchcpp +{ + +class IEngine; +class Target; +class Block; +class List; + +} // namespace libscratchcpp + +namespace libscratchcpp::test +{ + +class ScriptBuilderPrivate +{ + public: + ScriptBuilderPrivate(IEngine *engine, std::shared_ptr target); + ScriptBuilderPrivate(const ScriptBuilderPrivate &) = delete; + + IEngine *engine = nullptr; + std::shared_ptr target; + std::shared_ptr lastBlock; + std::vector> blocks; + std::vector> inputBlocks; + unsigned int blockId = 0; +}; + +} // namespace libscratchcpp::test diff --git a/test/dev/CMakeLists.txt b/test/dev/CMakeLists.txt index 60bec96f..b9db18f7 100644 --- a/test/dev/CMakeLists.txt +++ b/test/dev/CMakeLists.txt @@ -3,3 +3,4 @@ add_subdirectory(executioncontext) add_subdirectory(llvm) add_subdirectory(compiler) add_subdirectory(promise) +add_subdirectory(test_api) diff --git a/test/dev/test_api/CMakeLists.txt b/test/dev/test_api/CMakeLists.txt new file mode 100644 index 00000000..cd5f8141 --- /dev/null +++ b/test/dev/test_api/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library( + test_api_test_deps SHARED + testextension.cpp + testextension.h +) + +target_link_libraries( + test_api_test_deps + GTest::gtest_main + scratchcpp +) + +add_executable( + test_api_test + scriptbuilder_test.cpp +) + +target_link_libraries( + test_api_test + GTest::gtest_main + scratchcpp + test_api_test_deps +) + +gtest_discover_tests(test_api_test) diff --git a/test/dev/test_api/scriptbuilder_test.cpp b/test/dev/test_api/scriptbuilder_test.cpp new file mode 100644 index 00000000..c8ea801a --- /dev/null +++ b/test/dev/test_api/scriptbuilder_test.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../../common.h" +#include "testextension.h" + +using namespace libscratchcpp; +using namespace libscratchcpp::test; + +class ScriptBuilderTest : public testing::Test +{ + public: + void SetUp() override + { + m_engine = m_project.engine().get(); + m_extension.registerBlocks(m_engine); + m_target = std::make_shared(); + m_builder = std::make_unique(&m_extension, m_engine, m_target); + } + + Project m_project; + IEngine *m_engine = nullptr; + std::shared_ptr m_target; + TestExtension m_extension; + std::unique_ptr m_builder; +}; + +TEST_F(ScriptBuilderTest, AddBlock) +{ + m_builder->addBlock("test_simple"); + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n"); +} + +TEST_F(ScriptBuilderTest, AddValueInput) +{ + m_builder->addBlock("test_print"); + m_builder->addValueInput("STRING", 10); + m_builder->addBlock("test_print"); + m_builder->addValueInput("STRING", "Hello world"); + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "10\nHello world\n"); +} + +TEST_F(ScriptBuilderTest, AddNullInput) +{ + m_builder->addBlock("test_print"); + m_builder->addNullInput("STRING"); + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "0\n"); +} + +TEST_F(ScriptBuilderTest, AddObscuredInput) +{ + m_builder->addBlock("test_print"); + auto block = std::make_shared("", "test_teststr"); + m_builder->addObscuredInput("STRING", block); + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "test\n"); +} + +TEST_F(ScriptBuilderTest, AddNullObscuredInput) +{ + m_builder->addBlock("test_print"); + m_builder->addNullObscuredInput("STRING"); + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "0\n"); +} + +TEST_F(ScriptBuilderTest, AddDropdownInput) +{ + m_builder->addBlock("test_print_dropdown"); + m_builder->addDropdownInput("STRING", "hello"); + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "hello\n"); +} + +TEST_F(ScriptBuilderTest, AddDropdownField) +{ + m_builder->addBlock("test_print_field"); + m_builder->addDropdownField("STRING", "hello"); + m_builder->build(); + + testing::internal::CaptureStdout(); + m_builder->run(); + ASSERT_EQ(testing::internal::GetCapturedStdout(), "hello\n"); +} + +TEST_F(ScriptBuilderTest, ReporterBlocks) +{ + m_builder->addReporterBlock("test_teststr"); + m_builder->captureBlockReturnValue(); + + m_builder->addReporterBlock("test_input"); + m_builder->addValueInput("INPUT", -93.4); + m_builder->captureBlockReturnValue(); + + m_builder->build(); + m_builder->run(); + + List *values = m_builder->capturedValues(); + ASSERT_TRUE(values); + ASSERT_EQ(values->size(), 2); + std::string str; + value_toString(&values->operator[](0), &str); + ASSERT_EQ(str, "test"); + ASSERT_EQ(value_toDouble(&values->operator[](1)), -93.4); +} diff --git a/test/dev/test_api/testextension.cpp b/test/dev/test_api/testextension.cpp new file mode 100644 index 00000000..d24cbf3a --- /dev/null +++ b/test/dev/test_api/testextension.cpp @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "testextension.h" + +using namespace libscratchcpp; + +std::string TestExtension::name() const +{ + return "test"; +} + +std::string TestExtension::description() const +{ + return ""; +} + +void TestExtension::registerBlocks(IEngine *engine) +{ + engine->addCompileFunction(this, "test_simple", &compileSimple); + engine->addCompileFunction(this, "test_print", &compilePrint); + engine->addCompileFunction(this, "test_print_dropdown", &compilePrintDropdown); + engine->addCompileFunction(this, "test_print_field", &compilePrintField); + engine->addCompileFunction(this, "test_teststr", &compileTestStr); + engine->addCompileFunction(this, "test_input", &compileInput); +} + +CompilerValue *TestExtension::compileSimple(Compiler *compiler) +{ + compiler->addFunctionCall("test_simple", Compiler::StaticType::Void); + return nullptr; +} + +CompilerValue *TestExtension::compilePrint(Compiler *compiler) +{ + CompilerValue *input = compiler->addInput("STRING"); + compiler->addFunctionCall("test_print", Compiler::StaticType::Void, { Compiler::StaticType::String }, { input }); + return nullptr; +} + +CompilerValue *TestExtension::compilePrintDropdown(Compiler *compiler) +{ + EXPECT_TRUE(compiler->input("STRING")->pointsToDropdownMenu()); + CompilerValue *input = compiler->addInput("STRING"); + compiler->addFunctionCall("test_print", Compiler::StaticType::Void, { Compiler::StaticType::String }, { input }); + return nullptr; +} + +CompilerValue *TestExtension::compilePrintField(Compiler *compiler) +{ + CompilerValue *input = compiler->addConstValue(compiler->field("STRING")->value()); + compiler->addFunctionCall("test_print", Compiler::StaticType::Void, { Compiler::StaticType::String }, { input }); + return nullptr; +} + +CompilerValue *TestExtension::compileTestStr(Compiler *compiler) +{ + return compiler->addConstValue("test"); +} + +CompilerValue *TestExtension::compileInput(Compiler *compiler) +{ + return compiler->addInput("INPUT"); +} + +extern "C" void test_simple() +{ + std::cout << "test" << std::endl; +} + +extern "C" void test_print(const char *string) +{ + std::cout << string << std::endl; +} diff --git a/test/dev/test_api/testextension.h b/test/dev/test_api/testextension.h new file mode 100644 index 00000000..699d520f --- /dev/null +++ b/test/dev/test_api/testextension.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace libscratchcpp +{ + +class TestExtension : public IExtension +{ + public: + std::string name() const override; + std::string description() const override; + + void registerBlocks(IEngine *engine) override; + + private: + static CompilerValue *compileSimple(Compiler *compiler); + static CompilerValue *compilePrint(Compiler *compiler); + static CompilerValue *compilePrintDropdown(Compiler *compiler); + static CompilerValue *compilePrintField(Compiler *compiler); + static CompilerValue *compileTestStr(Compiler *compiler); + static CompilerValue *compileInput(Compiler *compiler); +}; + +} // namespace libscratchcpp