diff --git a/CHANGELOG.md b/CHANGELOG.md index f017b64e3..efd2f5483 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. ### Changed - MiElHVAC auto-enable i-See widevane when setting AirDirection (#24860) +- `PubSub` lib renamed `TasmotaPubSub`, hardening fixes and comprehensive non-regression tests ### Fixed - BLE EQ3 float output in mqtt messages regression from v15.4.0.2 (#24869) diff --git a/lib/default/pubsubclient-2.8.13/LICENSE.txt b/lib/default/TasmotaPubSub/LICENSE.txt similarity index 83% rename from lib/default/pubsubclient-2.8.13/LICENSE.txt rename to lib/default/TasmotaPubSub/LICENSE.txt index 12c1689e6..22d2e99fb 100644 --- a/lib/default/pubsubclient-2.8.13/LICENSE.txt +++ b/lib/default/TasmotaPubSub/LICENSE.txt @@ -1,4 +1,7 @@ -Copyright (c) 2008-2020 Nicholas O'Leary +TasmotaPubSub - Tasmota fork of the PubSubClient MQTT library + +Copyright (c) 2008-2020 Nicholas O'Leary (original PubSubClient) +Copyright (c) 2020-2025 Theo Arends and Tasmota contributors (fork changes) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/lib/default/TasmotaPubSub/README.md b/lib/default/TasmotaPubSub/README.md new file mode 100644 index 000000000..df27548e4 --- /dev/null +++ b/lib/default/TasmotaPubSub/README.md @@ -0,0 +1,14 @@ +# TasmotaPubSub + +Tasmota fork of Nick O'Leary's [PubSubClient](https://github.com/knolleary/pubsubclient) MQTT library, +hardened and adapted for use within Tasmota on ESP8266 and ESP32. + +The public API and the `PubSubClient` class name are kept for compatibility, so Tasmota code +continues to use `#include `. Tasmota-specific changes in the source are marked +with `// Start Tasmota patch` comments (see `CHANGES.txt`). + +Original library by Nick O'Leary — https://pubsubclient.knolleary.net + +## License + +Released under the MIT License. See `LICENSE.txt`. diff --git a/lib/default/TasmotaPubSub/library.json b/lib/default/TasmotaPubSub/library.json new file mode 100644 index 000000000..2ee2fa8f3 --- /dev/null +++ b/lib/default/TasmotaPubSub/library.json @@ -0,0 +1,26 @@ +{ + "name": "TasmotaPubSub", + "version": "2.8.13", + "keywords": "ethernet, mqtt, m2m, iot, TasmotaPubSub", + "description": "Tasmota fork of the PubSubClient MQTT library. A client library for MQTT messaging, a lightweight messaging protocol ideal for small devices. This fork is hardened and adapted for Tasmota on ESP8266 and ESP32. Based on the original PubSubClient by Nick O'Leary.", + "repository": { + "type": "git", + "url": "https://github.com/arendst/Tasmota/lib/TasmotaPubSub" + }, + "authors": [ + { + "name": "Nick O'Leary", + "email": "nick.oleary@gmail.com", + "url": "http://knolleary.net" + }, + { + "name": "Stephan Hadinger" + } + ], + "exclude": "tests", + "frameworks": "arduino", + "platforms": [ + "espressif8266", + "espressif32" + ] +} diff --git a/lib/default/TasmotaPubSub/library.properties b/lib/default/TasmotaPubSub/library.properties new file mode 100644 index 000000000..89362164f --- /dev/null +++ b/lib/default/TasmotaPubSub/library.properties @@ -0,0 +1,9 @@ +name=TasmotaPubSub +version=2.8.13 +author=Nick O'Leary , Stephan Hadinger +maintainer=Stephan Hadinger +sentence=Tasmota fork of the PubSubClient library for MQTT messaging. +paragraph=MQTT is a lightweight messaging protocol ideal for small devices. This library allows you to send and receive MQTT messages. This fork is hardened and adapted for Tasmota on ESP8266 and ESP32. Based on the original PubSubClient by Nick O'Leary. +category=Communication +url=https://github.com/arendst/Tasmota +architectures=esp8266,esp32 diff --git a/lib/default/pubsubclient-2.8.13/src/PubSubClient.cpp b/lib/default/TasmotaPubSub/src/PubSubClient.cpp similarity index 75% rename from lib/default/pubsubclient-2.8.13/src/PubSubClient.cpp rename to lib/default/TasmotaPubSub/src/PubSubClient.cpp index c51879af0..6cf7e3c45 100644 --- a/lib/default/pubsubclient-2.8.13/src/PubSubClient.cpp +++ b/lib/default/TasmotaPubSub/src/PubSubClient.cpp @@ -1,7 +1,18 @@ /* PubSubClient.cpp - A simple client for MQTT. - Nick O'Leary - http://knolleary.net + + TasmotaPubSub - Tasmota fork of the PubSubClient MQTT library. + + Original author: + Nick O'Leary - http://knolleary.net + Tasmota fork maintained by Theo Arends and the Tasmota contributors. + + SPDX-FileCopyrightText: 2008-2020 Nicholas O'Leary + SPDX-FileCopyrightText: 2020-2025 Theo Arends and Tasmota contributors + + SPDX-License-Identifier: MIT + + Tasmota-specific changes are marked with "Start Tasmota patch" comments. */ #include "PubSubClient.h" @@ -187,7 +198,22 @@ boolean PubSubClient::connect(const char *id, const char *user, const char *pass } // End Tasmota patch - if(_client->connected()) { +// Start Tasmota patch + // A null client id would crash the later strnlen()/writeString(). MQTT allows a + // zero-length id (with clean session) but not a null pointer. + if (id == nullptr) { + return false; + } + // Reject a Will with an out-of-range QoS before it is shifted into the CONNECT + // flags byte, and a Will topic without a message. + if (willTopic != nullptr) { + if (willQos > 2 || willMessage == nullptr) { + return false; + } + } +// End Tasmota patch + + if (_client->connected()) { result = 1; } else { @@ -231,10 +257,10 @@ boolean PubSubClient::connect(const char *id, const char *user, const char *pass v = v|0x02; } - if(user != NULL) { + if (user != NULL) { v = v|0x80; - if(pass != NULL) { + if (pass != NULL) { v = v|(0x80>>1); } } @@ -252,10 +278,10 @@ boolean PubSubClient::connect(const char *id, const char *user, const char *pass length = writeString(willMessage,this->buffer,length); } - if(user != NULL) { + if (user != NULL) { CHECK_STRING_LENGTH(length,user) length = writeString(user,this->buffer,length); - if(pass != NULL) { + if (pass != NULL) { CHECK_STRING_LENGTH(length,pass) length = writeString(pass,this->buffer,length); } @@ -281,7 +307,11 @@ boolean PubSubClient::connect(const char *id, const char *user, const char *pass uint8_t llen; uint32_t len = readPacket(&llen); - if (len == 4) { +// Start Tasmota patch +// Only accept a well-formed CONNACK: exact packet type (0x20), Remaining Length 2. +// Previously any 4-byte frame ending in 0 was treated as a successful connection. + if (len == 4 && (buffer[0] == MQTTCONNACK) && (buffer[1] == 2)) { +// End Tasmota patch if (buffer[3] == 0) { lastInActivity = millis(); pingOutstanding = false; @@ -310,7 +340,7 @@ boolean PubSubClient::readByte(uint8_t * result) { // End Tasmota patch uint32_t previousMillis = millis(); - while(!_client->available()) { + while (!_client->available()) { // Start Tasmota patch // yield(); @@ -319,7 +349,7 @@ boolean PubSubClient::readByte(uint8_t * result) { // End Tasmota patch uint32_t currentMillis = millis(); - if(currentMillis - previousMillis >= ((int32_t) this->socketTimeout * 1000)){ + if (currentMillis - previousMillis >= ((int32_t) this->socketTimeout * 1000)) { return false; } } @@ -328,10 +358,10 @@ boolean PubSubClient::readByte(uint8_t * result) { } // reads a byte into result[*index] and increments index -boolean PubSubClient::readByte(uint8_t * result, uint16_t * index){ +boolean PubSubClient::readByte(uint8_t * result, uint16_t * index) { uint16_t current_index = *index; uint8_t * write_address = &(result[current_index]); - if(readByte(write_address)){ + if (readByte(write_address)) { *index = current_index + 1; return true; } @@ -340,7 +370,7 @@ boolean PubSubClient::readByte(uint8_t * result, uint16_t * index){ uint32_t PubSubClient::readPacket(uint8_t* lengthLength) { uint16_t len = 0; - if(!readByte(this->buffer, &len)) return 0; + if (!readByte(this->buffer, &len)) { return 0; } bool isPublish = (this->buffer[0]&0xF0) == MQTTPUBLISH; uint32_t multiplier = 1; uint32_t length = 0; @@ -355,7 +385,7 @@ uint32_t PubSubClient::readPacket(uint8_t* lengthLength) { _client->stop(); return 0; } - if(!readByte(&digit)) return 0; + if (!readByte(&digit)) { return 0; } this->buffer[len++] = digit; length += (digit & 127) * multiplier; multiplier <<=7; //multiplier *= 128 @@ -368,10 +398,37 @@ uint32_t PubSubClient::readPacket(uint8_t* lengthLength) { *lengthLength = len-1; +// Start Tasmota patch (DoS mitigation + sentinel byte) +// In non-stream mode, if the declared packet cannot fit the buffer while leaving +// at least one spare byte (needed by consumers that NUL-terminate at buffer[len]), +// close the connection immediately instead of draining the whole Remaining Length +// byte-by-byte. Draining a large or trickle-fed body would block the event loop. + // total wire packet size = fixed header (1) + length bytes (llen) + Remaining Length + uint32_t total_packet = (uint32_t)1 + (uint32_t)(*lengthLength) + length; + + // Configurable hard cap, independent of buffer allocation (0 = disabled). + // Applies to both stream and non-stream mode: a larger packet is refused and the + // connection closed rather than draining/streaming an unbounded body. + if (this->maxIncomingPacketSize != 0 && total_packet > this->maxIncomingPacketSize) { + _state = MQTT_DISCONNECTED; + _client->stop(); + return 0; + } + + // Non-stream: the packet must also fit the buffer, leaving one spare byte for a + // downstream NUL terminator. Draining an oversized body would block the + // event loop, so close immediately instead. + if (!this->stream && total_packet >= (uint32_t)this->bufferSize) { + _state = MQTT_DISCONNECTED; + _client->stop(); + return 0; + } +// End Tasmota patch + if (isPublish) { // Read in topic length to calculate bytes to skip over for Stream writing - if(!readByte(this->buffer, &len)) return 0; - if(!readByte(this->buffer, &len)) return 0; + if (!readByte(this->buffer, &len)) { return 0; } + if (!readByte(this->buffer, &len)) { return 0; } skip = (this->buffer[*lengthLength+1]<<8)+this->buffer[*lengthLength+2]; start = 2; if (this->buffer[0]&MQTTQOS1) { @@ -382,7 +439,7 @@ uint32_t PubSubClient::readPacket(uint8_t* lengthLength) { uint32_t idx = len; for (uint32_t i = start;istream) { if (isPublish && idx-*lengthLength-2>skip) { this->stream->write(digit); @@ -394,11 +451,22 @@ uint32_t PubSubClient::readPacket(uint8_t* lengthLength) { len++; } idx++; + +// Start Tasmota patch +// Periodically yield while consuming a large (typically streamed) body so the +// watchdog is not starved. Non-stream packets are < bufferSize so this rarely fires. + if ((i & 0x3FF) == 0) { delay(0); } +// End Tasmota patch } - if (!this->stream && idx > this->bufferSize) { +// Start Tasmota patch (sentinel byte) +// Use >= so an exact-buffer packet is also ignored, guaranteeing a spare byte for +// downstream NUL-termination at buffer[len]. Non-stream oversized packets are +// normally already rejected above; this is defense-in-depth. + if (!this->stream && idx >= this->bufferSize) { len = 0; // This will cause the packet to be ignored. } +// End Tasmota patch return len; } @@ -443,7 +511,29 @@ boolean PubSubClient::loop() { // Start Tasmota patch // Observed heap corruption in some cases since v10.0.0 // Also see https://github.com/knolleary/pubsubclient/pull/843 - if (llen+3+tl>this->bufferSize) { +// +// Decode QoS from bits 1-2 and reject unsupported/illegal values +// (2 = unsupported, 3 = protocol violation) before parsing. +// Validate the topic and (for QoS 1) the message-id bytes against the +// number of bytes actually received (len), not just the buffer capacity. +// This prevents out-of-bounds reads and payload-length underflow when a +// broker sends a topic length that overruns the packet. + uint8_t qos = (this->buffer[0] & 0x06) >> 1; + if (qos > 1) { + _state = MQTT_DISCONNECTED; + _client->stop(); + return false; + } + + // Bytes required before the payload starts: + // fixed header (1) + length bytes (llen) + topic length field (2) + // + topic (tl) + message id for QoS 1 (2) + uint32_t header_len = (uint32_t)llen + 3 + tl + (qos ? 2 : 0); + // Topic must be non-empty, the header must fit within the received + // bytes, and (redundantly) within the buffer with room for the NUL. + if ((tl == 0) || + (header_len > (uint32_t)len) || + ((uint32_t)llen + 3 + tl > (uint32_t)this->bufferSize)) { _state = MQTT_DISCONNECTED; _client->stop(); return false; @@ -454,7 +544,7 @@ boolean PubSubClient::loop() { this->buffer[llen+2+tl] = 0; /* end the topic as a 'C' string with \x00 */ char *topic = (char*) this->buffer+llen+2; // msgId only present for QOS>0 - if ((this->buffer[0]&0x06) == MQTTQOS1) { + if (qos == 1) { msgId = (this->buffer[llen+3+tl]<<8)+this->buffer[llen+3+tl+1]; payload = this->buffer+llen+3+tl+2; callback(topic,payload,len-llen-3-tl-2); @@ -509,6 +599,11 @@ boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigne boolean PubSubClient::publish(const char* topic, const uint8_t* payload, unsigned int plength, boolean retained) { if (connected()) { +// Start Tasmota patch + if ((topic == nullptr) || ((payload == nullptr) && (plength != 0))) { + return false; + } +// End Tasmota patch if (this->bufferSize < MQTT_MAX_HEADER_SIZE + 2+strnlen(topic, this->bufferSize) + plength) { // Too long return false; @@ -552,7 +647,16 @@ boolean PubSubClient::publish_P(const char* topic, const uint8_t* payload, unsig return false; } +// Start Tasmota patch + if ((topic == nullptr) || ((payload == nullptr) && (plength != 0))) { + return false; + } tlen = strnlen(topic, this->bufferSize); + // Header + topic must fit the working buffer (payload itself is streamed, not buffered). + if ((size_t)MQTT_MAX_HEADER_SIZE + 2 + tlen > this->bufferSize) { + return false; + } +// End Tasmota patch header = MQTTPUBLISH; if (retained) { @@ -568,7 +672,7 @@ boolean PubSubClient::publish_P(const char* topic, const uint8_t* payload, unsig } this->buffer[pos++] = digit; llen++; - } while(len>0); + } while (len>0); pos = writeString(topic,this->buffer,pos); @@ -593,6 +697,21 @@ boolean PubSubClient::publish_P(const char* topic, const uint8_t* payload, unsig boolean PubSubClient::beginPublish(const char* topic, unsigned int plength, boolean retained) { if (connected()) { + +// Start Tasmota patch + if (topic == nullptr) { + return false; + } + // Header + topic must fit the working buffer (writeString starts at MQTT_MAX_HEADER_SIZE). + size_t tlen = strnlen(topic, this->bufferSize); + if ((size_t)MQTT_MAX_HEADER_SIZE + 2 + tlen > this->bufferSize) { + return false; + } + // Remaining Length now uses 32-bit accounting (buildHeader emits up to 4 bytes), + // so payloads above 65535 bytes are framed correctly instead of being truncated. + uint32_t remaining_length = (uint32_t)plength + 2 + tlen; +// End Tasmota patch + // Send the header and variable length field uint16_t length = MQTT_MAX_HEADER_SIZE; length = writeString(topic,this->buffer,length); @@ -600,7 +719,7 @@ boolean PubSubClient::beginPublish(const char* topic, unsigned int plength, bool if (retained) { header |= 1; } - size_t hlen = buildHeader(header, this->buffer, plength+length-MQTT_MAX_HEADER_SIZE); + size_t hlen = buildHeader(header, this->buffer, remaining_length); uint16_t rc = _client->write(this->buffer+(MQTT_MAX_HEADER_SIZE-hlen),length-(MQTT_MAX_HEADER_SIZE-hlen)); // Start Tasmota patch @@ -658,12 +777,17 @@ size_t PubSubClient::write(const uint8_t *buffer, size_t size) { } -size_t PubSubClient::buildHeader(uint8_t header, uint8_t* buf, uint16_t length) { +// Start Tasmota patch +// length is the MQTT Remaining Length and must be 32-bit: encoded as 1-4 bytes +// (max 268435455). A uint16_t parameter previously truncated payloads > 65535, +// desynchronising the connection. +size_t PubSubClient::buildHeader(uint8_t header, uint8_t* buf, uint32_t length) { uint8_t lenBuf[4]; uint8_t llen = 0; uint8_t digit; uint8_t pos = 0; - uint16_t len = length; + uint32_t len = length; +// End Tasmota patch do { digit = len & 127; //digit = len %128 @@ -673,7 +797,11 @@ size_t PubSubClient::buildHeader(uint8_t header, uint8_t* buf, uint16_t length) } lenBuf[pos++] = digit; llen++; - } while(len>0); +// Start Tasmota patch +// Remaining Length is encoded in at most 4 bytes; bound the loop to the lenBuf[4] +// size so an out-of-range length can never overflow lenBuf[] or underflow buf[4-llen]. + } while (len>0 && llen<4); +// End Tasmota patch buf[4-llen] = header; for (int i=0;i 0) && result) { + while ((bytesRemaining > 0) && result) { bytesToWrite = (bytesRemaining > MQTT_MAX_TRANSFER_SIZE)?MQTT_MAX_TRANSFER_SIZE:bytesRemaining; rc = _client->write(writeBuf,bytesToWrite); result = (rc == bytesToWrite); @@ -719,17 +847,22 @@ boolean PubSubClient::subscribe(const char* topic) { } boolean PubSubClient::subscribe(const char* topic, uint8_t qos) { - size_t topicLength = strnlen(topic, this->bufferSize); - if (topic == 0) { +// Start Tasmota patch +// Check for null before calling strnlen(). The SUBSCRIBE packet also writes a +// trailing QoS byte, so it needs one more byte than UNSUBSCRIBE: the buffer must +// hold header(5) + msgId(2) + topic-length(2) + topic + qos(1) = 10 + topicLength. + if (topic == nullptr) { return false; } + size_t topicLength = strnlen(topic, this->bufferSize); if (qos > 1) { return false; } - if (this->bufferSize < 9 + topicLength) { + if (this->bufferSize < 10 + topicLength) { // Too long return false; } +// End Tasmota patch if (connected()) { // Leave room in the buffer for header and variable length field uint16_t length = MQTT_MAX_HEADER_SIZE; @@ -747,14 +880,17 @@ boolean PubSubClient::subscribe(const char* topic, uint8_t qos) { } boolean PubSubClient::unsubscribe(const char* topic) { - size_t topicLength = strnlen(topic, this->bufferSize); - if (topic == 0) { +// Start Tasmota patch +// Check for null before calling strnlen(). + if (topic == nullptr) { return false; } + size_t topicLength = strnlen(topic, this->bufferSize); if (this->bufferSize < 9 + topicLength) { // Too long return false; } +// End Tasmota patch if (connected()) { uint16_t length = MQTT_MAX_HEADER_SIZE; nextMsgId++; @@ -859,17 +995,17 @@ PubSubClient& PubSubClient::setCallback(MQTT_CALLBACK_SIGNATURE) { return *this; } -PubSubClient& PubSubClient::setClient(Client& client){ +PubSubClient& PubSubClient::setClient(Client& client) { this->_client = &client; return *this; } -PubSubClient& PubSubClient::setStream(Stream& stream){ +PubSubClient& PubSubClient::setStream(Stream& stream) { this->stream = &stream; return *this; } -int PubSubClient::state() { +int PubSubClient::state() const { return this->_state; } @@ -878,23 +1014,33 @@ boolean PubSubClient::setBufferSize(uint16_t size) { // Cannot set it back to 0 return false; } +// Start Tasmota patch +// Commit bufferSize only after a successful (re)allocation. Previously bufferSize +// was set even when malloc() failed, leaving the object reporting a non-zero +// capacity while buffer == nullptr, which later operations would dereference. if (this->bufferSize == 0) { - this->buffer = (uint8_t*)malloc(size); - } else { - uint8_t* newBuffer = (uint8_t*)realloc(this->buffer, size); - if (newBuffer != NULL) { - this->buffer = newBuffer; - } else { + uint8_t* newBuffer = (uint8_t*)malloc(size); + if (newBuffer == NULL) { return false; } + this->buffer = newBuffer; + } else { + uint8_t* newBuffer = (uint8_t*)realloc(this->buffer, size); + if (newBuffer == NULL) { + return false; + } + this->buffer = newBuffer; } this->bufferSize = size; - return (this->buffer != NULL); + return true; +// End Tasmota patch } -uint16_t PubSubClient::getBufferSize() { +uint16_t PubSubClient::getBufferSize() const { return this->bufferSize; } + + PubSubClient& PubSubClient::setKeepAlive(uint16_t keepAlive) { this->keepAlive = keepAlive; return *this; diff --git a/lib/default/pubsubclient-2.8.13/src/PubSubClient.h b/lib/default/TasmotaPubSub/src/PubSubClient.h similarity index 81% rename from lib/default/pubsubclient-2.8.13/src/PubSubClient.h rename to lib/default/TasmotaPubSub/src/PubSubClient.h index 4edf5ec57..25447064f 100644 --- a/lib/default/pubsubclient-2.8.13/src/PubSubClient.h +++ b/lib/default/TasmotaPubSub/src/PubSubClient.h @@ -1,7 +1,18 @@ /* - PubSubClient.h - A simple client for MQTT. - Nick O'Leary - http://knolleary.net + PubSubClient.h - A simple client for MQTT. + + TasmotaPubSub - Tasmota fork of the PubSubClient MQTT library. + + Original author: + Nick O'Leary - http://knolleary.net + Tasmota fork maintained by Theo Arends and the Tasmota contributors. + + SPDX-FileCopyrightText: 2008-2020 Nicholas O'Leary + SPDX-FileCopyrightText: 2020-2025 Theo Arends and Tasmota contributors + + SPDX-License-Identifier: MIT + + Tasmota-specific changes are marked with "Start Tasmota patch" comments. */ #ifndef PubSubClient_h @@ -107,7 +118,7 @@ private: // Returns the size of the header // Note: the header is built at the end of the first MQTT_MAX_HEADER_SIZE bytes, so will start // (MQTT_MAX_HEADER_SIZE - ) bytes into the buffer - size_t buildHeader(uint8_t header, uint8_t* buf, uint16_t length); + size_t buildHeader(uint8_t header, uint8_t* buf, uint32_t length); // Tasmota patch: 32-bit Remaining Length IPAddress ip; // Start Tasmota patch @@ -119,6 +130,13 @@ private: uint16_t port; Stream* stream; int _state; + +// Start Tasmota patch +// Hard cap on the total size (fixed header + Remaining Length) of an inbound packet +// that the client will accept, independent of the working buffer allocation. +// 0 = disabled: fall back to the buffer size for non-stream, unbounded for stream. + uint32_t maxIncomingPacketSize = 0; +// End Tasmota patch public: PubSubClient(); PubSubClient(Client& client); @@ -137,6 +155,13 @@ public: ~PubSubClient(); +// Start Tasmota patch +// The class owns and frees `buffer`; a copy would share ownership and lead to a +// double-free / use-after-free. Copying is therefore disabled. + PubSubClient(const PubSubClient&) = delete; + PubSubClient& operator=(const PubSubClient&) = delete; +// End Tasmota patch + PubSubClient& setServer(IPAddress ip, uint16_t port); PubSubClient& setServer(uint8_t * ip, uint16_t port); PubSubClient& setServer(const char * domain, uint16_t port); @@ -147,7 +172,15 @@ public: PubSubClient& setSocketTimeout(uint16_t timeout); boolean setBufferSize(uint16_t size); - uint16_t getBufferSize(); + uint16_t getBufferSize() const; + +// Start Tasmota patch + // Set the maximum accepted inbound packet size (total wire bytes). 0 disables the + // cap. When set, a larger declared packet closes the connection instead of being + // drained/streamed. Useful to bound stream-mode input and tune DoS resistance. + PubSubClient& setMaxIncomingPacketSize(uint32_t size) { this->maxIncomingPacketSize = size; return *this; } + uint32_t getMaxIncomingPacketSize() const { return this->maxIncomingPacketSize; } +// End Tasmota patch boolean connect(const char* id); boolean connect(const char* id, const char* user, const char* pass); @@ -189,7 +222,7 @@ public: boolean unsubscribe(const char* topic); boolean loop(); boolean connected(); - int state(); + int state() const; }; diff --git a/lib/default/TasmotaPubSub/tests/.gitignore b/lib/default/TasmotaPubSub/tests/.gitignore new file mode 100644 index 000000000..7badd32ef --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/.gitignore @@ -0,0 +1,8 @@ +# Build artifacts for the host-based doctest test system +build/ + +# Legacy artifacts (harmless if absent) +.build +tmpbin +logs +*.pyc diff --git a/lib/default/TasmotaPubSub/tests/Makefile b/lib/default/TasmotaPubSub/tests/Makefile new file mode 100644 index 000000000..b7825c725 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/Makefile @@ -0,0 +1,105 @@ +# Makefile for the TasmotaPubSub host-based doctest test system. +# +# Builds the *unmodified* library under test (../src/PubSubClient.cpp) together +# with the Arduino environment shim, the test support library (src/lib/*.cpp), +# and every test translation unit (src/*.cpp -> test_main.cpp + *_test.cpp) +# into a single doctest binary: build/pubsub_tests. +# +# Usage: +# make # or `make all` - build build/pubsub_tests +# make test # build + run the whole binary +# make baseline # build + run only the baseline suite (-ts=baseline) +# make hardening # build + run only the hardening suite (-ts=hardening) +# make clean # remove the build/ directory +# +# make SANITIZE=0 ... # build without ASan/UBSan +# make test TS=-ts=baseline ARGS=--no-colors # extra pass-through flags + +# --- Toolchain ------------------------------------------------------------- +# CXX defaults to `c++` so this works with both Apple clang and GNU g++. +CXX ?= c++ + +# --- Layout ---------------------------------------------------------------- +BUILDDIR := build +OBJDIR := $(BUILDDIR)/obj +BIN := $(BUILDDIR)/pubsub_tests + +# Library under test (never modified). +LIB_SRC := ../src/PubSubClient.cpp + +# Arduino shim + test support library. Wildcard so future .cpp files under +# src/lib are picked up automatically. +SHIM_SRCS := $(wildcard src/lib/*.cpp) + +# Test translation units: test_main.cpp (task 4.2) + all *_test.cpp. +# Wildcard so the build does not fail before any test sources exist and so +# future *_test.cpp files are compiled automatically. +TEST_SRCS := $(wildcard src/*.cpp) + +# --- Object files ---------------------------------------------------------- +# Objects live under build/obj (mirrored subtrees) to keep the source tree clean. +LIB_OBJ := $(OBJDIR)/lib/PubSubClient.o +SHIM_OBJS := $(patsubst src/lib/%.cpp,$(OBJDIR)/shim/%.o,$(SHIM_SRCS)) +TEST_OBJS := $(patsubst src/%.cpp,$(OBJDIR)/test/%.o,$(TEST_SRCS)) +OBJS := $(LIB_OBJ) $(SHIM_OBJS) $(TEST_OBJS) + +# --- Flags ----------------------------------------------------------------- +# Sanitizers ON by default; toggle off with `make SANITIZE=0`. +SANITIZE ?= 1 +ifeq ($(SANITIZE),0) + SAN_FLAGS := +else + SAN_FLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all +endif + +# Include order matters: src/lib FIRST so the Arduino shim headers shadow any +# platform headers, then the library headers in ../src. +INCLUDES := -I src/lib -I ../src + +CXXFLAGS := -std=c++17 -Wall -Wextra -DESP32 $(INCLUDES) $(SAN_FLAGS) +LDFLAGS := $(SAN_FLAGS) + +# Suite selection / extra doctest flag pass-through. +# TS - suite selector, e.g. TS=-ts=baseline +# ARGS - any additional doctest flags +TS ?= +ARGS ?= + +# --- Targets --------------------------------------------------------------- +.PHONY: all test baseline hardening clean +.DEFAULT_GOAL := all + +all: $(BIN) + +$(BIN): $(OBJS) + @mkdir -p $(@D) + $(CXX) $(LDFLAGS) $(OBJS) -o $@ + +# The library under test is compiled with AllocShim.h force-included so its +# malloc()/realloc() calls route through the test allocation interposer +# (F-07 / Requirement 13.2). This force-include is scoped to THIS object only, +# so no other TU (shim, doctest, tests) is affected. The library source itself +# is never modified. +$(LIB_OBJ): $(LIB_SRC) + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) -include src/lib/AllocShim.h -c $< -o $@ + +$(OBJDIR)/shim/%.o: src/lib/%.cpp + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +$(OBJDIR)/test/%.o: src/%.cpp + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +test: $(BIN) + ./$(BIN) $(TS) $(ARGS) + +baseline: $(BIN) + ./$(BIN) -ts=baseline $(ARGS) + +hardening: $(BIN) + ./$(BIN) -ts=hardening $(ARGS) + +clean: + rm -rf $(BUILDDIR) diff --git a/lib/default/TasmotaPubSub/tests/README.md b/lib/default/TasmotaPubSub/tests/README.md new file mode 100644 index 000000000..91847e83f --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/README.md @@ -0,0 +1,174 @@ +# TasmotaPubSub Host Test System + +A host-based (no ESP32/ESP8266 hardware required) unit-test system for the +TasmotaPubSub `PubSubClient` MQTT library. It compiles the **unmodified** +library (`../src/PubSubClient.cpp` / `.h`) against a small host-side Arduino +environment shim, drives it through a scriptable mock transport, and validates +its observable MQTT 3.1.1 wire behavior with the +[doctest](https://github.com/doctest/doctest) single-header framework. + +All assertions go through the public API and the recorded/scripted wire bytes of +the mock transport — never through private members — so the suite stays +re-runnable unchanged across future refactors of the library (including a later +MQTT 5 migration). + +> The library under test is never modified. If a test fails, the fix belongs in +> the test (or the finding gets hardened in the library separately) — not in the +> library source to make a test pass. + +## Layout + +``` +tests/ + Makefile # single-binary build + run targets + README.md # this file + src/ + test_main.cpp # the one TU that defines doctest's main() + *_test.cpp # test suites (baseline + hardening cases) + lib/ # Arduino shim + test support library + doctest.h # vendored single-header framework + Arduino.h ... # host Arduino environment shim (Client/Stream/String/...) + TestClock.* # virtual millis()/delay() clock + MockClient.* # scriptable mock transport (inbound/outbound, faults) + MockStream.* # stream-mode mock + MqttPacket.* # MQTT packet builder + parser + CallbackContractAdapter.* # Tasmota MqttDataHandler NUL-write contract + AllocShim.h # malloc/realloc interposer (F-07 alloc-failure tests) + FindingStatus.h # expected-fail registry (see below) + build/ # generated; git-ignored +``` + +## Requirements + +- A C++17 host compiler: Apple `clang`/`clang++` or GNU `g++`. The Makefile + defaults to `CXX = c++`. +- `make`. + +No third-party dependencies: doctest is vendored at `src/lib/doctest.h`. + +## Building and running + +Run everything from the `tests/` directory. + +```sh +make # build build/pubsub_tests (default target: all) +make test # build + run the whole binary +make baseline # build + run only the Baseline suite (-ts=baseline) +make hardening # build + run only the Hardening suite (-ts=hardening) +make clean # remove the build/ directory +``` + +The build produces a single binary, `build/pubsub_tests`, which links the +unmodified `PubSubClient.cpp`, the Arduino shim, the test support library, and +every `src/*_test.cpp`. A compilation failure yields a nonzero exit status and +produces no binary. + +## Sanitizers (the `SANITIZE` toggle) + +AddressSanitizer and UndefinedBehaviorSanitizer are **on by default** +(`-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all`). +Any memory or undefined-behavior violation aborts the run with a nonzero exit +status. This is the primary detector for the memory-safety findings (F-01, F-02, +F-08, and the callback-contract boundary). + +Turn sanitizers off with: + +```sh +make SANITIZE=0 # build without ASan/UBSan +make SANITIZE=0 test # ... and run +``` + +## Suite selection (`-ts=`) + +Every test case is tagged into exactly one doctest test suite: `baseline` or +`hardening`. The `make baseline` / `make hardening` targets pass the matching +`-ts=` selector to the binary. You can also select suites (or any other doctest +flag) directly: + +```sh +./build/pubsub_tests -ts=baseline # only baseline cases +./build/pubsub_tests -ts=hardening # only hardening cases +./build/pubsub_tests # everything +./build/pubsub_tests -ts=baseline -tc="*publish*" # filter within a suite +``` + +You can also pass extra flags through the Makefile: + +```sh +make test TS=-ts=baseline ARGS=--no-colors +make hardening ARGS=-s # -s = show successful assertions +``` + +## Baseline vs Hardening + +- **Baseline suite** — characterization tests that lock in the library's + *current* observable MQTT 3.1.1 behavior across the full public API and the + wire protocol (CONNECT/CONNACK, PUBLISH/PUBACK, SUBSCRIBE/SUBACK, + UNSUBSCRIBE/UNSUBACK, PINGREQ/PINGRESP, DISCONNECT). These are a non-regression + safety net and **must all pass** against the current library. `make baseline` + is expected to be entirely green. + +- **Hardening suite** — tests that encode the *correct/hardened* behavior derived + from the static-analysis findings F-01..F-12. The current fork is already + partially hardened, so most hardening cases pass today. The genuinely open + findings are marked **expected-to-fail** (see the registry below) so their + failure is reported as *expected* and does not fail the run. `make hardening` + is expected to exit zero, reporting only the designated expected failures and + **no unexpected passes**. + +### Current expected-fail findings + +As of the last empirical verification (task 12.1), the open findings — the ones +whose hardening cases are expected to fail against the current library — are: + +| Finding | Area | Hardening case(s) | +|---|---|---| +| F-05 | Partial transport write reuses a desynchronized connection | `streaming_test.cpp`, `findings_test.cpp` | +| F-10 | `subscribe` ignores the SUBACK return code (reports success unconditionally) | `subscribe_test.cpp`, `findings_test.cpp` | +| F-11 | `disconnect()` with no argument sends no DISCONNECT packet | `connect_test.cpp`, `findings_test.cpp` | +| F-03 (deadline) | Trickle-fed inbound bytes are bounded only by a per-byte timeout, not a packet-wide deadline | `buffer_test.cpp`, `findings_test.cpp` | + +All other findings (F-01, F-02, F-04, F-06, F-07, F-08, F-09, and the F-03 +oversized prompt-close aspect) are already hardened and their cases pass. + +## Expected-fail registry workflow (`FindingStatus.h`) + +The expected-fail state of each finding lives in a single header, +`src/lib/FindingStatus.h`. Each finding maps to a doctest test-case decorator via +`FINDING_MARKER(Fxx)`: + +- `FINDING_OPEN` → `doctest::should_fail()` — the case is expected to fail + (library not yet hardened). A failure is reported as *expected* and does not + set a nonzero exit status. If such a case unexpectedly **passes**, doctest + reports an *unexpected pass* and fails the run — that is the signal that the + finding has been hardened. +- `FINDING_HARDENED` → `doctest::skip(false)` — a no-op decorator: the case still + runs and is expected to pass. + +We deliberately use `should_fail()` and never `may_fail()`, because `may_fail()` +would hide an unexpected pass. + +Hardening cases reference their finding like this: + +```cpp +TEST_CASE("F-05 partial write disables reuse" * FINDING_MARKER(F05)) { ... } +``` + +### Flipping a finding when the library gets hardened + +When the library is patched so a previously-open finding now behaves correctly, +`make hardening` will surface its case as an **unexpected pass** (and fail the +run). To resolve it, flip that finding's marker in `FindingStatus.h` — a +one-line edit: + +```c +// before (open): +#define FINDING_MARKER_F05 FINDING_OPEN +// after (hardened): +#define FINDING_MARKER_F05 FINDING_HARDENED +``` + +Then re-run `make hardening` and confirm it is green again with no unexpected +passes. The reverse edit (`FINDING_HARDENED` → `FINDING_OPEN`) applies if a +regression re-opens a finding. Always confirm the marker matches the empirically +observed behavior of the current library before committing. diff --git a/lib/default/TasmotaPubSub/tests/src/MockClient_test.cpp b/lib/default/TasmotaPubSub/tests/src/MockClient_test.cpp new file mode 100644 index 000000000..ca11a7a53 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/MockClient_test.cpp @@ -0,0 +1,260 @@ +/* + MockClient_test.cpp - Unit checks for the MockClient core (task 6.1). + + These verify the scriptable mock transport itself (not the library under + test): inbound read/consume, exhaustion semantics, outbound capture, and + connection / close tracking. They are tagged into the baseline suite so they + run as part of the standard non-regression pass. +*/ + +#include +#include + +#include "doctest.h" + +#include "Client.h" +#include "IPAddress.h" +#include "MockClient.h" +#include "TestClock.h" + +TEST_SUITE("baseline") { + + TEST_CASE("MockClient reads scripted inbound bytes in order and consumes them") { + MockClient c; + c.pushInbound({0x10, 0x20, 0x30}); + + CHECK(c.available() == 3); + CHECK(c.peek() == 0x10); // peek does not consume + CHECK(c.available() == 3); + + CHECK(c.read() == 0x10); + CHECK(c.read() == 0x20); + CHECK(c.available() == 1); + CHECK(c.read() == 0x30); + + // Exhausted queue. + CHECK(c.available() == 0); + CHECK(c.read() == -1); + CHECK(c.peek() == -1); + } + + TEST_CASE("MockClient read(buf,size) consumes up to size and reports the count") { + MockClient c; + c.pushInbound({1, 2, 3, 4, 5}); + + uint8_t buf[3] = {0, 0, 0}; + int n = c.read(buf, 3); + CHECK(n == 3); + CHECK(buf[0] == 1); + CHECK(buf[1] == 2); + CHECK(buf[2] == 3); + CHECK(c.available() == 2); + + // Requesting more than remains returns only what is left. + uint8_t rest[8] = {0}; + int m = c.read(rest, 8); + CHECK(m == 2); + CHECK(rest[0] == 4); + CHECK(rest[1] == 5); + CHECK(c.available() == 0); + + // Further reads on an empty queue yield nothing. + CHECK(c.read(rest, 8) == 0); + } + + TEST_CASE("MockClient pushInbound appends across multiple calls") { + MockClient c; + c.pushInbound({0xAA}); + c.pushInbound({0xBB, 0xCC}); + CHECK(c.available() == 3); + CHECK(c.read() == 0xAA); + CHECK(c.read() == 0xBB); + CHECK(c.read() == 0xCC); + CHECK(c.available() == 0); + } + + TEST_CASE("MockClient clearInbound drops unconsumed bytes") { + MockClient c; + c.pushInbound({1, 2, 3}); + CHECK(c.read() == 1); + c.clearInbound(); + CHECK(c.available() == 0); + CHECK(c.read() == -1); + } + + TEST_CASE("MockClient captures outbound writes (single and buffer)") { + MockClient c; + CHECK(c.write(uint8_t(0x30)) == 1); + + const uint8_t payload[] = {0x00, 0x03, 'a', 'b', 'c'}; + CHECK(c.write(payload, sizeof(payload)) == sizeof(payload)); + + const std::vector expected = {0x30, 0x00, 0x03, 'a', 'b', 'c'}; + CHECK(c.outbound() == expected); + + c.clearOutbound(); + CHECK(c.outbound().empty()); + } + + TEST_CASE("MockClient write(nullptr) records nothing") { + MockClient c; + CHECK(c.write(static_cast(nullptr), 4) == 0); + CHECK(c.outbound().empty()); + } + + TEST_CASE("MockClient connection state is settable and connect() records the endpoint") { + MockClient c; + CHECK(c.connected() == 0); + CHECK(static_cast(c) == false); + + c.setConnected(true); + CHECK(c.connected() == 1); + CHECK(static_cast(c) == true); + + // Default connect result is success (1) and brings the socket up. + MockClient d; + CHECK(d.connectCalled() == false); + CHECK(d.connect("broker.example", 1883) == 1); + CHECK(d.connectCalled() == true); + CHECK(d.lastHost() == "broker.example"); + CHECK(d.lastPort() == 1883); + CHECK(d.connected() == 1); + + // A scripted failure result does not bring the socket up. + MockClient e; + e.setConnectResult(-2); + IPAddress ip(192, 168, 1, 50); + CHECK(e.connect(ip, 8883) == -2); + CHECK(e.lastPort() == 8883); + CHECK(e.lastIp() == ip); + CHECK(e.connected() == 0); + } + + TEST_CASE("MockClient tracks stop() and flush()") { + MockClient c; + c.setConnected(true); + CHECK(c.stopCalled() == false); + CHECK(c.flushCalled() == false); + + c.flush(); + CHECK(c.flushCalled() == true); + CHECK(c.flushCount() == 1); + + c.stop(); + CHECK(c.stopCalled() == true); + CHECK(c.stopCount() == 1); + CHECK(c.connected() == 0); // stop() clears the connection + CHECK(static_cast(c) == false); + + c.stop(); + CHECK(c.stopCount() == 2); + } + + TEST_CASE("MockClient is usable through a Client* base pointer") { + MockClient c; + Client* base = &c; + c.pushInbound({0x42}); + CHECK(base->available() == 1); + CHECK(base->read() == 0x42); + CHECK(base->write(uint8_t(0x99)) == 1); + CHECK(c.outbound().size() == 1); + CHECK(c.outbound()[0] == 0x99); + } + + // --- Fault injection (task 6.2) ---------------------------------------- + + TEST_CASE("MockClient setWriteLimit truncates a single write to the limit") { + MockClient c; + c.setWriteLimit(3); + + const uint8_t payload[] = {'a', 'b', 'c', 'd', 'e', 'f'}; + // A single write is truncated: fewer than size, but >= 1. + size_t n = c.write(payload, sizeof(payload)); + CHECK(n == 3); + CHECK(n < sizeof(payload)); + CHECK(n >= 1); + + // Only the accepted prefix is recorded. + const std::vector expected = {'a', 'b', 'c'}; + CHECK(c.outbound() == expected); + } + + TEST_CASE("MockClient write limit makes progress across repeated writes") { + MockClient c; + c.setWriteLimit(2); + + const uint8_t payload[] = {1, 2, 3, 4, 5}; + // Emulate a caller resending the unaccepted tail until fully written. + size_t sent = 0; + int guard = 0; + while (sent < sizeof(payload) && guard++ < 100) { + size_t n = c.write(payload + sent, sizeof(payload) - sent); + CHECK(n >= 1); // always makes progress + CHECK(n <= 2); // never exceeds the limit + sent += n; + } + CHECK(sent == sizeof(payload)); + + const std::vector expected = {1, 2, 3, 4, 5}; + CHECK(c.outbound() == expected); + } + + TEST_CASE("MockClient a write shorter than the limit is accepted whole") { + MockClient c; + c.setWriteLimit(8); + const uint8_t payload[] = {0xDE, 0xAD}; + CHECK(c.write(payload, sizeof(payload)) == sizeof(payload)); + const std::vector expected = {0xDE, 0xAD}; + CHECK(c.outbound() == expected); + } + + TEST_CASE("MockClient trickle reveals bytes only as the virtual clock advances") { + TestClock::instance().reset(0); + + MockClient c; + c.pushInbound({10, 20, 30, 40}); + // Reveal one byte immediately, then one more per 100 virtual ms. + c.setTrickle(1, 100); + + // Only the first byte is visible at t0. + CHECK(c.available() == 1); + CHECK(c.peek() == 10); + + // Time has not advanced: still just one byte. + CHECK(c.available() == 1); + + // Advancing by less than msPerReveal does not reveal more. + TestClock::instance().advance(50); + CHECK(c.available() == 1); + + // Crossing a full step reveals the next byte. + TestClock::instance().advance(50); // total elapsed 100 + CHECK(c.available() == 2); + + TestClock::instance().advance(100); // total elapsed 200 + CHECK(c.available() == 3); + + // Enough time reveals all scripted bytes and no more. + TestClock::instance().advance(1000); + CHECK(c.available() == 4); + + // Reads consume from the revealed prefix in order. + CHECK(c.read() == 10); + CHECK(c.read() == 20); + CHECK(c.available() == 2); + } + + TEST_CASE("MockClient trickle bytesPerReveal>1 reveals in chunks") { + TestClock::instance().reset(0); + + MockClient c; + c.pushInbound({1, 2, 3, 4, 5}); + c.setTrickle(2, 100); // two bytes now, two more each 100 ms + + CHECK(c.available() == 2); + TestClock::instance().advance(100); + CHECK(c.available() == 4); + TestClock::instance().advance(100); + CHECK(c.available() == 5); // saturates at the scripted total + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/MockStream_test.cpp b/lib/default/TasmotaPubSub/tests/src/MockStream_test.cpp new file mode 100644 index 000000000..8f80effb7 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/MockStream_test.cpp @@ -0,0 +1,83 @@ +/* + MockStream_test.cpp - Unit checks for the MockStream test double. + + Verifies that bytes written to the stream (as the library does in stream-mode + PUBLISH payload routing via `stream->write(digit)`) are captured in order, + that both write overloads record correctly, and that the read side is inert. +*/ + +#include "doctest.h" + +#include "MockStream.h" + +TEST_SUITE("baseline") { + + TEST_CASE("MockStream records single-byte writes in order") { + MockStream stream; + + // Simulate the library routing payload digits one byte at a time. + CHECK(stream.write(static_cast('h')) == 1); + CHECK(stream.write(static_cast('i')) == 1); + CHECK(stream.write(static_cast('!')) == 1); + + const std::vector& out = stream.written(); + REQUIRE(out.size() == 3); + CHECK(out[0] == 'h'); + CHECK(out[1] == 'i'); + CHECK(out[2] == '!'); + } + + TEST_CASE("MockStream records buffer writes in order and appends") { + MockStream stream; + + const uint8_t first[] = {0x01, 0x02, 0x03}; + CHECK(stream.write(first, sizeof(first)) == sizeof(first)); + + // A subsequent single-byte write appends after the buffered bytes. + CHECK(stream.write(static_cast(0x04)) == 1); + + const uint8_t second[] = {0x05, 0x06}; + CHECK(stream.write(second, sizeof(second)) == sizeof(second)); + + const std::vector expected = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; + CHECK(stream.written() == expected); + } + + TEST_CASE("MockStream write(const char*) helper routes through the byte path") { + MockStream stream; + + // Print::write(const char*) is a non-virtual helper; ensure it is not + // hidden and still lands in the recorded buffer in order. + CHECK(stream.write("ab") == 2); + + const std::vector expected = {'a', 'b'}; + CHECK(stream.written() == expected); + } + + TEST_CASE("MockStream clear() discards recorded bytes") { + MockStream stream; + stream.write(static_cast(0xAA)); + REQUIRE(stream.written().size() == 1); + + stream.clear(); + CHECK(stream.written().empty()); + } + + TEST_CASE("MockStream read side is inert") { + MockStream stream; + CHECK(stream.available() == 0); + CHECK(stream.read() == -1); + CHECK(stream.peek() == -1); + } + + TEST_CASE("MockStream is usable as a Stream& (library construction seam)") { + MockStream stream; + Stream& asStream = stream; + + // The library holds a `Stream*` and calls write(digit) through it. + asStream.write(static_cast('X')); + + REQUIRE(stream.written().size() == 1); + CHECK(stream.written()[0] == 'X'); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/MqttPacket_test.cpp b/lib/default/TasmotaPubSub/tests/src/MqttPacket_test.cpp new file mode 100644 index 000000000..24f86c04b --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/MqttPacket_test.cpp @@ -0,0 +1,145 @@ +/* + MqttPacket_test.cpp - Unit checks for the MqttPacket builder and the + Remaining Length codec (task 7.1). + + These verify the fixture builder itself (not the library under test): each + builder must emit the exact wire byte layout of the MQTT 3.1.1 control packet, + and the Remaining Length encoder must match PubSubClient::buildHeader across + the 1..4 byte boundaries. Tagged into the baseline suite so they run as part + of the standard non-regression pass. +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "MockClient.h" +#include "MqttPacket.h" + +TEST_SUITE("baseline") { + + // --- Remaining Length codec (mirrors buildHeader) ---------------------- + + TEST_CASE("MqttPacket encodeRemainingLength matches the MQTT boundaries") { + using V = std::vector; + + // 1-byte range: 0..127. + CHECK(MqttPacket::encodeRemainingLength(0) == V{0x00}); + CHECK(MqttPacket::encodeRemainingLength(1) == V{0x01}); + CHECK(MqttPacket::encodeRemainingLength(127) == V{0x7F}); + + // 2-byte range: 128..16383. + CHECK(MqttPacket::encodeRemainingLength(128) == V{0x80, 0x01}); + CHECK(MqttPacket::encodeRemainingLength(16383) == V{0xFF, 0x7F}); + + // 3-byte range: 16384..2097151. + CHECK(MqttPacket::encodeRemainingLength(16384) == V{0x80, 0x80, 0x01}); + CHECK(MqttPacket::encodeRemainingLength(2097151) == V{0xFF, 0xFF, 0x7F}); + + // 4-byte range: 2097152..268435455. + CHECK(MqttPacket::encodeRemainingLength(2097152) == V{0x80, 0x80, 0x80, 0x01}); + CHECK(MqttPacket::encodeRemainingLength(268435455) == V{0xFF, 0xFF, 0xFF, 0x7F}); + } + + TEST_CASE("MqttPacket encodeRemainingLength is bounded to four bytes") { + // Values above the MQTT maximum are clamped to a 4-byte encoding (never + // a fifth byte), exactly as the library's buildHeader loop is bounded. + CHECK(MqttPacket::encodeRemainingLength(0xFFFFFFFFu).size() == 4); + } + + // --- Builders: exact byte layouts -------------------------------------- + + TEST_CASE("MqttPacket connack layout") { + using V = std::vector; + CHECK(MqttPacket::connack(0).bytes() == V{0x20, 0x02, 0x00, 0x00}); + CHECK(MqttPacket::connack(5).bytes() == V{0x20, 0x02, 0x00, 0x05}); + // Session-present flag sets bit0 of the acknowledge flags byte. + CHECK(MqttPacket::connack(0, true).bytes() == V{0x20, 0x02, 0x01, 0x00}); + } + + TEST_CASE("MqttPacket publish QoS 0 has no packet identifier") { + using V = std::vector; + // topic "a/b" (len 3), payload "hi" (len 2) => remaining length 7. + MqttPacket p = MqttPacket::publish("a/b", std::string("hi")); + const V expected = { + 0x30, // MQTTPUBLISH, qos 0, no retain + 0x07, // remaining length = 2 + 3 + 2 + 0x00, 0x03, // topic length + 'a', '/', 'b', // topic + 'h', 'i' // payload + }; + CHECK(p.bytes() == expected); + } + + TEST_CASE("MqttPacket publish retained flag sets bit0 of the fixed header") { + MqttPacket p = MqttPacket::publish("t", std::string(""), 0, true); + CHECK(p.bytes().at(0) == 0x31); // MQTTPUBLISH | retain + } + + TEST_CASE("MqttPacket publish QoS 1 embeds the packet identifier after the topic") { + using V = std::vector; + MqttPacket p = MqttPacket::publish("a/b", std::string("hi"), 1, false, 0x1234); + const V expected = { + 0x32, // MQTTPUBLISH | qos1 (1<<1) + 0x09, // remaining length = 2 + 3 + 2(msgId) + 2 + 0x00, 0x03, // topic length + 'a', '/', 'b', // topic + 0x12, 0x34, // packet identifier + 'h', 'i' // payload + }; + CHECK(p.bytes() == expected); + } + + TEST_CASE("MqttPacket publish empty payload frames correctly") { + using V = std::vector; + MqttPacket p = MqttPacket::publish("x", std::vector{}); + const V expected = {0x30, 0x03, 0x00, 0x01, 'x'}; + CHECK(p.bytes() == expected); + } + + TEST_CASE("MqttPacket publish frames a 2-byte remaining length past 127") { + // A payload that pushes the total remaining length above 127 must use a + // 2-byte Remaining Length. topic "t" (len 1) => body = 2 + 1 + plen. + const size_t plen = 200; + MqttPacket p = MqttPacket::publish("t", std::vector(plen, 0xAB)); + const uint32_t remaining = 2 + 1 + plen; // 203 + REQUIRE(p.bytes().size() >= 2); + CHECK(p.bytes().at(0) == 0x30); + CHECK(p.bytes().at(1) == 0xCB); // 203 & 127 | 0x80 = 0xCB + CHECK(p.bytes().at(2) == 0x01); // 203 >> 7 = 1 + // Total wire size = 1 (fixed) + 2 (remaining length) + body. + CHECK(p.bytes().size() == 1 + 2 + remaining); + } + + TEST_CASE("MqttPacket ack/ping/disconnect layouts") { + using V = std::vector; + CHECK(MqttPacket::puback(0x0102).bytes() == V{0x40, 0x02, 0x01, 0x02}); + CHECK(MqttPacket::suback(0x0102, 0x01).bytes() == V{0x90, 0x03, 0x01, 0x02, 0x01}); + CHECK(MqttPacket::unsuback(0x0102).bytes() == V{0xB0, 0x02, 0x01, 0x02}); + CHECK(MqttPacket::pingreq().bytes() == V{0xC0, 0x00}); + CHECK(MqttPacket::pingresp().bytes() == V{0xD0, 0x00}); + CHECK(MqttPacket::disconnect().bytes() == V{0xE0, 0x00}); + } + + TEST_CASE("MqttPacket raw passes bytes through verbatim") { + using V = std::vector; + const V adversarial = {0x30, 0xFF, 0xFF, 0xFF, 0x7F}; // huge declared length + CHECK(MqttPacket::raw(adversarial).bytes() == adversarial); + } + + // --- Compatibility with MockClient::pushPacket ------------------------- + + TEST_CASE("MockClient pushPacket enqueues the packet's exact wire bytes") { + MockClient c; + MqttPacket connack = MqttPacket::connack(0); + c.pushPacket(connack); + + CHECK(c.available() == static_cast(connack.size())); + for (uint8_t expected : connack.bytes()) { + CHECK(c.read() == expected); + } + CHECK(c.available() == 0); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/MqttParser_test.cpp b/lib/default/TasmotaPubSub/tests/src/MqttParser_test.cpp new file mode 100644 index 000000000..66b7eb6db --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/MqttParser_test.cpp @@ -0,0 +1,260 @@ +/* + MqttParser_test.cpp - Unit checks for the MqttParser decoder and the + structural PUBLISH validator (task 7.2). + + Two kinds of checks, both tagged into the baseline suite: + 1. Round-trip: build a control packet with the MqttPacket builder, decode it + with MqttParser, and assert the decoded fields equal the original inputs. + A subset also drives the *unmodified* PubSubClient to emit real outbound + bytes and decodes those, proving the parser reads genuine library output. + 2. Validator: isStructurallyValidPublish accepts well-framed PUBLISH packets + and rejects truncated / malformed ones. + + Every assertion goes through decoded wire bytes only (no private members), so + these stay durable across a future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +TEST_SUITE("baseline") { + + // --- Remaining Length codec: decode round-trips encode ----------------- + + TEST_CASE("MqttParser decodeRemainingLength round-trips encodeRemainingLength") { + // Curated boundary values spanning the 1..4 byte encodings. + const uint32_t values[] = { + 0, 1, 127, 128, 16383, 16384, 2097151, 2097152, 268435455 + }; + for (uint32_t n : values) { + const std::vector enc = MqttPacket::encodeRemainingLength(n); + uint32_t decoded = 0; + size_t consumed = 0; + REQUIRE(MqttParser::decodeRemainingLength(enc, 0, decoded, consumed)); + CHECK(decoded == n); + CHECK(consumed == enc.size()); + CHECK(consumed <= 4); + } + } + + TEST_CASE("MqttParser decodeRemainingLength rejects a 5-byte continuation and truncation") { + uint32_t value = 0; + size_t consumed = 0; + // Continuation bit still set after four bytes. + const std::vector tooLong = {0x80, 0x80, 0x80, 0x80, 0x01}; + CHECK_FALSE(MqttParser::decodeRemainingLength(tooLong, 0, value, consumed)); + // Field truncated mid-continuation. + const std::vector truncated = {0x80}; + CHECK_FALSE(MqttParser::decodeRemainingLength(truncated, 0, value, consumed)); + } + + // --- Generic decode framing ------------------------------------------- + + TEST_CASE("MqttParser decode splits fixed header and validates framing") { + MqttPacket p = MqttPacket::publish("a/b", std::string("hi"), 1, true, 0x1234); + DecodedPacket d = MqttParser::decode(p.bytes()); + REQUIRE(d.valid); + CHECK(d.type == static_cast(MQTTPUBLISH)); + CHECK(d.flags == 0x03); // qos1 (0x02) | retain (0x01) + CHECK(d.remainingLength == d.payload.size()); + } + + TEST_CASE("MqttParser decode rejects truncated and over-long frames") { + MqttPacket p = MqttPacket::publish("topic", std::string("payload")); + std::vector bytes = p.bytes(); + + // Well-formed decodes cleanly. + CHECK(MqttParser::decode(bytes).valid); + + // Drop the last byte: declared Remaining Length now exceeds trailing bytes. + std::vector truncated = bytes; + truncated.pop_back(); + CHECK_FALSE(MqttParser::decode(truncated).valid); + + // Append a stray byte: trailing bytes now exceed declared Remaining Length. + std::vector overlong = bytes; + overlong.push_back(0x00); + CHECK_FALSE(MqttParser::decode(overlong).valid); + + // Empty input is invalid. + CHECK_FALSE(MqttParser::decode(std::vector{}).valid); + } + + // --- CONNECT round-trip (builder side + real library output) ----------- + + TEST_CASE("MqttParser decodePublish round-trips PUBLISH fields") { + struct Case { std::string topic; std::string payload; uint8_t qos; bool retain; uint16_t msgId; }; + const Case cases[] = { + {"a/b", "hi", 0, false, 0}, + {"sensor/temp", "23.5", 1, true, 0x0102}, + {"x", "", 0, false, 0}, // empty payload + {"long/topic/name","payload-2", 1, false, 0xFFFF}, + }; + for (const Case& c : cases) { + MqttPacket p = MqttPacket::publish(c.topic, c.payload, c.qos, c.retain, c.msgId); + DecodedPublish d = MqttParser::decodePublish(p.bytes()); + REQUIRE(d.valid); + CHECK(d.topic == c.topic); + CHECK(d.qos == c.qos); + CHECK(d.retain == c.retain); + const std::vector expectedPayload(c.payload.begin(), c.payload.end()); + CHECK(d.payload == expectedPayload); + if (c.qos > 0) { + CHECK(d.msgId == c.msgId); + } + } + } + + TEST_CASE("MqttParser decodeSubscribe / decodeUnsubscribe round-trip filters and msgId") { + MqttPacket sub = MqttPacket::raw({ + 0x82, 0x08, // SUBSCRIBE | qos1, remaining length 8 + 0x00, 0x2A, // msgId 42 + 0x00, 0x03, 'a', '/', 'b', // topic filter "a/b" + 0x01 // requested qos 1 + }); + DecodedSubscribe ds = MqttParser::decodeSubscribe(sub.bytes()); + REQUIRE(ds.valid); + CHECK(ds.msgId == 42); + REQUIRE(ds.filters.size() == 1); + CHECK(ds.filters[0] == "a/b"); + REQUIRE(ds.requestedQos.size() == 1); + CHECK(ds.requestedQos[0] == 1); + + MqttPacket unsub = MqttPacket::raw({ + 0xA2, 0x07, // UNSUBSCRIBE | qos1, remaining length 7 + 0x00, 0x2A, // msgId 42 + 0x00, 0x03, 'a', '/', 'b' // topic filter "a/b" + }); + DecodedUnsubscribe du = MqttParser::decodeUnsubscribe(unsub.bytes()); + REQUIRE(du.valid); + CHECK(du.msgId == 42); + REQUIRE(du.filters.size() == 1); + CHECK(du.filters[0] == "a/b"); + } + + TEST_CASE("MqttParser decodeConnect round-trips a real library CONNECT packet") { + // Drive the unmodified library so the parser reads genuine wire bytes. + TestClock::instance().reset(); + MockClient client; + // Script a valid CONNACK so connect() completes in finite virtual time. + client.pushPacket(MqttPacket::connack(0)); + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + const bool ok = psc.connect("client-42", "alice", "s3cret"); + REQUIRE(ok); + + DecodedConnect c = MqttParser::decodeConnect(client.outbound()); + REQUIRE(c.valid); + CHECK(c.protocolName == "MQTT"); + CHECK(c.protocolLevel == MQTT_VERSION_3_1_1); + CHECK(c.clientId == "client-42"); + CHECK(c.userFlag); + CHECK(c.passwordFlag); + CHECK(c.username == "alice"); + CHECK(c.password == "s3cret"); + CHECK(c.cleanSession); + CHECK_FALSE(c.willFlag); + } + + TEST_CASE("MqttParser decodePublish round-trips a real library PUBLISH packet") { + // A successful connect is required for connected() to report true, which + // publish() guards on. Script a CONNACK, connect, then clear the CONNECT + // bytes so only the PUBLISH remains in the outbound record. + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("dev")); + client.clearOutbound(); + + REQUIRE(psc.publish("stat/dev/RESULT", "{\"POWER\":\"ON\"}", true)); + + DecodedPublish d = MqttParser::decodePublish(client.outbound()); + REQUIRE(d.valid); + CHECK(d.topic == "stat/dev/RESULT"); + CHECK(d.retain); + CHECK(d.qos == 0); + const std::string expected = "{\"POWER\":\"ON\"}"; + CHECK(std::string(d.payload.begin(), d.payload.end()) == expected); + } + + TEST_CASE("MqttParser decodeSubscribe round-trips a real library SUBSCRIBE packet") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("dev")); + client.clearOutbound(); + + REQUIRE(psc.subscribe("cmnd/dev/#", 1)); + + DecodedSubscribe d = MqttParser::decodeSubscribe(client.outbound()); + REQUIRE(d.valid); + CHECK(d.msgId != 0); // library assigns a nonzero id + REQUIRE(d.filters.size() == 1); + CHECK(d.filters[0] == "cmnd/dev/#"); + REQUIRE(d.requestedQos.size() == 1); + CHECK(d.requestedQos[0] == 1); + } + + // --- Structural PUBLISH validator: accept / reject --------------------- + + TEST_CASE("isStructurallyValidPublish accepts well-formed PUBLISH packets") { + CHECK(MqttParser::isStructurallyValidPublish( + MqttPacket::publish("a/b", std::string("hi")).bytes())); + CHECK(MqttParser::isStructurallyValidPublish( + MqttPacket::publish("t", std::string("")).bytes())); // empty payload + CHECK(MqttParser::isStructurallyValidPublish( + MqttPacket::publish("a/b", std::string("hi"), 1, false, 7).bytes())); // qos1 + msgId + // A 2-byte Remaining Length PUBLISH (payload pushes it past 127). + CHECK(MqttParser::isStructurallyValidPublish( + MqttPacket::publish("t", std::vector(200, 0xAB)).bytes())); + } + + TEST_CASE("isStructurallyValidPublish rejects wrong packet type") { + // CONNACK, PINGRESP, SUBACK are not PUBLISH. + CHECK_FALSE(MqttParser::isStructurallyValidPublish(MqttPacket::connack(0).bytes())); + CHECK_FALSE(MqttParser::isStructurallyValidPublish(MqttPacket::pingresp().bytes())); + CHECK_FALSE(MqttParser::isStructurallyValidPublish(MqttPacket::suback(1, 0).bytes())); + } + + TEST_CASE("isStructurallyValidPublish rejects truncated packets") { + std::vector bytes = MqttPacket::publish("a/b", std::string("hi")).bytes(); + + // Remaining Length says more bytes than are present. + std::vector truncated = bytes; + truncated.pop_back(); + CHECK_FALSE(MqttParser::isStructurallyValidPublish(truncated)); + + // Empty input. + CHECK_FALSE(MqttParser::isStructurallyValidPublish(std::vector{})); + + // Fixed header only, no Remaining Length byte. + CHECK_FALSE(MqttParser::isStructurallyValidPublish(std::vector{0x30})); + } + + TEST_CASE("isStructurallyValidPublish rejects a topic length that overruns the body") { + // remaining length = 3, but the declared topic length is 0x00FF which + // cannot fit: 2 (prefix) + 255 (topic) > 3. + const std::vector bad = {0x30, 0x03, 0x00, 0xFF, 0x00}; + CHECK_FALSE(MqttParser::isStructurallyValidPublish(bad)); + } + + TEST_CASE("isStructurallyValidPublish rejects a QoS>0 packet with no room for the packet id") { + // qos1 fixed header (0x32), remaining length 4: topic length prefix (2) + // + a 2-byte topic leaves no room for the 2-byte packet identifier. + const std::vector bad = {0x32, 0x04, 0x00, 0x02, 'a', 'b'}; + CHECK_FALSE(MqttParser::isStructurallyValidPublish(bad)); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/api_surface_test.cpp b/lib/default/TasmotaPubSub/tests/src/api_surface_test.cpp new file mode 100644 index 000000000..dda54028f --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/api_surface_test.cpp @@ -0,0 +1,498 @@ +/* + api_surface_test.cpp - Public-API surface characterization for the + TasmotaPubSub host test system (task 10.8). + + All cases here are Baseline (TEST_SUITE("baseline")) characterization tests: + they lock in the *current* observable behavior of the unmodified library + across the parts of the Public_API that connect_test.cpp does not already + cover in depth, so the two files together characterize the entire surface + without gaps. Specifically: + + - Every constructor form declared in PubSubClient.h (Requirement 18.1). + connect_test.cpp exercises six forms (default, client-only, ip+client, + domain+client, ip+callback+client, domain+callback+client); this file + adds the remaining eight (the uint8_t* byte-array IP forms and all the + Stream& / callback+Stream& variants) so no constructor form is missing. + - setServer / setClient / setCallback / setStream / setKeepAlive / + setSocketTimeout observable behavior (Requirement 18.2). setClient is + shown to actually route bytes through the supplied client; setStream is + shown to route the inbound PUBLISH payload to the supplied MockStream; + setKeepAlive is shown in the CONNECT keepalive field; setSocketTimeout is + shown to bound the connect read-wait in virtual time. + - Each connect() overload incl. user+password, Last Will, and cleanSession + (Requirement 18.3), complementing connect_test.cpp's field round-trip. + - state() across every documented code: MQTT_CONNECTION_TIMEOUT(-4), + MQTT_CONNECTION_LOST(-3), MQTT_CONNECT_FAILED(-2), MQTT_DISCONNECTED(-1), + MQTT_CONNECTED(0), and the CONNACK return codes 1..5 (Requirement 18.8), + each driven through a realistic scenario. + - loop() and connected() observable behavior. + + Every assertion goes through the public API and decoded MockClient.outbound() + wire bytes - never private members - so the baseline stays durable across a + future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "MockClient.h" +#include "MockStream.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// A harmless callback used only so the callback-taking constructor overloads +// can be instantiated; it records nothing. +void noopCallback(char*, uint8_t*, unsigned int) {} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 18.1: constructor forms (complement connect_test.cpp) -------------- + + // connect_test.cpp covers 6 of the 14 constructor forms. This case adds the + // remaining 8 (the uint8_t* byte-array IP forms and every Stream& variant) + // so all 14 declared forms are characterized for a stable initial state. + TEST_CASE("remaining constructor forms initialize to a stable disconnected state") { + TestClock::instance().reset(); + MockClient c1, c2, c3, c4, c5, c6, c7, c8; + MockStream s1, s2, s3, s4; + IPAddress ip(192, 168, 1, 20); + uint8_t ipb[4] = {10, 1, 2, 3}; + + // IPAddress + Stream forms. + PubSubClient ipStream(ip, 1883, c1, s1); + PubSubClient ipCbStream(ip, 1883, noopCallback, c2, s2); + // uint8_t* byte-array IP forms (all four). + PubSubClient ba(ipb, 1883, c3); + PubSubClient baStream(ipb, 1883, c4, s3); + PubSubClient baCb(ipb, 1883, noopCallback, c5); + PubSubClient baCbStream(ipb, 1883, noopCallback, c6, s4); + // domain + Stream forms (the two connect_test.cpp does not build). + PubSubClient domStream("broker.example", 1883, c7); // domain + client + PubSubClient domCbStream("broker.example", 1883, noopCallback, c8); + + PubSubClient* forms[] = { + &ipStream, &ipCbStream, &ba, &baStream, + &baCb, &baCbStream, &domStream, &domCbStream, + }; + for (PubSubClient* p : forms) { + CHECK(p->state() == MQTT_DISCONNECTED); + CHECK_FALSE(p->connected()); + CHECK(p->getBufferSize() == MQTT_MAX_PACKET_SIZE); + } + } + + // The uint8_t* byte-array IP constructor must route connect() to that + // address, proving the endpoint captured by this form is actually used. + TEST_CASE("byte-array IP constructor routes connect to that address") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + uint8_t ipb[4] = {203, 0, 113, 7}; + PubSubClient psc(ipb, 1883, client); + + REQUIRE(psc.connect("dev")); + CHECK(psc.connected()); + CHECK(client.connectCalled()); + CHECK(client.lastIp() == IPAddress(203, 0, 113, 7)); + CHECK(client.lastPort() == 1883); + } + + // A Stream-form constructor attaches the stream; in stream mode the library + // routes each inbound PUBLISH payload byte to that stream during readPacket. + TEST_CASE("stream-form constructor routes inbound PUBLISH payload to the attached stream") { + TestClock::instance().reset(); + MockClient client; + MockStream stream; + IPAddress ip(10, 0, 0, 8); + PubSubClient psc(ip, 1883, client, stream); + + client.pushPacket(MqttPacket::connack(0)); + REQUIRE(psc.connect("stream-dev")); + REQUIRE(psc.connected()); + + // A QoS 0 PUBLISH: topic "t", payload "hi". In stream mode the payload + // bytes (after the topic) are written to the attached stream. + const std::string topic = "t"; + const std::vector payload = {'h', 'i'}; + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/0)); + REQUIRE(psc.loop()); + + CHECK(stream.written() == payload); + } + + // --- 18.2: setClient routes outbound bytes through the supplied client --- + + TEST_CASE("setClient makes connect route bytes through the supplied client") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + + // Default-construct (no client) then attach one via setClient. + PubSubClient psc; + psc.setServer("broker.example", 1883); + psc.setClient(client); + + REQUIRE(psc.connect("dev")); + CHECK(psc.connected()); + + // The CONNECT bytes were recorded by the client supplied via setClient. + CHECK(client.connectCalled()); + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.clientId == "dev"); + } + + // --- 18.2: setCallback registers the dispatch callback; latest wins ------ + + TEST_CASE("setCallback registers the inbound dispatch callback and the latest registration wins") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + + int firstCount = 0; + int secondCount = 0; + std::string seenTopic; + + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + // Register a first callback, then replace it: only the second must fire. + psc.setCallback([&firstCount](char*, uint8_t*, unsigned int) { firstCount++; }); + psc.setCallback([&secondCount, &seenTopic](char* topic, uint8_t*, unsigned int) { + secondCount++; + seenTopic = topic; + }); + REQUIRE(psc.connect("cb-dev")); + client.clearOutbound(); + + const std::string topic = "tele/dev/STATE"; + const std::vector payload = {'O', 'N'}; + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/0)); + REQUIRE(psc.loop()); + + CHECK(firstCount == 0); // the replaced callback never fires + CHECK(secondCount == 1); // the latest callback is used + CHECK(seenTopic == topic); + } + + // --- 18.2: setStream routes inbound PUBLISH payload to the stream -------- + + TEST_CASE("setStream routes the inbound PUBLISH payload to the supplied stream") { + TestClock::instance().reset(); + MockClient client; + MockStream stream; + PubSubClient psc(client); + psc.setStream(stream); + + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("stream-dev")); + REQUIRE(psc.connected()); + + const std::string topic = "sensor"; + const std::vector payload = {'1', '2', '3', '4'}; + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/0)); + REQUIRE(psc.loop()); + + // Every payload byte (after the topic) was routed to the stream in order. + CHECK(stream.written() == payload); + } + + // --- 18.2: setKeepAlive is reflected in the CONNECT keepalive field ------ + + TEST_CASE("setKeepAlive is reflected in the CONNECT keepalive field") { + // Curated intervals, including the default, a short and a long value. + const uint16_t intervals[] = {MQTT_KEEPALIVE, 1, 30, 3600}; + + for (uint16_t ka : intervals) { + CAPTURE(ka); + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + psc.setKeepAlive(ka); // seconds, set BEFORE connect + REQUIRE(psc.connect("ka-dev")); + + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.keepAlive == ka); + } + } + + // --- 18.2: setSocketTimeout bounds the connect read-wait in virtual time - + + TEST_CASE("setSocketTimeout bounds the connect read-wait in virtual time") { + // With no CONNACK scripted, connect() busy-waits for the transport and + // gives up after socketTimeout seconds. Because the shim's delay() + // advances the same virtual clock millis() reads, the elapsed VIRTUAL + // time is bounded by socketTimeout*1000 ms - a larger timeout waits + // proportionally longer - with no real wall-clock time consumed. + auto measureConnectTimeout = [](uint16_t timeoutSecs) -> unsigned long { + TestClock::instance().reset(); + MockClient client; // empty inbound: no CONNACK + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + psc.setSocketTimeout(timeoutSecs); + + const unsigned long start = TestClock::instance().millis(); + const bool ok = psc.connect("to-dev"); // must fail via timeout + const unsigned long elapsed = TestClock::instance().millis() - start; + + CHECK_FALSE(ok); + CHECK(psc.state() == MQTT_CONNECTION_TIMEOUT); + CHECK(client.stopCalled()); + return elapsed; + }; + + const unsigned long elapsed1 = measureConnectTimeout(1); + const unsigned long elapsed3 = measureConnectTimeout(3); + + // Each wait lasts at least its configured timeout in virtual ms, and a + // larger timeout waits strictly longer. + CHECK(elapsed1 >= 1UL * 1000UL); + CHECK(elapsed3 >= 3UL * 1000UL); + CHECK(elapsed3 > elapsed1); + } + + // --- 18.2: setServer selects the most recent endpoint form --------------- + + TEST_CASE("setServer selects the most recently configured endpoint") { + // A later setServer(domain) overrides an earlier setServer(ip): connect + // routes to the domain form. (The library clears the domain when an IP + // form is set and vice-versa, so the last call wins.) + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + + PubSubClient psc(client); + psc.setServer(IPAddress(10, 0, 0, 1), 1883); // first: IP form + psc.setServer("broker.example", 8883); // then: domain form wins + REQUIRE(psc.connect("dev")); + + CHECK(client.lastHost() == "broker.example"); + CHECK(client.lastPort() == 8883); + } + + // --- 18.3: connect() overloads (complement connect_test.cpp) ------------- + + // connect_test.cpp round-trips the CONNECT fields per overload; this case + // consolidates the *delegation* contract: every non-explicit overload forces + // cleanSession=1, and only the fully-explicit overload can clear it. + TEST_CASE("connect overloads force cleanSession except the explicit cleanSession overload") { + SUBCASE("connect(id) => clean session, no user/will") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id")); + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.cleanSession); + CHECK_FALSE(d.userFlag); + CHECK_FALSE(d.willFlag); + } + + SUBCASE("connect(id, user, pass) => clean session + credentials") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id", "alice", "s3cret")); + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.cleanSession); + CHECK(d.userFlag); + CHECK(d.passwordFlag); + CHECK(d.username == "alice"); + CHECK(d.password == "s3cret"); + } + + SUBCASE("connect(id, willTopic, willQos, willRetain, willMessage) => clean session + will") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id", "tele/LWT", 1, true, "Offline")); + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.cleanSession); + CHECK(d.willFlag); + CHECK(d.willQos == 1); + CHECK(d.willRetain); + CHECK(d.willTopic == "tele/LWT"); + CHECK(d.willMessage == "Offline"); + } + + SUBCASE("connect(id, user, pass, will...) => clean session + credentials + will") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id", "u", "p", "tele/LWT", 0, false, "bye")); + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.cleanSession); + CHECK(d.userFlag); + CHECK(d.passwordFlag); + CHECK(d.willFlag); + } + + SUBCASE("connect(id, ..., cleanSession=false) => persistent session") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id", "u", "p", "tele/LWT", 2, true, "bye", false)); + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK_FALSE(d.cleanSession); // only this overload can clear it + CHECK(d.willQos == 2); + } + } + + // --- 18.8: state() across every documented code ------------------------- + + TEST_CASE("state() reports MQTT_DISCONNECTED before any connect attempt") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CHECK(psc.state() == MQTT_DISCONNECTED); + CHECK_FALSE(psc.connected()); + } + + TEST_CASE("state() reports MQTT_CONNECTED after a successful CONNACK") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + REQUIRE(psc.connect("dev")); + CHECK(psc.state() == MQTT_CONNECTED); + CHECK(psc.connected()); + } + + TEST_CASE("state() reports MQTT_CONNECT_FAILED when the transport connect fails") { + // Force the underlying transport connect() to fail; the library never + // sends a CONNECT and reports MQTT_CONNECT_FAILED. + TestClock::instance().reset(); + MockClient client; + client.setConnectResult(0); // transport refuses the connection + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + CHECK_FALSE(psc.connect("dev")); + CHECK(psc.state() == MQTT_CONNECT_FAILED); + CHECK_FALSE(psc.connected()); + // No CONNECT bytes were emitted because the transport never came up. + CHECK(client.outbound().empty()); + } + + TEST_CASE("state() reports MQTT_CONNECTION_TIMEOUT when no CONNACK arrives") { + // Transport connects, but no CONNACK is scripted; the connect read-wait + // times out in virtual time and reports MQTT_CONNECTION_TIMEOUT. + TestClock::instance().reset(); + MockClient client; // empty inbound queue + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + psc.setSocketTimeout(1); // bound the wait in virtual time + + CHECK_FALSE(psc.connect("dev")); + CHECK(psc.state() == MQTT_CONNECTION_TIMEOUT); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + + TEST_CASE("state() reports MQTT_CONNECTION_LOST when the transport drops after connect") { + // Establish a connection, then have the transport report itself down. + // The next connected() poll detects the drop and reports the lost state. + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + REQUIRE(psc.connect("dev")); + REQUIRE(psc.state() == MQTT_CONNECTED); + + client.setConnected(false); // transport silently drops + CHECK_FALSE(psc.connected()); // poll observes the drop + CHECK(psc.state() == MQTT_CONNECTION_LOST); + } + + TEST_CASE("state() reflects each CONNACK return code 1..5") { + struct Case { uint8_t code; int expectedState; }; + const Case cases[] = { + {1, MQTT_CONNECT_BAD_PROTOCOL}, + {2, MQTT_CONNECT_BAD_CLIENT_ID}, + {3, MQTT_CONNECT_UNAVAILABLE}, + {4, MQTT_CONNECT_BAD_CREDENTIALS}, + {5, MQTT_CONNECT_UNAUTHORIZED}, + }; + for (const Case& c : cases) { + CAPTURE(c.code); + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(c.code)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + CHECK_FALSE(psc.connect("dev")); + CHECK(psc.state() == c.expectedState); + CHECK_FALSE(psc.connected()); + } + } + + // --- loop() and connected() observable behavior ------------------------- + + TEST_CASE("loop() returns false before connect and true while idle-connected") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + + // Not connected yet: loop() is a no-op and reports false. + CHECK_FALSE(psc.loop()); + + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("dev")); + client.clearOutbound(); + + // Connected and idle (no inbound, well within keepalive): loop() reports + // true and writes nothing. + CHECK(psc.loop()); + CHECK(client.outbound().empty()); + CHECK(psc.connected()); + } + + TEST_CASE("connected() tracks the transport connection state") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + CHECK_FALSE(psc.connected()); // before connect + REQUIRE(psc.connect("dev")); + CHECK(psc.connected()); // after a successful CONNACK + + psc.disconnect(); // explicit local disconnect + CHECK_FALSE(psc.connected()); + CHECK(psc.state() == MQTT_DISCONNECTED); + CHECK(client.stopCalled()); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/buffer_test.cpp b/lib/default/TasmotaPubSub/tests/src/buffer_test.cpp new file mode 100644 index 000000000..b9b4d94ae --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/buffer_test.cpp @@ -0,0 +1,322 @@ +/* + buffer_test.cpp - buffer sizing and incoming-packet-cap characterization + + hardening for the TasmotaPubSub host test system (task 10.7). + + Baseline (TEST_SUITE("baseline")): + - setBufferSize with a valid size (allocation succeeds) => getBufferSize + returns that size (Requirement 13.1). + - Property 7 (setBufferSize / getBufferSize round-trip) is folded in as a + single deterministic, data-driven case over a curated table of sizes + (no randomized generators). A rejected size (0) leaves the prior size + unchanged. + - setMaxIncomingPacketSize / getMaxIncomingPacketSize round-trip, and the + OBSERVED behavior of the current implementation when an inbound packet + declares a total size larger than the configured cap (Requirement 13.3): + the connection is closed and loop() reports the disconnect. + + Hardening (TEST_SUITE("hardening")): + - F-07 (Requirement 13.2, expected PASS): with the malloc/realloc interposer + armed to fail the next allocation, a failed setBufferSize does NOT leave a + nonzero reported capacity backed by a null/failed buffer. The library + commits the new size only after a successful realloc, so getBufferSize + still reports the previous (valid) size and the buffer stays usable - proven + by a subsequent publish exercised under AddressSanitizer. + Marked FINDING_MARKER(F07). + - Property 12 / F-03 prompt-close (Requirement 13.4, expected PASS): an + inbound packet declaring a Remaining Length larger than the accepted + capacity closes the connection after a bounded prefix (assert stopCalled()) + rather than draining the whole declared body. Marked + FINDING_MARKER(F03_PROMPT_CLOSE). + - F-03 packet-wide deadline (Requirement 13.5, expected FAIL): trickle-fed + inbound bytes should be bounded by a packet-wide deadline, not only by the + per-byte socket timeout. The current fork enforces only a per-byte timeout, + so a byte-per-second trickle is read to completion and dispatched. Marked + FINDING_MARKER(F03_DEADLINE) (should_fail). + + Every assertion goes through the public API and decoded MockClient.outbound() + wire bytes - never private members - so the baseline stays durable across a + future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "AllocInterposer.h" +#include "FindingStatus.h" +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Records inbound PUBLISH dispatches so the deadline test can observe whether a +// trickle-fed packet was delivered (current behavior) or refused (hardened). +struct CallbackCapture { + int count = 0; + std::string topic; + unsigned length = 0; +}; + +// Connect the client/psc pair with a scripted CONNACK and clear the recorded +// CONNECT bytes so only the packet(s) under test remain in outbound(). +void connectAndClear(MockClient& client, PubSubClient& psc) { + client.pushPacket(MqttPacket::connack(0)); // return code 0 = accepted + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("buffer-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 13.1: setBufferSize (allocation succeeds) => getBufferSize --------- + + TEST_CASE("setBufferSize with a valid size makes getBufferSize return that size") { + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + + // The constructor already sized the buffer to MQTT_MAX_PACKET_SIZE. + CHECK(psc.getBufferSize() == MQTT_MAX_PACKET_SIZE); + + REQUIRE(psc.setBufferSize(512)); + CHECK(psc.getBufferSize() == 512); + } + + // --- Property 7: setBufferSize / getBufferSize round-trip --------------- + + // Feature: tasmota-pubsub-tests, Property 7: for all valid buffer sizes, + // calling setBufferSize with that size (when allocation succeeds) makes + // getBufferSize return exactly that size. Validated deterministically over a + // curated table of sizes spanning small, power-of-two, remaining-length + // boundaries, and the uint16_t maximum (no randomized generators). + TEST_CASE("Property 7: setBufferSize / getBufferSize round-trip over curated sizes") { + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + + const uint16_t sizes[] = { + 1, 2, 16, 64, 127, 128, 255, 256, 512, 1024, + MQTT_MAX_PACKET_SIZE, 2048, 8192, 16384, 65535, + }; + + for (uint16_t size : sizes) { + CAPTURE(size); + REQUIRE(psc.setBufferSize(size)); // allocation succeeds + CHECK(psc.getBufferSize() == size); // reports exactly that size + } + + // A size of 0 is rejected and leaves the previously committed size intact + // (the library refuses to shrink the capacity to zero). + const uint16_t last = psc.getBufferSize(); + CHECK_FALSE(psc.setBufferSize(0)); + CHECK(psc.getBufferSize() == last); + } + + // --- 13.3: setMaxIncomingPacketSize characterization -------------------- + + TEST_CASE("setMaxIncomingPacketSize / getMaxIncomingPacketSize round-trip") { + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + + // Default: the cap is disabled (0). + CHECK(psc.getMaxIncomingPacketSize() == 0); + + const uint32_t caps[] = {1, 64, 256, 1200, 100000, 0}; + for (uint32_t cap : caps) { + CAPTURE(cap); + psc.setMaxIncomingPacketSize(cap); + CHECK(psc.getMaxIncomingPacketSize() == cap); + } + } + + TEST_CASE("an inbound packet declaring a total size over the cap closes the connection") { + // Characterize the CURRENT behavior (Requirement 13.3): with a small cap + // configured, a packet whose declared total wire size exceeds the cap is + // refused and the connection is closed - the library does not attempt to + // drain or buffer the oversized body. + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + psc.setMaxIncomingPacketSize(50); + CHECK(psc.getMaxIncomingPacketSize() == 50); + + // A PUBLISH fixed header (0x30) followed by a single-byte Remaining + // Length of 100. total = 1 (fixed) + 1 (length byte) + 100 = 102 > 50, so + // the cap fires right after the length is decoded, before any body read. + // Only the two-byte prefix is supplied; a drain-the-body implementation + // would instead block on the (empty) queue and time out without closing. + std::vector frame = {static_cast(MQTTPUBLISH), 100}; + client.pushInbound(frame); + + psc.setSocketTimeout(1); // bound any unexpected read as a safety net + const bool looped = psc.loop(); + + CHECK_FALSE(looped); + CHECK_FALSE(psc.connected()); + CHECK(psc.state() == MQTT_DISCONNECTED); + CHECK(client.stopCalled()); + } +} + +TEST_SUITE("hardening") { + + // --- F-07 (13.2): failed setBufferSize must not report null-backed size -- + + // With the allocation interposer armed to fail the next allocation, the + // realloc() inside setBufferSize() returns NULL. The library must NOT commit + // the new size: getBufferSize() keeps reporting the previous (valid) size and + // the existing buffer remains intact and usable. If instead it committed the + // size while buffer became null, the subsequent publish would dereference a + // null/invalid buffer and AddressSanitizer would abort. F-07 is hardened + // (expected PASS). + TEST_CASE("F-07 a failed setBufferSize leaves no nonzero capacity backed by a null buffer" + * FINDING_MARKER(F07)) { + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + + // Baseline: the constructor allocated the working buffer successfully. + const uint16_t before = psc.getBufferSize(); + REQUIRE(before == MQTT_MAX_PACKET_SIZE); + + // Arm the next allocation (the realloc in setBufferSize) to fail. + AllocInterposer::failNextAllocation(); + const bool grew = psc.setBufferSize(4096); + AllocInterposer::reset(); + + // The (re)allocation failed, so setBufferSize reports failure and the + // reported size is UNCHANGED - never a nonzero size backed by a failed + // allocation. + CHECK_FALSE(grew); + CHECK(psc.getBufferSize() == before); + + // Prove the buffer behind that reported size is still valid and usable: + // connect and publish, decoding the emitted PUBLISH from the wire. Under + // AddressSanitizer this also proves no null/dangling buffer was left + // behind by the failed realloc. + connectAndClear(client, psc); + REQUIRE(psc.publish("tele/dev/STATE", "online")); + + DecodedPublish pub = MqttParser::decodePublish(client.outbound()); + REQUIRE(pub.valid); + CHECK(pub.topic == "tele/dev/STATE"); + const std::string payload(pub.payload.begin(), pub.payload.end()); + CHECK(payload == "online"); + CHECK(psc.connected()); + } + + // --- Property 12 / F-03 prompt-close (13.4) ----------------------------- + + // Feature: tasmota-pubsub-tests, Property 12: for all inbound packets + // declaring a Remaining Length larger than the accepted capacity, the library + // closes the connection after consuming only a bounded prefix rather than + // draining the entire declared body. + // + // The fixture supplies ONLY the fixed header + Remaining Length bytes and no + // body at all. A prompt-close implementation calls _client->stop() as soon as + // it sees the oversized declared length (so stopCalled() is true almost + // immediately). A drain-the-body implementation would instead loop reading + // the declared body, exhaust the (empty) queue, and time out - a path that + // returns without calling stop(), so stopCalled() would be false. Asserting + // stopCalled() therefore proves the prompt close. F-03 prompt-close is + // hardened (expected PASS). + TEST_CASE("Property 12: an oversized declared inbound packet closes the connection promptly" + * FINDING_MARKER(F03_PROMPT_CLOSE)) { + // Declared Remaining Lengths well beyond the buffer capacity (default + // 1200), each encoded in 1..4 Remaining-Length bytes. + const uint32_t declaredLengths[] = {2000, 300000, 5000000, 200000000}; + + for (uint32_t rl : declaredLengths) { + CAPTURE(rl); + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + // Fixed header (PUBLISH) + Remaining Length only; no body bytes. + std::vector frame; + frame.push_back(static_cast(MQTTPUBLISH)); + const std::vector rlBytes = MqttPacket::encodeRemainingLength(rl); + frame.insert(frame.end(), rlBytes.begin(), rlBytes.end()); + client.pushInbound(frame); + + psc.setSocketTimeout(1); // a drain path would time out fast (no stop) + const bool looped = psc.loop(); + + // Closed promptly after the bounded prefix, not after draining. + CHECK(client.stopCalled()); + CHECK_FALSE(looped); + CHECK_FALSE(psc.connected()); + CHECK(psc.state() == MQTT_DISCONNECTED); + } + } + + // --- F-03 packet-wide deadline (13.5) - expected FAIL -------------------- + + // A packet-wide deadline should bound how long the library spends assembling + // a single inbound packet, independent of how often individual bytes arrive. + // Here the broker trickles one byte per simulated second while the per-byte + // socket timeout is five seconds, so no single byte ever times out. A + // hardened implementation would still abandon and close the connection once + // the packet-wide deadline elapsed; the current fork enforces ONLY the + // per-byte timeout, so it reads the whole packet to completion and dispatches + // the callback. The assertions below encode the hardened expectation, so the + // case reports an EXPECTED FAILURE against the current implementation. + TEST_CASE("F-03 trickle-fed inbound bytes are bounded by a packet-wide deadline" + * FINDING_MARKER(F03_DEADLINE)) { + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + + psc.setServer("broker.example", 1883); + psc.setKeepAlive(120); // keep keepalive well clear of the read + psc.setCallback([&cap](char* topic, uint8_t* /*payload*/, unsigned int len) { + cap.count++; + cap.topic = topic; + cap.length = len; + }); + client.pushPacket(MqttPacket::connack(0)); + REQUIRE(psc.connect("deadline-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); + + // A small, well-formed QoS 0 PUBLISH (topic "t", payload "hi"). + const std::string topic = "t"; + const std::vector payload = {'h', 'i'}; + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/0)); + + // Per-byte timeout 5 s; trickle one byte per simulated second. Each byte + // arrives inside the per-byte window, so only a packet-wide deadline could + // stop the read. Virtual time advances via the shim's delay(), so this + // consumes no real wall-clock time. + psc.setSocketTimeout(5); + client.setTrickle(/*bytesPerReveal=*/1, /*msPerReveal=*/1000); + + psc.loop(); + + // Hardened expectation: the packet-wide deadline elapsed while the bytes + // trickled in, so the library abandoned the packet and closed the + // connection WITHOUT dispatching it. The current fork instead delivers the + // packet, so both checks fail here => reported as an expected failure. + CHECK(client.stopCalled()); + CHECK(cap.count == 0); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/connect_test.cpp b/lib/default/TasmotaPubSub/tests/src/connect_test.cpp new file mode 100644 index 000000000..826da6a8b --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/connect_test.cpp @@ -0,0 +1,453 @@ +/* + connect_test.cpp - Connect / CONNACK, constructor forms, and setServer + characterization + hardening for the TasmotaPubSub host test system (task 10.1). + + Baseline (TEST_SUITE("baseline")): + - A valid scripted CONNACK drives the *unmodified* library into the connected + state (Requirement 8.1). + - The recorded outbound bytes decode into a valid CONNECT packet (Requirement 8.2). + - Property 2 (outbound packets are structurally well-framed) and Property 3 + (CONNECT field round-trip) are exercised as single, deterministic, + data-driven cases over curated combinations of client id / user / password / + Last Will / cleanSession (no randomized generators). + - Constructor forms and setServer overloads are characterized (Requirements + 18.1, 18.3) and each connect() overload is decoded from the wire. + + Hardening (TEST_SUITE("hardening")): + - Property 10 / F-06: a four-byte inbound frame that is not a well-formed + CONNACK (wrong packet type, nonzero fixed flags, or Remaining Length != 2) + is rejected as a failed connection, independent of the fourth byte value + (expected PASS per the finding table). + - F-11 (expected FAIL): disconnect() with no argument should transmit an MQTT + DISCONNECT packet; the current fork sends nothing, so the case is marked + FINDING_MARKER(F11) (should_fail). + + Every assertion goes through the public API and decoded MockClient.outbound() + wire bytes - never private members - so the baseline stays durable across a + future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "FindingStatus.h" +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +TEST_SUITE("baseline") { + + // --- 8.1: valid CONNACK => connected state ------------------------------ + + TEST_CASE("connect with a valid scripted CONNACK enters the connected state") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); // return code 0 = accepted + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + const bool ok = psc.connect("client-1"); + REQUIRE(ok); + CHECK(psc.connected()); + CHECK(psc.state() == MQTT_CONNECTED); + } + + TEST_CASE("connect surfaces a CONNACK non-zero return code as the state and stays disconnected") { + // Characterize the documented CONNACK return codes 1..5: a nonzero code + // is reflected in state() and the client is not connected (Requirement 18.8). + struct Case { uint8_t code; int expectedState; }; + const Case cases[] = { + {1, MQTT_CONNECT_BAD_PROTOCOL}, + {2, MQTT_CONNECT_BAD_CLIENT_ID}, + {3, MQTT_CONNECT_UNAVAILABLE}, + {4, MQTT_CONNECT_BAD_CREDENTIALS}, + {5, MQTT_CONNECT_UNAUTHORIZED}, + }; + for (const Case& c : cases) { + CAPTURE(c.code); + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(c.code)); + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + CHECK_FALSE(psc.connect("client-1")); + CHECK(psc.state() == c.expectedState); + CHECK_FALSE(psc.connected()); + } + } + + // --- 8.2 + Property 2: recorded outbound is a valid CONNECT packet ------ + + // Feature: tasmota-pubsub-tests, Property 2: outbound packets are + // structurally well-framed - the recorded CONNECT decodes into a complete + // control packet whose declared Remaining Length equals the trailing bytes. + TEST_CASE("connect records a structurally well-framed CONNECT packet") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("dev-42")); + + const std::vector& out = client.outbound(); + + // Generic framing: the outbound bytes are one complete, self-consistent + // control packet whose Remaining Length matches the trailing byte count. + DecodedPacket generic = MqttParser::decode(out); + REQUIRE(generic.valid); + CHECK(generic.type == static_cast(MQTTCONNECT)); + CHECK(generic.flags == 0x00); + CHECK(generic.remainingLength == generic.payload.size()); + + // CONNECT-specific: protocol name / level and client id round-trip. + DecodedConnect c = MqttParser::decodeConnect(out); + REQUIRE(c.valid); + CHECK(c.protocolName == "MQTT"); + CHECK(c.protocolLevel == MQTT_VERSION_3_1_1); + CHECK(c.keepAlive == MQTT_KEEPALIVE); + CHECK(c.clientId == "dev-42"); + CHECK(c.cleanSession); + } + + // --- Property 3: CONNECT field round-trip ------------------------------- + + // Feature: tasmota-pubsub-tests, Property 3: for all valid combinations of + // client id, optional user, optional password, and optional Last Will with a + // cleanSession flag, the outbound CONNECT decodes to a packet whose protocol + // name/level, flags, keepalive, and embedded strings match the arguments. + // Validated deterministically over a curated table (no randomized generators). + TEST_CASE("CONNECT field round-trip over curated argument combinations") { + struct Case { + const char* clientId; + const char* user; // nullptr => no user + const char* pass; // nullptr => no password (only set when user set) + const char* willTopic; // nullptr => no will + uint8_t willQos; + bool willRetain; + const char* willMessage; + bool cleanSession; + }; + const Case cases[] = { + // clientId only, clean session. + {"c-min", nullptr, nullptr, nullptr, 0, false, nullptr, true}, + // clientId, clean session false (persistent session). + {"c-persist", nullptr, nullptr, nullptr, 0, false, nullptr, false}, + // user + password. + {"c-auth", "alice", "s3cret", nullptr, 0, false, nullptr, true}, + // user only (no password). + {"c-user", "bob", nullptr, nullptr, 0, false, nullptr, true}, + // Last Will, qos 0, not retained. + {"c-will0", nullptr, nullptr, "tele/dev/LWT", 0, false, "Offline", true}, + // Last Will, qos 1, retained. + {"c-will1", nullptr, nullptr, "tele/dev/LWT", 1, true, "Offline", true}, + // Last Will, qos 2, retained (qos accepted by connect()). + {"c-will2", nullptr, nullptr, "tele/dev/LWT", 2, true, "bye", false}, + // Everything at once: user + password + retained will + clean session. + {"c-full", "alice", "s3cret", "tele/dev/LWT", 1, true, "Offline", true}, + // Empty client id (allowed with clean session) + empty will message. + {"", nullptr, nullptr, "tele/x/LWT", 0, false, "", true}, + }; + + for (const Case& c : cases) { + CAPTURE(c.clientId); + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + + // Drive every combination through the fully-explicit overload so the + // cleanSession flag can be varied directly. + const bool ok = psc.connect(c.clientId, c.user, c.pass, + c.willTopic, c.willQos, c.willRetain, + c.willMessage, c.cleanSession); + REQUIRE(ok); + + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + + // Fixed protocol identity. + CHECK(d.protocolName == "MQTT"); + CHECK(d.protocolLevel == MQTT_VERSION_3_1_1); + CHECK(d.keepAlive == MQTT_KEEPALIVE); + + // Client id round-trip. + CHECK(d.clientId == std::string(c.clientId)); + + // cleanSession flag round-trip. + CHECK(d.cleanSession == c.cleanSession); + + // User / password flags and embedded strings. + const bool wantUser = (c.user != nullptr); + const bool wantPass = (c.user != nullptr && c.pass != nullptr); + CHECK(d.userFlag == wantUser); + CHECK(d.passwordFlag == wantPass); + if (wantUser) { CHECK(d.username == std::string(c.user)); } + if (wantPass) { CHECK(d.password == std::string(c.pass)); } + + // Last Will flags and embedded strings. + const bool wantWill = (c.willTopic != nullptr); + CHECK(d.willFlag == wantWill); + if (wantWill) { + CHECK(d.willQos == c.willQos); + CHECK(d.willRetain == c.willRetain); + CHECK(d.willTopic == std::string(c.willTopic)); + CHECK(d.willMessage == std::string(c.willMessage)); + } + } + } + + // --- 18.3: each connect() overload ------------------------------------ + + TEST_CASE("connect overloads set the expected CONNECT flags") { + // Each overload delegates to the 8-arg form with a fixed cleanSession=1 + // (except the explicit form). Decode the wire to confirm the flag layout. + + SUBCASE("connect(id) - clean session, no user/will") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id-a")); + + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.clientId == "id-a"); + CHECK(d.cleanSession); + CHECK_FALSE(d.userFlag); + CHECK_FALSE(d.passwordFlag); + CHECK_FALSE(d.willFlag); + } + + SUBCASE("connect(id, user, pass)") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id-b", "u", "p")); + + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.userFlag); + CHECK(d.passwordFlag); + CHECK(d.username == "u"); + CHECK(d.password == "p"); + CHECK_FALSE(d.willFlag); + } + + SUBCASE("connect(id, willTopic, willQos, willRetain, willMessage)") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id-c", "tele/c/LWT", 1, true, "Offline")); + + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.willFlag); + CHECK(d.willQos == 1); + CHECK(d.willRetain); + CHECK(d.willTopic == "tele/c/LWT"); + CHECK(d.willMessage == "Offline"); + CHECK_FALSE(d.userFlag); + } + + SUBCASE("connect(id, user, pass, willTopic, willQos, willRetain, willMessage)") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id-d", "u", "p", "tele/d/LWT", 0, false, "bye")); + + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK(d.userFlag); + CHECK(d.passwordFlag); + CHECK(d.willFlag); + CHECK(d.cleanSession); // this overload forces cleanSession=1 + } + + SUBCASE("connect(id, ..., cleanSession=false)") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("id-e", "u", "p", "tele/e/LWT", 2, false, "bye", false)); + + DecodedConnect d = MqttParser::decodeConnect(client.outbound()); + REQUIRE(d.valid); + CHECK_FALSE(d.cleanSession); + CHECK(d.willQos == 2); + } + } + + // --- 18.1: constructor forms ------------------------------------------- + + TEST_CASE("constructor forms have a stable initial disconnected state") { + TestClock::instance().reset(); + MockClient c1, c2, c3, c4, c5; + IPAddress ip(192, 168, 1, 10); + + PubSubClient def; // default + PubSubClient fromClient(c1); // client only + PubSubClient fromIp(ip, 1883, c2); // ip + port + client + PubSubClient fromDomain("broker.example", 1883, c3); // domain + port + client + auto cb = [](char*, uint8_t*, unsigned int) {}; + PubSubClient fromIpCb(ip, 1883, cb, c4); // ip + port + callback + client + PubSubClient fromDomainCb("broker.example", 1883, cb, c5); // domain + callback + + for (PubSubClient* p : {&def, &fromClient, &fromIp, &fromDomain, &fromIpCb, &fromDomainCb}) { + CHECK(p->state() == MQTT_DISCONNECTED); + CHECK_FALSE(p->connected()); + CHECK(p->getBufferSize() == MQTT_MAX_PACKET_SIZE); + } + } + + TEST_CASE("constructor-supplied endpoint is used by connect (ip and domain forms)") { + // IP-form constructor: connect routes to the constructor IP/port. + { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + IPAddress ip(10, 0, 0, 5); + PubSubClient psc(ip, 1883, client); + REQUIRE(psc.connect("dev")); + CHECK(psc.connected()); + CHECK(client.connectCalled()); + CHECK(client.lastIp() == ip); + CHECK(client.lastPort() == 1883); + } + // Domain-form constructor: connect routes to the constructor host/port. + { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc("mqtt.local", 8883, client); + REQUIRE(psc.connect("dev")); + CHECK(client.connectCalled()); + CHECK(client.lastHost() == "mqtt.local"); + CHECK(client.lastPort() == 8883); + } + } + + // --- setServer overloads ----------------------------------------------- + + TEST_CASE("setServer overloads route connect to the configured endpoint") { + SUBCASE("setServer(IPAddress, port)") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + IPAddress ip(172, 16, 0, 9); + psc.setServer(ip, 1883); + REQUIRE(psc.connect("dev")); + CHECK(client.lastIp() == ip); + CHECK(client.lastPort() == 1883); + } + + SUBCASE("setServer(uint8_t*, port)") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + uint8_t ipbytes[4] = {192, 0, 2, 44}; + psc.setServer(ipbytes, 1883); + REQUIRE(psc.connect("dev")); + CHECK(client.lastIp() == IPAddress(192, 0, 2, 44)); + CHECK(client.lastPort() == 1883); + } + + SUBCASE("setServer(const char* domain, port)") { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1884); + REQUIRE(psc.connect("dev")); + CHECK(client.lastHost() == "broker.example"); + CHECK(client.lastPort() == 1884); + } + } +} + +TEST_SUITE("hardening") { + + // Feature: tasmota-pubsub-tests, Property 10: for all four-byte inbound + // frames that are not a well-formed CONNACK (packet type other than 0x20, + // nonzero fixed flags, or Remaining Length other than 2), connect() reports + // failure and does not enter the connected state, regardless of the fourth + // byte value. F-06 is hardened in the current fork (expected PASS). + TEST_CASE("F-06 malformed four-byte CONNACK frames are rejected" * FINDING_MARKER(F06)) { + struct Case { const char* label; uint8_t b0; uint8_t b1; }; + const Case malformations[] = { + // Wrong packet type (Remaining Length 2, but not CONNACK 0x20). + {"type CONNECT (0x10)", 0x10, 0x02}, + {"type PUBLISH (0x30)", 0x30, 0x02}, + {"type PINGRESP (0xD0)", 0xD0, 0x02}, + // Correct type nibble (0x2) but nonzero fixed flags in the low nibble. + {"nonzero flags (0x21)", 0x21, 0x02}, + {"nonzero flags (0x2F)", 0x2F, 0x02}, + // Correct type + zero flags, but Remaining Length != 2. + {"remaining length 1", 0x20, 0x01}, + {"remaining length 3", 0x20, 0x03}, + {"remaining length 0", 0x20, 0x00}, + }; + // The fourth byte (CONNACK return-code position) must not influence the + // decision for a malformed frame - exercise both 0x00 and 0xFF. + const uint8_t fourthBytes[] = {0x00, 0xFF}; + + for (const Case& m : malformations) { + for (uint8_t fourth : fourthBytes) { + CAPTURE(m.label); + CAPTURE(fourth); + TestClock::instance().reset(); + MockClient client; + // Bound any over-declared-length read to a short virtual timeout. + client.pushInbound({m.b0, m.b1, 0x00, fourth}); + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + psc.setSocketTimeout(1); + + CHECK_FALSE(psc.connect("client-x")); + CHECK_FALSE(psc.connected()); + CHECK(psc.state() != MQTT_CONNECTED); + } + } + } + + // F-11 (Requirement 8.4): disconnect() with no argument SHOULD transmit an + // MQTT DISCONNECT packet. The current fork only flushes/stops the transport + // and writes nothing, so this is expected to fail until F-11 is hardened. + TEST_CASE("F-11 disconnect() with no argument transmits a DISCONNECT packet" * FINDING_MARKER(F11)) { + TestClock::instance().reset(); + MockClient client; + client.pushPacket(MqttPacket::connack(0)); + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("dev")); + + client.clearOutbound(); + psc.disconnect(); // default argument: no DISCONNECT packet in current fork + + // A hardened implementation writes the 2-byte DISCONNECT frame (0xE0 0x00). + DecodedPacket d = MqttParser::decode(client.outbound()); + REQUIRE(d.valid); + CHECK(d.type == static_cast(MQTTDISCONNECT)); + CHECK(d.remainingLength == 0); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/findings_test.cpp b/lib/default/TasmotaPubSub/tests/src/findings_test.cpp new file mode 100644 index 000000000..79a006719 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/findings_test.cpp @@ -0,0 +1,706 @@ +/* + findings_test.cpp - one consolidated adversarial/boundary fixture per + static-analysis finding F-01..F-12 for the TasmotaPubSub host test system + (task 10.9). + + Feature: tasmota-pubsub-tests + + Requirement 14.1 asks that EVERY documented finding (F-01..F-12) have at least + one corresponding executable check. Several findings are already exercised in + the behavior-specific suites (connect/publish/streaming/subscribe/receive/ + keepalive/buffer/api-surface); this file provides one clear, self-contained + fixture per finding in a single place so the finding->test mapping is obvious + and auditable. + + Rules honored throughout: + - Adversarial fixtures are built ONLY via MqttPacket / sized + std::vector, so the test code itself performs no out-of-bounds + writes (Requirement 14.3). Any over-read/over-write is therefore the + library's, and AddressSanitizer (on by default) is the detector + (Requirement 14.2). + - F-01 uses inbound PUBLISH fixtures at total packet sizes bufferSize-1, + bufferSize and bufferSize+1 (Requirement 14.4); setBufferSize shrinks the + working buffer AFTER connecting so these boundaries are small and precise. + - Every assertion goes through the public API, the registered callback, and + decoded MockClient.outbound() wire bytes - never private members + (Requirement 19.1). + - Expected-FAIL findings (F-05, F-10, F-11 and the F-03 packet-wide deadline) + carry FINDING_MARKER(...) expanding to should_fail(); the rest are expected + to pass against the already-partially-hardened fork. + + Observed status is verified empirically by `make hardening` / `make test`; the + FINDING_MARKER for each case matches the FindingStatus.h seeding. +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "AllocInterposer.h" +#include "CallbackContractAdapter.h" +#include "FindingStatus.h" +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Records every inbound PUBLISH the library dispatches to the callback so a +// rejection can be proven by the callback never firing. +struct CallbackCapture { + int count = 0; + std::string topic; + unsigned int length = 0; + std::vector payload; +}; + +// Connect the client/psc pair with a scripted CONNACK, register a recording +// callback, and clear the recorded CONNECT bytes so only the packet-under-test +// remains in outbound(). +void connectWithCallback(MockClient& client, PubSubClient& psc, CallbackCapture& cap) { + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + psc.setCallback([&cap](char* topic, uint8_t* payload, unsigned int len) { + cap.count++; + cap.topic = topic; + cap.length = len; + cap.payload.assign(payload, payload + len); + }); + REQUIRE(psc.connect("findings-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +// Connect the client/psc pair with a scripted CONNACK and clear the recorded +// CONNECT bytes; no callback registered. +void connectAndClear(MockClient& client, PubSubClient& psc) { + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("findings-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +// Deterministic payload of length n (includes 0x00 bytes so round-trips prove +// binary-safety, not C-string termination). +std::vector makePayload(size_t n) { + std::vector v(n); + for (size_t i = 0; i < n; ++i) { + v[i] = static_cast((i * 31u + 7u) & 0xFFu); + } + return v; +} + +} // namespace + +TEST_SUITE("hardening") { + + // ======================================================================= + // F-01 - Exact-buffer inbound one-byte heap overflow (Requirement 14.4) + // ======================================================================= + // + // A PUBLISH whose complete encoded packet is exactly bufferSize bytes would, + // in the unpatched code, let the consumer write a NUL at buffer[bufferSize] - + // one byte past the allocation. The fork reserves a sentinel byte: readPacket + // rejects total_packet >= bufferSize (and idx >= bufferSize). So: + // total == bufferSize-1 : ACCEPTED -> callback fires, NUL lands at the + // reserved buffer[bufferSize-1] (in-bounds) + // total == bufferSize : REJECTED -> connection closed, no callback + // total == bufferSize+1 : REJECTED -> connection closed, no callback + // + // The CallbackContractAdapter reproduces the real Tasmota `mqtt_data[data_len] + // = 0` write into the delivered (in-library) buffer, so the accepted case + // exercises the exact consumer-side boundary write under AddressSanitizer. If + // the guard were off-by-one, the accepted case's NUL write would hit + // buffer[bufferSize] and ASan would abort. F-01 is hardened (expected PASS). + TEST_CASE("F-01 exact-buffer inbound PUBLISH boundaries stay in-bounds under ASan" + * FINDING_MARKER(F01)) { + // A small, precise working buffer so the three boundary totals are exact. + // The buffer is shrunk AFTER connect (CONNECT needs the default buffer). + const uint16_t kBufferSize = 64; + const std::string topic = "t"; // topicLen == 1 + // total wire size of a QoS 0 PUBLISH = 1 (fixed) + 1 (RL byte, RL<128) + // + 2 (topic-length field) + topicLen + payloadLen = 4 + topicLen + payloadLen. + // With topicLen == 1 => total = 5 + payloadLen, so payloadLen = total - 5. + struct Case { const char* label; int totalDelta; bool accepted; }; + const Case cases[] = { + {"bufferSize-1 (accepted)", -1, true}, + {"bufferSize (rejected)", 0, false}, + {"bufferSize+1 (rejected)", +1, false}, + }; + + for (const Case& c : cases) { + CAPTURE(c.label); + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + + // Register the faithful MqttDataHandler-contract adapter as the + // callback so the accepted case performs the real NUL-termination + // write into the library buffer under ASan. + CallbackContractAdapter adapter; + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + psc.setCallback(adapter.callback()); + REQUIRE(psc.connect("f01-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); + + // Precisely size the buffer for the boundary math. + REQUIRE(psc.setBufferSize(kBufferSize)); + REQUIRE(psc.getBufferSize() == kBufferSize); + + const int total = static_cast(kBufferSize) + c.totalDelta; + const size_t payloadLen = static_cast(total) - 5; + const std::vector payload = makePayload(payloadLen); + + // Built structurally via the packet builder (sized vectors only). + const MqttPacket pkt = MqttPacket::publish(topic, payload, /*qos=*/0); + REQUIRE(pkt.size() == static_cast(total)); // fixture is exact + client.pushPacket(pkt); + + psc.setSocketTimeout(1); // bound any read as a safety net + psc.loop(); + + if (c.accepted) { + // Delivered exactly once with the fixture payload; the NUL write + // (adapter, enabled by default) landed on the reserved sentinel + // byte buffer[bufferSize-1] - clean under ASan. + REQUIRE(adapter.count() == 1); + CHECK(adapter.last().length == payloadLen); + CHECK(adapter.last().payload == payload); + CHECK(psc.connected()); + CHECK_FALSE(client.stopCalled()); + } else { + // Rejected before dispatch: no callback, connection closed. + CHECK(adapter.count() == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + } + } + + // ======================================================================= + // F-02 - PUBLISH parsing trusts topic length instead of packet length + // ======================================================================= + // + // A QoS 1 PUBLISH whose 2-byte topic-length field claims far more bytes than + // the packet actually contains must be rejected without reading past the + // received bytes, and a short PUBLISH whose length arithmetic would underflow + // must not reach the callback with a huge length. The fork's guard + // (header_len = llen + 3 + tl + (qos?2:0) > len) fires first. All reads stay + // within the scripted bytes, so ASan proves no over-read. Expected PASS. + TEST_CASE("F-02 PUBLISH with claimed topic length exceeding the packet is rejected (no over-read)" + * FINDING_MARKER(F02)) { + // {fixed header, Remaining Length byte, claimed topic length, body bytes}. + // The claimed topic length far exceeds the body, and for the underflow + // cases the QoS 0 payload length (len - llen - 3 - tl) would wrap. + struct Case { const char* label; uint8_t fixedHeader; uint8_t bodyLen; uint16_t claimedTopicLen; }; + const Case cases[] = { + {"QoS1 tl=500 body=4", static_cast(MQTTPUBLISH | MQTTQOS1), 4, 500}, + {"QoS1 tl=0x7FFF body=10",static_cast(MQTTPUBLISH | MQTTQOS1),10, 0x7FFF}, + {"QoS0 underflow tl=10 body=5", static_cast(MQTTPUBLISH), 5, 10}, + {"QoS0 underflow tl=5 body=2", static_cast(MQTTPUBLISH), 2, 5}, + }; + + for (const Case& c : cases) { + CAPTURE(c.label); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + // Sized vector => no OOB in the test code itself. + std::vector frame; + frame.push_back(c.fixedHeader); + frame.push_back(c.bodyLen); // RL < 128 + frame.push_back(static_cast(c.claimedTopicLen >> 8)); // topic len hi + frame.push_back(static_cast(c.claimedTopicLen & 0xFF)); // topic len lo + for (uint8_t i = 2; i < c.bodyLen; ++i) { + frame.push_back(static_cast('x')); + } + client.pushPacket(MqttPacket::raw(frame)); + + psc.setSocketTimeout(1); + psc.loop(); + + // No callback (so no over-read and no underflowed length), closed. + CHECK(cap.count == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + } + + // ======================================================================= + // F-03 (a) - Oversized inbound packet closes promptly (Requirement 13.4) + // ======================================================================= + // + // An inbound packet declaring a Remaining Length larger than the accepted + // capacity must close the connection after a bounded prefix rather than + // draining the whole declared body. The fixture supplies ONLY the fixed + // header + Remaining Length bytes (no body): a prompt-close implementation + // calls stop() as soon as it sees the oversized length; a drain-the-body + // implementation would block on the empty queue and time out WITHOUT calling + // stop(). Asserting stopCalled() proves the prompt close. Expected PASS. + TEST_CASE("F-03 oversized declared inbound packet closes the connection promptly" + * FINDING_MARKER(F03_PROMPT_CLOSE)) { + const uint32_t declaredLengths[] = {2000, 300000, 5000000, 200000000}; + + for (uint32_t rl : declaredLengths) { + CAPTURE(rl); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + std::vector frame; + frame.push_back(static_cast(MQTTPUBLISH)); + const std::vector rlBytes = MqttPacket::encodeRemainingLength(rl); + frame.insert(frame.end(), rlBytes.begin(), rlBytes.end()); + client.pushInbound(frame); + + psc.setSocketTimeout(1); + const bool looped = psc.loop(); + + CHECK(client.stopCalled()); + CHECK_FALSE(looped); + CHECK_FALSE(psc.connected()); + CHECK(psc.state() == MQTT_DISCONNECTED); + } + } + + // ======================================================================= + // F-03 (b) - Packet-wide read deadline (Requirement 13.5) - EXPECTED FAIL + // ======================================================================= + // + // Trickle-fed inbound bytes should be bounded by a packet-wide deadline, not + // only by a per-byte socket timeout. Here the broker trickles one byte per + // simulated second while the per-byte timeout is five seconds, so no single + // byte ever times out. A hardened implementation would still abandon and + // close the packet once a packet-wide deadline elapsed; the current fork + // enforces ONLY the per-byte timeout and reads the whole packet to + // completion, dispatching the callback. The assertions encode the hardened + // expectation, so this reports an EXPECTED FAILURE. + TEST_CASE("F-03 trickle-fed inbound bytes are bounded by a packet-wide deadline" + * FINDING_MARKER(F03_DEADLINE)) { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + + psc.setServer("broker.example", 1883); + psc.setKeepAlive(120); // keep keepalive well clear of the read + psc.setCallback([&cap](char* topic, uint8_t* payload, unsigned int len) { + cap.count++; + cap.topic = topic; + cap.length = len; + cap.payload.assign(payload, payload + len); + }); + client.pushPacket(MqttPacket::connack(0)); + REQUIRE(psc.connect("f03-deadline-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); + + // A small well-formed QoS 0 PUBLISH trickled one byte per simulated + // second, inside a 5 s per-byte window. Virtual time advances via the + // shim's delay(), so this consumes no real wall-clock time. + const std::string topic = "t"; + const std::vector payload = {'h', 'i'}; + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/0)); + psc.setSocketTimeout(5); + client.setTrickle(/*bytesPerReveal=*/1, /*msPerReveal=*/1000); + + psc.loop(); + + // Hardened expectation (fails against the current fork => expected fail). + CHECK(client.stopCalled()); + CHECK(cap.count == 0); + } + + // ======================================================================= + // F-04 - Streaming publish length 16-bit truncation (Requirement 9.4) + // ======================================================================= + // + // beginPublish() buffers only the header (fixed header + Remaining Length + + // topic) and streams the payload separately, so a very large *declared* + // length can be framed without allocating the payload. The emitted Remaining + // Length must be the full 32-bit value (plength + 2 + topicLen), not a 16-bit + // truncation that would desynchronize framing. The fork uses a uint32_t + // buildHeader bounded to four length bytes. Expected PASS. + TEST_CASE("F-04 large declared streaming length frames Remaining Length without truncation" + * FINDING_MARKER(F04)) { + const std::string topic = "t"; // topicLen == 1 + const size_t topicLen = topic.size(); + // Declared payload lengths above the 16-bit range, spanning the 2->3 and + // 3->4 byte Remaining Length transitions and the MQTT maximum. + const unsigned int plengths[] = { + 65533u, // RL = 65536 (first value needing 3 bytes) + 100000u, // RL = 100003 (3 bytes) + 2097149u, // RL = 2097152 (first value needing 4 bytes) + 268435455u - 2u - 1u, // RL = 268435455 (MQTT maximum, 4 bytes) + }; + + for (unsigned int plen : plengths) { + CAPTURE(plen); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.beginPublish(topic.c_str(), plen, false)); + + const std::vector& out = client.outbound(); + REQUIRE(out.size() >= 2); + CHECK(static_cast(out[0] & 0xF0) == static_cast(MQTTPUBLISH)); + + uint32_t rl = 0; + size_t rlBytes = 0; + REQUIRE(MqttParser::decodeRemainingLength(out, 1, rl, rlBytes)); + + const uint32_t expected = + static_cast(plen) + 2u + static_cast(topicLen); + CHECK(rl == expected); // full 32-bit value + CHECK(rl != (expected & 0xFFFFu)); // NOT a 16-bit truncation + + // Header framing is self-consistent: fixed(1) + RL bytes + topic + // length prefix(2) + topic. No payload streamed yet. + CHECK(out.size() == 1u + rlBytes + 2u + topicLen); + } + } + + // ======================================================================= + // F-05 - Partial write corrupts the stream, connection reused - EXPECTED FAIL + // ======================================================================= + // + // With a partial transport write injected (setWriteLimit), the publish path + // should report failure and leave the connection unusable so no later packet + // is written onto a desynchronized stream. The current fork returns success + // from endPublish() and keeps the connection, so the hardened assertions + // below report an EXPECTED FAILURE. Cut points are strictly less than the + // full packet length. + TEST_CASE("F-05 partial transport write fails the publish and disables reuse" + * FINDING_MARKER(F05)) { + const std::string topic = "s"; // topicLen == 1 + const std::vector payload = makePayload(300); + // Header = fixed(1) + RL(2, RL=303) + topic-len(2) + topic(1) = 6; full + // packet = 306. Cut points >= 6 (header write completes) and < 300 (the + // payload write is truncated) - all strictly less than 306. + const size_t cutPoints[] = {10, 50, 150, 299}; + + for (size_t cut : cutPoints) { + CAPTURE(cut); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + client.setWriteLimit(cut); + REQUIRE(psc.beginPublish(topic.c_str(), + static_cast(payload.size()), false)); + + const size_t written = psc.write(payload.data(), payload.size()); + REQUIRE(written < payload.size()); // a genuine partial write + + // Hardened contract: endPublish reports failure and the connection is + // left unusable. The current fork does neither => expected failure. + CHECK(psc.endPublish() == 0); + CHECK_FALSE(psc.connected()); + } + } + + // ======================================================================= + // F-06 - CONNACK response validation (Requirement 8.3) + // ======================================================================= + // + // A four-byte inbound frame that is not a well-formed CONNACK (packet type + // other than 0x20, nonzero fixed flags, or Remaining Length other than 2) + // must be rejected as a failed connection, regardless of the fourth byte. + // The fork requires len==4 && buffer[0]==0x20 && buffer[1]==2. Expected PASS. + TEST_CASE("F-06 malformed four-byte CONNACK frames are rejected" * FINDING_MARKER(F06)) { + struct Case { const char* label; uint8_t b0; uint8_t b1; }; + const Case malformations[] = { + {"type CONNECT (0x10)", 0x10, 0x02}, + {"type PUBLISH (0x30)", 0x30, 0x02}, + {"type PINGRESP (0xD0)", 0xD0, 0x02}, + {"nonzero flags (0x21)", 0x21, 0x02}, + {"nonzero flags (0x2F)", 0x2F, 0x02}, + {"remaining length 1", 0x20, 0x01}, + {"remaining length 3", 0x20, 0x03}, + {"remaining length 0", 0x20, 0x00}, + }; + const uint8_t fourthBytes[] = {0x00, 0xFF}; + + for (const Case& m : malformations) { + for (uint8_t fourth : fourthBytes) { + CAPTURE(m.label); + CAPTURE(fourth); + TestClock::instance().reset(); + MockClient client; + client.pushInbound({m.b0, m.b1, 0x00, fourth}); + + PubSubClient psc(client); + psc.setServer("broker.example", 1883); + psc.setSocketTimeout(1); + + CHECK_FALSE(psc.connect("f06-client")); + CHECK_FALSE(psc.connected()); + CHECK(psc.state() != MQTT_CONNECTED); + } + } + } + + // ======================================================================= + // F-07 - Allocation-failure buffer state (Requirement 13.2) + // ======================================================================= + // + // With the allocation interposer armed to fail the next allocation, the + // realloc inside setBufferSize returns NULL. The library must NOT commit the + // new size: getBufferSize keeps reporting the previous (valid) size and the + // existing buffer stays usable. If it committed a nonzero size backed by a + // null buffer, the subsequent publish would dereference it and ASan would + // abort. Expected PASS. + TEST_CASE("F-07 a failed setBufferSize leaves no nonzero capacity backed by a null buffer" + * FINDING_MARKER(F07)) { + TestClock::instance().reset(); + AllocInterposer::reset(); + MockClient client; + PubSubClient psc(client); + + const uint16_t before = psc.getBufferSize(); + REQUIRE(before == MQTT_MAX_PACKET_SIZE); + + AllocInterposer::failNextAllocation(); + const bool grew = psc.setBufferSize(4096); + AllocInterposer::reset(); + + CHECK_FALSE(grew); + CHECK(psc.getBufferSize() == before); // size unchanged + + // Prove the buffer behind that size is still valid and usable (clean + // under ASan - no null/dangling buffer left by the failed realloc). + connectAndClear(client, psc); + REQUIRE(psc.publish("tele/dev/STATE", "online")); + + DecodedPublish pub = MqttParser::decodePublish(client.outbound()); + REQUIRE(pub.valid); + CHECK(pub.topic == "tele/dev/STATE"); + const std::string payload(pub.payload.begin(), pub.payload.end()); + CHECK(payload == "online"); + CHECK(psc.connected()); + } + + // ======================================================================= + // F-08 - SUBSCRIBE off-by-one exact-buffer write + null-topic checks + // ======================================================================= + // + // A SUBSCRIBE whose topic length brings the packet to exactly the working + // buffer capacity must not write the trailing QoS byte out of bounds. The + // library's bound is bufferSize >= 10 + topicLength (header(5) + msgId(2) + + // topic-length(2) + topic + qos(1)); at exact capacity the QoS byte lands at + // buffer[bufferSize-1]. An off-by-one would write buffer[bufferSize] and ASan + // would abort. Null topics must be rejected before any strnlen dereference. + // Expected PASS. + TEST_CASE("F-08 exact-buffer SUBSCRIBE stays in-bounds and null topics are rejected" + * FINDING_MARKER(F08)) { + const size_t topicLengths[] = {1, 5, 16, 50, 117}; + const uint8_t qosValues[] = {0, 1}; + + for (size_t topicLen : topicLengths) { + for (uint8_t qos : qosValues) { + CAPTURE(topicLen); + CAPTURE(qos); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + // Shrink the buffer to EXACTLY the SUBSCRIBE bound (10 + topicLen). + const uint16_t exactCapacity = static_cast(10 + topicLen); + REQUIRE(psc.setBufferSize(exactCapacity)); + REQUIRE(psc.getBufferSize() == exactCapacity); + + const std::string topic(topicLen, 'a'); + // ASan proves the QoS byte is written at buffer[bufferSize-1], not + // buffer[bufferSize]. + REQUIRE(psc.subscribe(topic.c_str(), qos)); + + DecodedSubscribe s = MqttParser::decodeSubscribe(client.outbound()); + REQUIRE(s.valid); + REQUIRE(s.filters.size() == 1); + CHECK(s.filters[0] == topic); + REQUIRE(s.requestedQos.size() == 1); + CHECK(s.requestedQos[0] == qos); + } + } + + // Null-topic rejection (no strnlen(nullptr) dereference): subscribe / + // unsubscribe return false and emit nothing. + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + CHECK_FALSE(psc.subscribe(nullptr)); + CHECK_FALSE(psc.subscribe(nullptr, 1)); + CHECK_FALSE(psc.unsubscribe(nullptr)); + CHECK(client.outbound().empty()); + CHECK(psc.connected()); + } + + // ======================================================================= + // F-09 - Unsupported/malformed QoS in inbound PUBLISH (Req 11.5, 11.6) + // ======================================================================= + // + // An inbound PUBLISH whose QoS bits encode 2 (unsupported) or 3 (illegal) + // must be rejected rather than parsed as QoS 0. Each fixture is otherwise + // well-framed, so a naive parser would dispatch it; the fork rejects qos > 1 + // up front. Expected PASS. + TEST_CASE("F-09 inbound PUBLISH with QoS 2 or QoS 3 is rejected" * FINDING_MARKER(F09)) { + const uint8_t badQos[] = {2, 3}; + + for (uint8_t qos : badQos) { + CAPTURE(qos); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + const std::string topic = "tele/dev/SENSOR"; + const std::vector payload = makePayload(16); + // qos > 0 => the builder emits a packet identifier, so the frame is + // well-formed apart from the illegal/unsupported QoS. + client.pushPacket(MqttPacket::publish(topic, payload, qos, + /*retained=*/false, /*msgId=*/0x0042)); + + psc.setSocketTimeout(1); + psc.loop(); + + CHECK(cap.count == 0); // not dispatched as QoS 0 + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + } + + // ======================================================================= + // F-10 - SUBACK / session-present tracking (Requirement 10.4) - EXPECTED FAIL + // ======================================================================= + // + // A subscribe operation should reflect the SUBACK return code rather than + // reporting success unconditionally. A hardened implementation, on receiving + // a SUBACK with the failure return code (0x80), would surface the failure. + // The current fork ignores SUBACK and returns the transport write result, so + // the hardened assertion reports an EXPECTED FAILURE. + TEST_CASE("F-10 subscribe reflects the SUBACK return code" * FINDING_MARKER(F10)) { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + // SUBACK for the first packet identifier (nextMsgId advances to 1) with + // the MQTT "failure" return code 0x80. + client.pushPacket(MqttPacket::suback(1, 0x80)); + + CHECK_FALSE(psc.subscribe("tele/dev/SENSOR")); + } + + // ======================================================================= + // F-11 - Graceful disconnect default (Requirement 8.4) - EXPECTED FAIL + // ======================================================================= + // + // disconnect() with no argument should transmit an MQTT DISCONNECT packet. + // The current fork defaults to an ungraceful transport close and writes + // nothing, so the hardened assertion reports an EXPECTED FAILURE. + TEST_CASE("F-11 disconnect() with no argument transmits a DISCONNECT packet" + * FINDING_MARKER(F11)) { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + psc.disconnect(); // default argument: no DISCONNECT packet in current fork + + DecodedPacket d = MqttParser::decode(client.outbound()); + REQUIRE(d.valid); + CHECK(d.type == static_cast(MQTTDISCONNECT)); + CHECK(d.remainingLength == 0); + } + + // ======================================================================= + // F-12 - Test-suite quality meta-finding + // ======================================================================= + // + // F-12 is not a library bug but a property of the TEST SYSTEM: the original + // tests contained their own out-of-bounds stack writes and missed the + // dangerous boundaries. This test system addresses it by construction - + // adversarial fixtures are built via sized std::vector / MqttPacket + // (never a raw over-indexed array), memory-safety fixtures run under ASan/ + // UBSan (on by default), and an integration boundary exercises the real + // callback contract. This fixture documents and verifies that meta-finding: + // it builds a battery of adversarial frames the way the whole suite does, + // confirms each is a properly sized container (no OOB in the test code + // itself, Requirement 14.3), and drives the worst-case fixture through the + // library so a regression that read past the received bytes would trip ASan. + TEST_CASE("F-12 adversarial fixtures are built via sized containers and run clean under sanitizers") { + // A battery of adversarial frames, each built ONLY through MqttPacket / + // sized std::vector. Indexing stays within .size() throughout. + std::vector> fixtures; + + // Malformed CONNACK (F-06 shape). + fixtures.push_back({0x30, 0x02, 0x00, 0x00}); + // QoS 1 PUBLISH claiming a topic far larger than the packet (F-02 shape). + { + std::vector f = {static_cast(MQTTPUBLISH | MQTTQOS1), + 0x04, 0x01, 0xF4, 'x', 'y'}; + fixtures.push_back(f); + } + // Oversized declared Remaining Length, no body (F-03 shape). + { + std::vector f = {static_cast(MQTTPUBLISH)}; + const std::vector rl = MqttPacket::encodeRemainingLength(5000000); + f.insert(f.end(), rl.begin(), rl.end()); + fixtures.push_back(f); + } + // Well-formed QoS 3 PUBLISH (F-09 shape), built structurally. + fixtures.push_back(MqttPacket::publish("t", std::vector{'a'}, + /*qos=*/3, /*retained=*/false, + /*msgId=*/0x0001).bytes()); + + // Meta-assertion: every fixture is a properly sized container and every + // byte is addressable within bounds (the test code performs no OOB + // access when constructing or reading its own fixtures). + for (const std::vector& f : fixtures) { + CHECK(f.size() >= 2); + size_t seen = 0; + for (size_t i = 0; i < f.size(); ++i) { + (void)f[i]; // in-bounds by construction (i < f.size()) + ++seen; + } + CHECK(seen == f.size()); + } + + // Drive the worst-case over-claiming fixture through the library. If a + // regression read beyond the received bytes, ASan/UBSan would abort the + // process; reaching the end of this case cleanly is the verification. + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + client.pushPacket(MqttPacket::raw(fixtures[1])); // the F-02 over-claim + psc.setSocketTimeout(1); + psc.loop(); + + // The adversarial packet is rejected without dispatching the callback and + // without any out-of-bounds access (ASan-clean run). + CHECK(cap.count == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/integration_test.cpp b/lib/default/TasmotaPubSub/tests/src/integration_test.cpp new file mode 100644 index 000000000..8fcb0856f --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/integration_test.cpp @@ -0,0 +1,260 @@ +/* + integration_test.cpp - Tasmota callback-contract integration boundary tests + for the TasmotaPubSub host test system (task 10.10). + + Feature: tasmota-pubsub-tests + + These tests exercise the *seam between the library and its real consumer*: an + inbound PUBLISH is scripted into the MockClient, delivered to the *unmodified* + library via psc.loop(), and dispatched to the CallbackContractAdapter. The + adapter faithfully reproduces the Tasmota `MqttDataHandler` contract from + xdrv_02_9_mqtt.ino by writing a NUL byte at index `data_len` of the delivered + payload buffer (`mqtt_data[data_len] = 0`, Requirement 15.1). Because that + payload pointer points INTO the library's own working-buffer allocation, the + NUL write lands relative to that allocation and is therefore validated under + AddressSanitizer (on by default). This is what catches consumer-side boundary + bugs such as F-01 (Requirement 15). + + Baseline (TEST_SUITE("baseline")), Requirement 15.2: + - A normal delivered QoS 0 / QoS 1 PUBLISH runs the adapter's NUL-write path + end to end (delivered through the library, NUL terminator written into the + library buffer) with the recorded (topic, payload, length) matching the + fixture. ASan proves the NUL write is in-bounds. + + Hardening (TEST_SUITE("hardening")), Requirement 15.3 / F-01: + - Using setBufferSize to create a precise, small working buffer, inbound + PUBLISH fixtures are delivered at total wire sizes bufferSize-1, + bufferSize and bufferSize+1. Because the fork reserves a sentinel byte + (readPacket rejects total_packet >= bufferSize and idx >= bufferSize), the + exact-buffer and over-buffer packets are REJECTED before dispatch, so the + delivered length is bounded and the adapter's NUL write stays in-bounds: + no byte is written past the library buffer allocation (ASan proves it). + The accepted (bufferSize-1) case runs the real NUL write on the reserved + spare byte; the recorded length is asserted to be within the buffer bounds + and the callback dispatch is asserted to match only the accepted case. + F-01 is hardened, so this is an expected PASS (FINDING_MARKER(F01)). + + Every assertion goes through the public API, the adapter's recorded values, + and decoded MockClient.outbound() wire bytes - never private members - so the + suite stays durable across a future MQTT 5 migration (Requirement 19.1). The + library under test is NOT modified. +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "CallbackContractAdapter.h" +#include "FindingStatus.h" +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Connect the client/psc pair with a scripted CONNACK, register the faithful +// MqttDataHandler-contract adapter as the callback, and clear the recorded +// CONNECT bytes so only the packet-under-test remains in outbound(). +void connectWithAdapter(MockClient& client, PubSubClient& psc, + CallbackContractAdapter& adapter) { + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + psc.setCallback(adapter.callback()); + REQUIRE(psc.connect("integration-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +// Deterministic binary payload of length n (includes 0x00 bytes so the +// round-trip proves the dispatch is binary-safe and length-driven, not +// dependent on C-string termination). +std::vector makePayload(size_t n) { + std::vector v(n); + for (size_t i = 0; i < n; ++i) { + v[i] = static_cast((i * 31u + 7u) & 0xFFu); + } + return v; +} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 15.2: the adapter NUL-write path runs on a normal delivered PUBLISH - + + // A normal QoS 0 PUBLISH delivered through the library to the adapter runs + // the real `mqtt_data[data_len] = 0` write into the library working buffer. + // The recorded (topic, payload, length) matches the fixture and the NUL + // write is in-bounds (ASan proves it, since the write targets the reserved + // spare byte of the library allocation). + TEST_CASE("integration: adapter NUL-write path runs on a normal QoS 0 PUBLISH") { + struct Case { const char* label; std::string topic; std::vector payload; }; + std::vector cases = { + {"empty payload", "tele/dev/STATE", {}}, + {"short payload", "cmnd/dev/POWER", {'O','N'}}, + {"binary payload", "tele/dev/SENSOR", makePayload(128)}, + {"single-char topic","t", makePayload(40)}, + }; + + for (const Case& c : cases) { + CAPTURE(c.label); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackContractAdapter adapter; + connectWithAdapter(client, psc, adapter); + + client.pushPacket(MqttPacket::publish(c.topic, c.payload, /*qos=*/0)); + REQUIRE(psc.loop()); + + // The adapter recorded exactly one invocation (the payload copy is + // captured BEFORE the NUL write) and then performed the NUL write + // into the delivered library buffer - clean under ASan. + REQUIRE(adapter.performsNulWrite()); + REQUIRE(adapter.count() == 1); + CHECK(adapter.last().topic == c.topic); + CHECK(adapter.last().length == c.payload.size()); + CHECK(adapter.last().payload == c.payload); + + // A QoS 0 PUBLISH is not acknowledged; the connection stays healthy. + CHECK(client.outbound().empty()); + CHECK(psc.connected()); + CHECK_FALSE(client.stopCalled()); + } + } + + // A QoS 1 PUBLISH exercises the same adapter NUL-write contract while the + // library additionally records a PUBACK outbound. This confirms the + // adapter path is driven correctly on the acknowledged receive path too. + TEST_CASE("integration: adapter NUL-write path runs on a QoS 1 PUBLISH and a PUBACK is emitted") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackContractAdapter adapter; + connectWithAdapter(client, psc, adapter); + + const std::string topic = "cmnd/dev/POWER"; + const std::vector payload = makePayload(64); + const uint16_t msgId = 0x1234; + + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/1, + /*retained=*/false, msgId)); + REQUIRE(psc.loop()); + + REQUIRE(adapter.count() == 1); + CHECK(adapter.last().topic == topic); + CHECK(adapter.last().length == payload.size()); + CHECK(adapter.last().payload == payload); + + // The PUBACK echoing the message id is recorded outbound. + DecodedPacket ack = MqttParser::decode(client.outbound()); + REQUIRE(ack.valid); + CHECK(ack.type == static_cast(MQTTPUBACK)); + CHECK(ack.remainingLength == 2); + REQUIRE(ack.payload.size() == 2); + const uint16_t ackMsgId = + static_cast((ack.payload[0] << 8) | ack.payload[1]); + CHECK(ackMsgId == msgId); + + CHECK(psc.connected()); + } +} + +TEST_SUITE("hardening") { + + // --- 15.3 / F-01: exact-buffer inbound PUBLISH boundary under ASan ------- + + // Feature: tasmota-pubsub-tests + // + // An exact-buffer inbound PUBLISH must not cause the adapter's + // `mqtt_data[data_len] = 0` write to access memory beyond the library + // buffer allocation (Requirement 15.3). setBufferSize creates precise, small + // working buffers; PUBLISH fixtures are delivered at total wire sizes + // bufferSize-1, bufferSize and bufferSize+1: + // total == bufferSize-1 : ACCEPTED -> callback fires; the adapter NUL write + // lands on the reserved spare byte + // buffer[bufferSize-1] (in-bounds). + // total == bufferSize : REJECTED -> no callback; connection closed. + // total == bufferSize+1 : REJECTED -> no callback; connection closed. + // Because the exact/over-buffer packets are rejected before dispatch, the + // delivered length is bounded and no byte is ever written past the library + // allocation. AddressSanitizer proves the boundary; the assertions also pin + // the recorded length to within the buffer bounds and require the callback + // to fire only for the accepted case. F-01 is hardened => expected PASS. + TEST_CASE("F-01 integration: exact-buffer inbound PUBLISH keeps the adapter NUL write in-bounds" + * FINDING_MARKER(F01)) { + // Several precise buffer sizes. topicLen == 1 => total wire size of a + // QoS 0 PUBLISH = 1 (fixed) + 1 (RL byte, RL < 128) + 2 (topic-length + // field) + 1 (topic) + payloadLen = 5 + payloadLen, so payloadLen = + // total - 5. Sizes are kept small enough that the Remaining Length stays + // in a single byte (RL = 2 + topicLen + payloadLen < 128). + const uint16_t bufferSizes[] = {32, 64, 100}; + const std::string topic = "t"; // topicLen == 1 + + struct Case { const char* label; int totalDelta; bool accepted; }; + const Case cases[] = { + {"bufferSize-1 (accepted)", -1, true}, + {"bufferSize (rejected)", 0, false}, + {"bufferSize+1 (rejected)", +1, false}, + }; + + for (uint16_t kBufferSize : bufferSizes) { + for (const Case& c : cases) { + CAPTURE(kBufferSize); + CAPTURE(c.label); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackContractAdapter adapter; + + // Register the faithful MqttDataHandler-contract adapter so the + // accepted case performs the real NUL-termination write into the + // library buffer under ASan. + connectWithAdapter(client, psc, adapter); + + // Precisely size the working buffer AFTER connect (CONNECT needs + // the default buffer); the three boundary totals are then exact. + REQUIRE(psc.setBufferSize(kBufferSize)); + REQUIRE(psc.getBufferSize() == kBufferSize); + + const int total = static_cast(kBufferSize) + c.totalDelta; + const size_t payloadLen = static_cast(total) - 5; + const std::vector payload = makePayload(payloadLen); + + // Built structurally via the packet builder (sized vectors only, + // so the test code performs no out-of-bounds writes). + const MqttPacket pkt = MqttPacket::publish(topic, payload, /*qos=*/0); + REQUIRE(pkt.size() == static_cast(total)); // fixture is exact + client.pushPacket(pkt); + + psc.setSocketTimeout(1); // bound any read as a safety net + psc.loop(); + + if (c.accepted) { + // Delivered exactly once with the fixture payload; the NUL + // write (adapter, enabled by default) landed on the reserved + // sentinel byte buffer[bufferSize-1] - no byte written past + // the library allocation (ASan proves it). + REQUIRE(adapter.count() == 1); + CHECK(adapter.last().length == payloadLen); + CHECK(adapter.last().payload == payload); + // The delivered length + its NUL terminator stay within the + // library buffer bounds (data_len < bufferSize). + CHECK(adapter.last().length < kBufferSize); + CHECK(psc.connected()); + CHECK_FALSE(client.stopCalled()); + } else { + // Rejected before dispatch: the adapter is never invoked, so + // no NUL write occurs at or beyond buffer[bufferSize]; the + // connection is closed. + CHECK(adapter.count() == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + } + } + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/keepalive_test.cpp b/lib/default/TasmotaPubSub/tests/src/keepalive_test.cpp new file mode 100644 index 000000000..4de7b6def --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/keepalive_test.cpp @@ -0,0 +1,227 @@ +/* + keepalive_test.cpp - keepalive / ping cycle characterization for the + TasmotaPubSub host test system (task 10.6). + + Keepalive is driven entirely in VIRTUAL time: the shim's delay() advances the + same TestClock that the library's millis() reads, so advancing the clock past + the keepalive interval and calling loop() exercises the ping machinery with no + real elapsed wall-clock time (Requirement 7.3). setKeepAlive() (seconds) is + configured BEFORE connect so the CONNECT packet and the loop() timing both use + the chosen interval. + + Baseline (TEST_SUITE("baseline")): + - Advancing TestClock beyond the keepalive interval then calling loop() + records a PINGREQ (Requirement 12.1). + - A scripted PINGRESP clears the outstanding-ping state, so the connection + stays alive and a later idle interval issues a further PINGREQ instead of + declaring a timeout (Requirement 12.2). + - Advancing beyond the interval a second time with no PINGRESP reports a lost + connection (Requirement 12.3). + + Properties (folded in as single deterministic, data-driven cases over a curated + interval table {15 s default, 2 s short} - NO randomized generators): + - Property 8: exactly one PINGREQ is issued per idle interval, and after a + matching PINGRESP another PINGREQ is issued on the next interval rather + than a timeout being declared (Requirements 12.1, 12.2, 18.9). + - Property 9: advancing beyond two consecutive intervals with no PINGRESP + reports a lost/timed-out connection (Requirements 12.3, 18.8, 18.9). + + Every assertion goes through the public API and decoded MockClient.outbound() + wire bytes - never private members - so the baseline stays durable across a + future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include + +#include "doctest.h" + +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Connect the client/psc pair with a scripted CONNACK using the supplied +// keepalive interval (seconds, set BEFORE connect), then clear the recorded +// CONNECT bytes so only the packets emitted by loop() remain in outbound(). +void connectAndClear(MockClient& client, PubSubClient& psc, uint16_t keepAliveSecs) { + client.pushPacket(MqttPacket::connack(0)); // return code 0 = accepted + psc.setServer("broker.example", 1883); + psc.setKeepAlive(keepAliveSecs); + REQUIRE(psc.connect("keepalive-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +// Move virtual time strictly beyond one keepalive interval (the library's guard +// is `t - lastActivity > keepAlive*1000`). A generous 1 s margin also absorbs +// any small virtual time consumed during connect(). +void advanceBeyondInterval(uint16_t keepAliveSecs) { + TestClock::instance().advance(static_cast(keepAliveSecs) * 1000UL + 1000UL); +} + +// True when `out` is exactly one MQTT PINGREQ control packet (0xC0 0x00). +bool isSinglePingreq(const std::vector& out) { + DecodedPacket d = MqttParser::decode(out); + return d.valid && + d.type == static_cast(MQTTPINGREQ) && + d.flags == 0x00 && + d.remainingLength == 0 && + out.size() == 2; +} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 12.1: idle beyond the interval + loop() => PINGREQ recorded -------- + + TEST_CASE("advancing beyond the keepalive interval and calling loop records a PINGREQ") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc, MQTT_KEEPALIVE); // default 15 s + + advanceBeyondInterval(MQTT_KEEPALIVE); + REQUIRE(psc.loop()); // still connected + + // The recorded outbound is exactly one well-framed PINGREQ. + CHECK(isSinglePingreq(client.outbound())); + CHECK(psc.connected()); + } + + // --- 12.2: a scripted PINGRESP clears the outstanding-ping state -------- + + TEST_CASE("a scripted PINGRESP clears the outstanding-ping state and keeps the connection alive") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc, MQTT_KEEPALIVE); + + // First idle interval: a PINGREQ goes out and a ping is now outstanding. + advanceBeyondInterval(MQTT_KEEPALIVE); + REQUIRE(psc.loop()); + REQUIRE(isSinglePingreq(client.outbound())); + + // Deliver the matching PINGRESP; loop() consumes it and clears the + // outstanding-ping state. No bytes are written back for a PINGRESP. + client.clearOutbound(); + client.pushPacket(MqttPacket::pingresp()); + REQUIRE(psc.loop()); + CHECK(client.outbound().empty()); + CHECK(psc.connected()); + + // Because the outstanding ping was cleared, the next idle interval issues + // a FURTHER PINGREQ instead of declaring a timeout - the observable proof + // that the outstanding-ping state was reset. + client.clearOutbound(); + advanceBeyondInterval(MQTT_KEEPALIVE); + REQUIRE(psc.loop()); + CHECK(isSinglePingreq(client.outbound())); + CHECK(psc.connected()); + } + + // --- 12.3: idle beyond the interval with no PINGRESP => lost connection -- + + TEST_CASE("advancing beyond the interval without a PINGRESP reports a lost connection") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc, MQTT_KEEPALIVE); + + // First interval: PINGREQ sent, ping now outstanding. + advanceBeyondInterval(MQTT_KEEPALIVE); + REQUIRE(psc.loop()); + REQUIRE(isSinglePingreq(client.outbound())); + + // Second interval with no PINGRESP delivered: the library detects the + // unanswered ping and reports a lost/timed-out connection. + advanceBeyondInterval(MQTT_KEEPALIVE); + CHECK_FALSE(psc.loop()); + CHECK_FALSE(psc.connected()); + CHECK(psc.state() == MQTT_CONNECTION_TIMEOUT); + CHECK(client.stopCalled()); + } + + // --- Property 8: exactly one PINGREQ per idle interval ------------------ + + // Feature: tasmota-pubsub-tests, Property 8: for all keepalive intervals, + // when the virtual clock is advanced beyond the interval with no other + // activity and loop() is called, exactly one PINGREQ is recorded; and after a + // matching PINGRESP is delivered, advancing another interval issues a further + // PINGREQ rather than declaring a timeout. Validated deterministically over a + // curated interval table (default 15 s and a short 2 s), no randomized + // generators. + TEST_CASE("Property 8: keepalive issues exactly one PINGREQ per idle interval") { + const uint16_t intervals[] = {MQTT_KEEPALIVE, 2}; + + for (uint16_t keepAliveSecs : intervals) { + CAPTURE(keepAliveSecs); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc, keepAliveSecs); + + // One idle interval => exactly one PINGREQ. + advanceBeyondInterval(keepAliveSecs); + REQUIRE(psc.loop()); + CHECK(isSinglePingreq(client.outbound())); + + // Calling loop() again WITHOUT advancing must not emit a second + // PINGREQ within the same idle interval. + client.clearOutbound(); + REQUIRE(psc.loop()); + CHECK(client.outbound().empty()); + CHECK(psc.connected()); + + // Deliver the matching PINGRESP so the outstanding ping is cleared. + client.clearOutbound(); + client.pushPacket(MqttPacket::pingresp()); + REQUIRE(psc.loop()); + CHECK(client.outbound().empty()); + CHECK(psc.connected()); + + // The next idle interval issues a FURTHER PINGREQ (not a timeout). + client.clearOutbound(); + advanceBeyondInterval(keepAliveSecs); + REQUIRE(psc.loop()); + CHECK(isSinglePingreq(client.outbound())); + CHECK(psc.connected()); + } + } + + // --- Property 9: missing PINGRESP produces a lost connection ------------ + + // Feature: tasmota-pubsub-tests, Property 9: for all keepalive intervals, + // advancing the virtual clock beyond two consecutive intervals without + // delivering a PINGRESP causes the library to report a lost/timed-out + // connection. Validated deterministically over a curated interval table + // (default 15 s and a short 2 s), no randomized generators. + TEST_CASE("Property 9: missing PINGRESP over two intervals reports a lost connection") { + const uint16_t intervals[] = {MQTT_KEEPALIVE, 2}; + + for (uint16_t keepAliveSecs : intervals) { + CAPTURE(keepAliveSecs); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc, keepAliveSecs); + + // First interval: PINGREQ sent, connection still alive. + advanceBeyondInterval(keepAliveSecs); + REQUIRE(psc.loop()); + CHECK(isSinglePingreq(client.outbound())); + CHECK(psc.connected()); + + // Second interval with no PINGRESP: the unanswered ping trips the + // keepalive timeout and the connection is reported lost. + advanceBeyondInterval(keepAliveSecs); + CHECK_FALSE(psc.loop()); + CHECK_FALSE(psc.connected()); + CHECK(psc.state() == MQTT_CONNECTION_TIMEOUT); + CHECK(client.stopCalled()); + } + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/AllocInterposer.cpp b/lib/default/TasmotaPubSub/tests/src/lib/AllocInterposer.cpp new file mode 100644 index 000000000..80d7ed26f --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/AllocInterposer.cpp @@ -0,0 +1,72 @@ +/* + AllocInterposer.cpp - Implementation of the allocation-failure injection used + by the F-07 hardening test (Requirement 13.2). + + This translation unit is compiled with the NORMAL build rule (no AllocShim.h + force-include), so `malloc`/`realloc` below are the real C-library allocators. + Only the library TU (PubSubClient.cpp) has its malloc/realloc macro-routed to + the tpubsub_test_* wrappers defined here. +*/ + +#include "AllocInterposer.h" + +#include + +namespace { + +// Number of upcoming routed allocations still armed to fail. Single-threaded. +unsigned g_failCountdown = 0; +unsigned g_mallocCount = 0; +unsigned g_reallocCount = 0; + +} // namespace + +namespace AllocInterposer { + +void failNextAllocation() { + g_failCountdown = 1; +} + +void failNextAllocations(unsigned n) { + g_failCountdown = n; +} + +void reset() { + g_failCountdown = 0; + g_mallocCount = 0; + g_reallocCount = 0; +} + +unsigned pendingFailures() { + return g_failCountdown; +} + +unsigned mallocCount() { + return g_mallocCount; +} + +unsigned reallocCount() { + return g_reallocCount; +} + +} // namespace AllocInterposer + +extern "C" void* tpubsub_test_malloc(size_t size) { + ++g_mallocCount; + if (g_failCountdown > 0) { + --g_failCountdown; + return nullptr; // model malloc() returning NULL + } + return malloc(size); +} + +extern "C" void* tpubsub_test_realloc(void* ptr, size_t size) { + ++g_reallocCount; + if (g_failCountdown > 0) { + --g_failCountdown; + // Model realloc() failing: return NULL and leave the original block + // (ptr) untouched, exactly as the C contract specifies. + return nullptr; + } + return realloc(ptr, size); +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/AllocInterposer.h b/lib/default/TasmotaPubSub/tests/src/lib/AllocInterposer.h new file mode 100644 index 000000000..5d1d241ac --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/AllocInterposer.h @@ -0,0 +1,57 @@ +/* + AllocInterposer.h - Allocation-failure injection for the TasmotaPubSub host + test system (support for F-07 / Requirement 13.2). + + The *unmodified* library allocates its working buffer with malloc()/realloc() + in setBufferSize(). To exercise the allocation-failure path deterministically + on the host, the library translation unit is compiled with the malloc/realloc + calls routed through the C-linkage wrappers declared below (see + src/lib/AllocShim.h, force-included ONLY when compiling PubSubClient.cpp - so + no other translation unit, the shim, doctest, or the tests are affected). + + Tests arm the next allocation(s) to fail via the AllocInterposer API. When + disarmed (the default), every wrapper call passes straight through to the real + allocator, so behavior is identical to a normal build. When armed, the wrapper + returns nullptr WITHOUT calling the real allocator - modelling malloc()/ + realloc() returning NULL. Because realloc() leaving its original block intact + on failure is the real contract, an armed realloc wrapper also leaves the + caller's pointer untouched (it simply never calls realloc()). + + This is intentionally single-threaded (the test binary is single-threaded) and + keeps counters so tests can assert how many allocations the library performed. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_ALLOC_INTERPOSER_H +#define TASMOTA_PUBSUB_TEST_ALLOC_INTERPOSER_H + +#include + +namespace AllocInterposer { + +// Arm exactly the next routed allocation (malloc or realloc) to fail. +void failNextAllocation(); + +// Arm the next `n` routed allocations to fail (n == 0 disarms). +void failNextAllocations(unsigned n); + +// Disarm any pending failures and reset the malloc/realloc call counters. A +// test should call this before arming and again once the failure has been +// exercised, so later tests in the same process are never affected. +void reset(); + +// How many armed failures are still pending (0 when disarmed). +unsigned pendingFailures(); + +// Total number of routed malloc()/realloc() calls seen since the last reset(). +unsigned mallocCount(); +unsigned reallocCount(); + +} // namespace AllocInterposer + +// C-linkage wrappers the library TU's malloc()/realloc() calls are routed to +// (via the macros in AllocShim.h). Declared here so both the library shim and +// the interposer implementation agree on the signatures. +extern "C" void* tpubsub_test_malloc(size_t size); +extern "C" void* tpubsub_test_realloc(void* ptr, size_t size); + +#endif // TASMOTA_PUBSUB_TEST_ALLOC_INTERPOSER_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/AllocShim.h b/lib/default/TasmotaPubSub/tests/src/lib/AllocShim.h new file mode 100644 index 000000000..519b2285f --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/AllocShim.h @@ -0,0 +1,45 @@ +/* + AllocShim.h - malloc/realloc routing header, force-included ONLY when compiling + the library under test (PubSubClient.cpp). See tests/Makefile ($(LIB_OBJ)). + + Purpose: route the *unmodified* library's malloc()/realloc() calls through the + test interposer (AllocInterposer) so the F-07 allocation-failure path can be + exercised deterministically (Requirement 13.2), WITHOUT modifying the library + source itself. + + Safety: every standard header the library pulls in is included HERE, before the + macros are defined. That way the function-like `malloc(`/`realloc(` macros + only ever rewrite the library's OWN call sites and can never corrupt a + declaration inside , , , etc. (those are all + processed while the macros are still undefined, and their include guards make + the library's later #includes no-ops). The shim headers the library includes + (Arduino.h / Client.h / Stream.h / IPAddress.h) contain no malloc/realloc call + sites, so they are unaffected. + + Scope: because this header is force-included ONLY for the library object, no + other translation unit (the shim .cpp files, doctest, or the *_test.cpp files) + sees these macros. AllocInterposer.cpp therefore calls the real allocators. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_ALLOC_SHIM_H +#define TASMOTA_PUBSUB_TEST_ALLOC_SHIM_H + +// Lock in every standard header the library TU depends on BEFORE defining the +// macros, so no system declaration of malloc/realloc is ever rewritten. +#include +#include +#include +#include +#include +#include + +#include "AllocInterposer.h" + +// Route the library's own malloc()/realloc() call sites to the interposer. +// free() is intentionally left as the real deallocator: the wrappers return +// blocks obtained from the real malloc()/realloc(), so plain free() releases +// them correctly (including the destructor's free(this->buffer)). +#define malloc(sz) tpubsub_test_malloc((sz)) +#define realloc(p, sz) tpubsub_test_realloc((p), (sz)) + +#endif // TASMOTA_PUBSUB_TEST_ALLOC_SHIM_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/Arduino.cpp b/lib/default/TasmotaPubSub/tests/src/lib/Arduino.cpp new file mode 100644 index 000000000..05a4e7c65 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/Arduino.cpp @@ -0,0 +1,47 @@ +/* + Arduino.cpp - Host-side Arduino core shim time primitives for TasmotaPubSub tests. + + Defines the time / scheduling primitives declared in Arduino.h by wiring them to + the virtual TestClock (Clock_Injector). This is the mechanism that lets the + *unmodified* PubSubClient library's busy-wait loops terminate deterministically + in finite virtual time, without any real elapsed wall-clock time: + + - readByte() busy-waits calling delay(1) while polling the transport. + - connect() busy-waits calling delay(0) while waiting for CONNACK. + - loop() keepalive compares millis() against the last activity timestamp. + + Because onDelay() always advances the virtual clock by at least autoStep (>= 1 ms), + even delay(0) makes forward progress, so an exhausted inbound queue drives the + library's millis()-based timeout path to completion instead of spinning forever. + + Requirements: 7.1 (millis() supplies the library's time), 7.2 (advanced value is + observed on subsequent reads), 7.3 (target time reached without real elapsed time). +*/ + +#include "Arduino.h" +#include "TestClock.h" + +// Requirement 7.1 / 7.2: the library's notion of "now" is the virtual clock. +unsigned long millis() { + return TestClock::instance().millis(); +} + +// Requirement 7.3: delay() advances virtual time (never sleeps on the wall clock). +// onDelay() guarantees a minimum forward step so delay(0)/delay(1) busy-wait loops +// in the library terminate deterministically. +void delay(unsigned long ms) { + TestClock::instance().onDelay(ms); +} + +// Microsecond delays are sub-millisecond for the library's purposes. Route them +// through the same forward-progress path so no code path can wall-clock sleep; +// onDelay(0) advances by autoStep, keeping any micro-scale busy-wait deterministic. +void delayMicroseconds(unsigned int /*us*/) { + TestClock::instance().onDelay(0); +} + +// yield() cooperatively reschedules on-device; on the host there is nothing to +// yield to, so it is a no-op (advances by 0). +void yield() { + // no-op +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/Arduino.h b/lib/default/TasmotaPubSub/tests/src/lib/Arduino.h new file mode 100644 index 000000000..7961c9d7d --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/Arduino.h @@ -0,0 +1,60 @@ +/* + Arduino.h - Host-side Arduino core shim for TasmotaPubSub host tests. + + Provides the Arduino primitives the *unmodified* PubSubClient library depends + on when compiled on a host (g++/clang) instead of an ESP toolchain. This is the + platform seam: the library `#include ` resolves to this header + because `tests/src/lib` is first on the include path. + + Time primitives (millis/delay/yield) are DECLARED here but intentionally NOT + defined. They are wired to the virtual TestClock in a later task (3.2). The + library links against these declarations. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_ARDUINO_H +#define TASMOTA_PUBSUB_TEST_ARDUINO_H + +#include +#include +#include +#include +#include + +// The library selects the std::function callback signature only when building +// for an ESP target. The host tests must build with -DESP32 so lambdas with +// captures can be bound as callbacks. Make the requirement explicit. +#if !defined(ESP32) && !defined(ESP8266) +#error "Build the host shim with -DESP32 so MQTT_CALLBACK_SIGNATURE resolves to std::function" +#endif + +// Arduino type aliases used by the library. +typedef bool boolean; +typedef uint8_t byte; + +// PROGMEM is a no-op on a host with a flat address space. publish_P therefore +// reads normal memory via pgm_read_byte_near. +#ifndef PROGMEM +#define PROGMEM +#endif + +#ifndef PGM_P +typedef const char* PGM_P; +#endif + +#ifndef pgm_read_byte_near +#define pgm_read_byte_near(addr) (*reinterpret_cast(addr)) +#endif +#ifndef pgm_read_byte +#define pgm_read_byte(addr) (*reinterpret_cast(addr)) +#endif + +// Time / scheduling primitives. Declared only; defined against TestClock later. +unsigned long millis(); +void delay(unsigned long ms); +void delayMicroseconds(unsigned int us); +void yield(); + +// Arduino String, provided by on the real platform. +#include "ArduinoString.h" + +#endif // TASMOTA_PUBSUB_TEST_ARDUINO_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/ArduinoString.cpp b/lib/default/TasmotaPubSub/tests/src/lib/ArduinoString.cpp new file mode 100644 index 000000000..d57d5f535 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/ArduinoString.cpp @@ -0,0 +1,22 @@ +/* + ArduinoString.cpp - Host-side minimal Arduino `String` shim implementation. +*/ + +#include "ArduinoString.h" + +String::String() : _str() {} + +String::String(const char* cstr) : _str(cstr ? cstr : "") {} + +String& String::operator=(const char* cstr) { + _str = cstr ? cstr : ""; + return *this; +} + +size_t String::length() const { + return _str.length(); +} + +const char* String::c_str() const { + return _str.c_str(); +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/ArduinoString.h b/lib/default/TasmotaPubSub/tests/src/lib/ArduinoString.h new file mode 100644 index 000000000..668aab9fc --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/ArduinoString.h @@ -0,0 +1,35 @@ +/* + ArduinoString.h - Host-side minimal Arduino `String` shim for TasmotaPubSub tests. + + The library only uses a tiny slice of the Arduino String API: + - `String domain;` (default construction) + - `this->domain = domain;` (assignment from const char*) + - `this->domain = "";` (assignment from string literal) + - `domain.length()` (is-empty test) + - `domain.c_str()` (passed to Client::connect) + This class backs those with std::string and handles nullptr safely. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_ARDUINO_STRING_H +#define TASMOTA_PUBSUB_TEST_ARDUINO_STRING_H + +#include +#include + +class String { +public: + String(); + String(const char* cstr); + String(const String& other) = default; + + String& operator=(const char* cstr); + String& operator=(const String& other) = default; + + size_t length() const; + const char* c_str() const; + +private: + std::string _str; +}; + +#endif // TASMOTA_PUBSUB_TEST_ARDUINO_STRING_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/CallbackContractAdapter.cpp b/lib/default/TasmotaPubSub/tests/src/lib/CallbackContractAdapter.cpp new file mode 100644 index 000000000..83aefa060 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/CallbackContractAdapter.cpp @@ -0,0 +1,52 @@ +/* + CallbackContractAdapter.cpp - Implementation of the Tasmota MqttDataHandler + contract reproduction (task 8.1). See CallbackContractAdapter.h for the + rationale behind writing the NUL terminator into the delivered buffer. +*/ + +#include "CallbackContractAdapter.h" + +#include + +CallbackContractAdapter::CallbackContractAdapter() + : _invocations(), _performNulWrite(true) {} + +void CallbackContractAdapter::onMessage(char* topic, uint8_t* payload, + unsigned int length) { + // Record the invocation first: copy the delivered payload bytes exactly as + // received, before any NUL-termination write mutates the buffer. The topic + // is a C-string the library has already NUL-terminated. + Invocation inv; + inv.topic = (topic != nullptr) ? std::string(topic) : std::string(); + inv.length = length; + if (payload != nullptr && length > 0) { + inv.payload.assign(payload, payload + length); + } + _invocations.push_back(std::move(inv)); + + // Reproduce the exact Tasmota driver contract: mqtt_data[data_len] = 0. + // `payload` points into the library's working buffer, so this write lands + // relative to the library allocation. For a well-formed packet the library + // reserves the spare byte at buffer[len], so the write is in-bounds; an + // exact-buffer packet that slipped past the guard would write one byte past + // the allocation and trip AddressSanitizer (F-01). + if (_performNulWrite && payload != nullptr) { + payload[length] = 0; + } +} + +std::function +CallbackContractAdapter::callback() { + return [this](char* topic, uint8_t* payload, unsigned int length) { + this->onMessage(topic, payload, length); + }; +} + +const CallbackContractAdapter::Invocation& CallbackContractAdapter::last() const { + assert(!_invocations.empty() && "last() called with no recorded invocations"); + return _invocations.back(); +} + +void CallbackContractAdapter::clear() { + _invocations.clear(); +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/CallbackContractAdapter.h b/lib/default/TasmotaPubSub/tests/src/lib/CallbackContractAdapter.h new file mode 100644 index 000000000..ce5a8e001 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/CallbackContractAdapter.h @@ -0,0 +1,90 @@ +/* + CallbackContractAdapter.h - Faithful host reproduction of the Tasmota + `MqttDataHandler` callback contract for the TasmotaPubSub test system + (task 8.1). + + Feature: tasmota-pubsub-tests + + When registered as the PubSubClient callback, on each inbound PUBLISH this + adapter reproduces exactly what the real Tasmota driver does in + `xdrv_02_9_mqtt.ino`: it writes a NUL byte at index `data_len` of the + delivered payload buffer (`mqtt_data[data_len] = 0`). The delivered payload + pointer points *into the library's own working buffer allocation*, so the NUL + write lands relative to that allocation: + + payload == buffer + (llen + 3 + tl) + length == len - llen - 3 - tl + &payload[length] == &buffer[len] + + For a well-behaved PUBLISH the library guarantees `len < bufferSize` (the F-01 + sentinel-byte guard rejects `total_packet >= bufferSize` and `idx >= + bufferSize`), so `buffer[len]` is the reserved spare byte and the NUL write is + in-bounds. If a packet ever slipped past the guard so that `len == bufferSize`, + the write would hit `buffer[bufferSize]` — one byte past the allocation — and + AddressSanitizer would trip. This is what makes the F-01 consumer-side + boundary bug observable (Requirement 15), and why the write must target the + delivered buffer rather than a private copy. + + In addition to the NUL write, the adapter copies the delivered payload into a + recorded buffer and captures `(topic, payloadCopy, length)` for every + invocation, so callback-dispatch baseline assertions (Requirements 11.1/11.2) + can check the correct topic, payload bytes, and length. + + The build is compiled with `-DESP32`, so `MQTT_CALLBACK_SIGNATURE` resolves to + `std::function`. `callback()` returns such + a std::function bound to this instance, suitable for + `PubSubClient::setCallback(adapter.callback())`. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_CALLBACK_CONTRACT_ADAPTER_H +#define TASMOTA_PUBSUB_TEST_CALLBACK_CONTRACT_ADAPTER_H + +#include +#include +#include +#include +#include + +class CallbackContractAdapter { +public: + // One recorded callback invocation. `payload` is a copy of the delivered + // bytes captured BEFORE the NUL-termination write, and `length` is the + // authoritative payload length passed by the library. + struct Invocation { + std::string topic; // NUL-terminated C-string from the library + std::vector payload; // copy of the delivered payload (length bytes) + unsigned int length = 0; + }; + + CallbackContractAdapter(); + + // The MqttDataHandler-contract callback. Records the invocation and then + // performs the `payload[length] = 0` NUL-termination write into the + // delivered buffer (unless the write has been disabled for a record-only + // test). Matches the MQTT_CALLBACK_SIGNATURE argument order/types. + void onMessage(char* topic, uint8_t* payload, unsigned int length); + + // Return a std::function bound to this instance, suitable for passing to + // PubSubClient::setCallback(). Because the build defines ESP32, the callback + // signature is std::function. + std::function callback(); + + // --- Recorded invocations --------------------------------------------- + const std::vector& invocations() const { return _invocations; } + std::size_t count() const { return _invocations.size(); } + const Invocation& last() const; // precondition: count() > 0 + void clear(); + + // Toggle the NUL-termination write. Enabled by default (the faithful + // contract). Disable to record deliveries without mutating the library + // buffer (useful when a test wants to inspect the payload independently of + // the boundary write). + void setPerformNulWrite(bool enable) { _performNulWrite = enable; } + bool performsNulWrite() const { return _performNulWrite; } + +private: + std::vector _invocations; + bool _performNulWrite; +}; + +#endif // TASMOTA_PUBSUB_TEST_CALLBACK_CONTRACT_ADAPTER_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/Client.h b/lib/default/TasmotaPubSub/tests/src/lib/Client.h new file mode 100644 index 000000000..61f09649d --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/Client.h @@ -0,0 +1,46 @@ +/* + Client.h - Host-side Arduino `Client` shim for TasmotaPubSub host tests. + + Abstract reproduction of the Arduino network `Client` interface the library + holds as `Client* _client`. It declares the full pure-virtual set the library + actually exercises. MockClient (later task) subclasses this exactly as a real + `WiFiClient` would be used. + + Inheritance mirrors Arduino: `Client : public Stream : public Print`, so the + two `write` overloads originate in `Print`. They are re-declared here as pure + virtual to express the complete transport contract the library relies on. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_CLIENT_H +#define TASMOTA_PUBSUB_TEST_CLIENT_H + +#include +#include + +#include "IPAddress.h" +#include "Stream.h" + +class Client : public Stream { +public: + virtual ~Client() = default; + + virtual int connect(IPAddress ip, uint16_t port) = 0; + virtual int connect(const char* host, uint16_t port) = 0; + + // Transport write side (declared in Print; the library calls both forms). + virtual size_t write(uint8_t) = 0; + virtual size_t write(const uint8_t* buf, size_t size) = 0; + + // Read side. + virtual int available() = 0; + virtual int read() = 0; + virtual int read(uint8_t* buf, size_t size) = 0; + virtual int peek() = 0; + + virtual void flush() = 0; + virtual void stop() = 0; + virtual uint8_t connected() = 0; + virtual operator bool() = 0; +}; + +#endif // TASMOTA_PUBSUB_TEST_CLIENT_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/FindingStatus.h b/lib/default/TasmotaPubSub/tests/src/lib/FindingStatus.h new file mode 100644 index 000000000..9324582e1 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/FindingStatus.h @@ -0,0 +1,63 @@ +// FindingStatus.h — Expected-fail registry for the TasmotaPubSub Hardening_Suite. +// +// Feature: tasmota-pubsub-tests +// +// This header maps each static-analysis finding (F-01..F-12, plus the two +// distinct aspects of F-03) to a single doctest test-case decorator. Hardening +// test cases reference a finding via FINDING_MARKER(Fxx), e.g.: +// +// TEST_CASE("F-05 partial write disables reuse" * FINDING_MARKER(F05)) { ... } +// +// A finding that is still OPEN (the library is NOT yet hardened for it) expands +// to `doctest::should_fail()`, so the run reports an *expected* failure and does +// NOT set a nonzero exit status for that failure alone (Requirement 4.6). If the +// case unexpectedly PASSES, doctest reports an unexpected pass and fails the run +// (Requirement 4.7) — the signal to flip the finding to hardened here. +// +// A finding that is already HARDENED (expected to pass) expands to a no-op +// decorator: `doctest::skip(false)` — a decorator that does NOT skip the case, +// so the test always runs and is expected to pass. We deliberately use +// `should_fail()` (never `may_fail()`, which would hide an unexpected pass). +// +// Flipping a finding from open to hardened is a one-line edit: change its +// FINDING_MARKER_* definition below from FINDING_OPEN to FINDING_HARDENED. +// +// The finding-status seeding below is the starting hypothesis derived from the +// design's finding-status table; task 12.1 confirms each empirically and +// finalizes these markers. + +#ifndef TASMOTA_PUBSUB_TESTS_FINDING_STATUS_H +#define TASMOTA_PUBSUB_TESTS_FINDING_STATUS_H + +#include "doctest.h" + +// Semantic aliases for the two decorator states. +// FINDING_OPEN -> expected-FAIL (library not yet hardened for this finding) +// FINDING_HARDENED -> expected-PASS (no-op decorator that always runs the case) +#define FINDING_OPEN doctest::should_fail() +#define FINDING_HARDENED doctest::skip(false) + +// Indirection so FINDING_MARKER(F05) resolves to the per-finding status macro. +#define FINDING_MARKER(id) FINDING_MARKER_##id + +// --------------------------------------------------------------------------- +// Finding status seeding (starting hypothesis; finalized empirically in 12.1). +// --------------------------------------------------------------------------- + +// Expected-FAIL (open findings). +#define FINDING_MARKER_F05 FINDING_OPEN // partial-write reuse +#define FINDING_MARKER_F10 FINDING_OPEN // SUBACK / session-present tracking +#define FINDING_MARKER_F11 FINDING_OPEN // graceful disconnect default +#define FINDING_MARKER_F03_DEADLINE FINDING_OPEN // F-03 packet-wide read deadline + +// Expected-PASS (already hardened). +#define FINDING_MARKER_F01 FINDING_HARDENED // exact-buffer inbound overflow +#define FINDING_MARKER_F02 FINDING_HARDENED // PUBLISH structural checks +#define FINDING_MARKER_F04 FINDING_HARDENED // streaming length 16-bit truncation +#define FINDING_MARKER_F06 FINDING_HARDENED // CONNACK validation +#define FINDING_MARKER_F07 FINDING_HARDENED // alloc-failure buffer state +#define FINDING_MARKER_F08 FINDING_HARDENED // SUBSCRIBE off-by-one + null checks +#define FINDING_MARKER_F09 FINDING_HARDENED // QoS / fixed-header validation +#define FINDING_MARKER_F03_PROMPT_CLOSE FINDING_HARDENED // F-03 oversized prompt-close + +#endif // TASMOTA_PUBSUB_TESTS_FINDING_STATUS_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/IPAddress.cpp b/lib/default/TasmotaPubSub/tests/src/lib/IPAddress.cpp new file mode 100644 index 000000000..f069e6b46 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/IPAddress.cpp @@ -0,0 +1,38 @@ +/* + IPAddress.cpp - Host-side Arduino `IPAddress` shim implementation. +*/ + +#include "IPAddress.h" + +IPAddress::IPAddress() { + _octets[0] = 0; + _octets[1] = 0; + _octets[2] = 0; + _octets[3] = 0; +} + +IPAddress::IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { + _octets[0] = first; + _octets[1] = second; + _octets[2] = third; + _octets[3] = fourth; +} + +uint8_t IPAddress::operator[](int index) const { + return _octets[index & 3]; +} + +uint8_t& IPAddress::operator[](int index) { + return _octets[index & 3]; +} + +bool IPAddress::operator==(const IPAddress& other) const { + return _octets[0] == other._octets[0] && + _octets[1] == other._octets[1] && + _octets[2] == other._octets[2] && + _octets[3] == other._octets[3]; +} + +bool IPAddress::operator!=(const IPAddress& other) const { + return !(*this == other); +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/IPAddress.h b/lib/default/TasmotaPubSub/tests/src/lib/IPAddress.h new file mode 100644 index 000000000..69874ea3a --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/IPAddress.h @@ -0,0 +1,31 @@ +/* + IPAddress.h - Host-side Arduino `IPAddress` shim for TasmotaPubSub host tests. + + Minimal reproduction of the Arduino `IPAddress` type. The library holds an + `IPAddress ip;` member, constructs one from 4 octets in setServer(uint8_t*), + copies/assigns it, and passes it to `Client::connect(IPAddress, uint16_t)`. + Equality is provided for completeness (used when comparing endpoints). +*/ + +#ifndef TASMOTA_PUBSUB_TEST_IPADDRESS_H +#define TASMOTA_PUBSUB_TEST_IPADDRESS_H + +#include + +class IPAddress { +public: + IPAddress(); + IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth); + + // Access an individual octet (0..3). Mirrors the Arduino operator[]. + uint8_t operator[](int index) const; + uint8_t& operator[](int index); + + bool operator==(const IPAddress& other) const; + bool operator!=(const IPAddress& other) const; + +private: + uint8_t _octets[4]; +}; + +#endif // TASMOTA_PUBSUB_TEST_IPADDRESS_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/MockClient.cpp b/lib/default/TasmotaPubSub/tests/src/lib/MockClient.cpp new file mode 100644 index 000000000..ebbe3db43 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/MockClient.cpp @@ -0,0 +1,239 @@ +/* + MockClient.cpp - Implementation of the host-side scriptable mock transport. + + Task 6.1 core: inbound queue, outbound record, connection state and close + tracking. Fault injection (setWriteLimit / setTrickle) is deliberately absent + here and is added in task 6.2 at the revealedCount() / write() choke points. +*/ + +#include "MockClient.h" + +#include "MqttPacket.h" +#include "TestClock.h" + +MockClient::MockClient() + : _inbound(), + _readPos(0), + _outbound(), + _writeLimit(0), + _trickleBytesPerReveal(0), + _trickleMsPerReveal(0), + _trickleBaseMs(0), + _connected(false), + _connectResult(1), + _connectCalled(false), + _lastHost(), + _lastIp(), + _lastPort(0), + _stopCalled(false), + _stopCount(0), + _flushCalled(false), + _flushCount(0) {} + +// --- Inbound scripting ----------------------------------------------------- + +void MockClient::pushInbound(const std::vector& bytes) { + _inbound.insert(_inbound.end(), bytes.begin(), bytes.end()); +} + +void MockClient::pushPacket(const MqttPacket& p) { + // Append the built packet's exact wire bytes to the inbound queue so the + // library reads them via read()/available() just like any scripted bytes. + pushInbound(p.bytes()); +} + +void MockClient::clearInbound() { + _inbound.clear(); + _readPos = 0; +} + +// --- Outbound record ------------------------------------------------------- + +const std::vector& MockClient::outbound() const { + return _outbound; +} + +void MockClient::clearOutbound() { + _outbound.clear(); +} + +// --- Fault injection ------------------------------------------------------- + +void MockClient::setWriteLimit(size_t maxPerWrite) { + _writeLimit = maxPerWrite; +} + +void MockClient::setTrickle(size_t bytesPerReveal, unsigned long msPerReveal) { + _trickleBytesPerReveal = bytesPerReveal; + _trickleMsPerReveal = msPerReveal; + // Anchor the reveal schedule at "now" so bytesPerReveal are visible + // immediately and additional bytes appear as virtual time advances. + _trickleBaseMs = TestClock::instance().millis(); +} + +// --- Connection control ---------------------------------------------------- + +void MockClient::setConnected(bool connected) { + _connected = connected; +} + +void MockClient::setConnectResult(int result) { + _connectResult = result; +} + +bool MockClient::stopCalled() const { + return _stopCalled; +} + +unsigned MockClient::stopCount() const { + return _stopCount; +} + +bool MockClient::flushCalled() const { + return _flushCalled; +} + +unsigned MockClient::flushCount() const { + return _flushCount; +} + +bool MockClient::connectCalled() const { + return _connectCalled; +} + +const std::string& MockClient::lastHost() const { + return _lastHost; +} + +IPAddress MockClient::lastIp() const { + return _lastIp; +} + +uint16_t MockClient::lastPort() const { + return _lastPort; +} + +// --- Client interface ------------------------------------------------------ + +int MockClient::connect(IPAddress ip, uint16_t port) { + _connectCalled = true; + _lastIp = ip; + _lastPort = port; + // Model a real client: a successful connect brings the socket up. Tests can + // still override the reported state with setConnected(). + if (_connectResult == 1) { + _connected = true; + } + return _connectResult; +} + +int MockClient::connect(const char* host, uint16_t port) { + _connectCalled = true; + _lastHost = (host != nullptr) ? host : ""; + _lastPort = port; + if (_connectResult == 1) { + _connected = true; + } + return _connectResult; +} + +size_t MockClient::write(uint8_t b) { + _outbound.push_back(b); + return 1; +} + +size_t MockClient::write(const uint8_t* buf, size_t size) { + if (buf == nullptr) { + return 0; + } + // Partial-write injection (task 6.2): when a write limit is configured, + // accept at most _writeLimit bytes of this call. Because _writeLimit >= 1 + // whenever it is set, a nonempty write always makes >= 1 byte of progress + // (matching WiFiClientSecure_light). Only the accepted prefix is recorded, + // so repeated writes continue advancing through the caller's buffer. A + // limit of 0 preserves the core "accept everything" behavior. + size_t accepted = size; + if (_writeLimit != 0 && accepted > _writeLimit) { + accepted = _writeLimit; + } + _outbound.insert(_outbound.end(), buf, buf + accepted); + return accepted; +} + +int MockClient::available() { + return static_cast(revealedCount()); +} + +int MockClient::read() { + if (revealedCount() == 0) { + return -1; + } + return static_cast(_inbound[_readPos++]); +} + +int MockClient::read(uint8_t* buf, size_t size) { + if (buf == nullptr) { + return 0; + } + size_t n = 0; + while (n < size && revealedCount() > 0) { + buf[n++] = _inbound[_readPos++]; + } + return static_cast(n); +} + +int MockClient::peek() { + if (revealedCount() == 0) { + return -1; + } + return static_cast(_inbound[_readPos]); +} + +void MockClient::flush() { + _flushCalled = true; + ++_flushCount; +} + +void MockClient::stop() { + _connected = false; + _stopCalled = true; + ++_stopCount; +} + +uint8_t MockClient::connected() { + return _connected ? 1 : 0; +} + +MockClient::operator bool() { + return _connected; +} + +// --- Internals ------------------------------------------------------------- + +size_t MockClient::revealedCount() const { + const size_t unconsumed = _inbound.size() - _readPos; + + // Core / trickle-disabled behavior: every unconsumed byte is visible. + // msPerReveal == 0 would make the schedule ill-defined, so it also means + // "reveal everything". + if (_trickleBytesPerReveal == 0 || _trickleMsPerReveal == 0) { + return unconsumed; + } + + // Trickle mode: bytesPerReveal are revealed immediately (step 1), and + // bytesPerReveal more become visible for every msPerReveal of virtual time + // elapsed since the schedule was configured. + const unsigned long now = TestClock::instance().millis(); + const unsigned long elapsed = (now >= _trickleBaseMs) ? (now - _trickleBaseMs) : 0UL; + const unsigned long long steps = 1ULL + (elapsed / _trickleMsPerReveal); + + // Absolute count of revealed bytes, saturated at the total scripted size. + const unsigned long long revealedAbsWide = + steps * static_cast(_trickleBytesPerReveal); + const size_t revealedAbs = (revealedAbsWide >= _inbound.size()) + ? _inbound.size() + : static_cast(revealedAbsWide); + + // Only bytes past the read cursor are still visible to the caller. + return (revealedAbs <= _readPos) ? 0 : (revealedAbs - _readPos); +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/MockClient.h b/lib/default/TasmotaPubSub/tests/src/lib/MockClient.h new file mode 100644 index 000000000..56f4a173e --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/MockClient.h @@ -0,0 +1,136 @@ +/* + MockClient.h - Host-side scriptable mock transport (Mock_Transport) for the + TasmotaPubSub host test system. + + MockClient subclasses the abstract Arduino shim `Client` so the *unmodified* + PubSubClient library drives it exactly as it would a real `WiFiClient`. It + provides: + - a scriptable inbound byte queue the library reads from, + - an outbound record capturing every byte the library writes, + - controllable connection state / connect() results and close tracking. + + This file implements the task 6.1 "core" only. Fault injection + (partial-write via setWriteLimit and slow/trickle delivery via setTrickle) + is added later in task 6.2; the read/available and write paths below are + intentionally structured with single choke points (revealedCount() and the + write helpers) so that behavior can be layered in without reshaping the core. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_MOCK_CLIENT_H +#define TASMOTA_PUBSUB_TEST_MOCK_CLIENT_H + +#include +#include +#include +#include + +#include "Client.h" +#include "IPAddress.h" + +// MqttPacket is only forward-declared here to keep this header light; the +// pushPacket() overload is implemented in MockClient.cpp, which includes the +// full MqttPacket definition (task 7.1). +class MqttPacket; + +class MockClient : public Client { +public: + MockClient(); + ~MockClient() override = default; + + // --- Inbound scripting ------------------------------------------------- + // Append raw bytes to the inbound queue the library reads via read(). + void pushInbound(const std::vector& bytes); + // Append a built MQTT packet's exact wire bytes to the inbound queue. + void pushPacket(const MqttPacket& p); + // Drop any not-yet-consumed inbound bytes and reset the read cursor. + void clearInbound(); + + // --- Outbound record --------------------------------------------------- + // Every byte the library writes is appended here for structural assertion. + const std::vector& outbound() const; + void clearOutbound(); + + // --- Fault injection (task 6.2) ---------------------------------------- + // Partial-write injection (Requirement 6.4). When maxPerWrite is nonzero, a + // single write(buf,size) accepts, records and returns at most maxPerWrite + // bytes (but always >= 1 for a successful write with size >= 1), modelling + // WiFiClientSecure_light's short writes. Only the accepted prefix is + // appended to the outbound record; repeated writes keep making progress. + // A limit of 0 means "no limit" (accept all) - the core behavior. Used by F-05. + void setWriteLimit(size_t maxPerWrite); + + // Trickle delivery (Requirement 6.5). When bytesPerReveal is nonzero, + // available() reveals at most bytesPerReveal bytes immediately and + // bytesPerReveal more for every msPerReveal of virtual time that elapses + // (measured against TestClock) since this schedule was configured. This + // models a slow broker and drives the F-03 per-byte-timeout vs + // packet-deadline distinction. A bytesPerReveal of 0 means "reveal + // everything" - the core behavior. + void setTrickle(size_t bytesPerReveal, unsigned long msPerReveal); + + // --- Connection control ------------------------------------------------ + // Force the reported connection state. + void setConnected(bool connected); + // Script the value returned by the next connect() call (default 1 = ok). + void setConnectResult(int result); + // True once stop() has been called at least once (F-03 / F-06 closes). + bool stopCalled() const; + // Introspection helpers for connect()/close/flush bookkeeping. + unsigned stopCount() const; + bool flushCalled() const; + unsigned flushCount() const; + bool connectCalled() const; + const std::string& lastHost() const; // set by connect(const char*, ...) + IPAddress lastIp() const; // set by connect(IPAddress, ...) + uint16_t lastPort() const; + + // --- Client interface (see Client.h) ----------------------------------- + int connect(IPAddress ip, uint16_t port) override; + int connect(const char* host, uint16_t port) override; + size_t write(uint8_t b) override; + size_t write(const uint8_t* buf, size_t size) override; + int available() override; + int read() override; + int read(uint8_t* buf, size_t size) override; + int peek() override; + void flush() override; + void stop() override; + uint8_t connected() override; + operator bool() override; + +private: + // Number of inbound bytes currently visible to the library. With no fault + // injection this is simply every unconsumed byte; task 6.2's trickle mode + // overrides this single choke point to reveal bytes gradually. + size_t revealedCount() const; + + // Inbound queue and read cursor. Consumed bytes stay in _inbound; _readPos + // marks the boundary so accessors remain cheap and index math is obvious. + std::vector _inbound; + size_t _readPos; + + // Outbound capture. + std::vector _outbound; + + // Fault injection (task 6.2). All zero == disabled == core behavior. + size_t _writeLimit; // max bytes accepted per write() (0 = unlimited) + size_t _trickleBytesPerReveal; // bytes revealed per step (0 = reveal all) + unsigned long _trickleMsPerReveal; // virtual ms between reveal steps + unsigned long _trickleBaseMs; // virtual time the trickle schedule started + + // Connection bookkeeping. + bool _connected; + int _connectResult; + bool _connectCalled; + std::string _lastHost; + IPAddress _lastIp; + uint16_t _lastPort; + + // Close / flush tracking. + bool _stopCalled; + unsigned _stopCount; + bool _flushCalled; + unsigned _flushCount; +}; + +#endif // TASMOTA_PUBSUB_TEST_MOCK_CLIENT_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/MockStream.cpp b/lib/default/TasmotaPubSub/tests/src/lib/MockStream.cpp new file mode 100644 index 000000000..c62fec823 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/MockStream.cpp @@ -0,0 +1,36 @@ +/* + MockStream.cpp - Implementation of the host-side `Stream` test double. + + Records every byte the library writes to the stream (stream-mode PUBLISH + payload routing) and provides an inert read side. +*/ + +#include "MockStream.h" + +size_t MockStream::write(uint8_t b) { + _written.push_back(b); + return 1; +} + +size_t MockStream::write(const uint8_t* buffer, size_t size) { + if (buffer == nullptr) { + return 0; + } + _written.insert(_written.end(), buffer, buffer + size); + return size; +} + +int MockStream::available() { + // The library never reads from this stream; report "nothing to read". + return 0; +} + +int MockStream::read() { + // Inert read side: no bytes available. + return -1; +} + +int MockStream::peek() { + // Inert read side: no bytes available. + return -1; +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/MockStream.h b/lib/default/TasmotaPubSub/tests/src/lib/MockStream.h new file mode 100644 index 000000000..2cb953a9b --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/MockStream.h @@ -0,0 +1,61 @@ +/* + MockStream.h - Host-side test double for the Arduino `Stream` used in + stream-mode inbound PUBLISH (Mock_Transport stream variant). + + This is NOT part of the library. It is a test support type used when + constructing `PubSubClient` with the optional `Stream&` argument. In stream + mode the *unmodified* library routes each inbound PUBLISH payload byte to the + attached stream via `stream->write(digit)` (inherited from Print) during + `readPacket()`. MockStream records those written bytes, in order, so tests can: + - assert stream-mode payload routing, and + - exercise the stream branch of the DoS/trickle logic (F-03 stream mode). + + The read side is intentionally inert (available()==0, read()==-1, peek()==-1): + the library never reads *from* this stream; it only writes routed payload + digits *to* it. Capturing those written digits is the whole job of this class. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_MOCK_STREAM_H +#define TASMOTA_PUBSUB_TEST_MOCK_STREAM_H + +#include +#include +#include + +#include "Stream.h" + +class MockStream : public Stream { +public: + MockStream() = default; + ~MockStream() override = default; + + // --- Print write side (what the library actually exercises) ------------- + // Record a single routed payload byte. + size_t write(uint8_t b) override; + + // Record a buffer of routed payload bytes in order. Overridden (rather than + // relying on Print's byte-at-a-time default) so bulk writes are captured + // directly and efficiently; behavior is observably identical. + size_t write(const uint8_t* buffer, size_t size) override; + + // Bring the non-virtual Print::write(const char*) helper into scope so it is + // not hidden by the overrides above. + using Stream::write; + + // --- Stream read side (inert; the library never reads from this) -------- + int available() override; + int read() override; + int peek() override; + + // --- Test accessors ----------------------------------------------------- + // Bytes the library has written to this stream, in the order written. + const std::vector& written() const { return _written; } + + // Discard all recorded bytes (for reuse across sub-scenarios). + void clear() { _written.clear(); } + +private: + std::vector _written; +}; + +#endif // TASMOTA_PUBSUB_TEST_MOCK_STREAM_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/MqttPacket.cpp b/lib/default/TasmotaPubSub/tests/src/lib/MqttPacket.cpp new file mode 100644 index 000000000..2a939a37d --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/MqttPacket.cpp @@ -0,0 +1,390 @@ +/* + MqttPacket.cpp - Implementation of the structural MQTT 3.1.1 control-packet + builder (task 7.1). + + Every builder produces the exact bytes a conformant peer would put on the + wire. The Remaining Length encoder mirrors PubSubClient::buildHeader so the + fixtures frame packets identically to the library under test. +*/ + +#include "MqttPacket.h" + +// --- Remaining Length codec (mirrors PubSubClient::buildHeader) ------------- + +std::vector MqttPacket::encodeRemainingLength(uint32_t length) { + std::vector out; + uint8_t llen = 0; + uint32_t len = length; + // Same loop shape as buildHeader: emit at least one byte, continuation bit + // set while more remains, bounded to 4 bytes so the encoding can never run + // past the MQTT maximum of four Remaining Length bytes. + do { + uint8_t digit = len & 127; // digit = len % 128 + len >>= 7; // len = len / 128 + if (len > 0) { + digit |= 0x80; // continuation bit + } + out.push_back(digit); + llen++; + } while (len > 0 && llen < 4); + return out; +} + +// --- Internal helpers ------------------------------------------------------- + +void MqttPacket::appendString(std::vector& out, const std::string& s) { + const uint16_t len = static_cast(s.size()); + out.push_back(static_cast(len >> 8)); + out.push_back(static_cast(len & 0xFF)); + out.insert(out.end(), s.begin(), s.end()); +} + +MqttPacket MqttPacket::framed(uint8_t fixedHeader, const std::vector& body) { + std::vector bytes; + bytes.push_back(fixedHeader); + const std::vector rl = encodeRemainingLength(static_cast(body.size())); + bytes.insert(bytes.end(), rl.begin(), rl.end()); + bytes.insert(bytes.end(), body.begin(), body.end()); + return MqttPacket(std::move(bytes)); +} + +// --- Builders --------------------------------------------------------------- + +MqttPacket MqttPacket::connack(uint8_t returnCode, bool sessionPresent) { + std::vector body; + body.push_back(sessionPresent ? 0x01 : 0x00); // connack acknowledge flags + body.push_back(returnCode); + return framed(MQTTCONNACK, body); +} + +MqttPacket MqttPacket::publish(const std::string& topic, + const std::vector& payload, + uint8_t qos, + bool retained, + uint16_t msgId) { + uint8_t header = static_cast(MQTTPUBLISH) + | static_cast((qos & 0x03) << 1) + | (retained ? 0x01 : 0x00); + + std::vector body; + appendString(body, topic); + if (qos > 0) { + // Packet identifier is present only for QoS 1 and QoS 2. + body.push_back(static_cast(msgId >> 8)); + body.push_back(static_cast(msgId & 0xFF)); + } + body.insert(body.end(), payload.begin(), payload.end()); + return framed(header, body); +} + +MqttPacket MqttPacket::publish(const std::string& topic, + const std::string& payload, + uint8_t qos, + bool retained, + uint16_t msgId) { + const std::vector bytes(payload.begin(), payload.end()); + return publish(topic, bytes, qos, retained, msgId); +} + +MqttPacket MqttPacket::puback(uint16_t msgId) { + std::vector body; + body.push_back(static_cast(msgId >> 8)); + body.push_back(static_cast(msgId & 0xFF)); + return framed(MQTTPUBACK, body); +} + +MqttPacket MqttPacket::suback(uint16_t msgId, uint8_t code) { + std::vector body; + body.push_back(static_cast(msgId >> 8)); + body.push_back(static_cast(msgId & 0xFF)); + body.push_back(code); + return framed(MQTTSUBACK, body); +} + +MqttPacket MqttPacket::unsuback(uint16_t msgId) { + std::vector body; + body.push_back(static_cast(msgId >> 8)); + body.push_back(static_cast(msgId & 0xFF)); + return framed(MQTTUNSUBACK, body); +} + +MqttPacket MqttPacket::pingreq() { + return framed(MQTTPINGREQ, {}); +} + +MqttPacket MqttPacket::pingresp() { + return framed(MQTTPINGRESP, {}); +} + +MqttPacket MqttPacket::disconnect() { + return framed(MQTTDISCONNECT, {}); +} + +MqttPacket MqttPacket::raw(const std::vector& bytes) { + return MqttPacket(bytes); +} + +// =========================================================================== +// MqttParser - structural decoder + validators (task 7.2) +// =========================================================================== + +bool MqttParser::decodeRemainingLength(const std::vector& bytes, + size_t offset, + uint32_t& value, + size_t& bytesConsumed) { + // Mirror of the wire codec: up to four continuation-flagged bytes, low 7 + // bits each, little-endian by septet. A fifth continuation byte is illegal. + uint32_t multiplier = 1; + uint32_t result = 0; + size_t i = 0; + for (; i < 4; ++i) { + if (offset + i >= bytes.size()) { + return false; // truncated Remaining Length field + } + const uint8_t digit = bytes[offset + i]; + result += static_cast(digit & 0x7F) * multiplier; + multiplier <<= 7; + if ((digit & 0x80) == 0) { + value = result; + bytesConsumed = i + 1; + return true; + } + } + return false; // continuation bit still set after four bytes +} + +DecodedPacket MqttParser::decode(const std::vector& bytes) { + DecodedPacket p; + if (bytes.empty()) { + return p; // valid == false + } + + p.type = static_cast(bytes[0] & 0xF0); + p.flags = static_cast(bytes[0] & 0x0F); + + uint32_t remaining = 0; + size_t rlBytes = 0; + if (!decodeRemainingLength(bytes, 1, remaining, rlBytes)) { + return p; // malformed Remaining Length field + } + p.remainingLength = remaining; + + const size_t bodyStart = 1 + rlBytes; + const size_t trailing = bytes.size() - bodyStart; + + // Framing is self-consistent only when the declared Remaining Length equals + // the number of trailing bytes actually present. + if (remaining != trailing) { + return p; // valid == false: truncated or over-long + } + + p.payload.assign(bytes.begin() + static_cast(bodyStart), bytes.end()); + p.valid = true; + return p; +} + +// Read a 2-byte big-endian length-prefixed string from `body` at `pos`. +// Advances `pos` past the string. Returns false if it would run past the body. +static bool readString(const std::vector& body, size_t& pos, std::string& out) { + if (pos + 2 > body.size()) { + return false; + } + const uint16_t len = static_cast((body[pos] << 8) | body[pos + 1]); + pos += 2; + if (pos + len > body.size()) { + return false; + } + out.assign(body.begin() + static_cast(pos), + body.begin() + static_cast(pos + len)); + pos += len; + return true; +} + +DecodedConnect MqttParser::decodeConnect(const std::vector& bytes) { + DecodedConnect c; + const DecodedPacket p = decode(bytes); + if (!p.valid || p.type != static_cast(MQTTCONNECT)) { + return c; + } + + const std::vector& body = p.payload; + size_t pos = 0; + + // Variable header: protocol name, level, connect flags, keep alive. + if (!readString(body, pos, c.protocolName)) { + return c; + } + if (pos + 4 > body.size()) { // level(1) + flags(1) + keepAlive(2) + return c; + } + c.protocolLevel = body[pos++]; + c.connectFlags = body[pos++]; + c.keepAlive = static_cast((body[pos] << 8) | body[pos + 1]); + pos += 2; + + c.cleanSession = (c.connectFlags & 0x02) != 0; + c.willFlag = (c.connectFlags & 0x04) != 0; + c.willQos = static_cast((c.connectFlags >> 3) & 0x03); + c.willRetain = (c.connectFlags & 0x20) != 0; + c.userFlag = (c.connectFlags & 0x80) != 0; + c.passwordFlag = (c.connectFlags & 0x40) != 0; + + // Payload: client id, [will topic, will message], [username], [password]. + if (!readString(body, pos, c.clientId)) { + return c; + } + if (c.willFlag) { + if (!readString(body, pos, c.willTopic) || !readString(body, pos, c.willMessage)) { + return c; + } + } + if (c.userFlag) { + if (!readString(body, pos, c.username)) { + return c; + } + if (c.passwordFlag && !readString(body, pos, c.password)) { + return c; + } + } + + c.valid = true; + return c; +} + +DecodedPublish MqttParser::decodePublish(const std::vector& bytes) { + DecodedPublish r; + const DecodedPacket p = decode(bytes); + if (!p.valid || p.type != static_cast(MQTTPUBLISH)) { + return r; + } + + r.dup = (p.flags & 0x08) != 0; + r.qos = static_cast((p.flags >> 1) & 0x03); + r.retain = (p.flags & 0x01) != 0; + + const std::vector& body = p.payload; + size_t pos = 0; + if (!readString(body, pos, r.topic)) { + return r; + } + if (r.qos > 0) { + // Packet identifier is present only for QoS 1 and QoS 2. + if (pos + 2 > body.size()) { + return r; + } + r.msgId = static_cast((body[pos] << 8) | body[pos + 1]); + pos += 2; + } + r.payload.assign(body.begin() + static_cast(pos), body.end()); + r.valid = true; + return r; +} + +DecodedSubscribe MqttParser::decodeSubscribe(const std::vector& bytes) { + DecodedSubscribe s; + const DecodedPacket p = decode(bytes); + if (!p.valid || p.type != static_cast(MQTTSUBSCRIBE)) { + return s; + } + + const std::vector& body = p.payload; + size_t pos = 0; + if (pos + 2 > body.size()) { + return s; + } + s.msgId = static_cast((body[pos] << 8) | body[pos + 1]); + pos += 2; + + // One or more (topic filter, requested QoS) pairs. + while (pos < body.size()) { + std::string filter; + if (!readString(body, pos, filter)) { + return s; // valid == false: truncated filter + } + if (pos >= body.size()) { + return s; // missing requested-QoS byte + } + s.filters.push_back(filter); + s.requestedQos.push_back(body[pos++]); + } + if (s.filters.empty()) { + return s; // a SUBSCRIBE must carry at least one filter + } + + s.valid = true; + return s; +} + +DecodedUnsubscribe MqttParser::decodeUnsubscribe(const std::vector& bytes) { + DecodedUnsubscribe u; + const DecodedPacket p = decode(bytes); + if (!p.valid || p.type != static_cast(MQTTUNSUBSCRIBE)) { + return u; + } + + const std::vector& body = p.payload; + size_t pos = 0; + if (pos + 2 > body.size()) { + return u; + } + u.msgId = static_cast((body[pos] << 8) | body[pos + 1]); + pos += 2; + + // One or more topic filters (no QoS byte in UNSUBSCRIBE). + while (pos < body.size()) { + std::string filter; + if (!readString(body, pos, filter)) { + return u; // valid == false: truncated filter + } + u.filters.push_back(filter); + } + if (u.filters.empty()) { + return u; // an UNSUBSCRIBE must carry at least one filter + } + + u.valid = true; + return u; +} + +bool MqttParser::isStructurallyValidPublish(const std::vector& bytes) { + if (bytes.empty()) { + return false; + } + // Fixed-header high nibble must be PUBLISH. + if (static_cast(bytes[0] & 0xF0) != static_cast(MQTTPUBLISH)) { + return false; + } + + uint32_t remaining = 0; + size_t rlBytes = 0; + if (!decodeRemainingLength(bytes, 1, remaining, rlBytes)) { + return false; + } + + // Decoded Remaining Length must equal the actual trailing byte count. + const size_t bodyStart = 1 + rlBytes; + if (bodyStart > bytes.size()) { + return false; + } + if (remaining != bytes.size() - bodyStart) { + return false; + } + + // The 2-byte topic length must fit within the body. + if (remaining < 2) { + return false; // no room for the topic length prefix + } + const uint16_t topicLen = + static_cast((bytes[bodyStart] << 8) | bytes[bodyStart + 1]); + + // QoS > 0 reserves an extra 2-byte packet identifier after the topic. + const uint8_t qos = static_cast((bytes[0] >> 1) & 0x03); + const size_t reserved = (qos > 0) ? 2u : 0u; + + // topic length prefix (2) + topic bytes + packet id (if any) must fit. + if (static_cast(2) + topicLen + reserved > remaining) { + return false; + } + + return true; +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/MqttPacket.h b/lib/default/TasmotaPubSub/tests/src/lib/MqttPacket.h new file mode 100644 index 000000000..381663f08 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/MqttPacket.h @@ -0,0 +1,217 @@ +/* + MqttPacket.h - Structural MQTT 3.1.1 control-packet builder for the + TasmotaPubSub host test system (task 7.1). + + Tests express intent structurally rather than hard-coding brittle magic-byte + arrays: a builder produces a complete, correctly framed control packet whose + bytes can be scripted as inbound (via MockClient::pushPacket / pushInbound) or + compared against recorded outbound bytes. + + The Remaining Length codec here MIRRORS the library's PubSubClient::buildHeader + exactly (1..4 length bytes, bounded so a value can never emit a fifth byte), so + fixtures stay in sync with the code under test. Packet-type constants are + REUSED from PubSubClient.h (MQTTPUBLISH, MQTTCONNACK, ...) rather than + redefined, so the fixtures automatically track any change in the library's own + definitions. + + The decoder / structural validators (MqttParser) are added in task 7.2 at + the bottom of this header. They let tests assert on recorded MockClient + outbound bytes by DECODING them structurally (round-trip against the public + API arguments), never by comparing brittle magic-byte arrays. Every assertion + therefore goes through decoded wire bytes only, so the suite stays durable + across a future MQTT 5 migration (Requirement 19.1). +*/ + +#ifndef TASMOTA_PUBSUB_TEST_MQTT_PACKET_H +#define TASMOTA_PUBSUB_TEST_MQTT_PACKET_H + +#include +#include +#include + +// Reuse the library's own packet-type / QoS constants so fixtures cannot drift +// out of sync with the code under test. This header pulls in the Arduino shim +// transitively (Arduino.h / Client.h / Stream.h / IPAddress.h), which is fine +// on the host include path (src/lib is searched first). +#include "PubSubClient.h" + +// A single, immutable MQTT control packet represented as its exact wire bytes. +// +// Instances are produced only through the static builder functions below; the +// value can then be handed to MockClient::pushPacket() (inbound scripting) or +// its bytes() compared against decoded outbound bytes. +class MqttPacket { +public: + // --- Builders ---------------------------------------------------------- + + // CONNACK: 0x20 0x02 . + // ackFlags bit0 is the Session Present flag; all other bits are zero. + static MqttPacket connack(uint8_t returnCode, bool sessionPresent = false); + + // PUBLISH with a raw byte payload. + // fixed header = MQTTPUBLISH | (qos << 1) | (retained ? 1 : 0) + // variable header = topic (2-byte length prefix + bytes) + // + packet identifier (2 bytes) ONLY when qos > 0 + // payload = raw bytes + // msgId is emitted only for qos > 0 (a QoS 0 PUBLISH has no packet id). + static MqttPacket publish(const std::string& topic, + const std::vector& payload, + uint8_t qos = 0, + bool retained = false, + uint16_t msgId = 0); + + // PUBLISH convenience overload taking a string payload. + static MqttPacket publish(const std::string& topic, + const std::string& payload, + uint8_t qos = 0, + bool retained = false, + uint16_t msgId = 0); + + // PUBACK: 0x40 0x02 . + static MqttPacket puback(uint16_t msgId); + + // SUBACK: 0x90 0x03 . + static MqttPacket suback(uint16_t msgId, uint8_t code); + + // UNSUBACK: 0xB0 0x02 . + static MqttPacket unsuback(uint16_t msgId); + + // PINGREQ: 0xC0 0x00. + static MqttPacket pingreq(); + + // PINGRESP: 0xD0 0x00. + static MqttPacket pingresp(); + + // DISCONNECT: 0xE0 0x00. + static MqttPacket disconnect(); + + // Arbitrary bytes verbatim, for adversarial / malformed fixtures + // (F-01/F-02/F-06/F-09 etc.). No framing is added or validated. + static MqttPacket raw(const std::vector& bytes); + + // --- Accessors --------------------------------------------------------- + + const std::vector& bytes() const { return _bytes; } + size_t size() const { return _bytes.size(); } + + // --- Remaining Length codec (mirrors PubSubClient::buildHeader) --------- + + // Encode an MQTT Remaining Length into 1..4 bytes. The loop is bounded to + // four bytes exactly as the library's buildHeader is, so a value outside the + // 0..268435455 range is clamped to a 4-byte encoding rather than emitting a + // fifth byte. + static std::vector encodeRemainingLength(uint32_t length); + +private: + explicit MqttPacket(std::vector bytes) : _bytes(std::move(bytes)) {} + + // Build a fixed header (packet type + Remaining Length) followed by the + // given variable-header-plus-payload body. + static MqttPacket framed(uint8_t fixedHeader, const std::vector& body); + + // Append a 2-byte big-endian length-prefixed UTF-8 string, exactly as the + // library's writeString lays it out on the wire. + static void appendString(std::vector& out, const std::string& s); + + std::vector _bytes; +}; + +// =========================================================================== +// MqttParser - structural decoder + validators (task 7.2) +// =========================================================================== + +// Output of the generic decoder. `type` is the fixed-header high nibble (e.g. +// MQTTPUBLISH), `flags` the low nibble (dup/qos/retain), `remainingLength` the +// decoded 1..4 byte value. Because splitting a variable header from the payload +// is packet-type specific, the generic decode places the ENTIRE decoded body +// (everything after the fixed header + Remaining Length field) into `payload` +// and leaves `variableHeader` empty; the type-specific decoders below carve out +// the individual fields. `valid` is true only when the framing is +// self-consistent: a Remaining Length that decodes within 1..4 bytes and whose +// value exactly equals the number of trailing bytes actually present. +struct DecodedPacket { + uint8_t type = 0; // high nibble, e.g. MQTTPUBLISH + uint8_t flags = 0; // low nibble (dup/qos/retain) + uint32_t remainingLength = 0; // decoded 1..4 byte value + std::vector variableHeader; + std::vector payload; + bool valid = false; // framing self-consistent +}; + +// Decoded CONNECT fields (MQTT 3.1.1 variable header + payload). +struct DecodedConnect { + bool valid = false; + std::string protocolName; // "MQTT" for 3.1.1 + uint8_t protocolLevel = 0; // 4 for 3.1.1 + uint8_t connectFlags = 0; + uint16_t keepAlive = 0; + bool cleanSession = false; + bool willFlag = false; + uint8_t willQos = 0; + bool willRetain = false; + bool userFlag = false; + bool passwordFlag = false; + std::string clientId; + std::string willTopic; + std::string willMessage; + std::string username; + std::string password; +}; + +// Decoded PUBLISH fields. +struct DecodedPublish { + bool valid = false; + bool dup = false; + uint8_t qos = 0; + bool retain = false; + std::string topic; + uint16_t msgId = 0; // 0 when qos == 0 (no packet identifier) + std::vector payload; +}; + +// Decoded SUBSCRIBE fields. `filters[i]` carries requested QoS `requestedQos[i]`. +struct DecodedSubscribe { + bool valid = false; + uint16_t msgId = 0; + std::vector filters; + std::vector requestedQos; +}; + +// Decoded UNSUBSCRIBE fields. +struct DecodedUnsubscribe { + bool valid = false; + uint16_t msgId = 0; + std::vector filters; +}; + +class MqttParser { +public: + // Decode a single MQTT control packet from the front of `bytes`. The + // returned DecodedPacket is `valid` only when the framing is + // self-consistent (see DecodedPacket). + static DecodedPacket decode(const std::vector& bytes); + + // Type-specific decoders. Each first runs the generic decode, checks the + // fixed-header nibble, then parses the body. The returned struct's `valid` + // flag is false if the bytes are not a well-formed packet of that type. + static DecodedConnect decodeConnect(const std::vector& bytes); + static DecodedPublish decodePublish(const std::vector& bytes); + static DecodedSubscribe decodeSubscribe(const std::vector& bytes); + static DecodedUnsubscribe decodeUnsubscribe(const std::vector& bytes); + + // Structural validator for PUBLISH: verifies the fixed-header high nibble is + // MQTTPUBLISH, that the decoded Remaining Length equals the actual trailing + // byte count, and that the 2-byte topic length fits within the body (leaving + // room for the packet identifier when QoS > 0). Rejects truncated packets. + static bool isStructurallyValidPublish(const std::vector& bytes); + + // Decode a Remaining Length starting at `bytes[offset]`. Returns true on + // success and sets `value` (decoded length) and `bytesConsumed` (1..4). A + // value needing more than four bytes, or a truncated field, returns false. + static bool decodeRemainingLength(const std::vector& bytes, + size_t offset, + uint32_t& value, + size_t& bytesConsumed); +}; + +#endif // TASMOTA_PUBSUB_TEST_MQTT_PACKET_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/Print.h b/lib/default/TasmotaPubSub/tests/src/lib/Print.h new file mode 100644 index 000000000..ed8b6ca89 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/Print.h @@ -0,0 +1,64 @@ +/* + Print.h - Host-side Arduino `Print` shim for TasmotaPubSub host tests. + + This is NOT a test double of the library. It is a minimal reproduction of the + Arduino `Print` base class so the *unmodified* PubSubClient library (which is + `class PubSubClient : public Print`) compiles and links against a host platform. + + Only the surface the library depends on is provided: + - `virtual size_t write(uint8_t)` (pure) and + `virtual size_t write(const uint8_t*, size_t)` (default Arduino behavior: + loop calling write(uint8_t)). The library overrides both. + A few non-virtual print/println helpers are provided for parity with the real + Arduino API; they are unused by the library but harmless. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_PRINT_H +#define TASMOTA_PUBSUB_TEST_PRINT_H + +#include +#include +#include + +class Print { +public: + virtual ~Print() = default; + + // Core Arduino Print virtuals. + virtual size_t write(uint8_t) = 0; + + // Standard Arduino default: write a buffer by looping over single-byte writes, + // stopping early if a write reports failure (0 bytes written). Derived classes + // (including the library under test) may override this. + virtual size_t write(const uint8_t* buffer, size_t size) { + size_t n = 0; + while (size--) { + if (write(*buffer++) == 0) { + break; + } + n++; + } + return n; + } + + // Convenience overload matching the Arduino API. Non-virtual and unused by the + // library, but provided so this header is a faithful stand-in. + size_t write(const char* str) { + if (str == nullptr) { + return 0; + } + return write(reinterpret_cast(str), std::strlen(str)); + } + + // Harmless print/println helpers (unused by the library). + size_t print(const char* str) { + return write(str); + } + size_t println(const char* str) { + size_t n = write(str); + n += write(reinterpret_cast("\r\n"), 2); + return n; + } +}; + +#endif // TASMOTA_PUBSUB_TEST_PRINT_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/Stream.cpp b/lib/default/TasmotaPubSub/tests/src/lib/Stream.cpp new file mode 100644 index 000000000..46dd9370d --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/Stream.cpp @@ -0,0 +1,12 @@ +/* + Stream.cpp - Host-side Arduino `Stream` shim implementation. + + Most of `Stream` is pure-virtual/inline; only the non-pure `flush()` needs an + out-of-line definition. Kept present because the design lists Stream.h/.cpp. +*/ + +#include "Stream.h" + +void Stream::flush() { + // No-op default, matching the harmless Arduino base behavior. +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/Stream.h b/lib/default/TasmotaPubSub/tests/src/lib/Stream.h new file mode 100644 index 000000000..9f0f30fed --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/Stream.h @@ -0,0 +1,31 @@ +/* + Stream.h - Host-side Arduino `Stream` shim for TasmotaPubSub host tests. + + Minimal reproduction of the Arduino `Stream` class: `Stream : public Print` + adding the read side (available/read/peek/flush). The library holds an optional + `Stream* stream;` and, in stream mode, calls `stream->write(digit)` (inherited + from Print). MockStream subclasses this to record routed payload bytes. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_STREAM_H +#define TASMOTA_PUBSUB_TEST_STREAM_H + +#include "Print.h" + +class Stream : public Print { +public: + virtual ~Stream() = default; + + // Read-side interface added by Arduino `Stream`. + virtual int available() = 0; + virtual int read() = 0; + virtual int peek() = 0; + virtual void flush(); + + // Bring Print's write overloads into scope so a derived class that only + // overrides write(uint8_t) does not hide write(const uint8_t*, size_t) + // (avoids -Woverloaded-virtual under -Wextra). + using Print::write; +}; + +#endif // TASMOTA_PUBSUB_TEST_STREAM_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/TestClock.cpp b/lib/default/TasmotaPubSub/tests/src/lib/TestClock.cpp new file mode 100644 index 000000000..be89050b9 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/TestClock.cpp @@ -0,0 +1,35 @@ +/* + TestClock.cpp - Host-side virtual clock (Clock_Injector) implementation. +*/ + +#include "TestClock.h" + +TestClock::TestClock() : _now(0), _autoStep(1) {} + +TestClock& TestClock::instance() { + static TestClock clock; + return clock; +} + +unsigned long TestClock::millis() const { + return _now; +} + +void TestClock::advance(unsigned long ms) { + _now += ms; +} + +void TestClock::reset(unsigned long start) { + _now = start; +} + +void TestClock::onDelay(unsigned long ms) { + // Always make forward progress: even delay(0) advances by at least autoStep + // so the library's millis()-based busy-wait loops terminate deterministically. + unsigned long step = (ms > _autoStep) ? ms : _autoStep; + _now += step; +} + +void TestClock::setAutoStep(unsigned long step) { + _autoStep = step; +} diff --git a/lib/default/TasmotaPubSub/tests/src/lib/TestClock.h b/lib/default/TasmotaPubSub/tests/src/lib/TestClock.h new file mode 100644 index 000000000..fc64232a7 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/TestClock.h @@ -0,0 +1,59 @@ +/* + TestClock.h - Host-side virtual clock (Clock_Injector) for TasmotaPubSub tests. + + Virtualizes simulated time so the *unmodified* PubSubClient library's + time-dependent behavior (keepalive, socket timeouts, connect/readByte + busy-wait loops) runs deterministically and instantly on a host, without + real elapsed wall-clock time. + + The Arduino shim wires its time primitives to this singleton (task 3.2): + - `millis()` returns `TestClock::instance().millis()` + - `delay(ms)` calls `TestClock::instance().onDelay(ms)` + - `yield()` is a no-op + + Why delay() must advance time: the library busy-waits in readByte() calling + `delay(1)` and in connect() calling `delay(0)` while polling the transport. + If delay() did not move the virtual clock, an exhausted inbound queue would + spin those loops forever on the host. onDelay() therefore always advances by + at least `autoStep` (default 1 ms) so even `delay(0)` makes forward progress + and the library's `millis()`-based timeouts fire deterministically. +*/ + +#ifndef TASMOTA_PUBSUB_TEST_TEST_CLOCK_H +#define TASMOTA_PUBSUB_TEST_TEST_CLOCK_H + +class TestClock { +public: + // Process-global virtual clock instance shared by the shim and the tests. + static TestClock& instance(); + + // Current virtual time in milliseconds (what the shim's millis() returns). + unsigned long millis() const; + + // Jump virtual time forward by `ms` milliseconds. + void advance(unsigned long ms); + + // Reset virtual time to `start` (Requirement 7.4). A doctest fixture calls + // this before each test for isolation. + void reset(unsigned long start = 0); + + // Applied by the shim's delay(ms): advances by max(ms, autoStep) so that a + // library busy-wait calling delay(0)/delay(1) always makes forward progress. + void onDelay(unsigned long ms); + + // Configure the minimum advance applied by onDelay(). Default is 1 ms so + // delay(0) still moves time forward. + void setAutoStep(unsigned long step); + +private: + TestClock(); + + // Non-copyable singleton. + TestClock(const TestClock&) = delete; + TestClock& operator=(const TestClock&) = delete; + + unsigned long _now; + unsigned long _autoStep; +}; + +#endif // TASMOTA_PUBSUB_TEST_TEST_CLOCK_H diff --git a/lib/default/TasmotaPubSub/tests/src/lib/doctest.h b/lib/default/TasmotaPubSub/tests/src/lib/doctest.h new file mode 100644 index 000000000..70ed24050 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/lib/doctest.h @@ -0,0 +1,9119 @@ +// ============================================================= +// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED! == +// ============================================================= +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2023 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying file LICENSE.txt or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= +// +// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt +// +// The concept of subcases (sections in Catch) and expression decomposition are from there. +// Some parts of the code are taken directly: +// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> +// - the Approx() helper class for floating point comparison +// - colors in the console +// - breaking into a debugger +// - signal / SEH handling +// - timer +// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) +// +// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= + +#ifndef DOCTEST_LIBRARY_INCLUDED +#define DOCTEST_LIBRARY_INCLUDED + +// ================================================================================================= +// == VERSION ====================================================================================== +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_VERSION +#define DOCTEST_PARTS_PUBLIC_VERSION + +// NOLINTBEGIN(cppcoreguidelines-macro-to-enum, modernize-macro-to-enum) +#define DOCTEST_VERSION_MAJOR 2 +#define DOCTEST_VERSION_MINOR 5 +#define DOCTEST_VERSION_PATCH 0 +// NOLINTEND(cppcoreguidelines-macro-to-enum, modernize-macro-to-enum) + +// util we need here +#define DOCTEST_TOSTR_IMPL(x) #x +#define DOCTEST_TOSTR(x) DOCTEST_TOSTR_IMPL(x) + +// clang-format off +#define DOCTEST_VERSION_STR \ + DOCTEST_TOSTR(DOCTEST_VERSION_MAJOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_MINOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_PATCH) +// clang-format on + +#define DOCTEST_VERSION (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) + +#endif // DOCTEST_PARTS_PUBLIC_VERSION +// ================================================================================================= +// == COMPILER VERSION ============================================================================= +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_COMPILER +#define DOCTEST_PARTS_PUBLIC_COMPILER + +// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect + +#ifdef _MSC_VER +#define DOCTEST_CPLUSPLUS _MSVC_LANG +#else +#define DOCTEST_CPLUSPLUS __cplusplus +#endif + +#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR) * 10000000 + (MINOR) * 100000 + (PATCH)) + +// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... +#if defined(_MSC_VER) && defined(_MSC_FULL_VER) +#if _MSC_VER == _MSC_FULL_VER / 10000 +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) +#else // MSVC +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) +#endif // MSVC +#endif // MSVC +#if defined(__clang__) && defined(__clang_minor__) && defined(__clang_patchlevel__) +#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && !defined(__INTEL_COMPILER) +#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#endif // GCC +#if defined(__INTEL_COMPILER) +#define DOCTEST_ICC DOCTEST_COMPILER(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif // ICC + +#ifndef DOCTEST_MSVC +#define DOCTEST_MSVC 0 +#endif // DOCTEST_MSVC +#ifndef DOCTEST_CLANG +#define DOCTEST_CLANG 0 +#endif // DOCTEST_CLANG +#ifndef DOCTEST_GCC +#define DOCTEST_GCC 0 +#endif // DOCTEST_GCC +#ifndef DOCTEST_ICC +#define DOCTEST_ICC 0 +#endif // DOCTEST_ICC + +#endif // DOCTEST_PARTS_PUBLIC_COMPILER +// ================================================================================================= +// == COMPILER WARNINGS HELPERS ==================================================================== +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_WARNINGS +#define DOCTEST_PARTS_PUBLIC_WARNINGS + + +#if DOCTEST_CLANG && !DOCTEST_ICC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) +#else // DOCTEST_CLANG +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_CLANG + +#if DOCTEST_GCC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") +#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) +#else // DOCTEST_GCC +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH +#define DOCTEST_GCC_SUPPRESS_WARNING(w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_GCC + +#if DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) +#else // DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_MSVC + +// ================================================================================================= +// == COMPILER WARNINGS ============================================================================ +// ================================================================================================= + +// both the header and the implementation suppress all of these, +// so it only makes sense to aggregate them like so +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-warning-option") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunsafe-buffer-usage") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-macros") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + /* these 4 also disabled globally via cmake: */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4514) /* unreferenced inline function has been removed */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4571) /* SEH related */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4710) /* function not inlined */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4711) /* function selected for inline expansion*/ \ + /* common ones */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4616) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4619) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4996) /* The compiler encountered a deprecated declaration */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4706) /* assignment within conditional expression */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4512) /* 'class' : assignment operator could not be generated */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4127) /* conditional expression is constant */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4640) /* construction of local static object not thread-safe */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5264) /* 'variable-name': 'const' variable is not used */ \ + /* static analysis */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26439) /* Function may not throw. Declare it 'noexcept' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26495) /* Always initialize a member variable */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26451) /* Arithmetic overflow ... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26444) /* Avoid unnamed objects with custom ctor and dtor... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26812) /* Prefer 'enum class' over 'enum' */ + +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#define DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH \ + DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ + +#define DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#define DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH \ + DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-function") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wnonportable-system-include-path") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wnrvo") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnrvo") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING(4267) /* conversion from 'x' to 'y', possible loss of data */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4530) /* exception handler, but unwind semantics not enabled */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4577) /* 'noexcept' with no exception handling mode specified */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string in argument is not a string literal */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4800) /* forcing value to bool (performance warning) */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5245) /* unreferenced function with internal linkage removed */ + +#define DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + DOCTEST_MSVC_SUPPRESS_WARNING(4548) /* before comma no effect; expected side - effect */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4265) /* virtual functions, but destructor is not virtual */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4986) /* exception specification does not match previous */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4350) /* 'member1' called instead of 'member2' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4668) /* not defined as a preprocessor macro */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string not a string literal */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5105) /* macro producing 'defined' has undefined behavior */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4738) /* storing float result in memory, loss of performance */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5262) /* implicit fall-through */ + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_PARTS_PUBLIC_WARNINGS + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +// ================================================================================================= +// == FEATURE DETECTION ============================================================================ +// ================================================================================================= + +#ifndef DOCTEST_PARTS_PUBLIC_CONFIG +#define DOCTEST_PARTS_PUBLIC_CONFIG + +#ifndef DOCTEST_PARTS_PUBLIC_PLATFORM +#define DOCTEST_PARTS_PUBLIC_PLATFORM + +#if defined(__APPLE__) +// Apple detection taken from Catch2 codebase +// For information: +// https://github.com/swiftlang/swift-corelibs-foundation/blob/release/5.10/CoreFoundation/Base.subproj/SwiftRuntime/TargetConditionals.h +#include +#if (defined(TARGET_OS_MAC) && TARGET_OS_MAC == 1) || (defined(TARGET_OS_OSX) && TARGET_OS_OSX == 1) +#define DOCTEST_PLATFORM_MAC + +#elif defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1 +#define DOCTEST_PLATFORM_IPHONE +#endif + +#elif defined(WIN32) || defined(_WIN32) +#define DOCTEST_PLATFORM_WINDOWS + +#elif defined(__wasi__) +#define DOCTEST_PLATFORM_WASI + +#else // defined(linux) || defined(__linux) // defined(__linux__) +#define DOCTEST_PLATFORM_LINUX +#endif // DOCTEST_PLATFORM + +#endif // DOCTEST_PARTS_PUBLIC_PLATFORM + +// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support +// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx +// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html +// MSVC version table: +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +// MSVC++ 14.3 (17) _MSC_VER == 1930 (Visual Studio 2022) +// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) +// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) +// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) +// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) +// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) +// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) +// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) +// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) + +// Universal Windows Platform support +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_WINDOWS_SEH +#endif // WINAPI_FAMILY +#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#define DOCTEST_CONFIG_WINDOWS_SEH +#endif // MSVC +#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) +#undef DOCTEST_CONFIG_WINDOWS_SEH +#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH + +#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(__EMSCRIPTEN__) && \ + !defined(__wasi__) +#define DOCTEST_CONFIG_POSIX_SIGNALS +#endif // _WIN32 +#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) +#undef DOCTEST_CONFIG_POSIX_SIGNALS +#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) || defined(__wasi__) +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // no exceptions +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef __wasi__ +#define DOCTEST_CONFIG_NO_MULTITHREADING +#endif + +#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) +#define DOCTEST_CONFIG_IMPLEMENT +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#if defined(_WIN32) || defined(__CYGWIN__) +#if DOCTEST_MSVC +#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) +#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) +#else // MSVC +#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) +#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) +#endif // MSVC +#else // _WIN32 +#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define DOCTEST_SYMBOL_IMPORT +#endif // _WIN32 + +#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#ifdef DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT +#else // DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT +#endif // DOCTEST_CONFIG_IMPLEMENT +#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#define DOCTEST_INTERFACE +#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL + +// needed for extern template instantiations +// see https://github.com/fmtlib/fmt/issues/2228 +#if DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL +#define DOCTEST_INTERFACE_DEF DOCTEST_INTERFACE +#else // DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL DOCTEST_INTERFACE +#define DOCTEST_INTERFACE_DEF +#endif // DOCTEST_MSVC + +#define DOCTEST_EMPTY + +#if DOCTEST_MSVC +#define DOCTEST_NOINLINE __declspec(noinline) +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#elif DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 5, 0) +#define DOCTEST_NOINLINE +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#else +#define DOCTEST_NOINLINE __attribute__((noinline)) +#define DOCTEST_UNUSED __attribute__((unused)) +#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) +#endif + +#ifdef DOCTEST_CONFIG_NO_CONTRADICTING_INLINE +#define DOCTEST_INLINE_NOINLINE inline +#else +#define DOCTEST_INLINE_NOINLINE inline DOCTEST_NOINLINE +#endif + +#ifndef DOCTEST_NORETURN +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NORETURN +#else // DOCTEST_MSVC +#define DOCTEST_NORETURN [[noreturn]] +#endif // DOCTEST_MSVC +#endif // DOCTEST_NORETURN + +#ifndef DOCTEST_NOEXCEPT +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NOEXCEPT +#else // DOCTEST_MSVC +#define DOCTEST_NOEXCEPT noexcept +#endif // DOCTEST_MSVC +#endif // DOCTEST_NOEXCEPT + +#ifndef DOCTEST_CONSTEXPR +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_CONSTEXPR const +#define DOCTEST_CONSTEXPR_FUNC inline +#else // DOCTEST_MSVC +#define DOCTEST_CONSTEXPR constexpr +#define DOCTEST_CONSTEXPR_FUNC constexpr +#endif // DOCTEST_MSVC +#endif // DOCTEST_CONSTEXPR + +#ifndef DOCTEST_NO_SANITIZE_INTEGER +#if DOCTEST_CLANG >= DOCTEST_COMPILER(3, 7, 0) +#define DOCTEST_NO_SANITIZE_INTEGER __attribute__((no_sanitize("integer"))) +#else +#define DOCTEST_NO_SANITIZE_INTEGER +#endif +#endif // DOCTEST_NO_SANITIZE_INTEGER + +// this is kept here for backwards compatibility since the config option was changed +#ifdef DOCTEST_CONFIG_USE_IOSFWD +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // DOCTEST_CONFIG_USE_IOSFWD + +// for clang - always include or (which drags some std stuff) +// because we want to check if we are using libc++ with the _LIBCPP_VERSION macro in +// which case we don't want to forward declare stuff from std - for reference: +// https://github.com/doctest/doctest/issues/126 +// https://github.com/doctest/doctest/issues/356 +#if DOCTEST_CLANG +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN +#if DOCTEST_CPLUSPLUS >= 201703L && __has_include() +#include +#else +#include +#endif +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END +#endif // clang + +#ifdef _LIBCPP_VERSION +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // _LIBCPP_VERSION + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +#if defined(__has_builtin) +#define DOCTEST_HAS_BUILTIN(x) __has_builtin(x) +#else +#define DOCTEST_HAS_BUILTIN(x) 0 +#endif // __has_builtin + +#endif // DOCTEST_PARTS_PUBLIC_CONFIG + +// ================================================================================================= +// == FEATURE DETECTION END ======================================================================== +// ================================================================================================= +#ifndef DOCTEST_PARTS_PUBLIC_UTILITY +#define DOCTEST_PARTS_PUBLIC_UTILITY + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#define DOCTEST_DECLARE_INTERFACE(name) \ + virtual ~name(); \ + name() = default; \ + name(const name &) = delete; \ + name(name &&) = delete; \ + name &operator=(const name &) = delete; \ + name &operator=(name &&) = delete; + +#define DOCTEST_DEFINE_INTERFACE(name) name::~name() = default; + +#if !defined(DOCTEST_COUNTER) +#if DOCTEST_CLANG >= DOCTEST_COMPILER(22, 0, 0) +#define DOCTEST_COUNTER __LINE__ +#elif defined(__COUNTER__) +#define DOCTEST_COUNTER __COUNTER__ +#else +#define DOCTEST_COUNTER __LINE__ +#endif +#endif // defined(DOCTEST_COUNTER) + +// internal macros for string concatenation and anonymous variable name generation +#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 +#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, DOCTEST_COUNTER) + +#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x & +#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x +#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE + +namespace doctest { +namespace detail { +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-function") +static DOCTEST_CONSTEXPR int consume(const int *, int) noexcept { + return 0; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +} // namespace detail +} // namespace doctest + +#define DOCTEST_GLOBAL_NO_WARNINGS(var, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ + static const int var = doctest::detail::consume(&var, __VA_ARGS__); \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_UTILITY +#ifndef DOCTEST_PARTS_PUBLIC_DEBUGGER +#define DOCTEST_PARTS_PUBLIC_DEBUGGER + + +#ifndef DOCTEST_BREAK_INTO_DEBUGGER + +// should probably take a look at https://github.com/scottt/debugbreak +#if DOCTEST_CLANG && DOCTEST_HAS_BUILTIN(__builtin_debugtrap) +#define DOCTEST_BREAK_INTO_DEBUGGER() __builtin_debugtrap() + +#elif defined(DOCTEST_PLATFORM_LINUX) +#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64)) +// Break at the location of the failing check if possible +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" ::) // NOLINT(hicpp-no-assembler) +#else +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN +#include +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END +#define DOCTEST_BREAK_INTO_DEBUGGER() raise(SIGTRAP) +#endif + +#elif defined(DOCTEST_PLATFORM_MAC) +#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64__) || defined(__i386) +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" ::) // NOLINT(hicpp-no-assembler) +#elif defined(__ppc__) || defined(__ppc64__) +// https://www.cocoawithlove.com/2008/03/break-into-debugger.html +#define DOCTEST_BREAK_INTO_DEBUGGER() /* NOLINTNEXTLINE(hicpp-no-assembler) */ \ + __asm__("li r0, 20\nsc\nnop\nli r0, 37\nli r4, 2\nsc\nnop\n" ::: "memory", "r0", "r3", "r4") +#else +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("brk #0"); // NOLINT(hicpp-no-assembler) +#endif + +#elif DOCTEST_MSVC +#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() + +#elif defined(__MINGW32__) +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") +extern "C" __declspec(dllimport) void __stdcall DebugBreak(); +DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() +#else // linux +#define DOCTEST_BREAK_INTO_DEBUGGER() (static_cast(0)) +#endif // linux +#endif // DOCTEST_BREAK_INTO_DEBUGGER + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +DOCTEST_INTERFACE bool isDebuggerActive(); +} // namespace detail +} // namespace doctest + +#endif + +#endif // DOCTEST_PARTS_PUBLIC_DEBUGGER +#ifndef DOCTEST_PARTS_PUBLIC_STD_FWD +#define DOCTEST_PARTS_PUBLIC_STD_FWD + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN +#include +#include +#include +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END +#else // DOCTEST_CONFIG_USE_STD_HEADERS + +// Forward declaring 'X' in namespace std is not permitted by the C++ Standard. +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643) + +// NOLINTBEGIN(bugprone-std-namespace-modification, cert-dcl58-cpp) +namespace std { +typedef decltype(nullptr) nullptr_t; // NOLINT(modernize-use-using) +typedef decltype(sizeof(void *)) size_t; // NOLINT(modernize-use-using) +template +struct char_traits; +template <> +struct char_traits; +template +class basic_ostream; // NOLINT(fuchsia-virtual-inheritance) +typedef basic_ostream> ostream; // NOLINT(modernize-use-using) +template +// NOLINTNEXTLINE +basic_ostream &operator<<(basic_ostream &, const char *); +template +class basic_istream; +typedef basic_istream> istream; // NOLINT(modernize-use-using) +template +class tuple; +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +template +class allocator; +template +class basic_string; +using string = basic_string, allocator>; +#endif // VS 2019 +} // namespace std +// NOLINTEND(bugprone-std-namespace-modification, cert-dcl58-cpp) + +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +namespace doctest { +using std::size_t; +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STD_FWD +#ifndef DOCTEST_PARTS_PUBLIC_STD_TYPE_TRAITS +#define DOCTEST_PARTS_PUBLIC_STD_TYPE_TRAITS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN +#include +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + +namespace doctest { +namespace detail { +namespace types { + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +using namespace std; +#else +template +struct enable_if {}; + +template +struct enable_if { + using type = T; +}; + +struct true_type { + static DOCTEST_CONSTEXPR bool value = true; +}; + +struct false_type { + static DOCTEST_CONSTEXPR bool value = false; +}; + +template +struct remove_reference { + using type = T; +}; + +template +struct remove_reference { + using type = T; +}; + +template +struct remove_reference { + using type = T; +}; + +template +struct is_same : false_type {}; + +template +struct is_same : true_type {}; + +template +struct is_rvalue_reference : false_type {}; + +template +struct is_rvalue_reference : true_type {}; + +template +struct remove_const { + using type = T; +}; + +template +struct remove_const { + using type = T; +}; + +// Compiler intrinsics +template +struct is_enum { + static DOCTEST_CONSTEXPR bool value = __is_enum(T); +}; + +template +struct underlying_type { + using type = __underlying_type(T); +}; + +template +struct is_pointer : false_type {}; + +template +struct is_pointer : true_type {}; + +template +struct is_array : false_type {}; +// NOLINTNEXTLINE(*-avoid-c-arrays) +template +struct is_array : true_type {}; +#endif + +#if !(defined(DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS) && (DOCTEST_CPLUSPLUS >= 201703L)) +template +using void_t = void; +#endif + +} // namespace types +} // namespace detail +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STD_TYPE_TRAITS +#ifndef DOCTEST_PARTS_PUBLIC_STD_UTILITY +#define DOCTEST_PARTS_PUBLIC_STD_UTILITY + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +// +template +T &&declval(); + +template +DOCTEST_CONSTEXPR_FUNC T &&forward(typename types::remove_reference::type &t) DOCTEST_NOEXCEPT { + return static_cast(t); +} + +template +// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) +DOCTEST_CONSTEXPR_FUNC T &&forward(typename types::remove_reference::type &&t) DOCTEST_NOEXCEPT { + return static_cast(t); +} + +template +struct deferred_false : types::false_type {}; + +} // namespace detail +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STD_UTILITY +#ifndef DOCTEST_PARTS_PUBLIC_STRING +#define DOCTEST_PARTS_PUBLIC_STRING + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +#ifndef DOCTEST_CONFIG_STRING_SIZE_TYPE +#define DOCTEST_CONFIG_STRING_SIZE_TYPE unsigned +#endif + +namespace detail { + +template +struct is_std_string : types::false_type {}; + +template +struct is_std_string< + T, + typename types::enable_if< + types::is_same().c_str()), const char *>::value && + types::is_same().size()), size_t>::value>::type> : types::true_type {}; + +} // namespace detail + +// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings +// with length of up to 23 chars on the stack before going on the heap - +// the last byte of the buffer is used for: +// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) +// - if small - capacity left before going on the heap - using the lowest 5 bits +// - if small - 2 bits are left unused - the second and third highest ones +// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) +// and the "is small" bit remains "0" ("as well as the capacity left") so its OK +// Idea taken from this lecture about the string implementation of facebook/folly - fbstring +// https://www.youtube.com/watch?v=kPR8h4-qZdk +// TODO: +// - optimizations - like not deleting memory unnecessarily in operator= and etc. +// - resize/reserve/clear +// - replace +// - back/front +// - iterator stuff +// - find & friends +// - push_back/pop_back +// - assign/insert/erase +// - relational operators as free functions - taking const char* as one of the params +class DOCTEST_INTERFACE String { +public: + using size_type = DOCTEST_CONFIG_STRING_SIZE_TYPE; + +private: + static DOCTEST_CONSTEXPR size_type len = 24; + static DOCTEST_CONSTEXPR size_type last = len - 1; + + struct view // len should be more than sizeof(view) - because of the final byte for flags + { + char *ptr; + size_type size; + size_type capacity; + }; + + union { + char buf[len]; // NOLINT(*-avoid-c-arrays) + view data; + }; + + char *allocate(size_type sz); + + bool isOnStack() const noexcept { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-union-access) + return (buf[last] & 128) == 0; + } + + void setOnHeap() noexcept; + void setLast(size_type in = last) noexcept; + void setSize(size_type sz) noexcept; + + void copy(const String &other); + +public: + static DOCTEST_CONSTEXPR size_type npos = static_cast(-1); + + String() noexcept; + ~String(); + + String(const char *in); + String(const char *in, size_type in_size); + + String(std::istream &in, size_type in_size); + + template ::value, bool>::type = true> + String(const T &in) + : String(in.c_str(), static_cast(in.size())) {} + + String(const String &other); + String &operator=(const String &other); + + String &operator+=(const String &other); + + String(String &&other) noexcept; + String &operator=(String &&other) noexcept; + + char operator[](size_type i) const; + char &operator[](size_type i); + + // the only functions I'm willing to leave in the interface - available for inlining + const char *c_str() const { + return const_cast(this)->c_str(); // NOLINT + } + + // NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) + char *c_str() { + if (isOnStack()) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(buf); + } + return data.ptr; + } + // NOLINTEND(cppcoreguidelines-pro-type-union-access) + + size_type size() const; + size_type capacity() const; + + String substr(size_type pos, size_type cnt = npos) &&; + String substr(size_type pos, size_type cnt = npos) const &; + + size_type find(char ch, size_type pos = 0) const; + size_type rfind(char ch, size_type pos = npos) const; + + int compare(const char *other, bool no_case = false) const; + int compare(const String &other, bool no_case = false) const; + + friend DOCTEST_INTERFACE std::ostream &operator<<(std::ostream &s, const String &in); +}; + +DOCTEST_INTERFACE String operator+(const String &lhs, const String &rhs); + +DOCTEST_INTERFACE bool operator==(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator!=(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator<(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator>(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator<=(const String &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator>=(const String &lhs, const String &rhs); + +namespace detail { + +// MSVS 2015 :( +#if !DOCTEST_CLANG && defined(_MSC_VER) && _MSC_VER <= 1900 +template +struct has_global_insertion_operator : types::false_type {}; + +template +struct has_global_insertion_operator(), declval()), void())> + : types::true_type {}; + +template +struct has_insertion_operator { + static DOCTEST_CONSTEXPR bool value = has_global_insertion_operator::value; +}; + +template +struct insert_hack; + +template +struct insert_hack { + static void insert(std::ostream &os, const T &t) { + ::operator<<(os, t); + } +}; + +template +struct insert_hack { + static void insert(std::ostream &os, const T &t) { + operator<<(os, t); + } +}; + +template +using insert_hack_t = insert_hack::value>; +#else +template +struct has_insertion_operator : types::false_type {}; +#endif + +template +struct has_insertion_operator(), declval()), void())> + : types::true_type {}; + +template +struct should_stringify_as_underlying_type { + static DOCTEST_CONSTEXPR bool value = + detail::types::is_enum::value && !doctest::detail::has_insertion_operator::value; +}; + +template +struct is_pair : types::false_type {}; + +template +struct is_pair< + T, + types::void_t< + typename T::first_type, + typename T::second_type, + decltype(declval().first), + decltype(declval().second)>> : types::true_type {}; + +template +struct is_container : types::false_type {}; + +template +struct is_container< + T, + types::void_t< + typename T::value_type, + typename T::iterator, + decltype(declval().begin()), + decltype(declval().end())>> : types::true_type {}; + +DOCTEST_INTERFACE std::ostream *tlssPush(); +DOCTEST_INTERFACE String tlssPop(); + +template +struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T)) { +#ifdef DOCTEST_CONFIG_REQUIRE_STRINGIFICATION_FOR_ALL_USED_TYPES + static_assert(deferred_false::value, "No stringification detected for type T. See string conversion manual"); +#endif + return "{?}"; + } +}; + +template +struct filldata; + +template +void filloss(std::ostream *stream, const T &in) { + filldata::fill(stream, in); +} + +template +void filloss(std::ostream *stream, const T (&in)[N]) { // NOLINT(*-avoid-c-arrays) + // T[N], T(&)[N], T(&&)[N] have same behaviour. + // Hence remove reference. + filloss::type>(stream, in); +} + +template +String toStream(const T &in) { + std::ostream *stream = tlssPush(); + filloss(stream, in); + return tlssPop(); +} + +template <> +struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T) in) { + return toStream(in); + } +}; + +} // namespace detail + +template +struct StringMaker + : public detail::StringMakerBase< + detail::has_insertion_operator::value || detail::types::is_pointer::value || + detail::types::is_array::value || detail::is_pair::value || detail::is_container::value> {}; + +#ifndef DOCTEST_STRINGIFY +#ifdef DOCTEST_CONFIG_DOUBLE_STRINGIFY +#define DOCTEST_STRINGIFY(...) toString(toString(__VA_ARGS__)) +#else +#define DOCTEST_STRINGIFY(...) toString(__VA_ARGS__) +#endif +#endif + +template +String toString() { +#if DOCTEST_CLANG == 0 && DOCTEST_GCC == 0 && DOCTEST_ICC == 0 + String ret = __FUNCSIG__; // class doctest::String __cdecl doctest::toString(void) + String::size_type beginPos = ret.find('<'); + return ret.substr(beginPos + 1, ret.size() - beginPos - static_cast(sizeof(">(void)"))); +#else + const String ret = __PRETTY_FUNCTION__; // doctest::String toString() [with T = TYPE] + const String::size_type begin = ret.find('=') + 2; + return ret.substr(begin, ret.size() - begin - 1); +#endif // Compiler +} + +template < + typename T, + typename detail::types::enable_if::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + return StringMaker::convert(value); +} + +// NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) +inline String &&toString(String &&in) { + return static_cast(in); +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(const char *in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +DOCTEST_INTERFACE String toString(const std::string &in); +#endif // VS 2019 + +DOCTEST_INTERFACE String toString(const String &in); + +DOCTEST_INTERFACE String toString(std::nullptr_t); + +DOCTEST_INTERFACE String toString(bool in); + +DOCTEST_INTERFACE String toString(float in); +DOCTEST_INTERFACE String toString(double in); +DOCTEST_INTERFACE String toString(double long in); + +DOCTEST_INTERFACE String toString(char in); +DOCTEST_INTERFACE String toString(char signed in); +DOCTEST_INTERFACE String toString(char unsigned in); +DOCTEST_INTERFACE String toString(short in); +DOCTEST_INTERFACE String toString(short unsigned in); +DOCTEST_INTERFACE String toString(signed in); +DOCTEST_INTERFACE String toString(unsigned in); +DOCTEST_INTERFACE String toString(long in); +DOCTEST_INTERFACE String toString(long unsigned in); +DOCTEST_INTERFACE String toString(long long in); +DOCTEST_INTERFACE String toString(long long unsigned in); + +template < + typename T, + typename detail::types::enable_if::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + using UT = typename detail::types::underlying_type::type; + return (DOCTEST_STRINGIFY(static_cast(value))); +} + +namespace detail { +template +struct filldata { + static void fill(std::ostream *stream, const T &in) { +#if defined(_MSC_VER) && _MSC_VER <= 1900 + insert_hack_t::insert(*stream, in); +#else + operator<<(*stream, in); +#endif // _MSV_VER + } +}; + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) +// NOLINTBEGIN(*-avoid-c-arrays) +template +struct filldata { + static void fill(std::ostream *stream, const T (&in)[N]) { + *stream << "["; + for (size_t i = 0; i < N; i++) { + if (i != 0) { + *stream << ", "; + } + *stream << (DOCTEST_STRINGIFY(in[i])); + } + *stream << "]"; + } +}; +// NOLINTEND(*-avoid-c-arrays) +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +// Specialized since we don't want the terminating null byte! +// NOLINTBEGIN(*-avoid-c-arrays) +template +struct filldata { + static void fill(std::ostream *stream, const char (&in)[N]) { + *stream << String(in, in[N - 1] ? N : N - 1); + } // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +}; +// NOLINTEND(*-avoid-c-arrays) + +template <> +struct filldata { + DOCTEST_INTERFACE static void fill(std::ostream *stream, const void *in); +}; + +template <> +struct filldata { + DOCTEST_INTERFACE static void fill(std::ostream *stream, const volatile void *in); +}; + +template +struct filldata { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4180) + static void fill(std::ostream *stream, const T *in) { + DOCTEST_MSVC_SUPPRESS_WARNING_POP + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wmicrosoft-cast") + filldata::fill( + stream, +#if DOCTEST_GCC == 0 || DOCTEST_GCC >= DOCTEST_COMPILER(4, 9, 0) + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(in) +#else + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + *reinterpret_cast(&in) +#endif // DOCTEST_GCC + ); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + } +}; + +template +struct filldata< + T, + typename detail::types::enable_if::value && detail::is_pair::value>::type> { + static void fill(std::ostream *stream, const T &in) { + *stream << "{" << DOCTEST_STRINGIFY(in.first) << ", " << DOCTEST_STRINGIFY(in.second) << "}"; + } +}; + +template +struct filldata< + T, + typename detail::types::enable_if< + !detail::has_insertion_operator::value && detail::is_container::value>::type> { + static void fill(std::ostream *stream, const DOCTEST_REF_WRAP(T) in) { + *stream << "{"; + for (auto it = in.begin(); it != in.end(); ++it) { + if (it != in.begin()) { + *stream << ", "; + } + *stream << DOCTEST_STRINGIFY(*it); + } + *stream << "}"; + } +}; + +#ifndef DOCTEST_CONFIG_DISABLE +template +String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char *op, const DOCTEST_REF_WRAP(R) rhs) { + return (DOCTEST_STRINGIFY(lhs)) + op + (DOCTEST_STRINGIFY(rhs)); +} +#endif // DOCTEST_CONFIG_DISABLE +} // namespace detail + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_STRING +#ifndef DOCTEST_PARTS_PUBLIC_MATCHERS_CONTAINS +#define DOCTEST_PARTS_PUBLIC_MATCHERS_CONTAINS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +class DOCTEST_INTERFACE Contains { +public: + explicit Contains(const String &string); + + bool checkWith(const String &other) const; + + String string; +}; + +DOCTEST_INTERFACE String toString(const Contains &in); + +DOCTEST_INTERFACE bool operator==(const String &lhs, const Contains &rhs); +DOCTEST_INTERFACE bool operator==(const Contains &lhs, const String &rhs); +DOCTEST_INTERFACE bool operator!=(const String &lhs, const Contains &rhs); +DOCTEST_INTERFACE bool operator!=(const Contains &lhs, const String &rhs); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MATCHERS_CONTAINS +#ifndef DOCTEST_PARTS_PUBLIC_MATCHERS_APPROX +#define DOCTEST_PARTS_PUBLIC_MATCHERS_APPROX + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE Approx { + Approx(double value); + + Approx operator()(double value) const; + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + explicit Approx( + const T &value, + typename detail::types::enable_if::value>::type * = static_cast(nullptr) + ) { + *this = static_cast(value); + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx &epsilon(double newEpsilon); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx &>::type epsilon(const T &newEpsilon) { + m_epsilon = static_cast(newEpsilon); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx &scale(double newScale); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx &>::type scale(const T &newScale) { + m_scale = static_cast(newScale); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator==(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator!=(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator<=(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator>=(const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator< (const Approx &lhs, double rhs); + DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx &rhs); + DOCTEST_INTERFACE friend bool operator> (const Approx &lhs, double rhs); + // clang-format on + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_APPROX_PREFIX \ + template \ + friend typename std::enable_if::value, bool>::type + + // clang-format off + DOCTEST_APPROX_PREFIX operator==(const T &lhs, const Approx &rhs) { return operator==(static_cast(lhs), rhs); } + DOCTEST_APPROX_PREFIX operator==(const Approx &lhs, const T &rhs) { return operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator!=(const T &lhs, const Approx &rhs) { return !operator==(lhs, rhs); } + DOCTEST_APPROX_PREFIX operator!=(const Approx &lhs, const T &rhs) { return !operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator<=(const T &lhs, const Approx &rhs) { return static_cast(lhs) < rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator<=(const Approx &lhs, const T &rhs) { return lhs.m_value < static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const T &lhs, const Approx &rhs) { return static_cast(lhs) > rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const Approx &lhs, const T &rhs) { return lhs.m_value > static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator< (const T &lhs, const Approx &rhs) { return static_cast(lhs) < rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator< (const Approx &lhs, const T &rhs) { return lhs.m_value < static_cast(rhs) && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const T &lhs, const Approx &rhs) { return static_cast(lhs) > rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const Approx &lhs, const T &rhs) { return lhs.m_value > static_cast(rhs) && lhs != rhs; } + // clang-format off +#undef DOCTEST_APPROX_PREFIX +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + double m_epsilon; + double m_scale; + double m_value; +}; + +DOCTEST_INTERFACE String toString(const Approx &in); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MATCHERS_APPROX +#ifndef DOCTEST_PARTS_PUBLIC_MATCHERS_IS_NAN +#define DOCTEST_PARTS_PUBLIC_MATCHERS_IS_NAN + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +template +struct DOCTEST_INTERFACE_DECL IsNaN { + F value; + bool flipped; + IsNaN(F f, bool flip = false) + : value(f), flipped(flip) {} + + IsNaN operator!() const { + return {value, !flipped}; + } + + operator bool() const; +}; + +#ifndef __MINGW32__ +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +#endif + +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MATCHERS_IS_NAN +#ifndef DOCTEST_PARTS_PUBLIC_CONTEXT_OPTIONS +#define DOCTEST_PARTS_PUBLIC_CONTEXT_OPTIONS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace detail { +struct DOCTEST_INTERFACE TestCase; +} // namespace detail + +struct ContextOptions { + std::ostream *cout = nullptr; // stdout stream + String binary_name; // the test binary name + + const detail::TestCase *currentTest = nullptr; + + // == parameters from the command line + String out; // output filename + String order_by; // how tests should be ordered + unsigned rand_seed; // the seed for rand ordering + + unsigned first; // the first (matching) test to be executed + unsigned last; // the last (matching) test to be executed + + int abort_after; // stop tests after this many failed assertions + int subcase_filter_levels; // apply the subcase filters for the first N levels + + bool success; // include successful assertions in output + bool case_sensitive; // if filtering should be case sensitive + bool exit; // if the program should be exited after the tests are ran/whatever + bool duration; // print the time duration of each test case + bool minimal; // minimal console output (only test failures) + bool quiet; // no console output + bool no_throw; // to skip exceptions-related assertion macros + bool no_exitcode; // if the framework should return 0 as the exitcode + bool no_run; // to not run the tests at all (can be done with an "*" exclude) + bool no_intro; // to not print the intro of the framework + bool no_version; // to not print the version of the framework + bool no_colors; // if output to the console should be colorized + bool force_colors; // forces the use of colors even when a tty cannot be detected + bool no_breaks; // to not break into the debugger + bool no_skip; // don't skip test cases which are marked to be skipped + bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): + bool no_path_in_filenames; // if the path to files should be removed from the output + String strip_file_prefixes; // remove the longest matching one of these prefixes from any file paths in the output + bool no_line_numbers; // if source code line numbers should be omitted from the output + bool no_debug_output; // no output in the debug console when a debugger is attached + bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! + bool no_time_in_output; // omit any time/timestamps from output !!! UNDOCUMENTED !!! + + bool help; // to print the help + bool version; // to print the version + bool count; // if only the count of matching tests is to be retrieved + bool list_test_cases; // to list all tests matching the filters + bool list_test_suites; // to list all suites matching the filters + bool list_reporters; // lists all registered reporters +}; + +DOCTEST_INTERFACE const ContextOptions *getContextOptions(); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_CONTEXT_OPTIONS +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_TYPE +#define DOCTEST_PARTS_PUBLIC_ASSERT_TYPE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace assertType { +enum Enum { + // macro traits + + is_warn = 1, + is_check = 2 * is_warn, + is_require = 2 * is_check, + + is_normal = 2 * is_require, + is_throws = 2 * is_normal, + is_throws_as = 2 * is_throws, + is_throws_with = 2 * is_throws_as, + is_nothrow = 2 * is_throws_with, + + is_false = 2 * is_nothrow, + is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types + + is_eq = 2 * is_unary, + is_ne = 2 * is_eq, + + is_lt = 2 * is_ne, + is_gt = 2 * is_lt, + + is_ge = 2 * is_gt, + is_le = 2 * is_ge, + + // macro types + + DT_WARN = is_normal | is_warn, + DT_CHECK = is_normal | is_check, + DT_REQUIRE = is_normal | is_require, + + DT_WARN_FALSE = is_normal | is_false | is_warn, + DT_CHECK_FALSE = is_normal | is_false | is_check, + DT_REQUIRE_FALSE = is_normal | is_false | is_require, + + DT_WARN_THROWS = is_throws | is_warn, + DT_CHECK_THROWS = is_throws | is_check, + DT_REQUIRE_THROWS = is_throws | is_require, + + DT_WARN_THROWS_AS = is_throws_as | is_warn, + DT_CHECK_THROWS_AS = is_throws_as | is_check, + DT_REQUIRE_THROWS_AS = is_throws_as | is_require, + + DT_WARN_THROWS_WITH = is_throws_with | is_warn, + DT_CHECK_THROWS_WITH = is_throws_with | is_check, + DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, + + DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, + DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, + DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, + + DT_WARN_NOTHROW = is_nothrow | is_warn, + DT_CHECK_NOTHROW = is_nothrow | is_check, + DT_REQUIRE_NOTHROW = is_nothrow | is_require, + + DT_WARN_EQ = is_normal | is_eq | is_warn, + DT_CHECK_EQ = is_normal | is_eq | is_check, + DT_REQUIRE_EQ = is_normal | is_eq | is_require, + + DT_WARN_NE = is_normal | is_ne | is_warn, + DT_CHECK_NE = is_normal | is_ne | is_check, + DT_REQUIRE_NE = is_normal | is_ne | is_require, + + DT_WARN_GT = is_normal | is_gt | is_warn, + DT_CHECK_GT = is_normal | is_gt | is_check, + DT_REQUIRE_GT = is_normal | is_gt | is_require, + + DT_WARN_LT = is_normal | is_lt | is_warn, + DT_CHECK_LT = is_normal | is_lt | is_check, + DT_REQUIRE_LT = is_normal | is_lt | is_require, + + DT_WARN_GE = is_normal | is_ge | is_warn, + DT_CHECK_GE = is_normal | is_ge | is_check, + DT_REQUIRE_GE = is_normal | is_ge | is_require, + + DT_WARN_LE = is_normal | is_le | is_warn, + DT_CHECK_LE = is_normal | is_le | is_check, + DT_REQUIRE_LE = is_normal | is_le | is_require, + + DT_WARN_UNARY = is_normal | is_unary | is_warn, + DT_CHECK_UNARY = is_normal | is_unary | is_check, + DT_REQUIRE_UNARY = is_normal | is_unary | is_require, + + DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, + DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, + DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, +}; +} // namespace assertType + +DOCTEST_INTERFACE const char *assertString(assertType::Enum at); +DOCTEST_INTERFACE const char *failureString(assertType::Enum at); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_TYPE +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_DATA +#define DOCTEST_PARTS_PUBLIC_ASSERT_DATA + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE TestCaseData; + +struct DOCTEST_INTERFACE AssertData { + // common - for all asserts + const TestCaseData *m_test_case; + assertType::Enum m_at; + const char *m_file; + int m_line; + const char *m_expr; + bool m_failed; + + // exception-related - for all asserts + bool m_threw; + String m_exception; + + // for normal asserts + String m_decomp; + + // for specific exception-related asserts + bool m_threw_as; + const char *m_exception_type; + + class DOCTEST_INTERFACE StringContains { + private: + Contains content; + bool isContains; + + public: + StringContains(const String &str) + : content(str), isContains(false) {} + + StringContains(Contains cntn) + : content(static_cast(cntn)), isContains(true) {} + + bool check(const String &str) { + return isContains ? (content == str) : (content.string == str); + } + + operator const String &() const { + return content.string; + } + + const char *c_str() const { + return content.string.c_str(); + } + } m_exception_string; + + AssertData( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const StringContains &exception_string + ); +}; + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_DATA +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_COMPARATOR +#define DOCTEST_PARTS_PUBLIC_ASSERT_COMPARATOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +// clang-format off +template struct decay_array { using type = T; }; +template struct decay_array { using type = T *; }; +template struct decay_array { using type = T *; }; + +template struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 1; }; +template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; +template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; + +template struct can_use_op : public not_char_pointer::type> {}; +// clang-format on +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE bool +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +// clang-format off +#define DOCTEST_COMPARISON_RETURN_TYPE typename types::enable_if::value || can_use_op::value, bool>::type +inline bool eq(const char *lhs, const char *rhs) { return String(lhs) == String(rhs); } +inline bool ne(const char *lhs, const char *rhs) { return String(lhs) != String(rhs); } +inline bool lt(const char *lhs, const char *rhs) { return String(lhs) < String(rhs); } +inline bool gt(const char *lhs, const char *rhs) { return String(lhs) > String(rhs); } +inline bool le(const char *lhs, const char *rhs) { return String(lhs) <= String(rhs); } +inline bool ge(const char *lhs, const char *rhs) { return String(lhs) >= String(rhs); } +// clang-format on +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + +DOCTEST_RELATIONAL_OP(eq, ==) +DOCTEST_RELATIONAL_OP(ne, !=) +DOCTEST_RELATIONAL_OP(lt, <) +DOCTEST_RELATIONAL_OP(gt, >) +DOCTEST_RELATIONAL_OP(le, <=) +DOCTEST_RELATIONAL_OP(ge, >=) + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) l == r +#define DOCTEST_CMP_NE(l, r) l != r +#define DOCTEST_CMP_GT(l, r) l > r +#define DOCTEST_CMP_LT(l, r) l < r +#define DOCTEST_CMP_GE(l, r) l >= r +#define DOCTEST_CMP_LE(l, r) l <= r +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) eq(l, r) +#define DOCTEST_CMP_NE(l, r) ne(l, r) +#define DOCTEST_CMP_GT(l, r) gt(l, r) +#define DOCTEST_CMP_LT(l, r) lt(l, r) +#define DOCTEST_CMP_GE(l, r) ge(l, r) +#define DOCTEST_CMP_LE(l, r) le(l, r) +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +namespace binaryAssertComparison { +enum Enum { eq = 0, ne, gt, lt, ge, le }; +} // namespace binaryAssertComparison + +// clang-format off +template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; + +#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; +// clang-format on + +DOCTEST_BINARY_RELATIONAL_OP(0, doctest::detail::eq) +DOCTEST_BINARY_RELATIONAL_OP(1, doctest::detail::ne) +DOCTEST_BINARY_RELATIONAL_OP(2, doctest::detail::gt) +DOCTEST_BINARY_RELATIONAL_OP(3, doctest::detail::lt) +DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge) +DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le) + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_COMPARATOR +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_RESULT +#define DOCTEST_PARTS_PUBLIC_ASSERT_RESULT + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// more checks could be added - like in Catch: +// https://github.com/catchorg/Catch2/pull/1480/files +// https://github.com/catchorg/Catch2/pull/1481/files +#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ + template \ + rt &operator op(const R &) { \ + static_assert(deferred_false::value, "Expression Too Complex Please Rewrite As Binary Comparison!"); \ + return *this; \ + } + +struct DOCTEST_INTERFACE Result { // NOLINT(*-member-init) + bool m_passed; + String m_decomp; + + Result() = default; // TODO: Why do we need this? (To remove NOLINT) + Result(bool passed, const String &decomposition = String()); + + // forbidding some expressions based on this table: + // https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Result, &) + DOCTEST_FORBIT_EXPRESSION(Result, ^) + DOCTEST_FORBIT_EXPRESSION(Result, |) + DOCTEST_FORBIT_EXPRESSION(Result, &&) + DOCTEST_FORBIT_EXPRESSION(Result, ||) + DOCTEST_FORBIT_EXPRESSION(Result, ==) + DOCTEST_FORBIT_EXPRESSION(Result, !=) + DOCTEST_FORBIT_EXPRESSION(Result, <) + DOCTEST_FORBIT_EXPRESSION(Result, >) + DOCTEST_FORBIT_EXPRESSION(Result, <=) + DOCTEST_FORBIT_EXPRESSION(Result, >=) + DOCTEST_FORBIT_EXPRESSION(Result, =) + DOCTEST_FORBIT_EXPRESSION(Result, +=) + DOCTEST_FORBIT_EXPRESSION(Result, -=) + DOCTEST_FORBIT_EXPRESSION(Result, *=) + DOCTEST_FORBIT_EXPRESSION(Result, /=) + DOCTEST_FORBIT_EXPRESSION(Result, %=) + DOCTEST_FORBIT_EXPRESSION(Result, <<=) + DOCTEST_FORBIT_EXPRESSION(Result, >>=) + DOCTEST_FORBIT_EXPRESSION(Result, &=) + DOCTEST_FORBIT_EXPRESSION(Result, ^=) + DOCTEST_FORBIT_EXPRESSION(Result, |=) +}; + +struct DOCTEST_INTERFACE ResultBuilder : public AssertData { + ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type = "", + const String &exception_string = "" + ); + + ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const Contains &exception_string + ); + + void setResult(const Result &res); + + template + DOCTEST_NOINLINE bool binary_assert(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { + m_failed = !RelationalComparator()(lhs, rhs); + if (m_failed || getContextOptions()->success) { + m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); + } + return !m_failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(const DOCTEST_REF_WRAP(L) val) { + m_failed = !val; + + if (m_at & assertType::is_false) { + m_failed = !m_failed; + } + + if (m_failed || getContextOptions()->success) { + m_decomp = (DOCTEST_STRINGIFY(val)); + } + + return !m_failed; + } + + void translateException(); + + bool log(); + void react() const; +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_RESULT +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_EXPRESSION +#define DOCTEST_PARTS_PUBLIC_ASSERT_EXPRESSION + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-comparison") +#endif + +// This will check if there is any way it could find a operator like member or friend and uses it. +// If not it doesn't find the operator or if the operator at global scope is defined after +// this template, the template won't be instantiated due to SFINAE. Once the template is not +// instantiated it can look for global operator using normal conversions. +#ifdef __NVCC__ +#define SFINAE_OP(ret, op) ret +#else +#define SFINAE_OP(ret, op) decltype((void)(doctest::detail::declval() op doctest::detail::declval()), ret{}) +#endif + +#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ + /* NOLINTBEGIN(cppcoreguidelines-missing-std-forward) */ \ + template \ + DOCTEST_NOINLINE SFINAE_OP(Result, op) operator op(R &&rhs) { \ + bool res = op_macro(doctest::detail::forward(lhs), doctest::detail::forward(rhs)); \ + if (m_at & assertType::is_false) \ + res = !res; \ + if (!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } \ + /* NOLINTEND(cppcoreguidelines-missing-std-forward) */ + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") +// DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") +// DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") +// DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") +// DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") +// DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") +// DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +// https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 +DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch +// DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in +// operation + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +template +struct Expression_lhs { + L lhs; + assertType::Enum m_at; + + // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) + explicit Expression_lhs(L &&in, assertType::Enum at) + : lhs(static_cast(in)), m_at(at) {} + + DOCTEST_NOINLINE operator Result() { + // this is needed only for MSVC 2015 + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4800) // 'int': forcing value to bool + bool res = static_cast(lhs); // NOLINT(bugprone-non-zero-enum-to-bool-conversion) + DOCTEST_MSVC_SUPPRESS_WARNING_POP + if (m_at & assertType::is_false) { + res = !res; + } + + if (!res || getContextOptions()->success) { + return {res, (DOCTEST_STRINGIFY(lhs))}; + } + return {res}; + } + + /* This is required for user-defined conversions from Expression_lhs to L */ + operator L() const { + return lhs; + } + + // clang-format off + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) + // clang-format on + + // forbidding some expressions based on this table: + // https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) + // these 2 are unfortunate because they should be allowed - they have higher precedence over the + // comparisons, but the ExpressionDecomposer class uses the left shift operator to capture the + // left operand of the binary expression... + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) +}; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_POP +#endif + +struct DOCTEST_INTERFACE ExpressionDecomposer { + assertType::Enum m_at; + + ExpressionDecomposer(assertType::Enum at); + + // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator + // precedence table) but then there will be warnings from GCC about "-Wparentheses" and since + // "_Pragma()" is problematic this will stay for now... + // https://github.com/catchorg/Catch2/issues/870 + // https://github.com/catchorg/Catch2/issues/565 + template + Expression_lhs + operator<<(const L &&operand) { // bitfields bind to universal ref but not const rvalue ref + return Expression_lhs(static_cast(operand), m_at); + } + + template < + typename L, + typename types::enable_if::value, void>::type * = nullptr> + Expression_lhs operator<<(const L &operand) { + return Expression_lhs(operand, m_at); + } +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_EXPRESSION +#ifndef DOCTEST_PARTS_PUBLIC_COLOR +#define DOCTEST_PARTS_PUBLIC_COLOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace Color { +enum Enum { // NOLINT(cert-int09-c, readability-enum-initial-value) + None = 0, + White, + Red, + Green, + Blue, + Cyan, + Yellow, + Grey, + + Bright = 0x10, + + BrightRed = Bright | Red, + BrightGreen = Bright | Green, + LightGrey = Bright | Grey, + BrightWhite = Bright | White +}; + +DOCTEST_INTERFACE std::ostream &operator<<(std::ostream &s, Color::Enum code); +} // namespace Color +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_COLOR +#ifndef DOCTEST_PARTS_PUBLIC_SUBCASE +#define DOCTEST_PARTS_PUBLIC_SUBCASE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE SubcaseSignature { + String m_name; + const char *m_file; + int m_line; + + bool operator==(const SubcaseSignature &other) const; + bool operator<(const SubcaseSignature &other) const; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { +struct DOCTEST_INTERFACE Subcase { + SubcaseSignature m_signature; + bool m_entered = false; + + Subcase(const String &name, const char *file, int line); + Subcase(const Subcase &) = delete; + Subcase(Subcase &&) = delete; + Subcase &operator=(const Subcase &) = delete; + Subcase &operator=(Subcase &&) = delete; + ~Subcase(); + + operator bool() const; + +private: + bool checkFilters(); +}; +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_SUBCASE +#ifndef DOCTEST_PARTS_PUBLIC_TEST_SUITE +#define DOCTEST_PARTS_PUBLIC_TEST_SUITE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +struct DOCTEST_INTERFACE TestSuite { + const char *m_test_suite = nullptr; + const char *m_description = nullptr; + bool m_skip = false; + bool m_no_breaks = false; + bool m_no_output = false; + bool m_may_fail = false; + bool m_should_fail = false; + int m_expected_failures = 0; + double m_timeout = 0; + + TestSuite &operator*(const char *in) noexcept; + + template + TestSuite &operator*(const T &in) noexcept { + in.fill(*this); + return *this; + } +}; + +// forward declarations of functions used by the macros +DOCTEST_INTERFACE int setTestSuite(const TestSuite &ts) noexcept; + +} // namespace detail + +} // namespace doctest + +// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro +// introduces an anonymous namespace in which getCurrentTestSuite gets overridden +namespace doctest_detail_test_suite_ns { +DOCTEST_INTERFACE doctest::detail::TestSuite &getCurrentTestSuite() noexcept; + +// this is here to clear the 'current test suite' for the current translation unit - at the top +DOCTEST_GLOBAL_NO_WARNINGS(/* NOLINT(cert-err58-cpp) */ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), + doctest::detail::setTestSuite(doctest::detail::TestSuite() * "") +) + +} // namespace doctest_detail_test_suite_ns + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_TEST_SUITE +#ifndef DOCTEST_PARTS_PUBLIC_TEST_CASE +#define DOCTEST_PARTS_PUBLIC_TEST_CASE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE TestCaseData { + String m_file; // the file in which the test was registered (using String - see #350) + unsigned m_line; // the line where the test was registered + const char *m_name; // name of the test case + const char *m_test_suite; // the test suite in which the test was added + const char *m_description; + bool m_skip; + bool m_no_breaks; + bool m_no_output; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +using funcType = void (*)(); + +struct DOCTEST_INTERFACE TestCase : public TestCaseData { + funcType m_test; // a function pointer to the test case + + String m_type; // for templated test cases - gets appended to the real name + int m_template_id; // an ID used to distinguish between the different versions of a templated + // test case + String m_full_name; // contains the name (only for templated test cases!) + the template type + + TestCase( + funcType test, + const char *file, + unsigned line, + const TestSuite &test_suite, + const String &type = String(), + int template_id = -1 + ) noexcept; + + TestCase(const TestCase &other) noexcept; + TestCase(TestCase &&) = delete; + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase &operator=(const TestCase &other) noexcept; + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase &operator=(TestCase &&) = delete; + + TestCase &operator*(const char *in) noexcept; + + template + TestCase &operator*(const T &in) noexcept { + in.fill(*this); + return *this; + } + + bool operator<(const TestCase &other) const noexcept; + + ~TestCase() = default; +}; + +// forward declarations of functions used by the macros +DOCTEST_INTERFACE int regTest(const TestCase &tc) noexcept; + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_TEST_CASE +#ifndef DOCTEST_PARTS_PUBLIC_DECORATORS +#define DOCTEST_PARTS_PUBLIC_DECORATORS + + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ + struct name { \ + type data; \ + name(type in = def) noexcept \ + : data(in) {} \ + void fill(detail::TestCase &state) const noexcept { \ + state.DOCTEST_CAT(m_, name) = data; \ + } \ + void fill(detail::TestSuite &state) const noexcept { \ + state.DOCTEST_CAT(m_, name) = data; \ + } \ + } + +DOCTEST_DEFINE_DECORATOR(test_suite, const char *, ""); +DOCTEST_DEFINE_DECORATOR(description, const char *, ""); +DOCTEST_DEFINE_DECORATOR(skip, bool, true); +DOCTEST_DEFINE_DECORATOR(no_breaks, bool, true); +DOCTEST_DEFINE_DECORATOR(no_output, bool, true); +DOCTEST_DEFINE_DECORATOR(timeout, double, 0); +DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +#endif // DOCTEST_PARTS_PUBLIC_DECORATORS +#ifndef DOCTEST_PARTS_PUBLIC_EXCEPTION_TRANSLATOR +#define DOCTEST_PARTS_PUBLIC_EXCEPTION_TRANSLATOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +#ifndef DOCTEST_CONFIG_DISABLE + +struct DOCTEST_INTERFACE IExceptionTranslator { + DOCTEST_DECLARE_INTERFACE(IExceptionTranslator) + virtual bool translate(String &) const = 0; +}; + +template +class ExceptionTranslator : public IExceptionTranslator { +public: + explicit ExceptionTranslator(String (*translateFunction)(T)) noexcept + : m_translateFunction(translateFunction) {} + + bool translate(String &res) const override { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { + throw; + } catch (const T &ex) { + res = m_translateFunction(ex); + return true; + } catch (...) {} // NOLINT(bugprone-empty-catch) +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + static_cast(res); // to silence -Wunused-parameter + return false; + } + +private: + String (*m_translateFunction)(T); +}; + +DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator *et) noexcept; + +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace detail + +#ifndef DOCTEST_CONFIG_DISABLE + +template +int registerExceptionTranslator(String (*translateFunction)(T)) noexcept { + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") + static detail::ExceptionTranslator exceptionTranslator(translateFunction); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + detail::registerExceptionTranslatorImpl(&exceptionTranslator); + return 0; +} + +#else // DOCTEST_CONFIG_DISABLE + +template +int registerExceptionTranslator(String (*)(T)) noexcept { + return 0; +} + +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_EXCEPTION_TRANSLATOR +#ifndef DOCTEST_PARTS_PUBLIC_CONTEXT_SCOPE +#define DOCTEST_PARTS_PUBLIC_CONTEXT_SCOPE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE IContextScope { + DOCTEST_DECLARE_INTERFACE(IContextScope) + virtual void stringify(std::ostream *) const = 0; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +// ContextScope base class used to allow implementing methods of ContextScope +// that don't depend on the template parameter in doctest.cpp. +struct DOCTEST_INTERFACE ContextScopeBase : public IContextScope { + ContextScopeBase(const ContextScopeBase &) = delete; + + ContextScopeBase &operator=(const ContextScopeBase &) = delete; + ContextScopeBase &operator=(ContextScopeBase &&) = delete; + + ~ContextScopeBase() override = default; + +protected: + ContextScopeBase(); + ContextScopeBase(ContextScopeBase &&other) noexcept; + + void destroy(); + bool need_to_destroy{true}; +}; + +template +class ContextScope : public ContextScopeBase { + L lambda_; + +public: + explicit ContextScope(const L &lambda) + : lambda_(lambda) {} + + // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) + explicit ContextScope(L &&lambda) + : lambda_(static_cast(lambda)) {} + + ContextScope(const ContextScope &) = delete; + ContextScope(ContextScope &&) noexcept = default; + + ContextScope &operator=(const ContextScope &) = delete; + ContextScope &operator=(ContextScope &&) = delete; + + void stringify(std::ostream *s) const override { + lambda_(s); + } + + ~ContextScope() override { + if (need_to_destroy) { + destroy(); + } + } +}; + +template +ContextScope MakeContextScope(const L &lambda) { + return ContextScope(lambda); +} + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_CONTEXT_SCOPE +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_MESSAGE +#define DOCTEST_PARTS_PUBLIC_ASSERT_MESSAGE + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +struct DOCTEST_INTERFACE MessageData { + String m_string; + const char *m_file; + int m_line; + assertType::Enum m_severity; +}; + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +struct DOCTEST_INTERFACE MessageBuilder : public MessageData { + std::ostream *m_stream; + bool logged = false; + + MessageBuilder(const char *file, int line, assertType::Enum severity); + + MessageBuilder(const MessageBuilder &) = delete; + MessageBuilder(MessageBuilder &&) = delete; + + MessageBuilder &operator=(const MessageBuilder &) = delete; + MessageBuilder &operator=(MessageBuilder &&) = delete; + + ~MessageBuilder() noexcept(false); + + // the preferred way of chaining parameters for stringification + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) + template + MessageBuilder &operator,(const T &in) { + *m_stream << (DOCTEST_STRINGIFY(in)); + return *this; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + // kept here just for backwards-compatibility - the comma operator should be preferred now + template + MessageBuilder &operator<<(const T &in) { + return this->operator,(in); + } + + // the `,` operator has the lowest operator precedence - if `<<` is used by the user then + // the `,` operator will be called last which is not what we want and thus the `*` operator + // is used first (has higher operator precedence compared to `<<`) so that we guarantee that + // an operator of the MessageBuilder class is called first before the rest of the parameters + template + MessageBuilder &operator*(const T &in) { + return this->operator,(in); + } + + bool log(); + void react(); +}; + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_MESSAGE +#ifndef DOCTEST_PARTS_PUBLIC_PATH +#define DOCTEST_PARTS_PUBLIC_PATH + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_INTERFACE const char *skipPathFromFilename(const char *file); + +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_PATH +#ifndef DOCTEST_PARTS_PUBLIC_EXCEPTIONS +#define DOCTEST_PARTS_PUBLIC_EXCEPTIONS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +struct DOCTEST_INTERFACE TestFailureException {}; + +DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +DOCTEST_NORETURN +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +DOCTEST_INTERFACE void throwException(); + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_EXCEPTIONS +#ifndef DOCTEST_PARTS_PUBLIC_CONTEXT +#define DOCTEST_PARTS_PUBLIC_CONTEXT + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_INTERFACE extern bool is_running_in_test; + +namespace detail { +using assert_handler = void (*)(const AssertData &); +struct ContextState; +} // namespace detail + +class DOCTEST_INTERFACE Context { + detail::ContextState *p; + + void parseArgs(int argc, const char *const *argv, bool withDefaults = false); + +public: + explicit Context(int argc = 0, const char *const *argv = nullptr); + + Context(const Context &) = delete; + Context(Context &&) = delete; + + Context &operator=(const Context &) = delete; + Context &operator=(Context &&) = delete; + + ~Context(); // NOLINT(performance-trivially-destructible) + + void applyCommandLine(int argc, const char *const *argv); + + void addFilter(const char *filter, const char *value); + void clearFilters(); + void setOption(const char *option, bool value); + void setOption(const char *option, int value); + void setOption(const char *option, const char *value); + + bool shouldExit(); + + void setAsDefaultForAssertsOutOfTestCases(); + + void setAssertHandler(detail::assert_handler ah); + + void setCout(std::ostream *out); + + int run(); +}; +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_CONTEXT +#ifndef DOCTEST_PARTS_PUBLIC_ASSERT_HANDLER +#define DOCTEST_PARTS_PUBLIC_ASSERT_HANDLER + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData &ad); + +DOCTEST_INTERFACE bool +decomp_assert(assertType::Enum at, const char *file, int line, const char *expr, const Result &result); + +#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ + do { /* NOLINT(cppcoreguidelines-avoid-do-while) */ \ + if (!is_running_in_test) { \ + if (failed) { \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + rb.m_decomp = decomp; \ + failed_out_of_a_testing_context(rb); \ + if (isDebuggerActive() && !getContextOptions()->no_breaks) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if (checkIfShouldThrow(at)) \ + throwException(); \ + } \ + return !failed; \ + } \ + } while (false) + +#define DOCTEST_ASSERT_IN_TESTS(decomp) \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + if (rb.m_failed || getContextOptions()->success) \ + rb.m_decomp = decomp; \ + if (rb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if (rb.m_failed && checkIfShouldThrow(at)) \ + throwException() + +template +DOCTEST_NOINLINE bool binary_assert( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs +) { + const bool failed = !RelationalComparator()(lhs, rhs); + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + return !failed; +} + +template +DOCTEST_NOINLINE bool +unary_assert(assertType::Enum at, const char *file, int line, const char *expr, const DOCTEST_REF_WRAP(L) val) { + bool failed = !val; + + if (at & assertType::is_false) + failed = !failed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS((DOCTEST_STRINGIFY(val))); + DOCTEST_ASSERT_IN_TESTS((DOCTEST_STRINGIFY(val))); + return !failed; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_ASSERT_HANDLER +#ifndef DOCTEST_PARTS_PUBLIC_REPORTER +#define DOCTEST_PARTS_PUBLIC_REPORTER + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +namespace doctest { + +namespace TestCaseFailureReason { +enum Enum { + None = 0, + AssertFailure = 1, // an assertion has failed in the test case + Exception = 2, // test case threw an exception + Crash = 4, // a crash... + TooManyFailedAsserts = 8, // the abort-after option + Timeout = 16, // see the timeout decorator + ShouldHaveFailedButDidnt = 32, // see the should_fail decorator + ShouldHaveFailedAndDid = 64, // see the should_fail decorator + DidntFailExactlyNumTimes = 128, // see the expected_failures decorator + FailedExactlyNumTimes = 256, // see the expected_failures decorator + CouldHaveFailedAndDid = 512 // see the may_fail decorator +}; +} // namespace TestCaseFailureReason + +struct DOCTEST_INTERFACE CurrentTestCaseStats { + int numAssertsCurrentTest; + int numAssertsFailedCurrentTest; + double seconds; + int failure_flags; // use TestCaseFailureReason::Enum + bool testCaseSuccess; +}; + +struct DOCTEST_INTERFACE TestCaseException { + String error_string; + bool is_crash; +}; + +struct DOCTEST_INTERFACE TestRunStats { + unsigned numTestCases; + unsigned numTestCasesPassingFilters; + unsigned numTestSuitesPassingFilters; + unsigned numTestCasesFailed; + int numAsserts; + int numAssertsFailed; +}; + +struct QueryData { + const TestRunStats *run_stats = nullptr; + const TestCaseData **data = nullptr; + unsigned num_data = 0; +}; + +struct DOCTEST_INTERFACE IReporter { + // The constructor has to accept "const ContextOptions&" as a single argument + // which has most of the options for the run + a pointer to the stdout stream + // Reporter(const ContextOptions& in) + + // called when a query should be reported (listing test cases, printing the version, etc.) + virtual void report_query(const QueryData &) = 0; + + // called when the whole test run starts + virtual void test_run_start() = 0; + // called when the whole test run ends (caching a pointer to the input doesn't make sense here) + virtual void test_run_end(const TestRunStats &) = 0; + + // called when a test case is started (safe to cache a pointer to the input) + virtual void test_case_start(const TestCaseData &) = 0; + // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) + virtual void test_case_reenter(const TestCaseData &) = 0; + // called when a test case has ended + virtual void test_case_end(const CurrentTestCaseStats &) = 0; + + // called when an exception is thrown from the test case (or it crashes) + virtual void test_case_exception(const TestCaseException &) = 0; + + // called whenever a subcase is entered (don't cache pointers to the input) + virtual void subcase_start(const SubcaseSignature &) = 0; + // called whenever a subcase is exited (don't cache pointers to the input) + virtual void subcase_end() = 0; + + // called for each assert (don't cache pointers to the input) + virtual void log_assert(const AssertData &) = 0; + // called for each message (don't cache pointers to the input) + virtual void log_message(const MessageData &) = 0; + + // called when a test case is skipped either because it doesn't pass the filters, + // has a skip decorator or isn't in the execution range (between first and last) + // (safe to cache a pointer to the input) + virtual void test_case_skipped(const TestCaseData &) = 0; + + DOCTEST_DECLARE_INTERFACE(IReporter) + + // can obtain all currently active contexts and stringify them if one wishes to do so + static int get_num_active_contexts(); + static const IContextScope *const *get_active_contexts(); + + // can iterate through contexts which have been stringified automatically + // in their destructors when an exception has been thrown + static int get_num_stringified_contexts(); + static const String *get_stringified_contexts(); +}; + +namespace detail { +using reporterCreatorFunc = IReporter *(*)(const ContextOptions &); + +DOCTEST_INTERFACE void +registerReporterImpl(const char *name, int prio, reporterCreatorFunc c, bool isReporter) noexcept; + +template +IReporter *reporterCreator(const ContextOptions &o) { + return new Reporter(o); +} +} // namespace detail + +template +int registerReporter(const char *name, int priority, bool isReporter) noexcept { + detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); + return 0; +} +} // namespace doctest + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_REPORTER +#ifndef DOCTEST_PARTS_PUBLIC_GENERATOR +#define DOCTEST_PARTS_PUBLIC_GENERATOR + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE +namespace doctest { +namespace detail { + +DOCTEST_INTERFACE size_t acquireGeneratorDecisionIndex(size_t count); + +template +T acquireGeneratorValue(T first, Rest... rest) { + const T values[] = {first, static_cast(rest)...}; + const size_t idx = acquireGeneratorDecisionIndex(1 + sizeof...(Rest)); + return values[idx]; +} + +} // namespace detail +} // namespace doctest +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_GENERATOR +#ifndef DOCTEST_PARTS_PUBLIC_MACROS +#define DOCTEST_PARTS_PUBLIC_MACROS + + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE +namespace doctest { +namespace detail { +template +int instantiationHelper(const T &) noexcept { + return 0; +} + +} // namespace detail +} // namespace doctest +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_EMPTY [] { return false; }() +#else +#define DOCTEST_FUNC_EMPTY (void)0 +#endif + +// if registering is not disabled +#ifndef DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_SCOPE_BEGIN [&] +#define DOCTEST_FUNC_SCOPE_END () +#define DOCTEST_FUNC_SCOPE_RET(v) return v +#else +#define DOCTEST_FUNC_SCOPE_BEGIN do /* NOLINT(cppcoreguidelines-avoid-do-while)*/ +#define DOCTEST_FUNC_SCOPE_END while (false) +#define DOCTEST_FUNC_SCOPE_RET(v) (void)0 +#endif + +// common code in asserts - for convenience +#define DOCTEST_ASSERT_LOG_REACT_RETURN(b) \ + if (b.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + b.react(); \ + DOCTEST_FUNC_SCOPE_RET(!b.m_failed) + +#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) x; +#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) \ + try { \ + x; \ + } catch (...) { DOCTEST_RB.translateException(); } +#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ + static_cast(__VA_ARGS__); \ + DOCTEST_GCC_SUPPRESS_WARNING_POP +#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) __VA_ARGS__; +#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +// registers the test by initializing a dummy var with a function +#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ + global_prefix DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT */ \ + doctest::detail::regTest( \ + doctest::detail::TestCase(f, __FILE__, __LINE__, doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ + decorators \ + ) \ + ) + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ + namespace { /* NOLINT */ \ + struct der : public base { \ + void f(); \ + }; \ + static DOCTEST_INLINE_NOINLINE void func() { \ + der v; \ + v.f(); \ + } \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ + } \ + DOCTEST_INLINE_NOINLINE void der::f() // NOLINT(misc-definitions-in-headers) + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ + static void f(); \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ + static void f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ + static doctest::detail::funcType proxy() { \ + return f; \ + } \ + DOCTEST_REGISTER_FUNCTION(inline, proxy(), decorators) \ + static void f() + +// for registering tests +#define DOCTEST_TEST_CASE(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for registering tests in classes - requires C++17 for inline variables! +#if DOCTEST_CPLUSPLUS >= 201703L +#define DOCTEST_TEST_CASE_CLASS(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), DOCTEST_ANONYMOUS(DOCTEST_ANON_PROXY_), decorators \ + ) +#else // DOCTEST_TEST_CASE_CLASS +#define DOCTEST_TEST_CASE_CLASS(...) TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER +#endif // DOCTEST_TEST_CASE_CLASS + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ + DOCTEST_IMPLEMENT_FIXTURE( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), c, DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators \ + ) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) \ + namespace doctest { \ + template <> \ + inline String toString<__VA_ARGS__>() { \ + return str; \ + } \ + } \ + static_assert(true, "") + +#define DOCTEST_TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING_AS(#__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ + template \ + static void func(); \ + namespace { /* NOLINT */ \ + template \ + struct iter; \ + template \ + struct iter> { \ + iter(const char *file, unsigned line, int index) noexcept { \ + doctest::detail::regTest( \ + doctest::detail::TestCase( \ + func, \ + file, \ + line, \ + doctest_detail_test_suite_ns::getCurrentTestSuite(), \ + doctest::toString(), \ + int(line) * 1000 + index \ + ) * \ + dec \ + ); \ + iter>(file, line, index + 1); \ + } \ + }; \ + template <> \ + struct iter> { \ + iter(const char *, unsigned, int) {} \ + }; \ + } \ + template \ + static void func() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)) + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_CAT(anon, DUMMY), /* NOLINT(cert-err58-cpp, fuchsia-statically-constructed-objects) */ \ + doctest::detail::instantiationHelper(DOCTEST_CAT(id, ITERATOR) < __VA_ARGS__ > (__FILE__, __LINE__, 0)) \ + ) + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ + template \ + static void anon() + +#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) + +// for subcases +#define DOCTEST_SUBCASE(name) \ + if (const doctest::detail::Subcase &DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ + doctest::detail::Subcase(name, __FILE__, __LINE__)) + +// for generating value-parameterized test inputs +#define DOCTEST_GENERATE(...) doctest::detail::acquireGeneratorValue(__VA_ARGS__) + +// for grouping tests in test suites by using code blocks +#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ + namespace ns_name { \ + namespace doctest_detail_test_suite_ns { \ + static DOCTEST_NOINLINE doctest::detail::TestSuite &getCurrentTestSuite() noexcept { \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmissing-field-initializers") \ + static doctest::detail::TestSuite data{}; \ + static bool inited = false; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + if (!inited) { \ + data *decorators; \ + inited = true; \ + } \ + return data; \ + } \ + } \ + } \ + namespace ns_name + +#define DOCTEST_TEST_SUITE(decorators) DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(DOCTEST_ANON_SUITE_)) + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators) \ + ) \ + static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * "") \ + ) \ + using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +// for registering exception translators +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ + inline doctest::String translatorName(signature); \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerExceptionTranslator(translatorName) \ + ) \ + doctest::String translatorName(signature) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), signature) + +// for registering reporters +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, true) \ + ) \ + static_assert(true, "") + +// for registering listeners +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS( \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, false) \ + ) \ + static_assert(true, "") + +#define DOCTEST_INFO(...) \ + DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_MB_), DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_OTHER_), __VA_ARGS__) + +#define DOCTEST_INFO_IMPL(mb_name, s_name, ...) \ + auto DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope([&](std::ostream *s_name) { \ + doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ + mb_name.m_stream = s_name; \ + mb_name *__VA_ARGS__; \ + }) + +#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := ", x) + +#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ + mb *__VA_ARGS__; \ + if (mb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + mb.react(); \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) \ + DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) \ + DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) \ + DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) + +#define DOCTEST_MESSAGE(...) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL_CHECK(...) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL(...) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, __VA_ARGS__) + +#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY( \ + DOCTEST_RB.setResult(doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) << __VA_ARGS__) \ + ) /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB) \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ + } \ + DOCTEST_FUNC_SCOPE_END // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.binary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.unary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } \ + DOCTEST_FUNC_SCOPE_END + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +// necessary for _MESSAGE +#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::decomp_assert( \ + doctest::assertType::assert_type, \ + __FILE__, \ + __LINE__, \ + #__VA_ARGS__, \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) << __VA_ARGS__ \ + ) DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ + doctest::detail::binary_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__ \ + ) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) +#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) +#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) +#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) + +// clang-format off +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) +#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) +#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) +#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) +#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) +#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) +#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) +#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) +#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) +#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) +#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) +#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) +#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) +#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) +#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) +#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) +#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) + +#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if (!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #expr, #__VA_ARGS__, message \ + ); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch (const typename doctest::detail::types::remove_const< \ + typename doctest::detail::types::remove_reference<__VA_ARGS__>::type>::type &) { \ + DOCTEST_RB.translateException(); \ + DOCTEST_RB.m_threw_as = true; \ + } catch (...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_THROWS_WITH(expr, expr_str, assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if (!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB( \ + doctest::assertType::assert_type, __FILE__, __LINE__, expr_str, "", __VA_ARGS__ \ + ); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch (...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } \ + DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_NOTHROW(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(__VA_ARGS__) \ + } catch (...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } \ + DOCTEST_FUNC_SCOPE_END + +// clang-format off +#define DOCTEST_WARN_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_WARN_THROWS, "") +#define DOCTEST_CHECK_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_CHECK_THROWS, "") +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_REQUIRE_THROWS, "") + +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_WARN_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) + +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_WARN_NOTHROW, __VA_ARGS__) +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_CHECK_NOTHROW, __VA_ARGS__) +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_REQUIRE_NOTHROW, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// ================================================================================================= +// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == +// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == +// ================================================================================================= +#else // DOCTEST_CONFIG_DISABLE + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ + namespace /* NOLINT */ { \ + template \ + struct der : public base { \ + void f(); \ + }; \ + } \ + template \ + inline void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ + template \ + static inline void f() + +// for registering tests +#define DOCTEST_TEST_CASE(name) DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests in classes +#define DOCTEST_TEST_CASE_CLASS(name) DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), x, DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) static_assert(true, "") +#define DOCTEST_TYPE_TO_STRING(...) static_assert(true, "") + +// for typed tests +#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) static_assert(true, "") +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) static_assert(true, "") + +// for subcases +#define DOCTEST_SUBCASE(name) + +// for generating value-parameterized test inputs +#define DOCTEST_GENERATE_IMPL(first, ...) (first) +#define DOCTEST_GENERATE(...) DOCTEST_GENERATE_IMPL(__VA_ARGS__, DOCTEST_EMPTY) + +// for a testsuite block +#define DOCTEST_TEST_SUITE(name) namespace // NOLINT + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(name) static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + template \ + static inline doctest::String DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_)(signature) + +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) + +#define DOCTEST_INFO(...) (static_cast(0)) +#define DOCTEST_CAPTURE(x) (static_cast(0)) +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_MESSAGE(...) (static_cast(0)) +#define DOCTEST_FAIL_CHECK(...) (static_cast(0)) +#define DOCTEST_FAIL(...) (static_cast(0)) + +#if defined(DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED) && defined(DOCTEST_CONFIG_ASSERTS_RETURN_VALUES) + +#define DOCTEST_WARN(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#define DOCTEST_WARN_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() + +namespace doctest { +namespace detail { +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + bool name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + +DOCTEST_RELATIONAL_OP(eq, ==) +DOCTEST_RELATIONAL_OP(ne, !=) +DOCTEST_RELATIONAL_OP(lt, <) +DOCTEST_RELATIONAL_OP(gt, >) +DOCTEST_RELATIONAL_OP(le, <=) +DOCTEST_RELATIONAL_OP(ge, >=) +} // namespace detail +} // namespace doctest + +#define DOCTEST_WARN_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_CHECK_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_WARN_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_CHECK_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_WARN_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_CHECK_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_WARN_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_CHECK_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_WARN_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_CHECK_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_WARN_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_CHECK_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_WARN_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS_WITH(expr, with, ...) \ + [] { \ + static_assert(false, "Exception translation is not available when doctest is disabled."); \ + return false; \ + }() +#define DOCTEST_CHECK_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) + +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(, , ) + +// clang-format off +#define DOCTEST_WARN_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +// clang-format on + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#else // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#define DOCTEST_WARN(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC DOCTEST_FUNC_EMPTY +#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC \ + [] { \ + static_assert( \ + false, \ + "Exceptions are disabled! " \ + "Use DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS if you want " \ + "to compile with exceptions disabled." \ + ); \ + return false; \ + }() + +#undef DOCTEST_REQUIRE +#undef DOCTEST_REQUIRE_FALSE +#undef DOCTEST_REQUIRE_MESSAGE +#undef DOCTEST_REQUIRE_FALSE_MESSAGE +#undef DOCTEST_REQUIRE_EQ +#undef DOCTEST_REQUIRE_NE +#undef DOCTEST_REQUIRE_GT +#undef DOCTEST_REQUIRE_LT +#undef DOCTEST_REQUIRE_GE +#undef DOCTEST_REQUIRE_LE +#undef DOCTEST_REQUIRE_UNARY +#undef DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_REQUIRE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_EQ DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS +#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ +#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ +#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE +#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE +#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE +#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT +#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT +#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT +#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT +#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT +#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT +#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE +#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE +#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE +#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE +#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE +#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE + +#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY +#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY +#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) + +// BDD style macros +// clang-format off +#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) +#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) +#define DOCTEST_SCENARIO_METHOD(x, name) DOCTEST_TEST_CASE_FIXTURE(x, " Scenario: " name) +#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) +#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) + +#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) +#define DOCTEST_AND_GIVEN(name) DOCTEST_SUBCASE(" And: " name) +#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) +#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE(" And: " name) +#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) +#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) +// clang-format on + +// == SHORT VERSIONS OF THE MACROS +#ifndef DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#define TEST_CASE(name) DOCTEST_TEST_CASE(name) +#define TEST_CASE_CLASS(name) DOCTEST_TEST_CASE_CLASS(name) +#define TEST_CASE_FIXTURE(x, name) DOCTEST_TEST_CASE_FIXTURE(x, name) +#define TYPE_TO_STRING_AS(str, ...) DOCTEST_TYPE_TO_STRING_AS(str, __VA_ARGS__) +#define TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING(__VA_ARGS__) +#define TEST_CASE_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, T, id) +#define TEST_CASE_TEMPLATE_INVOKE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_APPLY(id, ...) DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, __VA_ARGS__) +#define SUBCASE(name) DOCTEST_SUBCASE(name) +#define GENERATE(...) DOCTEST_GENERATE(__VA_ARGS__) +#define TEST_SUITE(decorators) DOCTEST_TEST_SUITE(decorators) +#define TEST_SUITE_BEGIN(name) DOCTEST_TEST_SUITE_BEGIN(name) +#define TEST_SUITE_END DOCTEST_TEST_SUITE_END +#define REGISTER_EXCEPTION_TRANSLATOR(signature) DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) +#define REGISTER_REPORTER(name, priority, reporter) DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define REGISTER_LISTENER(name, priority, reporter) DOCTEST_REGISTER_LISTENER(name, priority, reporter) +#define INFO(...) DOCTEST_INFO(__VA_ARGS__) +#define CAPTURE(x) DOCTEST_CAPTURE(x) +#define ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_MESSAGE_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_FAIL_CHECK_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_FAIL_AT(file, line, __VA_ARGS__) +#define MESSAGE(...) DOCTEST_MESSAGE(__VA_ARGS__) +#define FAIL_CHECK(...) DOCTEST_FAIL_CHECK(__VA_ARGS__) +#define FAIL(...) DOCTEST_FAIL(__VA_ARGS__) +#define TO_LVALUE(...) DOCTEST_TO_LVALUE(__VA_ARGS__) + +#define WARN(...) DOCTEST_WARN(__VA_ARGS__) +#define WARN_FALSE(...) DOCTEST_WARN_FALSE(__VA_ARGS__) +#define WARN_THROWS(...) DOCTEST_WARN_THROWS(__VA_ARGS__) +#define WARN_THROWS_AS(expr, ...) DOCTEST_WARN_THROWS_AS(expr, __VA_ARGS__) +#define WARN_THROWS_WITH(expr, ...) DOCTEST_WARN_THROWS_WITH(expr, __VA_ARGS__) +#define WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_WARN_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define WARN_NOTHROW(...) DOCTEST_WARN_NOTHROW(__VA_ARGS__) +#define CHECK(...) DOCTEST_CHECK(__VA_ARGS__) +#define CHECK_FALSE(...) DOCTEST_CHECK_FALSE(__VA_ARGS__) +#define CHECK_THROWS(...) DOCTEST_CHECK_THROWS(__VA_ARGS__) +#define CHECK_THROWS_AS(expr, ...) DOCTEST_CHECK_THROWS_AS(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH(expr, ...) DOCTEST_CHECK_THROWS_WITH(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define CHECK_NOTHROW(...) DOCTEST_CHECK_NOTHROW(__VA_ARGS__) +#define REQUIRE(...) DOCTEST_REQUIRE(__VA_ARGS__) +#define REQUIRE_FALSE(...) DOCTEST_REQUIRE_FALSE(__VA_ARGS__) +#define REQUIRE_THROWS(...) DOCTEST_REQUIRE_THROWS(__VA_ARGS__) +#define REQUIRE_THROWS_AS(expr, ...) DOCTEST_REQUIRE_THROWS_AS(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH(expr, ...) DOCTEST_REQUIRE_THROWS_WITH(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define REQUIRE_NOTHROW(...) DOCTEST_REQUIRE_NOTHROW(__VA_ARGS__) + +// clang-format off +#define WARN_MESSAGE(cond, ...) DOCTEST_WARN_MESSAGE(cond, __VA_ARGS__) +#define WARN_FALSE_MESSAGE(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__) +#define WARN_THROWS_MESSAGE(expr, ...) DOCTEST_WARN_THROWS_MESSAGE(expr, __VA_ARGS__) +#define WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_WARN_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define CHECK_MESSAGE(cond, ...) DOCTEST_CHECK_MESSAGE(cond, __VA_ARGS__) +#define CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__) +#define CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_CHECK_THROWS_MESSAGE(expr, __VA_ARGS__) +#define CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_CHECK_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_MESSAGE(cond, ...) DOCTEST_REQUIRE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_REQUIRE_THROWS_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, __VA_ARGS__) +// clang-format on + +#define SCENARIO(name) DOCTEST_SCENARIO(name) +#define SCENARIO_METHOD(x, name) DOCTEST_SCENARIO_METHOD(x, name) +#define SCENARIO_CLASS(name) DOCTEST_SCENARIO_CLASS(name) +#define SCENARIO_TEMPLATE(name, T, ...) DOCTEST_SCENARIO_TEMPLATE(name, T, __VA_ARGS__) +#define SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) +#define GIVEN(name) DOCTEST_GIVEN(name) +#define AND_GIVEN(name) DOCTEST_AND_GIVEN(name) +#define WHEN(name) DOCTEST_WHEN(name) +#define AND_WHEN(name) DOCTEST_AND_WHEN(name) +#define THEN(name) DOCTEST_THEN(name) +#define AND_THEN(name) DOCTEST_AND_THEN(name) + +#define WARN_EQ(...) DOCTEST_WARN_EQ(__VA_ARGS__) +#define CHECK_EQ(...) DOCTEST_CHECK_EQ(__VA_ARGS__) +#define REQUIRE_EQ(...) DOCTEST_REQUIRE_EQ(__VA_ARGS__) +#define WARN_NE(...) DOCTEST_WARN_NE(__VA_ARGS__) +#define CHECK_NE(...) DOCTEST_CHECK_NE(__VA_ARGS__) +#define REQUIRE_NE(...) DOCTEST_REQUIRE_NE(__VA_ARGS__) +#define WARN_GT(...) DOCTEST_WARN_GT(__VA_ARGS__) +#define CHECK_GT(...) DOCTEST_CHECK_GT(__VA_ARGS__) +#define REQUIRE_GT(...) DOCTEST_REQUIRE_GT(__VA_ARGS__) +#define WARN_LT(...) DOCTEST_WARN_LT(__VA_ARGS__) +#define CHECK_LT(...) DOCTEST_CHECK_LT(__VA_ARGS__) +#define REQUIRE_LT(...) DOCTEST_REQUIRE_LT(__VA_ARGS__) +#define WARN_GE(...) DOCTEST_WARN_GE(__VA_ARGS__) +#define CHECK_GE(...) DOCTEST_CHECK_GE(__VA_ARGS__) +#define REQUIRE_GE(...) DOCTEST_REQUIRE_GE(__VA_ARGS__) +#define WARN_LE(...) DOCTEST_WARN_LE(__VA_ARGS__) +#define CHECK_LE(...) DOCTEST_CHECK_LE(__VA_ARGS__) +#define REQUIRE_LE(...) DOCTEST_REQUIRE_LE(__VA_ARGS__) +#define WARN_UNARY(...) DOCTEST_WARN_UNARY(__VA_ARGS__) +#define CHECK_UNARY(...) DOCTEST_CHECK_UNARY(__VA_ARGS__) +#define REQUIRE_UNARY(...) DOCTEST_REQUIRE_UNARY(__VA_ARGS__) +#define WARN_UNARY_FALSE(...) DOCTEST_WARN_UNARY_FALSE(__VA_ARGS__) +#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +// KEPT FOR BACKWARDS COMPATIBILITY +#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__) +#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__) +#define FAST_REQUIRE_EQ(...) DOCTEST_FAST_REQUIRE_EQ(__VA_ARGS__) +#define FAST_WARN_NE(...) DOCTEST_FAST_WARN_NE(__VA_ARGS__) +#define FAST_CHECK_NE(...) DOCTEST_FAST_CHECK_NE(__VA_ARGS__) +#define FAST_REQUIRE_NE(...) DOCTEST_FAST_REQUIRE_NE(__VA_ARGS__) +#define FAST_WARN_GT(...) DOCTEST_FAST_WARN_GT(__VA_ARGS__) +#define FAST_CHECK_GT(...) DOCTEST_FAST_CHECK_GT(__VA_ARGS__) +#define FAST_REQUIRE_GT(...) DOCTEST_FAST_REQUIRE_GT(__VA_ARGS__) +#define FAST_WARN_LT(...) DOCTEST_FAST_WARN_LT(__VA_ARGS__) +#define FAST_CHECK_LT(...) DOCTEST_FAST_CHECK_LT(__VA_ARGS__) +#define FAST_REQUIRE_LT(...) DOCTEST_FAST_REQUIRE_LT(__VA_ARGS__) +#define FAST_WARN_GE(...) DOCTEST_FAST_WARN_GE(__VA_ARGS__) +#define FAST_CHECK_GE(...) DOCTEST_FAST_CHECK_GE(__VA_ARGS__) +#define FAST_REQUIRE_GE(...) DOCTEST_FAST_REQUIRE_GE(__VA_ARGS__) +#define FAST_WARN_LE(...) DOCTEST_FAST_WARN_LE(__VA_ARGS__) +#define FAST_CHECK_LE(...) DOCTEST_FAST_CHECK_LE(__VA_ARGS__) +#define FAST_REQUIRE_LE(...) DOCTEST_FAST_REQUIRE_LE(__VA_ARGS__) + +#define FAST_WARN_UNARY(...) DOCTEST_FAST_WARN_UNARY(__VA_ARGS__) +#define FAST_CHECK_UNARY(...) DOCTEST_FAST_CHECK_UNARY(__VA_ARGS__) +#define FAST_REQUIRE_UNARY(...) DOCTEST_FAST_REQUIRE_UNARY(__VA_ARGS__) +#define FAST_WARN_UNARY_FALSE(...) DOCTEST_FAST_WARN_UNARY_FALSE(__VA_ARGS__) +#define FAST_CHECK_UNARY_FALSE(...) DOCTEST_FAST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define FAST_REQUIRE_UNARY_FALSE(...) DOCTEST_FAST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +#define TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_PARTS_PUBLIC_MACROS + +DOCTEST_SUPPRESS_PUBLIC_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_INCLUDED + +#if defined(DOCTEST_CONFIG_IMPLEMENT) && !defined(DOCTEST_LIBRARY_IMPLEMENTATION) + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") +#define DOCTEST_LIBRARY_IMPLEMENTATION +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_PARTS_PRIVATE_PRELUDE +#define DOCTEST_PARTS_PRIVATE_PRELUDE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN + +// required includes - will go only in one translation unit! +#include +#include +#include +// borland (Embarcadero) compiler requires math.h and not cmath - +// https://github.com/doctest/doctest/pull/37 +#ifdef __BORLANDC__ +#include +#endif // __BORLANDC__ +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#define DOCTEST_DECLARE_MUTEX(name) std::mutex name; +#define DOCTEST_DECLARE_STATIC_MUTEX(name) static DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) const std::lock_guard DOCTEST_ANONYMOUS(DOCTEST_ANON_LOCK_)(name); +#else // DOCTEST_CONFIG_NO_MULTITHREADING +#define DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_DECLARE_STATIC_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) +#endif // DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#include +#include +#include +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) +#include +#endif // DOCTEST_CONFIG_POSIX_SIGNALS +#include +#include +#include +#include + +#ifdef DOCTEST_PLATFORM_MAC +#include +#include +#include +#endif // DOCTEST_PLATFORM_MAC + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// defines for a leaner windows.h +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#define DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif // WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#define DOCTEST_UNDEF_NOMINMAX +#endif // NOMINMAX + +// not sure what AfxWin.h is for - here I do what Catch does +#ifdef __AFXDLL +#include +#else +#include +#endif +#include + +#ifdef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#undef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif // DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#ifdef DOCTEST_UNDEF_NOMINMAX +#undef NOMINMAX +#undef DOCTEST_UNDEF_NOMINMAX +#endif // DOCTEST_UNDEF_NOMINMAX + +#else // DOCTEST_PLATFORM_WINDOWS + +#include +#include + +#endif // DOCTEST_PLATFORM_WINDOWS + +// this is a fix for https://github.com/doctest/doctest/issues/348 +// https://mail.gnome.org/archives/xml/2012-January/msg00000.html +#if !defined(HAVE_UNISTD_H) && !defined(STDOUT_FILENO) +#define STDOUT_FILENO fileno(stdout) +#endif // HAVE_UNISTD_H + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END + +// counts the number of elements in a C array +#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) + +#ifdef DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled +#else // DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled +#endif // DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_THREAD_LOCAL +#if defined(DOCTEST_CONFIG_NO_MULTITHREADING) || DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_THREAD_LOCAL +#else // DOCTEST_MSVC +#define DOCTEST_THREAD_LOCAL thread_local +#endif // DOCTEST_MSVC +#endif // DOCTEST_THREAD_LOCAL + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES +#define DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES 32 +#endif + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE +#define DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE 64 +#endif + +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +#endif + +#ifndef DOCTEST_CDECL +#define DOCTEST_CDECL __cdecl +#endif + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_PRELUDE +#ifndef DOCTEST_PARTS_PRIVATE_CONTEXT_STATE +#define DOCTEST_PARTS_PRIVATE_CONTEXT_STATE + +#ifndef DOCTEST_PARTS_PRIVATE_TIMER +#define DOCTEST_PARTS_PRIVATE_TIMER + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +namespace timer_large_integer { + +#if defined(DOCTEST_PLATFORM_WINDOWS) +using type = ULONGLONG; +#else // DOCTEST_PLATFORM_WINDOWS +using type = std::uint64_t; +#endif // DOCTEST_PLATFORM_WINDOWS +} // namespace timer_large_integer + +using ticks_t = timer_large_integer::type; + +ticks_t getCurrentTicks(); + +struct Timer { + void start(); + unsigned int getElapsedMicroseconds() const; + double getElapsedSeconds() const; + +private: + ticks_t m_ticks = 0; +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_TIMER +#ifndef DOCTEST_PARTS_PRIVATE_ATOMIC +#define DOCTEST_PARTS_PRIVATE_ATOMIC + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_CONFIG_NO_MULTITHREADING +template +using Atomic = T; +#else // DOCTEST_CONFIG_NO_MULTITHREADING +template +using Atomic = std::atomic; +#endif // DOCTEST_CONFIG_NO_MULTITHREADING + +#if defined(DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS) || defined(DOCTEST_CONFIG_NO_MULTITHREADING) +template +using MultiLaneAtomic = Atomic; +#else // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +// Provides a multilane implementation of an atomic variable that supports add, sub, load, +// store. Instead of using a single atomic variable, this splits up into multiple ones, +// each sitting on a separate cache line. The goal is to provide a speedup when most +// operations are modifying. It achieves this with two properties: +// +// * Multiple atomics are used, so chance of congestion from the same atomic is reduced. +// * Each atomic sits on a separate cache line, so false sharing is reduced. +// +// The disadvantage is that there is a small overhead due to the use of TLS, and load/store +// is slower because all atomics have to be accessed. +template +class MultiLaneAtomic { + struct CacheLineAlignedAtomic { + Atomic atomic{}; + char padding[DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE - sizeof(Atomic)]; + }; + CacheLineAlignedAtomic m_atomics[DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES]; + + static_assert( + sizeof(CacheLineAlignedAtomic) == DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE, + "guarantee one atomic takes exactly one cache line" + ); + +public: + T operator++() DOCTEST_NOEXCEPT { + return fetch_add(1) + 1; + } + + T operator++(int) DOCTEST_NOEXCEPT { // NOLINT(cert-dcl21-cpp) + return fetch_add(1); + } + + T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_add(arg, order); + } + + T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_sub(arg, order); + } + + operator T() const DOCTEST_NOEXCEPT { + return load(); + } + + T load(std::memory_order order = std::memory_order_seq_cst) const DOCTEST_NOEXCEPT { + auto result = T(); + for (const auto &c: m_atomics) { + result += c.atomic.load(order); + } + return result; + } + + // NOLINTNEXTLINE(cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator) + T operator=(T desired) DOCTEST_NOEXCEPT { + store(desired); + return desired; + } + + void store(T desired, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + // first value becomes desired", all others become 0. + for (auto &c: m_atomics) { + c.atomic.store(desired, order); + desired = {}; + } + } + +private: + // Each thread has a different atomic that it operates on. If more than NumLanes threads + // use this, some will use the same atomic. So performance will degrade a bit, but still + // everything will work. + // + // The logic here is a bit tricky. The call should be as fast as possible, so that there + // is minimal to no overhead in determining the correct atomic for the current thread. + // + // 1. A global static counter laneCounter counts continuously up. + // 2. Each successive thread will use modulo operation of that counter so it gets an atomic + // assigned in a round-robin fashion. + // 3. This tlsLaneIdx is stored in the thread local data, so it is directly available with + // little overhead. + Atomic &myAtomic() DOCTEST_NOEXCEPT { + static Atomic laneCounter; + DOCTEST_THREAD_LOCAL size_t tlsLaneIdx = laneCounter++ % DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES; + + return m_atomics[tlsLaneIdx].atomic; + } +}; +#endif // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_ATOMIC +#ifndef DOCTEST_PARTS_PRIVATE_TRAVERSAL +#define DOCTEST_PARTS_PRIVATE_TRAVERSAL + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +struct DecisionPoint { + // Number of branches available at this depth for the current traversal path. + size_t branch_count = 0; + // Encountered sibling subcases in source order for subcase decision points. + std::vector subcases; +}; + +class TraversalState { +public: + size_t activeSubcaseDepth() const { + return m_activeSubcaseDepth; + } + + void resetForTestCase(); + void resetForRun(); + bool advance(); + bool tryEnterSubcase(const SubcaseSignature &signature); + void leaveSubcase(); + size_t unwindActiveSubcases(); + size_t acquireGeneratorIndex(size_t count); + +private: + // decisionPath is the selected traversal prefix; discoveredDecisionPath is rebuilt + // on each rerun to describe the branches encountered at each depth. + std::vector m_discoveredDecisionPath; + std::vector m_decisionPath; + size_t m_decisionDepth = 0; + std::vector m_enteredSubcaseDepths; + size_t m_activeSubcaseDepth = 0; + + DecisionPoint &ensureDecisionPointAtCurrentDepth(); +}; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_TRAVERSAL + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// this holds both parameters from the command line and runtime data for tests +struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats { + MultiLaneAtomic numAssertsCurrentTest_atomic; + MultiLaneAtomic numAssertsFailedCurrentTest_atomic; + + std::vector> filters = decltype(filters)(9); // 9 different filters + + std::vector reporters_currently_used; + + assert_handler ah = nullptr; + + Timer timer; + + std::vector stringifiedContexts; // logging from INFO() due to an exception + + // Backtrack traversal state shared by SUBCASE and GENERATE. + TraversalState traversal; + Atomic shouldLogCurrentException; + + void resetRunData(); + + void finalizeTestCaseData(); +}; + +extern ContextState *g_cs; + +// used to avoid locks for the debug output +// TODO: figure out if this is indeed necessary/correct - seems like either there still +// could be a race or that there wouldn't be a race even if using the context directly +extern DOCTEST_THREAD_LOCAL bool g_no_colors; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_CONTEXT_STATE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +using detail::g_cs; + +AssertData::AssertData( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const StringContains &exception_string +) + : m_test_case(g_cs->currentTest), + m_at(at), + m_file(file), + m_line(line), + m_expr(expr), + m_failed(true), + m_threw(false), + m_threw_as(false), + m_exception_type(exception_type), + m_exception_string(exception_string) { +#if DOCTEST_MSVC + if (m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC + ++m_expr; +#endif // MSVC +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) + : m_at(at) {} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_REPORTER +#define DOCTEST_PARTS_PRIVATE_REPORTER + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +// the int (priority) is part of the key for automatic sorting - sadly one can register a +// reporter with a duplicate name and a different priority but hopefully that won't happen often :| +using reporterMap = std::map, detail::reporterCreatorFunc>; + +reporterMap &getReporters() noexcept; +reporterMap &getListeners() noexcept; +} // namespace detail + +#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ + for (auto &curr_rep: g_cs->reporters_currently_used) \ + curr_rep->function(__VA_ARGS__) + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTER +#ifndef DOCTEST_PARTS_PRIVATE_ASSERT_HANDLER +#define DOCTEST_PARTS_PRIVATE_ASSERT_HANDLER + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +void addAssert(assertType::Enum at); + +void addFailedAssert(assertType::Enum at); + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) +void reportFatal(const std::string &message); +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_ASSERT_HANDLER + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +void addAssert(assertType::Enum at) { + if ((at & assertType::is_warn) == 0) + g_cs->numAssertsCurrentTest_atomic++; +} + +void addFailedAssert(assertType::Enum at) { + if ((at & assertType::is_warn) == 0) + g_cs->numAssertsFailedCurrentTest_atomic++; +} + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) +void reportFatal(const std::string &message) { + g_cs->failure_flags |= TestCaseFailureReason::Crash; + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); + + for (size_t i = g_cs->traversal.unwindActiveSubcases(); i > 0; --i) + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + g_cs->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); +} +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +void failed_out_of_a_testing_context(const AssertData &ad) { + if (g_cs->ah) + g_cs->ah(ad); + else + std::abort(); +} + +bool decomp_assert(assertType::Enum at, const char *file, int line, const char *expr, const Result &result) { + const bool failed = !result.m_passed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); + DOCTEST_ASSERT_IN_TESTS(result.m_decomp); + return !failed; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +MessageBuilder::MessageBuilder(const char *file, int line, assertType::Enum severity) { + m_stream = tlssPush(); + m_file = file; + m_line = line; + m_severity = severity; +} + +MessageBuilder::~MessageBuilder() noexcept(false) { + if (!logged) + tlssPop(); +} + +bool MessageBuilder::log() { + if (!logged) { + m_string = tlssPop(); + logged = true; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); + + const bool isWarn = m_severity & assertType::is_warn; + + // warn is just a message in this context so we don't treat it as an assert + if (!isWarn) { + addAssert(m_severity); + addFailedAssert(m_severity); + } + + return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger +} + +void MessageBuilder::react() { + if (m_severity & assertType::is_require) + throwException(); +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_EXCEPTION_TRANSLATOR +#define DOCTEST_PARTS_PRIVATE_EXCEPTION_TRANSLATOR + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +std::vector &getExceptionTranslators() noexcept; +String translateActiveException() noexcept; + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_EXCEPTION_TRANSLATOR + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +Result::Result(bool passed, const String &decomposition) + : m_passed(passed), m_decomp(decomposition) {} + +ResultBuilder::ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const String &exception_string +) + : AssertData(at, file, line, expr, exception_type, exception_string) { +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +ResultBuilder::ResultBuilder( + assertType::Enum at, + const char *file, + int line, + const char *expr, + const char *exception_type, + const Contains &exception_string +) + : AssertData(at, file, line, expr, exception_type, exception_string) {} + +void ResultBuilder::setResult(const Result &res) { + m_decomp = res.m_decomp; + m_failed = !res.m_passed; +} + +void ResultBuilder::translateException() { + m_threw = true; + m_exception = translateActiveException(); +} + +bool ResultBuilder::log() { + if (m_at & assertType::is_throws) { + m_failed = !m_threw; + } else if ((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { + m_failed = !m_threw_as || !m_exception_string.check(m_exception); + } else if (m_at & assertType::is_throws_as) { + m_failed = !m_threw_as; + } else if (m_at & assertType::is_throws_with) { + m_failed = !m_exception_string.check(m_exception); + } else if (m_at & assertType::is_nothrow) { + m_failed = m_threw; + } + + if (m_exception.size()) + m_exception = "\"" + m_exception + "\""; + + if (is_running_in_test) { + addAssert(m_at); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); + + if (m_failed) + addFailedAssert(m_at); + } else if (m_failed) { + failed_out_of_a_testing_context(*this); + } + + return m_failed && isDebuggerActive() && !getContextOptions()->no_breaks && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger +} + +void ResultBuilder::react() const { + if (m_failed && checkIfShouldThrow(m_at)) + throwException(); +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_EXCEPTIONS +#define DOCTEST_PARTS_PRIVATE_EXCEPTIONS + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +template +DOCTEST_NORETURN void throw_exception(const Ex &e) { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + throw e; +#else // DOCTEST_CONFIG_NO_EXCEPTIONS +#ifdef DOCTEST_CONFIG_HANDLE_EXCEPTION + DOCTEST_CONFIG_HANDLE_EXCEPTION(e); +#else // DOCTEST_CONFIG_HANDLE_EXCEPTION +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + std::cerr << "doctest will terminate because it needed to throw an exception.\n" + << "The message was: " << e.what() << '\n'; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#endif // DOCTEST_CONFIG_HANDLE_EXCEPTION + std::terminate(); +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} + +#ifndef DOCTEST_INTERNAL_ERROR +#define DOCTEST_INTERNAL_ERROR(msg) \ + detail::throw_exception(std::logic_error(__FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) +#endif // DOCTEST_INTERNAL_ERROR +} // namespace detail + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_EXCEPTIONS + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +const char *assertString(assertType::Enum at) { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4061) // enum 'x' in switch of enum 'y' is not explicitly handled +#define DOCTEST_GENERATE_ASSERT_TYPE_CASE(assert_type) \ + case assertType::DT_##assert_type: return #assert_type +#define DOCTEST_GENERATE_ASSERT_TYPE_CASES(assert_type) \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN_##assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK_##assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE_##assert_type) + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") + DOCTEST_GCC_SUPPRESS_WARNING_PUSH + DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") + switch (at) { + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(FALSE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NOTHROW); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(EQ); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY_FALSE); + + default: DOCTEST_INTERNAL_ERROR("Tried stringifying invalid assert type!"); + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP +} + +const char *failureString(assertType::Enum at) { + if (at & assertType::is_warn) + return "WARNING"; + if (at & assertType::is_check) + return "ERROR"; + if (at & assertType::is_require) + return "FATAL ERROR"; + return ""; +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#if !defined(DOCTEST_CONFIG_COLORS_NONE) +#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_CONFIG_COLORS_WINDOWS +#else // linux +#define DOCTEST_CONFIG_COLORS_ANSI +#endif // platform +#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI +#endif // DOCTEST_CONFIG_COLORS_NONE + +namespace doctest { + +namespace detail { +void color_to_stream(std::ostream &, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) +} // namespace detail + +namespace Color { +std::ostream &operator<<(std::ostream &s, Color::Enum code) { + detail::color_to_stream(s, code); + return s; +} +} // namespace Color + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") +void color_to_stream(std::ostream &s, Color::Enum code) { + static_cast(s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS + static_cast(code); // for DOCTEST_CONFIG_COLORS_NONE +#ifdef DOCTEST_CONFIG_COLORS_ANSI + if (g_no_colors || (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) + return; + + auto col = ""; // NOLINT(clang-analyzer-deadcode.DeadStores) + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") + switch (code) { + case Color::Red: col = "[0;31m"; break; + case Color::Green: col = "[0;32m"; break; + case Color::Blue: col = "[0;34m"; break; + case Color::Cyan: col = "[0;36m"; break; + case Color::Yellow: col = "[0;33m"; break; + case Color::Grey: col = "[1;30m"; break; + case Color::LightGrey: col = "[0;37m"; break; + case Color::BrightRed: col = "[1;31m"; break; + case Color::BrightGreen: col = "[1;32m"; break; + case Color::BrightWhite: col = "[1;37m"; break; + case Color::Bright: // invalid + case Color::None: + case Color::White: + default: col = "[0m"; + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + s << "\033" << col; +#endif // DOCTEST_CONFIG_COLORS_ANSI + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + if (g_no_colors || (_isatty(_fileno(stdout)) == false && getContextOptions()->force_colors == false)) + return; + + static struct ConsoleHelper { + HANDLE stdoutHandle; + WORD origFgAttrs; + WORD origBgAttrs; + + ConsoleHelper() { + stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO csbiInfo; + GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo); + origFgAttrs = + csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_INTENSITY); + origBgAttrs = + csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY); + } + } ch; + +#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(ch.stdoutHandle, x | ch.origBgAttrs) + + // clang-format off + switch (code) { + case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; + case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; + case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; + case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; + case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; + case Color::Grey: DOCTEST_SET_ATTR(0); break; + case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; + case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; + case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; + case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::None: + case Color::Bright: // invalid + default: DOCTEST_SET_ATTR(ch.origFgAttrs); + } + // clang-format on +#endif // DOCTEST_CONFIG_COLORS_WINDOWS +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLED +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +const ContextOptions *getContextOptions() { + return DOCTEST_BRANCH_ON_DISABLED(nullptr, detail::g_cs); +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_COMMON +#define DOCTEST_PARTS_PRIVATE_REPORTERS_COMMON + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX +#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" +#endif + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +void fulltext_log_assert_to_stream(std::ostream &s, const AssertData &rb); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_COMMON +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_DEBUG_OUTPUT_WINDOW +#define DOCTEST_PARTS_PRIVATE_REPORTERS_DEBUG_OUTPUT_WINDOW + +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_CONSOLE +#define DOCTEST_PARTS_PRIVATE_REPORTERS_CONSOLE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS +#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX +#else +#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" +#endif + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +struct Whitespace { + int nrSpaces; + explicit Whitespace(int nr); +}; + +std::ostream &operator<<(std::ostream &out, const Whitespace &ws); + +struct ConsoleReporter : public IReporter { + std::ostream &s; + bool hasLoggedCurrentTestStart; + std::vector subcasesStack; + size_t currentSubcaseLevel; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions &opt; + const TestCaseData *tc; + + ConsoleReporter(const ContextOptions &co); + + ConsoleReporter(const ContextOptions &co, std::ostream &ostr); + + // ========================================================================================= + // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE + // ========================================================================================= + + void separator_to_stream(); + + static const char *getSuccessOrFailString(bool success, assertType::Enum at, const char *success_str); + + static Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at); + + void successOrFailColoredStringToStream(bool success, assertType::Enum at, const char *success_str = "SUCCESS"); + + void log_contexts(); + + // this was requested to be made virtual so users could override it + virtual void file_line_to_stream(const char *file, int line, const char *tail = ""); + + void logTestStart(); + + void printVersion(); + + void printIntro(); + + void printHelp(); + + void printRegisteredReporters(); + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData &in) override; + + void test_run_start() override; + + void test_run_end(const TestRunStats &p) override; + + void test_case_start(const TestCaseData &in) override; + + void test_case_reenter(const TestCaseData &) override; + + void test_case_end(const CurrentTestCaseStats &st) override; + + void test_case_exception(const TestCaseException &e) override; + + void subcase_start(const SubcaseSignature &subc) override; + + void subcase_end() override; + + void log_assert(const AssertData &rb) override; + + void log_message(const MessageData &mb) override; + + void test_case_skipped(const TestCaseData &) override; +}; + +DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_CONSOLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_PLATFORM_WINDOWS +struct DebugOutputWindowReporter : public ConsoleReporter { + DOCTEST_THREAD_LOCAL static std::ostringstream oss; + + DebugOutputWindowReporter(const ContextOptions &co); + + void test_run_start() override; + void test_run_end(const TestRunStats &in) override; + void test_case_start(const TestCaseData &in) override; + void test_case_reenter(const TestCaseData &in) override; + void test_case_end(const CurrentTestCaseStats &in) override; + void test_case_exception(const TestCaseException &in) override; + void subcase_start(const SubcaseSignature &in) override; + void subcase_end(DOCTEST_EMPTY DOCTEST_EMPTY) override; + void log_assert(const AssertData &in) override; + void log_message(const MessageData &in) override; + void test_case_skipped(const TestCaseData &in) override; +}; +#endif // DOCTEST_PLATFORM_WINDOWS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_DEBUG_OUTPUT_WINDOW +#ifndef DOCTEST_PARTS_PRIVATE_TEST_CASE +#define DOCTEST_PARTS_PRIVATE_TEST_CASE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// all the registered tests +std::set &getRegisteredTests(); + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_TEST_CASE +#ifndef DOCTEST_PARTS_PRIVATE_FILTERS +#define DOCTEST_PARTS_PRIVATE_FILTERS + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// matching of a string against a wildcard mask (case sensitivity configurable) taken from +// https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing +int wildcmp(const char *str, const char *wild, bool caseSensitive); + +// checks if the name matches any of the filters (and can be configured what to do when empty) +bool matchesAny(const char *name, const std::vector &filters, bool matchEmpty, bool caseSensitive); + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_FILTERS +#ifndef DOCTEST_PARTS_PRIVATE_SIGNALS +#define DOCTEST_PARTS_PRIVATE_SIGNALS + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +struct FatalConditionHandler { + static void reset(); + static void allocateAltStackMem(); + static void freeAltStackMem(); +}; +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +#ifdef DOCTEST_PLATFORM_WINDOWS + +struct SignalDefs { + DWORD id; + const char *name; +}; + +struct FatalConditionHandler { + static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo); + static void allocateAltStackMem(); + static void freeAltStackMem(); + + FatalConditionHandler(); + + static void reset(); + + ~FatalConditionHandler(); + +private: + static UINT prev_error_mode_1; + static int prev_error_mode_2; + static unsigned int prev_abort_behavior; + static int prev_report_mode; + static _HFILE prev_report_file; + static void(DOCTEST_CDECL *prev_sigabrt_handler)(int); + static std::terminate_handler original_terminate_handler; + static bool isSet; + static ULONG guaranteeSize; + static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; +}; + +#else // DOCTEST_PLATFORM_WINDOWS + +struct SignalDefs { + int id; + const char *name; +}; + +struct FatalConditionHandler { + static bool isSet; + static struct sigaction oldSigActions[6]; + static stack_t oldSigStack; + static size_t altStackSize; + static char *altStackMem; + + static void handleSignal(int sig); + + static void allocateAltStackMem(); + + static void freeAltStackMem(); + + FatalConditionHandler(); + + ~FatalConditionHandler(); + static void reset(); +}; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_SIGNALS + +// Fix for #1035 +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_JUNIT +#define DOCTEST_PARTS_PRIVATE_REPORTERS_JUNIT + +#ifndef DOCTEST_PARTS_PRIVATE_XML +#define DOCTEST_PARTS_PRIVATE_XML + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= +/* clang-format off */ /* NOLINTBEGIN */ + + class XmlEncode { + public: + enum ForWhat { ForTextNodes, ForAttributes }; + + XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); + + void encodeTo( std::ostream& os ) const; + + friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); + + private: + std::string m_str; + ForWhat m_forWhat; + }; + + class XmlWriter { + public: + + class ScopedElement { + public: + ScopedElement( XmlWriter* writer ); + + ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT; + ScopedElement& operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT; + + ~ScopedElement(); + + ScopedElement& writeText( std::string const& text, bool indent = true ); + + template + ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { + m_writer->writeAttribute( name, attribute ); + return *this; + } + + private: + mutable XmlWriter* m_writer = nullptr; + }; + +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os = std::cout ); +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os ); +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + ~XmlWriter(); + + XmlWriter( XmlWriter const& ) = delete; + XmlWriter& operator=( XmlWriter const& ) = delete; + + XmlWriter& startElement( std::string const& name ); + + ScopedElement scopedElement( std::string const& name ); + + XmlWriter& endElement(); + + XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); + + XmlWriter& writeAttribute( std::string const& name, const char* attribute ); + + XmlWriter& writeAttribute( std::string const& name, bool attribute ); + + template + XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { + std::stringstream rss; + rss << attribute; + return writeAttribute( name, rss.str() ); + } + + XmlWriter& writeText( std::string const& text, bool indent = true ); + + //XmlWriter& writeComment( std::string const& text ); + + //void writeStylesheetRef( std::string const& url ); + + //XmlWriter& writeBlankLine(); + + void ensureTagClosed(); + + void writeDeclaration(); + + private: + + void newlineIfNecessary(); + + bool m_tagIsOpen = false; + bool m_needsNewline = false; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +/* clang-format on */ /* NOLINTEND */ +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_XML + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +// TODO: +// - log_message() +// - respond to queries +// - honor remaining options +// - more attributes in tags +struct JUnitReporter : public IReporter { + detail::XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + detail::Timer timer; + std::vector deepestSubcaseStackNames; + + struct JUnitTestCaseData { + static std::string getCurrentTimestamp(); + + struct JUnitTestMessage { + JUnitTestMessage(const std::string &_message, const std::string &_type, const std::string &_details); + + JUnitTestMessage(const std::string &_message, const std::string &_details); + + std::string message, type, details; + }; + + struct JUnitTestCase { + JUnitTestCase(const std::string &_classname, const std::string &_name); + + std::string classname, name; + double time; + std::vector failures, errors; + }; + + void add(const std::string &classname, const std::string &name); + + void appendSubcaseNamesToLastTestcase(std::vector nameStack); + + void addTime(double time); + + void addFailure(const std::string &message, const std::string &type, const std::string &details); + + void addError(const std::string &message, const std::string &details); + + std::vector testcases; + double totalSeconds = 0; + int totalErrors = 0, totalFailures = 0; + }; + + JUnitTestCaseData testCaseData; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions &opt; + const TestCaseData *tc = nullptr; + + JUnitReporter(const ContextOptions &co); + + unsigned line(unsigned l) const; + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData &) override; + + void test_run_start() override; + + void test_run_end(const TestRunStats &p) override; + + void test_case_start(const TestCaseData &in) override; + + void test_case_reenter(const TestCaseData &in) override; + + void test_case_end(const CurrentTestCaseStats &) override; + + void test_case_exception(const TestCaseException &e) override; + + void subcase_start(const SubcaseSignature &in) override; + + void subcase_end() override; + + void log_assert(const AssertData &rb) override; + + void log_message(const MessageData &mb) override; + + void test_case_skipped(const TestCaseData &) override; + + static void log_contexts(std::ostringstream &s); +}; + +DOCTEST_REGISTER_REPORTER("junit", 0, JUnitReporter); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_JUNIT +#ifndef DOCTEST_PARTS_PRIVATE_REPORTERS_XML +#define DOCTEST_PARTS_PRIVATE_REPORTERS_XML + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +struct XmlReporter : public IReporter { + detail::XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions &opt; + const TestCaseData *tc = nullptr; + + XmlReporter(const ContextOptions &co); + + void log_contexts(); + + unsigned line(unsigned l) const; + + void test_case_start_impl(const TestCaseData &in); + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData &in) override; + + void test_run_start() override; + + void test_run_end(const TestRunStats &p) override; + + void test_case_start(const TestCaseData &in) override; + + void test_case_reenter(const TestCaseData &) override; + + void test_case_end(const CurrentTestCaseStats &st) override; + + void test_case_exception(const TestCaseException &e) override; + + void subcase_start(const SubcaseSignature &in) override; + + void subcase_end() override; + + void log_assert(const AssertData &rb) override; + + void log_message(const MessageData &mb) override; + + void test_case_skipped(const TestCaseData &in) override; +}; + +DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_REPORTERS_XML + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +bool is_running_in_test = false; + +#ifdef DOCTEST_CONFIG_DISABLE + +// NOLINTBEGIN(readability-convert-member-functions-to-static) +Context::Context(int, const char *const *) {} +Context::~Context() = default; +void Context::applyCommandLine(int, const char *const *) {} +void Context::addFilter(const char *, const char *) {} +void Context::clearFilters() {} +void Context::setOption(const char *, bool) {} +void Context::setOption(const char *, int) {} +void Context::setOption(const char *, const char *) {} +bool Context::shouldExit() { + return false; +} +void Context::setAsDefaultForAssertsOutOfTestCases() {} +void Context::setAssertHandler(detail::assert_handler) {} +void Context::setCout(std::ostream *) {} +int Context::run() { + return 0; +} +// NOLINTEND(readability-convert-member-functions-to-static) + +#else + +namespace detail { +// for sorting tests by file/line +bool fileOrderComparator(const TestCase *lhs, const TestCase *rhs) { + // this is needed because MSVC gives different case for drive letters + // for __FILE__ when evaluated in a header and a source file + const int res = lhs->m_file.compare(rhs->m_file, static_cast(DOCTEST_MSVC)); + if (res != 0) + return res < 0; + if (lhs->m_line != rhs->m_line) + return lhs->m_line < rhs->m_line; + return lhs->m_template_id < rhs->m_template_id; +} + +// for sorting tests by suite/file/line +bool suiteOrderComparator(const TestCase *lhs, const TestCase *rhs) { + const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); + if (res != 0) + return res < 0; + return fileOrderComparator(lhs, rhs); +} + +// for sorting tests by name/suite/file/line +bool nameOrderComparator(const TestCase *lhs, const TestCase *rhs) { + const int res = std::strcmp(lhs->m_name, rhs->m_name); + if (res != 0) + return res < 0; + return suiteOrderComparator(lhs, rhs); +} + +// the implementation of parseOption() +bool parseOptionImpl(int argc, const char *const *argv, const char *pattern, String *value) { + // going from the end to the beginning and stopping on the first occurrence from the end + for (int i = argc; i > 0; --i) { + auto index = i - 1; + auto temp = std::strstr(argv[index], pattern); + if (temp && (value || strlen(temp) == strlen(pattern))) { + // eliminate matches in which the chars before the option are not '-' + bool noBadCharsFound = true; + auto curr = argv[index]; + while (curr != temp) { + if (*curr++ != '-') { + noBadCharsFound = false; + break; + } + } + if (noBadCharsFound && argv[index][0] == '-') { + if (value) { + // parsing the value of an option + temp += strlen(pattern); + const unsigned len = strlen(temp); + if (len) { + *value = temp; + return true; + } + } else { + // just a flag - no value + return true; + } + } + } + } + return false; +} + +// parses an option and returns the string after the '=' character +bool parseOption( + int argc, const char *const *argv, const char *pattern, String *value = nullptr, const String &defaultVal = String() +) { + if (value) + *value = defaultVal; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + // offset (normally 3 for "dt-") to skip prefix + if (parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) + return true; +#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + return parseOptionImpl(argc, argv, pattern, value); +} + +// locates a flag on the command line +bool parseFlag(int argc, const char *const *argv, const char *pattern) { + return parseOption(argc, argv, pattern); +} + +// parses a comma separated list of words after a pattern in one of the arguments in argv +bool parseCommaSepArgs(int argc, const char *const *argv, const char *pattern, std::vector &res) { + String filtersString; + if (parseOption(argc, argv, pattern, &filtersString)) { + // tokenize with "," as a separator, unless escaped with backslash + std::ostringstream s; + auto flush = [&s, &res]() { + auto string = s.str(); + if (!string.empty()) { + res.emplace_back(string.c_str()); + } + s.str(""); + }; + + bool seenBackslash = false; + const char *current = filtersString.c_str(); + const char *end = current + strlen(current); + while (current != end) { + const char character = *current++; + if (seenBackslash) { + seenBackslash = false; + if (character == ',' || character == '\\') { + s.put(character); + continue; + } + s.put('\\'); + } + if (character == '\\') { + seenBackslash = true; + } else if (character == ',') { + flush(); + } else { + s.put(character); + } + } + + if (seenBackslash) { + s.put('\\'); + } + flush(); + return true; + } + return false; +} + +enum optionType { option_bool, option_int }; + +// parses an int/bool option from the command line +bool parseIntOption(int argc, const char *const *argv, const char *pattern, optionType type, int &res) { + String parsedValue; + if (!parseOption(argc, argv, pattern, &parsedValue)) + return false; + + if (type) { + // integer + // TODO: change this to use std::stoi or something else! currently it uses undefined + // behavior - assumes '0' on failed parse... + // NOLINTNEXTLINE(bugprone-unchecked-string-to-number-conversion, cert-err34-c) + const int theInt = std::atoi(parsedValue.c_str()); + if (theInt != 0) { + res = theInt; + return true; + } + } else { + // boolean + const char positive[][5] = {"1", "true", "on", "yes"}; // 5 - strlen("true") + 1 + const char negative[][6] = {"0", "false", "off", "no"}; // 6 - strlen("false") + 1 + + // if the value matches any of the positive/negative possibilities + for (unsigned i = 0; i < 4; i++) { + if (parsedValue.compare(positive[i], true) == 0) { + res = 1; + return true; + } + if (parsedValue.compare(negative[i], true) == 0) { + res = 0; + return true; + } + } + } + return false; +} + +} // namespace detail + +Context::Context(int argc, const char *const *argv) + : p(new detail::ContextState) { + parseArgs(argc, argv, true); + if (argc) + p->binary_name = argv[0]; +} + +Context::~Context() { + if (detail::g_cs == p) + detail::g_cs = nullptr; + delete p; +} + +void Context::applyCommandLine(int argc, const char *const *argv) { + parseArgs(argc, argv); + if (argc) + p->binary_name = argv[0]; +} + +// parses args +void Context::parseArgs(int argc, const char *const *argv, bool withDefaults) { + using namespace detail; + + // clang-format off + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); + // clang-format on + + int intRes = 0; + String strRes; + +#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ + if (parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ + p->var = static_cast(intRes); \ + else if ( \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname) \ + ) \ + p->var = true; \ + else if (withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ + if (parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ + p->var = intRes; \ + else if (withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ + if (parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ + parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || withDefaults) \ + p->var = strRes + + DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); + DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); + DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); + + DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); + DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); + + DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); + DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); + + DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("minimal", "m", minimal, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("quiet", "q", quiet, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-intro", "ni", no_intro, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); + DOCTEST_PARSE_STR_OPTION("strip-file-prefixes", "sfp", strip_file_prefixes, ""); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-debug-output", "ndo", no_debug_output, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-time-in-output", "ntio", no_time_in_output, false); + + if (withDefaults) { + p->help = false; + p->version = false; + p->count = false; + p->list_test_cases = false; + p->list_test_suites = false; + p->list_reporters = false; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { + p->help = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { + p->version = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { + p->count = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { + p->list_test_cases = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { + p->list_test_suites = true; + p->exit = true; + } + if (parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { + p->list_reporters = true; + p->exit = true; + } +} + +// allows the user to add procedurally to the filters from the command line +void Context::addFilter(const char *filter, const char *value) { + setOption(filter, value); +} + +// allows the user to clear all filters from the command line +void Context::clearFilters() { + for (auto &curr: p->filters) + curr.clear(); +} + +// allows the user to override procedurally the bool options from the command line +void Context::setOption(const char *option, bool value) { + setOption(option, value ? "true" : "false"); +} + +// allows the user to override procedurally the int options from the command line +void Context::setOption(const char *option, int value) { + setOption(option, toString(value).c_str()); +} + +// allows the user to override procedurally the string options from the command line +void Context::setOption(const char *option, const char *value) { + auto argv = String("-") + option + "=" + value; + auto lvalue = argv.c_str(); + parseArgs(1, &lvalue); +} + +// users should query this in their main() and exit the program if true +bool Context::shouldExit() { + return p->exit; +} + +void Context::setAsDefaultForAssertsOutOfTestCases() { + detail::g_cs = p; +} + +void Context::setAssertHandler(detail::assert_handler ah) { + p->ah = ah; +} + +void Context::setCout(std::ostream *out) { + p->cout = out; +} + +static class DiscardOStream : public std::ostream { +private: + class : public std::streambuf { + private: + // allowing some buffering decreases the amount of calls to overflow + char buf[1024]; + + protected: + std::streamsize xsputn(const char_type *, std::streamsize count) override { + return count; + } + + int_type overflow(int_type ch) override { + setp(std::begin(buf), std::end(buf)); + return traits_type::not_eof(ch); + } + } discardBuf; + +public: + DiscardOStream() noexcept + : std::ostream(&discardBuf) {} +} discardOut; + +// the main function that does all the filtering and test running +int Context::run() { + using namespace detail; + + // save the old context state in case such was setup - for using asserts out of a testing context + auto old_cs = g_cs; + // this is the current contest + g_cs = p; + is_running_in_test = true; + + g_no_colors = p->no_colors; + p->resetRunData(); + + std::fstream fstr; + if (p->cout == nullptr) { + if (p->quiet) { + p->cout = &discardOut; + } else if (p->out.size()) { + // to a file if specified + fstr.open(p->out.c_str(), std::fstream::out); + p->cout = &fstr; + if (!fstr.is_open()) { + // clang-format off + std::cerr << Color::Cyan << "[doctest] " << Color::None << "Could not open " << p->out << " for writing!" << std::endl; + std::cerr << Color::Cyan << "[doctest] " << Color::None << "Defaulting to std::cout instead" << std::endl; + p->cout = &std::cout; + // clang-format on + } + + } else { +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + // stdout by default + p->cout = &std::cout; +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + return EXIT_FAILURE; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + } + } + + FatalConditionHandler::allocateAltStackMem(); + + auto cleanup_and_return = [&]() { + FatalConditionHandler::freeAltStackMem(); + + if (fstr.is_open()) + fstr.close(); + + // restore context + g_cs = old_cs; + is_running_in_test = false; + + // we have to free the reporters which were allocated when the run started + for (auto &curr: p->reporters_currently_used) + delete curr; + p->reporters_currently_used.clear(); + + if (p->numTestCasesFailed && !p->no_exitcode) + return EXIT_FAILURE; + return EXIT_SUCCESS; + }; + + // setup default reporter if none is given through the command line + if (p->filters[8].empty()) + p->filters[8].emplace_back("console"); + + // check to see if any of the registered reporters has been selected + for (auto &curr: getReporters()) { + if (matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) + p->reporters_currently_used.push_back(curr.second(*g_cs)); + } + + // TODO: check if there is nothing in reporters_currently_used + + // prepend all listeners + for (auto &curr: getListeners()) + p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); + +#ifdef DOCTEST_PLATFORM_WINDOWS + if (isDebuggerActive() && p->no_debug_output == false) + p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); +#endif // DOCTEST_PLATFORM_WINDOWS + + // handle version, help and no_run + if (p->no_run || p->version || p->help || p->list_reporters) { + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); + + return cleanup_and_return(); + } + + std::vector testArray; + for (auto &curr: getRegisteredTests()) + testArray.push_back(&curr); + p->numTestCases = testArray.size(); + + // sort the collected records + if (!testArray.empty()) { + if (p->order_by.compare("file", true) == 0) { + std::sort(testArray.begin(), testArray.end(), fileOrderComparator); + } else if (p->order_by.compare("suite", true) == 0) { + std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); + } else if (p->order_by.compare("name", true) == 0) { + std::sort(testArray.begin(), testArray.end(), nameOrderComparator); + } else if (p->order_by.compare("rand", true) == 0) { + std::srand(p->rand_seed); + + // random_shuffle implementation + const auto first = testArray.data(); + for (size_t i = testArray.size() - 1; i > 0; --i) { + // NOLINTNEXTLINE(cert-msc30-c, cert-msc50-cpp, concurrency-mt-unsafe, misc-predictable-rand) + const int idxToSwap = static_cast(std::rand() % (i + 1)); + + const auto temp = first[i]; + + first[i] = first[idxToSwap]; + first[idxToSwap] = temp; + } + } else if (p->order_by.compare("none", true) == 0) { + // means no sorting - beneficial for death tests which call into the executable + // with a specific test case in mind - we don't want to slow down the startup times + } + } + + std::set testSuitesPassingFilt; + + const bool query_mode = p->count || p->list_test_cases || p->list_test_suites; + std::vector queryResults; + + if (!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); + + // invoke the registered functions if they match the filter criteria (or just count them) + for (auto &curr: testArray) { + const auto &tc = *curr; + + bool skip_me = false; + if (tc.m_skip && !p->no_skip) + skip_me = true; + + if (!matchesAny(tc.m_file.c_str(), p->filters[0], true, p->case_sensitive)) + skip_me = true; + if (matchesAny(tc.m_file.c_str(), p->filters[1], false, p->case_sensitive)) + skip_me = true; + if (!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) + skip_me = true; + if (matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) + skip_me = true; + if (!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) + skip_me = true; + if (matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) + skip_me = true; + + if (!skip_me) + p->numTestCasesPassingFilters++; + + // skip the test if it is not in the execution range + if ((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || + (p->first > p->numTestCasesPassingFilters)) + skip_me = true; + + if (skip_me) { + if (!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); + continue; + } + + // do not execute the test if we are to only count the number of filter passing tests + if (p->count) + continue; + + // print the name of the test and don't execute it + if (p->list_test_cases) { + queryResults.push_back(&tc); + continue; + } + + // print the name of the test suite if not done already and don't execute it + if (p->list_test_suites) { + if ((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { + queryResults.push_back(&tc); + testSuitesPassingFilt.insert(tc.m_test_suite); + p->numTestSuitesPassingFilters++; + } + continue; + } + + // execute the test if it passes all the filtering + { + p->currentTest = &tc; + + p->failure_flags = TestCaseFailureReason::None; + p->seconds = 0; + + // reset atomic counters + p->numAssertsFailedCurrentTest_atomic = 0; + p->numAssertsCurrentTest_atomic = 0; + + p->traversal.resetForTestCase(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); + + p->timer.start(); + + bool run_test = true; + + do { // NOLINT(cppcoreguidelines-avoid-do-while) + // Reset per-run traversal data while keeping the current decision path prefix. + p->traversal.resetForRun(); + + p->shouldLogCurrentException = true; + + // reset stuff for logging with INFO() + p->stringifiedContexts.clear(); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + // MSVC 2015 diagnoses fatalConditionHandler as unused (because reset() is a + // static method) + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4101) // unreferenced local variable + const FatalConditionHandler fatalConditionHandler; // Handle signals + static_cast(fatalConditionHandler); + // execute the test + tc.m_test(); + FatalConditionHandler::reset(); + DOCTEST_MSVC_SUPPRESS_WARNING_POP +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + } catch (const TestFailureException &) { + p->failure_flags |= TestCaseFailureReason::AssertFailure; + } catch (...) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {translateActiveException(), false}); + p->failure_flags |= TestCaseFailureReason::Exception; + } +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + + // exit this loop if enough assertions have failed - even if there are more subcases + if (p->abort_after > 0 && + p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { + run_test = false; + p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; + } + + const bool has_next_path = run_test ? p->traversal.advance() : false; + + if (has_next_path && run_test) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); + if (!has_next_path) + run_test = false; + } while (run_test); + + p->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + p->currentTest = nullptr; + + // stop executing tests if enough assertions have failed + if (p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) + break; + } + } + + if (!query_mode) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } else { + QueryData qdata; + qdata.run_stats = g_cs; + qdata.data = queryResults.data(); + qdata.num_data = static_cast(queryResults.size()); + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); + } + + return cleanup_and_return(); +} + +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP +#ifndef DOCTEST_PARTS_PRIVATE_CONTEXT_SCOPE +#define DOCTEST_PARTS_PRIVATE_CONTEXT_SCOPE + + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +extern DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // DOCTEST_PARTS_PRIVATE_CONTEXT_SCOPE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_DEFINE_INTERFACE(IContextScope) + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { +DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() + +ContextScopeBase::ContextScopeBase() { + g_infoContexts.push_back(this); +} + +ContextScopeBase::ContextScopeBase(ContextScopeBase &&other) noexcept { + if (other.need_to_destroy) { + other.destroy(); + } + other.need_to_destroy = false; + g_infoContexts.push_back(this); +} + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") +// destroy cannot be inlined into the destructor because that would mean calling stringify after +// ContextScope has been destroyed (base class destructors run after derived class destructors). +// Instead, ContextScope calls this method directly from its destructor. +void ContextScopeBase::destroy() { +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && \ + (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if (std::uncaught_exceptions() > 0) { +#else + if (std::uncaught_exception()) { +#endif + std::ostringstream s; + this->stringify(&s); + g_cs->stringifiedContexts.emplace_back(s.str().c_str()); + } + g_infoContexts.pop_back(); +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +ContextState *g_cs = nullptr; +DOCTEST_THREAD_LOCAL bool g_no_colors; + +void ContextState::resetRunData() { + numTestCases = 0; + numTestCasesPassingFilters = 0; + numTestSuitesPassingFilters = 0; + numTestCasesFailed = 0; + numAsserts = 0; + numAssertsFailed = 0; + numAssertsCurrentTest = 0; + numAssertsFailedCurrentTest = 0; +} + +void ContextState::finalizeTestCaseData() { + seconds = timer.getElapsedSeconds(); + + // update the non-atomic counters + numAsserts += numAssertsCurrentTest_atomic; + numAssertsFailed += numAssertsFailedCurrentTest_atomic; + numAssertsCurrentTest = numAssertsCurrentTest_atomic; + numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; + + if (numAssertsFailedCurrentTest) + failure_flags |= TestCaseFailureReason::AssertFailure; + + if (Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && + Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) + failure_flags |= TestCaseFailureReason::Timeout; + + if (currentTest->m_should_fail) { + if (failure_flags) { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; + } else { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; + } + } else if (failure_flags && currentTest->m_may_fail) { + failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; + } else if (currentTest->m_expected_failures > 0) { + if (numAssertsFailedCurrentTest == currentTest->m_expected_failures) { + failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; + } else { + failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; + } + } + + const bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); + + // if any subcase has failed - the whole test case has failed + testCaseSuccess = !(failure_flags && !ok_to_fail); + if (!testCaseSuccess) + numTestCasesFailed++; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { +#ifdef DOCTEST_IS_DEBUGGER_ACTIVE +bool isDebuggerActive() { + return DOCTEST_IS_DEBUGGER_ACTIVE(); +} +#else // DOCTEST_IS_DEBUGGER_ACTIVE +#ifdef DOCTEST_PLATFORM_LINUX +class ErrnoGuard { +public: + ErrnoGuard() + : m_oldErrno(errno) {} + ~ErrnoGuard() { + errno = m_oldErrno; + } + +private: + int m_oldErrno; +}; +// See the comments in Catch2 for the reasoning behind this implementation: +// https://github.com/catchorg/Catch2/blob/v2.13.1/include/internal/catch_debugger.cpp#L79-L102 +bool isDebuggerActive() { + const ErrnoGuard guard; + std::ifstream in("/proc/self/status"); + for (std::string line; std::getline(in, line);) { + static const int PREFIX_LEN = 11; + if (line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) { + return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0'; + } + } + return false; +} +#elif defined(DOCTEST_PLATFORM_MAC) +// The following function is taken directly from the following technical note: +// https://developer.apple.com/library/archive/qa/qa1361/_index.html +// Returns true if the current process is being debugged (either +// running under the debugger or has a debugger attached post facto). +bool isDebuggerActive() { + int mib[4]; + kinfo_proc info; + size_t size; + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + // Call sysctl. + size = sizeof(info); + if (sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, nullptr, 0) != 0) { + std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; + return false; + } + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); +} +#elif DOCTEST_MSVC || defined(__MINGW32__) || defined(__MINGW64__) +bool isDebuggerActive() { + return ::IsDebuggerPresent() != 0; +} +#else +bool isDebuggerActive() { + return false; +} +#endif // Platform +#endif // DOCTEST_IS_DEBUGGER_ACTIVE +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +DOCTEST_DEFINE_INTERFACE(IExceptionTranslator) + +void registerExceptionTranslatorImpl(const IExceptionTranslator *et) noexcept { + if (std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == + getExceptionTranslators().end()) + getExceptionTranslators().push_back(et); +} + +std::vector &getExceptionTranslators() noexcept { + static std::vector data; + return data; +} + +String translateActiveException() noexcept { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + String res; + auto &translators = getExceptionTranslators(); + for (auto &curr: translators) + if (curr->translate(res)) + return res; + // clang-format off + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") + try { + throw; + } catch (std::exception &ex) { + return ex.what(); + } catch (std::string &msg) { + return msg.c_str(); + } catch (const char *msg) { + return msg; + } catch (...) { + return "unknown exception"; + } + DOCTEST_GCC_SUPPRESS_WARNING_POP + // clang-format on +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + return ""; +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +bool checkIfShouldThrow(assertType::Enum at) { + if (at & assertType::is_require) + return true; + + if ((at & assertType::is_check) && getContextOptions()->abort_after > 0 && + (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= getContextOptions()->abort_after) + return true; + + return false; +} + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +DOCTEST_NORETURN void throwException() { + g_cs->shouldLogCurrentException = false; + throw TestFailureException(); // NOLINT(hicpp-exception-baseclass) +} +#else // DOCTEST_CONFIG_NO_EXCEPTIONS +void throwException() {} +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +int wildcmp(const char *str, const char *wild, bool caseSensitive) { + const char *cp = str; + const char *mp = wild; + + while ((*str) && (*wild != '*')) { + if ((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && (*wild != '?')) { + return 0; + } + wild++; + str++; + } + + while (*str) { + if (*wild == '*') { + if (!*++wild) { + return 1; + } + mp = wild; + cp = str + 1; + } else if ((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || (*wild == '?')) { + wild++; + str++; + } else { + wild = mp; + str = cp++; + } + } + + while (*wild == '*') { + wild++; + } + return !*wild; +} + +bool matchesAny(const char *name, const std::vector &filters, bool matchEmpty, bool caseSensitive) { + if (filters.empty() && matchEmpty) + return true; + for (auto &curr: filters) + if (wildcmp(name, curr.c_str(), caseSensitive)) + return true; + return false; +} + +} // namespace detail +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 +int main(int argc, char **argv) { + return doctest::Context(argc, argv).run(); +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +Approx::Approx(double value) + : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100), m_scale(1.0), m_value(value) {} + +Approx Approx::operator()(double value) const { + Approx approx(value); + approx.epsilon(m_epsilon); + approx.scale(m_scale); + return approx; +} + +Approx &Approx::epsilon(double newEpsilon) { + m_epsilon = newEpsilon; + return *this; +} +Approx &Approx::scale(double newScale) { + m_scale = newScale; + return *this; +} + +bool operator==(double lhs, const Approx &rhs) { + // Thanks to Richard Harris for his help refining this formula + return std::fabs(lhs - rhs.m_value) < + rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); +} + +bool operator==(const Approx &lhs, double rhs) { + return operator==(rhs, lhs); +} + +bool operator!=(double lhs, const Approx &rhs) { + return !operator==(lhs, rhs); +} + +bool operator!=(const Approx &lhs, double rhs) { + return !operator==(rhs, lhs); +} + +bool operator<=(double lhs, const Approx &rhs) { + return lhs < rhs.m_value || lhs == rhs; +} + +bool operator<=(const Approx &lhs, double rhs) { + return lhs.m_value < rhs || lhs == rhs; +} + +bool operator>=(double lhs, const Approx &rhs) { + return lhs > rhs.m_value || lhs == rhs; +} + +bool operator>=(const Approx &lhs, double rhs) { + return lhs.m_value > rhs || lhs == rhs; +} + +bool operator<(double lhs, const Approx &rhs) { + return lhs < rhs.m_value && lhs != rhs; +} + +bool operator<(const Approx &lhs, double rhs) { + return lhs.m_value < rhs && lhs != rhs; +} + +bool operator>(double lhs, const Approx &rhs) { + return lhs > rhs.m_value && lhs != rhs; +} + +bool operator>(const Approx &lhs, double rhs) { + return lhs.m_value > rhs && lhs != rhs; +} + +String toString(const Approx &in) { + return "Approx( " + doctest::toString(in.m_value) + " )"; +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +Contains::Contains(const String &str) + : string(str) {} + +bool Contains::checkWith(const String &other) const { + return strstr(other.c_str(), string.c_str()) != nullptr; +} + +String toString(const Contains &in) { + return "Contains( " + in.string + " )"; +} + +bool operator==(const String &lhs, const Contains &rhs) { + return rhs.checkWith(lhs); +} + +bool operator==(const Contains &lhs, const String &rhs) { + return lhs.checkWith(rhs); +} + +bool operator!=(const String &lhs, const Contains &rhs) { + return !rhs.checkWith(lhs); +} + +bool operator!=(const Contains &lhs, const String &rhs) { + return !lhs.checkWith(rhs); +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738) +template +IsNaN::operator bool() const { + return std::isnan(value) ^ flipped; +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; + +template +String toString(IsNaN in) { + return String(in.flipped ? "! " : "") + "IsNaN( " + doctest::toString(in.value) + " )"; +} + +String toString(IsNaN in) { + return toString(in); +} + +String toString(IsNaN in) { + return toString(in); +} + +String toString(IsNaN in) { + return toString(in); +} + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR +#define DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR ':' +#endif + +namespace doctest { + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +// depending on the current options this will remove the path of filenames +const char *skipPathFromFilename(const char *file) { +#ifndef DOCTEST_CONFIG_DISABLE + if (getContextOptions()->no_path_in_filenames) { + auto back = std::strrchr(file, '\\'); + auto forward = std::strrchr(file, '/'); + if (back || forward) { + if (back > forward) + forward = back; + return forward + 1; + } + } else { + const auto prefixes = getContextOptions()->strip_file_prefixes; + const char separator = DOCTEST_CONFIG_OPTIONS_FILE_PREFIX_SEPARATOR; + String::size_type longest_match = 0U; + for (String::size_type pos = 0U; pos < prefixes.size(); ++pos) { + const auto prefix_start = pos; + pos = std::min(prefixes.find(separator, prefix_start), prefixes.size()); + + const auto prefix_size = pos - prefix_start; + if (prefix_size > longest_match) { + // TODO under DOCTEST_MSVC: does the comparison need strnicmp() to work with drive + // letter capitalization? + if (0 == std::strncmp(prefixes.c_str() + prefix_start, file, prefix_size)) { + longest_match = prefix_size; + } + } + } + return &file[longest_match]; + } +#endif // DOCTEST_CONFIG_DISABLE + return file; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +#ifdef DOCTEST_CONFIG_DISABLE + +DOCTEST_DEFINE_INTERFACE(IReporter) + +int IReporter::get_num_active_contexts() { + return 0; +} + +const IContextScope *const *IReporter::get_active_contexts() { + return nullptr; +} + +int IReporter::get_num_stringified_contexts() { + return 0; +} + +const String *IReporter::get_stringified_contexts() { + return nullptr; +} + +int registerReporter(const char *, int, IReporter *) { + return 0; +} + +#else + +namespace detail { +reporterMap &getReporters() noexcept { + static reporterMap data; + return data; +} + +reporterMap &getListeners() noexcept { + static reporterMap data; + return data; +} +} // namespace detail + +DOCTEST_DEFINE_INTERFACE(IReporter) + +int IReporter::get_num_active_contexts() { + return static_cast(detail::g_infoContexts.size()); +} + +const IContextScope *const *IReporter::get_active_contexts() { + return get_num_active_contexts() ? detail::g_infoContexts.data() : nullptr; +} + +int IReporter::get_num_stringified_contexts() { + return static_cast(detail::g_cs->stringifiedContexts.size()); +} + +const String *IReporter::get_stringified_contexts() { + return get_num_stringified_contexts() ? detail::g_cs->stringifiedContexts.data() : nullptr; +} + +namespace detail { +void registerReporterImpl(const char *name, int priority, reporterCreatorFunc c, bool isReporter) noexcept { + if (isReporter) + getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + else + getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); +} +} // namespace detail + +#endif // DOCTEST_CONFIG_DISABLE +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +void fulltext_log_assert_to_stream(std::ostream &s, const AssertData &rb) { + // clang-format off + if ((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == 0) + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " << Color::None; + + if (rb.m_at & assertType::is_throws) { + s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; + } else if ((rb.m_at & assertType::is_throws_as) && (rb.m_at & assertType::is_throws_with)) { + s << Color::Cyan << assertString(rb.m_at) << "( " + << rb.m_expr << ", \"" << rb.m_exception_string.c_str() << "\", " << rb.m_exception_type + << " ) " << Color::None; + + if (rb.m_threw) { + if (!rb.m_failed) { + s << "threw as expected!\n"; + } else { + s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; + } + } else { + s << "did NOT throw at all!\n"; + } + } else if (rb.m_at & assertType::is_throws_as) { + s << Color::Cyan << assertString(rb.m_at) << "( " + << rb.m_expr << ", " << rb.m_exception_type + << " ) " << Color::None + << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : "threw a DIFFERENT exception: ") : "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if (rb.m_at & assertType::is_throws_with) { + s << Color::Cyan << assertString(rb.m_at) << "( " + << rb.m_expr << ", \"" << rb.m_exception_string.c_str() + << "\" ) " << Color::None + << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : "threw a DIFFERENT exception: ") : "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if (rb.m_at & assertType::is_nothrow) { + s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan << rb.m_exception << "\n"; + } else { + s << (rb.m_threw ? "THREW exception: " : (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); + if (rb.m_threw) + s << rb.m_exception << "\n"; + else + s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; + } + // clang-format on +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +using detail::g_cs; + +Whitespace::Whitespace(int nr) + : nrSpaces(nr) {} + +std::ostream &operator<<(std::ostream &out, const Whitespace &ws) { + if (ws.nrSpaces != 0) + out << std::setw(ws.nrSpaces) << ' '; + return out; +} + +ConsoleReporter::ConsoleReporter(const ContextOptions &co) + : s(*co.cout), opt(co) {} + +ConsoleReporter::ConsoleReporter(const ContextOptions &co, std::ostream &ostr) + : s(ostr), opt(co) {} + +void ConsoleReporter::separator_to_stream() { + s << Color::Yellow + << "===============================================================================" + "\n"; +} + +const char *ConsoleReporter::getSuccessOrFailString(bool success, assertType::Enum at, const char *success_str) { + if (success) + return success_str; + return failureString(at); +} + +Color::Enum ConsoleReporter::getSuccessOrFailColor(bool success, assertType::Enum at) { + return success ? Color::BrightGreen : (at & assertType::is_warn) ? Color::Yellow : Color::Red; +} + +void ConsoleReporter::successOrFailColoredStringToStream(bool success, assertType::Enum at, const char *success_str) { + s << getSuccessOrFailColor(success, at) << getSuccessOrFailString(success, at, success_str) << ": "; +} + +void ConsoleReporter::log_contexts() { + const int num_contexts = get_num_active_contexts(); + if (num_contexts) { + auto contexts = get_active_contexts(); + + s << Color::None << " logged: "; + for (int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << "\n"; + } + } + + s << "\n"; +} + +// this was requested to be made virtual so users could override it +void ConsoleReporter::file_line_to_stream(const char *file, int line, const char *tail) { + s << Color::LightGrey << skipPathFromFilename(file) << (opt.gnu_file_line ? ":" : "(") + << (opt.no_line_numbers ? 0 : line) // 0 or the real num depending on the option + << (opt.gnu_file_line ? ":" : "):") << tail; +} + +void ConsoleReporter::logTestStart() { + if (hasLoggedCurrentTestStart) + return; + + separator_to_stream(); + file_line_to_stream(tc->m_file.c_str(), static_cast(tc->m_line), "\n"); + if (tc->m_description) + s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; + if (tc->m_test_suite && tc->m_test_suite[0] != '\0') + s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; + if (strncmp(tc->m_name, " Scenario:", 11) != 0) + s << Color::Yellow << "TEST CASE: "; + s << Color::None << tc->m_name << "\n"; + + for (size_t i = 0; i < currentSubcaseLevel; ++i) { + if (subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + + if (currentSubcaseLevel != subcasesStack.size()) { + s << Color::Yellow << "\nDEEPEST SUBCASE STACK REACHED (DIFFERENT FROM THE CURRENT ONE):\n" << Color::None; + for (size_t i = 0; i < subcasesStack.size(); ++i) { + if (subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + } + + s << "\n"; + + hasLoggedCurrentTestStart = true; +} + +void ConsoleReporter::printVersion() { + if (opt.no_version == false) + s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" << DOCTEST_VERSION_STR << "\"\n"; +} + +void ConsoleReporter::printIntro() { + if (opt.no_intro == false) { + printVersion(); + s << Color::Cyan << "[doctest] " << Color::None + << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; + } +} + +void ConsoleReporter::printHelp() { + const int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); + printVersion(); + // clang-format off + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filters use wildcards for matching strings\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "something passes a filter if any of the strings in a filter matches\n"; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; +#endif + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "Query flags - the program quits after them. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " + << Whitespace(sizePrefixDisplay * 0) << "prints this message\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " + << Whitespace(sizePrefixDisplay * 1) << "prints the version\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " + << Whitespace(sizePrefixDisplay * 1) << "prints the number of matching tests\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " + << Whitespace(sizePrefixDisplay * 1) << "lists all matching tests by name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " + << Whitespace(sizePrefixDisplay * 1) << "lists all matching test suites\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " + << Whitespace(sizePrefixDisplay * 1) << "lists all registered reporters\n\n"; + // ================================================================================== << 79 + s << Color::Cyan << "[doctest] " << Color::None; + s << "The available / options/filters are:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " + << Whitespace(sizePrefixDisplay * 1) << "filters tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " + << Whitespace(sizePrefixDisplay * 1) << "filters tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " + << Whitespace(sizePrefixDisplay * 1) << "filters tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " + << Whitespace(sizePrefixDisplay * 1) << "filters subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " + << Whitespace(sizePrefixDisplay * 1) << "filters OUT subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " + << Whitespace(sizePrefixDisplay * 1) << "reporters to use (console is default)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " + << Whitespace(sizePrefixDisplay * 1) << "output filename\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " + << Whitespace(sizePrefixDisplay * 1) << "how the tests should be ordered\n"; + s << Whitespace(sizePrefixDisplay * 3) << " - [file/suite/name/rand/none]\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " + << Whitespace(sizePrefixDisplay * 1) << "seed for random ordering\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " + << Whitespace(sizePrefixDisplay * 1) << "the first test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay * 3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " + << Whitespace(sizePrefixDisplay * 1) << "the last test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay * 3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " + << Whitespace(sizePrefixDisplay * 1) << "stop after failed assertions\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " + << Whitespace(sizePrefixDisplay * 1) << "apply filters for the first levels\n"; + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " + << Whitespace(sizePrefixDisplay * 1) << "include successful assertions in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " + << Whitespace(sizePrefixDisplay * 1) << "filters being treated as case sensitive\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " + << Whitespace(sizePrefixDisplay * 1) << "exits after the tests finish\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " + << Whitespace(sizePrefixDisplay * 1) << "prints the time duration of each test\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "m, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "minimal= " + << Whitespace(sizePrefixDisplay * 1) << "minimal console output (only failures)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "q, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "quiet= " + << Whitespace(sizePrefixDisplay * 1) << "no console output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " + << Whitespace(sizePrefixDisplay * 1) << "skips exceptions-related assert checks\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " + << Whitespace(sizePrefixDisplay * 1) << "returns (or exits) always with success\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " + << Whitespace(sizePrefixDisplay * 1) << "skips all runtime doctest operations\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ni, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-intro= " + << Whitespace(sizePrefixDisplay * 1) << "omit the framework intro in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " + << Whitespace(sizePrefixDisplay * 1) << "omit the framework version in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " + << Whitespace(sizePrefixDisplay * 1) << "disables colors in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " + << Whitespace(sizePrefixDisplay * 1) << "use colors even when not in a tty\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " + << Whitespace(sizePrefixDisplay * 1) << "disables breakpoints in debuggers\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " + << Whitespace(sizePrefixDisplay * 1) << "don't skip test cases marked as skip\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " + << Whitespace(sizePrefixDisplay * 1) << ":n: vs (n): for line numbers in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " + << Whitespace(sizePrefixDisplay * 1) << "only filenames and no paths in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfp, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "strip-file-prefixes= " + << Whitespace(sizePrefixDisplay * 1) << "whenever file paths start with this prefix, remove it from the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " + << Whitespace(sizePrefixDisplay * 1) << "0 instead of real line numbers in output\n"; + // ================================================================================== << 79 + // clang-format on + + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "for more information visit the project documentation\n\n"; +} + +void ConsoleReporter::printRegisteredReporters() { + printVersion(); + auto printReporters = [this](const detail::reporterMap &reporters, const char *type) { + if (!reporters.empty()) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; + for (auto &curr: reporters) + s << "priority: " << std::setw(5) << curr.first.first << " name: " << curr.first.second << "\n"; + } + }; + printReporters(detail::getListeners(), "listeners"); + printReporters(detail::getReporters(), "reporters"); +} + +void ConsoleReporter::report_query(const QueryData &in) { + if (opt.version) { + printVersion(); + } else if (opt.help) { + printHelp(); + } else if (opt.list_reporters) { + printRegisteredReporters(); + } else if (opt.count || opt.list_test_cases) { + if (opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test case names\n"; + separator_to_stream(); + } + + for (unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_name << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " << g_cs->numTestCasesPassingFilters << "\n"; + + } else if (opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; + separator_to_stream(); + + for (unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_test_suite << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " << g_cs->numTestSuitesPassingFilters + << "\n"; + } +} + +void ConsoleReporter::test_run_start() { + if (!opt.minimal) + printIntro(); +} + +void ConsoleReporter::test_run_end(const TestRunStats &p) { + if (opt.minimal && p.numTestCasesFailed == 0) + return; + + separator_to_stream(); + s << std::dec; + + auto totwidth = static_cast(std::ceil( + log10(static_cast(std::max(p.numTestCasesPassingFilters, static_cast(p.numAsserts))) + 1) + )); + auto passwidth = static_cast(std::ceil(log10( + static_cast(std::max( + p.numTestCasesPassingFilters - p.numTestCasesFailed, + static_cast(p.numAsserts - p.numAssertsFailed) + )) + + 1 + ))); + auto failwidth = static_cast(std::ceil( + log10(static_cast(std::max(p.numTestCasesFailed, static_cast(p.numAssertsFailed))) + 1) + )); + const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; + s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(totwidth) + << p.numTestCasesPassingFilters << " | " + << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : Color::Green) << std::setw(passwidth) + << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" << Color::None << " | " + << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) << p.numTestCasesFailed + << " failed" << Color::None << " |"; + if (opt.no_skipped_summary == false) { + const unsigned int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; + s << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped << " skipped" << Color::None; + } + s << "\n"; + s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(totwidth) << p.numAsserts << " | " + << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) << std::setw(passwidth) + << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None << " | " + << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) << p.numAssertsFailed << " failed" + << Color::None << " |\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) + << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; +} + +void ConsoleReporter::test_case_start(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + hasLoggedCurrentTestStart = false; + tc = ∈ + subcasesStack.clear(); + currentSubcaseLevel = 0; +} + +void ConsoleReporter::test_case_reenter(const TestCaseData &) { + DOCTEST_LOCK_MUTEX(mutex) + subcasesStack.clear(); +} + +void ConsoleReporter::test_case_end(const CurrentTestCaseStats &st) { + DOCTEST_LOCK_MUTEX(mutex) + if (tc->m_no_output) + return; + + // log the preamble of the test case only if there is something + // else to print - something other than that an assert has failed + if (opt.duration || + (st.failure_flags && st.failure_flags != static_cast(TestCaseFailureReason::AssertFailure))) + logTestStart(); + + if (opt.duration) + s << Color::None << std::setprecision(6) << std::fixed << st.seconds << " s: " << tc->m_name << "\n"; + + if (st.failure_flags & TestCaseFailureReason::Timeout) + s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) << std::fixed << tc->m_timeout + << "!\n"; + + if (st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; + } else if (st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { + s << Color::Yellow << "Failed as expected so marking it as not failed\n"; + } else if (st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { + s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; + } else if (st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures << " times so marking it as failed!\n"; + } else if (st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { + s << Color::Yellow << "Failed exactly " << tc->m_expected_failures + << " times as expected so marking it as not failed!\n"; + } + if (st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + s << Color::Red << "Aborting - too many failed asserts!\n"; + } + s << Color::None; +} + +void ConsoleReporter::test_case_exception(const TestCaseException &e) { + DOCTEST_LOCK_MUTEX(mutex) + if (tc->m_no_output) + return; + + logTestStart(); + + file_line_to_stream(tc->m_file.c_str(), static_cast(tc->m_line), " "); + successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : assertType::is_check); + s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: "); + s << Color::Cyan << e.error_string << "\n"; + + const int num_stringified_contexts = get_num_stringified_contexts(); + if (num_stringified_contexts) { + auto stringified_contexts = get_stringified_contexts(); + s << Color::None << " logged: "; + for (int i = num_stringified_contexts; i > 0; --i) { + s << (i == num_stringified_contexts ? "" : " ") << stringified_contexts[i - 1] << "\n"; + } + } + s << "\n" << Color::None; +} + +void ConsoleReporter::subcase_start(const SubcaseSignature &subc) { + DOCTEST_LOCK_MUTEX(mutex) + subcasesStack.push_back(subc); + ++currentSubcaseLevel; + hasLoggedCurrentTestStart = false; +} + +void ConsoleReporter::subcase_end() { + DOCTEST_LOCK_MUTEX(mutex) + --currentSubcaseLevel; + hasLoggedCurrentTestStart = false; +} + +void ConsoleReporter::log_assert(const AssertData &rb) { + if ((!rb.m_failed && !opt.success) || tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(rb.m_file, rb.m_line, " "); + successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); + + fulltext_log_assert_to_stream(s, rb); + + log_contexts(); +} + +void ConsoleReporter::log_message(const MessageData &mb) { + if (tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(mb.m_file, mb.m_line, " "); + s << getSuccessOrFailColor(false, mb.m_severity) + << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, "MESSAGE") << ": "; + s << Color::None << mb.m_string << "\n"; + log_contexts(); +} + +void ConsoleReporter::test_case_skipped(const TestCaseData &) {} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) +#endif // Platform + +#ifdef DOCTEST_PLATFORM_WINDOWS + +DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; + +DebugOutputWindowReporter::DebugOutputWindowReporter(const ContextOptions &co) + : ConsoleReporter(co, oss) {} + +#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ + void DebugOutputWindowReporter::func(type arg) { \ + using detail::g_no_colors; \ + bool with_col = g_no_colors; \ + g_no_colors = false; \ + ConsoleReporter::func(arg); \ + if (oss.tellp() != std::streampos{}) { \ + DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ + oss.str(""); \ + } \ + g_no_colors = with_col; \ + } + +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData &, in) +DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData &, in) + +#endif // DOCTEST_PLATFORM_WINDOWS + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +std::string JUnitReporter::JUnitTestCaseData::getCurrentTimestamp() { + // Beware, this is not reentrant because of backward compatibility issues + // Also, UTC only, again because of backward compatibility (%z is C++11) + time_t rawtime{}; + static_cast(std::time(&rawtime)); + const auto timeStampSize = sizeof("2017-01-16T17:06:45Z"); + + std::tm timeInfo; +#if defined(DOCTEST_PLATFORM_WINDOWS) + gmtime_s(&timeInfo, &rawtime); +#elif defined(__STDC_LIB_EXT1__) + gmtime_s(&rawtime, &timeInfo); +#else // DOCTEST_PLATFORM_WINDOWS + gmtime_r(&rawtime, &timeInfo); +#endif // DOCTEST_PLATFORM_WINDOWS + + char timeStamp[timeStampSize]; + const char *const fmt = "%Y-%m-%dT%H:%M:%SZ"; + + static_cast(std::strftime(timeStamp, timeStampSize, fmt, &timeInfo)); + return std::string(timeStamp); +} + +JUnitReporter::JUnitTestCaseData::JUnitTestMessage::JUnitTestMessage( + const std::string &_message, const std::string &_type, const std::string &_details +) + : message(_message), type(_type), details(_details) {} + +JUnitReporter::JUnitTestCaseData::JUnitTestMessage::JUnitTestMessage( + const std::string &_message, const std::string &_details +) + : message(_message), type(), details(_details) {} + +JUnitReporter::JUnitTestCaseData::JUnitTestCase::JUnitTestCase(const std::string &_classname, const std::string &_name) + : classname(_classname), name(_name), time(0), failures(), errors() {} + +void JUnitReporter::JUnitTestCaseData::add(const std::string &classname, const std::string &name) { + testcases.emplace_back(classname, name); +} + +void JUnitReporter::JUnitTestCaseData::appendSubcaseNamesToLastTestcase(std::vector nameStack) { + for (auto &curr: nameStack) + if (curr.size()) + testcases.back().name += std::string("/") + curr.c_str(); +} + +void JUnitReporter::JUnitTestCaseData::addTime(double time) { + if (time < 1e-4) + time = 0; + testcases.back().time = time; + totalSeconds += time; +} + +void JUnitReporter::JUnitTestCaseData::addFailure( + const std::string &message, const std::string &type, const std::string &details +) { + testcases.back().failures.emplace_back(message, type, details); + ++totalFailures; +} + +void JUnitReporter::JUnitTestCaseData::addError(const std::string &message, const std::string &details) { + testcases.back().errors.emplace_back(message, details); + ++totalErrors; +} + +JUnitReporter::JUnitReporter(const ContextOptions &co) + : xml(*co.cout), opt(co) {} + +unsigned JUnitReporter::line(unsigned l) const { + return opt.no_line_numbers ? 0 : l; +} + +void JUnitReporter::report_query(const QueryData &) { + xml.writeDeclaration(); +} + +void JUnitReporter::test_run_start() { + xml.writeDeclaration(); +} + +void JUnitReporter::test_run_end(const TestRunStats &p) { + // remove .exe extension - mainly to have the same output on UNIX and Windows + // NOLINTNEXTLINE(misc-const-correctness) + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if (binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("testsuites"); + xml.startElement("testsuite") + .writeAttribute("name", binary_name) + .writeAttribute("errors", testCaseData.totalErrors) + .writeAttribute("failures", testCaseData.totalFailures) + .writeAttribute("tests", p.numAsserts); + if (opt.no_time_in_output == false) { + xml.writeAttribute("time", testCaseData.totalSeconds); + xml.writeAttribute("timestamp", JUnitTestCaseData::getCurrentTimestamp()); + } + if (opt.no_version == false) + xml.writeAttribute("doctest_version", DOCTEST_VERSION_STR); + + for (const auto &testCase: testCaseData.testcases) { + xml.startElement("testcase") + .writeAttribute("classname", testCase.classname) + .writeAttribute("name", testCase.name); + if (opt.no_time_in_output == false) + xml.writeAttribute("time", testCase.time); + // This is not ideal, but it should be enough to mimic gtest's junit output. + xml.writeAttribute("status", "run"); + + for (const auto &failure: testCase.failures) { + xml.scopedElement("failure") + .writeAttribute("message", failure.message) + .writeAttribute("type", failure.type) + .writeText(failure.details, false); + } + + for (const auto &error: testCase.errors) { + xml.scopedElement("error").writeAttribute("message", error.message).writeText(error.details); + } + + xml.endElement(); + } + xml.endElement(); + xml.endElement(); +} + +void JUnitReporter::test_case_start(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + timer.start(); + tc = ∈ +} + +void JUnitReporter::test_case_reenter(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + + timer.start(); + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + tc = ∈ +} + +void JUnitReporter::test_case_end(const CurrentTestCaseStats &st) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + + if (st.failure_flags & TestCaseFailureReason::Timeout) { + auto *stream = detail::tlssPush(); + *stream << "Test case exceeded time limit of " << std::setprecision(6) << std::fixed << tc->m_timeout; + testCaseData.addError("timeout", detail::tlssPop().c_str()); + } + + if (st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + testCaseData.addError("should_fail", "Should have failed, but didn't"); + } else if (st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + auto *stream = detail::tlssPush(); + *stream << "Should have failed exactly " << tc->m_expected_failures << " times, but didn't"; + testCaseData.addError("should_fail", detail::tlssPop().c_str()); + } + + if (st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + testCaseData.addError("abort_after", "Too many failed asserts"); + } + + tc = nullptr; +} + +void JUnitReporter::test_case_exception(const TestCaseException &e) { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addError("exception", e.error_string.c_str()); +} + +void JUnitReporter::subcase_start(const SubcaseSignature &in) { + DOCTEST_LOCK_MUTEX(mutex) + deepestSubcaseStackNames.push_back(in.m_name); +} + +void JUnitReporter::subcase_end() {} + +void JUnitReporter::log_assert(const AssertData &rb) { + if (!rb.m_failed) // report only failures & ignore the `success` option + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(") << line(rb.m_line) + << (opt.gnu_file_line ? ":" : "):") << std::endl; + + fulltext_log_assert_to_stream(os, rb); + log_contexts(os); + testCaseData.addFailure(rb.m_decomp.c_str(), assertString(rb.m_at), os.str()); +} + +void JUnitReporter::log_message(const MessageData &mb) { + if (mb.m_severity & assertType::is_warn) // report only failures + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(mb.m_file) << (opt.gnu_file_line ? ":" : "(") << line(mb.m_line) + << (opt.gnu_file_line ? ":" : "):") << std::endl; + + os << mb.m_string.c_str() << "\n"; + log_contexts(os); + + testCaseData.addFailure( + mb.m_string.c_str(), mb.m_severity & assertType::is_check ? "FAIL_CHECK" : "FAIL", os.str() + ); +} + +void JUnitReporter::test_case_skipped(const TestCaseData &) {} + +void JUnitReporter::log_contexts(std::ostringstream &s) { + const int num_contexts = get_num_active_contexts(); + if (num_contexts) { + auto contexts = get_active_contexts(); + + s << " logged: "; + for (int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << std::endl; + } + } +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { + +XmlReporter::XmlReporter(const ContextOptions &co) + : xml(*co.cout), opt(co) {} + +void XmlReporter::log_contexts() { + const int num_contexts = get_num_active_contexts(); + if (num_contexts) { + auto contexts = get_active_contexts(); + std::stringstream ss; + for (int i = 0; i < num_contexts; ++i) { + contexts[i]->stringify(&ss); + xml.scopedElement("Info").writeText(ss.str()); + ss.str(""); + } + } +} + +unsigned XmlReporter::line(unsigned l) const { + return opt.no_line_numbers ? 0 : l; +} + +void XmlReporter::test_case_start_impl(const TestCaseData &in) { + bool open_ts_tag = false; + if (tc != nullptr) { // we have already opened a test suite + if (std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { + xml.endElement(); + open_ts_tag = true; + } + } else { + open_ts_tag = true; // first test case ==> first test suite + } + + if (open_ts_tag) { + xml.startElement("TestSuite"); + xml.writeAttribute("name", in.m_test_suite); + } + + tc = ∈ + xml.startElement("TestCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file.c_str())) + .writeAttribute("line", line(in.m_line)) + .writeAttribute("description", in.m_description); + + if (Approx(in.m_timeout) != 0) + xml.writeAttribute("timeout", in.m_timeout); + if (in.m_may_fail) + xml.writeAttribute("may_fail", true); + if (in.m_should_fail) + xml.writeAttribute("should_fail", true); +} + +void XmlReporter::report_query(const QueryData &in) { + test_run_start(); + if (opt.list_reporters) { + for (auto &curr: detail::getListeners()) + xml.scopedElement("Listener") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + for (auto &curr: detail::getReporters()) + xml.scopedElement("Reporter") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + } else if (opt.count || opt.list_test_cases) { + for (unsigned i = 0; i < in.num_data; ++i) { + xml.scopedElement("TestCase") + .writeAttribute("name", in.data[i]->m_name) + .writeAttribute("testsuite", in.data[i]->m_test_suite) + .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file.c_str())) + .writeAttribute("line", line(in.data[i]->m_line)) + .writeAttribute("skipped", in.data[i]->m_skip); + } + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + } else if (opt.list_test_suites) { + for (unsigned i = 0; i < in.num_data; ++i) + xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + xml.scopedElement("OverallResultsTestSuites") + .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); + } + xml.endElement(); +} + +void XmlReporter::test_run_start() { + xml.writeDeclaration(); + + // remove .exe extension - mainly to have the same output on UNIX and Windows + // NOLINTNEXTLINE(misc-const-correctness) + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if (binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("doctest").writeAttribute("binary", binary_name); + if (opt.no_version == false) + xml.writeAttribute("version", DOCTEST_VERSION_STR); + + // only the consequential ones (TODO: filters) + xml.scopedElement("Options") + .writeAttribute("order_by", opt.order_by.c_str()) + .writeAttribute("rand_seed", opt.rand_seed) + .writeAttribute("first", opt.first) + .writeAttribute("last", opt.last) + .writeAttribute("abort_after", opt.abort_after) + .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) + .writeAttribute("case_sensitive", opt.case_sensitive) + .writeAttribute("no_throw", opt.no_throw) + .writeAttribute("no_skip", opt.no_skip); +} + +void XmlReporter::test_run_end(const TestRunStats &p) { + if (tc) // the TestSuite tag - only if there has been at least 1 test case + xml.endElement(); + + xml.scopedElement("OverallResultsAsserts") + .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) + .writeAttribute("failures", p.numAssertsFailed); + + xml.startElement("OverallResultsTestCases") + .writeAttribute("successes", p.numTestCasesPassingFilters - p.numTestCasesFailed) + .writeAttribute("failures", p.numTestCasesFailed); + if (opt.no_skipped_summary == false) + xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); + xml.endElement(); + + xml.endElement(); +} + +void XmlReporter::test_case_start(const TestCaseData &in) { + DOCTEST_LOCK_MUTEX(mutex) + test_case_start_impl(in); + xml.ensureTagClosed(); +} + +void XmlReporter::test_case_reenter(const TestCaseData &) {} + +void XmlReporter::test_case_end(const CurrentTestCaseStats &st) { + DOCTEST_LOCK_MUTEX(mutex) + xml.startElement("OverallResultsAsserts") + .writeAttribute("successes", st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) + .writeAttribute("failures", st.numAssertsFailedCurrentTest) + .writeAttribute("test_case_success", st.testCaseSuccess); + if (opt.duration) + xml.writeAttribute("duration", st.seconds); + if (tc->m_expected_failures) + xml.writeAttribute("expected_failures", tc->m_expected_failures); + xml.endElement(); + + xml.endElement(); +} + +void XmlReporter::test_case_exception(const TestCaseException &e) { + DOCTEST_LOCK_MUTEX(mutex) + + xml.scopedElement("Exception").writeAttribute("crash", e.is_crash).writeText(e.error_string.c_str()); +} + +void XmlReporter::subcase_start(const SubcaseSignature &in) { + DOCTEST_LOCK_MUTEX(mutex) + xml.startElement("SubCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)); + xml.ensureTagClosed(); +} + +void XmlReporter::subcase_end() { + DOCTEST_LOCK_MUTEX(mutex) + xml.endElement(); +} + +void XmlReporter::log_assert(const AssertData &rb) { + if (!rb.m_failed && !opt.success) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Expression") + .writeAttribute("success", !rb.m_failed) + .writeAttribute("type", assertString(rb.m_at)) + .writeAttribute("filename", skipPathFromFilename(rb.m_file)) + .writeAttribute("line", line(rb.m_line)); + + xml.scopedElement("Original").writeText(rb.m_expr); + + if (rb.m_threw) + xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); + + if (rb.m_at & assertType::is_throws_as) + xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); + if (rb.m_at & assertType::is_throws_with) + xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string.c_str()); + if ((rb.m_at & assertType::is_normal) && !rb.m_threw) + xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); + + log_contexts(); + + xml.endElement(); +} + +void XmlReporter::log_message(const MessageData &mb) { + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Message") + .writeAttribute("type", failureString(mb.m_severity)) + .writeAttribute("filename", skipPathFromFilename(mb.m_file)) + .writeAttribute("line", line(mb.m_line)); + + xml.scopedElement("Text").writeText(mb.m_string.c_str()); + + log_contexts(); + + xml.endElement(); +} + +void XmlReporter::test_case_skipped(const TestCaseData &in) { + if (opt.no_skipped_summary == false) { + DOCTEST_LOCK_MUTEX(mutex) + test_case_start_impl(in); + xml.writeAttribute("skipped", "true"); + xml.endElement(); + } +} + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +void FatalConditionHandler::reset() {} +void FatalConditionHandler::allocateAltStackMem() {} +void FatalConditionHandler::freeAltStackMem() {} +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// There is no 1-1 mapping between signals and windows exceptions. +// Windows can easily distinguish between SO and SigSegV, +// but SigInt, SigTerm, etc are handled differently. +SignalDefs signalDefs[] = { + {static_cast(EXCEPTION_ILLEGAL_INSTRUCTION), "SIGILL - Illegal instruction signal"}, + {static_cast(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"}, + {static_cast(EXCEPTION_ACCESS_VIOLATION), "SIGSEGV - Segmentation violation signal"}, + {static_cast(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"}, +}; + +LONG CALLBACK FatalConditionHandler::handleException(PEXCEPTION_POINTERS ExceptionInfo) { + // Multiple threads may enter this filter/handler at once. We want the error message to be + // printed on the console just once no matter how many threads have crashed. + DOCTEST_DECLARE_STATIC_MUTEX(mutex) + static bool execute = true; + { + DOCTEST_LOCK_MUTEX(mutex) + if (execute) { + bool reported = false; + for (size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + if (ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { + reportFatal(signalDefs[i].name); + reported = true; + break; + } + } + if (reported == false) + reportFatal("Unhandled SEH exception caught"); + if (isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + } + execute = false; + } + std::exit(EXIT_FAILURE); +} + +void FatalConditionHandler::allocateAltStackMem() {} +void FatalConditionHandler::freeAltStackMem() {} + +FatalConditionHandler::FatalConditionHandler() { + isSet = true; + // 32k seems enough for doctest to handle stack overflow, + // but the value was found experimentally, so there is no strong guarantee + guaranteeSize = 32 * 1024; + // Register an unhandled exception filter + previousTop = SetUnhandledExceptionFilter(handleException); + // Pass in guarantee size to be filled + SetThreadStackGuarantee(&guaranteeSize); + + // On Windows uncaught exceptions from another thread, exceptions from + // destructors, or calls to std::terminate are not a SEH exception + + // The terminal handler gets called when: + // - std::terminate is called FROM THE TEST RUNNER THREAD + // - an exception is thrown from a destructor FROM THE TEST RUNNER THREAD + original_terminate_handler = std::get_terminate(); + std::set_terminate([]() DOCTEST_NOEXCEPT { + reportFatal("Terminate handler called"); + if (isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); // explicitly exit - otherwise the SIGABRT handler may be called as well + }); + + // SIGABRT is raised when: + // - std::terminate is called FROM A DIFFERENT THREAD + // - an exception is thrown from a destructor FROM A DIFFERENT THREAD + // - an uncaught exception is thrown FROM A DIFFERENT THREAD + prev_sigabrt_handler = std::signal(SIGABRT, [](int signal) DOCTEST_NOEXCEPT { + if (signal == SIGABRT) { + reportFatal("SIGABRT - Abort (abnormal termination) signal"); + if (isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); + } + }); + + // The following settings are taken from google test, and more + // specifically from UnitTest::Run() inside of gtest.cc + + // the user does not want to see pop-up dialogs about crashes + prev_error_mode_1 = SetErrorMode( + SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX + ); + // This forces the abort message to go to stderr in all circumstances. + prev_error_mode_2 = _set_error_mode(_OUT_TO_STDERR); + // In the debug version, Visual Studio pops up a separate dialog + // offering a choice to debug the aborted program - we want to disable that. + prev_abort_behavior = _set_abort_behavior(0x0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + // In debug mode, the Windows CRT can crash with an assertion over invalid + // input (e.g. passing an invalid file descriptor). The default handling + // for these assertions is to pop up a dialog and wait for user input. + // Instead ask the CRT to dump such assertions to stderr non-interactively. + prev_report_mode = _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); + prev_report_file = _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); +} + +void FatalConditionHandler::reset() { + if (isSet) { + // Unregister handler and restore the old guarantee + SetUnhandledExceptionFilter(previousTop); + SetThreadStackGuarantee(&guaranteeSize); + std::set_terminate(original_terminate_handler); + std::signal(SIGABRT, prev_sigabrt_handler); + SetErrorMode(prev_error_mode_1); + _set_error_mode(prev_error_mode_2); + _set_abort_behavior(prev_abort_behavior, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + static_cast(_CrtSetReportMode(_CRT_ASSERT, prev_report_mode)); + static_cast(_CrtSetReportFile(_CRT_ASSERT, prev_report_file)); + isSet = false; + } +} + +FatalConditionHandler::~FatalConditionHandler() { + reset(); +} + +UINT FatalConditionHandler::prev_error_mode_1; +int FatalConditionHandler::prev_error_mode_2; +unsigned int FatalConditionHandler::prev_abort_behavior; +int FatalConditionHandler::prev_report_mode; +_HFILE FatalConditionHandler::prev_report_file; +void(DOCTEST_CDECL *FatalConditionHandler::prev_sigabrt_handler)(int); +std::terminate_handler FatalConditionHandler::original_terminate_handler; +bool FatalConditionHandler::isSet = false; +ULONG FatalConditionHandler::guaranteeSize = 0; +LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; + +#else // DOCTEST_PLATFORM_WINDOWS + +SignalDefs signalDefs[] = { + {SIGINT, "SIGINT - Terminal interrupt signal"}, + {SIGILL, "SIGILL - Illegal instruction signal"}, + {SIGFPE, "SIGFPE - Floating point error signal"}, + {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, + {SIGTERM, "SIGTERM - Termination request signal"}, + {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"} +}; +static_assert( + DOCTEST_COUNTOF(signalDefs) == DOCTEST_COUNTOF(FatalConditionHandler::oldSigActions), "arrays should match in size" +); + +void FatalConditionHandler::handleSignal(int sig) { + const char *name = ""; + for (std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + const SignalDefs &def = signalDefs[i]; + if (sig == def.id) { + name = def.name; + break; + } + } + reset(); + reportFatal(name); + static_cast(raise(sig)); +} + +void FatalConditionHandler::allocateAltStackMem() { + altStackMem = new char[altStackSize]; +} + +void FatalConditionHandler::freeAltStackMem() { + delete[] altStackMem; +} + +FatalConditionHandler::FatalConditionHandler() { + isSet = true; + stack_t sigStack; + sigStack.ss_sp = altStackMem; + sigStack.ss_size = altStackSize; + sigStack.ss_flags = 0; + sigaltstack(&sigStack, &oldSigStack); + struct sigaction sa = {}; + sa.sa_handler = handleSignal; + sa.sa_flags = SA_ONSTACK; + for (std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); + } +} + +FatalConditionHandler::~FatalConditionHandler() { + reset(); +} + +void FatalConditionHandler::reset() { + if (isSet) { + // Set signals back to previous values -- hopefully nobody overwrote them in the meantime + for (std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); + } + // Return the old stack + sigaltstack(&oldSigStack, nullptr); + isSet = false; + } +} + +bool FatalConditionHandler::isSet = false; +struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; +stack_t FatalConditionHandler::oldSigStack = {}; +size_t FatalConditionHandler::altStackSize = 4 * SIGSTKSZ; +char *FatalConditionHandler::altStackMem = nullptr; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { +namespace detail { + +DOCTEST_THREAD_LOCAL class oss { + std::vector stack; + std::stringstream ss; + +public: + std::ostream *push() { + stack.push_back(ss.tellp()); + return &ss; + } + + String pop() { + if (stack.empty()) + DOCTEST_INTERNAL_ERROR("TLSS was empty when trying to pop!"); + + const std::streampos pos = stack.back(); + stack.pop_back(); + const unsigned sz = static_cast(ss.tellp() - pos); + ss.rdbuf()->pubseekpos(pos, std::ios::in | std::ios::out); + return String(ss, sz); + } +} g_oss; // NOLINT(bugprone-throwing-static-initialization, cert-err58-cpp) + +std::ostream *tlssPush() { + return g_oss.push(); +} + +String tlssPop() { + return g_oss.pop(); +} + +} // namespace detail + +// case insensitive strcmp +static int stricmp(const char *a, const char *b) { + for (;; a++, b++) { + const int d = tolower(*a) - tolower(*b); + if (d != 0 || !*a) + return d; + } +} + +// NOLINTBEGIN(cppcoreguidelines-pro-type-union-access) + +char *String::allocate(size_type sz) { + if (sz <= last) { + buf[sz] = '\0'; + setLast(last - sz); + return buf; + } else { + setOnHeap(); + data.size = sz; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + data.ptr[sz] = '\0'; + return data.ptr; + } +} + +void String::setOnHeap() noexcept { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + *reinterpret_cast(&buf[last]) = 128; +} + +void String::setLast(size_type in) noexcept { + buf[last] = static_cast(in); +} + +void String::setSize(size_type sz) noexcept { + if (isOnStack()) { + buf[sz] = '\0'; + setLast(last - sz); + } else { + data.ptr[sz] = '\0'; + data.size = sz; + } +} + +void String::copy(const String &other) { + if (other.isOnStack()) { + memcpy(buf, other.buf, len); + } else { + memcpy(allocate(other.data.size), other.data.ptr, other.data.size); + } +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +String::String() noexcept { + buf[0] = '\0'; + setLast(); +} + +String::~String() { + if (!isOnStack()) + delete[] data.ptr; +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +String::String(const char *in) + : String(in, strlen(in)) {} + +String::String(const char *in, size_type in_size) { + memcpy(allocate(in_size), in, in_size); +} + +String::String(std::istream &in, size_type in_size) { + in.read(allocate(in_size), in_size); +} + +String::String(const String &other) { + copy(other); +} + +String &String::operator=(const String &other) { + if (this != &other) { + if (!isOnStack()) + delete[] data.ptr; + + copy(other); + } + + return *this; +} + +String &String::operator+=(const String &other) { + const size_type my_old_size = size(); + const size_type other_size = other.size(); + const size_type total_size = my_old_size + other_size; + if (isOnStack()) { + if (total_size < len) { + // append to the current stack space + memcpy(buf + my_old_size, other.c_str(), other_size + 1); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + setLast(last - total_size); + } else { + // alloc new chunk + char *temp = new char[total_size + 1]; + // copy current data to new location before writing in the union + memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed + // update data in union + setOnHeap(); + data.size = total_size; + data.capacity = data.size + 1; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } else { + if (data.capacity > total_size) { + // append to the current heap block + data.size = total_size; + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } else { + // resize + data.capacity *= 2; + if (data.capacity <= total_size) + data.capacity = total_size + 1; + // alloc new chunk + char *temp = new char[data.capacity]; + // copy current data to new location before releasing it + memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed + // release old chunk + delete[] data.ptr; + // update the rest of the union members + data.size = total_size; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } + + return *this; +} + +String::String(String &&other) noexcept { + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); +} + +String &String::operator=(String &&other) noexcept { + if (this != &other) { + if (!isOnStack()) + delete[] data.ptr; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); + } + return *this; +} + +char String::operator[](size_type i) const { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + return const_cast(this)->operator[](i); +} + +char &String::operator[](size_type i) { + if (isOnStack()) + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(buf)[i]; + return data.ptr[i]; +} + +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") +String::size_type String::size() const { + if (isOnStack()) + return last - (static_cast(buf[last]) & 31); // using "last" would work only if "len" is 32 + return data.size; +} +DOCTEST_GCC_SUPPRESS_WARNING_POP + +String::size_type String::capacity() const { + if (isOnStack()) + return len; + return data.capacity; +} + +String String::substr(size_type pos, size_type cnt) && { + cnt = std::min(cnt, size() - pos); + char *cptr = c_str(); + memmove(cptr, cptr + pos, cnt); + setSize(cnt); + return std::move(*this); +} + +String String::substr(size_type pos, size_type cnt) const & { + cnt = std::min(cnt, size() - pos); + return String{c_str() + pos, cnt}; +} + +String::size_type String::find(char ch, size_type pos) const { + const char *begin = c_str(); + const char *end = begin + size(); + const char *it = begin + pos; + for (; it < end && *it != ch; it++) {} + if (it < end) { + return static_cast(it - begin); + } else { + return npos; + } +} + +String::size_type String::rfind(char ch, size_type pos) const { + if (size() == 0) { + return npos; + } + + const char *begin = c_str(); + const char *it = begin + std::min(pos, size() - 1); + for (; it >= begin && *it != ch; it--) {} + if (it >= begin) { + return static_cast(it - begin); + } else { + return npos; + } +} + +int String::compare(const char *other, bool no_case) const { + if (no_case) + return doctest::stricmp(c_str(), other); + return std::strcmp(c_str(), other); +} + +int String::compare(const String &other, bool no_case) const { + return compare(other.c_str(), no_case); +} + +String operator+(const String &lhs, const String &rhs) { + return String(lhs) += rhs; +} + +bool operator==(const String &lhs, const String &rhs) { + return lhs.compare(rhs) == 0; +} + +bool operator!=(const String &lhs, const String &rhs) { + return lhs.compare(rhs) != 0; +} + +bool operator<(const String &lhs, const String &rhs) { + return lhs.compare(rhs) < 0; +} + +bool operator>(const String &lhs, const String &rhs) { + return lhs.compare(rhs) > 0; +} + +bool operator<=(const String &lhs, const String &rhs) { + return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; +} + +bool operator>=(const String &lhs, const String &rhs) { + return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; +} + +std::ostream &operator<<(std::ostream &s, const String &in) { + return s << in.c_str(); +} + +namespace detail { + +void filldata::fill(std::ostream *stream, const void *in) { + filldata::fill(stream, in); +} + +void filldata::fill(std::ostream *stream, const volatile void *in) { + if (in) { + *stream << in; + } else { + *stream << "nullptr"; + } +} + +template +String toStreamLit(T t) { + // NOLINTNEXTLINE(misc-const-correctness) + std::ostream *os = tlssPush(); + os->operator<<(t); + return tlssPop(); +} +} // namespace detail + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(const char *in) { + return String("\"") + (in ? in : "{null string}") + "\""; +} +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +String toString(const std::string &in) { + return in.c_str(); +} +#endif // VS 2019 + +String toString(const String &in) { + return in; +} + +String toString(std::nullptr_t) { + return "nullptr"; +} + +String toString(bool in) { + return in ? "true" : "false"; +} + +String toString(float in) { + return detail::toStreamLit(in); +} +String toString(double in) { + return detail::toStreamLit(in); +} +String toString(double long in) { + return detail::toStreamLit(in); +} + +String toString(char in) { + return detail::toStreamLit(static_cast(in)); +} +String toString(char signed in) { + return detail::toStreamLit(static_cast(in)); +} +String toString(char unsigned in) { + return detail::toStreamLit(static_cast(in)); +} +String toString(short in) { + return detail::toStreamLit(in); +} +String toString(short unsigned in) { + return detail::toStreamLit(in); +} +String toString(signed in) { + return detail::toStreamLit(in); +} +String toString(unsigned in) { + return detail::toStreamLit(in); +} +String toString(long in) { + return detail::toStreamLit(in); +} +String toString(long unsigned in) { + return detail::toStreamLit(in); +} +String toString(long long in) { + return detail::toStreamLit(in); +} +String toString(long long unsigned in) { + return detail::toStreamLit(in); +} + +// NOLINTEND(cppcoreguidelines-pro-type-union-access) + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +namespace doctest { + +bool SubcaseSignature::operator==(const SubcaseSignature &other) const { + return m_line == other.m_line && std::strcmp(m_file, other.m_file) == 0 && m_name == other.m_name; +} + +bool SubcaseSignature::operator<(const SubcaseSignature &other) const { + if (m_line != other.m_line) + return m_line < other.m_line; + if (std::strcmp(m_file, other.m_file) != 0) + return std::strcmp(m_file, other.m_file) < 0; + return m_name.compare(other.m_name) < 0; +} + +#ifndef DOCTEST_CONFIG_DISABLE +namespace detail { + +bool Subcase::checkFilters() { + if (g_cs->traversal.activeSubcaseDepth() < static_cast(g_cs->subcase_filter_levels)) { + if (!matchesAny(m_signature.m_name.c_str(), g_cs->filters[6], true, g_cs->case_sensitive)) + return true; + if (matchesAny(m_signature.m_name.c_str(), g_cs->filters[7], false, g_cs->case_sensitive)) + return true; + } + return false; +} + +Subcase::Subcase(const String &name, const char *file, int line) + : m_signature({name, file, line}) { + if (checkFilters()) + return; + + if (!g_cs->traversal.tryEnterSubcase(m_signature)) + return; + + m_entered = true; + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); +} + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + +Subcase::~Subcase() { + if (m_entered) { + g_cs->traversal.leaveSubcase(); + +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && \ + (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if (std::uncaught_exceptions() > 0 +#else + if (std::uncaught_exception() +#endif + && g_cs->shouldLogCurrentException) { + DOCTEST_ITERATE_THROUGH_REPORTERS( + test_case_exception, + {"exception thrown in subcase - will translate later " + "when the whole test case has been exited (cannot " + "translate while there is an active exception)", + false} + ); + g_cs->shouldLogCurrentException = false; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } +} + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +Subcase::operator bool() const { + return m_entered; +} + +} // namespace detail +#endif // DOCTEST_CONFIG_DISABLE + +} // namespace doctest + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +std::set &getRegisteredTests() { + static std::set data; + return data; +} + +TestCase::TestCase( + funcType test, const char *file, unsigned line, const TestSuite &test_suite, const String &type, int template_id +) noexcept { + m_file = file; + m_line = line; + m_name = nullptr; // will be later overridden in operator* + m_test_suite = test_suite.m_test_suite; + m_description = test_suite.m_description; + m_skip = test_suite.m_skip; + m_no_breaks = test_suite.m_no_breaks; + m_no_output = test_suite.m_no_output; + m_may_fail = test_suite.m_may_fail; + m_should_fail = test_suite.m_should_fail; + m_expected_failures = test_suite.m_expected_failures; + m_timeout = test_suite.m_timeout; + + m_test = test; + m_type = type; + m_template_id = template_id; +} + +TestCase::TestCase(const TestCase &other) noexcept // NOLINT(bugprone-copy-constructor-init) + : TestCaseData() { + *this = other; +} + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function +TestCase &TestCase::operator=(const TestCase &other) noexcept { // NOLINT(cert-oop54-cpp) + TestCaseData::operator=(other); + m_test = other.m_test; + m_type = other.m_type; + m_template_id = other.m_template_id; + m_full_name = other.m_full_name; + + if (m_template_id != -1) + m_name = m_full_name.c_str(); + return *this; +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +TestCase &TestCase::operator*(const char *in) noexcept { + m_name = in; + // make a new name with an appended type for templated test case + if (m_template_id != -1) { + m_full_name = String(m_name) + "<" + m_type + ">"; + // redirect the name to point to the newly constructed full name + m_name = m_full_name.c_str(); + } + return *this; +} + +bool TestCase::operator<(const TestCase &other) const noexcept { + // this will be used only to differentiate between test cases - not relevant for sorting + if (m_line != other.m_line) + return m_line < other.m_line; + const int name_cmp = strcmp(m_name, other.m_name); + if (name_cmp != 0) + return name_cmp < 0; + const int file_cmp = m_file.compare(other.m_file); + if (file_cmp != 0) + return file_cmp < 0; + return m_template_id < other.m_template_id; +} + +// used by the macros for registering tests +int regTest(const TestCase &tc) noexcept { + getRegisteredTests().insert(tc); + return 0; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +TestSuite &TestSuite::operator*(const char *in) noexcept { + m_test_suite = in; + return *this; +} + +// sets the current test suite +int setTestSuite(const TestSuite &ts) noexcept { + doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; + return 0; +} + +} // namespace detail +} // namespace doctest + +namespace doctest_detail_test_suite_ns { +// holds the current test suite +doctest::detail::TestSuite &getCurrentTestSuite() noexcept { + static doctest::detail::TestSuite data{}; + return data; +} +} // namespace doctest_detail_test_suite_ns + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +#ifdef DOCTEST_CONFIG_GETCURRENTTICKS +ticks_t getCurrentTicks() { + return DOCTEST_CONFIG_GETCURRENTTICKS(); +} +#elif defined(DOCTEST_PLATFORM_WINDOWS) +ticks_t getCurrentTicks() { + static LARGE_INTEGER hz = {{0}}, hzo = {{0}}; + if (!hz.QuadPart) { + QueryPerformanceFrequency(&hz); + QueryPerformanceCounter(&hzo); + } + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; +} +#else // DOCTEST_PLATFORM_WINDOWS +ticks_t getCurrentTicks() { + timeval t; + gettimeofday(&t, nullptr); + return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); +} +#endif // DOCTEST_PLATFORM_WINDOWS + +void Timer::start() { + m_ticks = getCurrentTicks(); +} + +unsigned int Timer::getElapsedMicroseconds() const { + return static_cast(getCurrentTicks() - m_ticks); +} + +// unsigned int Timer::getElapsedMilliseconds() const { +// return static_cast(getElapsedMicroseconds() / 1000); +// } + +double Timer::getElapsedSeconds() const { + return static_cast(getCurrentTicks() - m_ticks) / 1000000.0; +} + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + + +#include + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +DOCTEST_NOINLINE DecisionPoint &TraversalState::ensureDecisionPointAtCurrentDepth() { + const size_t depth = m_decisionDepth; + + if (m_discoveredDecisionPath.size() == depth) { + m_discoveredDecisionPath.emplace_back(); + + if (m_decisionPath.size() == depth) + m_decisionPath.push_back(0); + } + + return m_discoveredDecisionPath[depth]; +} + +void TraversalState::resetForTestCase() { + m_decisionPath.clear(); + m_discoveredDecisionPath.clear(); + m_enteredSubcaseDepths.clear(); + m_activeSubcaseDepth = 0; + m_decisionDepth = 0; +} + +void TraversalState::resetForRun() { + m_activeSubcaseDepth = 0; + m_discoveredDecisionPath.clear(); + m_decisionDepth = 0; + m_enteredSubcaseDepths.clear(); +} + +bool TraversalState::advance() { + const size_t maxDepth = std::min(m_decisionPath.size(), m_discoveredDecisionPath.size()); + for (size_t depth = maxDepth; depth > 0; --depth) { + const size_t index = depth - 1; + if (m_decisionPath[index] + 1 < m_discoveredDecisionPath[index].branch_count) { + ++m_decisionPath[index]; + m_decisionPath.resize(index + 1); + return true; + } + } + + return false; +} + +bool TraversalState::tryEnterSubcase(const SubcaseSignature &signature) { + DecisionPoint &point = ensureDecisionPointAtCurrentDepth(); + std::vector &subcases = point.subcases; + size_t siblingIndex = 0; + + for (; siblingIndex < subcases.size(); ++siblingIndex) { + if (subcases[siblingIndex] == signature) + break; + } + + if (siblingIndex == subcases.size()) + subcases.push_back(signature); + + point.branch_count = subcases.size(); + + if (siblingIndex != m_decisionPath[m_decisionDepth]) + return false; + + m_enteredSubcaseDepths.push_back(m_decisionDepth); + m_activeSubcaseDepth++; + m_decisionDepth++; + return true; +} + +void TraversalState::leaveSubcase() { + m_decisionDepth = m_enteredSubcaseDepths.back(); + m_enteredSubcaseDepths.pop_back(); + m_activeSubcaseDepth--; +} + +size_t TraversalState::unwindActiveSubcases() { + const size_t activeSubcaseCount = m_activeSubcaseDepth; + + while (m_activeSubcaseDepth > 0) + leaveSubcase(); + + return activeSubcaseCount; +} + +size_t TraversalState::acquireGeneratorIndex(size_t count) { + DecisionPoint &point = ensureDecisionPointAtCurrentDepth(); + point.branch_count = count; + + const size_t index = m_decisionPath[m_decisionDepth]; + m_decisionDepth++; + return index < count ? index : 0; +} + +size_t acquireGeneratorDecisionIndex(size_t count) { + return g_cs->traversal.acquireGeneratorIndex(count); +} +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_PUSH + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace doctest { +namespace detail { + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= +/* clang-format off */ /* NOLINTBEGIN */ + +using uchar = unsigned char; + + size_t trailingBytes(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return 2; + } + if ((c & 0xF0) == 0xE0) { + return 3; + } + if ((c & 0xF8) == 0xF0) { + return 4; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + uint32_t headerValue(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return c & 0x1F; + } + if ((c & 0xF0) == 0xE0) { + return c & 0x0F; + } + if ((c & 0xF8) == 0xF0) { + return c & 0x07; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + void hexEscapeChar(std::ostream& os, unsigned char c) { + std::ios_base::fmtflags f(os.flags()); + os << "\\x" + << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(c); + os.flags(f); + } + + XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) + : m_str( str ), + m_forWhat( forWhat ) + {} + + void XmlEncode::encodeTo( std::ostream& os ) const { + // Apostrophe escaping not necessary if we always use " to write attributes + // (see: https://www.w3.org/TR/xml/#syntax) + + for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { + uchar c = m_str[idx]; + switch (c) { + case '<': os << "<"; break; + case '&': os << "&"; break; + + case '>': + // See: https://www.w3.org/TR/xml/#syntax + if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') + os << ">"; + else + os << c; + break; + + case '\"': + if (m_forWhat == ForAttributes) + os << """; + else + os << c; + break; + + default: + // Check for control characters and invalid utf-8 + + // Escape control characters in standard ascii + // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { + hexEscapeChar(os, c); + break; + } + + // Plain ASCII: Write it to stream + if (c < 0x7F) { + os << c; + break; + } + + // UTF-8 territory + // Check if the encoding is valid and if it is not, hex escape bytes. + // Important: We do not check the exact decoded values for validity, only the encoding format + // First check that this bytes is a valid lead byte: + // This means that it is not encoded as 1111 1XXX + // Or as 10XX XXXX + if (c < 0xC0 || + c >= 0xF8) { + hexEscapeChar(os, c); + break; + } + + auto encBytes = trailingBytes(c); + // Are there enough bytes left to avoid accessing out-of-bounds memory? + if (idx + encBytes - 1 >= m_str.size()) { + hexEscapeChar(os, c); + break; + } + // The header is valid, check data + // The next encBytes bytes must together be a valid utf-8 + // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + bool valid = true; + uint32_t value = headerValue(c); + for (std::size_t n = 1; n < encBytes; ++n) { + uchar nc = m_str[idx + n]; + valid &= ((nc & 0xC0) == 0x80); + value = (value << 6) | (nc & 0x3F); + } + + if ( + // Wrong bit pattern of following bytes + (!valid) || + // Overlong encodings + (value < 0x80) || + ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant + (0x800 < value && value < 0x10000 && encBytes > 3) || + // Encoded value out of range + (value >= 0x110000) + ) { + hexEscapeChar(os, c); + break; + } + + // If we got here, this is in fact a valid(ish) utf-8 sequence + for (std::size_t n = 0; n < encBytes; ++n) { + os << m_str[idx + n]; + } + idx += encBytes - 1; + break; + } + } + } + + std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { + xmlEncode.encodeTo( os ); + return os; + } + + XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + {} + + XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT + : m_writer( other.m_writer ){ + other.m_writer = nullptr; + } + XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT { + if ( m_writer ) { + m_writer->endElement(); + } + m_writer = other.m_writer; + other.m_writer = nullptr; + return *this; + } + + + XmlWriter::ScopedElement::~ScopedElement() { + if( m_writer ) + m_writer->endElement(); + } + + XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { + m_writer->writeText( text, indent ); + return *this; + } + + XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) + { + // writeDeclaration(); // called explicitly by the reporters that use the writer class - see issue #627 + } + + XmlWriter::~XmlWriter() { + while( !m_tags.empty() ) + endElement(); + } + + XmlWriter& XmlWriter::startElement( std::string const& name ) { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << '<' << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& XmlWriter::endElement() { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) { + m_os << "/>"; + m_tagIsOpen = false; + } + else { + m_os << m_indent << ""; + } + m_os << std::endl; + m_tags.pop_back(); + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { + if( !name.empty() && !attribute.empty() ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { + if( !name.empty() && attribute && attribute[0] != '\0' ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { + m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { + if( !text.empty() ){ + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen && indent ) + m_os << m_indent; + m_os << XmlEncode( text ); + m_needsNewline = true; + } + return *this; + } + + //XmlWriter& XmlWriter::writeComment( std::string const& text ) { + // ensureTagClosed(); + // m_os << m_indent << ""; + // m_needsNewline = true; + // return *this; + //} + + //void XmlWriter::writeStylesheetRef( std::string const& url ) { + // m_os << "\n"; + //} + + //XmlWriter& XmlWriter::writeBlankLine() { + // ensureTagClosed(); + // m_os << '\n'; + // return *this; + //} + + void XmlWriter::ensureTagClosed() { + if( m_tagIsOpen ) { + m_os << ">" << std::endl; + m_tagIsOpen = false; + } + } + + void XmlWriter::writeDeclaration() { + m_os << "\n"; + } + + void XmlWriter::newlineIfNecessary() { + if( m_needsNewline ) { + m_os << std::endl; + m_needsNewline = false; + } + } + +/* clang-format on */ /* NOLINTEND */ +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + +} // namespace detail +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_SUPPRESS_PRIVATE_WARNINGS_POP + +#endif // defined(DOCTEST_CONFIG_IMPLEMENT) && !defined(DOCTEST_LIBRARY_IMPLEMENTATION) diff --git a/lib/default/TasmotaPubSub/tests/src/publish_test.cpp b/lib/default/TasmotaPubSub/tests/src/publish_test.cpp new file mode 100644 index 000000000..3a3dd0cff --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/publish_test.cpp @@ -0,0 +1,467 @@ +/* + publish_test.cpp - publish / publish_P / retained-flag characterization for + the TasmotaPubSub host test system (task 10.2). + + Baseline (TEST_SUITE("baseline")): + - publish() with a topic and payload records a structurally valid PUBLISH + packet (Requirement 9.1). + - publish_P() records a structurally valid PUBLISH packet (Requirement 9.2). + - Retained-message flag handling: the decoded retain bit matches the + requested flag across both publish paths (Requirement 18.4). + + Property 4 (PUBLISH field round-trip: decoded topic == input topic, decoded + payload == input payload, retain bit == requested flag) and Property 1 + (remaining-length codec round-trip) are folded in as single, deterministic, + data-driven cases over curated boundary tables (no randomized generators). + + Curated boundary coverage (design "Property validation via curated + data-driven tests"): payload/topic lengths 0, 1, 127, 128, 16383, 16384, + 65535, 65536 and near-buffer sizes; topics/payloads that are empty, typical, + max-fitting, and multi-byte/UTF-8; retained flag on and off. + + The default working buffer is MQTT_MAX_PACKET_SIZE = 1200 and the buffer size + is a uint16_t, so the buffered publish() path cannot frame very large + payloads; this file characterizes the library's *actual* observable behavior + there (publish() returns false and emits no packet) rather than assuming a + packet is produced. The streamed publish_P() path frames large payloads + because its payload is not buffered, so it is used to exercise the 2-/3-byte + Remaining Length boundaries against the library's real emitted bytes; the + 4-byte boundary is driven through the MqttPacket/MqttParser codec. + + Every assertion goes through the public API and decoded MockClient.outbound() + wire bytes - never private members - so the baseline stays durable across a + future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Deterministic payload of length n. The pattern intentionally produces 0x00 +// bytes so the round-trip also proves the paths are binary-safe (no reliance on +// C-string NUL termination for the explicit-length / streamed overloads). +std::vector makePayload(size_t n) { + std::vector v(n); + for (size_t i = 0; i < n; ++i) { + v[i] = static_cast((i * 31u + 7u) & 0xFFu); + } + return v; +} + +// A topic of exactly n 'a' bytes (used for length-boundary coverage). +std::string makeTopic(size_t n) { + return std::string(n, 'a'); +} + +// Connect the client/psc pair with a scripted CONNACK and clear the recorded +// CONNECT bytes so only the packet-under-test remains in outbound(). The buffer +// size can be pre-set by the caller before invoking this helper. +void connectAndClear(MockClient& client, PubSubClient& psc) { + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("pub-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 9.1: publish() records a valid PUBLISH packet ---------------------- + + TEST_CASE("publish with a topic and payload records a valid PUBLISH packet") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.publish("tele/dev/SENSOR", "hello world")); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.qos == 0); + CHECK(d.msgId == 0); // QoS 0 => no packet identifier + CHECK_FALSE(d.retain); + CHECK(d.topic == "tele/dev/SENSOR"); + const std::string msg = "hello world"; + const std::vector expected(msg.begin(), msg.end()); + CHECK(d.payload == expected); + } + + // --- 9.2: publish_P() records a valid PUBLISH packet -------------------- + + TEST_CASE("publish_P records a valid PUBLISH packet") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::string topic = "tele/dev/STATE"; + const std::vector payload = makePayload(64); + REQUIRE(psc.publish_P(topic.c_str(), payload.data(), + static_cast(payload.size()), false)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.qos == 0); + CHECK(d.msgId == 0); + CHECK_FALSE(d.retain); + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + + // --- 18.4: retained-message flag handling ------------------------------- + + TEST_CASE("retained flag round-trips through publish and publish_P") { + const bool retainedFlags[] = {false, true}; + for (bool retained : retainedFlags) { + CAPTURE(retained); + + // publish(topic, payload, retained) - C-string overload. + { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.publish("tele/dev/POWER", "ON", retained)); + + DecodedPublish d = MqttParser::decodePublish(client.outbound()); + REQUIRE(d.valid); + CHECK(d.retain == retained); + CHECK(d.topic == "tele/dev/POWER"); + const std::vector expected{'O', 'N'}; + CHECK(d.payload == expected); + } + + // publish(topic, payload, plength, retained) - explicit-length overload. + { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::vector payload = makePayload(32); + REQUIRE(psc.publish("stat/dev/RESULT", payload.data(), + static_cast(payload.size()), retained)); + + DecodedPublish d = MqttParser::decodePublish(client.outbound()); + REQUIRE(d.valid); + CHECK(d.retain == retained); + CHECK(d.payload == payload); + } + + // publish_P(topic, payload, plength, retained). + { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::vector payload = makePayload(48); + REQUIRE(psc.publish_P("cmnd/dev/Backlog", payload.data(), + static_cast(payload.size()), retained)); + + DecodedPublish d = MqttParser::decodePublish(client.outbound()); + REQUIRE(d.valid); + CHECK(d.retain == retained); + CHECK(d.payload == payload); + } + } + } + + // --- Property 4: PUBLISH field round-trip over curated payload lengths --- + + // Feature: tasmota-pubsub-tests, Property 4: for all topics and payloads + // (including the retained flag and empty payloads), every publish path emits + // a PUBLISH whose decoded topic equals the input topic, whose decoded payload + // equals the input payload, and whose retain bit equals the requested flag. + // Validated deterministically over a curated boundary table (no randomized + // generators). The streamed publish_P() path frames every size because its + // payload is not buffered. + TEST_CASE("Property 4: publish_P PUBLISH field round-trip over payload-length boundaries") { + // Payload-length boundaries: empty, single byte, the 1->2 and 2->3 byte + // Remaining Length transitions, and sizes that exceed the 16-bit range. + const size_t payloadLengths[] = {0, 1, 127, 128, 16383, 16384, 65535, 65536}; + const bool retainedFlags[] = {false, true}; + const std::string topic = "tele/dev/SENSOR"; + + for (size_t plen : payloadLengths) { + for (bool retained : retainedFlags) { + CAPTURE(plen); + CAPTURE(retained); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::vector payload = makePayload(plen); + REQUIRE(psc.publish_P(topic.c_str(), payload.data(), + static_cast(payload.size()), retained)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.qos == 0); + CHECK(d.msgId == 0); + CHECK(d.retain == retained); + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + } + } + + // Property 4 for the buffered publish() path over the payload lengths that + // fit a working buffer. Small sizes use the default 1200-byte buffer; the + // 16383/16384 sizes use an enlarged buffer (still within the uint16_t buffer + // size limit) so the real buffered framing path is exercised end to end. + TEST_CASE("Property 4: buffered publish PUBLISH field round-trip within buffer capacity") { + struct Case { size_t plen; uint16_t bufferSize; }; + const Case cases[] = { + {0, MQTT_MAX_PACKET_SIZE}, + {1, MQTT_MAX_PACKET_SIZE}, + {127, MQTT_MAX_PACKET_SIZE}, + {128, MQTT_MAX_PACKET_SIZE}, + {16383, 20000}, + {16384, 20000}, + }; + const bool retainedFlags[] = {false, true}; + const std::string topic = "tele/dev/SENSOR"; + + for (const Case& c : cases) { + for (bool retained : retainedFlags) { + CAPTURE(c.plen); + CAPTURE(retained); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + REQUIRE(psc.setBufferSize(c.bufferSize)); + REQUIRE(psc.getBufferSize() == c.bufferSize); + connectAndClear(client, psc); + + const std::vector payload = makePayload(c.plen); + REQUIRE(psc.publish(topic.c_str(), payload.data(), + static_cast(payload.size()), retained)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.qos == 0); + CHECK(d.retain == retained); + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + } + } + + // Property 4 over curated topic-length and multi-byte/UTF-8 vectors: empty, + // single byte, the 127/128 boundary, a near-buffer topic, and UTF-8 topics + // and payloads. Both publish paths are checked for each vector. + TEST_CASE("Property 4: PUBLISH topic round-trip incl. multi-byte/UTF-8 and near-buffer") { + struct Case { const char* label; std::string topic; std::vector payload; }; + // A UTF-8 topic (degree sign + checkmark) and a UTF-8 payload. + const std::string utf8Topic = "tele/\xC2\xB0" "C/\xE2\x9C\x93"; + const std::string utf8PayloadStr = "temp=21\xC2\xB0" "C \xE2\x9C\x93"; + const std::vector utf8Payload(utf8PayloadStr.begin(), utf8PayloadStr.end()); + + std::vector cases = { + {"empty topic", makeTopic(0), makePayload(4)}, + {"single-char topic",makeTopic(1), makePayload(4)}, + {"topic len 127", makeTopic(127), makePayload(8)}, + {"topic len 128", makeTopic(128), makePayload(8)}, + {"near-buffer topic",makeTopic(1000), makePayload(8)}, + {"utf8 topic+payload", utf8Topic, utf8Payload}, + }; + + for (const Case& c : cases) { + CAPTURE(c.label); + + // Streamed path (publish_P): frames every case (topic fits 1200). + { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.publish_P(c.topic.c_str(), c.payload.data(), + static_cast(c.payload.size()), false)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.topic == c.topic); + CHECK(d.payload == c.payload); + } + + // Buffered path (publish): all these cases fit the default 1200 buffer. + { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.publish(c.topic.c_str(), c.payload.data(), + static_cast(c.payload.size()), false)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.topic == c.topic); + CHECK(d.payload == c.payload); + } + } + } + + // --- Buffered publish oversized-payload behavior (characterization) ------ + + // The buffered publish() path must frame the whole packet in the working + // buffer. Characterize the library's real behavior when the payload cannot + // fit: publish() returns false and emits no bytes. The buffer size is a + // uint16_t, so payloads at/above ~65529 bytes can never fit any buffer. + TEST_CASE("buffered publish refuses payloads that exceed the working buffer") { + SUBCASE("payload just over the default buffer capacity emits nothing") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); // default buffer 1200 + connectAndClear(client, psc); + + // topic "t" (len 1): fits when 1200 >= 7 + 1 + plength, i.e. plength <= 1192. + const std::vector fits = makePayload(1192); + REQUIRE(psc.publish("t", fits.data(), + static_cast(fits.size()), false)); + REQUIRE(MqttParser::isStructurallyValidPublish(client.outbound())); + + client.clearOutbound(); + const std::vector tooBig = makePayload(1193); + CHECK_FALSE(psc.publish("t", tooBig.data(), + static_cast(tooBig.size()), false)); + CHECK(client.outbound().empty()); + } + + SUBCASE("payloads >= 16-bit buffer limit cannot be buffered even at max buffer") { + const size_t hugeLengths[] = {16384, 65535, 65536}; + for (size_t plen : hugeLengths) { + CAPTURE(plen); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + REQUIRE(psc.setBufferSize(65535)); // largest a uint16_t buffer allows + connectAndClear(client, psc); + + // 16384 fits a 65535 buffer; 65535/65536 cannot (need > uint16_t). + const std::vector payload = makePayload(plen); + const bool ok = psc.publish("t", payload.data(), + static_cast(payload.size()), false); + if (plen + 8 <= 65535) { + CHECK(ok); + CHECK(MqttParser::isStructurallyValidPublish(client.outbound())); + } else { + CHECK_FALSE(ok); + CHECK(client.outbound().empty()); + } + } + } + } + + // --- Property 1: remaining-length codec round-trip ---------------------- + + // Feature: tasmota-pubsub-tests, Property 1: for all integers n in the MQTT + // range 0..268435455, encoding n as an MQTT Remaining Length and decoding it + // yields n using at most four bytes. Exercised deterministically at the + // 1/2/3/4-byte boundaries via the MqttPacket/MqttParser codec, which mirrors + // the library's buildHeader. + TEST_CASE("Property 1: Remaining Length codec round-trip at the byte-count boundaries") { + struct RLCase { uint32_t value; size_t bytes; }; + const RLCase cases[] = { + {0, 1}, // minimum + {1, 1}, + {127, 1}, // last 1-byte value + {128, 2}, // first 2-byte value + {16383, 2}, // last 2-byte value + {16384, 3}, // first 3-byte value + {2097151, 3}, // last 3-byte value + {2097152, 4}, // first 4-byte value + {268435455, 4}, // MQTT maximum Remaining Length + }; + + for (const RLCase& c : cases) { + CAPTURE(c.value); + const std::vector enc = MqttPacket::encodeRemainingLength(c.value); + CHECK(enc.size() == c.bytes); + + uint32_t decoded = 0xFFFFFFFFu; + size_t consumed = 0; + REQUIRE(MqttParser::decodeRemainingLength(enc, 0, decoded, consumed)); + CHECK(decoded == c.value); + CHECK(consumed == c.bytes); + } + } + + // Property 1 against the library's real emitted bytes: for PUBLISH packets + // whose Remaining Length crosses the 1->2 and 2->3 byte transitions, the + // decoded Remaining Length equals the actual trailing byte count and equals + // 2 + topicLen + plength. The streamed publish_P() path is used so the + // larger sizes are framed (the buffered path cannot hold them). + TEST_CASE("Property 1: emitted PUBLISH Remaining Length equals trailing byte count") { + const std::string topic = "t"; // topicLen == 1 + const size_t topicLen = topic.size(); + // Remaining Length = 2 + topicLen + plength. Choose plength so RL lands + // on each byte-count boundary. + struct Case { size_t plen; size_t expectedRlBytes; }; + const Case cases[] = { + {127 - 2 - 1, 1}, // RL = 127 (1 byte) + {128 - 2 - 1, 2}, // RL = 128 (2 bytes) + {16383 - 2 - 1, 2}, // RL = 16383 (2 bytes) + {16384 - 2 - 1, 3}, // RL = 16384 (3 bytes) + }; + + for (const Case& c : cases) { + CAPTURE(c.plen); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::vector payload = makePayload(c.plen); + REQUIRE(psc.publish_P(topic.c_str(), payload.data(), + static_cast(payload.size()), false)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + // Decode the Remaining Length field directly from the emitted bytes. + uint32_t rl = 0; + size_t rlBytes = 0; + REQUIRE(MqttParser::decodeRemainingLength(out, 1, rl, rlBytes)); + CHECK(rlBytes == c.expectedRlBytes); + + // Declared Remaining Length equals the actual trailing byte count... + const size_t trailing = out.size() - (1 + rlBytes); + CHECK(rl == trailing); + // ...and equals 2 + topicLen + plength (topic-length prefix + topic + payload). + CHECK(rl == 2 + topicLen + c.plen); + } + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/receive_test.cpp b/lib/default/TasmotaPubSub/tests/src/receive_test.cpp new file mode 100644 index 000000000..e191bd49e --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/receive_test.cpp @@ -0,0 +1,404 @@ +/* + receive_test.cpp - inbound PUBLISH parsing and callback dispatch + characterization + hardening for the TasmotaPubSub host test system (task 10.5). + + Inbound PUBLISH bytes are scripted into the MockClient and delivered to the + *unmodified* library by calling psc.loop() (the callback is registered via + psc.setCallback; the std::function signature is enabled by the -DESP32 build + flag, so lambdas with captures can be bound). + + Baseline (TEST_SUITE("baseline")): + - A well-formed QoS 0 PUBLISH scripted inbound => the registered callback is + invoked with the correct topic, payload pointer, and payload length + (Requirement 11.1). + - A well-formed QoS 1 PUBLISH => the callback is invoked AND a PUBACK is + recorded outbound echoing the message id (Requirement 11.2). + - Property 6 (inbound PUBLISH callback round-trip for QoS {0,1}) is folded in + as a single deterministic, data-driven case (SUBCASE / table loops, no + randomized generators) over curated topic/payload vectors: empty payload, + typical, multi-byte/binary, and various topic lengths. The callback + captures (topic, payload copy, length) for assertion (Requirement 18.5). + + Hardening (TEST_SUITE("hardening")), Property 11 / F-02 + F-09 + (Requirements 11.3-11.6), all expected PASS under AddressSanitizer: + - A QoS 1 PUBLISH whose claimed topic length exceeds the received packet is + rejected without reading beyond the received bytes (Req 11.3). + - A short PUBLISH whose length arithmetic would underflow the payload length + is rejected without invoking the callback with an underflowed length + (Req 11.4). + - A PUBLISH with QoS value 3 is rejected (Req 11.5). + - A PUBLISH with QoS value 2 is rejected rather than parsed as QoS 0 + (Req 11.6). + On every rejected packet the callback is NOT invoked, and the library closes + the connection. Adversarial fixtures are built via MqttPacket / sized + std::vector so the test code itself performs no out-of-bounds access; + AddressSanitizer proves the library reads no bytes beyond those received. + + Every assertion goes through the public API, the registered callback, and + decoded MockClient.outbound() wire bytes - never private members - so the + baseline stays durable across a future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "FindingStatus.h" +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Records every inbound PUBLISH the library dispatches to the callback. The +// callback signature is std::function under +// -DESP32, so a lambda capturing this struct by reference can be registered. +struct CallbackCapture { + int count = 0; // number of callback invocations + std::string topic; // last topic (NUL-terminated C string) + const uint8_t* payloadPtr = nullptr; // last payload pointer as delivered + unsigned int length = 0; // last payload length as delivered + std::vector payload; // copy of the last payload bytes +}; + +// Connect the client/psc pair with a scripted CONNACK, register a callback that +// records into `cap`, and clear the recorded CONNECT bytes so only the +// packet-under-test remains in outbound(). +void connectWithCallback(MockClient& client, PubSubClient& psc, CallbackCapture& cap) { + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + psc.setCallback([&cap](char* topic, uint8_t* payload, unsigned int len) { + cap.count++; + cap.topic = topic; // library NUL-terminates the topic + cap.payloadPtr = payload; + cap.length = len; + // Copy exactly `len` bytes; for len == 0 this dereferences nothing. + cap.payload.assign(payload, payload + len); + }); + REQUIRE(psc.connect("recv-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +// Deterministic binary payload of length n (includes 0x00 bytes so the +// round-trip proves the dispatch is binary-safe and length-driven, not +// dependent on C-string termination). +std::vector makePayload(size_t n) { + std::vector v(n); + for (size_t i = 0; i < n; ++i) { + v[i] = static_cast((i * 31u + 7u) & 0xFFu); + } + return v; +} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 11.1: well-formed QoS 0 PUBLISH => callback dispatch --------------- + + TEST_CASE("inbound QoS 0 PUBLISH invokes the callback with correct topic/payload/length") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + const std::string topic = "tele/dev/SENSOR"; + const std::string msg = "hello world"; + const std::vector payload(msg.begin(), msg.end()); + + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/0)); + REQUIRE(psc.loop()); + + // The callback fired exactly once with the fixture topic, a non-null + // payload pointer, and the exact payload bytes and length. + CHECK(cap.count == 1); + CHECK(cap.topic == topic); + CHECK(cap.payloadPtr != nullptr); + CHECK(cap.length == payload.size()); + CHECK(cap.payload == payload); + + // A QoS 0 PUBLISH is not acknowledged, so nothing is written back. + CHECK(client.outbound().empty()); + CHECK(psc.connected()); + } + + // --- 11.2: well-formed QoS 1 PUBLISH => callback + PUBACK --------------- + + TEST_CASE("inbound QoS 1 PUBLISH invokes the callback and records a PUBACK echoing the msgId") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + const std::string topic = "cmnd/dev/POWER"; + const std::string msg = "ON"; + const std::vector payload(msg.begin(), msg.end()); + const uint16_t msgId = 0x1234; + + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/1, + /*retained=*/false, msgId)); + REQUIRE(psc.loop()); + + // Callback dispatched with the fixture fields. + CHECK(cap.count == 1); + CHECK(cap.topic == topic); + CHECK(cap.payloadPtr != nullptr); + CHECK(cap.length == payload.size()); + CHECK(cap.payload == payload); + + // A PUBACK echoing the message id is recorded outbound. + DecodedPacket ack = MqttParser::decode(client.outbound()); + REQUIRE(ack.valid); + CHECK(ack.type == static_cast(MQTTPUBACK)); + CHECK(ack.remainingLength == 2); + REQUIRE(ack.payload.size() == 2); + const uint16_t ackMsgId = + static_cast((ack.payload[0] << 8) | ack.payload[1]); + CHECK(ackMsgId == msgId); + + CHECK(psc.connected()); + } + + // --- Property 6: inbound PUBLISH callback round-trip (QoS 0 and 1) ------- + + // Feature: tasmota-pubsub-tests, Property 6: for all well-formed inbound + // PUBLISH fixtures with QoS in {0,1}, arbitrary nonempty topic, and arbitrary + // payload, the registered callback is invoked exactly once with a topic + // string equal to the fixture topic and a payload region whose bytes and + // length equal the fixture payload; and for QoS 1 a PUBACK echoing the + // fixture message id is recorded outbound. Validated deterministically over a + // curated table of topic/payload vectors (no randomized generators). + TEST_CASE("Property 6: inbound PUBLISH callback round-trip over curated topics/payloads") { + struct Case { const char* label; std::string topic; std::vector payload; }; + + // A multi-byte / UTF-8 topic and payload (degree sign + checkmark). + const std::string utf8Topic = "tele/\xC2\xB0" "C/\xE2\x9C\x93"; + const std::string utf8PayloadStr = "temp=21\xC2\xB0" "C \xE2\x9C\x93"; + const std::vector utf8Payload(utf8PayloadStr.begin(), utf8PayloadStr.end()); + + std::vector cases = { + {"single-char topic, empty payload", "a", {}}, + {"typical topic, short payload", "tele/dev/STATE", {'O','N'}}, + {"single-char topic, binary payload","t", makePayload(64)}, + {"long topic, empty payload", + "tele/very/deeply/nested/topic/filter/name", {}}, + {"long topic, binary payload", + "stat/tasmota_ABCDEF/RESULT", makePayload(200)}, + {"utf8 topic + utf8 payload", utf8Topic, utf8Payload}, + }; + + const uint8_t qosValues[] = {0, 1}; + + for (const Case& c : cases) { + for (uint8_t qos : qosValues) { + CAPTURE(c.label); + CAPTURE(qos); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + // A distinct nonzero message id per QoS-1 fixture. + const uint16_t msgId = static_cast(0x2000 + c.topic.size()); + client.pushPacket(MqttPacket::publish(c.topic, c.payload, qos, + /*retained=*/false, + qos ? msgId : 0)); + REQUIRE(psc.loop()); + + // Callback invoked exactly once with the fixture topic/payload. + CHECK(cap.count == 1); + CHECK(cap.topic == c.topic); + CHECK(cap.payloadPtr != nullptr); + CHECK(cap.length == c.payload.size()); + CHECK(cap.payload == c.payload); + + if (qos == 1) { + // QoS 1 => a PUBACK echoing the fixture message id. + DecodedPacket ack = MqttParser::decode(client.outbound()); + REQUIRE(ack.valid); + CHECK(ack.type == static_cast(MQTTPUBACK)); + CHECK(ack.remainingLength == 2); + REQUIRE(ack.payload.size() == 2); + const uint16_t ackMsgId = + static_cast((ack.payload[0] << 8) | ack.payload[1]); + CHECK(ackMsgId == msgId); + } else { + // QoS 0 => no acknowledgement is written. + CHECK(client.outbound().empty()); + } + + CHECK(psc.connected()); + } + } + } +} + +TEST_SUITE("hardening") { + + // Feature: tasmota-pubsub-tests, Property 11: for all inbound PUBLISH + // fixtures whose QoS bits encode 2 or 3, or whose claimed topic length + // exceeds the received packet length, or whose length arithmetic would + // underflow the payload length, the library rejects the packet without + // invoking the callback and without reading beyond the received bytes + // (verified clean under AddressSanitizer). + // + // F-02 (Requirement 11.3): a QoS 1 PUBLISH whose claimed topic length exceeds + // the received packet is rejected. The fixture is a fully well-framed frame + // (Remaining Length equals the trailing byte count) whose 2-byte topic-length + // field claims far more bytes than the packet contains, so the library's + // guard header_len = llen + 3 + tl + 2 > len fires. All reads stay within the + // scripted bytes, so ASan proves no over-read. F-02 is hardened (expected PASS). + TEST_CASE("F-02 QoS 1 PUBLISH with claimed topic length exceeding the packet is rejected" + * FINDING_MARKER(F02)) { + // Each fixture: fixed header PUBLISH|QoS1 (0x32), a single Remaining + // Length byte, then `bodyLen` body bytes whose first two bytes are the + // (oversized) topic-length field. The claimed topic length far exceeds + // the body, so header_len > len. + struct Case { uint8_t bodyLen; uint16_t claimedTopicLen; }; + const Case cases[] = { + {4, 500}, // tl=500 but only 4 body bytes present + {6, 1000}, + {10, 0x7FFF}, // near-max claimed topic length + {2, 300}, // only the topic-length field present, no topic bytes + }; + + for (const Case& c : cases) { + CAPTURE(c.bodyLen); + CAPTURE(c.claimedTopicLen); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + // Build the adversarial frame in a sized vector (no OOB in test code). + std::vector frame; + frame.push_back(static_cast(MQTTPUBLISH | MQTTQOS1)); // 0x32 + frame.push_back(c.bodyLen); // RL (< 128) + frame.push_back(static_cast(c.claimedTopicLen >> 8)); // topic len hi + frame.push_back(static_cast(c.claimedTopicLen & 0xFF));// topic len lo + // Remaining body filler so the frame length matches the declared RL. + for (uint8_t i = 2; i < c.bodyLen; ++i) { + frame.push_back(static_cast('x')); + } + client.pushPacket(MqttPacket::raw(frame)); + + // Bound any read to a short virtual timeout as a safety net. + psc.setSocketTimeout(1); + psc.loop(); + + // The packet is rejected: no callback, connection closed. + CHECK(cap.count == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + } + + // F-02 (Requirement 11.4): a short PUBLISH whose length arithmetic would + // underflow the payload length (payload length = len - llen - 3 - tl for + // QoS 0) must not invoke the callback with an underflowed (huge) length. The + // library's guard header_len > len rejects the frame before the subtraction, + // so the callback is never reached. F-02 is hardened (expected PASS). + TEST_CASE("F-02 short PUBLISH that would underflow the payload length is rejected" + * FINDING_MARKER(F02)) { + // QoS 0 frames whose claimed topic length is just large enough that the + // header (llen + 3 + tl) exceeds the received length: without the guard + // the payload length len - llen - 3 - tl would wrap to a huge value. + struct Case { uint8_t bodyLen; uint16_t claimedTopicLen; }; + const Case cases[] = { + {5, 10}, // len=7, header_len=1+3+10=14 > 7 + {3, 8}, // len=5, header_len=1+3+8=12 > 5 + {2, 5}, // len=4, header_len=1+3+5=9 > 4 (no topic bytes at all) + {8, 20}, // len=10, header_len=1+3+20=24 > 10 + }; + + for (const Case& c : cases) { + CAPTURE(c.bodyLen); + CAPTURE(c.claimedTopicLen); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + std::vector frame; + frame.push_back(static_cast(MQTTPUBLISH)); // 0x30, QoS 0 + frame.push_back(c.bodyLen); // RL (< 128) + frame.push_back(static_cast(c.claimedTopicLen >> 8)); // topic len hi + frame.push_back(static_cast(c.claimedTopicLen & 0xFF));// topic len lo + for (uint8_t i = 2; i < c.bodyLen; ++i) { + frame.push_back(static_cast('y')); + } + client.pushPacket(MqttPacket::raw(frame)); + + psc.setSocketTimeout(1); + psc.loop(); + + // No callback (so no underflowed length was ever delivered), and the + // connection is closed. + CHECK(cap.count == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + } + + // F-09 (Requirement 11.5): a PUBLISH whose QoS bits encode 3 (a protocol + // violation) is rejected. The fixture is otherwise well-framed (valid topic + // and payload), so a naive parser would dispatch it; the library rejects + // qos > 1 up front. F-09 is hardened (expected PASS). + TEST_CASE("F-09 inbound PUBLISH with QoS 3 is rejected" * FINDING_MARKER(F09)) { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + const std::string topic = "tele/dev/SENSOR"; + const std::vector payload{'d', 'a', 't', 'a'}; + // qos=3 sets both QoS bits; the builder emits a message id (qos > 0) so + // the frame is well-formed apart from the illegal QoS. + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/3, + /*retained=*/false, /*msgId=*/0x0007)); + + psc.setSocketTimeout(1); + psc.loop(); + + CHECK(cap.count == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } + + // F-09 (Requirement 11.6): an inbound QoS 2 PUBLISH is rejected rather than + // parsed as QoS 0. The fixture is a fully well-framed QoS 2 PUBLISH (topic, + // packet identifier, payload); if the library ignored the QoS bits it would + // dispatch the callback. Because it rejects qos > 1, the callback never + // fires. F-09 is hardened (expected PASS). + TEST_CASE("F-09 inbound PUBLISH with QoS 2 is rejected rather than parsed as QoS 0" + * FINDING_MARKER(F09)) { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + CallbackCapture cap; + connectWithCallback(client, psc, cap); + + const std::string topic = "tele/dev/SENSOR"; + const std::vector payload = makePayload(16); + client.pushPacket(MqttPacket::publish(topic, payload, /*qos=*/2, + /*retained=*/false, /*msgId=*/0x0042)); + + psc.setSocketTimeout(1); + psc.loop(); + + // Not dispatched as QoS 0 (or anything else) - callback never fired. + CHECK(cap.count == 0); + CHECK_FALSE(psc.connected()); + CHECK(client.stopCalled()); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/streaming_test.cpp b/lib/default/TasmotaPubSub/tests/src/streaming_test.cpp new file mode 100644 index 000000000..cfd71df14 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/streaming_test.cpp @@ -0,0 +1,399 @@ +/* + streaming_test.cpp - beginPublish / write / endPublish streaming-publish + characterization + hardening for the TasmotaPubSub host test system (task 10.3). + + Baseline (TEST_SUITE("baseline")): + - A payload streamed via beginPublish() + write() + endPublish() is reflected + in the recorded outbound bytes (Requirement 9.3). beginPublish() emits only + the fixed header + Remaining Length + topic; the payload is streamed + afterwards through write(), so the recorded outbound bytes form one complete + PUBLISH packet whose decoded topic/payload equal the input. + - Property 4 (PUBLISH field round-trip: decoded topic == input, decoded + payload == input, retain bit == requested flag) is folded in for the + streaming path as a single deterministic, data-driven case (SUBCASE / table + loops, no randomized generators) covering both the single-byte write(data) + and the buffered write(buf,size) forms, various payload lengths, and the + retained flag on/off. + + Hardening (TEST_SUITE("hardening")): + - F-04 (Requirement 9.4, expected PASS): a declared payload length exceeding + the 16-bit range must not emit a truncated Remaining Length that desyncs the + framing. beginPublish() takes a uint32_t Remaining Length and buildHeader is + bounded to four length bytes. Because beginPublish() buffers only the header + (the payload is streamed via write()), a very large *declared* length is + feasible to frame without allocating the payload, so the emitted fixed-header + Remaining Length is verified to equal plength + 2 + topicLen (full 32-bit) + rather than a 16-bit truncation. Marked FINDING_MARKER(F04). + - Property 13 / F-05 (Requirement 9.5, expected FAIL): with a partial transport + write injected (MockClient::setWriteLimit), the publish path should report + failure and leave the connection unusable with intact framing so no later + packet is written onto a desynchronized stream. The current fork returns + success from endPublish() and reuses the connection, so the case is marked + FINDING_MARKER(F05) (should_fail). Cut points are strictly less than the full + packet length. + + Every assertion goes through the public API and decoded MockClient.outbound() + wire bytes - never private members - so the baseline stays durable across a + future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "FindingStatus.h" +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Deterministic payload of length n. The pattern intentionally produces 0x00 +// bytes so the round-trip also proves the streaming path is binary-safe (no +// reliance on C-string NUL termination). +std::vector makePayload(size_t n) { + std::vector v(n); + for (size_t i = 0; i < n; ++i) { + v[i] = static_cast((i * 31u + 7u) & 0xFFu); + } + return v; +} + +// Connect the client/psc pair with a scripted CONNACK and clear the recorded +// CONNECT bytes so only the packet-under-test remains in outbound(). +void connectAndClear(MockClient& client, PubSubClient& psc) { + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("stream-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +// How the payload is fed to the library after beginPublish(). +enum class WriteMode { Single, Buffered }; + +// Perform a complete streamed publish through the public API only: +// beginPublish() -> write() (per WriteMode) -> endPublish(). Returns true when +// every step reported success. All bytes written are recorded by the MockClient +// and, together with the header emitted by beginPublish(), form the full PUBLISH. +bool streamPublish(PubSubClient& psc, const std::string& topic, + const std::vector& payload, bool retained, + WriteMode mode) { + if (!psc.beginPublish(topic.c_str(), + static_cast(payload.size()), retained)) { + return false; + } + if (mode == WriteMode::Single) { + for (uint8_t b : payload) { + if (psc.write(b) != 1) { + return false; + } + } + } else { // WriteMode::Buffered + if (!payload.empty()) { + if (psc.write(payload.data(), payload.size()) != payload.size()) { + return false; + } + } + } + return psc.endPublish() != 0; +} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 9.3: streamed payload is reflected in the recorded outbound bytes --- + + TEST_CASE("streamed publish (single-byte write) is reflected in the outbound PUBLISH") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::string topic = "tele/dev/SENSOR"; + const std::string msg = "streamed hello"; + const std::vector payload(msg.begin(), msg.end()); + + REQUIRE(psc.beginPublish(topic.c_str(), + static_cast(payload.size()), false)); + for (uint8_t b : payload) { + REQUIRE(psc.write(b) == 1); + } + REQUIRE(psc.endPublish() != 0); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.qos == 0); + CHECK(d.msgId == 0); // streaming publish is QoS 0 (no packet id) + CHECK_FALSE(d.retain); + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + + TEST_CASE("streamed publish (buffered write) is reflected in the outbound PUBLISH") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::string topic = "stat/dev/RESULT"; + const std::vector payload = makePayload(200); + + REQUIRE(psc.beginPublish(topic.c_str(), + static_cast(payload.size()), true)); + REQUIRE(psc.write(payload.data(), payload.size()) == payload.size()); + REQUIRE(psc.endPublish() != 0); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.qos == 0); + CHECK(d.retain); // retained flag requested + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + + // The payload may be streamed across several write() calls; the recorded + // outbound bytes must still assemble into one complete, well-framed PUBLISH. + TEST_CASE("streamed publish across chunked writes assembles one framed PUBLISH") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::string topic = "tele/dev/CHUNKED"; + const std::vector payload = makePayload(50); + + REQUIRE(psc.beginPublish(topic.c_str(), + static_cast(payload.size()), false)); + // Mix single-byte and buffered writes to model an incremental producer. + REQUIRE(psc.write(payload[0]) == 1); + REQUIRE(psc.write(payload.data() + 1, 9) == 9); + for (size_t i = 10; i < 20; ++i) { + REQUIRE(psc.write(payload[i]) == 1); + } + REQUIRE(psc.write(payload.data() + 20, payload.size() - 20) + == payload.size() - 20); + REQUIRE(psc.endPublish() != 0); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + + // --- Property 4: streaming PUBLISH field round-trip --------------------- + + // Feature: tasmota-pubsub-tests, Property 4: for all topics and payloads + // (including the retained flag and empty payloads), every publish path emits + // a PUBLISH whose decoded topic equals the input topic, whose decoded payload + // equals the input payload, and whose retain bit equals the requested flag. + // Validated deterministically for the streaming path over a curated table of + // payload lengths, both write modes, and the retained flag (no randomized + // generators). + TEST_CASE("Property 4: streaming PUBLISH field round-trip over write modes and lengths") { + // Payload-length boundaries reachable through the default 1200-byte + // working buffer's header (the payload itself is streamed, not buffered): + // empty, single byte, the 1->2 byte Remaining Length transition, and a + // few larger sizes. + const size_t payloadLengths[] = {0, 1, 2, 127, 128, 255, 1000}; + const bool retainedFlags[] = {false, true}; + const WriteMode modes[] = {WriteMode::Single, WriteMode::Buffered}; + const std::string topic = "tele/dev/SENSOR"; + + for (WriteMode mode : modes) { + for (size_t plen : payloadLengths) { + for (bool retained : retainedFlags) { + CAPTURE(static_cast(mode)); + CAPTURE(plen); + CAPTURE(retained); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::vector payload = makePayload(plen); + REQUIRE(streamPublish(psc, topic, payload, retained, mode)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.qos == 0); + CHECK(d.msgId == 0); + CHECK(d.retain == retained); + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + } + } + } + + // Property 4 for streamed payloads that cross the 16-bit Remaining Length + // boundary, asserted against the library's *real* emitted bytes. The buffered + // publish() path cannot frame these (its payload must fit the working + // buffer), but the streaming path frames them because the payload is not + // buffered. This exercises the 2->3 byte Remaining Length transition and + // payloads above 65535 bytes end to end. + TEST_CASE("Property 4: streaming PUBLISH field round-trip across the 16-bit boundary") { + const size_t payloadLengths[] = {16383, 16384, 65535, 65536}; + const bool retainedFlags[] = {false, true}; + const std::string topic = "t"; // small topic keeps the header tiny + + for (size_t plen : payloadLengths) { + for (bool retained : retainedFlags) { + CAPTURE(plen); + CAPTURE(retained); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + const std::vector payload = makePayload(plen); + // Buffered write is used so the large payload is emitted in one + // call; the recorded outbound bytes are the full PUBLISH. + REQUIRE(streamPublish(psc, topic, payload, retained, WriteMode::Buffered)); + + const std::vector& out = client.outbound(); + REQUIRE(MqttParser::isStructurallyValidPublish(out)); + + DecodedPublish d = MqttParser::decodePublish(out); + REQUIRE(d.valid); + CHECK(d.retain == retained); + CHECK(d.topic == topic); + CHECK(d.payload == payload); + } + } + } +} + +TEST_SUITE("hardening") { + + // F-04 (Requirement 9.4): a declared payload length exceeding the 16-bit + // range must not emit a truncated Remaining Length that desynchronizes the + // packet framing. beginPublish() buffers only the header (fixed header + + // Remaining Length + topic) and streams the payload separately, so a very + // large *declared* length can be framed without allocating the payload. + // Verify the emitted Remaining Length equals plength + 2 + topicLen using the + // full 32-bit value rather than a 16-bit truncation, and that the header's + // byte layout stays self-consistent. F-04 is hardened in the current fork + // (32-bit buildHeader bounded to four length bytes), so this is expected PASS. + TEST_CASE("F-04 large declared payload length frames the Remaining Length without truncation" + * FINDING_MARKER(F04)) { + const std::string topic = "t"; // topicLen == 1 + const size_t topicLen = topic.size(); + // Declared lengths that force Remaining Length above the 16-bit range, + // including the 2->3 and 3->4 byte transitions and the MQTT maximum. + // Remaining Length = plength + 2 + topicLen. + const unsigned int plengths[] = { + 65533u, // RL = 65536 (first value needing 3 bytes) + 65536u, // RL = 65539 (3 bytes) + 100000u, // RL = 100003 (3 bytes) + 2097149u, // RL = 2097152 (first value needing 4 bytes) + 10000000u, // RL = 10000003 (4 bytes) + 268435455u - 2u - 1u, // RL = 268435455 (MQTT maximum, 4 bytes) + }; + + for (unsigned int plen : plengths) { + CAPTURE(plen); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + // beginPublish emits the header only; with no write limit the mock + // accepts every byte so beginPublish reports success. + REQUIRE(psc.beginPublish(topic.c_str(), plen, false)); + + const std::vector& out = client.outbound(); + REQUIRE(out.size() >= 2); + + // Fixed-header high nibble is PUBLISH, retain bit clear. + CHECK(static_cast(out[0] & 0xF0) == static_cast(MQTTPUBLISH)); + CHECK((out[0] & 0x01) == 0x00); + + // Decode the Remaining Length field directly from the emitted header. + uint32_t rl = 0; + size_t rlBytes = 0; + REQUIRE(MqttParser::decodeRemainingLength(out, 1, rl, rlBytes)); + + const uint32_t expected = + static_cast(plen) + 2u + static_cast(topicLen); + + // The Remaining Length is the full 32-bit value, not a 16-bit + // truncation that would desync framing. + CHECK(rl == expected); + if (expected > 0xFFFFu) { + CHECK(rl != (expected & 0xFFFFu)); + } + + // Header framing stays self-consistent: beginPublish emits exactly + // the fixed header (1) + Remaining Length bytes + topic length prefix + // (2) + topic bytes. No payload has been streamed yet. + CHECK(out.size() == 1u + rlBytes + 2u + topicLen); + + // The emitted 2-byte topic length prefix matches the topic. + const size_t topicLenPos = 1u + rlBytes; + REQUIRE(out.size() >= topicLenPos + 2u); + const uint16_t emittedTopicLen = static_cast( + (out[topicLenPos] << 8) | out[topicLenPos + 1]); + CHECK(emittedTopicLen == topicLen); + } + } + + // Feature: tasmota-pubsub-tests, Property 13: for all partial-write cut + // points strictly less than the full packet length, the publish path reports + // failure and leaves the connection unusable (lost/closed) so that no + // subsequent packet is written onto a desynchronized stream. The current fork + // returns success from endPublish() and keeps the connection, so this is + // expected to fail until F-05 is hardened. + TEST_CASE("F-05 partial transport write fails the streaming publish and disables reuse" + * FINDING_MARKER(F05)) { + const std::string topic = "s"; // topicLen == 1 + const std::vector payload = makePayload(300); + // Header = fixed(1) + Remaining Length(2, since RL=303) + topic len(2) + // + topic(1) = 6 bytes; full packet length = 6 + 300 = 306. Cut points + // are >= 6 (so beginPublish's header write completes) and < 300 (so the + // buffered payload write is truncated) - all strictly less than 306. + const size_t cutPoints[] = {10, 50, 150, 299}; + + for (size_t cut : cutPoints) { + CAPTURE(cut); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + client.setWriteLimit(cut); + + // The header (6 bytes) fits within the cut, so beginPublish succeeds. + REQUIRE(psc.beginPublish(topic.c_str(), + static_cast(payload.size()), false)); + + // The transport accepts fewer payload bytes than requested: a genuine + // partial write is injected here. + const size_t written = psc.write(payload.data(), payload.size()); + REQUIRE(written < payload.size()); + + // Hardened contract: the publish path reports failure... + const int endRc = psc.endPublish(); + CHECK(endRc == 0); + + // ...and leaves the connection unusable so no later packet can be + // written onto the now-desynchronized stream. + CHECK_FALSE(psc.connected()); + } + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/subscribe_test.cpp b/lib/default/TasmotaPubSub/tests/src/subscribe_test.cpp new file mode 100644 index 000000000..8dedc2087 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/subscribe_test.cpp @@ -0,0 +1,324 @@ +/* + subscribe_test.cpp - subscribe / unsubscribe characterization + hardening for + the TasmotaPubSub host test system (task 10.4). + + Baseline (TEST_SUITE("baseline")): + - subscribe() records a structurally valid SUBSCRIBE packet (Requirement 10.1). + - unsubscribe() records a structurally valid UNSUBSCRIBE packet (Req 10.2). + - Property 5 (SUBSCRIBE/UNSUBSCRIBE field round-trip: nonzero message id, the + trailing requested-QoS byte for SUBSCRIBE, and the topic filter equal to the + input) is folded in as a single deterministic, data-driven case (SUBCASE / + table loops, no randomized generators) over curated QoS {0,1} and topic + vectors. The explicit-QoS subscribe overload is covered here (Req 18.6). + + Hardening (TEST_SUITE("hardening")): + - F-08 (Requirement 10.3, expected PASS): a SUBSCRIBE whose topic length brings + the packet to exactly the working-buffer capacity does not write the trailing + QoS byte out of bounds. The buffer is sized to exactly 10 + topicLength (the + library's own bound: header(5) + msgId(2) + topic-length(2) + topic + qos(1)), + so the QoS byte lands at buffer[bufferSize - 1]; an off-by-one would write at + buffer[bufferSize] and trip AddressSanitizer. Marked FINDING_MARKER(F08). + - Null-topic rejection (Requirement 10.5, expected PASS): subscribe() and + unsubscribe() reject a null topic pointer before any length computation + dereferences it (no strnlen on nullptr, no packet emitted). Plain hardening + case (expected to pass against the current fork). + - F-10 (Requirement 10.4, expected FAIL): a subscribe operation should reflect + the SUBACK return code rather than reporting success unconditionally. The + current fork ignores SUBACK entirely and returns the transport write result, + so the case is marked FINDING_MARKER(F10) (should_fail). + + Every assertion goes through the public API and decoded MockClient.outbound() + wire bytes - never private members - so the baseline stays durable across a + future MQTT 5 migration (Requirement 19.1). +*/ + +#include +#include +#include + +#include "doctest.h" + +#include "FindingStatus.h" +#include "MockClient.h" +#include "MqttPacket.h" +#include "TestClock.h" +#include "PubSubClient.h" + +namespace { + +// Connect the client/psc pair with a scripted CONNACK and clear the recorded +// CONNECT bytes so only the packet-under-test remains in outbound(). +void connectAndClear(MockClient& client, PubSubClient& psc) { + client.pushPacket(MqttPacket::connack(0)); + psc.setServer("broker.example", 1883); + REQUIRE(psc.connect("sub-client")); + REQUIRE(psc.connected()); + client.clearOutbound(); +} + +} // namespace + +TEST_SUITE("baseline") { + + // --- 10.1: subscribe() records a valid SUBSCRIBE packet ----------------- + + TEST_CASE("subscribe records a structurally well-framed SUBSCRIBE packet") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.subscribe("tele/dev/SENSOR")); + + const std::vector& out = client.outbound(); + + // Generic framing: one complete control packet whose declared Remaining + // Length equals the trailing byte count, SUBSCRIBE type, QoS1 fixed flags + // (the library always sends MQTTSUBSCRIBE | MQTTQOS1). + DecodedPacket generic = MqttParser::decode(out); + REQUIRE(generic.valid); + CHECK(generic.type == static_cast(MQTTSUBSCRIBE)); + CHECK(generic.flags == static_cast(MQTTQOS1)); + CHECK(generic.remainingLength == generic.payload.size()); + + // SUBSCRIBE-specific fields. + DecodedSubscribe s = MqttParser::decodeSubscribe(out); + REQUIRE(s.valid); + CHECK(s.msgId != 0); // packet identifier is nonzero + REQUIRE(s.filters.size() == 1); + CHECK(s.filters[0] == "tele/dev/SENSOR"); + REQUIRE(s.requestedQos.size() == 1); + CHECK(s.requestedQos[0] == 0); // subscribe(topic) defaults QoS 0 + } + + // --- 10.2: unsubscribe() records a valid UNSUBSCRIBE packet ------------- + + TEST_CASE("unsubscribe records a structurally well-framed UNSUBSCRIBE packet") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.unsubscribe("cmnd/dev/POWER")); + + const std::vector& out = client.outbound(); + + DecodedPacket generic = MqttParser::decode(out); + REQUIRE(generic.valid); + CHECK(generic.type == static_cast(MQTTUNSUBSCRIBE)); + CHECK(generic.flags == static_cast(MQTTQOS1)); + CHECK(generic.remainingLength == generic.payload.size()); + + DecodedUnsubscribe u = MqttParser::decodeUnsubscribe(out); + REQUIRE(u.valid); + CHECK(u.msgId != 0); // packet identifier is nonzero + REQUIRE(u.filters.size() == 1); + CHECK(u.filters[0] == "cmnd/dev/POWER"); + } + + // --- Property 5: SUBSCRIBE/UNSUBSCRIBE field round-trip ----------------- + + // Feature: tasmota-pubsub-tests, Property 5: for all topics and requested QoS + // in {0,1}, subscribe emits a SUBSCRIBE with a nonzero message id, the topic + // filter equal to the input, and a trailing requested-QoS byte equal to the + // argument; unsubscribe emits an UNSUBSCRIBE with a nonzero message id and the + // topic filter equal to the input. Validated deterministically over a curated + // table of QoS values and topic vectors (no randomized generators). The + // explicit-QoS subscribe overload (Requirement 18.6) is exercised for QoS 1. + TEST_CASE("Property 5: SUBSCRIBE/UNSUBSCRIBE field round-trip over curated QoS and topics") { + // Curated topic filters: single level, multi-level, wildcards, a + // single-character topic, and a longer nested filter. + const std::string topics[] = { + "a", + "tele/dev/SENSOR", + "cmnd/tasmota_ABCDEF/+", + "stat/#", + "tele/very/deeply/nested/topic/filter/name", + }; + const uint8_t qosValues[] = {0, 1}; + + for (const std::string& topic : topics) { + // --- SUBSCRIBE for each requested QoS in {0,1} ------------------ + for (uint8_t qos : qosValues) { + CAPTURE(topic); + CAPTURE(qos); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + // Exercise the explicit-QoS overload (Requirement 18.6). + REQUIRE(psc.subscribe(topic.c_str(), qos)); + + const std::vector& out = client.outbound(); + DecodedPacket generic = MqttParser::decode(out); + REQUIRE(generic.valid); + CHECK(generic.type == static_cast(MQTTSUBSCRIBE)); + CHECK(generic.flags == static_cast(MQTTQOS1)); + CHECK(generic.remainingLength == generic.payload.size()); + + DecodedSubscribe s = MqttParser::decodeSubscribe(out); + REQUIRE(s.valid); + CHECK(s.msgId != 0); + REQUIRE(s.filters.size() == 1); + CHECK(s.filters[0] == topic); + REQUIRE(s.requestedQos.size() == 1); + CHECK(s.requestedQos[0] == qos); + } + + // --- UNSUBSCRIBE (no requested-QoS byte on the wire) ------------ + { + CAPTURE(topic); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.unsubscribe(topic.c_str())); + + const std::vector& out = client.outbound(); + DecodedPacket generic = MqttParser::decode(out); + REQUIRE(generic.valid); + CHECK(generic.type == static_cast(MQTTUNSUBSCRIBE)); + CHECK(generic.flags == static_cast(MQTTQOS1)); + CHECK(generic.remainingLength == generic.payload.size()); + + DecodedUnsubscribe u = MqttParser::decodeUnsubscribe(out); + REQUIRE(u.valid); + CHECK(u.msgId != 0); + REQUIRE(u.filters.size() == 1); + CHECK(u.filters[0] == topic); + } + } + } + + // Successive subscribe/unsubscribe calls advance the packet identifier, so + // each emitted packet carries its own nonzero message id (characterization of + // the observable nextMsgId behavior across the public API). + TEST_CASE("successive subscribe/unsubscribe calls carry distinct nonzero message ids") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + REQUIRE(psc.subscribe("tele/a/#")); + DecodedSubscribe s1 = MqttParser::decodeSubscribe(client.outbound()); + REQUIRE(s1.valid); + client.clearOutbound(); + + REQUIRE(psc.subscribe("tele/b/#")); + DecodedSubscribe s2 = MqttParser::decodeSubscribe(client.outbound()); + REQUIRE(s2.valid); + client.clearOutbound(); + + REQUIRE(psc.unsubscribe("tele/a/#")); + DecodedUnsubscribe u1 = MqttParser::decodeUnsubscribe(client.outbound()); + REQUIRE(u1.valid); + + CHECK(s1.msgId != 0); + CHECK(s2.msgId != 0); + CHECK(u1.msgId != 0); + CHECK(s2.msgId != s1.msgId); // identifier advances between calls + CHECK(u1.msgId != s2.msgId); + } +} + +TEST_SUITE("hardening") { + + // F-08 (Requirement 10.3): a SUBSCRIBE whose topic length brings the packet to + // exactly the buffer capacity must not write the trailing QoS byte beyond the + // buffer bounds. The library's own bound is bufferSize >= 10 + topicLength + // (header(5) + msgId(2) + topic-length(2) + topic + qos(1)); at exact capacity + // the QoS byte is written at buffer[9 + topicLength] == buffer[bufferSize - 1]. + // An off-by-one would write at buffer[bufferSize] and AddressSanitizer would + // abort. The buffer is set AFTER connecting (the CONNECT packet needs the full + // default buffer) so the exact-capacity boundary applies only to the SUBSCRIBE. + // F-08 is hardened in the current fork, so this is expected PASS. + TEST_CASE("F-08 exact-buffer SUBSCRIBE does not write the QoS byte out of bounds" + * FINDING_MARKER(F08)) { + // Topic lengths kept small so the Remaining Length stays a single byte; + // the point is the exact-capacity boundary, not large packets. + const size_t topicLengths[] = {1, 5, 16, 50, 117}; + const uint8_t qosValues[] = {0, 1}; + + for (size_t topicLen : topicLengths) { + for (uint8_t qos : qosValues) { + CAPTURE(topicLen); + CAPTURE(qos); + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + // Shrink the working buffer to EXACTLY the SUBSCRIBE bound. The + // realloc succeeds; the connection state is unaffected. + const uint16_t exactCapacity = + static_cast(10 + topicLen); + REQUIRE(psc.setBufferSize(exactCapacity)); + REQUIRE(psc.getBufferSize() == exactCapacity); + + const std::string topic(topicLen, 'a'); + + // The QoS byte is written at buffer[bufferSize - 1]; ASan proves + // no write at buffer[bufferSize]. + REQUIRE(psc.subscribe(topic.c_str(), qos)); + + // The emitted packet is still a well-framed SUBSCRIBE carrying the + // exact topic and requested QoS. + const std::vector& out = client.outbound(); + DecodedSubscribe s = MqttParser::decodeSubscribe(out); + REQUIRE(s.valid); + CHECK(s.msgId != 0); + REQUIRE(s.filters.size() == 1); + CHECK(s.filters[0] == topic); + REQUIRE(s.requestedQos.size() == 1); + CHECK(s.requestedQos[0] == qos); + } + } + } + + // Requirement 10.5: subscribe() and unsubscribe() must reject a null topic + // pointer before any length computation dereferences it. A hardened + // implementation returns false and emits nothing (no strnlen(nullptr, ...)). + // The current fork already guards this, so it is expected PASS. Run under + // ASan, a missing guard would fault while computing the topic length. + TEST_CASE("subscribe / unsubscribe reject a null topic pointer") { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + CHECK_FALSE(psc.subscribe(nullptr)); + CHECK_FALSE(psc.subscribe(nullptr, 0)); + CHECK_FALSE(psc.subscribe(nullptr, 1)); + CHECK_FALSE(psc.unsubscribe(nullptr)); + + // No packet was written for any rejected call. + CHECK(client.outbound().empty()); + + // The connection remains usable after the rejections. + CHECK(psc.connected()); + } + + // F-10 (Requirement 10.4): a subscribe operation should reflect the SUBACK + // return code rather than reporting success unconditionally. A hardened + // implementation, on receiving a SUBACK carrying a failure return code + // (0x80), would surface the failure to the caller. The current fork ignores + // SUBACK entirely and returns the transport write result (true), so this case + // is expected to fail until F-10 is hardened. + TEST_CASE("F-10 subscribe reflects the SUBACK return code" * FINDING_MARKER(F10)) { + TestClock::instance().reset(); + MockClient client; + PubSubClient psc(client); + connectAndClear(client, psc); + + // Script a SUBACK with the MQTT "failure" return code (0x80) for the + // packet identifier the first subscribe() will use (nextMsgId advances to + // 1 on the first call after connect). + client.pushPacket(MqttPacket::suback(1, 0x80)); + + // Hardened contract: subscribe reflects the SUBACK failure and reports + // failure. The current fork returns true unconditionally. + CHECK_FALSE(psc.subscribe("tele/dev/SENSOR")); + } +} diff --git a/lib/default/TasmotaPubSub/tests/src/test_main.cpp b/lib/default/TasmotaPubSub/tests/src/test_main.cpp new file mode 100644 index 000000000..fe668c5a1 --- /dev/null +++ b/lib/default/TasmotaPubSub/tests/src/test_main.cpp @@ -0,0 +1,40 @@ +/* + test_main.cpp - doctest entry point for the TasmotaPubSub host test system. + + This is the SINGLE translation unit in the whole test binary that defines + DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN before including the doctest single header, + so doctest's main() and implementation are emitted here exactly once. Every + other *_test.cpp includes "doctest.h" only. + + It also carries the first trivial baseline test that constructs the unmodified + PubSubClient library against the Arduino shim, proving the whole toolchain + (library + shim + support lib + doctest) compiles, links, runs, and reports + pass/fail/skip counts under ASan/UBSan (task 4.2 / Requirements 1.2, 1.3, + 3.1, 3.2, 3.3). +*/ + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest.h" + +#include "TestClock.h" +#include "PubSubClient.h" + +// Every case is tagged into either the "baseline" or "hardening" suite so the +// two groups can be selected independently via -ts=baseline / -ts=hardening. +TEST_SUITE("baseline") { + + // Characterization test: a freshly default-constructed PubSubClient has a + // stable, well-defined initial observable state. Per PubSubClient.h the + // _state member is initialized to MQTT_DISCONNECTED (-1) and no transport + // is attached, so connected() must report false. + TEST_CASE("PubSubClient default construction has a stable initial state") { + // Reset the virtual clock for determinism/isolation (optional here). + TestClock::instance().reset(); + + PubSubClient client; + + CHECK(client.state() == MQTT_DISCONNECTED); + CHECK(client.state() == -1); + CHECK(client.connected() == false); + } +} diff --git a/lib/default/pubsubclient-2.8.13/.gitignore b/lib/default/pubsubclient-2.8.13/.gitignore deleted file mode 100644 index a42cc406e..000000000 --- a/lib/default/pubsubclient-2.8.13/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -tests/bin -.pioenvs -.piolibdeps -.clang_complete -.gcc-flags.json diff --git a/lib/default/pubsubclient-2.8.13/.travis.yml b/lib/default/pubsubclient-2.8.13/.travis.yml deleted file mode 100644 index e7b28cb9c..000000000 --- a/lib/default/pubsubclient-2.8.13/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -sudo: false -language: cpp -compiler: - - g++ -script: cd tests && make && make test -os: - - linux diff --git a/lib/default/pubsubclient-2.8.13/CHANGES.txt b/lib/default/pubsubclient-2.8.13/CHANGES.txt deleted file mode 100644 index e23d5315f..000000000 --- a/lib/default/pubsubclient-2.8.13/CHANGES.txt +++ /dev/null @@ -1,85 +0,0 @@ -2.8 - * Add setBufferSize() to override MQTT_MAX_PACKET_SIZE - * Add setKeepAlive() to override MQTT_KEEPALIVE - * Add setSocketTimeout() to overide MQTT_SOCKET_TIMEOUT - * Added check to prevent subscribe/unsubscribe to empty topics - * Declare wifi mode prior to connect in ESP example - * Use `strnlen` to avoid overruns - * Support pre-connected Client objects - -2.7 - * Fix remaining-length handling to prevent buffer overrun - * Add large-payload API - beginPublish/write/publish/endPublish - * Add yield call to improve reliability on ESP - * Add Clean Session flag to connect options - * Add ESP32 support for functional callback signature - * Various other fixes - -2.4 - * Add MQTT_SOCKET_TIMEOUT to prevent it blocking indefinitely - whilst waiting for inbound data - * Fixed return code when publishing >256 bytes - -2.3 - * Add publish(topic,payload,retained) function - -2.2 - * Change code layout to match Arduino Library reqs - -2.1 - * Add MAX_TRANSFER_SIZE def to chunk messages if needed - * Reject topic/payloads that exceed MQTT_MAX_PACKET_SIZE - -2.0 - * Add (and default to) MQTT 3.1.1 support - * Fix PROGMEM handling for Intel Galileo/ESP8266 - * Add overloaded constructors for convenience - * Add chainable setters for server/callback/client/stream - * Add state function to return connack return code - -1.9 - * Do not split MQTT packets over multiple calls to _client->write() - * API change: All constructors now require an instance of Client - to be passed in. - * Fixed example to match 1.8 api changes - dpslwk - * Added username/password support - WilHall - * Added publish_P - publishes messages from PROGMEM - jobytaffey - -1.8 - * KeepAlive interval is configurable in PubSubClient.h - * Maximum packet size is configurable in PubSubClient.h - * API change: Return boolean rather than int from various functions - * API change: Length parameter in message callback changed - from int to unsigned int - * Various internal tidy-ups around types -1.7 - * Improved keepalive handling - * Updated to the Arduino-1.0 API -1.6 - * Added the ability to publish a retained message - -1.5 - * Added default constructor - * Fixed compile error when used with arduino-0021 or later - -1.4 - * Fixed connection lost handling - -1.3 - * Fixed packet reading bug in PubSubClient.readPacket - -1.2 - * Fixed compile error when used with arduino-0016 or later - - -1.1 - * Reduced size of library - * Added support for Will messages - * Clarified licensing - see LICENSE.txt - - -1.0 - * Only Quality of Service (QOS) 0 messaging is supported - * The maximum message size, including header, is 128 bytes - * The keepalive interval is set to 30 seconds - * No support for Will messages diff --git a/lib/default/pubsubclient-2.8.13/README.md b/lib/default/pubsubclient-2.8.13/README.md deleted file mode 100644 index 2e1317185..000000000 --- a/lib/default/pubsubclient-2.8.13/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Arduino Client for MQTT - -This library provides a client for doing simple publish/subscribe messaging with -a server that supports MQTT. - -## Examples - -The library comes with a number of example sketches. See File > Examples > PubSubClient -within the Arduino application. - -Full API documentation is available here: https://pubsubclient.knolleary.net - -## Limitations - - - It can only publish QoS 0 messages. It can subscribe at QoS 0 or QoS 1. - - The maximum message size, including header, is **256 bytes** by default. This - is configurable via `MQTT_MAX_PACKET_SIZE` in `PubSubClient.h` or can be changed - by calling `PubSubClient::setBufferSize(size)`. - - The keepalive interval is set to 15 seconds by default. This is configurable - via `MQTT_KEEPALIVE` in `PubSubClient.h` or can be changed by calling - `PubSubClient::setKeepAlive(keepAlive)`. - - The client uses MQTT 3.1.1 by default. It can be changed to use MQTT 3.1 by - changing value of `MQTT_VERSION` in `PubSubClient.h`. - - -## Compatible Hardware - -The library uses the Arduino Ethernet Client api for interacting with the -underlying network hardware. This means it Just Works with a growing number of -boards and shields, including: - - - Arduino Ethernet - - Arduino Ethernet Shield - - Arduino YUN – use the included `YunClient` in place of `EthernetClient`, and - be sure to do a `Bridge.begin()` first - - Arduino WiFi Shield - if you want to send packets > 90 bytes with this shield, - enable the `MQTT_MAX_TRANSFER_SIZE` define in `PubSubClient.h`. - - Sparkfun WiFly Shield – [library](https://github.com/dpslwk/WiFly) - - TI CC3000 WiFi - [library](https://github.com/sparkfun/SFE_CC3000_Library) - - Intel Galileo/Edison - - ESP8266 - - ESP32 - -The library cannot currently be used with hardware based on the ENC28J60 chip – -such as the Nanode or the Nuelectronics Ethernet Shield. For those, there is an -[alternative library](https://github.com/njh/NanodeMQTT) available. - -## License - -This code is released under the MIT License. diff --git a/lib/default/pubsubclient-2.8.13/examples/mqtt_auth/mqtt_auth.ino b/lib/default/pubsubclient-2.8.13/examples/mqtt_auth/mqtt_auth.ino deleted file mode 100644 index 04bd7bb29..000000000 --- a/lib/default/pubsubclient-2.8.13/examples/mqtt_auth/mqtt_auth.ino +++ /dev/null @@ -1,43 +0,0 @@ -/* - Basic MQTT example with Authentication - - - connects to an MQTT server, providing username - and password - - publishes "hello world" to the topic "outTopic" - - subscribes to the topic "inTopic" -*/ - -#include -#include -#include - -// Update these with values suitable for your network. -byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; -IPAddress ip(172, 16, 0, 100); -IPAddress server(172, 16, 0, 2); - -void callback(char* topic, byte* payload, unsigned int length) { - // handle message arrived -} - -EthernetClient ethClient; -PubSubClient client(server, 1883, callback, ethClient); - -void setup() -{ - Ethernet.begin(mac, ip); - // Note - the default maximum packet size is 128 bytes. If the - // combined length of clientId, username and password exceed this use the - // following to increase the buffer size: - // client.setBufferSize(255); - - if (client.connect("arduinoClient", "testuser", "testpass")) { - client.publish("outTopic","hello world"); - client.subscribe("inTopic"); - } -} - -void loop() -{ - client.loop(); -} diff --git a/lib/default/pubsubclient-2.8.13/examples/mqtt_basic/mqtt_basic.ino b/lib/default/pubsubclient-2.8.13/examples/mqtt_basic/mqtt_basic.ino deleted file mode 100644 index f545adef8..000000000 --- a/lib/default/pubsubclient-2.8.13/examples/mqtt_basic/mqtt_basic.ino +++ /dev/null @@ -1,77 +0,0 @@ -/* - Basic MQTT example - - This sketch demonstrates the basic capabilities of the library. - It connects to an MQTT server then: - - publishes "hello world" to the topic "outTopic" - - subscribes to the topic "inTopic", printing out any messages - it receives. NB - it assumes the received payloads are strings not binary - - It will reconnect to the server if the connection is lost using a blocking - reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to - achieve the same result without blocking the main loop. - -*/ - -#include -#include -#include - -// Update these with values suitable for your network. -byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; -IPAddress ip(172, 16, 0, 100); -IPAddress server(172, 16, 0, 2); - -void callback(char* topic, byte* payload, unsigned int length) { - Serial.print("Message arrived ["); - Serial.print(topic); - Serial.print("] "); - for (int i=0;i Preferences -> Additional Boards Manager URLs": - http://arduino.esp8266.com/stable/package_esp8266com_index.json - - Open the "Tools -> Board -> Board Manager" and click install for the ESP8266" - - Select your ESP8266 in "Tools -> Board" -*/ - -#include -#include - -// Update these with values suitable for your network. - -const char* ssid = "........"; -const char* password = "........"; -const char* mqtt_server = "broker.mqtt-dashboard.com"; - -WiFiClient espClient; -PubSubClient client(espClient); -unsigned long lastMsg = 0; -#define MSG_BUFFER_SIZE (50) -char msg[MSG_BUFFER_SIZE]; -int value = 0; - -void setup_wifi() { - - delay(10); - // We start by connecting to a WiFi network - Serial.println(); - Serial.print("Connecting to "); - Serial.println(ssid); - - WiFi.mode(WIFI_STA); - WiFi.begin(ssid, password); - - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } - - randomSeed(micros()); - - Serial.println(""); - Serial.println("WiFi connected"); - Serial.println("IP address: "); - Serial.println(WiFi.localIP()); -} - -void callback(char* topic, byte* payload, unsigned int length) { - Serial.print("Message arrived ["); - Serial.print(topic); - Serial.print("] "); - for (int i = 0; i < length; i++) { - Serial.print((char)payload[i]); - } - Serial.println(); - - // Switch on the LED if an 1 was received as first character - if ((char)payload[0] == '1') { - digitalWrite(BUILTIN_LED, LOW); // Turn the LED on (Note that LOW is the voltage level - // but actually the LED is on; this is because - // it is active low on the ESP-01) - } else { - digitalWrite(BUILTIN_LED, HIGH); // Turn the LED off by making the voltage HIGH - } - -} - -void reconnect() { - // Loop until we're reconnected - while (!client.connected()) { - Serial.print("Attempting MQTT connection..."); - // Create a random client ID - String clientId = "ESP8266Client-"; - clientId += String(random(0xffff), HEX); - // Attempt to connect - if (client.connect(clientId.c_str())) { - Serial.println("connected"); - // Once connected, publish an announcement... - client.publish("outTopic", "hello world"); - // ... and resubscribe - client.subscribe("inTopic"); - } else { - Serial.print("failed, rc="); - Serial.print(client.state()); - Serial.println(" try again in 5 seconds"); - // Wait 5 seconds before retrying - delay(5000); - } - } -} - -void setup() { - pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output - Serial.begin(115200); - setup_wifi(); - client.setServer(mqtt_server, 1883); - client.setCallback(callback); -} - -void loop() { - - if (!client.connected()) { - reconnect(); - } - client.loop(); - - unsigned long now = millis(); - if (now - lastMsg > 2000) { - lastMsg = now; - ++value; - snprintf (msg, MSG_BUFFER_SIZE, "hello world #%ld", value); - Serial.print("Publish message: "); - Serial.println(msg); - client.publish("outTopic", msg); - } -} diff --git a/lib/default/pubsubclient-2.8.13/examples/mqtt_large_message/mqtt_large_message.ino b/lib/default/pubsubclient-2.8.13/examples/mqtt_large_message/mqtt_large_message.ino deleted file mode 100644 index e048c3ed3..000000000 --- a/lib/default/pubsubclient-2.8.13/examples/mqtt_large_message/mqtt_large_message.ino +++ /dev/null @@ -1,179 +0,0 @@ -/* - Long message ESP8266 MQTT example - - This sketch demonstrates sending arbitrarily large messages in combination - with the ESP8266 board/library. - - It connects to an MQTT server then: - - publishes "hello world" to the topic "outTopic" - - subscribes to the topic "greenBottles/#", printing out any messages - it receives. NB - it assumes the received payloads are strings not binary - - If the sub-topic is a number, it publishes a "greenBottles/lyrics" message - with a payload consisting of the lyrics to "10 green bottles", replacing - 10 with the number given in the sub-topic. - - It will reconnect to the server if the connection is lost using a blocking - reconnect function. See the 'mqtt_reconnect_nonblocking' example for how to - achieve the same result without blocking the main loop. - - To install the ESP8266 board, (using Arduino 1.6.4+): - - Add the following 3rd party board manager under "File -> Preferences -> Additional Boards Manager URLs": - http://arduino.esp8266.com/stable/package_esp8266com_index.json - - Open the "Tools -> Board -> Board Manager" and click install for the ESP8266" - - Select your ESP8266 in "Tools -> Board" - -*/ - -#include -#include - -// Update these with values suitable for your network. - -const char* ssid = "........"; -const char* password = "........"; -const char* mqtt_server = "broker.mqtt-dashboard.com"; - -WiFiClient espClient; -PubSubClient client(espClient); -long lastMsg = 0; -char msg[50]; -int value = 0; - -void setup_wifi() { - - delay(10); - // We start by connecting to a WiFi network - Serial.println(); - Serial.print("Connecting to "); - Serial.println(ssid); - - WiFi.begin(ssid, password); - - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } - - randomSeed(micros()); - - Serial.println(""); - Serial.println("WiFi connected"); - Serial.println("IP address: "); - Serial.println(WiFi.localIP()); -} - -void callback(char* topic, byte* payload, unsigned int length) { - Serial.print("Message arrived ["); - Serial.print(topic); - Serial.print("] "); - for (int i = 0; i < length; i++) { - Serial.print((char)payload[i]); - } - Serial.println(); - - // Find out how many bottles we should generate lyrics for - String topicStr(topic); - int bottleCount = 0; // assume no bottles unless we correctly parse a value from the topic - if (topicStr.indexOf('/') >= 0) { - // The topic includes a '/', we'll try to read the number of bottles from just after that - topicStr.remove(0, topicStr.indexOf('/')+1); - // Now see if there's a number of bottles after the '/' - bottleCount = topicStr.toInt(); - } - - if (bottleCount > 0) { - // Work out how big our resulting message will be - int msgLen = 0; - for (int i = bottleCount; i > 0; i--) { - String numBottles(i); - msgLen += 2*numBottles.length(); - if (i == 1) { - msgLen += 2*String(" green bottle, standing on the wall\n").length(); - } else { - msgLen += 2*String(" green bottles, standing on the wall\n").length(); - } - msgLen += String("And if one green bottle should accidentally fall\nThere'll be ").length(); - switch (i) { - case 1: - msgLen += String("no green bottles, standing on the wall\n\n").length(); - break; - case 2: - msgLen += String("1 green bottle, standing on the wall\n\n").length(); - break; - default: - numBottles = i-1; - msgLen += numBottles.length(); - msgLen += String(" green bottles, standing on the wall\n\n").length(); - break; - }; - } - - // Now we can start to publish the message - client.beginPublish("greenBottles/lyrics", msgLen, false); - for (int i = bottleCount; i > 0; i--) { - for (int j = 0; j < 2; j++) { - client.print(i); - if (i == 1) { - client.print(" green bottle, standing on the wall\n"); - } else { - client.print(" green bottles, standing on the wall\n"); - } - } - client.print("And if one green bottle should accidentally fall\nThere'll be "); - switch (i) { - case 1: - client.print("no green bottles, standing on the wall\n\n"); - break; - case 2: - client.print("1 green bottle, standing on the wall\n\n"); - break; - default: - client.print(i-1); - client.print(" green bottles, standing on the wall\n\n"); - break; - }; - } - // Now we're done! - client.endPublish(); - } -} - -void reconnect() { - // Loop until we're reconnected - while (!client.connected()) { - Serial.print("Attempting MQTT connection..."); - // Create a random client ID - String clientId = "ESP8266Client-"; - clientId += String(random(0xffff), HEX); - // Attempt to connect - if (client.connect(clientId.c_str())) { - Serial.println("connected"); - // Once connected, publish an announcement... - client.publish("outTopic", "hello world"); - // ... and resubscribe - client.subscribe("greenBottles/#"); - } else { - Serial.print("failed, rc="); - Serial.print(client.state()); - Serial.println(" try again in 5 seconds"); - // Wait 5 seconds before retrying - delay(5000); - } - } -} - -void setup() { - pinMode(BUILTIN_LED, OUTPUT); // Initialize the BUILTIN_LED pin as an output - Serial.begin(115200); - setup_wifi(); - client.setServer(mqtt_server, 1883); - client.setCallback(callback); -} - -void loop() { - - if (!client.connected()) { - reconnect(); - } - client.loop(); -} diff --git a/lib/default/pubsubclient-2.8.13/examples/mqtt_publish_in_callback/mqtt_publish_in_callback.ino b/lib/default/pubsubclient-2.8.13/examples/mqtt_publish_in_callback/mqtt_publish_in_callback.ino deleted file mode 100644 index 42afb2a3a..000000000 --- a/lib/default/pubsubclient-2.8.13/examples/mqtt_publish_in_callback/mqtt_publish_in_callback.ino +++ /dev/null @@ -1,60 +0,0 @@ -/* - Publishing in the callback - - - connects to an MQTT server - - subscribes to the topic "inTopic" - - when a message is received, republishes it to "outTopic" - - This example shows how to publish messages within the - callback function. The callback function header needs to - be declared before the PubSubClient constructor and the - actual callback defined afterwards. - This ensures the client reference in the callback function - is valid. - -*/ - -#include -#include -#include - -// Update these with values suitable for your network. -byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; -IPAddress ip(172, 16, 0, 100); -IPAddress server(172, 16, 0, 2); - -// Callback function header -void callback(char* topic, byte* payload, unsigned int length); - -EthernetClient ethClient; -PubSubClient client(server, 1883, callback, ethClient); - -// Callback function -void callback(char* topic, byte* payload, unsigned int length) { - // In order to republish this payload, a copy must be made - // as the orignal payload buffer will be overwritten whilst - // constructing the PUBLISH packet. - - // Allocate the correct amount of memory for the payload copy - byte* p = (byte*)malloc(length); - // Copy the payload to the new buffer - memcpy(p,payload,length); - client.publish("outTopic", p, length); - // Free the memory - free(p); -} - -void setup() -{ - - Ethernet.begin(mac, ip); - if (client.connect("arduinoClient")) { - client.publish("outTopic","hello world"); - client.subscribe("inTopic"); - } -} - -void loop() -{ - client.loop(); -} diff --git a/lib/default/pubsubclient-2.8.13/examples/mqtt_reconnect_nonblocking/mqtt_reconnect_nonblocking.ino b/lib/default/pubsubclient-2.8.13/examples/mqtt_reconnect_nonblocking/mqtt_reconnect_nonblocking.ino deleted file mode 100644 index 080b7391c..000000000 --- a/lib/default/pubsubclient-2.8.13/examples/mqtt_reconnect_nonblocking/mqtt_reconnect_nonblocking.ino +++ /dev/null @@ -1,67 +0,0 @@ -/* - Reconnecting MQTT example - non-blocking - - This sketch demonstrates how to keep the client connected - using a non-blocking reconnect function. If the client loses - its connection, it attempts to reconnect every 5 seconds - without blocking the main loop. - -*/ - -#include -#include -#include - -// Update these with values suitable for your hardware/network. -byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; -IPAddress ip(172, 16, 0, 100); -IPAddress server(172, 16, 0, 2); - -void callback(char* topic, byte* payload, unsigned int length) { - // handle message arrived -} - -EthernetClient ethClient; -PubSubClient client(ethClient); - -long lastReconnectAttempt = 0; - -boolean reconnect() { - if (client.connect("arduinoClient")) { - // Once connected, publish an announcement... - client.publish("outTopic","hello world"); - // ... and resubscribe - client.subscribe("inTopic"); - } - return client.connected(); -} - -void setup() -{ - client.setServer(server, 1883); - client.setCallback(callback); - - Ethernet.begin(mac, ip); - delay(1500); - lastReconnectAttempt = 0; -} - - -void loop() -{ - if (!client.connected()) { - long now = millis(); - if (now - lastReconnectAttempt > 5000) { - lastReconnectAttempt = now; - // Attempt to reconnect - if (reconnect()) { - lastReconnectAttempt = 0; - } - } - } else { - // Client connected - - client.loop(); - } - -} diff --git a/lib/default/pubsubclient-2.8.13/examples/mqtt_stream/mqtt_stream.ino b/lib/default/pubsubclient-2.8.13/examples/mqtt_stream/mqtt_stream.ino deleted file mode 100644 index 67c22872c..000000000 --- a/lib/default/pubsubclient-2.8.13/examples/mqtt_stream/mqtt_stream.ino +++ /dev/null @@ -1,57 +0,0 @@ -/* - Example of using a Stream object to store the message payload - - Uses SRAM library: https://github.com/ennui2342/arduino-sram - but could use any Stream based class such as SD - - - connects to an MQTT server - - publishes "hello world" to the topic "outTopic" - - subscribes to the topic "inTopic" -*/ - -#include -#include -#include -#include - -// Update these with values suitable for your network. -byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED }; -IPAddress ip(172, 16, 0, 100); -IPAddress server(172, 16, 0, 2); - -SRAM sram(4, SRAM_1024); - -void callback(char* topic, byte* payload, unsigned int length) { - sram.seek(1); - - // do something with the message - for(uint8_t i=0; i -maintainer=Nick O'Leary -sentence=A client library for MQTT messaging. -paragraph=MQTT is a lightweight messaging protocol ideal for small devices. This library allows you to send and receive MQTT messages. It supports the latest MQTT 3.1.1 protocol and can be configured to use the older MQTT 3.1 if needed. It supports all Arduino Ethernet Client compatible hardware, including the Intel Galileo/Edison, ESP8266 and TI CC3000. -category=Communication -url=http://pubsubclient.knolleary.net -architectures=esp8266,esp32 diff --git a/lib/default/pubsubclient-2.8.13/tests/.gitignore b/lib/default/pubsubclient-2.8.13/tests/.gitignore deleted file mode 100644 index 215de78d7..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.build -tmpbin -logs -*.pyc diff --git a/lib/default/pubsubclient-2.8.13/tests/Makefile b/lib/default/pubsubclient-2.8.13/tests/Makefile deleted file mode 100644 index 1f7163675..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -SRC_PATH=./src -OUT_PATH=./bin -TEST_SRC=$(wildcard ${SRC_PATH}/*_spec.cpp) -TEST_BIN= $(TEST_SRC:${SRC_PATH}/%.cpp=${OUT_PATH}/%) -VPATH=${SRC_PATH} -SHIM_FILES=${SRC_PATH}/lib/*.cpp -PSC_FILE=../src/PubSubClient.cpp -CC=g++ -CFLAGS=-I${SRC_PATH}/lib -I../src - -all: $(TEST_BIN) - -${OUT_PATH}/%: ${SRC_PATH}/%.cpp ${PSC_FILE} ${SHIM_FILES} - mkdir -p ${OUT_PATH} - ${CC} ${CFLAGS} $^ -o $@ - -clean: - @rm -rf ${OUT_PATH} - -test: - @bin/connect_spec - @bin/publish_spec - @bin/receive_spec - @bin/subscribe_spec - @bin/keepalive_spec diff --git a/lib/default/pubsubclient-2.8.13/tests/README.md b/lib/default/pubsubclient-2.8.13/tests/README.md deleted file mode 100644 index e5700a6d6..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Arduino Client for MQTT Test Suite - -This is a regression test suite for the `PubSubClient` library. - -There are two parts: - - - Tests that can be compiled and run on any machine - - Tests that build the example sketches using the Arduino IDE - - -It is a work-in-progress and is subject to complete refactoring as the whim takes -me. - - -## Local tests - -These are a set of executables that can be run to test specific areas of functionality. -They do not require a real Arduino to be attached, nor the use of the Arduino IDE. - -The tests include a set of mock files to stub out the parts of the Arduino environment the library -depends on. - -### Dependencies - - - g++ - -### Running - -Build the tests using the provided `Makefile`: - - $ make - -This will create a set of executables in `./bin/`. Run each of these executables to test the corresponding functionality. - -*Note:* the `connect_spec` and `keepalive_spec` tests involve testing keepalive timers so naturally take a few minutes to run through. - -## Arduino tests - -*Note:* INO Tool doesn't currently play nicely with Arduino 1.5. This has broken this test suite. - -Without a suitable arduino plugged in, the test suite will only check the -example sketches compile cleanly against the library. - -With an arduino plugged in, each sketch that has a corresponding python -test case is built, uploaded and then the tests run. - -### Dependencies - - - Python 2.7+ - - [INO Tool](http://inotool.org/) - this provides command-line build/upload of Arduino sketches - -### Running - -The test suite _does not_ run an MQTT server - it is assumed to be running already. - - $ python testsuite.py - -A summary of activity is printed to the console. More comprehensive logs are written -to the `logs` directory. - -### What it does - -For each sketch in the library's `examples` directory, e.g. `mqtt_basic.ino`, the suite looks for a matching test case -`testcases/mqtt_basic.py`. - -The test case must follow these conventions: - - sub-class `unittest.TestCase` - - provide the class methods `setUpClass` and `tearDownClass` (TODO: make this optional) - - all test method names begin with `test_` - -The suite will call the `setUpClass` method _before_ uploading the sketch. This -allows any test setup to be performed before the sketch runs - such as connecting -a client and subscribing to topics. - - -### Settings - -The file `testcases/settings.py` is used to config the test environment. - - - `server_ip` - the IP address of the broker the client should connect to (the broker port is assumed to be 1883). - - `arduino_ip` - the IP address the arduino should use (when not testing DHCP). - -Before each sketch is compiled, these values are automatically substituted in. To -do this, the suite looks for lines that _start_ with the following: - - byte server[] = { - byte ip[] = { - -and replaces them with the appropriate values. - - - - diff --git a/lib/default/pubsubclient-2.8.13/tests/src/connect_spec.cpp b/lib/default/pubsubclient-2.8.13/tests/src/connect_spec.cpp deleted file mode 100644 index e8545c49d..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/connect_spec.cpp +++ /dev/null @@ -1,336 +0,0 @@ -#include "PubSubClient.h" -#include "ShimClient.h" -#include "Buffer.h" -#include "BDDTest.h" -#include "trace.h" - - -byte server[] = { 172, 16, 0, 2 }; - -void callback(char* topic, byte* payload, unsigned int length) { - // handle message arrived -} - - -int test_connect_fails_no_network() { - IT("fails to connect if underlying client doesn't connect"); - ShimClient shimClient; - shimClient.setAllowConnect(false); - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_FALSE(rc); - int state = client.state(); - IS_TRUE(state == MQTT_CONNECT_FAILED); - END_IT -} - -int test_connect_fails_on_no_response() { - IT("fails to connect if no response received after 15 seconds"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_FALSE(rc); - int state = client.state(); - IS_TRUE(state == MQTT_CONNECTION_TIMEOUT); - END_IT -} - -int test_connect_properly_formatted() { - IT("sends a properly formatted connect packet and succeeds"); - ShimClient shimClient; - - shimClient.setAllowConnect(true); - byte expectServer[] = { 172, 16, 0, 2 }; - shimClient.expectConnect(expectServer,1883); - byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - - shimClient.expect(connect,26); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int state = client.state(); - IS_TRUE(state == MQTT_DISCONNECTED); - - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - state = client.state(); - IS_TRUE(state == MQTT_CONNECTED); - - END_IT -} - -int test_connect_properly_formatted_hostname() { - IT("accepts a hostname"); - ShimClient shimClient; - - shimClient.setAllowConnect(true); - shimClient.expectConnect((char* const)"localhost",1883); - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client((char* const)"localhost", 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - END_IT -} - - -int test_connect_fails_on_bad_rc() { - IT("fails to connect if a bad return code is received"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - byte connack[] = { 0x20, 0x02, 0x00, 0x01 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_FALSE(rc); - - int state = client.state(); - IS_TRUE(state == 0x01); - - END_IT -} - -int test_connect_non_clean_session() { - IT("sends a properly formatted non-clean session connect packet and succeeds"); - ShimClient shimClient; - - shimClient.setAllowConnect(true); - byte expectServer[] = { 172, 16, 0, 2 }; - shimClient.expectConnect(expectServer,1883); - byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x0,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - - shimClient.expect(connect,26); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int state = client.state(); - IS_TRUE(state == MQTT_DISCONNECTED); - - int rc = client.connect((char*)"client_test1",0,0,0,0,0,0,0); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - state = client.state(); - IS_TRUE(state == MQTT_CONNECTED); - - END_IT -} - -int test_connect_accepts_username_password() { - IT("accepts a username and password"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connect[] = { 0x10,0x24,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xc2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x4,0x75,0x73,0x65,0x72,0x0,0x4,0x70,0x61,0x73,0x73}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.expect(connect,0x26); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1",(char*)"user",(char*)"pass"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_connect_accepts_username_no_password() { - IT("accepts a username but no password"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connect[] = { 0x10,0x1e,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x82,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x4,0x75,0x73,0x65,0x72}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.expect(connect,0x20); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1",(char*)"user",0); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - END_IT -} -int test_connect_accepts_username_blank_password() { - IT("accepts a username and blank password"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connect[] = { 0x10,0x20,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xc2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x4,0x75,0x73,0x65,0x72,0x0,0x0}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.expect(connect,0x26); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1",(char*)"user",(char*)"pass"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_connect_ignores_password_no_username() { - IT("ignores a password but no username"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.expect(connect,26); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1",0,(char*)"pass"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_connect_with_will() { - IT("accepts a will"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connect[] = {0x10,0x30,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xe,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x9,0x77,0x69,0x6c,0x6c,0x54,0x6f,0x70,0x69,0x63,0x0,0xb,0x77,0x69,0x6c,0x6c,0x4d,0x65,0x73,0x73,0x61,0x67,0x65}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.expect(connect,0x32); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1",(char*)"willTopic",1,0,(char*)"willMessage"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_connect_with_will_username_password() { - IT("accepts a will, username and password"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connect[] = {0x10,0x40,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0xce,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31,0x0,0x9,0x77,0x69,0x6c,0x6c,0x54,0x6f,0x70,0x69,0x63,0x0,0xb,0x77,0x69,0x6c,0x6c,0x4d,0x65,0x73,0x73,0x61,0x67,0x65,0x0,0x4,0x75,0x73,0x65,0x72,0x0,0x8,0x70,0x61,0x73,0x73,0x77,0x6f,0x72,0x64}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.expect(connect,0x42); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1",(char*)"user",(char*)"password",(char*)"willTopic",1,0,(char*)"willMessage"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_connect_disconnect_connect() { - IT("connects, disconnects and connects again"); - ShimClient shimClient; - - shimClient.setAllowConnect(true); - byte expectServer[] = { 172, 16, 0, 2 }; - shimClient.expectConnect(expectServer,1883); - byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x2,0x0,0xf,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - - shimClient.expect(connect,26); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - - int state = client.state(); - IS_TRUE(state == MQTT_DISCONNECTED); - - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - state = client.state(); - IS_TRUE(state == MQTT_CONNECTED); - - byte disconnect[] = {0xE0,0x00}; - shimClient.expect(disconnect,2); - - client.disconnect(); - - IS_FALSE(client.connected()); - IS_FALSE(shimClient.connected()); - IS_FALSE(shimClient.error()); - - state = client.state(); - IS_TRUE(state == MQTT_DISCONNECTED); - - shimClient.expect(connect,28); - shimClient.respond(connack,4); - rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - state = client.state(); - IS_TRUE(state == MQTT_CONNECTED); - - END_IT -} - -int test_connect_custom_keepalive() { - IT("sends a properly formatted connect packet with custom keepalive value"); - ShimClient shimClient; - - shimClient.setAllowConnect(true); - byte expectServer[] = { 172, 16, 0, 2 }; - shimClient.expectConnect(expectServer,1883); - - // Set keepalive to 300secs == 0x01 0x2c - byte connect[] = {0x10,0x18,0x0,0x4,0x4d,0x51,0x54,0x54,0x4,0x2,0x01,0x2c,0x0,0xc,0x63,0x6c,0x69,0x65,0x6e,0x74,0x5f,0x74,0x65,0x73,0x74,0x31}; - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - - shimClient.expect(connect,26); - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int state = client.state(); - IS_TRUE(state == MQTT_DISCONNECTED); - - client.setKeepAlive(300); - - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - - state = client.state(); - IS_TRUE(state == MQTT_CONNECTED); - - END_IT -} - - -int main() -{ - SUITE("Connect"); - - test_connect_fails_no_network(); - test_connect_fails_on_no_response(); - - test_connect_properly_formatted(); - test_connect_non_clean_session(); - test_connect_accepts_username_password(); - test_connect_fails_on_bad_rc(); - test_connect_properly_formatted_hostname(); - - test_connect_accepts_username_no_password(); - test_connect_ignores_password_no_username(); - test_connect_with_will(); - test_connect_with_will_username_password(); - test_connect_disconnect_connect(); - - test_connect_custom_keepalive(); - FINISH -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/keepalive_spec.cpp b/lib/default/pubsubclient-2.8.13/tests/src/keepalive_spec.cpp deleted file mode 100644 index ea643cf17..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/keepalive_spec.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "PubSubClient.h" -#include "ShimClient.h" -#include "Buffer.h" -#include "BDDTest.h" -#include "trace.h" -#include - -byte server[] = { 172, 16, 0, 2 }; - -void callback(char* topic, byte* payload, unsigned int length) { - // handle message arrived -} - - -int test_keepalive_pings_idle() { - IT("keeps an idle connection alive (takes 1 minute)"); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte pingreq[] = { 0xC0,0x0 }; - shimClient.expect(pingreq,2); - byte pingresp[] = { 0xD0,0x0 }; - shimClient.respond(pingresp,2); - - for (int i = 0; i < 50; i++) { - sleep(1); - if ( i == 15 || i == 31 || i == 47) { - shimClient.expect(pingreq,2); - shimClient.respond(pingresp,2); - } - rc = client.loop(); - IS_TRUE(rc); - } - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_keepalive_pings_with_outbound_qos0() { - IT("keeps a connection alive that only sends qos0 (takes 1 minute)"); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - - for (int i = 0; i < 50; i++) { - TRACE(i<<":"); - shimClient.expect(publish,16); - rc = client.publish((char*)"topic",(char*)"payload"); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - sleep(1); - if ( i == 15 || i == 31 || i == 47) { - byte pingreq[] = { 0xC0,0x0 }; - shimClient.expect(pingreq,2); - byte pingresp[] = { 0xD0,0x0 }; - shimClient.respond(pingresp,2); - } - rc = client.loop(); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - } - - END_IT -} - -int test_keepalive_pings_with_inbound_qos0() { - IT("keeps a connection alive that only receives qos0 (takes 1 minute)"); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - - for (int i = 0; i < 50; i++) { - TRACE(i<<":"); - sleep(1); - if ( i == 15 || i == 31 || i == 47) { - byte pingreq[] = { 0xC0,0x0 }; - shimClient.expect(pingreq,2); - byte pingresp[] = { 0xD0,0x0 }; - shimClient.respond(pingresp,2); - } - shimClient.respond(publish,16); - rc = client.loop(); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - } - - END_IT -} - -int test_keepalive_no_pings_inbound_qos1() { - IT("does not send pings for connections with inbound qos1 (takes 1 minute)"); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x32,0x10,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x12,0x34,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - byte puback[] = {0x40,0x2,0x12,0x34}; - - for (int i = 0; i < 50; i++) { - shimClient.respond(publish,18); - shimClient.expect(puback,4); - sleep(1); - rc = client.loop(); - IS_TRUE(rc); - IS_FALSE(shimClient.error()); - } - - END_IT -} - -int test_keepalive_disconnects_hung() { - IT("disconnects a hung connection (takes 30 seconds)"); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte pingreq[] = { 0xC0,0x0 }; - shimClient.expect(pingreq,2); - - for (int i = 0; i < 32; i++) { - sleep(1); - rc = client.loop(); - } - IS_FALSE(rc); - - int state = client.state(); - IS_TRUE(state == MQTT_CONNECTION_TIMEOUT); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int main() -{ - SUITE("Keep-alive"); - test_keepalive_pings_idle(); - test_keepalive_pings_with_outbound_qos0(); - test_keepalive_pings_with_inbound_qos0(); - test_keepalive_no_pings_inbound_qos1(); - test_keepalive_disconnects_hung(); - - FINISH -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/Arduino.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/Arduino.h deleted file mode 100644 index 2a00f24bc..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/Arduino.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef Arduino_h -#define Arduino_h - -#include -#include -#include -#include -#include "Print.h" - - -extern "C"{ - typedef uint8_t byte ; - typedef uint8_t boolean ; - - /* sketch */ - extern void setup( void ) ; - extern void loop( void ) ; - uint32_t millis( void ); -} - -#define PROGMEM -#define pgm_read_byte_near(x) *(x) - -#define yield(x) {} - -#endif // Arduino_h diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/BDDTest.cpp b/lib/default/pubsubclient-2.8.13/tests/src/lib/BDDTest.cpp deleted file mode 100644 index a72bf65e2..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/BDDTest.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "BDDTest.h" -#include "trace.h" -#include -#include -#include -#include - -int testCount = 0; -int testPasses = 0; -const char* testDescription; - -std::list failureList; - -void bddtest_suite(const char* name) { - LOG(name << "\n"); -} - -int bddtest_test(const char* file, int line, const char* assertion, int result) { - if (!result) { - LOG("✗\n"); - std::ostringstream os; - os << " ! "<::iterator it = failureList.begin(); it != failureList.end(); it++) { - LOG("\n"); - LOG(*it); - LOG("\n"); - } - - LOG(std::dec << testPasses << "/" << testCount << " tests passed\n\n"); - if (testPasses == testCount) { - return 0; - } - return 1; -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/BDDTest.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/BDDTest.h deleted file mode 100644 index 1197fdd42..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/BDDTest.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef bddtest_h -#define bddtest_h - -void bddtest_suite(const char* name); -int bddtest_test(const char*, int, const char*, int); -void bddtest_start(const char*); -void bddtest_end(); -int bddtest_summary(); - -#define SUITE(x) { bddtest_suite(x); } -#define TEST(x) { if (!bddtest_test(__FILE__, __LINE__, #x, (x))) return false; } - -#define IT(x) { bddtest_start(x); } -#define END_IT { bddtest_end();return true;} - -#define FINISH { return bddtest_summary(); } - -#define IS_TRUE(x) TEST(x) -#define IS_FALSE(x) TEST(!(x)) -#define IS_EQUAL(x,y) TEST(x==y) -#define IS_NOT_EQUAL(x,y) TEST(x!=y) - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/Buffer.cpp b/lib/default/pubsubclient-2.8.13/tests/src/lib/Buffer.cpp deleted file mode 100644 index f07759a3a..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/Buffer.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "Buffer.h" -#include "Arduino.h" - -Buffer::Buffer() { - this->pos = 0; - this->length = 0; -} - -Buffer::Buffer(uint8_t* buf, size_t size) { - this->pos = 0; - this->length = 0; - this->add(buf,size); -} -bool Buffer::available() { - return this->pos < this->length; -} - -uint8_t Buffer::next() { - if (this->available()) { - return this->buffer[this->pos++]; - } - return 0; -} - -void Buffer::reset() { - this->pos = 0; -} - -void Buffer::add(uint8_t* buf, size_t size) { - uint16_t i = 0; - for (;ibuffer[this->length++] = buf[i]; - } -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/Buffer.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/Buffer.h deleted file mode 100644 index c6a2cb584..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/Buffer.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef buffer_h -#define buffer_h - -#include "Arduino.h" - -class Buffer { -private: - uint8_t buffer[2048]; - uint16_t pos; - uint16_t length; - -public: - Buffer(); - Buffer(uint8_t* buf, size_t size); - - virtual bool available(); - virtual uint8_t next(); - virtual void reset(); - - virtual void add(uint8_t* buf, size_t size); -}; - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/Client.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/Client.h deleted file mode 100644 index 9e18c0764..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/Client.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef client_h -#define client_h -#include "IPAddress.h" - -class Client { -public: - virtual int connect(IPAddress ip, uint16_t port) =0; - virtual int connect(const char *host, uint16_t port) =0; - virtual size_t write(uint8_t) =0; - virtual size_t write(const uint8_t *buf, size_t size) =0; - virtual int available() = 0; - virtual int read() = 0; - virtual int read(uint8_t *buf, size_t size) = 0; - virtual int peek() = 0; - virtual void flush() = 0; - virtual void stop() = 0; - virtual uint8_t connected() = 0; - virtual operator bool() = 0; -}; - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/IPAddress.cpp b/lib/default/pubsubclient-2.8.13/tests/src/lib/IPAddress.cpp deleted file mode 100644 index 610ff4c53..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/IPAddress.cpp +++ /dev/null @@ -1,44 +0,0 @@ - -#include -#include - -IPAddress::IPAddress() -{ - memset(_address, 0, sizeof(_address)); -} - -IPAddress::IPAddress(uint8_t first_octet, uint8_t second_octet, uint8_t third_octet, uint8_t fourth_octet) -{ - _address[0] = first_octet; - _address[1] = second_octet; - _address[2] = third_octet; - _address[3] = fourth_octet; -} - -IPAddress::IPAddress(uint32_t address) -{ - memcpy(_address, &address, sizeof(_address)); -} - -IPAddress::IPAddress(const uint8_t *address) -{ - memcpy(_address, address, sizeof(_address)); -} - -IPAddress& IPAddress::operator=(const uint8_t *address) -{ - memcpy(_address, address, sizeof(_address)); - return *this; -} - -IPAddress& IPAddress::operator=(uint32_t address) -{ - memcpy(_address, (const uint8_t *)&address, sizeof(_address)); - return *this; -} - -bool IPAddress::operator==(const uint8_t* addr) -{ - return memcmp(addr, _address, sizeof(_address)) == 0; -} - diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/IPAddress.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/IPAddress.h deleted file mode 100644 index e75a8fe65..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/IPAddress.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * - * MIT License: - * Copyright (c) 2011 Adrian McEwen - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * adrianm@mcqn.com 1/1/2011 - */ - -#ifndef IPAddress_h -#define IPAddress_h - - -// A class to make it easier to handle and pass around IP addresses - -class IPAddress { -private: - uint8_t _address[4]; // IPv4 address - // Access the raw byte array containing the address. Because this returns a pointer - // to the internal structure rather than a copy of the address this function should only - // be used when you know that the usage of the returned uint8_t* will be transient and not - // stored. - uint8_t* raw_address() { return _address; }; - -public: - // Constructors - IPAddress(); - IPAddress(uint8_t first_octet, uint8_t second_octet, uint8_t third_octet, uint8_t fourth_octet); - IPAddress(uint32_t address); - IPAddress(const uint8_t *address); - - // Overloaded cast operator to allow IPAddress objects to be used where a pointer - // to a four-byte uint8_t array is expected - operator uint32_t() { return *((uint32_t*)_address); }; - bool operator==(const IPAddress& addr) { return (*((uint32_t*)_address)) == (*((uint32_t*)addr._address)); }; - bool operator==(const uint8_t* addr); - - // Overloaded index operator to allow getting and setting individual octets of the address - uint8_t operator[](int index) const { return _address[index]; }; - uint8_t& operator[](int index) { return _address[index]; }; - - // Overloaded copy operators to allow initialisation of IPAddress objects from other types - IPAddress& operator=(const uint8_t *address); - IPAddress& operator=(uint32_t address); - - - friend class EthernetClass; - friend class UDP; - friend class Client; - friend class Server; - friend class DhcpClass; - friend class DNSClient; -}; - - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/Print.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/Print.h deleted file mode 100644 index 02ef77c2c..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/Print.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - Print.h - Base class that provides print() and println() - Copyright (c) 2008 David A. Mellis. All right reserved. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#ifndef Print_h -#define Print_h - -class Print { - public: - virtual size_t write(uint8_t) = 0; -}; - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/ShimClient.cpp b/lib/default/pubsubclient-2.8.13/tests/src/lib/ShimClient.cpp deleted file mode 100644 index f70115fa8..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/ShimClient.cpp +++ /dev/null @@ -1,153 +0,0 @@ -#include "ShimClient.h" -#include "trace.h" -#include -#include -#include - -extern "C" { - uint32_t millis(void) { - return time(0)*1000; - } -} - -ShimClient::ShimClient() { - this->responseBuffer = new Buffer(); - this->expectBuffer = new Buffer(); - this->_allowConnect = true; - this->_connected = false; - this->_error = false; - this->expectAnything = true; - this->_received = 0; - this->_expectedPort = 0; -} - -int ShimClient::connect(IPAddress ip, uint16_t port) { - if (this->_allowConnect) { - this->_connected = true; - } - if (this->_expectedPort !=0) { - // if (memcmp(ip,this->_expectedIP,4) != 0) { - // TRACE( "ip mismatch\n"); - // this->_error = true; - // } - if (port != this->_expectedPort) { - TRACE( "port mismatch\n"); - this->_error = true; - } - } - return this->_connected; -} -int ShimClient::connect(const char *host, uint16_t port) { - if (this->_allowConnect) { - this->_connected = true; - } - if (this->_expectedPort !=0) { - if (strcmp(host,this->_expectedHost) != 0) { - TRACE( "host mismatch\n"); - this->_error = true; - } - if (port != this->_expectedPort) { - TRACE( "port mismatch\n"); - this->_error = true; - } - - } - return this->_connected; -} -size_t ShimClient::write(uint8_t b) { - this->_received += 1; - TRACE(std::hex << (unsigned int)b); - if (!this->expectAnything) { - if (this->expectBuffer->available()) { - uint8_t expected = this->expectBuffer->next(); - if (expected != b) { - this->_error = true; - TRACE("!=" << (unsigned int)expected); - } - } else { - this->_error = true; - } - } - TRACE("\n"<< std::dec); - return 1; -} -size_t ShimClient::write(const uint8_t *buf, size_t size) { - this->_received += size; - TRACE( "[" << std::dec << (unsigned int)(size) << "] "); - uint16_t i=0; - for (;i0) { - TRACE(":"); - } - TRACE(std::hex << (unsigned int)(buf[i])); - - if (!this->expectAnything) { - if (this->expectBuffer->available()) { - uint8_t expected = this->expectBuffer->next(); - if (expected != buf[i]) { - this->_error = true; - TRACE("!=" << (unsigned int)expected); - } - } else { - this->_error = true; - } - } - } - TRACE("\n"<responseBuffer->available(); -} -int ShimClient::read() { return this->responseBuffer->next(); } -int ShimClient::read(uint8_t *buf, size_t size) { - uint16_t i = 0; - for (;iread(); - } - return size; -} -int ShimClient::peek() { return 0; } -void ShimClient::flush() {} -void ShimClient::stop() { - this->setConnected(false); -} -uint8_t ShimClient::connected() { return this->_connected; } -ShimClient::operator bool() { return true; } - - -ShimClient* ShimClient::respond(uint8_t *buf, size_t size) { - this->responseBuffer->add(buf,size); - return this; -} - -ShimClient* ShimClient::expect(uint8_t *buf, size_t size) { - this->expectAnything = false; - this->expectBuffer->add(buf,size); - return this; -} - -void ShimClient::setConnected(bool b) { - this->_connected = b; -} -void ShimClient::setAllowConnect(bool b) { - this->_allowConnect = b; -} - -bool ShimClient::error() { - return this->_error; -} - -uint16_t ShimClient::received() { - return this->_received; -} - -void ShimClient::expectConnect(IPAddress ip, uint16_t port) { - this->_expectedIP = ip; - this->_expectedPort = port; -} - -void ShimClient::expectConnect(const char *host, uint16_t port) { - this->_expectedHost = host; - this->_expectedPort = port; -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/ShimClient.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/ShimClient.h deleted file mode 100644 index 2e3f874fc..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/ShimClient.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef shimclient_h -#define shimclient_h - -#include "Arduino.h" -#include "Client.h" -#include "IPAddress.h" -#include "Buffer.h" - - -class ShimClient : public Client { -private: - Buffer* responseBuffer; - Buffer* expectBuffer; - bool _allowConnect; - bool _connected; - bool expectAnything; - bool _error; - uint16_t _received; - IPAddress _expectedIP; - uint16_t _expectedPort; - const char* _expectedHost; - -public: - ShimClient(); - virtual int connect(IPAddress ip, uint16_t port); - virtual int connect(const char *host, uint16_t port); - virtual size_t write(uint8_t); - virtual size_t write(const uint8_t *buf, size_t size); - virtual int available(); - virtual int read(); - virtual int read(uint8_t *buf, size_t size); - virtual int peek(); - virtual void flush(); - virtual void stop(); - virtual uint8_t connected(); - virtual operator bool(); - - virtual ShimClient* respond(uint8_t *buf, size_t size); - virtual ShimClient* expect(uint8_t *buf, size_t size); - - virtual void expectConnect(IPAddress ip, uint16_t port); - virtual void expectConnect(const char *host, uint16_t port); - - virtual uint16_t received(); - virtual bool error(); - - virtual void setAllowConnect(bool b); - virtual void setConnected(bool b); -}; - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/Stream.cpp b/lib/default/pubsubclient-2.8.13/tests/src/lib/Stream.cpp deleted file mode 100644 index b0ecbb44e..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/Stream.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "Stream.h" -#include "trace.h" -#include -#include - -Stream::Stream() { - this->expectBuffer = new Buffer(); - this->_error = false; - this->_written = 0; -} - -size_t Stream::write(uint8_t b) { - this->_written++; - TRACE(std::hex << (unsigned int)b); - if (this->expectBuffer->available()) { - uint8_t expected = this->expectBuffer->next(); - if (expected != b) { - this->_error = true; - TRACE("!=" << (unsigned int)expected); - } - } else { - this->_error = true; - } - TRACE("\n"<< std::dec); - return 1; -} - - -bool Stream::error() { - return this->_error; -} - -void Stream::expect(uint8_t *buf, size_t size) { - this->expectBuffer->add(buf,size); -} - -uint16_t Stream::length() { - return this->_written; -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/Stream.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/Stream.h deleted file mode 100644 index 4e41f86fa..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/Stream.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef Stream_h -#define Stream_h - -#include "Arduino.h" -#include "Buffer.h" - -class Stream { -private: - Buffer* expectBuffer; - bool _error; - uint16_t _written; - -public: - Stream(); - virtual size_t write(uint8_t); - - virtual bool error(); - virtual void expect(uint8_t *buf, size_t size); - virtual uint16_t length(); -}; - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/lib/trace.h b/lib/default/pubsubclient-2.8.13/tests/src/lib/trace.h deleted file mode 100644 index 42eb99104..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/lib/trace.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef trace_h -#define trace_h -#include - -#include - -#define LOG(x) {std::cout << x << std::flush; } -#define TRACE(x) {if (getenv("TRACE")) { std::cout << x << std::flush; }} - -#endif diff --git a/lib/default/pubsubclient-2.8.13/tests/src/publish_spec.cpp b/lib/default/pubsubclient-2.8.13/tests/src/publish_spec.cpp deleted file mode 100644 index ee3d3bedb..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/publish_spec.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include "PubSubClient.h" -#include "ShimClient.h" -#include "Buffer.h" -#include "BDDTest.h" -#include "trace.h" - - -byte server[] = { 172, 16, 0, 2 }; - -void callback(char* topic, byte* payload, unsigned int length) { - // handle message arrived -} - -int test_publish() { - IT("publishes a null-terminated string"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - shimClient.expect(publish,16); - - rc = client.publish((char*)"topic",(char*)"payload"); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - - -int test_publish_bytes() { - IT("publishes a byte array"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte payload[] = { 0x01,0x02,0x03,0x0,0x05 }; - int length = 5; - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1,0x2,0x3,0x0,0x5}; - shimClient.expect(publish,14); - - rc = client.publish((char*)"topic",payload,length); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - - -int test_publish_retained() { - IT("publishes retained - 1"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte payload[] = { 0x01,0x02,0x03,0x0,0x05 }; - int length = 5; - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x31,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1,0x2,0x3,0x0,0x5}; - shimClient.expect(publish,14); - - rc = client.publish((char*)"topic",payload,length,true); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_publish_retained_2() { - IT("publishes retained - 2"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x31,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,'A','B','C','D','E'}; - shimClient.expect(publish,14); - - rc = client.publish((char*)"topic",(char*)"ABCDE",true); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_publish_not_connected() { - IT("publish fails when not connected"); - ShimClient shimClient; - - PubSubClient client(server, 1883, callback, shimClient); - - int rc = client.publish((char*)"topic",(char*)"payload"); - IS_FALSE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_publish_too_long() { - IT("publish fails when topic/payload are too long"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - client.setBufferSize(128); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - // 0 1 2 3 4 5 6 7 8 9 0 1 2 - rc = client.publish((char*)"topic",(char*)"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); - IS_FALSE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_publish_P() { - IT("publishes using PROGMEM"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte payload[] = { 0x01,0x02,0x03,0x0,0x05 }; - int length = 5; - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x31,0xc,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1,0x2,0x3,0x0,0x5}; - shimClient.expect(publish,14); - - rc = client.publish_P((char*)"topic",payload,length,true); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - - - - -int main() -{ - SUITE("Publish"); - test_publish(); - test_publish_bytes(); - test_publish_retained(); - test_publish_retained_2(); - test_publish_not_connected(); - test_publish_too_long(); - test_publish_P(); - - FINISH -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/receive_spec.cpp b/lib/default/pubsubclient-2.8.13/tests/src/receive_spec.cpp deleted file mode 100644 index 93e909aeb..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/receive_spec.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#include "PubSubClient.h" -#include "ShimClient.h" -#include "Buffer.h" -#include "BDDTest.h" -#include "trace.h" - - -byte server[] = { 172, 16, 0, 2 }; - -bool callback_called = false; -char lastTopic[1024]; -char lastPayload[1024]; -unsigned int lastLength; - -void reset_callback() { - callback_called = false; - lastTopic[0] = '\0'; - lastPayload[0] = '\0'; - lastLength = 0; -} - -void callback(char* topic, byte* payload, unsigned int length) { - TRACE("Callback received topic=[" << topic << "] length=" << length << "\n") - callback_called = true; - strcpy(lastTopic,topic); - memcpy(lastPayload,payload,length); - lastLength = length; -} - -int test_receive_callback() { - IT("receives a callback message"); - reset_callback(); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - shimClient.respond(publish,16); - - rc = client.loop(); - - IS_TRUE(rc); - - IS_TRUE(callback_called); - IS_TRUE(strcmp(lastTopic,"topic")==0); - IS_TRUE(memcmp(lastPayload,"payload",7)==0); - IS_TRUE(lastLength == 7); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_receive_stream() { - IT("receives a streamed callback message"); - reset_callback(); - - Stream stream; - stream.expect((uint8_t*)"payload",7); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient, stream); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,0xe,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - shimClient.respond(publish,16); - - rc = client.loop(); - - IS_TRUE(rc); - - IS_TRUE(callback_called); - IS_TRUE(strcmp(lastTopic,"topic")==0); - IS_TRUE(lastLength == 7); - - IS_FALSE(stream.error()); - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_receive_max_sized_message() { - IT("receives an max-sized message"); - reset_callback(); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int length = 80; // If this is changed to > 128 then the publish packet below - // is no longer valid as it assumes the remaining length - // is a single-byte. Don't make that mistake like I just - // did and lose a whole evening tracking down the issue. - client.setBufferSize(length); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - - byte publish[] = {0x30,length-2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - byte bigPublish[length]; - memset(bigPublish,'A',length); - bigPublish[length] = 'B'; - memcpy(bigPublish,publish,16); - shimClient.respond(bigPublish,length); - - rc = client.loop(); - - IS_TRUE(rc); - - IS_TRUE(callback_called); - IS_TRUE(strcmp(lastTopic,"topic")==0); - IS_TRUE(lastLength == length-9); - IS_TRUE(memcmp(lastPayload,bigPublish+9,lastLength)==0); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_receive_oversized_message() { - IT("drops an oversized message"); - reset_callback(); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - int length = 80; // See comment in test_receive_max_sized_message before changing this value - - PubSubClient client(server, 1883, callback, shimClient); - client.setBufferSize(length-1); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,length-2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - byte bigPublish[length]; - memset(bigPublish,'A',length); - bigPublish[length] = 'B'; - memcpy(bigPublish,publish,16); - shimClient.respond(bigPublish,length); - - rc = client.loop(); - - IS_TRUE(rc); - - IS_FALSE(callback_called); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_drop_invalid_remaining_length_message() { - IT("drops invalid remaining length message"); - reset_callback(); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,0x92,0x92,0x92,0x92,0x01,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - shimClient.respond(publish,20); - - rc = client.loop(); - - IS_FALSE(rc); - - IS_FALSE(callback_called); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_resize_buffer() { - IT("receives a message larger than the default maximum"); - reset_callback(); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - int length = 80; // See comment in test_receive_max_sized_message before changing this value - - PubSubClient client(server, 1883, callback, shimClient); - client.setBufferSize(length-1); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,length-2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - byte bigPublish[length]; - memset(bigPublish,'A',length); - bigPublish[length] = 'B'; - memcpy(bigPublish,publish,16); - // Send it twice - shimClient.respond(bigPublish,length); - shimClient.respond(bigPublish,length); - - rc = client.loop(); - IS_TRUE(rc); - - // First message fails as it is too big - IS_FALSE(callback_called); - - // Resize the buffer - client.setBufferSize(length); - - rc = client.loop(); - IS_TRUE(rc); - - IS_TRUE(callback_called); - - IS_TRUE(strcmp(lastTopic,"topic")==0); - IS_TRUE(lastLength == length-9); - IS_TRUE(memcmp(lastPayload,bigPublish+9,lastLength)==0); - - IS_FALSE(shimClient.error()); - - END_IT -} - - -int test_receive_oversized_stream_message() { - IT("receive an oversized streamed message"); - reset_callback(); - - Stream stream; - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - int length = 80; // See comment in test_receive_max_sized_message before changing this value - - PubSubClient client(server, 1883, callback, shimClient, stream); - client.setBufferSize(length-1); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x30,length-2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - - byte bigPublish[length]; - memset(bigPublish,'A',length); - bigPublish[length] = 'B'; - memcpy(bigPublish,publish,16); - - shimClient.respond(bigPublish,length); - stream.expect(bigPublish+9,length-9); - - rc = client.loop(); - - IS_TRUE(rc); - - IS_TRUE(callback_called); - IS_TRUE(strcmp(lastTopic,"topic")==0); - - IS_TRUE(lastLength == length-10); - - IS_FALSE(stream.error()); - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_receive_qos1() { - IT("receives a qos1 message"); - reset_callback(); - - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte publish[] = {0x32,0x10,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x12,0x34,0x70,0x61,0x79,0x6c,0x6f,0x61,0x64}; - shimClient.respond(publish,18); - - byte puback[] = {0x40,0x2,0x12,0x34}; - shimClient.expect(puback,4); - - rc = client.loop(); - - IS_TRUE(rc); - - IS_TRUE(callback_called); - IS_TRUE(strcmp(lastTopic,"topic")==0); - IS_TRUE(memcmp(lastPayload,"payload",7)==0); - IS_TRUE(lastLength == 7); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int main() -{ - SUITE("Receive"); - test_receive_callback(); - test_receive_stream(); - test_receive_max_sized_message(); - test_drop_invalid_remaining_length_message(); - test_receive_oversized_message(); - test_resize_buffer(); - test_receive_oversized_stream_message(); - test_receive_qos1(); - - FINISH -} diff --git a/lib/default/pubsubclient-2.8.13/tests/src/subscribe_spec.cpp b/lib/default/pubsubclient-2.8.13/tests/src/subscribe_spec.cpp deleted file mode 100644 index 22dc8a443..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/src/subscribe_spec.cpp +++ /dev/null @@ -1,178 +0,0 @@ -#include "PubSubClient.h" -#include "ShimClient.h" -#include "Buffer.h" -#include "BDDTest.h" -#include "trace.h" - - -byte server[] = { 172, 16, 0, 2 }; - -void callback(char* topic, byte* payload, unsigned int length) { - // handle message arrived -} - -int test_subscribe_no_qos() { - IT("subscribe without qos defaults to 0"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte subscribe[] = { 0x82,0xa,0x0,0x2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x0 }; - shimClient.expect(subscribe,12); - byte suback[] = { 0x90,0x3,0x0,0x2,0x0 }; - shimClient.respond(suback,5); - - rc = client.subscribe((char*)"topic"); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_subscribe_qos_1() { - IT("subscribes qos 1"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte subscribe[] = { 0x82,0xa,0x0,0x2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63,0x1 }; - shimClient.expect(subscribe,12); - byte suback[] = { 0x90,0x3,0x0,0x2,0x1 }; - shimClient.respond(suback,5); - - rc = client.subscribe((char*)"topic",1); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_subscribe_not_connected() { - IT("subscribe fails when not connected"); - ShimClient shimClient; - - PubSubClient client(server, 1883, callback, shimClient); - - int rc = client.subscribe((char*)"topic"); - IS_FALSE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_subscribe_invalid_qos() { - IT("subscribe fails with invalid qos values"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - rc = client.subscribe((char*)"topic",2); - IS_FALSE(rc); - rc = client.subscribe((char*)"topic",254); - IS_FALSE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_subscribe_too_long() { - IT("subscribe fails with too long topic"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - client.setBufferSize(128); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - // max length should be allowed - // 0 1 2 3 4 5 6 7 8 9 0 1 2 - rc = client.subscribe((char*)"12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"); - IS_TRUE(rc); - - // 0 1 2 3 4 5 6 7 8 9 0 1 2 - rc = client.subscribe((char*)"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"); - IS_FALSE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - - -int test_unsubscribe() { - IT("unsubscribes"); - ShimClient shimClient; - shimClient.setAllowConnect(true); - - byte connack[] = { 0x20, 0x02, 0x00, 0x00 }; - shimClient.respond(connack,4); - - PubSubClient client(server, 1883, callback, shimClient); - int rc = client.connect((char*)"client_test1"); - IS_TRUE(rc); - - byte unsubscribe[] = { 0xA2,0x9,0x0,0x2,0x0,0x5,0x74,0x6f,0x70,0x69,0x63 }; - shimClient.expect(unsubscribe,12); - byte unsuback[] = { 0xB0,0x2,0x0,0x2 }; - shimClient.respond(unsuback,4); - - rc = client.unsubscribe((char*)"topic"); - IS_TRUE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int test_unsubscribe_not_connected() { - IT("unsubscribe fails when not connected"); - ShimClient shimClient; - - PubSubClient client(server, 1883, callback, shimClient); - - int rc = client.unsubscribe((char*)"topic"); - IS_FALSE(rc); - - IS_FALSE(shimClient.error()); - - END_IT -} - -int main() -{ - SUITE("Subscribe"); - test_subscribe_no_qos(); - test_subscribe_qos_1(); - test_subscribe_not_connected(); - test_subscribe_invalid_qos(); - test_subscribe_too_long(); - test_unsubscribe(); - test_unsubscribe_not_connected(); - FINISH -} diff --git a/lib/default/pubsubclient-2.8.13/tests/testcases/__init__.py b/lib/default/pubsubclient-2.8.13/tests/testcases/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/default/pubsubclient-2.8.13/tests/testcases/mqtt_basic.py b/lib/default/pubsubclient-2.8.13/tests/testcases/mqtt_basic.py deleted file mode 100644 index f23ef71c1..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/testcases/mqtt_basic.py +++ /dev/null @@ -1,39 +0,0 @@ -import unittest -import settings -import time -import mosquitto - - -def on_message(mosq, obj, msg): - obj.message_queue.append(msg) - - -class mqtt_basic(unittest.TestCase): - - message_queue = [] - - @classmethod - def setUpClass(self): - self.client = mosquitto.Mosquitto("pubsubclient_ut", clean_session=True, obj=self) - self.client.connect(settings.server_ip) - self.client.on_message = on_message - self.client.subscribe("outTopic", 0) - - @classmethod - def tearDownClass(self): - self.client.disconnect() - - def test_one(self): - i = 30 - while len(self.message_queue) == 0 and i > 0: - self.client.loop() - time.sleep(0.5) - i -= 1 - self.assertTrue(i > 0, "message receive timed-out") - self.assertEqual(len(self.message_queue), 1, "unexpected number of messages received") - msg = self.message_queue[0] - self.assertEqual(msg.mid, 0, "message id not 0") - self.assertEqual(msg.topic, "outTopic", "message topic incorrect") - self.assertEqual(msg.payload, "hello world") - self.assertEqual(msg.qos, 0, "message qos not 0") - self.assertEqual(msg.retain, False, "message retain flag incorrect") diff --git a/lib/default/pubsubclient-2.8.13/tests/testcases/mqtt_publish_in_callback.py b/lib/default/pubsubclient-2.8.13/tests/testcases/mqtt_publish_in_callback.py deleted file mode 100644 index 45b0a8515..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/testcases/mqtt_publish_in_callback.py +++ /dev/null @@ -1,59 +0,0 @@ -import unittest -import settings -import time -import mosquitto - - -def on_message(mosq, obj, msg): - obj.message_queue.append(msg) - - -class mqtt_publish_in_callback(unittest.TestCase): - - message_queue = [] - - @classmethod - def setUpClass(self): - self.client = mosquitto.Mosquitto("pubsubclient_ut", clean_session=True, obj=self) - self.client.connect(settings.server_ip) - self.client.on_message = on_message - self.client.subscribe("outTopic", 0) - - @classmethod - def tearDownClass(self): - self.client.disconnect() - - def test_connect(self): - i = 30 - while len(self.message_queue) == 0 and i > 0: - self.client.loop() - time.sleep(0.5) - i -= 1 - self.assertTrue(i > 0, "message receive timed-out") - self.assertEqual(len(self.message_queue), 1, "unexpected number of messages received") - msg = self.message_queue.pop(0) - self.assertEqual(msg.mid, 0, "message id not 0") - self.assertEqual(msg.topic, "outTopic", "message topic incorrect") - self.assertEqual(msg.payload, "hello world") - self.assertEqual(msg.qos, 0, "message qos not 0") - self.assertEqual(msg.retain, False, "message retain flag incorrect") - - def test_publish(self): - self.assertEqual(len(self.message_queue), 0, "message queue not empty") - payload = "abcdefghij" - self.client.publish("inTopic", payload) - - i = 30 - while len(self.message_queue) == 0 and i > 0: - self.client.loop() - time.sleep(0.5) - i -= 1 - - self.assertTrue(i > 0, "message receive timed-out") - self.assertEqual(len(self.message_queue), 1, "unexpected number of messages received") - msg = self.message_queue.pop(0) - self.assertEqual(msg.mid, 0, "message id not 0") - self.assertEqual(msg.topic, "outTopic", "message topic incorrect") - self.assertEqual(msg.payload, payload) - self.assertEqual(msg.qos, 0, "message qos not 0") - self.assertEqual(msg.retain, False, "message retain flag incorrect") diff --git a/lib/default/pubsubclient-2.8.13/tests/testcases/settings.py b/lib/default/pubsubclient-2.8.13/tests/testcases/settings.py deleted file mode 100644 index 4ad8719d8..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/testcases/settings.py +++ /dev/null @@ -1,2 +0,0 @@ -server_ip = "172.16.0.2" -arduino_ip = "172.16.0.100" diff --git a/lib/default/pubsubclient-2.8.13/tests/testsuite.py b/lib/default/pubsubclient-2.8.13/tests/testsuite.py deleted file mode 100644 index 788fc5d97..000000000 --- a/lib/default/pubsubclient-2.8.13/tests/testsuite.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python -import os -import os.path -import sys -import shutil -from subprocess import call -import importlib -import unittest -import re - -from testcases import settings - - -class Workspace(object): - - def __init__(self): - self.root_dir = os.getcwd() - self.build_dir = os.path.join(self.root_dir, "tmpbin") - self.log_dir = os.path.join(self.root_dir, "logs") - self.tests_dir = os.path.join(self.root_dir, "testcases") - self.examples_dir = os.path.join(self.root_dir, "../PubSubClient/examples") - self.examples = [] - self.tests = [] - if not os.path.isdir("../PubSubClient"): - raise Exception("Cannot find PubSubClient library") - try: - return __import__('ino') - except ImportError: - raise Exception("ino tool not installed") - - def init(self): - if os.path.isdir(self.build_dir): - shutil.rmtree(self.build_dir) - os.mkdir(self.build_dir) - if os.path.isdir(self.log_dir): - shutil.rmtree(self.log_dir) - os.mkdir(self.log_dir) - - os.chdir(self.build_dir) - call(["ino", "init"]) - - shutil.copytree("../../PubSubClient", "lib/PubSubClient") - - filenames = [] - for root, dirs, files in os.walk(self.examples_dir): - filenames += [os.path.join(root, f) for f in files if f.endswith(".ino")] - filenames.sort() - for e in filenames: - self.examples.append(Sketch(self, e)) - - filenames = [] - for root, dirs, files in os.walk(self.tests_dir): - filenames += [os.path.join(root, f) for f in files if f.endswith(".ino")] - filenames.sort() - for e in filenames: - self.tests.append(Sketch(self, e)) - - def clean(self): - shutil.rmtree(self.build_dir) - - -class Sketch(object): - def __init__(self, wksp, fn): - self.w = wksp - self.filename = fn - self.basename = os.path.basename(self.filename) - self.build_log = os.path.join(self.w.log_dir, "%s.log" % (os.path.basename(self.filename),)) - self.build_err_log = os.path.join(self.w.log_dir, "%s.err.log" % (os.path.basename(self.filename),)) - self.build_upload_log = os.path.join(self.w.log_dir, "%s.upload.log" % (os.path.basename(self.filename),)) - - def build(self): - sys.stdout.write(" Build: ") - sys.stdout.flush() - - # Copy sketch over, replacing IP addresses as necessary - fin = open(self.filename, "r") - lines = fin.readlines() - fin.close() - fout = open(os.path.join(self.w.build_dir, "src", "sketch.ino"), "w") - for l in lines: - if re.match(r"^byte server\[\] = {", l): - fout.write("byte server[] = { %s };\n" % (settings.server_ip.replace(".", ", "),)) - elif re.match(r"^byte ip\[\] = {", l): - fout.write("byte ip[] = { %s };\n" % (settings.arduino_ip.replace(".", ", "),)) - else: - fout.write(l) - fout.flush() - fout.close() - - # Run build - fout = open(self.build_log, "w") - ferr = open(self.build_err_log, "w") - rc = call(["ino", "build"], stdout=fout, stderr=ferr) - fout.close() - ferr.close() - if rc == 0: - sys.stdout.write("pass") - sys.stdout.write("\n") - return True - else: - sys.stdout.write("fail") - sys.stdout.write("\n") - with open(self.build_err_log) as f: - for line in f: - print(" " + line) - return False - - def upload(self): - sys.stdout.write(" Upload: ") - sys.stdout.flush() - fout = open(self.build_upload_log, "w") - rc = call(["ino", "upload"], stdout=fout, stderr=fout) - fout.close() - if rc == 0: - sys.stdout.write("pass") - sys.stdout.write("\n") - return True - else: - sys.stdout.write("fail") - sys.stdout.write("\n") - with open(self.build_upload_log) as f: - for line in f: - print(" " + line) - return False - - def test(self): - # import the matching test case, if it exists - try: - basename = os.path.basename(self.filename)[:-4] - i = importlib.import_module("testcases." + basename) - except: - sys.stdout.write(" Test: no tests found") - sys.stdout.write("\n") - return - c = getattr(i, basename) - - testmethods = [m for m in dir(c) if m.startswith("test_")] - testmethods.sort() - tests = [] - for m in testmethods: - tests.append(c(m)) - - result = unittest.TestResult() - c.setUpClass() - if self.upload(): - sys.stdout.write(" Test: ") - sys.stdout.flush() - for t in tests: - t.run(result) - print(str(result.testsRun - len(result.failures) - len(result.errors)) + "/" + str(result.testsRun)) - if not result.wasSuccessful(): - if len(result.failures) > 0: - for f in result.failures: - print("-- " + str(f[0])) - print(f[1]) - if len(result.errors) > 0: - print(" Errors:") - for f in result.errors: - print("-- " + str(f[0])) - print(f[1]) - c.tearDownClass() - - -if __name__ == '__main__': - run_tests = True - - w = Workspace() - w.init() - - for e in w.examples: - print("--------------------------------------") - print("[" + e.basename + "]") - if e.build() and run_tests: - e.test() - for e in w.tests: - print("--------------------------------------") - print("[" + e.basename + "]") - if e.build() and run_tests: - e.test() - - w.clean() diff --git a/tasmota/tasmota_xdrv_driver/xdrv_02_9_mqtt.ino b/tasmota/tasmota_xdrv_driver/xdrv_02_9_mqtt.ino index 4436a993e..f1805e6e6 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_02_9_mqtt.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_02_9_mqtt.ino @@ -220,6 +220,7 @@ void MqttSetClientTimeout(void) { void MqttInit(void) { // Force buffer size since the #define may not be visible from Arduino lib MqttClient.setBufferSize(MQTT_MAX_PACKET_SIZE); + MqttClient.setMaxIncomingPacketSize(MQTT_MAX_PACKET_SIZE); #ifdef USE_MQTT_AZURE_IOT Settings->mqtt_port = 8883; @@ -544,7 +545,12 @@ bool MqttPublishLib(const char* topic, const uint8_t* payload, unsigned int plen uint32_t written = MqttClient.write(payload, plength); if (written != plength) { - AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Message too large")); + // The fixed header (and possibly part of the payload) has already been sent + // by beginPublish()/write(). A short write leaves an incomplete MQTT control packet + // on the wire, so the connection is desynchronised and must not be reused. Drop it; + // Tasmota will reconnect on the next loop. + AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Partial write (%u/%u), dropping connection"), written, plength); + MqttClient.disconnect(); return false; } /* diff --git a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_mqttclient.ino b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_mqttclient.ino index dd5a999c7..6a2f2b819 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_mqttclient.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_52_3_berry_mqttclient.ino @@ -221,6 +221,7 @@ struct BerryMqttClient { mqtt = new PubSubClient(); mqtt->setClient(*active_client); mqtt->setBufferSize(MQTT_MAX_PACKET_SIZE); + mqtt->setMaxIncomingPacketSize(MQTT_MAX_PACKET_SIZE); mqtt->setKeepAlive(Settings->mqtt_keepalive); mqtt->setSocketTimeout(Settings->mqtt_socket_timeout);