Skip to content

Commit

Permalink
[Topic] Wildcards added
Browse files Browse the repository at this point in the history
  + wildcard added
  # wildcard added
  * wildcard added (but does not appear in mqtt specification...)
  $SYS messages compare is supported
  • Loading branch information
hsaturn committed Nov 21, 2022
1 parent 354aec2 commit 96766f7
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 2 deletions.
60 changes: 58 additions & 2 deletions src/TinyMqtt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
98 changes: 98 additions & 0 deletions tests/nowifi-tests/nowifi-tests.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions tests/topic-tests/Makefile
Original file line number Diff line number Diff line change
@@ -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
85 changes: 85 additions & 0 deletions tests/topic-tests/topic-tests.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include <Arduino.h>
#include <AUnit.h>
#include <TinyMqtt.h>
#include <map>
#include <iostream>

#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();
}

0 comments on commit 96766f7

Please sign in to comment.