diff --git a/CMakeLists.txt b/CMakeLists.txt index 8aca0d680..6c530e9cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,9 @@ # The version number. set(AGENT_VERSION_MAJOR 2) -set(AGENT_VERSION_MINOR 6) +set(AGENT_VERSION_MINOR 7) set(AGENT_VERSION_PATCH 0) -set(AGENT_VERSION_BUILD 7) -set(AGENT_VERSION_RC "") +set(AGENT_VERSION_BUILD 1) +set(AGENT_VERSION_RC "RC1") # This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent cmake_minimum_required(VERSION 3.23 FATAL_ERROR) diff --git a/agent_lib/CMakeLists.txt b/agent_lib/CMakeLists.txt index 6c22ca22a..4af4e1b36 100644 --- a/agent_lib/CMakeLists.txt +++ b/agent_lib/CMakeLists.txt @@ -27,7 +27,11 @@ set(AGENT_SOURCES "${SOURCE_DIR}/asset/component_configuration_parameters.hpp" "${SOURCE_DIR}/asset/physical_asset.hpp" "${SOURCE_DIR}/asset/fixture.hpp" + "${SOURCE_DIR}/asset/part.hpp" + "${SOURCE_DIR}/asset/process.hpp" "${SOURCE_DIR}/asset/pallet.hpp" + "${SOURCE_DIR}/asset/target.hpp" + "${SOURCE_DIR}/asset/task.hpp" # src/asset SOURCE_FILES_ONLY @@ -39,7 +43,11 @@ set(AGENT_SOURCES "${SOURCE_DIR}/asset/component_configuration_parameters.cpp" "${SOURCE_DIR}/asset/physical_asset.cpp" "${SOURCE_DIR}/asset/fixture.cpp" + "${SOURCE_DIR}/asset/part.cpp" + "${SOURCE_DIR}/asset/process.cpp" "${SOURCE_DIR}/asset/pallet.cpp" + "${SOURCE_DIR}/asset/target.cpp" + "${SOURCE_DIR}/asset/task.cpp" # src/buffer HEADER_FILES_ONLY diff --git a/src/mtconnect/agent.cpp b/src/mtconnect/agent.cpp index 7f27ad008..f34205c1a 100644 --- a/src/mtconnect/agent.cpp +++ b/src/mtconnect/agent.cpp @@ -48,8 +48,11 @@ #include "mtconnect/asset/file_asset.hpp" #include "mtconnect/asset/fixture.hpp" #include "mtconnect/asset/pallet.hpp" +#include "mtconnect/asset/part.hpp" +#include "mtconnect/asset/process.hpp" #include "mtconnect/asset/qif_document.hpp" #include "mtconnect/asset/raw_material.hpp" +#include "mtconnect/asset/task.hpp" #include "mtconnect/configuration/config_options.hpp" #include "mtconnect/device_model/agent_device.hpp" #include "mtconnect/entity/xml_parser.hpp" @@ -102,6 +105,12 @@ namespace mtconnect { ComponentConfigurationParameters::registerAsset(); Pallet::registerAsset(); Fixture::registerAsset(); + Process::registerAsset(); + ProcessArchetype::registerAsset(); + Part::registerAsset(); + PartArchetype::registerAsset(); + Task::registerAsset(); + TaskArchetype::registerAsset(); m_assetStorage = make_unique( GetOption(options, mtconnect::configuration::MaxAssets).value_or(1024)); diff --git a/src/mtconnect/asset/asset.cpp b/src/mtconnect/asset/asset.cpp index 92422d010..c30926825 100644 --- a/src/mtconnect/asset/asset.cpp +++ b/src/mtconnect/asset/asset.cpp @@ -15,11 +15,13 @@ // limitations under the License. // -#include "mtconnect/asset/asset.hpp" +#include "asset.hpp" #include #include +#include "mtconnect/device_model/configuration/configuration.hpp" + using namespace std; namespace mtconnect { @@ -27,11 +29,13 @@ namespace mtconnect { namespace asset { FactoryPtr Asset::getFactory() { + using namespace device_model::configuration; static auto asset = make_shared( - Requirements({Requirement("assetId", false), Requirement("deviceUuid", false), - Requirement("timestamp", ValueType::TIMESTAMP, false), - Requirement("hash", false), - Requirement("removed", ValueType::BOOL, false)}), + Requirements( + {Requirement("assetId", false), Requirement("deviceUuid", false), + Requirement("timestamp", ValueType::TIMESTAMP, false), Requirement("hash", false), + Requirement("Configuration", ValueType::ENTITY, Configuration::getFactory(), false), + Requirement("removed", ValueType::BOOL, false)}), [](const std::string &name, Properties &props) -> EntityPtr { return make_shared(name, props); }); diff --git a/src/mtconnect/asset/part.cpp b/src/mtconnect/asset/part.cpp new file mode 100644 index 000000000..ac642438c --- /dev/null +++ b/src/mtconnect/asset/part.cpp @@ -0,0 +1,108 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "part.hpp" + +using namespace std; + +namespace mtconnect { + using namespace entity; + namespace asset { + FactoryPtr PartArchetype::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + auto ext = make_shared(); + ext->registerFactory(regex(".+"), ext); + ext->setAny(true); + ext->setList(true); + + auto customer = make_shared(Requirements { + {"customerId", true}, + {"name", false}, + {"Address", false}, + {"Description", false}, + }); + + auto customers = make_shared(Requirements { + {"Customer", ValueType::ENTITY, customer, 1, entity::Requirement::Infinite}}); + + factory = make_shared(*Asset::getFactory()); + factory->addRequirements({ + {"revision", true}, + {"family", false}, + {"drawing", false}, + {"Customers", ValueType::ENTITY_LIST, customers, false}, + }); + factory->registerFactory(regex(".+"), ext); + factory->setAny(true); + } + return factory; + } + + void PartArchetype::registerAsset() + { + static bool once {true}; + if (once) + { + Asset::registerAssetType("PartArchetype", getFactory()); + once = false; + } + } + + FactoryPtr Part::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + auto ext = make_shared(); + ext->registerFactory(regex(".+"), ext); + ext->setAny(true); + ext->setList(true); + + auto identifier = make_shared( + Requirements {{"type", ControlledVocab {"UNIQUE_IDENTIFIER", "GROUP_IDENTIFIER"}, true}, + {"stepIdRef", false}, + {"timestamp", ValueType::TIMESTAMP, true}, + {"VALUE", true}}); + + auto identifiers = make_shared(Requirements { + {"Identifier", ValueType::ENTITY, identifier, 1, entity::Requirement::Infinite}}); + + factory = make_shared(*Asset::getFactory()); + factory->addRequirements({{"revision", true}, + {"family", false}, + {"drawing", false}, + {"PartIdentifiers", ValueType::ENTITY_LIST, identifiers, false}}); + factory->registerFactory(regex(".+"), ext); + factory->setAny(true); + } + return factory; + } + + void Part::registerAsset() + { + static bool once {true}; + if (once) + { + Asset::registerAssetType("Part", getFactory()); + once = false; + } + } + } // namespace asset +} // namespace mtconnect diff --git a/src/mtconnect/asset/part.hpp b/src/mtconnect/asset/part.hpp new file mode 100644 index 000000000..6036ce19a --- /dev/null +++ b/src/mtconnect/asset/part.hpp @@ -0,0 +1,44 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include +#include +#include + +#include "asset.hpp" +#include "mtconnect/entity/factory.hpp" +#include "mtconnect/utilities.hpp" + +namespace mtconnect::asset { + /// @brief Manufacturing process archetype asset + class AGENT_LIB_API PartArchetype : public Asset + { + public: + static entity::FactoryPtr getFactory(); + static void registerAsset(); + }; + + /// @brief Manufacturing process asset + class AGENT_LIB_API Part : public Asset + { + public: + static entity::FactoryPtr getFactory(); + static void registerAsset(); + }; +} // namespace mtconnect::asset diff --git a/src/mtconnect/asset/process.cpp b/src/mtconnect/asset/process.cpp new file mode 100644 index 000000000..4154cf4c4 --- /dev/null +++ b/src/mtconnect/asset/process.cpp @@ -0,0 +1,113 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "process.hpp" + +#include "target.hpp" + +using namespace std; + +namespace mtconnect { + using namespace entity; + namespace asset { + FactoryPtr ProcessArchetype::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + auto activity = + make_shared(Requirements {{"sequence", ValueType::INTEGER, false}, + {"activityId", true}, + {"precedence", ValueType::INTEGER, false}, + {"optional", ValueType::BOOL, false}, + {"Description", false}}); + + auto activityGroup = make_shared(Requirements { + {"activityGroupId", true}, + {"name", false}, + {"Activity", ValueType::ENTITY, activity, 1, entity::Requirement::Infinite}, + }); + + auto activityGroups = make_shared(Requirements { + {"ActivityGroup", ValueType::ENTITY, activityGroup, 1, entity::Requirement::Infinite}}); + + auto processStep = make_shared( + Requirements {{{"stepId", true}, + {"optional", ValueType::BOOL, false}, + {"sequence", ValueType::INTEGER, false}, + {"Description", false}, + {"StartTime", ValueType::TIMESTAMP, false}, + {"Duration", ValueType::DOUBLE, false}, + {"Targets", ValueType::ENTITY_LIST, Target::getTargetsFactory(), false}, + {"ActivityGroups", ValueType::ENTITY_LIST, activityGroups, false}}}); + processStep->setOrder( + {"Description", "StartTime", "Duration", "Targets", "ActivityGroups"}); + + auto routing = make_shared(Requirements { + {"precedence", ValueType::INTEGER, true}, + {"routingId", true}, + {"ProcessStep", ValueType::ENTITY, processStep, 1, entity::Requirement::Infinite}, + }); + + auto routings = make_shared(Requirements { + {"Routing", ValueType::ENTITY, routing, 1, entity::Requirement::Infinite}}); + + factory = make_shared(*Asset::getFactory()); + factory->addRequirements({ + {"revision", true}, + {"Targets", ValueType::ENTITY_LIST, Target::getTargetsFactory(), false}, + {"Routings", ValueType::ENTITY_LIST, routings, true}, + }); + factory->setOrder({"Configuration", "Routings", "Targets"}); + } + return factory; + } + + void ProcessArchetype::registerAsset() + { + static bool once {true}; + if (once) + { + Asset::registerAssetType("ProcessArchetype", getFactory()); + once = false; + } + } + + FactoryPtr Process::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + factory = ProcessArchetype::getFactory()->deepCopy(); + auto routings = factory->getRequirement("Routings"); + auto routing = routings->getFactory()->getRequirement("Routing"); + routing->setMultiplicity(1, 1); + } + return factory; + } + + void Process::registerAsset() + { + static bool once {true}; + if (once) + { + Asset::registerAssetType("Process", getFactory()); + once = false; + } + } + } // namespace asset +} // namespace mtconnect diff --git a/src/mtconnect/asset/process.hpp b/src/mtconnect/asset/process.hpp new file mode 100644 index 000000000..ccc3a43e9 --- /dev/null +++ b/src/mtconnect/asset/process.hpp @@ -0,0 +1,44 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include +#include +#include + +#include "asset.hpp" +#include "mtconnect/entity/factory.hpp" +#include "mtconnect/utilities.hpp" + +namespace mtconnect::asset { + /// @brief Manufacturing process archetype asset + class AGENT_LIB_API ProcessArchetype : public Asset + { + public: + static entity::FactoryPtr getFactory(); + static void registerAsset(); + }; + + /// @brief Manufacturing process asset + class AGENT_LIB_API Process : public Asset + { + public: + static entity::FactoryPtr getFactory(); + static void registerAsset(); + }; +} // namespace mtconnect::asset diff --git a/src/mtconnect/asset/target.cpp b/src/mtconnect/asset/target.cpp new file mode 100644 index 000000000..9dc916ad3 --- /dev/null +++ b/src/mtconnect/asset/target.cpp @@ -0,0 +1,164 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "target.hpp" + +using namespace std; + +namespace mtconnect { + using namespace entity; + namespace asset { + FactoryPtr Target::getFactory() + { + static FactoryPtr target = make_shared(); + return target; + } + + FactoryPtr Target::getDeviceTargetsFactory() + { + static FactoryPtr targets; + if (!targets) + { + targets = make_shared(entity::Requirements { + entity::Requirement("TargetDevice", ValueType::ENTITY, TargetDevice::getFactory(), 0, + entity::Requirement::Infinite), + entity::Requirement("TargetGroup", ValueType::ENTITY_LIST, TargetGroup::getFactory(), 0, + entity::Requirement::Infinite)}); + + targets->registerMatchers(); + targets->setMinListSize(1); + } + + return targets; + } + + FactoryPtr Target::getTargetsFactory() + { + static FactoryPtr targets; + if (!targets) + { + targets = make_shared(entity::Requirements { + entity::Requirement("TargetDevice", ValueType::ENTITY, TargetDevice::getFactory(), 0, + entity::Requirement::Infinite), + entity::Requirement("TargetGroup", ValueType::ENTITY_LIST, TargetGroup::getFactory(), 0, + entity::Requirement::Infinite), + entity::Requirement("TargetRef", ValueType::ENTITY, TargetRef::getFactory(), 0, + entity::Requirement::Infinite)}); + + targets->registerMatchers(); + targets->setMinListSize(1); + } + + return targets; + } + + FactoryPtr Target::getAllTargetsFactory() + { + static FactoryPtr targets; + if (!targets) + { + targets = make_shared(entity::Requirements { + entity::Requirement("TargetDevice", ValueType::ENTITY, TargetDevice::getFactory(), 0, + entity::Requirement::Infinite), + entity::Requirement("TargetGroup", ValueType::ENTITY_LIST, TargetGroup::getFactory(), 0, + entity::Requirement::Infinite), + entity::Requirement("TargetRef", ValueType::ENTITY, TargetRef::getFactory(), 0, + entity::Requirement::Infinite), + entity::Requirement("TargetRequirementTable", ValueType::ENTITY, + TargetRequirement::getFactory(), 0, + entity::Requirement::Infinite)}); + + targets->registerMatchers(); + targets->setMinListSize(1); + } + + return targets; + } + + FactoryPtr Target::getRequirementTargetsFactory() + { + static FactoryPtr targets; + if (!targets) + { + targets = make_shared(entity::Requirements {entity::Requirement( + "TargetRequirementTable", ValueType::ENTITY, TargetRequirement::getFactory(), 0, + entity::Requirement::Infinite)}); + + targets->registerMatchers(); + targets->setMinListSize(1); + } + + return targets; + } + + FactoryPtr TargetDevice::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + factory = make_shared(*Target::getFactory()); + factory->addRequirements({{"deviceUuid", true}}); + } + + return factory; + } + + FactoryPtr TargetRef::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + factory = make_shared(*Target::getFactory()); + factory->addRequirements({{"groupIdRef", true}}); + } + + return factory; + } + + FactoryPtr TargetGroup::getFactory() + { + using namespace entity; + static FactoryPtr factory; + if (!factory) + { + factory = make_shared(*Target::getFactory()); + factory->addRequirements({{"groupId", true}, + {"TargetDevice", ValueType::ENTITY, TargetDevice::getFactory(), 0, + entity::Requirement::Infinite}, + {"TargetRef", ValueType::ENTITY, TargetRef::getFactory(), 0, + entity::Requirement::Infinite}}); + factory->registerMatchers(); + factory->setMinListSize(1); + } + + return factory; + } + + FactoryPtr TargetRequirement::getFactory() + { + using namespace entity; + static FactoryPtr factory; + if (!factory) + { + factory = make_shared(*Target::getFactory()); + factory->addRequirements({{"requirementId", true}, {"VALUE", ValueType::TABLE, true}}); + } + + return factory; + } + } // namespace asset +} // namespace mtconnect diff --git a/src/mtconnect/asset/target.hpp b/src/mtconnect/asset/target.hpp new file mode 100644 index 000000000..43e52fb92 --- /dev/null +++ b/src/mtconnect/asset/target.hpp @@ -0,0 +1,69 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include +#include +#include + +#include "asset.hpp" +#include "mtconnect/entity/entity.hpp" +#include "mtconnect/entity/factory.hpp" +#include "mtconnect/utilities.hpp" + +namespace mtconnect::asset { + /// @brief A target of a process or a task + class AGENT_LIB_API Target : public entity::Entity + { + public: + static entity::FactoryPtr getFactory(); + static entity::FactoryPtr getDeviceTargetsFactory(); + static entity::FactoryPtr getTargetsFactory(); + static entity::FactoryPtr getAllTargetsFactory(); + static entity::FactoryPtr getRequirementTargetsFactory(); + }; + + /// @brief A group of possible targets + class AGENT_LIB_API TargetGroup : public Target + { + public: + static entity::FactoryPtr getFactory(); + }; + + /// @brief A device target where the `targetId` is the device UUID + class AGENT_LIB_API TargetDevice : public Target + { + public: + static entity::FactoryPtr getFactory(); + }; + + /// @brief A reference to a target group + class AGENT_LIB_API TargetRef : public Target + { + public: + static entity::FactoryPtr getFactory(); + }; + + /// @brief A requirement for a target + class AGENT_LIB_API TargetRequirement : public Target + { + public: + static entity::FactoryPtr getFactory(); + }; + +} // namespace mtconnect::asset diff --git a/src/mtconnect/asset/task.cpp b/src/mtconnect/asset/task.cpp new file mode 100644 index 000000000..b85f67305 --- /dev/null +++ b/src/mtconnect/asset/task.cpp @@ -0,0 +1,135 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "task.hpp" + +#include "target.hpp" + +using namespace std; + +namespace mtconnect { + using namespace entity; + namespace asset { + FactoryPtr TaskArchetype::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + auto collaborator = make_shared(Requirements { + {"collaboratorId", true}, + {"type", false}, + {"optional", ValueType::BOOL, false}, + {"Targets", ValueType::ENTITY_LIST, Target::getAllTargetsFactory(), true}}); + + auto collaborators = make_shared(Requirements { + {"Collaborator", ValueType::ENTITY, collaborator, 1, entity::Requirement::Infinite}}); + auto coordinator = make_shared( + Requirements {{"Collaborator", ValueType::ENTITY, collaborator, true}}); + + auto ext = make_shared(); + ext->registerFactory(regex(".+"), ext); + ext->setAny(true); + ext->setList(true); + + auto subTaskRef = make_shared(Requirements {{"order", ValueType::INTEGER, true}, + {"parallel", ValueType::BOOL, false}, + {"optional", ValueType::BOOL, false}, + {"group", false}, + {"VALUE", true}}); + + auto subTaskRefs = make_shared(Requirements { + {"SubTaskRef", ValueType::ENTITY, subTaskRef, 1, entity::Requirement::Infinite}}); + + factory = make_shared(*Asset::getFactory()); + factory->addRequirements( + {{"TaskType", true}, + {"Priority", ValueType::INTEGER, false}, + {"Coordinator", ValueType::ENTITY, coordinator, true}, + {"Collaborators", ValueType::ENTITY_LIST, collaborators, true}, + {"Targets", ValueType::ENTITY_LIST, Target::getAllTargetsFactory(), false}, + {"SubTaskRefs", ValueType::ENTITY_LIST, subTaskRefs, false}}); + factory->setOrder({{"Configuration", "TaskType", "Priority", "Targets", "Coordinator", + "Collaborators", "SubTaskREfs"}}); + factory->registerFactory(regex(".+"), ext); + factory->setAny(true); + } + return factory; + } + + void TaskArchetype::registerAsset() + { + static bool once {true}; + if (once) + { + Asset::registerAssetType("TaskArchetype", getFactory()); + once = false; + } + } + + FactoryPtr Task::getFactory() + { + static FactoryPtr factory; + if (!factory) + { + auto collaborator = make_shared(Requirements {{"collaboratorId", true}, + {"type", false}, + {"collaboratorDeviceUuid", false}, + {"requirementId", false}}); + + auto collaborators = make_shared(Requirements { + {"Collaborator", ValueType::ENTITY, collaborator, 1, entity::Requirement::Infinite}}); + auto coordinator = make_shared( + Requirements {{"Collaborator", ValueType::ENTITY, collaborator, true}}); + + auto ext = make_shared(); + ext->registerFactory(regex(".+"), ext); + ext->setAny(true); + ext->setList(true); + + factory = make_shared(*Asset::getFactory()); + factory->addRequirements({{"TaskType", true}, + {"TaskState", + ControlledVocab {"INACTIVE", "PREPARING", "COMMITTING", + "COMMITTED", "COMPLETE", "FAIL"}, + true}, + {"ParentTaskAssetId", false}, + {"TaskArchetypeAssetId", false}, + {"Coordinator", ValueType::ENTITY, coordinator, true}, + {"Collaborators", ValueType::ENTITY_LIST, collaborators, true}}); + auto task = make_shared( + Requirements {{"Task", ValueType::ENTITY, factory, 1, entity::Requirement::Infinite}}); + + factory->addRequirements({{"SubTasks", ValueType::ENTITY_LIST, task, false}}); + factory->setOrder({"Configuration", "TaskType", "TaskState", "ParentTaskAssetId", + "TaskArchetypeAssetId", "Coordinator", "Collaborators", "SubTasks"}); + factory->registerFactory(regex(".+"), ext); + factory->setAny(true); + } + return factory; + } + + void Task::registerAsset() + { + static bool once {true}; + if (once) + { + Asset::registerAssetType("Task", getFactory()); + once = false; + } + } + } // namespace asset +} // namespace mtconnect diff --git a/src/mtconnect/asset/task.hpp b/src/mtconnect/asset/task.hpp new file mode 100644 index 000000000..3967dae59 --- /dev/null +++ b/src/mtconnect/asset/task.hpp @@ -0,0 +1,44 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#include +#include +#include + +#include "asset.hpp" +#include "mtconnect/entity/factory.hpp" +#include "mtconnect/utilities.hpp" + +namespace mtconnect::asset { + /// @brief Manufacturing process archetype asset + class AGENT_LIB_API TaskArchetype : public Asset + { + public: + static entity::FactoryPtr getFactory(); + static void registerAsset(); + }; + + /// @brief Manufacturing process asset + class AGENT_LIB_API Task : public Asset + { + public: + static entity::FactoryPtr getFactory(); + static void registerAsset(); + }; +} // namespace mtconnect::asset diff --git a/src/mtconnect/entity/data_set.hpp b/src/mtconnect/entity/data_set.hpp index 671768b19..252ca949f 100644 --- a/src/mtconnect/entity/data_set.hpp +++ b/src/mtconnect/entity/data_set.hpp @@ -130,7 +130,7 @@ namespace mtconnect::entity { }; /// @brief A set of data set entries - /// @tparam EV the entry type for the set, must have < operator. + /// @tparam ET the entry type for the set, must have < operator. template class Set : public std::set { diff --git a/src/mtconnect/entity/factory.hpp b/src/mtconnect/entity/factory.hpp index 2f9474721..99933eff9 100644 --- a/src/mtconnect/entity/factory.hpp +++ b/src/mtconnect/entity/factory.hpp @@ -148,6 +148,14 @@ namespace mtconnect { /// @return `true` if this is a table bool isTable(const std::string &name) const { return m_tables.count(name) > 0; } + /// @brief is the value of this entity a data set or table + /// @returns `true` if the value is a data set or table + bool isValueDataSet() const { return m_isValueDataSet; } + + /// @brief is the value of this entity a table + /// @returns `true` if the value is a table + bool isValueTable() const { return m_isValueTable; } + /// @brief get the requirement pointer for a key /// @param name the property key /// @return requirement pointer @@ -390,9 +398,15 @@ namespace mtconnect { } else if (BaseValueType(r.getType()) == ValueType::DATA_SET) { + if (r.getName() == "VALUE") + m_isValueDataSet = true; m_dataSets.insert(r.getName()); if (r.getType() == ValueType::TABLE) + { m_tables.insert(r.getName()); + if (r.getName() == "VALUE") + m_isValueTable = true; + } } else { @@ -443,6 +457,8 @@ namespace mtconnect { size_t m_minListSize {0}; bool m_hasRaw {false}; bool m_any {false}; + bool m_isValueDataSet {false}; + bool m_isValueTable {false}; std::set m_propertySets; std::set m_dataSets; diff --git a/src/mtconnect/entity/json_printer.hpp b/src/mtconnect/entity/json_printer.hpp index 47e64f648..3dc3216c9 100644 --- a/src/mtconnect/entity/json_printer.hpp +++ b/src/mtconnect/entity/json_printer.hpp @@ -47,8 +47,15 @@ namespace mtconnect::entity { bool isPropertyList = *m_key != "LIST"; if (m_entity->hasListWithAttribute()) { - m_obj->Key("list"); - m_printer.printEntityList(arg); + if (m_printer.m_version == 1) + { + m_obj->Key("list"); + m_printer.printEntityList(arg); + } + else + { + m_printer.printEntityList(arg, true); + } } else if (isPropertyList) { @@ -147,12 +154,12 @@ namespace mtconnect::entity { /// @param[in] list a list of EntityPtr objects /// @tparam T2 Type of iterable collection must contain Entity subclass template - void printEntityList(const T2 &list) + void printEntityList(const T2 &list, bool embed = false) { if (m_version == 1) printEntityList1(list); else if (m_version == 2) - printEntityList2(list); + printEntityList2(list, embed); else throw std::runtime_error("Invalid json printer version"); } @@ -184,9 +191,9 @@ namespace mtconnect::entity { /// @param[in] list a list of EntityPtr objects /// @tparam T2 Type of iterable collection must contain Entity subclass template - void printEntityList2(const T2 &list) + void printEntityList2(const T2 &list, bool embed = false) { - AutoJsonObject obj(m_writer); + AutoJsonObject obj(m_writer, !embed); // Sort the entities by name, use a string view so we don't copy std::multimap entities; for (auto &e : list) diff --git a/src/mtconnect/entity/xml_parser.cpp b/src/mtconnect/entity/xml_parser.cpp index 0a88a6269..a6e146bdd 100644 --- a/src/mtconnect/entity/xml_parser.cpp +++ b/src/mtconnect/entity/xml_parser.cpp @@ -225,6 +225,11 @@ namespace mtconnect::entity { if (holds_alternative(value)) properties.insert({"RAW", value}); } + else if (ef->isValueDataSet()) + { + auto ds = &properties["VALUE"].emplace(); + parseDataSet(node, *ds, ef->isValueTable()); + } else { int orderCount = 0; diff --git a/src/mtconnect/utilities.hpp b/src/mtconnect/utilities.hpp index dff98f5d8..0b10cc783 100644 --- a/src/mtconnect/utilities.hpp +++ b/src/mtconnect/utilities.hpp @@ -241,14 +241,15 @@ namespace mtconnect { /// @brief Thread safe localtime function that uses localtime_s or localtime_r based on platform /// @param[in] timer pointer to time_t /// @param[out] buf pointer to tm struct to fill - inline void safe_localtime(const std::time_t* timer, std::tm* buf) { + inline void safe_localtime(const std::time_t *timer, std::tm *buf) + { #ifdef _WINDOWS localtime_s(buf, timer); #else localtime_r(timer, buf); #endif } - + /// @brief Formats the timePoint as string given the format /// @param[in] timePoint the time /// @param[in] format the format diff --git a/test_package/CMakeLists.txt b/test_package/CMakeLists.txt index 2dc65edb9..b93a366fa 100644 --- a/test_package/CMakeLists.txt +++ b/test_package/CMakeLists.txt @@ -221,6 +221,10 @@ add_agent_test(asset_hash TRUE asset) add_agent_test(physical_asset FALSE asset) add_agent_test(pallet FALSE asset) add_agent_test(fixture FALSE asset) +add_agent_test(target FALSE asset) +add_agent_test(process FALSE asset) +add_agent_test(part FALSE asset) +add_agent_test(task FALSE asset) add_agent_test(agent_device TRUE device_model) add_agent_test(component FALSE device_model) diff --git a/test_package/agent_asset_test.cpp b/test_package/agent_asset_test.cpp index bb934e015..36b8d9843 100644 --- a/test_package/agent_asset_test.cpp +++ b/test_package/agent_asset_test.cpp @@ -95,10 +95,10 @@ TEST_F(AgentAssetTest, should_store_assets_in_buffer) auto rest = m_agentTestHelper->getRestService(); ASSERT_TRUE(rest->getServer()->arePutsAllowed()); - string body = "TEST"; + string body = "TEST"; QueryMap queries; - queries["type"] = "Part"; + queries["type"] = "FakeAsset"; queries["device"] = "LinuxCNC"; ASSERT_EQ((unsigned int)4, agent->getAssetStorage()->getMaxAssets()); @@ -113,14 +113,14 @@ TEST_F(AgentAssetTest, should_store_assets_in_buffer) PARSE_XML_RESPONSE("/asset/123"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetBufferSize", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST"); } // The device should generate an asset changed event as well. { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:AssetChanged", "123"); - ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:AssetChanged@assetType", "FakeAsset"); } } @@ -130,10 +130,10 @@ TEST_F(AgentAssetTest, should_store_assets_in_buffer_and_generate_asset_added) auto rest = m_agentTestHelper->getRestService(); ASSERT_TRUE(rest->getServer()->arePutsAllowed()); - string body = "TEST"; + string body = "TEST"; QueryMap queries; - queries["type"] = "Part"; + queries["type"] = "FakeAsset"; queries["device"] = "LinuxCNC"; ASSERT_EQ((unsigned int)4, agent->getAssetStorage()->getMaxAssets()); @@ -148,25 +148,25 @@ TEST_F(AgentAssetTest, should_store_assets_in_buffer_and_generate_asset_added) PARSE_XML_RESPONSE("/asset/123"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetBufferSize", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST"); } // The device should generate an asset added event as well. { PARSE_XML_RESPONSE("/LinuxCNC/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:AssetAdded", "123"); - ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:AssetAdded@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:DeviceStream//m:AssetAdded@assetType", "FakeAsset"); } } TEST_F(AgentAssetTest, should_handle_asset_buffer_and_buffer_limits) { auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "1.3", 4, true); - string body = "TEST 1"; + string body = "TEST 1"; QueryMap queries; queries["device"] = "000"; - queries["type"] = "Part"; + queries["type"] = "FakeAsset"; const auto &storage = agent->getAssetStorage(); @@ -176,51 +176,51 @@ TEST_F(AgentAssetTest, should_handle_asset_buffer_and_buffer_limits) { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)1, storage->getCount()); - ASSERT_EQ(1, storage->getCountForType("Part")); + ASSERT_EQ(1, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 1"); } // Make sure replace works properly { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)1, storage->getCount()); - ASSERT_EQ(1, storage->getCountForType("Part")); + ASSERT_EQ(1, storage->getCountForType("FakeAsset")); } - body = "TEST 2"; + body = "TEST 2"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)2, storage->getCount()); - ASSERT_EQ(2, storage->getCountForType("Part")); + ASSERT_EQ(2, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P2"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "2"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 2"); } - body = "TEST 3"; + body = "TEST 3"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)3, storage->getCount()); - ASSERT_EQ(3, storage->getCountForType("Part")); + ASSERT_EQ(3, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P3"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "3"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 3"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 3"); } - body = "TEST 4"; + body = "TEST 4"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); @@ -230,52 +230,52 @@ TEST_F(AgentAssetTest, should_handle_asset_buffer_and_buffer_limits) { PARSE_XML_RESPONSE("/asset/P4"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 4"); - ASSERT_EQ(4, storage->getCountForType("Part")); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 4"); + ASSERT_EQ(4, storage->getCountForType("FakeAsset")); } // Test multiple asset get { PARSE_XML_RESPONSE("/assets"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[4]", "TEST 1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[3]", "TEST 2"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[2]", "TEST 3"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[1]", "TEST 4"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[4]", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[3]", "TEST 2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[2]", "TEST 3"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[1]", "TEST 4"); } // Test multiple asset get with filter { PARSE_XML_RESPONSE_QUERY("/asset", queries); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[4]", "TEST 4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[3]", "TEST 3"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[2]", "TEST 2"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[1]", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[4]", "TEST 4"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[3]", "TEST 3"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[2]", "TEST 2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[1]", "TEST 1"); } queries["count"] = "2"; { PARSE_XML_RESPONSE_QUERY("/assets", queries); ASSERT_XML_PATH_COUNT(doc, "//m:Assets/*", 2); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[1]", "TEST 1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part[2]", "TEST 2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[1]", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset[2]", "TEST 2"); } queries.erase("count"); - body = "TEST 5"; + body = "TEST 5"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)4, storage->getCount()); - ASSERT_EQ(4, storage->getCountForType("Part")); + ASSERT_EQ(4, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P5"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 5"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 5"); } { @@ -284,46 +284,46 @@ TEST_F(AgentAssetTest, should_handle_asset_buffer_and_buffer_limits) ASSERT_XML_PATH_EQUAL(doc, "//m:MTConnectError/m:Errors/m:Error", "Cannot find asset: P1"); } - body = "TEST 6"; + body = "TEST 6"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)4, storage->getCount()); - ASSERT_EQ(4, storage->getCountForType("Part")); + ASSERT_EQ(4, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P3"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 6"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 6"); } { PARSE_XML_RESPONSE("/asset/P2"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 2"); } - body = "TEST 7"; + body = "TEST 7"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)4, storage->getCount()); - ASSERT_EQ(4, storage->getCountForType("Part")); + ASSERT_EQ(4, storage->getCountForType("FakeAsset")); } - body = "TEST 8"; + body = "TEST 8"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); ASSERT_EQ((unsigned int)4, storage->getCount()); - ASSERT_EQ(4, storage->getCountForType("Part")); + ASSERT_EQ(4, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P6"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "4"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 8"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 8"); } // Now since two and three have been modified, asset 4 should be removed. @@ -383,14 +383,14 @@ TEST_F(AgentAssetTest, should_handle_asset_from_adapter_on_one_line) const auto &storage = agent->getAssetStorage(); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); ASSERT_EQ((unsigned int)1, storage->getCount()); { PARSE_XML_RESPONSE("/asset/P1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 1"); } } @@ -401,14 +401,14 @@ TEST_F(AgentAssetTest, should_handle_multiline_asset) const auto &storage = agent->getAssetStorage(); m_agentTestHelper->m_adapter->parseBuffer( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|--multiline--AAAA\n"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|--multiline--AAAA\n"); m_agentTestHelper->m_adapter->parseBuffer( - "\n" + "\n" " TEST 1\n" " Some Text\n" " XXX\n"); m_agentTestHelper->m_adapter->parseBuffer( - "\n" + "\n" "--multiline--AAAA\n"); ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); ASSERT_EQ((unsigned int)1, storage->getCount()); @@ -416,11 +416,11 @@ TEST_F(AgentAssetTest, should_handle_multiline_asset) { PARSE_XML_RESPONSE("/asset/P1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part/m:PartXXX", "TEST 1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part/m:Extra", "XXX"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@assetId", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@deviceUuid", "000"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@timestamp", "2021-02-01T12:00:00Z"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset/m:PartXXX", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset/m:Extra", "XXX"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@assetId", "P1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@deviceUuid", "000"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@timestamp", "2021-02-01T12:00:00Z"); } // Make sure we can still add a line and we are out of multiline mode... @@ -446,11 +446,11 @@ TEST_F(AgentAssetTest, should_handle_bad_asset_from_adapter) TEST_F(AgentAssetTest, should_handle_asset_removal_from_REST_api) { - string body = "TEST 1"; + string body = "TEST 1"; QueryMap query; query["device"] = "LinuxCNC"; - query["type"] = "Part"; + query["type"] = "FakeAsset"; const auto &storage = m_agentTestHelper->m_agent->getAssetStorage(); @@ -460,62 +460,62 @@ TEST_F(AgentAssetTest, should_handle_asset_removal_from_REST_api) { PARSE_XML_RESPONSE_PUT("/asset", body, query); ASSERT_EQ((unsigned int)1, storage->getCount()); - ASSERT_EQ(1, storage->getCountForType("Part")); + ASSERT_EQ(1, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 1"); } // Make sure replace works properly { PARSE_XML_RESPONSE_PUT("/asset", body, query); ASSERT_EQ((unsigned int)1, storage->getCount()); - ASSERT_EQ(1, storage->getCountForType("Part")); + ASSERT_EQ(1, storage->getCountForType("FakeAsset")); } - body = "TEST 2"; + body = "TEST 2"; { PARSE_XML_RESPONSE_PUT("/asset", body, query); ASSERT_EQ((unsigned int)2, storage->getCount()); - ASSERT_EQ(2, storage->getCountForType("Part")); + ASSERT_EQ(2, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P2"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "2"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 2"); } - body = "TEST 3"; + body = "TEST 3"; { PARSE_XML_RESPONSE_PUT("/asset", body, query); ASSERT_EQ((unsigned int)3, storage->getCount()); - ASSERT_EQ(3, storage->getCountForType("Part")); + ASSERT_EQ(3, storage->getCountForType("FakeAsset")); } { PARSE_XML_RESPONSE("/asset/P3"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "3"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 3"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 3"); } - body = "TEST 2"; + body = "TEST 2"; { PARSE_XML_RESPONSE_PUT("/asset", body, query); ASSERT_EQ((unsigned int)3, storage->getCount(false)); - ASSERT_EQ(3, storage->getCountForType("Part", false)); + ASSERT_EQ(3, storage->getCountForType("FakeAsset", false)); } { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved", "P2"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "FakeAsset"); } { @@ -548,21 +548,21 @@ TEST_F(AgentAssetTest, should_handle_asset_removal_from_adapter) ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)1, storage->getCount()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P2|Part|TEST 2"); + "2021-02-01T12:00:00Z|@ASSET@|P2|FakeAsset|TEST 2"); ASSERT_EQ((unsigned int)2, storage->getCount()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P3|Part|TEST 3"); + "2021-02-01T12:00:00Z|@ASSET@|P3|FakeAsset|TEST 3"); ASSERT_EQ((unsigned int)3, storage->getCount()); { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "P3"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "FakeAsset"); } m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|@REMOVE_ASSET@|P2\r"); @@ -571,7 +571,7 @@ TEST_F(AgentAssetTest, should_handle_asset_removal_from_adapter) { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved", "P2"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "FakeAsset"); } { @@ -650,15 +650,15 @@ TEST_F(AgentAssetTest, AssetPrependId) const auto &storage = agent->getAssetStorage(); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|@1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|@1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); ASSERT_EQ((unsigned int)1, storage->getCount()); { PARSE_XML_RESPONSE("/asset/0001"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part", "TEST 1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@assetId", "0001"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@assetId", "0001"); } } @@ -671,13 +671,13 @@ TEST_F(AgentAssetTest, should_remove_changed_asset) ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)1, storage->getCount()); { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "FakeAsset"); } m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|@REMOVE_ASSET@|P1"); @@ -686,9 +686,9 @@ TEST_F(AgentAssetTest, should_remove_changed_asset) { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "FakeAsset"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "UNAVAILABLE"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "FakeAsset"); } } @@ -703,26 +703,26 @@ TEST_F(AgentAssetTest, should_remove_changed_observation_asset_in_2_6) ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)1, storage->getCount()); { PARSE_XML_RESPONSE("/LinuxCNC/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "FakeAsset"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "UNAVAILABLE"); } m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 2"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 2"); ASSERT_EQ((unsigned int)1, storage->getCount()); { PARSE_XML_RESPONSE("/LinuxCNC/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "FakeAsset"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded", "UNAVAILABLE"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "FakeAsset"); } } @@ -737,13 +737,13 @@ TEST_F(AgentAssetTest, should_remove_added_asset_observation_in_2_6) ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)1, storage->getCount()); { PARSE_XML_RESPONSE("/LinuxCNC/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "FakeAsset"); } m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|@REMOVE_ASSET@|P1"); @@ -752,9 +752,9 @@ TEST_F(AgentAssetTest, should_remove_added_asset_observation_in_2_6) { PARSE_XML_RESPONSE("/LinuxCNC/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "FakeAsset"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded", "UNAVAILABLE"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetAdded@assetType", "FakeAsset"); } } @@ -767,13 +767,13 @@ TEST_F(AgentAssetTest, should_remove_asset_using_http_delete) ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)1, storage->getCount(false)); { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "FakeAsset"); } { @@ -783,7 +783,7 @@ TEST_F(AgentAssetTest, should_remove_asset_using_http_delete) { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "FakeAsset"); } } @@ -820,32 +820,32 @@ TEST_F(AgentAssetTest, should_remove_all_assets) ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); ASSERT_EQ((unsigned int)1, storage->getCount()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P2|Part|TEST 2"); + "2021-02-01T12:00:00Z|@ASSET@|P2|FakeAsset|TEST 2"); ASSERT_EQ((unsigned int)2, storage->getCount()); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P3|Part|TEST 3"); + "2021-02-01T12:00:00Z|@ASSET@|P3|FakeAsset|TEST 3"); ASSERT_EQ((unsigned int)3, storage->getCount()); { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "P3"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "FakeAsset"); } - m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|@REMOVE_ALL_ASSETS@|Part"); + m_agentTestHelper->m_adapter->processData("2021-02-01T12:00:00Z|@REMOVE_ALL_ASSETS@|FakeAsset"); ASSERT_EQ((unsigned int)3, storage->getCount(false)); { PARSE_XML_RESPONSE("/current"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved", "P3"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetRemoved@assetType", "FakeAsset"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged", "UNAVAILABLE"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetChanged@assetType", "FakeAsset"); } ASSERT_EQ((unsigned int)0, storage->getCount()); @@ -871,12 +871,12 @@ TEST_F(AgentAssetTest, should_remove_all_assets) TEST_F(AgentAssetTest, probe_should_have_the_asset_counts) { auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "1.3", 4, true); - string body = "TEST 1"; + string body = "TEST 1"; QueryMap queries; const auto &storage = agent->getAssetStorage(); queries["device"] = "LinuxCNC"; - queries["type"] = "Part"; + queries["type"] = "FakeAsset"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); @@ -889,7 +889,7 @@ TEST_F(AgentAssetTest, probe_should_have_the_asset_counts) { PARSE_XML_RESPONSE("/probe"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Header/m:AssetCounts/m:AssetCount@assetType", "Part"); + ASSERT_XML_PATH_EQUAL(doc, "//m:Header/m:AssetCounts/m:AssetCount@assetType", "FakeAsset"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header/m:AssetCounts/m:AssetCount", "2"); } } @@ -963,21 +963,21 @@ TEST_F(AgentAssetTest, update_asset_count_data_item_v2_0) addAdapter(); m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P1|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|TEST 1"); { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry@key", "Part"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry@key", "FakeAsset"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "1"); } m_agentTestHelper->m_adapter->processData( - "2021-02-01T12:00:00Z|@ASSET@|P2|Part|TEST 1"); + "2021-02-01T12:00:00Z|@ASSET@|P2|FakeAsset|TEST 1"); { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry@key", "Part"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry@key", "FakeAsset"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "2"); } m_agentTestHelper->m_adapter->processData( @@ -985,7 +985,7 @@ TEST_F(AgentAssetTest, update_asset_count_data_item_v2_0) { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "2"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Tool']", "1"); } @@ -994,7 +994,7 @@ TEST_F(AgentAssetTest, update_asset_count_data_item_v2_0) { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "2"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Tool']", "2"); } @@ -1003,7 +1003,7 @@ TEST_F(AgentAssetTest, update_asset_count_data_item_v2_0) { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "2"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "2"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Tool']", "3"); } @@ -1011,7 +1011,7 @@ TEST_F(AgentAssetTest, update_asset_count_data_item_v2_0) { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "1"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Tool']", "3"); } @@ -1019,7 +1019,7 @@ TEST_F(AgentAssetTest, update_asset_count_data_item_v2_0) { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_COUNT(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", 0); + ASSERT_XML_PATH_COUNT(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", 0); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Tool']", "3"); } @@ -1035,12 +1035,12 @@ TEST_F(AgentAssetTest, asset_count_should_not_occur_in_header_post_20) { auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 4, true); - string body = "TEST 1"; + string body = "TEST 1"; QueryMap queries; const auto &storage = agent->getAssetStorage(); queries["device"] = "LinuxCNC"; - queries["type"] = "Part"; + queries["type"] = "FakeAsset"; { PARSE_XML_RESPONSE_PUT("/asset", body, queries); @@ -1061,12 +1061,12 @@ TEST_F(AgentAssetTest, asset_count_should_track_asset_additions_by_type) { auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 4, true); - string body1 = "TEST 1"; + string body1 = "TEST 1"; QueryMap queries; const auto &storage = agent->getAssetStorage(); queries["device"] = "LinuxCNC"; - queries["type"] = "Part"; + queries["type"] = "FakeAsset"; { PARSE_XML_RESPONSE_PUT("/asset", body1, queries); @@ -1075,7 +1075,7 @@ TEST_F(AgentAssetTest, asset_count_should_track_asset_additions_by_type) { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "1"); } string body2 = "TEST 2"; @@ -1088,7 +1088,7 @@ TEST_F(AgentAssetTest, asset_count_should_track_asset_additions_by_type) { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "1"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='PartThing']", "1"); } @@ -1098,7 +1098,7 @@ TEST_F(AgentAssetTest, asset_count_should_track_asset_additions_by_type) } { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "1"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='PartThing']", "2"); } @@ -1110,7 +1110,7 @@ TEST_F(AgentAssetTest, asset_count_should_track_asset_additions_by_type) } { PARSE_XML_RESPONSE("/LinuxCNC/current"); - ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='Part']", "1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='FakeAsset']", "1"); ASSERT_XML_PATH_EQUAL(doc, "//m:AssetCountDataSet/m:Entry[@key='PartThing']", "1"); } } @@ -1119,7 +1119,7 @@ TEST_F(AgentAssetTest, asset_should_also_work_using_post_with_assets) { auto agent = m_agentTestHelper->createAgent("/samples/test_config.xml", 8, 4, "2.0", 4, true); - string body = "TEST 1"; + string body = "TEST 1"; QueryMap queries; const auto &storage = agent->getAssetStorage(); diff --git a/test_package/asset_hash_test.cpp b/test_package/asset_hash_test.cpp index 4705947ad..d33cf314f 100644 --- a/test_package/asset_hash_test.cpp +++ b/test_package/asset_hash_test.cpp @@ -76,12 +76,12 @@ TEST_F(AssetHashTest, should_assign_hash_when_receiving_asset) const auto &storage = agent->getAssetStorage(); m_agentTestHelper->m_adapter->parseBuffer( - R"("2021-02-01T12:00:00Z|@ASSET@|P1|Part|--multiline--AAAA - + R"("2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|--multiline--AAAA + TEST 1 Some Text XXX - + --multiline--AAAA )"); ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); @@ -93,12 +93,12 @@ TEST_F(AssetHashTest, should_assign_hash_when_receiving_asset) { PARSE_XML_RESPONSE("/asset/P1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part/m:PartXXX", "TEST 1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part/m:Extra", "XXX"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@assetId", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@deviceUuid", "000"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@timestamp", "2021-02-01T12:00:00Z"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@hash", hash.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset/m:PartXXX", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset/m:Extra", "XXX"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@assetId", "P1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@deviceUuid", "000"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@timestamp", "2021-02-01T12:00:00Z"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@hash", hash.c_str()); } { @@ -107,12 +107,12 @@ TEST_F(AssetHashTest, should_assign_hash_when_receiving_asset) } m_agentTestHelper->m_adapter->parseBuffer( - R"("2023-02-01T12:00:00Z|@ASSET@|P1|Part|--multiline--AAAA - + R"("2023-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|--multiline--AAAA + TEST 1 Some Text XXX - + --multiline--AAAA )"); @@ -123,8 +123,8 @@ TEST_F(AssetHashTest, should_assign_hash_when_receiving_asset) { PARSE_XML_RESPONSE("/asset/P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@timestamp", "2023-02-01T12:00:00Z"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@hash", hash.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@timestamp", "2023-02-01T12:00:00Z"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@hash", hash.c_str()); } { @@ -140,12 +140,12 @@ TEST_F(AssetHashTest, hash_should_change_when_doc_changes) const auto &storage = agent->getAssetStorage(); m_agentTestHelper->m_adapter->parseBuffer( - R"("2021-02-01T12:00:00Z|@ASSET@|P1|Part|--multiline--AAAA - + R"("2021-02-01T12:00:00Z|@ASSET@|P1|FakeAsset|--multiline--AAAA + TEST 1 Some Text XXX - + --multiline--AAAA )"); ASSERT_EQ((unsigned int)4, storage->getMaxAssets()); @@ -157,12 +157,12 @@ TEST_F(AssetHashTest, hash_should_change_when_doc_changes) { PARSE_XML_RESPONSE("/asset/P1"); ASSERT_XML_PATH_EQUAL(doc, "//m:Header@assetCount", "1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part/m:PartXXX", "TEST 1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part/m:Extra", "XXX"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@assetId", "P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@deviceUuid", "000"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@timestamp", "2021-02-01T12:00:00Z"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@hash", hash.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset/m:PartXXX", "TEST 1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset/m:Extra", "XXX"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@assetId", "P1"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@deviceUuid", "000"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@timestamp", "2021-02-01T12:00:00Z"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@hash", hash.c_str()); } { @@ -172,11 +172,11 @@ TEST_F(AssetHashTest, hash_should_change_when_doc_changes) m_agentTestHelper->m_adapter->parseBuffer( R"("2023-02-01T12:00:00Z|@ASSET@|P1|Part|--multiline--AAAA - + TEST 2 Some Text XXX - + --multiline--AAAA )"); @@ -187,8 +187,8 @@ TEST_F(AssetHashTest, hash_should_change_when_doc_changes) { PARSE_XML_RESPONSE("/asset/P1"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@timestamp", "2023-02-01T12:00:00Z"); - ASSERT_XML_PATH_EQUAL(doc, "//m:Part@hash", hash2.c_str()); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@timestamp", "2023-02-01T12:00:00Z"); + ASSERT_XML_PATH_EQUAL(doc, "//m:FakeAsset@hash", hash2.c_str()); } { diff --git a/test_package/json_printer_test.cpp b/test_package/json_printer_test.cpp index 0405d6ea2..dd35fc59c 100644 --- a/test_package/json_printer_test.cpp +++ b/test_package/json_printer_test.cpp @@ -356,10 +356,8 @@ TEST_F(JsonPrinterTest, elements_with_property_list_version_2) json jdoc = json::parse(sdoc); ASSERT_EQ(2, jdoc.at("/Root/CuttingItems/count"_json_pointer).get()); - ASSERT_EQ("1", - jdoc.at("/Root/CuttingItems/list/CuttingItem/0/itemId"_json_pointer).get()); - ASSERT_EQ("2", - jdoc.at("/Root/CuttingItems/list/CuttingItem/1/itemId"_json_pointer).get()); + ASSERT_EQ("1", jdoc.at("/Root/CuttingItems/CuttingItem/0/itemId"_json_pointer).get()); + ASSERT_EQ("2", jdoc.at("/Root/CuttingItems/CuttingItem/1/itemId"_json_pointer).get()); } TEST_F(JsonPrinterTest, should_honor_include_hidden_parameter) diff --git a/test_package/mqtt_entity_sink_test.cpp b/test_package/mqtt_entity_sink_test.cpp index f5b781ab1..4cc274038 100644 --- a/test_package/mqtt_entity_sink_test.cpp +++ b/test_package/mqtt_entity_sink_test.cpp @@ -218,16 +218,15 @@ TEST_F(MqttEntitySinkTest, mqtt_entity_sink_should_use_flat_topic_structure) client->set_keep_alive_sec(30); bool subscribed = false; - client->set_connack_handler( - [client](bool sp, mqtt::connect_return_code connack_return_code) { - if (connack_return_code == mqtt::connect_return_code::accepted) - { - auto pid = client->acquire_unique_packet_id(); - client->async_subscribe(pid, "MTConnect/#", MQTT_NS::qos::at_least_once, - [](MQTT_NS::error_code ec) { EXPECT_FALSE(ec); }); - } - return true; - }); + client->set_connack_handler([client](bool sp, mqtt::connect_return_code connack_return_code) { + if (connack_return_code == mqtt::connect_return_code::accepted) + { + auto pid = client->acquire_unique_packet_id(); + client->async_subscribe(pid, "MTConnect/#", MQTT_NS::qos::at_least_once, + [](MQTT_NS::error_code ec) { EXPECT_FALSE(ec); }); + } + return true; + }); client->set_suback_handler( [&subscribed](std::uint16_t packet_id, std::vector results) { diff --git a/test_package/part_test.cpp b/test_package/part_test.cpp new file mode 100644 index 000000000..8cff804fd --- /dev/null +++ b/test_package/part_test.cpp @@ -0,0 +1,633 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Ensure that gtest is the first header otherwise Windows raises an error +#include +// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) + +#include +#include +#include +#include +#include +#include + +#include "agent_test_helper.hpp" +#include "json_helper.hpp" +#include "mtconnect/agent.hpp" +#include "mtconnect/asset/asset.hpp" +#include "mtconnect/asset/part.hpp" +#include "mtconnect/entity/xml_parser.hpp" +#include "mtconnect/entity/xml_printer.hpp" +#include "mtconnect/printer/xml_printer_helper.hpp" +#include "mtconnect/source/adapter/adapter.hpp" + +using json = nlohmann::json; +using namespace std; +using namespace mtconnect; +using namespace mtconnect::entity; +using namespace mtconnect::source::adapter; +using namespace mtconnect::asset; +using namespace mtconnect::printer; + +// main +int main(int argc, char *argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +class PartAssetTest : public testing::Test +{ +protected: + void SetUp() override + { // Create an agent with only 16 slots and 8 data items. + Part::registerAsset(); + PartArchetype::registerAsset(); + m_writer = make_unique(true); + } + + void TearDown() override { m_writer.reset(); } + + std::unique_ptr m_writer; + std::unique_ptr m_agentTestHelper; +}; + +/// @section PartArchetype tests + +TEST_F(PartAssetTest, should_parse_a_part_archetype) +{ + const auto doc = + R"DOC( + + + + + + + + +
100 Fruitstand Rd, Ork Arkansas, 11111
+ Some customer +
+
+
+)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + ASSERT_EQ("PartArchetype", asset->getName()); + ASSERT_EQ("PART1234", asset->getAssetId()); + ASSERT_EQ("5", asset->get("revision")); + ASSERT_EQ("STEP222", asset->get("drawing")); + ASSERT_EQ("HHH", asset->get("family")); + + auto configuration = asset->get("Configuration"); + ASSERT_TRUE(configuration); + + auto relationships = configuration->getList("Relationships"); + ASSERT_TRUE(relationships); + ASSERT_EQ(2, relationships->size()); + + { + auto it = relationships->begin(); + ASSERT_EQ("A", (*it)->get("id")); + ASSERT_EQ("MATERIAL", (*it)->get("assetIdRef")); + ASSERT_EQ("PEER", (*it)->get("type")); + ASSERT_EQ("RawMaterial", (*it)->get("assetType")); + + it++; + ASSERT_EQ("B", (*it)->get("id")); + ASSERT_EQ("PROCESS", (*it)->get("assetIdRef")); + ASSERT_EQ("PEER", (*it)->get("type")); + ASSERT_EQ("ProcessArchetype", (*it)->get("assetType")); + } + + auto customers = asset->getList("Customers"); + ASSERT_TRUE(customers); + ASSERT_EQ(1, customers->size()); + + { + auto customer = customers->front(); + ASSERT_EQ("C00241", customer->get("customerId")); + ASSERT_EQ("customer name", customer->get("name")); + ASSERT_EQ("100 Fruitstand Rd, Ork Arkansas, 11111", customer->get("Address")); + ASSERT_EQ("Some customer", customer->get("Description")); + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(PartAssetTest, process_archetype_can_have_multiple_customers) +{ + const auto doc = + R"DOC( + + +
100 Fruitstand Rd, Ork Arkansas, 11111
+ Some customer +
+ +
Somewhere in Austrailia
+ Another customer +
+
+
+)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + ASSERT_EQ("PartArchetype", asset->getName()); + + auto customers = asset->getList("Customers"); + ASSERT_TRUE(customers); + ASSERT_EQ(2, customers->size()); + + { + auto it = customers->begin(); + auto customer = *it; + EXPECT_EQ("C00241", customer->get("customerId")); + EXPECT_EQ("customer name", customer->get("name")); + EXPECT_EQ("100 Fruitstand Rd, Ork Arkansas, 11111", customer->get("Address")); + EXPECT_EQ("Some customer", customer->get("Description")); + + it++; + customer = *it; + EXPECT_EQ("C1111", customer->get("customerId")); + EXPECT_EQ("another customer", customer->get("name")); + EXPECT_EQ("Somewhere in Austrailia", customer->get("Address")); + EXPECT_EQ("Another customer", customer->get("Description")); + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(PartAssetTest, customers_are_optional) +{ + const auto doc = + R"DOC( +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + ASSERT_EQ("PartArchetype", asset->getName()); + ASSERT_EQ("PART1234", asset->getAssetId()); + ASSERT_EQ("5", asset->get("revision")); + ASSERT_EQ("STEP222", asset->get("drawing")); + ASSERT_EQ("HHH", asset->get("family")); + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(PartAssetTest, should_generate_json) +{ + const auto doc = + R"DOC( + + + + + + + + +
100 Fruitstand Rd, Ork Arkansas, 11111
+ Some customer +
+
+
+)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + ASSERT_EQ("PartArchetype", asset->getName()); + + // Round trip test + entity::JsonEntityPrinter jprinter(2, true); + + auto jdoc = jprinter.print(entity); + EXPECT_EQ(R"({ + "PartArchetype": { + "Configuration": { + "Relationships": { + "AssetRelationship": [ + { + "assetIdRef": "MATERIAL", + "assetType": "RawMaterial", + "id": "A", + "type": "PEER" + }, + { + "assetIdRef": "PROCESS", + "assetType": "ProcessArchetype", + "id": "B", + "type": "PEER" + } + ] + } + }, + "Customers": { + "Customer": [ + { + "Address": "100 Fruitstand Rd, Ork Arkansas, 11111", + "Description": "Some customer", + "customerId": "C00241", + "name": "customer name" + } + ] + }, + "assetId": "PART1234", + "drawing": "STEP222", + "family": "HHH", + "revision": "5" + } +})", + jdoc); +} + +TEST_F(PartAssetTest, part_archetype_should_be_extensible) +{ + const auto doc = + R"DOC( + + + + + + + + +
100 Fruitstand Rd, Ork Arkansas, 11111
+ Some customer +
+
+ + + + + Some simple extension value +
+)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + ASSERT_EQ("PartArchetype", asset->getName()); + + auto properties = asset->getList("Properties"); + ASSERT_TRUE(properties); + + ASSERT_EQ(2, properties->size()); + { + auto it = properties->begin(); + EXPECT_EQ("CustomProperty1", (*it)->get("name")); + EXPECT_EQ("Value1", (*it)->get("value")); + it++; + EXPECT_EQ("CustomProperty2", (*it)->get("name")); + EXPECT_EQ("Value2", (*it)->get("value")); + } + + auto sext = asset->get("SimpleExtension"); + ASSERT_EQ("Some simple extension value", sext); +} + +/// @section Part asset tests + +TEST_F(PartAssetTest, should_parse_a_part) +{ + const auto doc = + R"DOC( + + + + + + + + UID123456 + GID1235 + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + ASSERT_EQ("Part", asset->getName()); + EXPECT_EQ("PART1234", asset->getAssetId()); + EXPECT_EQ("5", asset->get("revision")); + EXPECT_EQ("STEP222", asset->get("drawing")); + EXPECT_EQ("HHH", asset->get("family")); + EXPECT_EQ("NATIVE001", asset->get("nativeId")); + + auto configuration = asset->get("Configuration"); + ASSERT_TRUE(configuration); + + auto relationships = configuration->getList("Relationships"); + ASSERT_TRUE(relationships); + ASSERT_EQ(2, relationships->size()); + + { + auto it = relationships->begin(); + EXPECT_EQ("A", (*it)->get("id")); + EXPECT_EQ("MATERIAL", (*it)->get("assetIdRef")); + EXPECT_EQ("PEER", (*it)->get("type")); + EXPECT_EQ("RawMaterial", (*it)->get("assetType")); + + it++; + EXPECT_EQ("B", (*it)->get("id")); + EXPECT_EQ("PROCESS", (*it)->get("assetIdRef")); + EXPECT_EQ("PEER", (*it)->get("type")); + EXPECT_EQ("ProcessArchetype", (*it)->get("assetType")); + } + + auto identifiers = asset->getList("PartIdentifiers"); + ASSERT_TRUE(identifiers); + ASSERT_EQ(2, identifiers->size()); + + { + auto it = identifiers->begin(); + auto identifier = *it; + EXPECT_EQ("UNIQUE_IDENTIFIER", identifier->get("type")); + EXPECT_EQ("10", identifier->get("stepIdRef")); + auto st = identifier->get("timestamp"); + EXPECT_EQ("2025-11-28T00:01:00Z", getCurrentTime(st, GMT)); + EXPECT_EQ("UID123456", identifier->getValue()); + + it++; + identifier = *it; + EXPECT_EQ("GROUP_IDENTIFIER", identifier->get("type")); + EXPECT_EQ("11", identifier->get("stepIdRef")); + st = identifier->get("timestamp"); + EXPECT_EQ("2025-11-28T00:02:00Z", getCurrentTime(st, GMT)); + EXPECT_EQ("GID1235", identifier->getValue()); + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(PartAssetTest, part_identifiers_are_optional) +{ + const auto doc = + R"DOC( +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + ASSERT_EQ("Part", asset->getName()); + EXPECT_EQ("PART1234", asset->getAssetId()); + EXPECT_EQ("5", asset->get("revision")); + EXPECT_EQ("STEP222", asset->get("drawing")); + EXPECT_EQ("HHH", asset->get("family")); + EXPECT_EQ("NATIVE001", asset->get("nativeId")); + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(PartAssetTest, part_identifiers_type_must_be_unique_or_group) +{ + const auto doc = + R"DOC( + + UID123456 + GID1235 + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(2, errors.size()); + + auto it = errors.begin(); + { + auto error = dynamic_cast(it->get()); + ASSERT_TRUE(error); + EXPECT_EQ("Identifier(type): Invalid value for 'type': 'OTHER_IDENTIFIER' is not allowed"s, + error->what()); + EXPECT_EQ("Identifier", error->getEntity()); + EXPECT_EQ("type", error->getProperty()); + } + + it++; + { + auto error = it->get(); + ASSERT_TRUE(error); + EXPECT_EQ("PartIdentifiers: Invalid element 'Identifier'"s, error->what()); + EXPECT_EQ("PartIdentifiers", error->getEntity()); + } +} + +TEST_F(PartAssetTest, part_should_be_extensible) +{ + const auto doc = + R"DOC( + + + + + + + + UID123456 + GID1235 + + + 2025-12-01T00:00:00Z + 2025-12-20T00:00:00Z + 100 + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + auto workOrder = asset->get("WorkOrder"); + ASSERT_TRUE(workOrder); + + ASSERT_EQ("WO12345", workOrder->get("number")); + ASSERT_EQ("2025-12-01T00:00:00Z", workOrder->get("OrderDate")); + ASSERT_EQ("2025-12-20T00:00:00Z", workOrder->get("DueDate")); + ASSERT_EQ("100", workOrder->get("PlannedQuantity")); +} + +TEST_F(PartAssetTest, part_should_generate_json) +{ + const auto doc = + R"DOC( + + + + + + + + UID123456 + GID1235 + + + 2025-12-01T00:00:00Z + 2025-12-20T00:00:00Z + 100 + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + // Round trip test + entity::JsonEntityPrinter jprinter(2, true); + + auto jdoc = jprinter.print(entity); + EXPECT_EQ(R"({ + "Part": { + "Configuration": { + "Relationships": { + "AssetRelationship": [ + { + "assetIdRef": "MATERIAL", + "assetType": "RawMaterial", + "id": "A", + "type": "PEER" + }, + { + "assetIdRef": "PROCESS", + "assetType": "ProcessArchetype", + "id": "B", + "type": "PEER" + } + ] + } + }, + "PartIdentifiers": { + "Identifier": [ + { + "value": "UID123456", + "stepIdRef": "10", + "timestamp": "2025-11-28T00:01:00Z", + "type": "UNIQUE_IDENTIFIER" + }, + { + "value": "GID1235", + "stepIdRef": "11", + "timestamp": "2025-11-28T00:02:00Z", + "type": "GROUP_IDENTIFIER" + } + ] + }, + "WorkOrder": { + "DueDate": "2025-12-20T00:00:00Z", + "OrderDate": "2025-12-01T00:00:00Z", + "PlannedQuantity": "100", + "number": "WO12345" + }, + "assetId": "PART1234", + "drawing": "STEP222", + "family": "HHH", + "nativeId": "NATIVE001", + "revision": "5" + } +})", + jdoc); +} diff --git a/test_package/process_test.cpp b/test_package/process_test.cpp new file mode 100644 index 000000000..35b391f19 --- /dev/null +++ b/test_package/process_test.cpp @@ -0,0 +1,771 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Ensure that gtest is the first header otherwise Windows raises an error +#include +// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) + +#include +#include +#include +#include +#include +#include + +#include "agent_test_helper.hpp" +#include "json_helper.hpp" +#include "mtconnect/agent.hpp" +#include "mtconnect/asset/asset.hpp" +#include "mtconnect/asset/process.hpp" +#include "mtconnect/entity/xml_parser.hpp" +#include "mtconnect/entity/xml_printer.hpp" +#include "mtconnect/printer/xml_printer_helper.hpp" +#include "mtconnect/source/adapter/adapter.hpp" + +using json = nlohmann::json; +using namespace std; +using namespace mtconnect; +using namespace mtconnect::entity; +using namespace mtconnect::source::adapter; +using namespace mtconnect::asset; +using namespace mtconnect::printer; + +// main +int main(int argc, char *argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +class ProcessAssetTest : public testing::Test +{ +protected: + void SetUp() override + { // Create an agent with only 16 slots and 8 data items. + ProcessArchetype::registerAsset(); + Process::registerAsset(); + m_writer = make_unique(true); + } + + void TearDown() override { m_writer.reset(); } + + std::unique_ptr m_writer; + std::unique_ptr m_agentTestHelper; +}; + +TEST_F(ProcessAssetTest, should_parse_a_process_archetype) +{ + const auto doc = + R"DOC( + + + + + + + + + Process Step 10 + 2025-11-24T00:00:00Z + 23000 + + + + + + + First Activity + + + + + + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + ASSERT_EQ("ProcessArchetype", asset->getName()); + ASSERT_EQ("PROCESS_ARCH_ID", asset->getAssetId()); + ASSERT_EQ("1", asset->get("revision")); + + auto configuration = asset->get("Configuration"); + ASSERT_TRUE(configuration); + + auto relationships = configuration->getList("Relationships"); + ASSERT_TRUE(relationships); + ASSERT_EQ(1, relationships->size()); + + { + auto it = relationships->begin(); + ASSERT_EQ("reference_id", (*it)->get("id")); + ASSERT_EQ("PART_ID", (*it)->get("assetIdRef")); + ASSERT_EQ("PEER", (*it)->get("type")); + ASSERT_EQ("PART_ARCHETYPE", (*it)->get("assetType")); + } + + auto routings = asset->getList("Routings"); + ASSERT_TRUE(routings); + ASSERT_EQ(1, routings->size()); + + { + auto routing = routings->front(); + ASSERT_EQ("routng1", routing->get("routingId")); + ASSERT_EQ(1, routing->get("precedence")); + + auto processSteps = routing->get("ProcessStep"); + ASSERT_EQ(1, processSteps.size()); + + auto step = processSteps.front(); + ASSERT_EQ("10", step->get("stepId")); + ASSERT_EQ("Process Step 10", step->get("Description")); + + auto st = step->get("StartTime"); + ASSERT_EQ("2025-11-24T00:00:00Z", getCurrentTime(st, GMT)); + ASSERT_EQ(23000, step->get("Duration")); + + auto targets = step->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(1, targets->size()); + + { + auto it = targets->begin(); + ASSERT_EQ("group1", (*it)->get("groupIdRef")); + } + + auto activityGroups = step->getList("ActivityGroups"); + ASSERT_TRUE(activityGroups); + ASSERT_EQ(1, activityGroups->size()); + + { + auto it = activityGroups->begin(); + auto activityGroup = *it; + ASSERT_EQ("act1", activityGroup->get("activityGroupId")); + + auto activities = activityGroup->get("Activity"); + ASSERT_EQ(1, activities.size()); + + auto activity = activities.front(); + ASSERT_EQ("a1", activity->get("activityId")); + ASSERT_EQ(1, activity->get("sequence")); + ASSERT_EQ("First Activity", activity->get("Description")); + } + } + + auto targets = asset->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(2, targets->size()); + + { + auto it = targets->begin(); + ASSERT_EQ("TargetDevice", (*it)->getName()); + ASSERT_EQ("device1", (*it)->get("deviceUuid")); + + it++; + auto targetGroup = *it; + ASSERT_EQ("TargetGroup", targetGroup->getName()); + ASSERT_EQ("group1", targetGroup->get("groupId")); + + auto targetDevices = targetGroup->get("LIST"); + ASSERT_EQ(2, targetDevices.size()); + + { + auto dit = targetDevices.begin(); + ASSERT_EQ("TargetDevice", (*dit)->getName()); + ASSERT_EQ("device2", (*dit)->get("deviceUuid")); + dit++; + ASSERT_EQ("TargetDevice", (*dit)->getName()); + ASSERT_EQ("device3", (*dit)->get("deviceUuid")); + } + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(ProcessAssetTest, process_archetype_can_have_multiple_routings) +{ + const auto doc = + R"DOC( + + + + + + + + + Process Step 10 + 2025-11-24T00:00:00Z + 23000 + + + + + Process Step 11 + 2025-11-25T00:00:00Z + 20000 + + + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + auto routings = asset->getList("Routings"); + ASSERT_TRUE(routings); + ASSERT_EQ(2, routings->size()); + + auto it = routings->begin(); + { + auto routing = *it; + ASSERT_EQ("routng1", routing->get("routingId")); + ASSERT_EQ(1, routing->get("precedence")); + + auto processSteps = routing->get("ProcessStep"); + ASSERT_EQ(1, processSteps.size()); + + auto step = processSteps.front(); + ASSERT_EQ("10", step->get("stepId")); + ASSERT_EQ("Process Step 10", step->get("Description")); + + auto st = step->get("StartTime"); + ASSERT_EQ("2025-11-24T00:00:00Z", getCurrentTime(st, GMT)); + ASSERT_EQ(23000, step->get("Duration")); + } + it++; + { + auto routing = *it; + ASSERT_EQ("routng2", routing->get("routingId")); + ASSERT_EQ(2, routing->get("precedence")); + + auto processSteps = routing->get("ProcessStep"); + ASSERT_EQ(1, processSteps.size()); + + auto step = processSteps.front(); + ASSERT_EQ("11", step->get("stepId")); + ASSERT_EQ("Process Step 11", step->get("Description")); + + auto st = step->get("StartTime"); + ASSERT_EQ("2025-11-25T00:00:00Z", getCurrentTime(st, GMT)); + ASSERT_EQ(20000, step->get("Duration")); + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(ProcessAssetTest, process_steps_can_be_optional) +{ + const auto doc = + R"DOC( + + + + Process Step 10 + 2025-11-24T00:00:00Z + 23000 + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + auto routings = asset->getList("Routings"); + ASSERT_TRUE(routings); + ASSERT_EQ(1, routings->size()); + + auto routing = routings->front(); + ASSERT_EQ("routng1", routing->get("routingId")); + + auto processSteps = routing->get("ProcessStep"); + ASSERT_EQ(1, processSteps.size()); + auto step = processSteps.front(); + + ASSERT_EQ("10", step->get("stepId")); + ASSERT_EQ(5, step->get("sequence")); + ASSERT_EQ(true, step->get("optional")); + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(ProcessAssetTest, process_archetype_must_have_at_least_one_routing) +{ + const auto doc = + R"DOC( + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(1, errors.size()); + auto error = dynamic_cast(errors.front().get()); + + ASSERT_EQ("ProcessArchetype(Routings): Property Routings is required and not provided"s, + error->what()); + ASSERT_EQ("ProcessArchetype", error->getEntity()); + ASSERT_EQ("Routings", error->getProperty()); +} + +TEST_F(ProcessAssetTest, process_archetype_routing_must_have_a_process_step) +{ + const auto doc = + R"DOC( + + + + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(5, errors.size()); + + auto it = errors.begin(); + { + auto error = dynamic_cast(it->get()); + ASSERT_TRUE(error); + EXPECT_EQ("Routing(ProcessStep): Property ProcessStep is required and not provided"s, + error->what()); + EXPECT_EQ("Routing", error->getEntity()); + EXPECT_EQ("ProcessStep", error->getProperty()); + } + + it++; + { + auto error = it->get(); + ASSERT_TRUE(error); + EXPECT_EQ("Routings: Invalid element 'Routing'"s, error->what()); + EXPECT_EQ("Routings", error->getEntity()); + } + + it++; + { + auto error = dynamic_cast(it->get()); + ASSERT_TRUE(error); + EXPECT_EQ( + "Routings(Routing): Entity list requirement Routing must have at least 1 entries, 0 found"s, + error->what()); + EXPECT_EQ("Routings", error->getEntity()); + EXPECT_EQ("Routing", error->getProperty()); + } + + it++; + { + auto error = it->get(); + ASSERT_TRUE(error); + EXPECT_EQ("ProcessArchetype: Invalid element 'Routings'"s, error->what()); + EXPECT_EQ("ProcessArchetype", error->getEntity()); + } + + it++; + { + auto error = dynamic_cast(it->get()); + ASSERT_TRUE(error); + EXPECT_EQ("ProcessArchetype(Routings): Property Routings is required and not provided"s, + error->what()); + EXPECT_EQ("ProcessArchetype", error->getEntity()); + EXPECT_EQ("Routings", error->getProperty()); + } +} + +TEST_F(ProcessAssetTest, activity_can_have_a_sequence_precedence_and_be_optional) +{ + const auto doc = + R"DOC( + + + + + + + First Activity + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + auto routings = asset->getList("Routings"); + ASSERT_TRUE(routings); + ASSERT_EQ(1, routings->size()); + + auto routing = routings->front(); + ASSERT_EQ("routng1", routing->get("routingId")); + + auto processSteps = routing->get("ProcessStep"); + ASSERT_EQ(1, processSteps.size()); + auto step = processSteps.front(); + + auto activityGroups = step->getList("ActivityGroups"); + ASSERT_TRUE(activityGroups); + ASSERT_EQ(1, activityGroups->size()); + + auto activityGroup = activityGroups->front(); + ASSERT_EQ("act1", activityGroup->get("activityGroupId")); + ASSERT_EQ("fred", activityGroup->get("name")); + + auto activities = activityGroup->get("Activity"); + ASSERT_EQ(1, activities.size()); + + auto activity = activities.front(); + ASSERT_EQ("a1", activity->get("activityId")); + ASSERT_EQ(2, activity->get("sequence")); + ASSERT_EQ("First Activity", activity->get("Description")); + ASSERT_EQ(true, activity->get("optional")); + ASSERT_EQ(3, activity->get("precedence")); + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(ProcessAssetTest, should_generate_json) +{ + const auto doc = + R"DOC( + + + + + + + + + Process Step 10 + 2025-11-24T00:00:00Z + 23000 + + + + + + + First Activity + + + + + + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + entity::JsonEntityPrinter jprinter(2, true); + + auto sdoc = jprinter.print(entity); + EXPECT_EQ(R"({ + "ProcessArchetype": { + "Configuration": { + "Relationships": { + "AssetRelationship": [ + { + "assetIdRef": "PART_ID", + "assetType": "PART_ARCHETYPE", + "id": "reference_id", + "type": "PEER" + } + ] + } + }, + "Routings": { + "Routing": [ + { + "ProcessStep": [ + { + "ActivityGroups": { + "ActivityGroup": [ + { + "Activity": [ + { + "Description": "First Activity", + "activityId": "a1", + "optional": true, + "precedence": 2, + "sequence": 1 + } + ], + "activityGroupId": "act1", + "name": "fred" + } + ] + }, + "Description": "Process Step 10", + "Duration": 23000.0, + "StartTime": "2025-11-24T00:00:00Z", + "Targets": { + "TargetRef": [ + { + "groupIdRef": "group1" + } + ] + }, + "stepId": "10" + } + ], + "precedence": 1, + "routingId": "routng1" + } + ] + }, + "Targets": { + "TargetDevice": [ + { + "deviceUuid": "device1" + } + ], + "TargetGroup": [ + { + "TargetDevice": [ + { + "deviceUuid": "device2" + }, + { + "deviceUuid": "device3" + } + ], + "groupId": "group1" + } + ] + }, + "assetId": "PROCESS_ARCH_ID", + "revision": "1" + } +})", + sdoc); +} + +TEST_F(ProcessAssetTest, should_parse_and_generate_a_process) +{ + const auto doc = + R"DOC( + + + + + + + + + Process Step 10 + 2025-11-24T00:00:00Z + 23000 + + + + + + + First Activity + + + + + + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(ProcessAssetTest, process_can_only_have_one_routings) +{ + const auto doc = + R"DOC( + + + + + + + + + Process Step 10 + 2025-11-24T00:00:00Z + 23000 + + + + + Process Step 11 + 2025-11-25T00:00:00Z + 20000 + + + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(3, errors.size()); + + auto it = errors.begin(); + { + auto error = dynamic_cast(it->get()); + ASSERT_TRUE(error); + EXPECT_EQ( + "Routings(Routing): Entity list requirement Routing must have at least 1 and no more than 1 entries, 2 found"s, + error->what()); + EXPECT_EQ("Routings", error->getEntity()); + EXPECT_EQ("Routing", error->getProperty()); + } + + it++; + { + auto error = it->get(); + ASSERT_TRUE(error); + EXPECT_EQ("Process: Invalid element 'Routings'"s, error->what()); + EXPECT_EQ("Process", error->getEntity()); + } + + it++; + { + auto error = dynamic_cast(it->get()); + ASSERT_TRUE(error); + EXPECT_EQ("Process(Routings): Property Routings is required and not provided"s, error->what()); + EXPECT_EQ("Process", error->getEntity()); + EXPECT_EQ("Routings", error->getProperty()); + } +} diff --git a/test_package/target_test.cpp b/test_package/target_test.cpp new file mode 100644 index 000000000..f6e7af2b1 --- /dev/null +++ b/test_package/target_test.cpp @@ -0,0 +1,421 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Ensure that gtest is the first header otherwise Windows raises an error +#include +// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) + +#include +#include +#include +#include +#include +#include +#include + +#include "json_helper.hpp" +#include "mtconnect/agent.hpp" +#include "mtconnect/asset/asset.hpp" +#include "mtconnect/asset/target.hpp" +#include "mtconnect/entity/entity.hpp" +#include "mtconnect/entity/json_printer.hpp" +#include "mtconnect/entity/xml_parser.hpp" +#include "mtconnect/entity/xml_printer.hpp" +#include "mtconnect/printer/xml_printer.hpp" +#include "mtconnect/printer/xml_printer_helper.hpp" +#include "mtconnect/source/adapter/adapter.hpp" + +using json = nlohmann::json; +using namespace std; +using namespace mtconnect; +using namespace mtconnect::entity; +using namespace mtconnect::source::adapter; +using namespace mtconnect::asset; +using namespace mtconnect::printer; + +// main +int main(int argc, char *argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +class TargetTest : public testing::Test +{ +protected: + void SetUp() override { m_writer = make_unique(true); } + + void TearDown() override { m_writer.reset(); } + + std::unique_ptr m_writer; +}; + +TEST_F(TargetTest, simple_target_device) +{ + const auto doc = R"DOC( + + + + + +)DOC"; + + auto root = make_shared(); + auto tf = make_shared( + Requirements {{"Targets", ValueType::ENTITY_LIST, Target::getDeviceTargetsFactory(), false}}); + root->registerFactory("Root", tf); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + ASSERT_TRUE(entity); + + auto targets = entity->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(1, targets->size()); + auto it = targets->begin(); + + ASSERT_EQ("TargetDevice", (*it)->getName()); + ASSERT_EQ("device-1234", (*it)->get("deviceUuid")); +} + +TEST_F(TargetTest, target_device_and_device_group) +{ + const auto doc = R"DOC( + + + + + + + + + +)DOC"; + + auto root = make_shared(); + auto tf = make_shared( + Requirements {{"Targets", ValueType::ENTITY_LIST, Target::getDeviceTargetsFactory(), false}}); + root->registerFactory("Root", tf); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + ASSERT_TRUE(entity); + + auto targets = entity->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(2, targets->size()); + auto it = targets->begin(); + + ASSERT_EQ("TargetDevice", (*it)->getName()); + ASSERT_EQ("device-1234", (*it)->get("deviceUuid")); + + it++; + auto group = *it; + + ASSERT_EQ("TargetGroup", group->getName()); + ASSERT_EQ("group_id", group->get("groupId")); + + ASSERT_TRUE(group->hasProperty("LIST")); + const auto groupTargets = group->getListProperty(); + ASSERT_EQ(2, groupTargets.size()); + + auto git = groupTargets.begin(); + ASSERT_EQ("TargetDevice", (*git)->getName()); + ASSERT_EQ("device-5678", (*git)->get("deviceUuid")); + + git++; + ASSERT_EQ("TargetDevice", (*git)->getName()); + ASSERT_EQ("device-9999", (*git)->get("deviceUuid")); +} + +TEST_F(TargetTest, target_device_and_device_group_json) +{ + const auto doc = R"DOC( + + + + + + + + + +)DOC"; + + auto root = make_shared(); + auto tf = make_shared( + Requirements {{"Targets", ValueType::ENTITY_LIST, Target::getDeviceTargetsFactory(), false}}); + root->registerFactory("Root", tf); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + ASSERT_TRUE(entity); + + entity::JsonEntityPrinter jsonPrinter(2, true); + auto json = jsonPrinter.print(entity); + + ASSERT_EQ(R"JSON({ + "Root": { + "Targets": { + "TargetDevice": [ + { + "deviceUuid": "device-1234" + } + ], + "TargetGroup": [ + { + "TargetDevice": [ + { + "deviceUuid": "device-5678" + }, + { + "deviceUuid": "device-9999" + } + ], + "groupId": "group_id" + } + ] + } + } +})JSON", + json); +} + +TEST_F(TargetTest, nested_target_groups_with_target_refs) +{ + const auto doc = R"DOC( + + + + + + + + + + + + + +)DOC"; + + auto root = make_shared(); + auto tf = make_shared( + Requirements {{"Targets", ValueType::ENTITY_LIST, Target::getDeviceTargetsFactory(), false}}); + root->registerFactory("Root", tf); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + ASSERT_TRUE(entity); + + auto targets = entity->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(3, targets->size()); + auto it = targets->begin(); + + ASSERT_EQ("TargetDevice", (*it)->getName()); + ASSERT_EQ("device-1234", (*it)->get("deviceUuid")); + + it++; + auto group = *it; + + ASSERT_EQ("TargetGroup", group->getName()); + ASSERT_EQ("A", group->get("groupId")); + + ASSERT_TRUE(group->hasProperty("LIST")); + const auto groupTargets = group->getListProperty(); + ASSERT_EQ(2, groupTargets.size()); + + auto git = groupTargets.begin(); + ASSERT_EQ("TargetDevice", (*git)->getName()); + ASSERT_EQ("device-5678", (*git)->get("deviceUuid")); + + git++; + ASSERT_EQ("TargetDevice", (*git)->getName()); + ASSERT_EQ("device-9999", (*git)->get("deviceUuid")); + + group = *(++it); + ASSERT_EQ("TargetGroup", group->getName()); + ASSERT_EQ("B", group->get("groupId")); + + ASSERT_TRUE(group->hasProperty("LIST")); + const auto groupTargets2 = group->getListProperty(); + ASSERT_EQ(2, groupTargets2.size()); + + git = groupTargets2.begin(); + ASSERT_EQ("TargetDevice", (*git)->getName()); + ASSERT_EQ("device-2222", (*git)->get("deviceUuid")); + + git++; + ASSERT_EQ("TargetRef", (*git)->getName()); + ASSERT_EQ("A", (*git)->get("groupIdRef")); +} + +TEST_F(TargetTest, reject_empty_groups) +{ + using namespace date; + const auto doc = R"DOC( + + + + + + + +)DOC"; + + auto root = make_shared(); + auto tf = make_shared( + Requirements {{"Targets", ValueType::ENTITY_LIST, Target::getDeviceTargetsFactory(), false}}); + root->registerFactory("Root", tf); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(2, errors.size()); + ASSERT_TRUE(entity); + + auto targets = entity->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(1, targets->size()); + auto it = targets->begin(); + + ASSERT_EQ("TargetDevice", (*it)->getName()); + ASSERT_EQ("device-1234", (*it)->get("deviceUuid")); +} + +TEST_F(TargetTest, verify_target_requirement) +{ + const auto doc = R"DOC( + + + + ABC + 123 + + + +)DOC"; + + auto root = make_shared(); + auto tf = make_shared(Requirements { + {"Targets", ValueType::ENTITY_LIST, Target::getRequirementTargetsFactory(), false}}); + root->registerFactory("Root", tf); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + ASSERT_TRUE(entity); + + auto targets = entity->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(1, targets->size()); + auto it = targets->begin(); + + ASSERT_EQ("TargetRequirementTable", (*it)->getName()); + ASSERT_EQ("req1", (*it)->get("requirementId")); + + const auto table = (*it)->getValue(); + ASSERT_EQ(2, table.size()); + + auto rowIt = table.begin(); + ASSERT_EQ("R1", rowIt->m_key); + ASSERT_TRUE(holds_alternative(rowIt->m_value)); + auto &row = get(rowIt->m_value); + ASSERT_EQ(1, row.size()); + + auto cellIt = row.begin(); + ASSERT_EQ("C1", cellIt->m_key); + ASSERT_TRUE(holds_alternative(cellIt->m_value)); + ASSERT_EQ("ABC", get(cellIt->m_value)); + + rowIt++; + ASSERT_EQ("R2", rowIt->m_key); + ASSERT_TRUE(holds_alternative(rowIt->m_value)); + auto &row2 = get(rowIt->m_value); + ASSERT_EQ(1, row2.size()); + + cellIt = row2.begin(); + ASSERT_EQ("C2", cellIt->m_key); + ASSERT_TRUE(holds_alternative(cellIt->m_value)); + ASSERT_EQ(123, get(cellIt->m_value)); +} + +TEST_F(TargetTest, verify_target_requirement_in_json) +{ + const auto doc = R"DOC( + + + + ABC + 123 + + + +)DOC"; + + auto root = make_shared(); + auto tf = make_shared(Requirements { + {"Targets", ValueType::ENTITY_LIST, Target::getRequirementTargetsFactory(), false}}); + root->registerFactory("Root", tf); + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(root, doc, errors); + ASSERT_EQ(0, errors.size()); + ASSERT_TRUE(entity); + + entity::JsonEntityPrinter jsonPrinter(2, true); + auto json = jsonPrinter.print(entity); + + ASSERT_EQ(R"JSON({ + "Root": { + "Targets": { + "TargetRequirementTable": [ + { + "value": { + "R1": { + "C1": "ABC" + }, + "R2": { + "C2": 123 + } + }, + "requirementId": "req1" + } + ] + } + } +})JSON", + json); +} diff --git a/test_package/task_test.cpp b/test_package/task_test.cpp new file mode 100644 index 000000000..cf72162db --- /dev/null +++ b/test_package/task_test.cpp @@ -0,0 +1,1300 @@ +// +// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”) +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Ensure that gtest is the first header otherwise Windows raises an error +#include +// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!) + +#include +#include +#include +#include +#include +#include +#include + +#include "agent_test_helper.hpp" +#include "json_helper.hpp" +#include "mtconnect/agent.hpp" +#include "mtconnect/asset/asset.hpp" +#include "mtconnect/asset/task.hpp" +#include "mtconnect/entity/xml_parser.hpp" +#include "mtconnect/entity/xml_printer.hpp" +#include "mtconnect/printer/xml_printer_helper.hpp" +#include "mtconnect/source/adapter/adapter.hpp" + +using json = nlohmann::json; +using namespace std; +using namespace mtconnect; +using namespace mtconnect::entity; +using namespace mtconnect::source::adapter; +using namespace mtconnect::asset; +using namespace mtconnect::printer; + +// main +int main(int argc, char *argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} + +class TaskAssetTest : public testing::Test +{ +protected: + void SetUp() override + { // Create an agent with only 16 slots and 8 data items. + Task::registerAsset(); + TaskArchetype::registerAsset(); + m_writer = make_unique(true); + } + + void TearDown() override { m_writer.reset(); } + + std::unique_ptr m_writer; + std::unique_ptr m_agentTestHelper; +}; + +/// @section TaskArchetype tests + +TEST_F(TaskAssetTest, should_parse_a_part_archetype) +{ + const auto doc = + R"DOC( + MATERIAL_UNLOAD + 10 + + + + + + + + + + + + + + + + + + + + + + 1000 + + + 1500 + + + + + + + + + + + + + UnloadConv + LoadCnc + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + EXPECT_EQ("TaskArchetype", asset->getName()); + EXPECT_EQ("1aa7eece248093", asset->getAssetId()); + EXPECT_EQ("mxi_m001", asset->get("deviceUuid")); + + auto targets = asset->getList("Targets"); + ASSERT_TRUE(targets); + ASSERT_EQ(3, targets->size()); + + { + auto it = targets->begin(); + EXPECT_EQ("TargetDevice", (*it)->getName()); + EXPECT_EQ("Mazak123", (*it)->get("deviceUuid")); + + it++; + EXPECT_EQ("TargetDevice", (*it)->getName()); + EXPECT_EQ("Mazak456", (*it)->get("deviceUuid")); + + auto targetGroup = *++it; + EXPECT_EQ("TargetGroup", targetGroup->getName()); + EXPECT_EQ("MyRobots", targetGroup->get("groupId")); + + auto targetDevices = targetGroup->get("LIST"); + ASSERT_EQ(2, targetDevices.size()); + + { + auto dit = targetDevices.begin(); + EXPECT_EQ("TargetDevice", (*dit)->getName()); + EXPECT_EQ("UR123", (*dit)->get("deviceUuid")); + dit++; + + EXPECT_EQ("TargetDevice", (*dit)->getName()); + EXPECT_EQ("UR456", (*dit)->get("deviceUuid")); + } + } + + auto coordinator = asset->get("Coordinator"); + ASSERT_TRUE(coordinator); + EXPECT_EQ("Coordinator", coordinator->getName()); + auto collaborator = coordinator->get("Collaborator"); + ASSERT_TRUE(collaborator); + EXPECT_EQ("machine", collaborator->get("collaboratorId")); + EXPECT_EQ("CNC", collaborator->get("type")); + + auto collTargets = collaborator->getList("Targets"); + ASSERT_EQ(2, collTargets->size()); + { + auto it = collTargets->begin(); + EXPECT_EQ("TargetDevice", (*it)->getName()); + EXPECT_EQ("Mazak123", (*it)->get("deviceUuid")); + it++; + EXPECT_EQ("TargetDevice", (*it)->getName()); + EXPECT_EQ("Mazak456", (*it)->get("deviceUuid")); + } + + auto collaborators = asset->getList("Collaborators"); + ASSERT_EQ(2, collaborators->size()); + { + auto it = collaborators->begin(); + auto collab1 = *it; + EXPECT_EQ("Robot", collab1->get("collaboratorId")); + EXPECT_EQ("ROBOT", collab1->get("type")); + auto targets = collab1->getList("Targets"); + ASSERT_EQ(2, targets->size()); + { + auto tit = targets->begin(); + EXPECT_EQ("TargetRequirementTable", (*tit)->getName()); + EXPECT_EQ("ab", (*tit)->get("requirementId")); + + auto table = (*tit)->getValue(); + ASSERT_EQ(2, table.size()); + + const auto &row1 = get(table.find(DataSetEntry("PAYLOAD"))->m_value); + ASSERT_EQ(1, row1.size()); + EXPECT_EQ(1000, get(row1.find(TableCell("maximum"))->m_value)); + + const auto &row2 = get(table.find(DataSetEntry("REACH"))->m_value); + ASSERT_EQ(1, row2.size()); + EXPECT_EQ(1500, get(row2.find(TableCell("minimum"))->m_value)); + + tit++; + ASSERT_EQ("TargetRef", (*tit)->getName()); + ASSERT_EQ("MyRobots", (*tit)->get("groupIdRef")); + } + + it++; + auto collab2 = *it; + EXPECT_EQ("robot2", collab2->get("collaboratorId")); + EXPECT_EQ("ROBOT", collab2->get("type")); + + auto targets2 = collab2->getList("Targets"); + ASSERT_EQ(1, targets2->size()); + { + auto target = targets2->front(); + EXPECT_EQ("TargetDevice", target->getName()); + EXPECT_EQ("UR890", target->get("deviceUuid")); + } + } + + auto subtasks = asset->getList("SubTaskRefs"); + ASSERT_TRUE(subtasks); + ASSERT_EQ(2, subtasks->size()); + + { + auto it = subtasks->begin(); + EXPECT_EQ("SubTaskRef", (*it)->getName()); + EXPECT_EQ(1, (*it)->get("order")); + EXPECT_EQ("UnloadConv", (*it)->getValue()); + + it++; + EXPECT_EQ("SubTaskRef", (*it)->getName()); + EXPECT_EQ(2, (*it)->get("order")); + EXPECT_EQ("LoadCnc", (*it)->getValue()); + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(TaskAssetTest, task_archetype_should_produce_json) +{ + const auto doc = + R"DOC( + MATERIAL_UNLOAD + 10 + + + + + + + + + + + + + + + + + + + + + + 1000 + + + 1500 + + + + + + + + + + + + + UnloadConv + LoadCnc + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + entity::JsonEntityPrinter jprinter(2, true); + + auto jdoc = jprinter.print(entity); + EXPECT_EQ(R"({ + "TaskArchetype": { + "Collaborators": { + "Collaborator": [ + { + "Targets": { + "TargetRef": [ + { + "groupIdRef": "MyRobots" + } + ], + "TargetRequirementTable": [ + { + "value": { + "PAYLOAD": { + "maximum": 1000 + }, + "REACH": { + "minimum": 1500 + } + }, + "requirementId": "ab" + } + ] + }, + "collaboratorId": "Robot", + "type": "ROBOT" + }, + { + "Targets": { + "TargetDevice": [ + { + "deviceUuid": "UR890" + } + ] + }, + "collaboratorId": "robot2", + "type": "ROBOT" + } + ] + }, + "Coordinator": { + "Collaborator": { + "Targets": { + "TargetDevice": [ + { + "deviceUuid": "Mazak123" + }, + { + "deviceUuid": "Mazak456" + } + ] + }, + "collaboratorId": "machine", + "type": "CNC" + } + }, + "Priority": 10, + "SubTaskRefs": { + "SubTaskRef": [ + { + "value": "UnloadConv", + "order": 1 + }, + { + "value": "LoadCnc", + "order": 2 + } + ] + }, + "Targets": { + "TargetDevice": [ + { + "deviceUuid": "Mazak123" + }, + { + "deviceUuid": "Mazak456" + } + ], + "TargetGroup": [ + { + "TargetDevice": [ + { + "deviceUuid": "UR123" + }, + { + "deviceUuid": "UR456" + } + ], + "groupId": "MyRobots" + } + ] + }, + "TaskType": "MATERIAL_UNLOAD", + "assetId": "1aa7eece248093", + "deviceUuid": "mxi_m001", + "hash": "fCI1rCQv8BcHbzZeoMxt3kHmb9k=", + "timestamp": "2024-12-10T05:17:05.531454Z" + } +})", + jdoc); +} + +TEST_F(TaskAssetTest, task_archetype_must_have_collaborators) +{ + const auto doc = + R"DOC( + MATERIAL_UNLOAD + 10 + + + + + + + + + + + + + + + + + + UnloadConv + LoadCnc + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(1, errors.size()); + + EXPECT_EQ("TaskArchetype(Collaborators): Property Collaborators is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_archetype_must_have_collaborators_with_at_least_one_collaborator) +{ + const auto doc = + R"DOC( + MATERIAL_UNLOAD + 10 + + + + + + + + + + + + + + + + + + + UnloadConv + LoadCnc + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(3, errors.size()); + + EXPECT_EQ( + "Collaborators(Collaborator): Entity list requirement Collaborator must have at least 1 entries, 0 found"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_archetype_must_have_a_coordinator) +{ + const auto doc = + R"DOC( + MATERIAL_UNLOAD + 10 + + + + + + + + + + + + + + 1000 + + + 1500 + + + + + + + + + + + + + UnloadConv + LoadCnc + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(1, errors.size()); + + EXPECT_EQ("TaskArchetype(Coordinator): Property Coordinator is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_archetype_must_have_a_coordinator_with_a_collaborator) +{ + const auto doc = + R"DOC( + MATERIAL_UNLOAD + 10 + + + + + + + + + + + + + + + + 1000 + + + 1500 + + + + + + + + + + + + + UnloadConv + LoadCnc + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(3, errors.size()); + + EXPECT_EQ("Coordinator(Collaborator): Property Collaborator is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_archetype_should_have_optional_fields_for_sub_task_refs) +{ + const auto doc = + R"DOC( + MATERIAL_UNLOAD + 10 + + + + + + + + + + + + + + + + + + + + + + 1000 + + + 1500 + + + + + + + + + + + + + UnloadConv + LoadCnc + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + auto subtasks = asset->getList("SubTaskRefs"); + ASSERT_TRUE(subtasks); + ASSERT_EQ(2, subtasks->size()); + + { + auto it = subtasks->begin(); + EXPECT_EQ("SubTaskRef", (*it)->getName()); + EXPECT_EQ("g1", (*it)->get("group")); + EXPECT_EQ(1, (*it)->get("order")); + EXPECT_EQ(false, (*it)->get("optional")); + EXPECT_EQ(true, (*it)->get("parallel")); + EXPECT_EQ("UnloadConv", (*it)->getValue()); + + it++; + EXPECT_EQ("SubTaskRef", (*it)->getName()); + EXPECT_EQ(2, (*it)->get("order")); + EXPECT_EQ("g1", (*it)->get("group")); + EXPECT_EQ(true, (*it)->get("optional")); + EXPECT_EQ(false, (*it)->get("parallel")); + EXPECT_EQ("LoadCnc", (*it)->getValue()); + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +/// @section Task tests + +TEST_F(TaskAssetTest, should_parse_simple_task) +{ + const auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + COMMITTED + dfgfdghfkj + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + EXPECT_EQ("Task", asset->getName()); + EXPECT_EQ("2aa7eece24", asset->getAssetId()); + EXPECT_EQ("mxi_m001", asset->get("deviceUuid")); + + auto configuration = asset->get("Configuration"); + ASSERT_TRUE(configuration); + + auto relationships = configuration->getList("Relationships"); + ASSERT_TRUE(relationships); + ASSERT_EQ(1, relationships->size()); + + { + auto it = relationships->begin(); + ASSERT_EQ("A", (*it)->get("id")); + ASSERT_EQ("1aa7eece248093", (*it)->get("assetIdRef")); + ASSERT_EQ("PEER", (*it)->get("type")); + ASSERT_EQ("TASK_ARCHETYPE", (*it)->get("assetType")); + } + + auto coordinator = asset->get("Coordinator"); + ASSERT_TRUE(coordinator); + + auto collab = coordinator->get("Collaborator"); + ASSERT_TRUE(collab); + EXPECT_EQ("machine", collab->get("collaboratorId")); + EXPECT_EQ("xyz", collab->get("collaboratorDeviceUuid")); + + auto collaborators = asset->getList("Collaborators"); + ASSERT_TRUE(collaborators); + + { + auto it = collaborators->begin(); + auto collab1 = *it; + EXPECT_EQ("robot1", collab1->get("collaboratorId")); + EXPECT_EQ("abc", collab1->get("collaboratorDeviceUuid")); + EXPECT_EQ("ab", collab1->get("requirementId")); + + it++; + auto collab2 = *it; + EXPECT_EQ("robot2", collab2->get("collaboratorId")); + EXPECT_EQ("Mazak123", collab2->get("collaboratorDeviceUuid")); + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(TaskAssetTest, should_parse_simple_task_with_subtasks) +{ + const auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + COMMITTED + dfgfdghfkj + + + + + + + + + + OPEN_DOOR + COMMITTED + 2aa7eece24 + + + + + + + + + + OPEN_CHUCK + COMMITTED + 2aa7eece24 + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + auto asset = dynamic_cast(entity.get()); + ASSERT_NE(nullptr, asset); + + auto subtasks = asset->getList("SubTasks"); + ASSERT_TRUE(subtasks); + ASSERT_EQ(2, subtasks->size()); + + auto it = subtasks->begin(); + { + auto task = *it; + EXPECT_EQ("Task", task->getName()); + EXPECT_EQ("4afb7fc0", task->get("assetId")); + EXPECT_EQ("mxi_m001", task->get("deviceUuid")); + EXPECT_EQ("OPEN_DOOR", task->get("TaskType")); + EXPECT_EQ("COMMITTED", task->get("TaskState")); + EXPECT_EQ("2aa7eece24", task->get("ParentTaskAssetId")); + + auto coordinator = task->get("Coordinator"); + ASSERT_TRUE(coordinator); + + auto collab = coordinator->get("Collaborator"); + ASSERT_TRUE(collab); + + EXPECT_EQ("robot1", collab->get("collaboratorId")); + EXPECT_EQ("UR012", collab->get("collaboratorDeviceUuid")); + + auto collaborators = task->getList("Collaborators"); + EXPECT_TRUE(collaborators); + EXPECT_EQ(2, collaborators->size()); + { + auto cit = collaborators->begin(); + EXPECT_EQ("machine", (*cit)->get("collaboratorId")); + EXPECT_EQ("CNC", (*cit)->get("collaboratorDeviceUuid")); + EXPECT_EQ("ab", (*cit)->get("requirementId")); + + cit++; + EXPECT_EQ("robot2", (*cit)->get("collaboratorId")); + EXPECT_EQ("UR543", (*cit)->get("collaboratorDeviceUuid")); + } + } + + it++; + { + auto task = *it; + EXPECT_EQ("Task", task->getName()); + EXPECT_EQ("a9ef8c40", task->get("assetId")); + EXPECT_EQ("mxi_m001", task->get("deviceUuid")); + EXPECT_EQ("OPEN_CHUCK", task->get("TaskType")); + EXPECT_EQ("COMMITTED", task->get("TaskState")); + EXPECT_EQ("2aa7eece24", task->get("ParentTaskAssetId")); + + auto coordinator = task->get("Coordinator"); + ASSERT_TRUE(coordinator); + + auto collab = coordinator->get("Collaborator"); + ASSERT_TRUE(collab); + + EXPECT_EQ("machine", collab->get("collaboratorId")); + EXPECT_EQ("CNC", collab->get("collaboratorDeviceUuid")); + + auto collaborators = task->getList("Collaborators"); + EXPECT_TRUE(collaborators); + EXPECT_EQ(1, collaborators->size()); + { + auto cit = collaborators->begin(); + EXPECT_EQ("robot2", (*cit)->get("collaboratorId")); + EXPECT_EQ("UR543", (*cit)->get("collaboratorDeviceUuid")); + EXPECT_EQ("ab", (*cit)->get("requirementId")); + } + } + + // Round trip test + entity::XmlPrinter printer; + printer.print(*m_writer, entity, {}); + + string content = m_writer->getContent(); + ASSERT_EQ(content, doc); +} + +TEST_F(TaskAssetTest, task_should_produce_json) +{ + const auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + COMMITTED + dfgfdghfkj + + + + + + + + + + OPEN_DOOR + COMMITTED + 2aa7eece24 + + + + + + + + + + OPEN_CHUCK + COMMITTED + 2aa7eece24 + + + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(0, errors.size()); + + entity::JsonEntityPrinter jprinter(2, true); + + auto jdoc = jprinter.print(entity); + EXPECT_EQ(R"({ + "Task": { + "Collaborators": { + "Collaborator": [ + { + "collaboratorDeviceUuid": "UR543", + "collaboratorId": "robot1", + "requirementId": "ab" + }, + { + "collaboratorDeviceUuid": "UR012", + "collaboratorId": "robot2" + } + ] + }, + "Configuration": { + "Relationships": { + "AssetRelationship": [ + { + "assetIdRef": "1aa7eece248093", + "assetType": "TASK_ARCHETYPE", + "id": "A", + "type": "PEER" + } + ] + } + }, + "Coordinator": { + "Collaborator": { + "collaboratorDeviceUuid": "CNC", + "collaboratorId": "machine" + } + }, + "ParentTaskAssetId": "dfgfdghfkj", + "SubTasks": { + "Task": [ + { + "Collaborators": { + "Collaborator": [ + { + "collaboratorDeviceUuid": "CNC", + "collaboratorId": "machine", + "requirementId": "ab" + }, + { + "collaboratorDeviceUuid": "UR543", + "collaboratorId": "robot2" + } + ] + }, + "Coordinator": { + "Collaborator": { + "collaboratorDeviceUuid": "UR012", + "collaboratorId": "robot1" + } + }, + "ParentTaskAssetId": "2aa7eece24", + "TaskState": "COMMITTED", + "TaskType": "OPEN_DOOR", + "assetId": "4afb7fc0", + "deviceUuid": "mxi_m001", + "timestamp": "2024-12-10T05:17:05.531454Z" + }, + { + "Collaborators": { + "Collaborator": [ + { + "collaboratorDeviceUuid": "UR543", + "collaboratorId": "robot2", + "requirementId": "ab" + } + ] + }, + "Coordinator": { + "Collaborator": { + "collaboratorDeviceUuid": "CNC", + "collaboratorId": "machine" + } + }, + "ParentTaskAssetId": "2aa7eece24", + "TaskState": "COMMITTED", + "TaskType": "OPEN_CHUCK", + "assetId": "a9ef8c40", + "deviceUuid": "mxi_m001", + "timestamp": "2024-12-10T05:17:05.531454Z" + } + ] + }, + "TaskState": "COMMITTED", + "TaskType": "MATERIAL_UNLOAD", + "assetId": "2aa7eece24", + "deviceUuid": "mxi_m001", + "hash": "fCI1rCQv8BcHbzZeoMxt3kHmb9k=", + "timestamp": "2024-12-10T05:17:05.531454Z" + } +})", + jdoc); +} + +TEST_F(TaskAssetTest, task_must_have_a_coordinator) +{ + const auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + COMMITTED + dfgfdghfkj + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(1, errors.size()); + + EXPECT_EQ("Task(Coordinator): Property Coordinator is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_must_have_a_coordinator_with_a_collaborator) +{ + const auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + COMMITTED + dfgfdghfkj + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(3, errors.size()); + + EXPECT_EQ("Coordinator(Collaborator): Property Collaborator is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_must_have_collaborators) +{ + const auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + COMMITTED + dfgfdghfkj + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(1, errors.size()); + + EXPECT_EQ("Task(Collaborators): Property Collaborators is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_must_have_collaborators_with_at_least_one_collaborator) +{ + const auto doc = + R"DOC( + + + + + +MATERIAL_UNLOAD +COMMITTED +dfgfdghfkj + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(3, errors.size()); + + EXPECT_EQ( + "Collaborators(Collaborator): Entity list requirement Collaborator must have at least 1 entries, 0 found"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_must_have_a_task_state) +{ + constexpr auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + dfgfdghfkj + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(1, errors.size()); + + EXPECT_EQ("Task(TaskState): Property TaskState is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_must_have_a_task_type) +{ + constexpr auto doc = + R"DOC( + + + + + + COMMITTED + dfgfdghfkj + + + + + + + +)DOC"; + + ErrorList errors; + entity::XmlParser parser; + + auto entity = parser.parse(Asset::getRoot(), doc, errors); + ASSERT_EQ(1, errors.size()); + + EXPECT_EQ("Task(TaskType): Property TaskType is required and not provided"s, + errors.front()->what()); +} + +TEST_F(TaskAssetTest, task_should_accept_all_task_states) +{ + constexpr auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + {} + dfgfdghfkj + + + + + + + +)DOC"; + + list states {"INACTIVE", "PREPARING", "COMMITTING", "COMMITTED", "COMPLETE", "FAIL"}; + + for (const auto &state : states) + { + ErrorList errors; + entity::XmlParser parser; + + auto sd = format(doc, state); + auto entity = parser.parse(Asset::getRoot(), sd, errors); + EXPECT_EQ(0, errors.size()) << "should accept task state: " << state; + } +} + +TEST_F(TaskAssetTest, task_should_not_accept_invalid_task_states) +{ + constexpr auto doc = + R"DOC( + + + + + + MATERIAL_UNLOAD + {} + dfgfdghfkj + + + + + + + +)DOC"; + + list states {"BAD", "STATE", "123", "DONE", ""}; + + for (const auto &state : states) + { + ErrorList errors; + entity::XmlParser parser; + + auto sd = format(doc, state); + auto entity = parser.parse(Asset::getRoot(), sd, errors); + EXPECT_EQ(1, errors.size()) << "Should fail task state: " << state; + } +}