diff --git a/include/parser/Parser.hpp b/include/parser/Parser.hpp index 26bf0dd..7153a0c 100644 --- a/include/parser/Parser.hpp +++ b/include/parser/Parser.hpp @@ -19,6 +19,7 @@ class Parser { Parser() = default; std::shared_ptr parse(const std::string& filePath); + std::shared_ptr parseStream(std::istream& stream); private: static std::shared_ptr buildSceneObject(const NeuronIDE::SceneObject& protoObj); diff --git a/src/parser/Parser.cpp b/src/parser/Parser.cpp index 235dba1..54d68bf 100644 --- a/src/parser/Parser.cpp +++ b/src/parser/Parser.cpp @@ -11,15 +11,24 @@ #include "scene/components/ComponentRegistry.hpp" std::shared_ptr Parser::parse(const std::string& filePath) { - NeuronIDE::Scene protoScene; - std::ifstream file(filePath, std::ios::binary); if (!file.is_open()) { throw std::runtime_error("Parser: cannot open file: " + filePath); } - if (!protoScene.ParseFromIstream(&file)) { - throw std::runtime_error("Parser: failed to parse protobuf from: " + filePath); + try { + return parseStream(file); + } catch (const std::runtime_error& e) { + throw std::runtime_error(std::string("Parser: failed to parse file ") + filePath + " - " + + e.what()); + } +} + +std::shared_ptr Parser::parseStream(std::istream& stream) { + NeuronIDE::Scene protoScene; + + if (!protoScene.ParseFromIstream(&stream)) { + throw std::runtime_error("Parser: failed to parse protobuf from stream"); } auto scene = std::make_shared<::Scene>(); diff --git a/tests/component_tests/CMakeLists.txt b/tests/component_tests/CMakeLists.txt index 998cfe3..4ec7a3e 100644 --- a/tests/component_tests/CMakeLists.txt +++ b/tests/component_tests/CMakeLists.txt @@ -3,6 +3,7 @@ file(GLOB COMP_TEST_SOURCES "*.cpp") if(COMP_TEST_SOURCES) add_executable(component_tests ${COMP_TEST_SOURCES}) target_link_libraries(component_tests PRIVATE gtest_main runtime_core) + target_include_directories(component_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) gtest_discover_tests(component_tests PROPERTIES LABELS "component") endif() \ No newline at end of file diff --git a/tests/component_tests/parser_component_test.cpp b/tests/component_tests/parser_component_test.cpp new file mode 100644 index 0000000..5bc0020 --- /dev/null +++ b/tests/component_tests/parser_component_test.cpp @@ -0,0 +1,34 @@ +#include + +#include +#include +#include +#include +#include + +#include "parser/Parser.hpp" +#include "scene/Scene.hpp" +#include "scene/SceneObject.hpp" +#include "scene/components/BlinkComponent.hpp" +#include "utils/ParserTestUtils.hpp" + +TEST(ParserFileTest, ThrowsWhenFileDoesNotExist) { + Parser parser; + EXPECT_THROW(parser.parse("/nonexistent/path/scene.pb"), std::runtime_error); +} + +TEST(ParserFileTest, ReturnsNonNullSceneForValidFile) { + auto scene = utils::buildSimpleScene(); + const std::string path = (std::filesystem::temp_directory_path() / "valid_scene.pb").string(); + { + std::ofstream out(path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(out.is_open()); + scene.SerializeToOstream(&out); + } + + Parser parser; + auto result = parser.parse(path); + ASSERT_NE(result, nullptr); + + std::filesystem::remove(path); +} \ No newline at end of file diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index cddb417..040706c 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -3,6 +3,7 @@ file(GLOB UNIT_TEST_SOURCES "*.cpp") if(UNIT_TEST_SOURCES) add_executable(unit_tests ${UNIT_TEST_SOURCES}) target_link_libraries(unit_tests PRIVATE gtest_main runtime_core) + target_include_directories(unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) gtest_discover_tests(unit_tests PROPERTIES LABELS "unit") endif() \ No newline at end of file diff --git a/tests/unit_tests/parser_unit_test.cpp b/tests/unit_tests/parser_unit_test.cpp new file mode 100644 index 0000000..b425a4e --- /dev/null +++ b/tests/unit_tests/parser_unit_test.cpp @@ -0,0 +1,268 @@ +#include + +#include +#include +#include +#include +#include + +#include "neuronide.pb.h" +#include "parser/Parser.hpp" +#include "scene/Scene.hpp" +#include "scene/SceneObject.hpp" +#include "scene/components/BlinkComponent.hpp" +#include "utils/ParserTestUtils.hpp" + +// Grupa: Parser -- nazwa projektu (Scene.project_name) +TEST(ParserSceneNameTest, SetsProjectName) { + auto scene = utils::buildSimpleScene("MojProjekt"); + auto result = utils::parseProtoScene(scene); + EXPECT_EQ(result->getExperimentName(), "MojProjekt"); +} + +TEST(ParserSceneNameTest, EmptyProjectNameIsPreserved) { + auto scene = utils::buildSimpleScene(""); + auto result = utils::parseProtoScene(scene); + EXPECT_EQ(result->getExperimentName(), ""); +} + +// Grupa: Parser -- liczba obiektow sceny +TEST(ParserObjectCountTest, EmptySceneHasNoObjects) { + NeuronIDE::Scene protoScene; + auto result = utils::parseProtoScene(protoScene); + EXPECT_TRUE(result->getObjects().empty()); +} + +TEST(ParserObjectCountTest, SingleObjectIsLoaded) { + auto scene = utils::buildSimpleScene("P", "Obj1"); + auto result = utils::parseProtoScene(scene); + EXPECT_EQ(result->getObjects().size(), 1u); +} + +TEST(ParserObjectCountTest, MultipleObjectsAreAllLoaded) { + NeuronIDE::Scene scene; + scene.set_project_name("Multi"); + for (int i = 0; i < 5; ++i) { + auto* obj = scene.add_scene_objects(); + obj->set_name("Obj" + std::to_string(i)); + obj->set_is_visible(true); + auto* comp = obj->add_components(); + comp->mutable_blinker()->set_blink_frequency_hz(static_cast(i)); + } + auto result = utils::parseProtoScene(scene); + EXPECT_EQ(result->getObjects().size(), 5u); +} + +// Grupa: Parser -- atrybuty SceneObject +TEST(ParserSceneObjectTest, ObjectNameIsCorrect) { + auto scene = utils::buildSimpleScene("P", "MojaRakieta"); + auto result = utils::parseProtoScene(scene); + EXPECT_EQ(result->getObjects()[0]->name, "MojaRakieta"); +} + +TEST(ParserSceneObjectTest, ObjectIsVisibleWhenTrue) { + auto scene = utils::buildSimpleScene("P", "Obj", true); + auto result = utils::parseProtoScene(scene); + EXPECT_TRUE(result->getObjects()[0]->isVisible); +} + +TEST(ParserSceneObjectTest, ObjectIsHiddenWhenFalse) { + auto scene = utils::buildSimpleScene("P", "Obj", false); + auto result = utils::parseProtoScene(scene); + EXPECT_FALSE(result->getObjects()[0]->isVisible); +} + +TEST(ParserSceneObjectTest, ObjectsPreserveInsertionOrder) { + NeuronIDE::Scene scene; + scene.set_project_name("Order"); + const std::vector names = {"Alpha", "Beta", "Gamma"}; + for (const auto& n : names) { + auto* obj = scene.add_scene_objects(); + obj->set_name(n); + } + auto result = utils::parseProtoScene(scene); + ASSERT_EQ(result->getObjects().size(), names.size()); + for (size_t i = 0; i < names.size(); ++i) { + EXPECT_EQ(result->getObjects()[i]->name, names[i]); + } +} + +// Grupa: Parser -- Transform +TEST(ParserTransformTest, TransformFieldsAreParsedCorrectly) { + NeuronIDE::Scene scene; + scene.set_project_name("TransformTest"); + auto* obj = scene.add_scene_objects(); + obj->set_name("Sprite"); + obj->set_is_visible(true); + + auto* tra = obj->mutable_transform(); + tra->set_x(10.5); + tra->set_y(20.25); + tra->set_width(64.0); + tra->set_height(128.0); + tra->set_rotation(45.0); + + auto result = utils::parseProtoScene(scene); + ASSERT_EQ(result->getObjects().size(), 1u); + + const auto& t = result->getObjects()[0]->transform; + EXPECT_DOUBLE_EQ(t.posX, 10.5); + EXPECT_DOUBLE_EQ(t.posY, 20.25); + EXPECT_DOUBLE_EQ(t.width, 64.0); + EXPECT_DOUBLE_EQ(t.height, 128.0); + EXPECT_DOUBLE_EQ(t.rotation, 45.0); +} + +TEST(ParserTransformTest, DefaultTransformIsZeroWhenNotProvided) { + NeuronIDE::Scene scene; + scene.set_project_name("NoTransform"); + auto* obj = scene.add_scene_objects(); + obj->set_name("Bezpozycyjny"); + obj->set_is_visible(true); + + auto result = utils::parseProtoScene(scene); + ASSERT_EQ(result->getObjects().size(), 1u); + + const auto& t = result->getObjects()[0]->transform; + EXPECT_DOUBLE_EQ(t.posX, 0.0); + EXPECT_DOUBLE_EQ(t.posY, 0.0); + EXPECT_DOUBLE_EQ(t.width, 0.0); + EXPECT_DOUBLE_EQ(t.height, 0.0); + EXPECT_DOUBLE_EQ(t.rotation, 0.0); +} + +TEST(ParserTransformTest, NegativeTransformValuesAreAccepted) { + NeuronIDE::Scene scene; + scene.set_project_name("NegTrans"); + auto* obj = scene.add_scene_objects(); + obj->set_name("Ujemny"); + obj->set_is_visible(true); + auto* tra = obj->mutable_transform(); + tra->set_x(-50.0); + tra->set_y(-100.0); + tra->set_rotation(-90.0); + + auto result = utils::parseProtoScene(scene); + const auto& t = result->getObjects()[0]->transform; + EXPECT_DOUBLE_EQ(t.posX, -50.0); + EXPECT_DOUBLE_EQ(t.posY, -100.0); + EXPECT_DOUBLE_EQ(t.rotation, -90.0); +} + +// Grupa: Parser -- komponenty (BlinkComponent) +TEST(ParserComponentTest, ObjectWithNoComponentsHasEmptyComponentList) { + NeuronIDE::Scene scene; + scene.set_project_name("NoComp"); + auto* obj = scene.add_scene_objects(); + obj->set_name("PustyObiekt"); + + auto result = utils::parseProtoScene(scene); + EXPECT_TRUE(result->getObjects()[0]->components.empty()); +} + +TEST(ParserComponentTest, BlinkComponentIsCreated) { + auto scene = utils::buildSimpleScene("P", "Mrugacz", true, 2.0); + auto result = utils::parseProtoScene(scene); + ASSERT_EQ(result->getObjects().size(), 1u); + EXPECT_EQ(result->getObjects()[0]->components.size(), 1u); + EXPECT_NE(result->getObjects()[0]->components[0], nullptr); +} + +TEST(ParserComponentTest, BlinkComponentIsCorrectType) { + auto scene = utils::buildSimpleScene("P", "Blinker", true, 3.0); + auto result = utils::parseProtoScene(scene); + auto* raw = result->getObjects()[0]->components[0].get(); + EXPECT_NE(dynamic_cast(raw), nullptr); +} + +TEST(ParserComponentTest, UnknownComponentTypeIsIgnored) { + NeuronIDE::Scene scene; + scene.set_project_name("UnknownComp"); + auto* obj = scene.add_scene_objects(); + obj->set_name("Obiekt"); + obj->set_is_visible(true); + obj->add_components(); // pusty Component, brak oneof + + auto result = utils::parseProtoScene(scene); + EXPECT_TRUE(result->getObjects()[0]->components.empty()); +} + +TEST(ParserComponentTest, DuplicateComponentTypeThrows) { + NeuronIDE::Scene scene; + scene.set_project_name("DupComp"); + auto* obj = scene.add_scene_objects(); + obj->set_name("Zduplikowany"); + obj->set_is_visible(true); + + obj->add_components()->mutable_blinker()->set_blink_frequency_hz(1.0); + obj->add_components()->mutable_blinker()->set_blink_frequency_hz(2.0); + + std::stringstream ss; + scene.SerializeToOstream(&ss); + + Parser parser; + EXPECT_THROW(parser.parseStream(ss), std::runtime_error); +} + +TEST(ParserComponentTest, MultipleObjectsEachHaveTheirOwnComponents) { + NeuronIDE::Scene scene; + scene.set_project_name("IndepComp"); + for (int i = 0; i < 3; ++i) { + auto* obj = scene.add_scene_objects(); + obj->set_name("Obj" + std::to_string(i)); + obj->set_is_visible(true); + obj->add_components()->mutable_blinker()->set_blink_frequency_hz( + static_cast(i + 1)); + } + auto result = utils::parseProtoScene(scene); + for (const auto& obj : result->getObjects()) { + EXPECT_EQ(obj->components.size(), 1u); + } +} + +// Grupa: Parser -- scenariusze brzegowe +TEST(ParserEdgeCaseTest, ObjectWithEmptyNameIsParsed) { + NeuronIDE::Scene scene; + scene.set_project_name("EmptyObjName"); + auto* obj = scene.add_scene_objects(); + obj->set_name(""); + obj->set_is_visible(true); + + auto result = utils::parseProtoScene(scene); + ASSERT_EQ(result->getObjects().size(), 1u); + EXPECT_EQ(result->getObjects()[0]->name, ""); +} + +TEST(ParserEdgeCaseTest, LargeNumberOfObjectsIsHandled) { + NeuronIDE::Scene scene; + scene.set_project_name("LargeScene"); + const int N = 500; + for (int i = 0; i < N; ++i) { + auto* obj = scene.add_scene_objects(); + obj->set_name("O" + std::to_string(i)); + obj->set_is_visible(i % 2 == 0); + obj->add_components()->mutable_blinker()->set_blink_frequency_hz(static_cast(i)); + } + auto result = utils::parseProtoScene(scene); + EXPECT_EQ(static_cast(result->getObjects().size()), N); +} + +TEST(ParserEdgeCaseTest, BlinkFrequencyZeroIsValid) { + auto scene = utils::buildSimpleScene("P", "ZeroHz", true, 0.0); + auto result = utils::parseProtoScene(scene); + ASSERT_EQ(result->getObjects().size(), 1u); + EXPECT_EQ(result->getObjects()[0]->components.size(), 1u); +} + +TEST(ParserEdgeCaseTest, ParseReturnsDifferentObjectEachCall) { + auto scene = utils::buildSimpleScene(); + std::stringstream ss1; + scene.SerializeToOstream(&ss1); + std::stringstream ss2; + scene.SerializeToOstream(&ss2); + + Parser parser; + auto r1 = parser.parseStream(ss1); + auto r2 = parser.parseStream(ss2); + EXPECT_NE(r1.get(), r2.get()); +} \ No newline at end of file diff --git a/tests/utils/ParserTestUtils.hpp b/tests/utils/ParserTestUtils.hpp new file mode 100644 index 0000000..0f1c391 --- /dev/null +++ b/tests/utils/ParserTestUtils.hpp @@ -0,0 +1,40 @@ +#ifndef PARSER_TEST_UTILS_HPP +#define PARSER_TEST_UTILS_HPP + +#include +#include +#include + +#include "neuronide.pb.h" +#include "parser/Parser.hpp" +#include "scene/Scene.hpp" + +namespace utils { + +inline NeuronIDE::Scene buildSimpleScene(const std::string& projectName = "TestProject", + const std::string& objectName = "ObiektA", + bool isVisible = true, double blinkFrequency = 1.5) { + NeuronIDE::Scene scene; + scene.set_project_name(projectName); + + auto* obj = scene.add_scene_objects(); + obj->set_name(objectName); + obj->set_is_visible(isVisible); + + auto* comp = obj->add_components(); + auto* blinker = comp->mutable_blinker(); + blinker->set_blink_frequency_hz(blinkFrequency); + + return scene; +} + +inline std::shared_ptr parseProtoScene(const NeuronIDE::Scene& scene) { + std::stringstream ss; + scene.SerializeToOstream(&ss); + Parser parser; + return parser.parseStream(ss); +} + +} // namespace utils + +#endif // PARSER_TEST_UTILS_HPP