From 96766f70917659192594e8e4cc5acce1a2d6b0f6 Mon Sep 17 00:00:00 2001 From: hsaturn Date: Mon, 21 Nov 2022 01:22:47 +0100 Subject: [PATCH] [Topic] Wildcards added + wildcard added # wildcard added * wildcard added (but does not appear in mqtt specification...) $SYS messages compare is supported --- src/TinyMqtt.cpp | 60 +++++++++++++++++- tests/nowifi-tests/nowifi-tests.ino | 98 +++++++++++++++++++++++++++++ tests/topic-tests/Makefile | 10 +++ tests/topic-tests/topic-tests.ino | 85 +++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 tests/topic-tests/Makefile create mode 100644 tests/topic-tests/topic-tests.ino diff --git a/src/TinyMqtt.cpp b/src/TinyMqtt.cpp index 44ee735..cd3030c 100644 --- a/src/TinyMqtt.cpp +++ b/src/TinyMqtt.cpp @@ -636,10 +636,66 @@ if (mesg->type() != MqttMessage::Type::PingReq && mesg->type() != MqttMessage::T bool Topic::matches(const Topic& topic) const { if (getIndex() == topic.getIndex()) return true; - if (str() == topic.str()) return true; - return false; + const char* p1 = c_str(); + const char* p2 = topic.c_str(); + + if (p1 == p2) return true; + if (*p2 == '$' and *p1 != '$') return false; + + while(*p1 and *p2) + { + if (*p1 == '+') + { + ++p1; + if (*p1 and *p1!='/') return false; + if (*p1) ++p1; + while(*p2 and *p2++!='/'); + } + else if (*p1 == '#') + { + if (*++p1==0) return true; + return false; + } + else if (*p1 == '*') + { + const char c=*(p1+1); + if (c==0) return true; + if (c!='/') return false; + const char*p = p1+2; + while(*p and *p2) + { + if (*p == *p2) + { + if (*p==0) return true; + if (*p=='/') + { + p1=p; + break; + } + } + else + { + while(*p2 and *p2++!='/'); + break; + } + ++p; + ++p2; + } + if (*p==0) return true; + } + else if (*p1 == *p2) + { + ++p1; + ++p2; + } + else + return false; + } + if (*p1=='/' and p1[1]=='#' and p1[2]==0) return true; + return *p1==0 and *p2==0; } + // publish from local client MqttError MqttClient::publish(const Topic& topic, const char* payload, size_t pay_length) { diff --git a/tests/nowifi-tests/nowifi-tests.ino b/tests/nowifi-tests/nowifi-tests.ino index fc32904..179c4b6 100644 --- a/tests/nowifi-tests/nowifi-tests.ino +++ b/tests/nowifi-tests/nowifi-tests.ino @@ -93,6 +93,104 @@ test(nowifi_publish_should_be_dispatched_to_clients) assertEqual(published["B"]["a/c"], 0); } +test(nowifi_subscribe_with_star_wildcard) +{ + published.clear(); + assertEqual(broker.clientsCount(), (size_t)0); + + MqttClient subscriber(&broker, "A"); + subscriber.setCallback(onPublish); + subscriber.subscribe("house/*/temp"); + + MqttClient publisher(&broker); + publisher.publish("house/bedroom/temp"); + publisher.publish("house/kitchen/temp"); + publisher.publish("house/living_room/tv/temp"); + publisher.publish("building/location1/bedroom/temp"); + + assertEqual(published["A"]["house/bedroom/temp"], 1); + assertEqual(published["A"]["house/kitchen/temp"], 1); + assertEqual(published["A"]["house/living_room/tv/temp"], 1); + assertEqual(published["A"]["building/location1/bedroom/temp"], 0); +} + +test(nowifi_subscribe_with_plus_wildcard) +{ + published.clear(); + assertEqual(broker.clientsCount(), (size_t)0); + + MqttClient subscriber(&broker, "A"); + subscriber.setCallback(onPublish); + subscriber.subscribe("house/+/temp"); + + MqttClient publisher(&broker); + publisher.publish("house/bedroom/temp"); + publisher.publish("house/kitchen/temp"); + publisher.publish("house/living_room/tv/temp"); + publisher.publish("building/location1/bedroom/temp"); + + assertEqual(published["A"]["house/bedroom/temp"], 1); + assertEqual(published["A"]["house/kitchen/temp"], 1); + assertEqual(published["A"]["house/living_room/tv/temp"], 0); + assertEqual(published["A"]["building/location1/bedroom/temp"], 0); +} + +test(nowifi_should_not_receive_sys_msg) +{ + published.clear(); + assertEqual(broker.clientsCount(), (size_t)0); + + MqttClient subscriber(&broker, "A"); + subscriber.setCallback(onPublish); + subscriber.subscribe("+/data"); + + MqttClient publisher(&broker); + publisher.publish("$SYS/data"); + + assertEqual(published["A"]["$SYS/data"], 0); +} + +test(nowifi_subscribe_with_mixed_wildcards) +{ + published.clear(); + assertEqual(broker.clientsCount(), (size_t)0); + + MqttClient subscriber(&broker, "A"); + subscriber.setCallback(onPublish); + subscriber.subscribe("+/data/#"); + + MqttClient publisher(&broker); + publisher.publish("node1/data/update"); + publisher.publish("node2/data/delta"); + publisher.publish("node3/data"); + + assertEqual(published["A"]["node1/data/update"], 1); + assertEqual(published["A"]["node2/data/delta"], 1); + assertEqual(published["A"]["node3/data"], 1); +} + +test(nowifi_unsubscribe_with_wildcards) +{ + published.clear(); + assertEqual(broker.clientsCount(), (size_t)0); + + MqttClient subscriber(&broker, "A"); + subscriber.setCallback(onPublish); + subscriber.subscribe("one/two/+"); + subscriber.subscribe("one/two/three"); + + MqttClient publisher(&broker); + publisher.publish("one/two/three"); + publisher.publish("one/two/four"); + + subscriber.unsubscribe("one/two/+"); + publisher.publish("one/two/five"); + + assertEqual(published["A"]["one/two/three"], 1); + assertEqual(published["A"]["one/two/four"], 1); + assertEqual(published["A"]["one/two/five"], 0); +} + test(nowifi_unsubscribe) { published.clear(); diff --git a/tests/topic-tests/Makefile b/tests/topic-tests/Makefile new file mode 100644 index 0000000..faa3b40 --- /dev/null +++ b/tests/topic-tests/Makefile @@ -0,0 +1,10 @@ +# See https://github.com/bxparks/EpoxyDuino for documentation about this +# Makefile to compile and run Arduino programs natively on Linux or MacOS. + +EXTRA_CXXFLAGS=-g3 -O0 + +APP_NAME := topic-tests +ARDUINO_LIBS := AUnit AceCommon AceTime TinyMqtt EspMock ESP8266WiFi ESPAsync +ARDUINO_LIB_DIRS := ../../../EspMock/libraries +EPOXY_CORE := EPOXY_CORE_ESP8266 +include ../../../EpoxyDuino/EpoxyDuino.mk diff --git a/tests/topic-tests/topic-tests.ino b/tests/topic-tests/topic-tests.ino new file mode 100644 index 0000000..ecbaf68 --- /dev/null +++ b/tests/topic-tests/topic-tests.ino @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include + +#define endl "\n" + +/** + * TinyMqtt / StringIndexer unit tests. + * + **/ + +using namespace std; + +bool testTopicMatch(const char* a, const char* b, bool expected) +{ + Topic ta(a); + Topic tb(b); + bool match(ta.matches(tb)); + cout << " " << ta.c_str() << ' '; + if (match != expected) + cout << (expected ? " should match " : " should not match "); + else + cout << (expected ? " matches " : " unmatches "); + cout << tb.c_str() << endl; + return expected == match; +} + +test(topic_matches) +{ + // matching + assertTrue(testTopicMatch("a/b/c" , "a/b/c" , true)); + assertTrue(testTopicMatch("a/*/c" , "a/xyz/c" , true)); + assertTrue(testTopicMatch("a/*/e" , "a/b/c/d/e" , true)); + assertTrue(testTopicMatch("a/*" , "a/b/c/d/e" , true)); + assertTrue(testTopicMatch("*/c" , "a/b/c" , true)); + assertTrue(testTopicMatch("/*/c" , "/a/b/c" , true)); + assertTrue(testTopicMatch("a/*" , "a/b/c/d" , true)); + assertTrue(testTopicMatch("a/+/c" , "a/b/c" , true)); + assertTrue(testTopicMatch("a/+/c/+/e", "a/b/c/d/e" , true)); + assertTrue(testTopicMatch("a/*/c/+/e", "a/b/c/d/e" , true)); + assertTrue(testTopicMatch("/+/b" , "/a/b" , true)); + assertTrue(testTopicMatch("+" , "a" , true)); + assertTrue(testTopicMatch("a/b/#" , "a/b/c/d" , true)); + assertTrue(testTopicMatch("a/b/#" , "a/b" , true)); + assertTrue(testTopicMatch("a/*/c" , "a/*/c" , true)); + + // not matching + assertTrue(testTopicMatch("a/b/c" , "a/b/d" , false)); + assertTrue(testTopicMatch("a/b/c" , "a/b/d" , false)); + assertTrue(testTopicMatch("a/*/e" , "a/b/c/d/f" , false)); + assertTrue(testTopicMatch("a/+" , "a" , false)); + assertTrue(testTopicMatch("a/+" , "a/b/d" , false)); + assertTrue(testTopicMatch("a/+/" , "a/" , false)); + + // $SYS topics + assertTrue(testTopicMatch("+/any" , "$SYS/any" , false)); + assertTrue(testTopicMatch("*/any" , "$SYS/any" , false)); + assertTrue(testTopicMatch("$SYS/any" , "$SYS/any" , true)); + assertTrue(testTopicMatch("$SYS/+/y" , "$SYS/a/y" , true)); + assertTrue(testTopicMatch("$SYS/#" , "$SYS/a/y" , true)); + + // not valid + assertTrue(testTopicMatch("a/#/b" , "a/x/b" , false)); + assertTrue(testTopicMatch("a+" , "a/b/d" , false)); + assertTrue(testTopicMatch("a/b/#/d" , "a/b/c/d" , false)); + +} + +//---------------------------------------------------------------------------- +// setup() and loop() +void setup() { + delay(1000); + Serial.begin(115200); + while(!Serial); + + Serial.println("=============[ TinyMqtt StringIndexer TESTS ]========================"); +} + +void loop() { + aunit::TestRunner::run(); + + // if (Serial.available()) ESP.reset(); +}