Test Coverage and mruby logging - #614
Conversation
Close the Tier 1 coverage gaps on the MQTT/JSON ingestion path: - topic_mapping_test: cover TopicMapper::operator() and resolve() — JSON object/array messages, all four topic-resolution strategies (device/name, default device, last path segment, device scan), the resolution cache, and the unresolved / no-topic paths. (7.7% -> 100% lines) - message_mapping_test (new): cover DataMapper — observation creation with data-source capture, invalid-value rejection, and SHDR re-processing of unmapped messages. (7.5% -> 77% lines; remaining lines are an unreachable non-string branch with a latent bad_variant_access, flagged separately) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the Tier 2 gap on sink/rest_sink/server.cpp (was ~49% lines). The Swagger/OpenAPI doc generation (renderSwaggerResponse, AddRouting, AddParameter, for both Writer and PrettyWriter) was entirely untested: - exercise /swagger JSON (compact and pretty) with a routing covering every parameter type and a documented path parameter, hitting all AddParameter type/default-visitor branches and AddRouting summary/description paths - exercise the /swagger HTML branch via an Accept: text/html request (added an optional m_accept to the test Client) - cover Server::allowPutFrom success and the unresolvable-host failure path server.cpp line coverage ~49% -> ~88%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover parseUrl() in agent_config.cpp (was 0%): a URL-based adapter config is parsed into protocol, host, port, and topics, and a URL with an empty host is rejected. parseUrl line coverage 0% -> 100%. Note: the config-variable-expansion code (expandConfigVariables / ExpandValue / ExpandValues) remains uncovered because it has no call site in loadConfig -- it is dead code. Flagged separately for wiring-up or removal rather than covering a back door. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Focused coverage pass on critical agent.cpp paths (additive tests only): - source_failed_removes_source_and_keeps_running_when_others_remain: fail one of two external adapters and verify it is removed while the agent keeps running (no shutdown). Agent::sourceFailed 0% -> ~69%. - get_device_by_name_const_resolves_default_known_and_unknown: cover the const getDeviceByName overload (empty -> default, known -> device, unknown -> null). 0% -> 100%. The sourceFailed fatal-shutdown branch and the not-found branch (which has a latent null-deref bug, flagged separately) are intentionally not exercised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR expands automated test coverage around topic/message mapping, HTTP swagger rendering, agent behavior, and configuration parsing, while also improving logging and robustness in the adapter pipeline and Ruby integration.
Changes:
- Added/expanded pipeline- and mapping-focused tests (topic mapping, message mapping, agent behavior, HTTP swagger rendering, adapter URL parsing).
- Improved logging and diagnostics (agent adapter lifecycle/recovery logs, mruby logger integration, richer Ruby transform error output).
- Small API/utility adjustments (URL formatting const-correctness + stream operator, schema version propagation during config load, safer error logging in
DataMapper).
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test_package/topic_mapping_test.cpp | Adds TopicMapper-focused helpers and new mapping/cache/path-scan tests. |
| test_package/message_mapping_test.cpp | New DataMapper tests covering resolved/unresolved mapping and non-string value behavior. |
| test_package/http_server_test.cpp | Adds Accept header support in test client and swagger rendering/allowPutFrom tests. |
| test_package/config_test.cpp | Adds adapter URL parsing test coverage and malformed URL rejection test. |
| test_package/CMakeLists.txt | Registers new message_mapping test target. |
| test_package/agent_test.cpp | Adds tests for getDeviceByName const behavior and source failure handling. |
| src/mtconnect/utilities.hpp | Makes Url::getUrlText const and adds operator<< for Url. |
| src/mtconnect/source/adapter/agent_adapter/agent_adapter.cpp | Adds additional lifecycle/recovery logging to agent adapter. |
| src/mtconnect/sink/rest_sink/rest_service.cpp | Fixes typo “stapshot” → “snapshot” in REST routing documentation. |
| src/mtconnect/ruby/ruby_vm.hpp | Updates mruby logging to use BOOST_LOG_SEV with the agent logger. |
| src/mtconnect/ruby/ruby_transform.hpp | Adds additional logging when Ruby transform execution fails. |
| src/mtconnect/ruby/ruby_observation.hpp | Adds detailed Ruby observation wrapper documentation/notes. |
| src/mtconnect/pipeline/message_mapper.hpp | Makes error logging type-safe for non-string message values. |
| src/mtconnect/configuration/agent_config.hpp | Exposes expandConfigVariables publicly and documents behavior. |
| src/mtconnect/configuration/agent_config.cpp | Runs config variable expansion earlier and adjusts schema version option handling. |
| src/mtconnect/agent.cpp | Attempts to derive schema version from the device XML when not explicitly provided. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| auto dev = m_devices.find(device); | ||
| EXPECT_NE(m_devices.end(), dev) << "Cannot find device: " << device; | ||
| Properties ps(props); | ||
| ErrorList errors; | ||
| auto di = DataItem::make(ps, errors); | ||
| dev->second->addDataItem(di, errors); | ||
| return di; |
| DataItemPtr makeDataItem(const std::string &device, const Properties &props) | ||
| { | ||
| auto dev = m_devices.find(device); | ||
| EXPECT_NE(m_devices.end(), dev) << "Cannot find device: " << device; | ||
| Properties ps(props); | ||
| ErrorList errors; | ||
| auto di = DataItem::make(ps, errors); | ||
| m_dataItems.emplace(di->getId(), di); | ||
| dev->second->addDataItem(di, errors); | ||
| return di; | ||
| } |
| req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); | ||
| req.set(http::field::content_type, contentType); | ||
| if (!m_accept.empty()) | ||
| req.set(http::field::accept, m_accept); |
| if (self->canRecover() && self->m_streamRequest) | ||
| { | ||
| LOG(info) << "Attmempting to recover stream for " << self->m_url; | ||
| self->recover(); | ||
| } |
| /// @brief output operator for Value | ||
| /// @param os the output stream | ||
| /// @param v the Value to output | ||
| inline std::ostream &operator<<(std::ostream &os, const Url &url) |
| if (!HasOption(options, configuration::SchemaVersion) && | ||
| m_agent->getSchemaVersion()) | ||
| { | ||
| options[configuration::SchemaVersion] = *m_agent->getSchemaVersion(); | ||
| } | ||
| else | ||
| { | ||
| options[configuration::SchemaVersion] = std::format("{}.{}", std::to_string(AGENT_VERSION_MAJOR), | ||
| std::to_string(AGENT_VERSION_MINOR)); | ||
|
|
||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
test_package/topic_mapping_test.cpp:124
makeDeviceOnlyDataItemusesEXPECT_NEto validate the device exists, but then immediately dereferencesdev->second. If the device is missing, this is undefined behavior (the test continues after a failed EXPECT). UseASSERT_NEhere to abort the test before dereferencing.
auto dev = m_devices.find(device);
EXPECT_NE(m_devices.end(), dev) << "Cannot find device: " << device;
test_package/http_server_test.cpp:104
m_acceptis documented as applying to “the next request”, butrequest()never clears it after setting the header. This makes the Accept header sticky across subsequent requests and can cause cross-test/request coupling. Clearm_acceptafter applying it (or rename it to indicate it is persistent).
if (!m_accept.empty())
req.set(http::field::accept, m_accept);
src/mtconnect/source/adapter/agent_adapter/agent_adapter.cpp:244
- Typo in log message: "Attmempting" -> "Attempting".
LOG(info) << "Attmempting to recover stream for " << self->m_url;
src/mtconnect/configuration/agent_config.cpp:981
- This logic overwrites an explicitly configured
SchemaVersionoption: whenHasOption(SchemaVersion)is true, execution falls into theelsebranch and resets the option to the agent version. This is a breaking behavior change for configuration. Only setSchemaVersionwhen it is not already present.
if (!HasOption(options, configuration::SchemaVersion) &&
m_agent->getSchemaVersion())
{
options[configuration::SchemaVersion] = *m_agent->getSchemaVersion();
}
src/mtconnect/utilities.hpp:1017
- The Doxygen for
operator<<refers to “Value” and parameterv, but the overload is forUrland takesurl. This makes generated docs misleading.
/// @brief output operator for Value
/// @param os the output stream
/// @param v the Value to output
inline std::ostream &operator<<(std::ostream &os, const Url &url)
src/mtconnect/agent.cpp:132
- This adds an extra
m_xmlParser->parseFile()in the constructor to derivem_schemaVersion, butinitialize()already parses the same devices file vialoadXMLDeviceFile()(and setsm_schemaVersionthere). WhenSchemaVersionisn’t configured, this results in parsing the devices file twice during startup.
if (!m_schemaVersion)
{
m_xmlParser->parseFile(m_deviceXmlPath, dynamic_cast<printer::XmlPrinter *>(m_printers["xml"].get()));
m_schemaVersion = m_xmlParser->getSchemaVersion();
}
| BOOST_LOG_STREAM_WITH_PARAMS(::boost::log::trivial::logger::get(), | ||
| (::boost::log::keywords::severity = level)) | ||
| << mrb_str_to_cstr(mrb, msg); | ||
| BOOST_LOG_SEV(agent_logger::get(), level) << mrb_str_to_cstr(mrb, msg); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/mtconnect/ruby/ruby_vm.hpp:69
- ruby_vm.hpp now logs via agent_logger/BOOST_LOG_SEV, but this header does not include mtconnect/logging.hpp (or the required Boost.Log headers). That introduces an include-order dependency and can fail to compile in translation units that include ruby_vm.hpp without already including the logging headers.
BOOST_LOG_SEV(agent_logger::get(), level) << mrb_str_to_cstr(mrb, msg);
test_package/message_mapping_test.cpp:2
- The file header contains a duplicated word ("Copyright Copyright"). This looks like a copy/paste typo and should be corrected to avoid propagating it to new files.
// Copyright Copyright 2009-2025, AMT – The Association For Manufacturing Technology (“AMT”)
src/mtconnect/configuration/agent_config.cpp:988
- This uses std::format but this .cpp does not include , which can break the build depending on transitive includes/toolchain. Since this is just constructing ".", simple concatenation avoids the extra header dependency.
options[configuration::SchemaVersion] = std::format("{}.{}", std::to_string(AGENT_VERSION_MAJOR),
std::to_string(AGENT_VERSION_MINOR));
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 285 out of 285 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
test_package/message_mapping_test.cpp:116
makeDataItemusesASSERT_NEinside a non-void helper. Like other gtestASSERT_*macros, this can expand toreturn;on failure, which is not valid in a function returningDataItemPtrand can cause compile errors. Prefer a non-fatal check with an explicitreturn nullptr.
test_package/message_mapping_test.cpp:58MockPipelineContract::findDevice/findDataItemusestd::map::operator[], which inserts a default-constructed (null) entry when the key is missing. That mutates the test fixture state and can mask lookup failures. Usefind()and returnnullptrwhen not found.
This issue also appears on line 112 of the same file.
| DataItemPtr makeDeviceOnlyDataItem(const std::string &device, const Properties &props) | ||
| { | ||
| auto dev = m_devices.find(device); | ||
| ASSERT_NE(m_devices.end(), dev) << "Cannot find device: " << device; | ||
| Properties ps(props); |
| options[configuration::SchemaVersion] = std::format("{}.{}", std::to_string(AGENT_VERSION_MAJOR), | ||
| std::to_string(AGENT_VERSION_MINOR)); | ||
|
|
||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 285 out of 285 changed files in this pull request and generated no new comments.
Suppressed comments (5)
test_package/message_mapping_test.cpp:33
- This helper depends on
using namespace std;and a specific include order to compile because mtconnect/pipeline/message_mapper.hpp currently uses unqualifiedstring(and references adapter Handler types) without providing the required declarations itself. Encoding this workaround in a new test makes the test brittle; it would be better to make message_mapper.hpp self-contained (qualifystd::stringand include/forward-declare needed types) and then remove the workaround here.
test_package/topic_mapping_test.cpp:132 - DataItem::make() can return nullptr (and/or populate errors). This helper adds the returned pointer to the device without asserting creation succeeded, which can turn a config/test issue into a null dereference or an unhelpful downstream failure. Add assertions for
dianderrorsbefore calling addDataItem().
test_package/message_mapping_test.cpp:120 - DataItem::make() can return nullptr (and/or populate errors). This helper immediately dereferences
difor its id and stores it, which can crash the test binary if DataItem creation fails. Add assertions fordianderrorsbefore using it.
src/mtconnect/agent.cpp:131 - When SchemaVersion is not provided via options, the constructor now parses the device XML solely to extract the schema version, but the same file is parsed again during initialize() via loadXMLDeviceFile(). This double-parse is avoidable and adds startup overhead. Consider deferring schema-version detection to loadXMLDeviceFile() (where the parse is already required) and only setting printers’ schema version after that parse completes.
if (!m_schemaVersion)
{
m_xmlParser->parseFile(m_deviceXmlPath, dynamic_cast<printer::XmlPrinter *>(m_printers["xml"].get()));
m_schemaVersion = m_xmlParser->getSchemaVersion();
}
test_package/http_server_test.cpp:1099
- This test uses a DNS-style hostname to exercise the failure path of Server::allowPutFrom(), but allowPutFrom() performs a resolver lookup (src/mtconnect/sink/rest_sink/server.cpp:165-171). Depending on the CI runner’s resolver configuration, this can introduce long timeouts/flakiness. Prefer an immediately-invalid hostname so resolution fails fast without DNS/network dependencies.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 285 out of 285 changed files in this pull request and generated no new comments.
Suppressed comments (4)
test_package/message_mapping_test.cpp:124
- makeDataItem() assumes DataItem::make() always returns a valid pointer and immediately dereferences it (getId()) and passes it to addDataItem(). If DataItem::make() fails and returns nullptr (or reports errors), this will crash the test binary.
test_package/topic_mapping_test.cpp:132 - makeDeviceOnlyDataItem() calls DataItem::make() and then unconditionally passes the result to addDataItem(). DataItem::make() can return nullptr (and/or populate errors), which would make this a potential null dereference inside addDataItem().
test_package/message_mapping_test.cpp:33 - This test needs an unusual include order plus a global
using namespace std;to compile message_mapper.hpp (see comment). That strongly suggests message_mapper.hpp is not self-contained (unqualifiedstringusage / missing includes or forward declarations), and the workaround risks leakingstdsymbols into other includes in this TU.
test_package/message_mapping_test.cpp:58 - MockPipelineContract::findDevice/findDataItem use operator[] on the maps, which mutates the maps by inserting missing keys. That can hide lookup bugs and make tests pass unintentionally (missing keys become present with nullptr values). Prefer a non-mutating lookup and return nullptr when not found.
This issue also appears on line 112 of the same file.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 285 out of 285 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/mtconnect/mqtt/mqtt_server_impl.hpp:40
using namespace std;was removed from this header, but the file still uses unqualifiedstring(e.g.,GetOption<string>(...)and a lambda returning-> string). As-is, this will not compile unless a consumer happens to injectstd::stringinto scope. Either fully-qualify those occurrences or add a narrowusing std::string;in this namespace.
test_package/message_mapping_test.cpp:58MockPipelineContract::findDevice/findDataItemusestd::map::operator[], which inserts a new (null) entry on misses. That can mask lookup problems in tests and mutates fixture state unexpectedly. Preferfind()and returnnullptrwhen not found.
src/mtconnect/agent.cpp:131- This constructor now parses the device XML via
m_xmlParser->parseFile(...)to derivem_schemaVersion, butinitialize()later callsloadXMLDeviceFile()which parses the same file again. This duplicates potentially expensive XML parsing work and can introduce duplicated side-effects from parsing. Consider refactoring so schema version can be derived without a full parse, or cache/reuse the first parse result so the device XML is only parsed once.
if (!m_schemaVersion)
{
m_xmlParser->parseFile(m_deviceXmlPath, dynamic_cast<printer::XmlPrinter *>(m_printers["xml"].get()));
m_schemaVersion = m_xmlParser->getSchemaVersion();
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 285 out of 285 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/mtconnect/mqtt/mqtt_server_impl.hpp:40
- Removing
using namespace std;means this header now has several unqualifiedstringuses later (e.g., the TLS password callback return type andGetOption<string>(...)), which will fail to compile. Either qualify those asstd::stringthroughout or add a narrowusing std::string;in the namespace scope.
test_package/message_mapping_test.cpp:58 findDevice/findDataItemusemap::operator[], which inserts missing keys and mutates the maps during lookups. This can hide test setup issues and affect later assertions. Preferfind()and returnnullptron misses without side effects.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 285 out of 285 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/mtconnect/mqtt/mqtt_server_impl.hpp:40
- The file no longer has
using namespace std;, but it still uses unqualifiedstring(e.g., TLS password callback return type) andGetOption<string>later in the file. Withoutstd::qualification (or a local alias), this will not compile.
src/mtconnect/agent.cpp:131 - This constructor now parses the device XML (
m_xmlParser->parseFile) solely to obtain the schema version, butinitialize()immediately parses the same file again vialoadXMLDeviceFile(). This guarantees the device XML is parsed twice on startup, which can significantly increase agent startup time for large device files.
if (!m_schemaVersion)
{
m_xmlParser->parseFile(m_deviceXmlPath, dynamic_cast<printer::XmlPrinter *>(m_printers["xml"].get()));
m_schemaVersion = m_xmlParser->getSchemaVersion();
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 285 out of 285 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/mtconnect/configuration/agent_config.cpp:982
- SchemaVersion is being injected into
optionsafter theAgentis constructed, so the Agent will not see this value (it copiesoptionsin its constructor). Also,m_agent->getSchemaVersion()is read beforem_agent->initialize(...), so it will typically be empty and this block will fall back even when the device XML schema version could differ. Consider setting the default SchemaVersion before constructing the Agent so the Agent and sinks are configured consistently.
test_package/message_mapping_test.cpp:58 MockPipelineContract::findDevice/findDataItemusestd::map::operator[], which will insert a default-constructed entry on lookup misses. That side effect can hide errors in tests and can also mutate the maps in ways that affect later expectations.
src/mtconnect/mqtt/mqtt_server_impl.hpp:360- The log message string contains a non-printable control character after "Certificates". This can make logs hard to read and may cause issues in some terminals/log processors.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 286 out of 286 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
src/mtconnect/mqtt/mqtt_server_impl.hpp:361
- This log message contains a stray control character (looks like U+0013) in the string literal (between "Certificates" and "."). It will make logs hard to search/copy and can cause encoding issues depending on the sink.
test_package/message_mapping_test.cpp:58 MockPipelineContract::findDeviceandfindDataItemusestd::map::operator[], which will insert a new entry with a default-constructed (null) value when the key is missing. That mutates test state during lookups and can mask missing-device/data-item bugs.
Prefer find() and return nullptr when not present.
| def tool_requires_version(self, package, version): | ||
| self.output.info(f"Checking version of {package} > {version}") | ||
| buf = io.StringIO() | ||
| command = f"{package} --version" | ||
| res = self.run(command, shell=True, stdout=buf) | ||
| self.output.info(f"Command: '{command}' returned {res}") | ||
| ver = [0, 0, 0] | ||
| if res == 0: | ||
| text = buf.getvalue() | ||
| self.output.debug(f"{command} returned:\n{text}") | ||
| ver = [int(d) for d in re.search(r"\d+\.\d+\.\d+", text).group(0).split('.')] | ||
| self.output.info(f"Version of {package} is {ver}") |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 286 out of 286 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/mtconnect/pipeline/message_mapper.hpp:51
DataMapper's constructor takessource::adapter::Handler*, but this header does not declareHandler(nor include the header that defines it). This makesmessage_mapper.hppnon-self-contained and forces include-order workarounds in users/tests.
test_package/conanfile.py:10- Duplicate/unused imports at the top of this recipe make it harder to maintain (and can confuse linters).
osis imported twice and isn't used in this file.
Added additional tests
Fixed mruby logging issues
Added more logging in the agent adapter