From 1653b2e3861e170e46adac81a8dc50d6e7eee860 Mon Sep 17 00:00:00 2001 From: hsaturn Date: Mon, 20 Feb 2023 02:28:12 +0100 Subject: [PATCH] [tests] Added a compilation test (based on example tiny-tests) --- tests/Makefile | 1 + tests/compile-test/Makefile | 11 + tests/compile-test/tinymqtt-test.ino | 847 +++++++++++++++++++++++++++ 3 files changed, 859 insertions(+) create mode 100644 tests/compile-test/Makefile create mode 100644 tests/compile-test/tinymqtt-test.ino diff --git a/tests/Makefile b/tests/Makefile index 5580c4d..c87e065 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -6,6 +6,7 @@ tests: echo '==== Making:' $$(dirname $$i); \ $(MAKE) -C $$(dirname $$i) -j; \ done + $(MAKE) -C compile-test valgrind: @set -e; \ diff --git a/tests/compile-test/Makefile b/tests/compile-test/Makefile new file mode 100644 index 0000000..61c33b5 --- /dev/null +++ b/tests/compile-test/Makefile @@ -0,0 +1,11 @@ +# See https://github.com/bxparks/EpoxyDuino for documentation about this +# Makefile to compile and run Arduino programs natively on Linux or MacOS. + +include ../Makefile.opts + +APP_NAME := tinymqtt-test +ARDUINO_LIBS := AUnit AceCommon AceTime TinyMqtt EspMock ESP8266WiFi ESPAsync TinyConsole +ARDUINO_LIB_DIRS := ../../../EspMock/libraries +EPOXY_CORE := EPOXY_CORE_ESP8266 +DUMMY := 1 +include ../../../EpoxyDuino/EpoxyDuino.mk diff --git a/tests/compile-test/tinymqtt-test.ino b/tests/compile-test/tinymqtt-test.ino new file mode 100644 index 0000000..7412b48 --- /dev/null +++ b/tests/compile-test/tinymqtt-test.ino @@ -0,0 +1,847 @@ +// vim: ts=2 sw=2 expandtab smartindent +#include +#include // https://github.com/hsaturn/TinyMqtt +#include + +#include +#include +#include + +bool echo_on = true; +auto green = TinyConsole::green; +auto red = TinyConsole::red; +auto white = TinyConsole::white; +auto cyan = TinyConsole::cyan; +auto yellow = TinyConsole::yellow; +auto magenta = TinyConsole::magenta; +auto save_cursor = TinyConsole::save_cursor; +auto restore_cursor = TinyConsole::restore_cursor; +auto erase_to_end = TinyConsole::erase_to_end; + +const char* ssid = ""; +const char* password = ""; + +struct free_broker +{ + public: + free_broker(const char* s, uint16_t p, const char* comment) : url(s), port(p) {} + + TinyString url; + uint16_t port; +}; + +const std::map list = +{ + { "mqtthq", { "public.mqtthq.com" , 8083, "publish/subscribe" }}, + { "hivemq", { "broker.hivemq.com", 1883, "" }} +}; + +/** Very complex example + * Console allowing to make any kind of test, + * even some stress tests. + * + * Upload the sketch, the use the terminal. + * Press H for mini help. + * + * tested with mqtt-spy-0.5.4 + * TODO examples of scripts + */ + +void onPublish(const MqttClient* srce, const Topic& topic, const char* payload, size_t length) +{ + Console << cyan << "--> " << srce->id().c_str() << ": received " << topic.c_str() << white; + if (payload) + { + Console << ", payload[" << length << "]=["; + while(length--) + { + const char c=*payload++; + if (c<32) + Console << '?'; + else + Console << c; + } + Console << ']' << endl; + } +} + +std::map clients; +std::map brokers; +std::map vars; + +void eval(TinyString& cmd); + +void replace(const char* d, TinyString& str, TinyString srch, TinyString to) +{ + if (d[0] && d[1]) + { + srch=d[0]+srch+d[1]; + to=d[0]+to+d[1]; + + size_t pos = 0; + while((pos=str.find(srch, pos)) != TinyString::npos) + { + str.erase(pos, srch.length()); + str.insert(pos, to); + pos += to.length()-1; + } + } +} + +void replaceVars(TinyString& cmd) +{ + cmd = ' '+cmd+' '; + + for(auto it: vars) + { + replace("..", cmd, it.first, it.second); + replace(". ", cmd, it.first, it.second); + replace(" .", cmd, it.first, it.second); + replace(" ", cmd, it.first, it.second); + } + cmd.erase(0, cmd.find_first_not_of(' ')); + cmd.erase(cmd.find_last_not_of(' ')+1); + +} + +void onCommand(const TinyString& command) +{ + Console << endl; + TinyString cmd=command; + if (cmd.substr(0,3)!="set") replaceVars(cmd); + eval(cmd); + Console << endl; + Console.prompt(); +} + +void setup() +{ + Serial.begin(115200); + Console.begin(Serial); + Console.setPrompt("> "); + Console.setCallback(onCommand); + delay(500); + + Console.cls(); + Console << endl << endl; + Console << yellow + << "***************************************************************" << endl; + Console << "* Welcome to the TinyMqtt console" << endl; + Console << "* The console allows to test all features of the libraries." << endl; + Console << "* Enter help to view the list of commands." << endl; + Console << "***************************************************************" << endl; + Console << endl; + if (strlen(ssid)==0) + Console << red << "* ERROR: You must modify ssid/password in order" << endl + << " to be able to connect to your Wifi network." << endl; + Console << endl << white; + + Console << "Connecting to '" << ssid << "' "; + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) + { Console << '-'; delay(500); } + + Console << endl << "Connected to " << ssid << "IP address: " << WiFi.localIP() << endl; + + const char* name="tinytest"; + Console << "Starting MDNS, name= " << name; + + MqttBroker* broker = new MqttBroker(1883); + broker->begin(); + brokers["broker"] = broker; + + if (Console.isTerm()) onCommand("every 333 view"); +} + +TinyString getword(TinyString& str, const char* if_empty=nullptr, char sep=' '); + +int getint(TinyString& str, const int if_empty=0) +{ + TinyString str2=str; + TinyString sword = getword(str); + if (sword[0] and isdigit(sword[0])) + { + int ret=atoi(sword.c_str()); + while(isdigit(sword[0]) or sword[0]==' ') sword.erase(0,1); + if (sword.length()) str = sword+' '+str; + return ret; + } + str=str2; + return if_empty; +} + +TinyString getword(TinyString& str, const char* if_empty/*=nullptr*/, char sep/*=' '*/) +{ + char quote=(str[0]=='"' or str[0]=='\'' ? str[0] : 0); + if (quote) str.erase(0,1); + TinyString sword; + while(str.length() and (str[0]!=sep or quote)) + { + if (str[0]==quote) + { + str.erase(0,1); + break; + } + sword += str[0]; + str.erase(0,1); + } + while(str[0]==sep) str.erase(0,1); + if (if_empty and sword.length()==0) return if_empty; + if (quote==false and sword.length()>=4 and sword.substr(0,3)=="rnd") + { + sword.erase(0,3); + if (sword[0]=='(') + { + int to = 100; + sword.erase(0,1); + int from=getint(sword); + if (sword[0]==',') + { + sword.erase(0,1); + to = getint(sword); + if (sword[0]!=')') Console << "Missing ')'" << endl; + } + else + { + to=from; + from=0; + } + return String(random(from,to)).c_str(); + } + else + { + Console << "Missing '('" << endl; + } + } + while(str[0]==' ') str.erase(0,1); + return sword; +} + +bool isaddr(TinyString s) +{ + if (s.length()==0 or s.length()>3) return false; + for(char c: s) + if (c<'0' or c>'9') return false; + return true; +} + +TinyString getip(TinyString& str, const char* if_empty=nullptr, char sep=' ') +{ + TinyString addr=getword(str, if_empty, sep); + TinyString ip=addr; + std::vector build; + while(ip.length()) + { + TinyString b=getword(ip,nullptr,'.'); + if (isaddr(b) && build.size()<4) + { + build.push_back(b); + } + else + return addr; + } + IPAddress local=WiFi.localIP(); + addr.clear(); + while(build.size()!=4) + { + std::stringstream b; + b << (int)local[3-build.size()]; + build.insert(build.begin(), b.str().c_str()); + } + for(TinyString s: build) + { + if (addr.length()) addr += '.'; + addr += s; + } + Console << "connect address: " << addr << endl; + return addr; +} + +std::set commands = { + "broker", "blink", "client", "connect", + "create", "delete", "debug", "help", "interval", + "list", "ls", "ip", "off", "on", "set", + "publish", "reset", "subscribe", "unsubscribe", "view", "echo", "every" +}; + +void convertToCommand(TinyString& search) +{ + while(search[0]==' ') search.erase(0,1); + if (search.length()==0) return; + TinyString matches; + int count=0; + for(TinyString cmd: commands) + { + if (cmd.substr(0, search.length()) == search) + { + if (count) matches +=", "; + count++; + matches += cmd; + } + } + if (count==1) + search = matches; + else if (count>1) + { + Console << "Ambiguous command: " << matches << endl; + search.clear(); + } +} + +bool compare(TinyString s, const char* cmd) +{ + uint8_t p=0; + while(s[p++]==*cmd++) + { + if (*cmd==0 or s[p]==0) return true; + if (s[p]==' ') return true; + } + return false; +} + +using ClientFunction = void(*)(TinyString& cmd, MqttClient* publish); + +struct Every +{ + TinyString cmd; + uint32_t ms; + uint32_t next; + uint32_t underrun=0; + bool active=true; + + void dump() + { + if (active) + Console << green << "enabled"; + else + Console << red << "disabled"; + + auto mill=millis(); + Console << white << ms << "ms [" << cmd << "] next in "; + if (mill > next) + Console << "now"; + else + Console << next-mill << "ms"; + } +}; + +uint32_t blink_ms_on[16]; +uint32_t blink_ms_off[16]; +uint32_t blink_next[16]; +bool blink_state[16]; +int16_t blink; + +std::vector everies; + +void clientConnect(MqttClient* client, TinyString& cmd) +{ + TinyString remote = getword(cmd); + uint16_t port; + auto it=list.find(remote); + if (it != list.end()) + { + Console << "Connecting to free broker: " << remote << endl; + remote = it->second.url; + port=it->second.port; + } + else + port=getint(cmd); + + client->connect(remote.c_str(), port, getint(cmd, 60)); + Console << (client->connected() ? "connected." : "not connected") << endl; +} + +void eval(TinyString& cmd) +{ + while(cmd.length()) + { + MqttError retval = MqttOk; + + TinyString s; + MqttBroker* broker = nullptr; + MqttClient* client = nullptr; + + // client.function notation + if (cmd.find('.') != TinyString::npos && + cmd.find('.') < cmd.find(' ')) + { + s=getword(cmd, nullptr, '.'); + + if (s.length()) + { + if (clients.find(s) != clients.end()) + { + client = clients[s]; + } + else if (brokers.find(s) != brokers.end()) + { + broker = brokers[s]; + } + else + { + Console << red << "Unknown class (" << s.c_str() << ")" << white << endl; + cmd.clear(); + } + } + } + + s = getword(cmd); + if (s.length()) convertToCommand(s); + if (s.length()==0) + {} + else if (compare(s, "debug")) + { +#if TINY_MQTT_DEBUG + TinyMqtt::debug = getint(cmd); +#else + Console << red << "TinyMqtt not compiled in debug" << endl; +#endif + } + else if (compare(s, "list")) + { + Console << "List of free servers" << endl; + for(const auto& fb: list) + { + Console << " " << fb.first << " : " << fb.second.url << ":" << fb.second.port << endl; + } + } + else if (compare(s, "delete")) + { + if (client==nullptr && broker==nullptr) + { + s = getword(cmd); + if (clients.find(s) != clients.end()) + { + client = clients[s]; + } + else if (brokers.find(s) != brokers.end()) + { + broker = brokers[s]; + } + else + Console << red << "Unable to find (" << s.c_str() << ")" << white << endl; + } + if (client) + { + for (auto it: clients) + { + if (it.second != client) continue; + Console << "deleted" << endl; + delete (it.second); + clients.erase(it.first); + break; + } + cmd += " ls"; + } + else if (broker) + { + for(auto it: brokers) + { + if (broker != it.second) continue; + Console << "deleted" << endl; + delete (it.second); + brokers.erase(it.first); + break; + } + cmd += " ls"; + } + else + Console << "Nothing to delete" << endl; + } + else if (broker) + { + if (compare(s,"connect")) + { + Console << "NYI" << endl; + } + else if (compare(s, "view")) + { + broker->dump(); + } + else + { + Console << "Unknown broker command (" << s << ")" << endl; + s.clear(); + } + } + else if (client) + { + if (compare(s,"connect")) + { + clientConnect(client, cmd); + } + else if (compare(s,"publish")) + { + retval = client->publish(getword(cmd), getword(cmd)); + } + else if (compare(s,"subscribe")) + { + client->subscribe(getword(cmd)); + } + else if (compare(s, "unsubscribe")) + { + client->unsubscribe(getword(cmd)); + } + else if (compare(s, "view")) + { + client->dump(); + } + else + { + Console << "Unknown client command (" << s << ")" << endl; + s.clear(); + } + } + else if (compare(s, "on")) + { + uint8_t pin=getint(cmd, 2); + pinMode(pin, OUTPUT); + digitalWrite(pin, HIGH); + } + else if (compare(s, "off")) + { + uint8_t pin=getint(cmd, 2); + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); + } + else if (compare(s, "echo")) + { + s=getword(cmd); + if (s=="on") + echo_on = true; + else if (s=="off") + echo_on = false; + else + { + Console << s << ' '; + while(cmd.length()) + { + Console << getword(cmd) << ' '; + } + } + } + else if (compare(s, "every")) + { + uint32_t ms = getint(cmd, 0); + if (ms) + { + if (cmd.length()) + { + Every every; + every.ms=ms; + every.cmd=cmd; + every.next=millis()+ms; + everies.push_back(every); + every.dump(); + Console << endl; + cmd.clear(); + } + } + else if (compare(cmd, "off") or compare(cmd, "on")) + { + bool active=getword(cmd)=="on"; + uint8_t ever=getint(cmd, 100); + uint8_t count=0; + for(auto& every: everies) + { + if (count==ever or (ever==100)) + { + if (every.active != active) + { + Console << "every #" << count << (active ? " on" :" off") << endl; + every.active = active; + every.underrun = 0; + } + } + count++; + } + } + else if (compare(cmd, "list") or cmd.length()==0) + { + getword(cmd); + Console << "List of everies (ms=" << millis() << ")" << endl; + uint8_t count=0; + for(auto& every: everies) + { + Console << count << ": "; + every.dump(); + Console << endl; + count++; + } + } + else if (compare(cmd, "remove")) + { + Console << "Removing..." << endl; + getword(cmd); + int8_t every=getint(cmd, -1); + if (every==-1 and compare(cmd, "last") and everies.size()) + { + getword(cmd); + everies.erase(everies.begin()+everies.size()-1); + } + else if (every==-1 and compare(cmd, "all")) + { + getword(cmd); + everies.clear(); + } + else if (everies.size() > (uint8_t)every) + { + everies.erase(everies.begin()+every); + } + else + Console << "Bad colmmand" << endl; + } + else + Console << "Bad command" << endl; + } + else if (compare(s, "blink")) + { + int8_t blink_nr = getint(cmd, -1); + if (blink_nr >= 0) + { + blink_ms_on[blink_nr]=getint(cmd, blink_ms_on[blink_nr]); + blink_ms_off[blink_nr]=getint(cmd, blink_ms_on[blink_nr]); + pinMode(blink_nr, OUTPUT); + blink_next[blink_nr] = millis(); + Console << "Blink " << blink_nr << ' ' << (blink_ms_on[blink_nr] ? "on" : "off") << endl; + if (blink_ms_on[blink_nr]) + blink |= 1<< blink_nr; + else + { + blink &= ~(1<< blink_nr); + } + } + } + else if (compare(s, "broker")) + { + TinyString id=getword(cmd); + if (clients.find(id) != clients.end()) + { + Console << "A client already have that name" << endl; + cmd.clear(); + } + else if (id.length() or brokers.find(id)!=brokers.end()) + { + int port=getint(cmd, 0); + if (port) + { + MqttBroker* broker = new MqttBroker(port); + broker->begin(); + + brokers[id] = broker; + Console << "new broker (" << id.c_str() << ")" << endl; + } + else + { + Console << "Missing port" << endl; + cmd.clear(); + } + } + else + { + Console << "Missing or existing broker name (" << id.c_str() << ")" << endl; + cmd.clear(); + } + } + else if (compare(s, "client")) + { + TinyString id=getword(cmd); + if (brokers.find(id) != brokers.end()) + { + Console << "A broker have that name" << endl; + cmd.clear(); + } + else if (id.length() or clients.find(id)!=clients.end()) + { + s=getword(cmd); // broker + if (s=="" or brokers.find(s) != brokers.end() or list.find(s) != list.end()) + { + MqttBroker* broker = nullptr; + if (s.length()) broker = brokers[s]; + MqttClient* client = new MqttClient(broker, id); + clients[id]=client; + client->setCallback(onPublish); + if (list.find(s) != list.end()) + { + cmd=s+' '+cmd; + clientConnect(client, cmd); + } + Console << "new client (" << id.c_str() << ", " << s.c_str() << ')' << endl; + } + else if (s.length()) + { + Console << " not found." << endl; + cmd.clear(); + } + } + else + { + Console << "Missing or existing client name" << endl; + cmd.clear(); + } + } + else if (compare(s, "set")) + { + TinyString name(getword(cmd)); + if (name.length()==0) + { + for(auto it: vars) + { + Console << " " << it.first << " -> " << it.second << endl; + } + } + else if (commands.find(name) != commands.end()) + { + Console << "Reserved keyword (" << name << ")" << endl; + cmd.clear(); + } + else + { + if (cmd.length()) + { + vars[name] = cmd; + cmd.clear(); + } + else if (vars.find(name) != vars.end()) + vars.erase(vars.find(name)); + } + } + else if (compare(s, "ls") or compare(s, "view")) + { + bool view = compare(s, "view"); + if (view) + { + Console << save_cursor << magenta; + Console.gotoxy(1,1); + } + Console << "--< " << '/' << clients.size() << " client/s. >--" << erase_to_end << endl; + for(auto it: clients) + { + it.second->dump(" "); + } + + Console << "--< " << brokers.size() << " brokers/s. >--" << erase_to_end << endl; + for(auto it: brokers) + { + Console << " +-- '" << it.first.c_str() << "' " << it.second->clientsCount() << " client/s."<< erase_to_end << endl; + it.second->dump(" "); + } + if (view) + { + Console.bg(white); + Console << erase_to_end << restore_cursor; + } + } + else if (compare(s, "reset")) + ESP.restart(); + else if (compare(s, "ip")) + Console << "IP: " << WiFi.localIP() << endl; + else if (compare(s,"help")) + { + Console << "syntax:" << endl; + Console << " MqttBroker:" << endl; + Console << " broker {broker_name} {port} : create a new broker" << endl; + Console << " broker_name can be one of 'list'" << endl; + Console << " broker_name.delete : delete a broker (buggy)" << endl; + Console << " broker_name.view : dump a broker" << endl; + Console << endl; + Console << " MqttClient:" << endl; + Console << " client {name} {broker} : create a client then" << endl; + Console << " name.connect [ip] [port] [alive]" << endl; + Console << " name.[un]subscribe topic" << endl; + Console << " name.publish topic [payload]" << endl; + Console << " name.view" << endl; + Console << " name.delete" << endl; + Console << endl; + Console << " list : list of free brokers (debug 1 advised)" << endl; + Console << " debug #" << endl; + Console << " list : get list of free brokers" << endl; + Console << " blink [Dx on_ms off_ms] : make pin blink" << endl; + Console << " ls / ip / reset" << endl; + Console << " set [name][value]" << endl; + Console << " ! repeat last command" << endl; + Console << endl; + Console << " echo [on|off] or strings" << endl; + Console << " every ms [command]; every list; every remove [nr|all]; every (on|off) [#]" << endl; + Console << " on {output}; off {output}" << endl; + Console << " $id : name of the client." << endl; + Console << " rnd[(min[,max])] random number." << endl; + Console << endl; + } + else + { + while(s[0]==' ') s.erase(0,1); + if (s.length()) + Console << "Unknown command (" << s.c_str() << ")" << endl; + } + + if (retval != MqttOk) + { + Console << "# MQTT ERROR " << retval << endl; + } + } +} + +void loop() +{ + auto ms=millis(); + int8_t out=0; + int16_t blink_bits = blink; + uint8_t e=0; + + for(auto& every: everies) + { + if (not every.active) continue; + if (every.ms && every.cmd.length() && ms > every.next) + { + TinyString cmd(every.cmd); + eval(cmd); + every.next += every.ms; + if (ms > every.next and ms > every.underrun) + { + every.next += every.ms; + Console << yellow << "Underrun every #" << e << ", " << (ms - every.next) << "ms late" << endl; + every.underrun = ms+5000; + } + } + e++; + } + + while(blink_bits) + { + if (blink_ms_on[out] and ms > blink_next[out]) + { + if (blink_state[out]) + { + blink_next[out] += blink_ms_on[out]; + digitalWrite(out, LOW); + } + else + { + blink_next[out] += blink_ms_off[out]; + digitalWrite(abs(out), HIGH); + } + blink_state[out] = not blink_state[out]; + } + blink_bits >>=1; + out++; + } + + static long count; +#if defined(ESP9266) + MDNS.update(); +#endif + + for(auto it: brokers) + it.second->loop(); + + for(auto it: clients) + it.second->loop(); + + Console.loop(); +}