diff --git a/n_request.c b/n_request.c index 0be148fb..bd989bab 100644 --- a/n_request.c +++ b/n_request.c @@ -527,13 +527,22 @@ J *_noteTransactionShouldLock(J *req, bool lockNotecard) _LockNote(); } - // Make sure that if anything was pending in the notecard's receive buffer, - // that it is line-terminated so that the request we're about to send - // begins on a new line. + // If a reset of the I/O interface is required for any reason, do it now. if (resetRequired) { NOTE_C_LOG_DEBUG("Resetting Notecard I/O Interface..."); - _Reset(); - resetRequired = false; + if ((resetRequired = !_Reset())) { + if (lockNotecard) { + _UnlockNote(); + } + _Free(json); + _TransactionStop(); + const char *errStr = ERRSTR("failed to reset Notecard interface {io}", c_iobad); + if (cmdFound) { + NOTE_C_LOG_ERROR(errStr); + return NULL; + } + return _errDoc(id, errStr); + } } // If we're performing retries, this is where we come back to diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f4a0c38a..c6fd18f7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -168,6 +168,7 @@ add_test(NoteNewRequest_test) add_test(NotePayload_test) add_test(NotePayloadRetrieveAfterSleep_test) add_test(NotePayloadSaveAndSleep_test) +add_test(NotePing_test) add_test(NotePrint_test) add_test(NotePrintf_test) add_test(NotePrintln_test) diff --git a/test/src/NotePing_test.cpp b/test/src/NotePing_test.cpp new file mode 100644 index 00000000..1803e844 --- /dev/null +++ b/test/src/NotePing_test.cpp @@ -0,0 +1,259 @@ +/*! + * @file NotePing_test.cpp + * + * Written by the Blues Inc. team. + * + * Copyright (c) 2026 Blues Inc. MIT License. Use of this source code is + * governed by licenses granted by the copyright holder including that found in + * the + * LICENSE + * file. + * + */ + +#include +#include + +#include "n_lib.h" + +#include +#include + +DEFINE_FFF_GLOBALS +FAKE_VALUE_FUNC(bool, _noteTransactionStart, uint32_t) +FAKE_VOID_FUNC(_noteTransactionStop) +FAKE_VOID_FUNC(_noteLockNote) +FAKE_VOID_FUNC(_noteUnlockNote) +FAKE_VALUE_FUNC(const char *, _noteJSONTransaction, const char *, size_t, char **, uint32_t) +FAKE_VALUE_FUNC(bool, _noteSerialAvailable) +FAKE_VALUE_FUNC(char, _noteSerialReceive) +FAKE_VALUE_FUNC(bool, _noteHardReset) + +extern volatile int hookActiveInterface; + +namespace +{ + +enum class PingResponse { + Echo, + WrongNonce, + Error, + InvalidJson, + NoResponse, + TransactionError, +}; + +PingResponse pingResponse = PingResponse::Echo; +uint32_t currentMs = 1000; +size_t serialBytesRemaining = 0; +size_t serialBytesRemainingAtTransaction = 0; +size_t lastRequestLength = 0; +uint32_t lastTransactionTimeoutMs = 0; +bool lastRequestEndedWithNewline = false; +bool lastRequestHadCrc = false; + +char *copyString(const char *src) +{ + const size_t len = strlen(src); + char *dst = static_cast(malloc(len + 1)); + if (dst != NULL) { + memcpy(dst, src, len + 1); + } + return dst; +} + +uint32_t getMs() +{ + return currentMs; +} + +void delayMs(uint32_t ms) +{ + currentMs += ms; +} + +bool serialAvailable() +{ + return serialBytesRemaining > 0; +} + +char serialReceive() +{ + if (serialBytesRemaining > 0) { + --serialBytesRemaining; + } + return 'x'; +} + +char *makeResponse(const char *text) +{ + J *rsp = JCreateObject(); + if (rsp == NULL) { + return NULL; + } + JAddStringToObject(rsp, "text", text); + char *json = JPrintUnformatted(rsp); + JDelete(rsp); + return json; +} + +const char *pingTransaction(const char *request, size_t reqLen, char **response, uint32_t timeoutMs) +{ + lastRequestLength = reqLen; + lastTransactionTimeoutMs = timeoutMs; + lastRequestEndedWithNewline = (reqLen > 0 && request[reqLen - 1] == '\n'); + lastRequestHadCrc = (strstr(request, "\"crc\"") != NULL); + serialBytesRemainingAtTransaction = serialBytesRemaining; + + if (pingResponse == PingResponse::TransactionError) { + return ERRSTR("transaction failed {io}", c_ioerr); + } + if (response == NULL || pingResponse == PingResponse::NoResponse) { + return NULL; + } + if (pingResponse == PingResponse::InvalidJson) { + *response = copyString("not-json"); + return NULL; + } + if (pingResponse == PingResponse::Error) { + *response = copyString("{\"err\":\"failed\"}"); + return NULL; + } + + char *requestCopy = static_cast(malloc(reqLen + 1)); + if (requestCopy == NULL) { + return ERRSTR("malloc failed {mem}", c_mem); + } + memcpy(requestCopy, request, reqLen); + requestCopy[reqLen] = '\0'; + if (reqLen > 0 && requestCopy[reqLen - 1] == '\n') { + requestCopy[reqLen - 1] = '\0'; + } + + J *req = JParse(requestCopy); + free(requestCopy); + if (req == NULL) { + return ERRSTR("parse failed {bad}", c_bad); + } + + const char *nonce = JGetString(req, "text"); + *response = makeResponse(pingResponse == PingResponse::WrongNonce ? "wrong" : nonce); + JDelete(req); + + return NULL; +} + +void resetTestState(void) +{ + RESET_FAKE(_noteTransactionStart); + RESET_FAKE(_noteTransactionStop); + RESET_FAKE(_noteLockNote); + RESET_FAKE(_noteUnlockNote); + RESET_FAKE(_noteJSONTransaction); + RESET_FAKE(_noteSerialAvailable); + RESET_FAKE(_noteSerialReceive); + RESET_FAKE(_noteHardReset); + + pingResponse = PingResponse::Echo; + currentMs = 1000; + serialBytesRemaining = 0; + serialBytesRemainingAtTransaction = 0; + lastRequestLength = 0; + lastTransactionTimeoutMs = 0; + lastRequestEndedWithNewline = false; + lastRequestHadCrc = false; + resetRequired = false; + + NoteSetFnDefault(malloc, free, delayMs, getMs); + RESET_FAKE(_noteLockNote); + RESET_FAKE(_noteUnlockNote); + + hookActiveInterface = NOTE_C_INTERFACE_SERIAL; + _noteTransactionStart_fake.return_val = true; + _noteJSONTransaction_fake.custom_fake = pingTransaction; + _noteSerialAvailable_fake.custom_fake = serialAvailable; + _noteSerialReceive_fake.custom_fake = serialReceive; +} + +SCENARIO("NotePing") +{ + resetTestState(); + + SECTION("returns true when echo response matches the nonce") { + CHECK(NotePing()); + CHECK(_noteJSONTransaction_fake.call_count == 1); + CHECK(lastRequestLength > 0); + CHECK(lastRequestEndedWithNewline); + CHECK(!lastRequestHadCrc); + CHECK(lastTransactionTimeoutMs == 500); + CHECK(_noteTransactionStart_fake.call_count == 1); + CHECK(_noteTransactionStop_fake.call_count == 1); + CHECK(_noteLockNote_fake.call_count == 1); + CHECK(_noteUnlockNote_fake.call_count == 1); + } + + SECTION("returns false when the echoed nonce does not match") { + pingResponse = PingResponse::WrongNonce; + + CHECK(!NotePing()); + CHECK(_noteJSONTransaction_fake.call_count == 1); + } + + SECTION("returns false when the response contains an error") { + pingResponse = PingResponse::Error; + + CHECK(!NotePing()); + CHECK(_noteJSONTransaction_fake.call_count == 1); + } + + SECTION("returns false when the response is not valid JSON") { + pingResponse = PingResponse::InvalidJson; + + CHECK(!NotePing()); + CHECK(_noteJSONTransaction_fake.call_count == 1); + } + + SECTION("returns false when the transaction fails and does not retry") { + pingResponse = PingResponse::TransactionError; + + CHECK(!NotePing()); + CHECK(_noteJSONTransaction_fake.call_count == 1); + } + + SECTION("returns false when no response is received and does not retry") { + pingResponse = PingResponse::NoResponse; + + CHECK(!NotePing()); + CHECK(_noteJSONTransaction_fake.call_count == 1); + } + + SECTION("does not reset even when resetRequired is set") { + resetRequired = true; + pingResponse = PingResponse::TransactionError; + + CHECK(!NotePing()); + CHECK(_noteHardReset_fake.call_count == 0); + CHECK(resetRequired == true); + } + + SECTION("returns false without locking or transacting when transaction start fails") { + _noteTransactionStart_fake.return_val = false; + + CHECK(!NotePing()); + CHECK(_noteLockNote_fake.call_count == 0); + CHECK(_noteJSONTransaction_fake.call_count == 0); + CHECK(_noteTransactionStop_fake.call_count == 0); + } + + SECTION("drains serial input before sending the ping") { + serialBytesRemaining = 3; + + CHECK(NotePing()); + CHECK(_noteSerialReceive_fake.call_count == 3); + CHECK(serialBytesRemainingAtTransaction == 0); + } + + resetTestState(); +} + +}