diff --git a/.github/workflows/arduino-lint.yml b/.github/workflows/arduino-lint.yml index e18b6e4..0bf27f0 100644 --- a/.github/workflows/arduino-lint.yml +++ b/.github/workflows/arduino-lint.yml @@ -5,7 +5,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.1.1 with: token: ${{ secrets.GITHUB_TOKEN }} - uses: arduino/arduino-lint-action@v1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1b2dd98..4975be4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.1 - name: Requirements run: | pip3 install -r requirements.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3196458..a710c31 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,13 +1,13 @@ -name: tests +name: Run GoogleTest on: push: - branches: [main] + branches: [main, devel] pull_request: - branches: [main] + branches: [main, devel] jobs: - x86_64-linux-gnu-gcc: + build_run_tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.1 @@ -15,4 +15,4 @@ jobs: fetch-depth: 0 - run: cmake -Bbuild - run: cmake --build build --parallel --target DCCEXProtocolTests - - run: ./build/tests/DCCEXProtocolTests --gtest_shuffle + - run: ./build/tests/DCCEXProtocolTests --gtest_shuffle --gtest_repeat=5 --gtest_recreate_environments_when_repeating diff --git a/docs/usage.rst b/docs/usage.rst index 3d24a79..9564a64 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -87,6 +87,16 @@ Retrieving and referring to object lists To retrieve the various objects lists from |EX-CS|, use the `getLists(bool rosterRequired, bool turnoutListRequired, bool routeListRequired, bool turntableListRequired)` method within your `loop()` function to ensure these are retrieved successfully. +If you have a lot of defined objects in your |EX-CS| (eg. 50+ turnouts or 50+ roster entries), you will likely need to increase the maximum number of parameters allowed when defining the DCCEXProtocol instance which is now a configurable parameter as of version 1.0.0 of the library. + +You can set the command buffer size and parameter count: + +.. code-block:: cpp + + // dccexProtocol(maxCmdBuffer, maxCommandParams); + DCCEXProtocol dccexProtocol; // Use default 500 byte buffer, 50 parameters + DCCEXProtocol dccexProtocol(500, 100); // Use default 500 byte buffer, 100 parameters + All objects are contained within linked lists and can be access via for loops: .. code-block:: cpp diff --git a/library.properties b/library.properties index d514aa0..d270b75 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=DCCEXProtocol -version=0.0.17 +version=1.0.0 author=Peter Cole, Peter Akers maintainer=Peter Cole, Peter Akers sentence=DCC-EX Native Protocol implementation diff --git a/src/DCCEXLoco.cpp b/src/DCCEXLoco.cpp index cc13182..a3d2df1 100644 --- a/src/DCCEXLoco.cpp +++ b/src/DCCEXLoco.cpp @@ -5,6 +5,7 @@ * This package implements a DCCEX native protocol connection, * allow a device to communicate with a DCC-EX EX-CommandStation. * + * Copyright © 2024 Peter Cole * Copyright © 2023 Peter Akers * Copyright © 2023 Peter Cole * @@ -34,31 +35,42 @@ Loco *Loco::_first = nullptr; -Loco::Loco(int address, LocoSource source) { - _address = address; - _source = source; +Loco::Loco(int address, LocoSource source) : _address(address), _source(source) { + for (int i = 0; i < MAX_FUNCTIONS; i++) { + _functionNames[i] = nullptr; + } _direction = Forward; _speed = 0; _name = nullptr; _functionStates = 0; _momentaryFlags = 0; _next = nullptr; - if (!_first) { - _first = this; - } else { - Loco *current = _first; - while (current->_next != nullptr) { - current = current->_next; + if (_source == LocoSource::LocoSourceRoster) { + if (!_first) { + _first = this; + } else { + Loco *current = _first; + while (current->_next != nullptr) { + current = current->_next; + } + current->_next = this; } - current->_next = this; } } int Loco::getAddress() { return _address; } -void Loco::setName(char *name) { _name = name; } +void Loco::setName(const char *name) { + if (_name) { + delete[] _name; + _name = nullptr; + } + int nameLength = strlen(name); + _name = new char[nameLength + 1]; + strcpy(_name, name); +} -char *Loco::getName() { return _name; } +const char *Loco::getName() { return _name; } void Loco::setSpeed(int speed) { _speed = speed; } @@ -70,57 +82,64 @@ Direction Loco::getDirection() { return (Direction)_direction; } LocoSource Loco::getSource() { return (LocoSource)_source; } -void Loco::setupFunctions(char *functionNames) { - // Important note: - // The functionNames string is modified in place. - // console->print(F("Splitting \"")); - // console->print(functionNames); - // console->println(F("\"")); - char *t = functionNames; - int fkey = 0; - - while (*t) { - bool momentary = false; - if (*t == '*') { - momentary = true; - t++; - } - char *fName = t; // function name starts here - while (*t) { // loop completes at end of name ('/' or 0) - if (*t == '/') { - // found end of name - *t = '\0'; // mark name ends here - t++; - break; - } - t++; +void Loco::setupFunctions(const char *functionNames) { + if (functionNames == nullptr) { + return; + } + // Copy functionNames so we can clean up later + char *fNames = new char[strlen(functionNames) + 1]; + if (fNames == nullptr) { + return; // Bail out if malloc failed + } + strcpy(fNames, functionNames); // Copy names + + // Remove any existing names first + for (int nameIndex = 0; nameIndex < MAX_FUNCTIONS; nameIndex++) { + if (_functionNames[nameIndex] != nullptr) { + delete[] _functionNames[nameIndex]; + _functionNames[nameIndex] = nullptr; } + } - // At this point we have a function key - // int fkey = function number 0.... - // bool momentary = is it a momentary - // fName = pointer to the function name - if (fkey < MAX_FUNCTIONS) { - _functionNames[fkey] = fName; - if (momentary) { - _momentaryFlags |= 1 << fkey; + int fNameIndex = 0; // Index for each function name + int fNamesLength = strlen(fNames); // Length of all names for sizing later + int fNameStartChar = 0; // Position of the first char in the name + + // Iterate through the fNames char array to look for names + for (int charIndex = 0; charIndex <= fNamesLength; charIndex++) { + // End of name is either / or null terminator + // Start of name will be in char array index fNameStart + if (fNames[charIndex] == '/' || fNames[charIndex] == '\0') { + // Make sure we're at a sane index + if (fNameIndex < MAX_FUNCTIONS) { + bool momentary = false; + // If start is *, it's momentary, name starts at following index + if (fNames[fNameStartChar] == '*') { + momentary = true; + fNameStartChar++; + } + int nameLength = charIndex - fNameStartChar; // Calculate length of name + _functionNames[fNameIndex] = new char[nameLength + 1]; // Allocate mem + null terminator + if (_functionNames[fNameIndex] != nullptr) { + // Copy the name to the array index and null terminate it + strncpy(_functionNames[fNameIndex], &fNames[fNameStartChar], nameLength); + _functionNames[fNameIndex][nameLength] = '\0'; + } + // Set the momentary flag + if (momentary) { + _momentaryFlags |= 1 << fNameIndex; + } else { + _momentaryFlags &= ~(1 << fNameIndex); + } + // Move to the next index + fNameIndex++; } else { - _momentaryFlags &= ~(1 << fkey); + break; } - } - // Serial.print("Function "); - // Serial.print(fkey); - // Serial.print(momentary ? F(" Momentary ") : F("")); - // Serial.print(" "); - // Serial.println(fName); - // Serial.println(_functionNames[fkey]); - fkey++; - } - if (fkey < MAX_FUNCTIONS) { - for (int i = fkey; i < MAX_FUNCTIONS; i++) { - _functionNames[i] = nullptr; + fNameStartChar = charIndex + 1; // Calculate the start index of the next name } } + delete[] fNames; // Clean up fNames } bool Loco::isFunctionOn(int function) { return _functionStates & 1 << function; } @@ -129,12 +148,14 @@ void Loco::setFunctionStates(int functionStates) { _functionStates = functionSta int Loco::getFunctionStates() { return _functionStates; } -char *Loco::getFunctionName(int function) { return _functionNames[function]; } +const char *Loco::getFunctionName(int function) { return _functionNames[function]; } bool Loco::isFunctionMomentary(int function) { return _momentaryFlags & 1 << function; } Loco *Loco::getFirst() { return _first; } +void Loco::setNext(Loco *loco) { _next = loco; } + Loco *Loco::getNext() { return _next; } Loco *Loco::getByAddress(int address) { @@ -146,6 +167,73 @@ Loco *Loco::getByAddress(int address) { return nullptr; } +void Loco::clearRoster() { + // Count Locos in roster + int locoCount = 0; + Loco *currentLoco = Loco::getFirst(); + while (currentLoco != nullptr) { + locoCount++; + currentLoco = currentLoco->getNext(); + } + + // Store Loco pointers in an array for clean up + Loco **deleteLocos = new Loco *[locoCount]; + currentLoco = Loco::getFirst(); + for (int i = 0; i < locoCount; i++) { + deleteLocos[i] = currentLoco; + currentLoco = currentLoco->getNext(); + } + + // Delete each Loco + for (int i = 0; i < locoCount; i++) { + delete deleteLocos[i]; + } + + // Clean up the array of pointers + delete[] deleteLocos; + + // Reset first pointer + Loco::_first = nullptr; +} + +Loco::~Loco() { + _removeFromList(this); + + if (_name) { + delete[] _name; + _name = nullptr; + } + + for (int i = 0; i < MAX_FUNCTIONS; i++) { + if (_functionNames[i]) { + delete[] _functionNames[i]; + _functionNames[i] = nullptr; + } + } + + _next = nullptr; +} + +// Private methods + +void Loco::_removeFromList(Loco *loco) { + if (!loco) { + return; + } + + if (getFirst() == loco) { + _first = loco->getNext(); + } else { + Loco *currentLoco = _first; + while (currentLoco && currentLoco->getNext() != loco) { + currentLoco = currentLoco->getNext(); + } + if (currentLoco) { + currentLoco->setNext(loco->getNext()); + } + } +} + // class ConsistLoco // Public methods @@ -165,6 +253,14 @@ ConsistLoco *ConsistLoco::getNext() { return _next; } void ConsistLoco::setNext(ConsistLoco *consistLoco) { _next = consistLoco; } +ConsistLoco::~ConsistLoco() { + if (_loco && _loco->getSource() == LocoSource::LocoSourceEntry) { + delete _loco; + _loco = nullptr; + } + _next = nullptr; +} + // class Consist // Public methods @@ -174,16 +270,32 @@ Consist::Consist() { _first = nullptr; } -void Consist::setName(char *name) { _name = name; } +void Consist::setName(const char *name) { + if (name == nullptr) { + return; + } + if (_name) { + delete[] _name; + _name = nullptr; + } + int nameLength = strlen(name); + _name = new char[nameLength + 1]; + if (_name == nullptr) { + return; + } + strcpy(_name, name); +} -char *Consist::getName() { return _name; } +const char *Consist::getName() { return _name; } void Consist::addLoco(Loco *loco, Facing facing) { if (inConsist(loco)) return; // Already in the consist if (_locoCount == 0) { - facing = FacingForward; // Force forward facing for the first loco added - _name = loco->getName(); // Set consist name to the first loco name + facing = FacingForward; // Force forward facing for the first loco added + if (_name == nullptr) { + setName(loco->getName()); // Set consist name to the first loco name if not already set + } } ConsistLoco *conLoco = new ConsistLoco(loco, facing); _addLocoToConsist(conLoco); @@ -194,9 +306,13 @@ void Consist::addLoco(int address, Facing facing) { return; if (_locoCount == 0) { facing = FacingForward; - char temp[6]; - snprintf(temp, 6, "%d", address); - _name = temp; + if (_name == nullptr) { + int addressLength = (address == 0) ? 1 : log10(address) + 1; + char *newName = new char[addressLength + 1]; + snprintf(newName, addressLength + 1, "%d", address); + setName(newName); + delete[] newName; + } } Loco *loco = new Loco(address, LocoSourceEntry); ConsistLoco *conLoco = new ConsistLoco(loco, facing); @@ -204,25 +320,31 @@ void Consist::addLoco(int address, Facing facing) { } void Consist::removeLoco(Loco *loco) { - ConsistLoco *previous = nullptr; - ConsistLoco *current = _first; - while (current) { - if (current->getLoco() == loco) { - if (loco->getSource() == LocoSourceEntry) { - // delete loco; - } - if (previous) { - previous->setNext(current->getNext()); + // Start with no previous, and the first CL + ConsistLoco *previousCL = nullptr; + ConsistLoco *currentCL = _first; + while (currentCL) { + // If the currentCL is our loco to remove, process it + if (currentCL->getLoco() == loco) { + // If there is a previous, set its next to the current's next to skip currentCL + ConsistLoco *nextCL = currentCL->getNext(); + if (previousCL) { + previousCL->setNext(nextCL); + // Otherwise the first is now the next } else { - _first = current->getNext(); + _first = nextCL; } - // delete current; + // Delete the currentCL and decrement the count of locos + delete currentCL; _locoCount--; - break; + currentCL = nextCL; + // Otherwise move to the next one in the list + } else { + previousCL = currentCL; + currentCL = currentCL->getNext(); } - previous = current; - current = current->getNext(); } + // When we're finished, if this was the last one, clean up if (!_first) { _first = nullptr; _locoCount = 0; @@ -230,16 +352,17 @@ void Consist::removeLoco(Loco *loco) { } void Consist::removeAllLocos() { - ConsistLoco *current = _first; - while (current) { - ConsistLoco *next = current->getNext(); - Loco *loco = current->getLoco(); - if (loco->getSource() == LocoSourceEntry) { - // delete loco; - } - // delete current; - current = next; + // Clean up the linked list + ConsistLoco *currentCL = _first; + while (currentCL != nullptr) { + // Capture the next one + ConsistLoco *nextCL = currentCL->getNext(); + // Delete the current one + delete currentCL; + // Set next as current + currentCL = nextCL; } + // Set _first to nullptr _first = nullptr; _locoCount = 0; } @@ -297,6 +420,28 @@ ConsistLoco *Consist::getByAddress(int address) { return nullptr; } +Consist::~Consist() { + // Clean up the name + if (_name) { + delete[] _name; + _name = nullptr; + } + + // Clean up the linked list + ConsistLoco *currentCL = _first; + while (currentCL != nullptr) { + // Capture the next one + ConsistLoco *nextCL = currentCL->getNext(); + // Delete the current one + delete currentCL; + // Set next as current + currentCL = nextCL; + } + // Set _first to nullptr + _first = nullptr; + _locoCount = 0; +} + // Private methods void Consist::_addLocoToConsist(ConsistLoco *conLoco) { diff --git a/src/DCCEXLoco.h b/src/DCCEXLoco.h index 4105991..8f55a7d 100644 --- a/src/DCCEXLoco.h +++ b/src/DCCEXLoco.h @@ -5,6 +5,7 @@ * This package implements a DCCEX native protocol connection, * allow a device to communicate with a DCC-EX EX-CommandStation. * + * Copyright © 2024 Peter Cole * Copyright © 2023 Peter Akers * Copyright © 2023 Peter Cole * @@ -64,11 +65,11 @@ class Loco { /// @brief Set loco name /// @param name Name of the loco - void setName(char *name); + void setName(const char *name); /// @brief Get loco name /// @return Name of the loco - char *getName(); + const char *getName(); /// @brief Set loco speed /// @param speed Valid speed (0 - 126) @@ -92,7 +93,7 @@ class Loco { /// @brief Setup functions for the loco /// @param functionNames Char array of function names - void setupFunctions(char *functionNames); + void setupFunctions(const char *functionNames); /// @brief Test if function is on /// @param function Number of the function to test @@ -110,7 +111,7 @@ class Loco { /// @brief Get the name/label for a function /// @param function Number of the function to return the name/label of /// @return char* representing the function name/label - char *getFunctionName(int function); + const char *getFunctionName(int function); /// @brief Get the name/label for a function /// @param function Number of the function to return the name/label of @@ -121,6 +122,10 @@ class Loco { /// @return Pointer to the first Loco object static Loco *getFirst(); + /// @brief Set the next loco in the roster list + /// @param loco Pointer to the next Loco object + void setNext(Loco *loco); + /// @brief Get next Loco object /// @return Pointer to the next Loco object Loco *getNext(); @@ -130,6 +135,12 @@ class Loco { /// @return Loco object or nullptr if it doesn't exist static Loco *getByAddress(int address); + /// @brief Clear all Locos from the roster + static void clearRoster(); + + /// @brief Destructor for the Loco object + ~Loco(); + private: int _address; char *_name; @@ -142,6 +153,10 @@ class Loco { static Loco *_first; Loco *_next; + /// @brief Method to remove this loco from the roster list + /// @param loco Pointer to the Loco to remove + static void _removeFromList(Loco *loco); + friend class Consist; }; @@ -173,6 +188,9 @@ class ConsistLoco { /// @param consistLoco Pointer to the ConsistLoco object void setNext(ConsistLoco *consistLoco); + /// @brief Destructor for a ConsistLoco + ~ConsistLoco(); + private: Loco *_loco; Facing _facing; @@ -189,11 +207,11 @@ class Consist { /// @brief Set consist name /// @param name Name to set for the consist - void setName(char *name); + void setName(const char *name); /// @brief Get consist name /// @return Current name of the consist - char *getName(); + const char *getName(); /// @brief Add a loco to the consist using a Loco object /// @param loco Pointer to a loco object @@ -205,11 +223,11 @@ class Consist { /// @param facing Direction the loco is facing (FacingForward|FacingReversed) void addLoco(int address, Facing facing); - /// @brief Remove a loco from the consist + /// @brief Remove a loco from the consist - Loco objects with LocoSourceEntry will also be deleted /// @param loco Pointer to a loco object to remove void removeLoco(Loco *loco); - /// @brief Remove all locos from a consist + /// @brief Remove all locos from a consist - Loco objects with LocoSourceEntry will also be deleted void removeAllLocos(); /// @brief Update the direction of a loco in the consist @@ -248,6 +266,9 @@ class Consist { /// @return Pointer to the first ConsistLoco object ConsistLoco *getByAddress(int address); + /// @brief Destructor for a Consist + ~Consist(); + private: char *_name; int _locoCount; diff --git a/src/DCCEXProtocol.cpp b/src/DCCEXProtocol.cpp index 92bb798..31c9eb9 100644 --- a/src/DCCEXProtocol.cpp +++ b/src/DCCEXProtocol.cpp @@ -5,6 +5,7 @@ * This package implements a DCCEX native protocol connection, * allow a device to communicate with a DCC-EX EX-CommandStation. * + * Copyright © 2024 Peter Cole * Copyright © 2023 Peter Akers * Copyright © 2023 Peter Cole * @@ -47,7 +48,7 @@ static const int MAX_SPEED = 126; // Public methods // Protocol and server methods -DCCEXProtocol::DCCEXProtocol(int maxCmdBuffer) { +DCCEXProtocol::DCCEXProtocol(int maxCmdBuffer, int maxCommandParams) { // Init streams _stream = &_nullStream; _console = &_nullStream; @@ -57,7 +58,7 @@ DCCEXProtocol::DCCEXProtocol(int maxCmdBuffer) { _maxCmdBuffer = maxCmdBuffer; // Setup command parser - DCCEXInbound::setup(MAX_COMMAND_PARAMS); + DCCEXInbound::setup(maxCommandParams); _cmdBuffer[0] = 0; _bufflen = 0; @@ -69,7 +70,7 @@ DCCEXProtocol::DCCEXProtocol(int maxCmdBuffer) { DCCEXProtocol::~DCCEXProtocol() { // Free memory for command buffer - delete[](_cmdBuffer); + delete[] (_cmdBuffer); // Cleanup command parser DCCEXInbound::cleanup(); @@ -138,7 +139,7 @@ void DCCEXProtocol::sendCommand(char *cmd) { // sequentially request and get the required lists. To avoid overloading the buffer void DCCEXProtocol::getLists(bool rosterRequired, bool turnoutListRequired, bool routeListRequired, bool turntableListRequired) { - // console->println(F("getLists()")); + // Serial.println(F("getLists()")); if (!_receivedLists) { if (rosterRequired && !_rosterRequested) { _getRoster(); @@ -157,7 +158,7 @@ void DCCEXProtocol::getLists(bool rosterRequired, bool turnoutListRequired, bool } else { if (!turntableListRequired || _receivedTurntableList) { _receivedLists = true; - _console->println(F("Lists Fully Received")); + // Serial.println(F("Lists Fully Received")); } } } @@ -167,18 +168,18 @@ void DCCEXProtocol::getLists(bool rosterRequired, bool turnoutListRequired, bool } } } - // console->println(F("getLists(): end")); + // Serial.println(F("getLists(): end")); } bool DCCEXProtocol::receivedLists() { return _receivedLists; } void DCCEXProtocol::requestServerVersion() { - // console->println(F("requestServerVersion(): ")); + // Serial.println(F("requestServerVersion(): ")); if (_delegate) { sprintf(_outboundCommand, ""); _sendCommand(); } - // console->println(F("requestServerVersion(): end")); + // Serial.println(F("requestServerVersion(): end")); } bool DCCEXProtocol::receivedVersion() { return _receivedVersion; } @@ -191,6 +192,20 @@ int DCCEXProtocol::getPatchVersion() { return _version[2]; } unsigned long DCCEXProtocol::getLastServerResponseTime() { return _lastServerResponseTime; } +void DCCEXProtocol::clearAllLists() { + clearRoster(); + clearTurnoutList(); + clearTurntableList(); + clearRouteList(); +} + +void DCCEXProtocol::refreshAllLists() { + refreshRoster(); + refreshTurnoutList(); + refreshTurntableList(); + refreshRouteList(); +} + // Consist/loco methods void DCCEXProtocol::setThrottle(Loco *loco, int speed, Direction direction) { @@ -318,6 +333,19 @@ Loco *DCCEXProtocol::findLocoInRoster(int address) { return nullptr; } +void DCCEXProtocol::clearRoster() { + Loco::clearRoster(); + roster = nullptr; + _rosterCount = 0; +} + +void DCCEXProtocol::refreshRoster() { + clearRoster(); + _receivedLists = false; + _receivedRoster = false; + _rosterRequested = false; +} + // Turnout methods int DCCEXProtocol::getTurnoutCount() { return _turnoutCount; } @@ -359,6 +387,19 @@ void DCCEXProtocol::toggleTurnout(int turnoutId) { } } +void DCCEXProtocol::clearTurnoutList() { + Turnout::clearTurnoutList(); + turnouts = nullptr; + _turnoutCount = 0; +} + +void DCCEXProtocol::refreshTurnoutList() { + clearTurnoutList(); + _receivedLists = false; + _receivedTurnoutList = false; + _turnoutListRequested = false; +} + // Route methods int DCCEXProtocol::getRouteCount() { return _routeCount; } @@ -392,6 +433,19 @@ void DCCEXProtocol::resumeRoutes() { // console->println(F("sendResumeRoutes() end")); } +void DCCEXProtocol::clearRouteList() { + Route::clearRouteList(); + routes = nullptr; + _routeCount = 0; +} + +void DCCEXProtocol::refreshRouteList() { + clearRouteList(); + _receivedLists = false; + _receivedRouteList = false; + _routeListRequested = false; +} + // Turntable methods int DCCEXProtocol::getTurntableCount() { return _turntableCount; } @@ -426,6 +480,19 @@ void DCCEXProtocol::rotateTurntable(int turntableId, int position, int activity) // console->println(F("sendTurntable() end")); } +void DCCEXProtocol::clearTurntableList() { + Turntable::clearTurntableList(); + turntables = nullptr; + _turntableCount = 0; +} + +void DCCEXProtocol::refreshTurntableList() { + clearTurntableList(); + _receivedLists = false; + _receivedTurntableList = false; + _turntableListRequested = false; +} + // Track management methods void DCCEXProtocol::powerOn() { @@ -854,6 +921,9 @@ void DCCEXProtocol::_processRosterEntry() { //println(getRosterCount()); _delegate->receivedRosterList(); } + + free(name); + free(funcs); // console->println(F("processRosterEntry(): end")); } @@ -910,7 +980,7 @@ void DCCEXProtocol::_processTurnoutEntry() { char *name = DCCEXInbound::copyTextParameter(3); bool missingTurnouts = false; - Turnout *t = turnouts->getById(id); + Turnout *t = Turnout::getById(id); if (t) { t->setName(name); t->setThrown(thrown); @@ -920,6 +990,8 @@ void DCCEXProtocol::_processTurnoutEntry() { } } + free(name); + if (!missingTurnouts) { _receivedTurnoutList = true; // console->println(F("processTurnoutsEntry(): received all")); @@ -994,7 +1066,7 @@ void DCCEXProtocol::_processRouteEntry() { char *name = DCCEXInbound::copyTextParameter(3); bool missingRoutes = false; - Route *r = routes->getById(id); + Route *r = Route::getById(id); if (r) { r->setType(type); r->setName(name); @@ -1004,6 +1076,8 @@ void DCCEXProtocol::_processRouteEntry() { } } + free(name); + if (!missingRoutes) { _receivedRouteList = true; // console->println(F("processRoutesEntry(): received all")); @@ -1063,7 +1137,7 @@ void DCCEXProtocol::_processTurntableEntry() { // getById(id); + Turntable *tt = Turntable::getById(id); if (tt) { tt->setType(ttType); tt->setIndex(index); @@ -1074,6 +1148,8 @@ void DCCEXProtocol::_processTurntableEntry() { // getNext()->getId()); } } + + free(name); // console->println(F("processTurntableEntry(): end")); } @@ -1095,7 +1171,9 @@ void DCCEXProtocol::_processTurntableIndexEntry() { // println(F("processTurntableIndexEntry(): received all")); diff --git a/src/DCCEXProtocol.h b/src/DCCEXProtocol.h index 10d8707..4fbdbdd 100644 --- a/src/DCCEXProtocol.h +++ b/src/DCCEXProtocol.h @@ -34,6 +34,16 @@ /* Version information: +1.0.0 - First Production release + - Add methods to clear and refresh the various lists + - Various memory leak bugfixes + - Fix bug where any Loco created was added to the roster, despite LocoSourceEntry being set + - Fix bug where getById() for Turnout, Route, and Turntable was not a static method, causing runtime errors + - Removed redundant count on Turnout, Route, and Turntable as these are available from getRosterCount, + getTurnoutCount, getRouteCount, getTurntableCount + - Updated all public methods setting and getting names from char * to const char * to remove compiler warnings + - Enable configuring the max parameters parsed by DCCEXInbound via the DCCEXProtocol constructor + - Implemented many new tests 0.0.17 - Fix typo in turntable example - Fix bug where the turntable isMoving() method always returned true - Add enableHeartbeat(heartbeatDelay) to send a heartbeat every x ms if a command is not sent @@ -75,9 +85,7 @@ Version information: #include "DCCEXTurntables.h" #include -const int MAX_OUTBOUND_COMMAND_LENGTH = 100; // Max number of bytes for outbound commands -const int MAX_SERVER_DESCRIPTION_PARAM_LENGTH = 100; // Max number of bytes for server details response -const int MAX_COMMAND_PARAMS = 50; // Max number of params to parse via DCCEXInbound parser +const int MAX_OUTBOUND_COMMAND_LENGTH = 100; // Max number of bytes for outbound commands // Valid track power state values enum TrackPower { @@ -201,7 +209,8 @@ class DCCEXProtocol { /// @brief Constructor for the DCCEXProtocol object /// @param maxCmdBuffer Optional - maximum number of bytes for the command buffer (default 500) - DCCEXProtocol(int maxCmdBuffer = 500); + /// @param maxCommandParams Optional - maximum number of parameters to parse via the DCCEXInbound parser (default 50) + DCCEXProtocol(int maxCmdBuffer = 500, int maxCommandParams = 50); /// @brief Destructor for the DCCEXProtocol object ~DCCEXProtocol(); @@ -266,6 +275,12 @@ class DCCEXProtocol { /// @return Last response time in milliseconds (from millis()) unsigned long getLastServerResponseTime(); // seconds since Arduino start + /// @brief Clear roster, turnout, turntable, and route lists + void clearAllLists(); + + /// @brief Clear roster, turnout, turntable, and route lists and request new ones + void refreshAllLists(); + // Consist/Loco methods /// @brief Set the provided loco to the specified speed and direction @@ -337,6 +352,12 @@ class DCCEXProtocol { /// @return Pointer to the Loco object Loco *findLocoInRoster(int address); + /// @brief Clear the roster + void clearRoster(); + + /// @brief Clear the roster and request again + void refreshRoster(); + // Turnout methods /// @brief Get the number of turnouts @@ -364,6 +385,12 @@ class DCCEXProtocol { /// @param turnoutId ID of the turnout/point void toggleTurnout(int turnoutId); + /// @brief Clear the list of turnouts + void clearTurnoutList(); + + /// @brief Clear the list of turnouts and request again + void refreshTurnoutList(); + // Route methods /// @brief Get the number of route entries @@ -384,6 +411,12 @@ class DCCEXProtocol { /// @brief Resume all routes/automations void resumeRoutes(); + /// @brief Clear all routes + void clearRouteList(); + + /// @brief Clear all routes and request a new list + void refreshRouteList(); + // Turntable methods /// @brief Get the number of turntable entries @@ -405,6 +438,12 @@ class DCCEXProtocol { /// @param activity Optional activity for EX-Turntable objects only void rotateTurntable(int turntableId, int position, int activity = 0); + /// @brief Clear all turntables + void clearTurntableList(); + + /// @brief Clear all turntables and request a new list + void refreshTurntableList(); + // Track management methods /// @brief Global track power on command diff --git a/src/DCCEXRoutes.cpp b/src/DCCEXRoutes.cpp index 9831cf6..4a006f3 100644 --- a/src/DCCEXRoutes.cpp +++ b/src/DCCEXRoutes.cpp @@ -29,7 +29,6 @@ #include "DCCEXRoutes.h" #include - // Public methods Route *Route::_first = nullptr; @@ -47,30 +46,97 @@ Route::Route(int id) { } current->_next = this; } - _count++; } int Route::getId() { return _id; } -void Route::setName(char *name) { _name = name; } +void Route::setName(const char *name) { + if (_name) { + delete[] _name; + _name = nullptr; + } + int nameLength = strlen(name); + _name = new char[nameLength + 1]; + strcpy(_name, name); +} -char *Route::getName() { return _name; } +const char *Route::getName() { return _name; } void Route::setType(RouteType type) { _type = type; } RouteType Route::getType() { return (RouteType)_type; } -int Route::getCount() { return _count; } - Route *Route::getFirst() { return _first; } +void Route::setNext(Route *route) { _next = route; } + Route *Route::getNext() { return _next; } Route *Route::getById(int id) { - for (Route *r = getFirst(); r; r = r->getNext()) { + for (Route *r = Route::getFirst(); r; r = r->getNext()) { if (r->getId() == id) { return r; } } return nullptr; } + +void Route::clearRouteList() { + // Count Routes in list + int routeCount = 0; + Route *currentRoute = Route::getFirst(); + while (currentRoute != nullptr) { + routeCount++; + currentRoute = currentRoute->getNext(); + } + + // Store Route pointers in an array for clean up + Route **deleteRoutes = new Route *[routeCount]; + currentRoute = Route::getFirst(); + for (int i = 0; i < routeCount; i++) { + deleteRoutes[i] = currentRoute; + currentRoute = currentRoute->getNext(); + } + + // Delete each Route + for (int i = 0; i < routeCount; i++) { + delete deleteRoutes[i]; + } + + // Clean up the array of pointers + delete[] deleteRoutes; + + // Reset first pointer + Route::_first = nullptr; +} + +Route::~Route() { + _removeFromList(this); + + if (_name) { + delete[] _name; + _name = nullptr; + } + + _next = nullptr; +} + +// Private methods + +void Route::_removeFromList(Route *route) { + if (!route) { + return; + } + + if (getFirst() == route) { + _first = route->getNext(); + } else { + Route *currentRoute = _first; + while (currentRoute && currentRoute->getNext() != route) { + currentRoute = currentRoute->getNext(); + } + if (currentRoute) { + currentRoute->setNext(route->getNext()); + } + } +} diff --git a/src/DCCEXRoutes.h b/src/DCCEXRoutes.h index 8237432..e0b29b8 100644 --- a/src/DCCEXRoutes.h +++ b/src/DCCEXRoutes.h @@ -49,11 +49,11 @@ class Route { /// @brief Set route name /// @param name Name to set for the route - void setName(char *name); + void setName(const char *name); /// @brief Get route name /// @return Current name of the route - char *getName(); + const char *getName(); /// @brief Set route type (A automation, R route) /// @param type RouteType - RouteTypeAutomation|RouteTypeRoute @@ -63,21 +63,27 @@ class Route { /// @return RouteTypeAutomation|RouteTypeRoute RouteType getType(); - /// @brief Get count of routes - /// @return Count of routes - int getCount(); - /// @brief Get first Route object /// @return Pointer to the first Route object static Route *getFirst(); + /// @brief Set the next route in the list + /// @param route Pointer to the next route + void setNext(Route *route); + /// @brief Get next Route object /// @return Pointer to the next Route object Route *getNext(); /// @brief Get route object by its ID /// @return Pointer to the Route, or nullptr if not found - Route *getById(int id); + static Route *getById(int id); + + /// @brief Clear the list of routes + static void clearRouteList(); + + /// @brief Destructor for a route + ~Route(); private: int _id; @@ -85,7 +91,10 @@ class Route { char _type; static Route *_first; Route *_next; - int _count = 0; + + /// @brief Remove the route from the list + /// @param route Pointer to the route to remove + void _removeFromList(Route *route); }; #endif \ No newline at end of file diff --git a/src/DCCEXTurnouts.cpp b/src/DCCEXTurnouts.cpp index 9699665..53d2bf3 100644 --- a/src/DCCEXTurnouts.cpp +++ b/src/DCCEXTurnouts.cpp @@ -29,7 +29,6 @@ #include "DCCEXTurnouts.h" #include - Turnout *Turnout::_first = nullptr; Turnout::Turnout(int id, bool thrown) { @@ -46,30 +45,97 @@ Turnout::Turnout(int id, bool thrown) { } current->_next = this; } - _count++; } void Turnout::setThrown(bool thrown) { _thrown = thrown; } -void Turnout::setName(char *name) { _name = name; } +void Turnout::setName(const char *name) { + if (_name) { + delete[] _name; + _name = nullptr; + } + int nameLength = strlen(name); + _name = new char[nameLength + 1]; + strcpy(_name, name); +} int Turnout::getId() { return _id; } -char *Turnout::getName() { return _name; } +const char *Turnout::getName() { return _name; } bool Turnout::getThrown() { return _thrown; } Turnout *Turnout::getFirst() { return _first; } -Turnout *Turnout::getNext() { return _next; } +void Turnout::setNext(Turnout *turnout) { _next = turnout; } -int Turnout::getCount() { return _count; } +Turnout *Turnout::getNext() { return _next; } Turnout *Turnout::getById(int id) { - for (Turnout *t = getFirst(); t; t = t->getNext()) { + for (Turnout *t = Turnout::getFirst(); t; t = t->getNext()) { if (t->getId() == id) { return t; } } return nullptr; } + +void Turnout::clearTurnoutList() { + // Count Turnouts in list + int turnoutCount = 0; + Turnout *currentTurnout = Turnout::getFirst(); + while (currentTurnout != nullptr) { + turnoutCount++; + currentTurnout = currentTurnout->getNext(); + } + + // Store Turnout pointers in an array for clean up + Turnout **deleteTurnouts = new Turnout *[turnoutCount]; + currentTurnout = Turnout::getFirst(); + for (int i = 0; i < turnoutCount; i++) { + deleteTurnouts[i] = currentTurnout; + currentTurnout = currentTurnout->getNext(); + } + + // Delete each Turnout + for (int i = 0; i < turnoutCount; i++) { + delete deleteTurnouts[i]; + } + + // Clean up the array of pointers + delete[] deleteTurnouts; + + // Reset first pointer + Turnout::_first = nullptr; +} + +Turnout::~Turnout() { + _removeFromList(this); + + if (_name) { + delete[] _name; + _name = nullptr; + } + + _next = nullptr; +} + +// Private methods + +void Turnout::_removeFromList(Turnout *turnout) { + if (!turnout) { + return; + } + + if (getFirst() == turnout) { + _first = turnout->getNext(); + } else { + Turnout *currentTurnout = _first; + while (currentTurnout && currentTurnout->getNext() != turnout) { + currentTurnout = currentTurnout->getNext(); + } + if (currentTurnout) { + currentTurnout->setNext(turnout->getNext()); + } + } +} diff --git a/src/DCCEXTurnouts.h b/src/DCCEXTurnouts.h index c410181..7ef5b05 100644 --- a/src/DCCEXTurnouts.h +++ b/src/DCCEXTurnouts.h @@ -45,7 +45,7 @@ class Turnout { /// @brief Set turnout name /// @param _name Name to set the turnout - void setName(char *_name); + void setName(const char *_name); /// @brief Get turnout Id /// @return ID of the turnout @@ -53,7 +53,7 @@ class Turnout { /// @brief Get turnout name /// @return Current name of the turnout - char *getName(); + const char *getName(); /// @brief Get thrown state (true thrown, false closed) /// @return true|false @@ -63,18 +63,24 @@ class Turnout { /// @return Pointer to the first Turnout object static Turnout *getFirst(); + /// @brief Set the next turnout in the list + /// @param turnout Pointer to the next Turnout + void setNext(Turnout *turnout); + /// @brief Get next turnout object /// @return Pointer to the next Turnout object Turnout *getNext(); - /// @brief Get the number of turnouts - /// @return Count of turnouts - int getCount(); - /// @brief Get turnout object by turnout ID /// @param id ID of the turnout to retrieve /// @return Pointer to the turnout object or nullptr if not found - Turnout *getById(int id); + static Turnout *getById(int id); + + /// @brief Clear the list of turnouts + static void clearTurnoutList(); + + /// @brief Destructor for a Turnout + ~Turnout(); private: static Turnout *_first; @@ -82,7 +88,10 @@ class Turnout { int _id; char *_name; bool _thrown; - int _count = 0; + + /// @brief Remove the turnout from the list + /// @param turnout Pointer to the turnout to remove + void _removeFromList(Turnout *turnout); }; #endif \ No newline at end of file diff --git a/src/DCCEXTurntables.cpp b/src/DCCEXTurntables.cpp index 5f8502b..67bb092 100644 --- a/src/DCCEXTurntables.cpp +++ b/src/DCCEXTurntables.cpp @@ -31,13 +31,17 @@ // class TurntableIndex -TurntableIndex *TurntableIndex::_first = nullptr; - -TurntableIndex::TurntableIndex(int ttId, int id, int angle, char *name) { +TurntableIndex::TurntableIndex(int ttId, int id, int angle, const char *name) { _ttId = ttId; _id = id; _angle = angle; - _name = name; + if (name) { + int nameLength = strlen(name); + _name = new char[nameLength + 1]; + strcpy(_name, name); + } else { + _name = nullptr; + } _nextIndex = nullptr; } @@ -47,10 +51,18 @@ int TurntableIndex::getId() { return _id; } int TurntableIndex::getAngle() { return _angle; } -char *TurntableIndex::getName() { return _name; } +const char *TurntableIndex::getName() { return _name; } TurntableIndex *TurntableIndex::getNextIndex() { return _nextIndex; } +TurntableIndex::~TurntableIndex() { + if (_name) { + delete[] _name; + _name = nullptr; + } + _nextIndex = nullptr; +} + // class Turntable Turntable *Turntable::_first = nullptr; @@ -74,7 +86,6 @@ Turntable::Turntable(int id) { } current->_next = this; } - _count++; } int Turntable::getId() { return _id; } @@ -91,20 +102,28 @@ void Turntable::setNumberOfIndexes(int numberOfIndexes) { _numberOfIndexes = num int Turntable::getNumberOfIndexes() { return _numberOfIndexes; } -void Turntable::setName(char *name) { _name = name; } +void Turntable::setName(const char *name) { + if (_name) { + delete[] _name; + _name = nullptr; + } + int nameLength = strlen(name); + _name = new char[nameLength + 1]; + strcpy(_name, name); +} -char *Turntable::getName() { return _name; } +const char *Turntable::getName() { return _name; } void Turntable::setMoving(bool moving) { _isMoving = moving; } bool Turntable::isMoving() { return _isMoving; } -int Turntable::getCount() { return _count; } - int Turntable::getIndexCount() { return _indexCount; } Turntable *Turntable::getFirst() { return _first; } +void Turntable::setNext(Turntable *turntable) { _next = turntable; } + Turntable *Turntable::getNext() { return _next; } void Turntable::addIndex(TurntableIndex *index) { @@ -123,7 +142,7 @@ void Turntable::addIndex(TurntableIndex *index) { TurntableIndex *Turntable::getFirstIndex() { return _firstIndex; } Turntable *Turntable::getById(int id) { - for (Turntable *tt = getFirst(); tt; tt = tt->getNext()) { + for (Turntable *tt = Turntable::getFirst(); tt; tt = tt->getNext()) { if (tt->getId() == id) { return tt; } @@ -139,3 +158,76 @@ TurntableIndex *Turntable::getIndexById(int id) { } return nullptr; } + +void Turntable::clearTurntableList() { + // Count Turntables in list + int turntableCount = 0; + Turntable *currentTurntable = Turntable::getFirst(); + while (currentTurntable != nullptr) { + turntableCount++; + currentTurntable = currentTurntable->getNext(); + } + + // Store Turntable pointers in an array for clean up + Turntable **deleteTurntables = new Turntable *[turntableCount]; + currentTurntable = Turntable::getFirst(); + for (int i = 0; i < turntableCount; i++) { + deleteTurntables[i] = currentTurntable; + currentTurntable = currentTurntable->getNext(); + } + + // Delete each Turntable + for (int i = 0; i < turntableCount; i++) { + delete deleteTurntables[i]; + } + + // Clean up the array of pointers + delete[] deleteTurntables; + + // Reset first pointer + Turntable::_first = nullptr; +} + +Turntable::~Turntable() { + _removeFromList(this); + + if (_name) { + delete[] _name; + _name = nullptr; + } + + if (_firstIndex) { + // Clean up index list + TurntableIndex *currentIndex = _firstIndex; + while (currentIndex != nullptr) { + // Capture next index + TurntableIndex *nextIndex = currentIndex->getNextIndex(); + // Delete current index + delete currentIndex; + // Move to the next + currentIndex = nextIndex; + } + // Set first to nullptr + _firstIndex = nullptr; + } + + _next = nullptr; +} + +void Turntable::_removeFromList(Turntable *turntable) { + if (!turntable) { + return; + } + + if (getFirst() == turntable) { + _first = turntable->getNext(); + } else { + Turntable *currentTurntable = _first; + while (currentTurntable && currentTurntable->getNext() != turntable) { + currentTurntable = currentTurntable->getNext(); + } + if (currentTurntable) { + currentTurntable->setNext(turntable->getNext()); + } + } +} diff --git a/src/DCCEXTurntables.h b/src/DCCEXTurntables.h index 25a8268..d5377b0 100644 --- a/src/DCCEXTurntables.h +++ b/src/DCCEXTurntables.h @@ -45,7 +45,7 @@ class TurntableIndex { /// @param id ID of the index /// @param angle Angle from home for this index (0 - 3600) /// @param name Name of the index - TurntableIndex(int ttId, int id, int angle, char *name); + TurntableIndex(int ttId, int id, int angle, const char *name); /// @brief Get the turntable ID /// @return ID of the turntable this index is associated with @@ -61,18 +61,20 @@ class TurntableIndex { /// @brief Get index name /// @return Current name of the index - char *getName(); + const char *getName(); /// @brief Get next TurntableIndex object /// @return Pointer to the next TurntableIndex object TurntableIndex *getNextIndex(); + /// @brief Destructor for an index + ~TurntableIndex(); + private: int _ttId; int _id; int _angle; char *_name; - static TurntableIndex *_first; TurntableIndex *_nextIndex; friend class Turntable; @@ -115,11 +117,11 @@ class Turntable { /// @brief Set the turntable name /// @param name Name to set for the turntable - void setName(char *name); + void setName(const char *name); /// @brief Get the turntable name /// @return Current name of the turntable - char *getName(); + const char *getName(); /// @brief Set the movement state (false stationary, true moving) /// @param moving true|false @@ -129,10 +131,6 @@ class Turntable { /// @return true|false bool isMoving(); - /// @brief Get the number of turntables - /// @return Count of turntables - int getCount(); - /// @brief Get the count of indexes added to the index list (counted from the \ command response) /// @return Count of indexes received for this turntable including home int getIndexCount(); @@ -141,6 +139,10 @@ class Turntable { /// @return Pointer to the first Turntable object static Turntable *getFirst(); + /// @brief Set the next turntable in the list + /// @param turntable Pointer to the next turntable + void setNext(Turntable *turntable); + /// @brief Get the next turntable object /// @return Pointer to the next Turntable object Turntable *getNext(); @@ -156,13 +158,19 @@ class Turntable { /// @brief Get a turntable object by its ID /// @param id ID of the turntable to retrieve /// @return Pointer to the Turntable object or nullptr if not found - Turntable *getById(int id); + static Turntable *getById(int id); /// @brief Get TurntableIndex object by its ID /// @param id ID of the index to retrieve /// @return Pointer to the TurntableIndex object or nullptr if not found TurntableIndex *getIndexById(int id); + /// @brief Clear the list of turntables + static void clearTurntableList(); + + /// @brief Destructor for a turntable + ~Turntable(); + private: int _id; TurntableType _type; @@ -170,11 +178,14 @@ class Turntable { int _numberOfIndexes; char *_name; bool _isMoving; - int _count = 0; int _indexCount; static Turntable *_first; Turntable *_next; TurntableIndex *_firstIndex; + + /// @brief Remove the turntable from the list + /// @param turntable Pointer to the turntable to remove + void _removeFromList(Turntable *turntable); }; #endif \ No newline at end of file diff --git a/tests/DCCEXProtocol.cpp b/tests/DCCEXProtocol.cpp deleted file mode 100644 index 9f095d9..0000000 --- a/tests/DCCEXProtocol.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "DCCEXProtocolTest.hpp" - -TEST_F(DCCEXProtocolTest, clearBufferWhenFull) { - // Fill buffer with garbage - for (auto i{0uz}; i < 500uz; ++i) - _stream.write(static_cast('A' + (random() % 26))); - - // Proceed with normal message - _stream << R"()"; - EXPECT_CALL(_delegate, receivedMessage(StrEq("Hello World"))).Times(Exactly(1)); - _dccexProtocol.check(); -} diff --git a/tests/DCCEXProtocolTest.cpp b/tests/DCCEXProtocolTest.cpp deleted file mode 100644 index ce1983f..0000000 --- a/tests/DCCEXProtocolTest.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "DCCEXProtocolTest.hpp" - -namespace { - -std::string stream2string(StreamMock stream) { - std::string retval; - while (stream.available()) - retval.push_back(static_cast(stream.read())); - return retval; -} - -} // namespace - -bool operator==(StreamMock lhs, StreamMock rhs) { return stream2string(lhs) == stream2string(rhs); } - -bool operator==(StreamMock lhs, std::string rhs) { return stream2string(lhs) == rhs; } - -// Set delegate and stream -DCCEXProtocolTest::DCCEXProtocolTest() { - _dccexProtocol.setDelegate(&_delegate); - _dccexProtocol.setLogStream(&_console); - _dccexProtocol.connect(&_stream); -} - -DCCEXProtocolTest::~DCCEXProtocolTest() {} \ No newline at end of file diff --git a/tests/DCCEXProtocolTest.hpp b/tests/DCCEXProtocolTest.hpp deleted file mode 100644 index a9ff127..0000000 --- a/tests/DCCEXProtocolTest.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "DCCEXProtocolDelegateMock.hpp" -#include -#include -#include - -using namespace testing; - -// Make StreamMock comparable -bool operator==(StreamMock lhs, StreamMock rhs); -bool operator==(StreamMock lhs, std::string rhs); - -// DCCEXProtocol test fixture -class DCCEXProtocolTest : public Test { -public: - DCCEXProtocolTest(); - virtual ~DCCEXProtocolTest(); - -protected: - DCCEXProtocol _dccexProtocol; - DCCEXProtocolDelegateMock _delegate; - StreamMock _console; - StreamMock _stream; -}; \ No newline at end of file diff --git a/tests/Message.cpp b/tests/Message.cpp deleted file mode 100644 index 59f04d4..0000000 --- a/tests/Message.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "DCCEXProtocolTest.hpp" - -TEST_F(DCCEXProtocolTest, broadcastHelloWorld) { - _stream << R"()"; - EXPECT_CALL(_delegate, receivedMessage(StrEq("Hello World"))).Times(Exactly(1)); - _dccexProtocol.check(); -} diff --git a/tests/TrackPower.cpp b/tests/TrackPower.cpp deleted file mode 100644 index dc0b229..0000000 --- a/tests/TrackPower.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "DCCEXProtocolTest.hpp" - -TEST_F(DCCEXProtocolTest, allTracksOff) { - _stream << ""; - EXPECT_CALL(_delegate, receivedTrackPower(TrackPower::PowerOff)).Times(Exactly(1)); - _dccexProtocol.check(); -} - -TEST_F(DCCEXProtocolTest, allTracksOn) { - _stream << ""; - EXPECT_CALL(_delegate, receivedTrackPower(TrackPower::PowerOn)).Times(Exactly(1)); - _dccexProtocol.check(); -} diff --git a/tests/general/test_DCCEXProtocol.cpp b/tests/general/test_DCCEXProtocol.cpp new file mode 100644 index 0000000..34f2c6d --- /dev/null +++ b/tests/general/test_DCCEXProtocol.cpp @@ -0,0 +1,40 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/DCCEXProtocolTests.h" + +TEST_F(DCCEXProtocolTests, clearBufferWhenFull) { + // Fill buffer with garbage + for (auto i{0uz}; i < 500uz; ++i) + _stream.write(static_cast('A' + (random() % 26))); + + // Proceed with normal message + _stream << R"()"; + EXPECT_CALL(_delegate, receivedMessage(StrEq("Hello World"))).Times(Exactly(1)); + _dccexProtocol.check(); +} diff --git a/tests/general/test_Message.cpp b/tests/general/test_Message.cpp new file mode 100644 index 0000000..d10f5f5 --- /dev/null +++ b/tests/general/test_Message.cpp @@ -0,0 +1,35 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/DCCEXProtocolTests.h" + +TEST_F(DCCEXProtocolTests, broadcastHelloWorld) { + _stream << R"()"; + EXPECT_CALL(_delegate, receivedMessage(StrEq("Hello World"))).Times(Exactly(1)); + _dccexProtocol.check(); +} diff --git a/tests/general/test_TrackPower.cpp b/tests/general/test_TrackPower.cpp new file mode 100644 index 0000000..e4b9664 --- /dev/null +++ b/tests/general/test_TrackPower.cpp @@ -0,0 +1,41 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/DCCEXProtocolTests.h" + +TEST_F(DCCEXProtocolTests, allTracksOff) { + _stream << ""; + EXPECT_CALL(_delegate, receivedTrackPower(TrackPower::PowerOff)).Times(Exactly(1)); + _dccexProtocol.check(); +} + +TEST_F(DCCEXProtocolTests, allTracksOn) { + _stream << ""; + EXPECT_CALL(_delegate, receivedTrackPower(TrackPower::PowerOn)).Times(Exactly(1)); + _dccexProtocol.check(); +} diff --git a/tests/Version.cpp b/tests/general/test_Version.cpp similarity index 59% rename from tests/Version.cpp rename to tests/general/test_Version.cpp index 95aabe8..efb1ded 100644 --- a/tests/Version.cpp +++ b/tests/general/test_Version.cpp @@ -1,11 +1,39 @@ -#include "DCCEXProtocolTest.hpp" +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ -TEST_F(DCCEXProtocolTest, request) { +#include "../setup/DCCEXProtocolTests.h" + +TEST_F(DCCEXProtocolTests, request) { _dccexProtocol.requestServerVersion(); EXPECT_EQ(_stream, "\r\n"); } -TEST_F(DCCEXProtocolTest, versionJustZeros) { +TEST_F(DCCEXProtocolTests, versionJustZeros) { EXPECT_FALSE(_dccexProtocol.receivedVersion()); _stream << ""; EXPECT_CALL(_delegate, receivedServerVersion(0, 0, 0)).Times(Exactly(1)); @@ -16,7 +44,7 @@ TEST_F(DCCEXProtocolTest, versionJustZeros) { EXPECT_EQ(_dccexProtocol.getPatchVersion(), 0); } -TEST_F(DCCEXProtocolTest, versionSingleDigits) { +TEST_F(DCCEXProtocolTests, versionSingleDigits) { EXPECT_FALSE(_dccexProtocol.receivedVersion()); _stream << ""; EXPECT_CALL(_delegate, receivedServerVersion(1, 2, 3)).Times(Exactly(1)); @@ -27,7 +55,7 @@ TEST_F(DCCEXProtocolTest, versionSingleDigits) { EXPECT_EQ(_dccexProtocol.getPatchVersion(), 3); } -TEST_F(DCCEXProtocolTest, versionMultipleDigits) { +TEST_F(DCCEXProtocolTests, versionMultipleDigits) { EXPECT_FALSE(_dccexProtocol.receivedVersion()); _stream << ""; EXPECT_CALL(_delegate, receivedServerVersion(92, 210, 10)).Times(Exactly(1)); @@ -38,7 +66,7 @@ TEST_F(DCCEXProtocolTest, versionMultipleDigits) { EXPECT_EQ(_dccexProtocol.getPatchVersion(), 10); } -TEST_F(DCCEXProtocolTest, versionIgnoreLabels) { +TEST_F(DCCEXProtocolTests, versionIgnoreLabels) { EXPECT_FALSE(_dccexProtocol.receivedVersion()); _stream << ""; EXPECT_CALL(_delegate, receivedServerVersion(1, 2, 3)).Times(Exactly(1)); diff --git a/tests/loco/test_Consist.cpp b/tests/loco/test_Consist.cpp new file mode 100644 index 0000000..17e1f67 --- /dev/null +++ b/tests/loco/test_Consist.cpp @@ -0,0 +1,165 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/LocoTests.h" + +/// @brief Create a consist with three Loco objects +TEST_F(LocoTests, createConsistByLoco) { + // Create three locos for the consist + char *functionList = "Lights/*Horn"; + Loco *loco10 = new Loco(10, LocoSourceRoster); + loco10->setName("Loco 10"); + loco10->setupFunctions(functionList); + Loco *loco2 = new Loco(2, LocoSourceRoster); + loco2->setName("Loco 2"); + loco2->setupFunctions(functionList); + Loco *loco10000 = new Loco(10000, LocoSourceRoster); + loco10000->setName("Loco 10000"); + loco10000->setupFunctions(functionList); + + // Add locos to the consist, with 2 reversed + Consist *consist = new Consist(); + consist->setName("Test Legacy Consist"); + consist->addLoco(loco10, Facing::FacingForward); + consist->addLoco(loco2, Facing::FacingReversed); + consist->addLoco(loco10000, Facing::FacingForward); + + // Validate consist makeup by object and address + EXPECT_STREQ(consist->getName(), "Test Legacy Consist"); + EXPECT_EQ(consist->getLocoCount(), 3); + EXPECT_TRUE(consist->inConsist(loco10)); + EXPECT_TRUE(consist->inConsist(loco2)); + EXPECT_TRUE(consist->inConsist(loco10000)); + EXPECT_TRUE(consist->inConsist(10)); + EXPECT_TRUE(consist->inConsist(2)); + EXPECT_TRUE(consist->inConsist(10000)); + + // Validate the first loco is 10 + EXPECT_EQ(consist->getFirst()->getLoco(), loco10); + + // Validate the consist speed and direction comes from the first loco + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Forward); + loco2->setSpeed(35); + loco10000->setDirection(Direction::Reverse); + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Forward); + loco10->setSpeed(21); + loco10->setDirection(Direction::Reverse); + EXPECT_EQ(consist->getSpeed(), 21); + EXPECT_EQ(consist->getDirection(), Direction::Reverse); + + // Validate removal of middle loco is as expected + consist->removeLoco(loco2); + EXPECT_EQ(consist->getLocoCount(), 2); + EXPECT_EQ(consist->getFirst()->getLoco(), loco10); + EXPECT_EQ(consist->getSpeed(), 21); + EXPECT_EQ(consist->getDirection(), Direction::Reverse); + + // Validate removal of first loco is as expected + consist->removeLoco(loco10); + EXPECT_EQ(consist->getLocoCount(), 1); + EXPECT_EQ(consist->getFirst()->getLoco(), loco10000); + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Reverse); + + // Validate removal of all locos + consist->removeAllLocos(); + EXPECT_EQ(consist->getLocoCount(), 0); + EXPECT_EQ(consist->getFirst(), nullptr); + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Forward); + + // Clean up + delete consist; +} + +/// @brief Create a consist with three Locos by address +TEST_F(LocoTests, createConsistByAddress) { + // Add locos to the consist, with 2 reversed + Consist *consist = new Consist(); + consist->addLoco(10, Facing::FacingForward); + consist->addLoco(2, Facing::FacingReversed); + consist->addLoco(10000, Facing::FacingForward); + + // Validate consist makeup by object and address + EXPECT_STREQ(consist->getName(), "10"); // name should be address of first loco + EXPECT_EQ(consist->getLocoCount(), 3); + EXPECT_TRUE(consist->inConsist(10)); + EXPECT_TRUE(consist->inConsist(2)); + EXPECT_TRUE(consist->inConsist(10000)); + + // Get loco objects for the next tests + Loco *loco10 = consist->getByAddress(10)->getLoco(); + ASSERT_NE(loco10, nullptr); + ASSERT_EQ(loco10->getAddress(), 10); + Loco *loco2 = consist->getByAddress(2)->getLoco(); + ASSERT_NE(loco2, nullptr); + ASSERT_EQ(loco2->getAddress(), 2); + Loco *loco10000 = consist->getByAddress(10000)->getLoco(); + ASSERT_NE(loco10000, nullptr); + ASSERT_EQ(loco10000->getAddress(), 10000); + + // Validate the first loco address is 10 + EXPECT_EQ(consist->getFirst()->getLoco()->getAddress(), 10); + + // Validate the consist speed and direction comes from the first loco + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Forward); + loco2->setSpeed(35); + loco10000->setDirection(Direction::Reverse); + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Forward); + loco10->setSpeed(21); + loco10->setDirection(Direction::Reverse); + EXPECT_EQ(consist->getSpeed(), 21); + EXPECT_EQ(consist->getDirection(), Direction::Reverse); + + // Validate removal of middle loco is as expected + consist->removeLoco(loco2); + EXPECT_EQ(consist->getLocoCount(), 2); + EXPECT_EQ(consist->getFirst()->getLoco(), loco10); + EXPECT_EQ(consist->getSpeed(), 21); + EXPECT_EQ(consist->getDirection(), Direction::Reverse); + + // Validate removal of first loco is as expected + consist->removeLoco(loco10); + EXPECT_EQ(consist->getLocoCount(), 1); + EXPECT_EQ(consist->getFirst()->getLoco(), loco10000); + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Reverse); + + // Validate removal of all locos + consist->removeAllLocos(); + EXPECT_EQ(consist->getLocoCount(), 0); + EXPECT_EQ(consist->getFirst(), nullptr); + EXPECT_EQ(consist->getSpeed(), 0); + EXPECT_EQ(consist->getDirection(), Direction::Forward); + + // Clean up the consist + delete consist; +} diff --git a/tests/loco/test_Loco.cpp b/tests/loco/test_Loco.cpp new file mode 100644 index 0000000..f1c8714 --- /dev/null +++ b/tests/loco/test_Loco.cpp @@ -0,0 +1,109 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/LocoTests.h" + +/// @brief Create a single Loco using the legacy constructor +TEST_F(LocoTests, createSingleLoco) { + // Create an individual loco + Loco *loco1 = new Loco(1, LocoSource::LocoSourceEntry); + loco1->setName("Loco 1"); + loco1->setupFunctions("Lights/*Horn/Bell///Function 5"); + + // Check address is 1, name is correct, and LocoSource correct + EXPECT_EQ(loco1->getAddress(), 1); + EXPECT_STREQ(loco1->getName(), "Loco 1"); + EXPECT_EQ(loco1->getSource(), LocoSource::LocoSourceEntry); + + // Check our functions + EXPECT_FALSE(loco1->isFunctionMomentary(0)); + EXPECT_TRUE(loco1->isFunctionMomentary(1)); + EXPECT_STREQ(loco1->getFunctionName(2), "Bell"); + EXPECT_STREQ(loco1->getFunctionName(5), "Function 5"); + + // Check speed/direction changes + EXPECT_EQ(loco1->getSpeed(), 0); + EXPECT_EQ(loco1->getDirection(), Direction::Forward); + loco1->setSpeed(13); + loco1->setDirection(Direction::Reverse); + EXPECT_EQ(loco1->getSpeed(), 13); + EXPECT_EQ(loco1->getDirection(), Direction::Reverse); + + // Make sure this is not in the roster + EXPECT_EQ(Loco::getFirst(), nullptr); + + // Make sure we can't find it by address either + EXPECT_EQ(Loco::getByAddress(1), nullptr); + + // Ensure next is nullptr as this is the only loco + EXPECT_EQ(loco1->getNext(), nullptr); + + // Clean up + delete loco1; +} + +/// @brief Create a roster of Locos using the legacy constructor +TEST_F(LocoTests, createRoster) { + // Roster should start empty, don't continue if it isn't + ASSERT_EQ(_dccexProtocol.roster->getFirst(), nullptr); + + // Add three locos + Loco *loco42 = new Loco(42, LocoSource::LocoSourceRoster); + loco42->setName("Loco42"); + Loco *loco9 = new Loco(9, LocoSource::LocoSourceRoster); + loco9->setName("Loco9"); + Loco *loco120 = new Loco(120, LocoSource::LocoSourceRoster); + loco120->setName("Loco120"); + + // Now verify the roster, fatal error if first is nullptr + Loco *firstLoco = _dccexProtocol.roster->getFirst(); + ASSERT_NE(firstLoco, nullptr); + + // Check first loco details + EXPECT_EQ(firstLoco->getAddress(), 42); + EXPECT_STREQ(firstLoco->getName(), "Loco42"); + EXPECT_EQ(firstLoco->getSource(), LocoSource::LocoSourceRoster); + + // Verify second loco details and fail fatally if next is nullptr + Loco *secondLoco = firstLoco->getNext(); + ASSERT_NE(secondLoco, nullptr); + EXPECT_EQ(secondLoco->getAddress(), 9); + EXPECT_STREQ(secondLoco->getName(), "Loco9"); + EXPECT_EQ(secondLoco->getSource(), LocoSource::LocoSourceRoster); + + // Verify third loco details and fail fatally if next is nullptr + Loco *thirdLoco = secondLoco->getNext(); + ASSERT_NE(thirdLoco, nullptr); + EXPECT_EQ(thirdLoco->getAddress(), 120); + EXPECT_STREQ(thirdLoco->getName(), "Loco120"); + EXPECT_EQ(thirdLoco->getSource(), LocoSource::LocoSourceRoster); + + // Verify end of linked list and fail fatally if next is not nullptr + EXPECT_EQ(thirdLoco->getNext(), nullptr) + << "Unexpected fourth Loco at address: " << thirdLoco->getNext()->getAddress(); +} diff --git a/tests/Roster.cpp b/tests/loco/test_RosterParsing.cpp similarity index 50% rename from tests/Roster.cpp rename to tests/loco/test_RosterParsing.cpp index 4ba2c58..578a021 100644 --- a/tests/Roster.cpp +++ b/tests/loco/test_RosterParsing.cpp @@ -1,6 +1,34 @@ -#include "DCCEXProtocolTest.hpp" +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ -TEST_F(DCCEXProtocolTest, getEmptyRoster) { +#include "../setup/LocoTests.h" + +TEST_F(LocoTests, parseEmptyRoster) { EXPECT_FALSE(_dccexProtocol.receivedRoster()); _dccexProtocol.getLists(true, false, false, false); EXPECT_EQ(_stream, "\r\n"); @@ -14,7 +42,7 @@ TEST_F(DCCEXProtocolTest, getEmptyRoster) { EXPECT_TRUE(_dccexProtocol.receivedRoster()); } -TEST_F(DCCEXProtocolTest, getRosterWithThreeIDs) { +TEST_F(LocoTests, parseRosterWithThreeIDs) { EXPECT_FALSE(_dccexProtocol.receivedRoster()); _dccexProtocol.getLists(true, false, false, false); EXPECT_EQ(_stream, "\r\n"); diff --git a/tests/route/test_RouteParsing.cpp b/tests/route/test_RouteParsing.cpp new file mode 100644 index 0000000..f526eff --- /dev/null +++ b/tests/route/test_RouteParsing.cpp @@ -0,0 +1,75 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/RouteTests.h" + +TEST_F(RouteTests, parseEmptyRouteList) { + // Received flag should be false to start + EXPECT_FALSE(_dccexProtocol.receivedRouteList()); + _dccexProtocol.getLists(false, false, true, false); + EXPECT_EQ(_stream, "\r\n"); + _stream = {}; + + // Empty route list response + _stream << ""; + _dccexProtocol.check(); + + // Should be true given route list is empty + EXPECT_TRUE(_dccexProtocol.receivedRouteList()); +} + +TEST_F(RouteTests, parseThreeRoutes) { + // Received flag should be false to start + EXPECT_FALSE(_dccexProtocol.receivedRouteList()); + _dccexProtocol.getLists(false, false, true, false); + EXPECT_EQ(_stream, "\r\n"); + _stream = {}; + + // Three route response + _stream << ""; + _dccexProtocol.check(); + + // Flag should still be false while awaiting details + EXPECT_FALSE(_dccexProtocol.receivedRouteList()); + + // First route response - Route with description + _stream << R"()"; + _dccexProtocol.check(); + + // Second route response - Automation with description + _stream << R"()"; + _dccexProtocol.check(); + + // Third route response - Route with no description + _stream << R"()"; + // Delegate should call one here + EXPECT_CALL(_delegate, receivedRouteList()).Times(Exactly(1)); + _dccexProtocol.check(); + + // Flag should now be true when all routes received + EXPECT_TRUE(_dccexProtocol.receivedRouteList()); +} diff --git a/tests/route/test_Routes.cpp b/tests/route/test_Routes.cpp new file mode 100644 index 0000000..0315be3 --- /dev/null +++ b/tests/route/test_Routes.cpp @@ -0,0 +1,77 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/RouteTests.h" + +TEST_F(RouteTests, createSingleRoute) { + // Create a route 200 as a route type + Route *route200 = new Route(200); + route200->setName("Route 200"); + route200->setType(RouteType::RouteTypeRoute); + + // Validate details are correct + EXPECT_EQ(route200->getId(), 200); + EXPECT_STREQ(route200->getName(), "Route 200"); + EXPECT_EQ(route200->getType(), RouteType::RouteTypeRoute); + + // Validate it is the first in the list with no next + EXPECT_EQ(Route::getFirst(), route200); + EXPECT_EQ(route200->getNext(), nullptr); +} + +TEST_F(RouteTests, createThreeRoutes) { + // Create three routes, route, automation, and route with no name + Route *route200 = new Route(200); + route200->setName("Route 200"); + route200->setType(RouteType::RouteTypeRoute); + Route *route300 = new Route(300); + route300->setName("Automation 300"); + route300->setType(RouteType::RouteTypeAutomation); + Route *route400 = new Route(400); + route400->setName(""); + route400->setType(RouteType::RouteTypeRoute); + + // Validate routes are in the route list + EXPECT_EQ(_dccexProtocol.routes->getById(200), route200); + EXPECT_EQ(_dccexProtocol.routes->getById(300), route300); + EXPECT_EQ(_dccexProtocol.routes->getById(400), route400); + + // Validate route details + EXPECT_EQ(route200->getId(), 200); + EXPECT_STREQ(route200->getName(), "Route 200"); + EXPECT_EQ(route200->getType(), RouteType::RouteTypeRoute); + + // Validate route details + EXPECT_EQ(route300->getId(), 300); + EXPECT_STREQ(route300->getName(), "Automation 300"); + EXPECT_EQ(route300->getType(), RouteType::RouteTypeAutomation); + + // Validate route details + EXPECT_EQ(route400->getId(), 400); + EXPECT_STREQ(route400->getName(), ""); + EXPECT_EQ(route400->getType(), RouteType::RouteTypeRoute); +} \ No newline at end of file diff --git a/tests/DCCEXProtocolDelegateMock.hpp b/tests/setup/DCCEXProtocolDelegateMock.hpp similarity index 100% rename from tests/DCCEXProtocolDelegateMock.hpp rename to tests/setup/DCCEXProtocolDelegateMock.hpp diff --git a/tests/setup/DCCEXProtocolTests.h b/tests/setup/DCCEXProtocolTests.h new file mode 100644 index 0000000..fb6c982 --- /dev/null +++ b/tests/setup/DCCEXProtocolTests.h @@ -0,0 +1,36 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef DCCEXPROTOCOLTESTS_H +#define DCCEXPROTOCOLTESTS_H + +#include "TestHarnessBase.hpp" + +/// @brief Test harness for DCCEX protocol parsing tests +class DCCEXProtocolTests : public TestHarnessBase {}; + +#endif // DCCEXPROTOCOLTESTS_H diff --git a/tests/setup/LocoTests.h b/tests/setup/LocoTests.h new file mode 100644 index 0000000..12512b8 --- /dev/null +++ b/tests/setup/LocoTests.h @@ -0,0 +1,36 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef LOCOTESTS_H +#define LOCOTESTS_H + +#include "TestHarnessBase.hpp" + +/// @brief Test harness for Loco and associated classes +class LocoTests : public TestHarnessBase {}; + +#endif // LOCOTESTS_H \ No newline at end of file diff --git a/tests/setup/MockSetup.cpp b/tests/setup/MockSetup.cpp new file mode 100644 index 0000000..1d29371 --- /dev/null +++ b/tests/setup/MockSetup.cpp @@ -0,0 +1,44 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "MockSetup.h" + +namespace { + +std::string stream2string(StreamMock stream) { + std::string retval; + while (stream.available()) + retval.push_back(static_cast(stream.read())); + return retval; +} + +} // namespace + +bool operator==(StreamMock lhs, StreamMock rhs) { return stream2string(lhs) == stream2string(rhs); } + +bool operator==(StreamMock lhs, std::string rhs) { return stream2string(lhs) == rhs; } diff --git a/tests/setup/MockSetup.h b/tests/setup/MockSetup.h new file mode 100644 index 0000000..68a6e80 --- /dev/null +++ b/tests/setup/MockSetup.h @@ -0,0 +1,39 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef MOCKSETUP_H +#define MOCKSETUP_H + +#include +#include + +// Make StreamMock comparable +bool operator==(StreamMock lhs, StreamMock rhs); +bool operator==(StreamMock lhs, std::string rhs); + +#endif // MOCKSETUP_H diff --git a/tests/setup/RouteTests.h b/tests/setup/RouteTests.h new file mode 100644 index 0000000..cb7f75e --- /dev/null +++ b/tests/setup/RouteTests.h @@ -0,0 +1,36 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef ROUTETESTS_H +#define ROUTETESTS_H + +#include "TestHarnessBase.hpp" + +/// @brief Test harness for the Route class +class RouteTests : public TestHarnessBase {}; + +#endif // ROUTETESTS_H \ No newline at end of file diff --git a/tests/setup/TestHarnessBase.cpp b/tests/setup/TestHarnessBase.cpp new file mode 100644 index 0000000..650973c --- /dev/null +++ b/tests/setup/TestHarnessBase.cpp @@ -0,0 +1,44 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "TestHarnessBase.hpp" + +TestHarnessBase::TestHarnessBase() {} + +TestHarnessBase::~TestHarnessBase() {} + +void TestHarnessBase::SetUp() { + _dccexProtocol.setDelegate(&_delegate); + _dccexProtocol.setLogStream(&_console); + _dccexProtocol.connect(&_stream); + _dccexProtocol.clearRoster(); +} + +void TestHarnessBase::TearDown() { + _dccexProtocol.clearAllLists(); +} diff --git a/tests/setup/TestHarnessBase.hpp b/tests/setup/TestHarnessBase.hpp new file mode 100644 index 0000000..e28e881 --- /dev/null +++ b/tests/setup/TestHarnessBase.hpp @@ -0,0 +1,54 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Vincent Hamp + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef TESTHARNESSBASE_HPP +#define TESTHARNESSBASE_HPP + +#include "DCCEXProtocolDelegateMock.hpp" +#include "MockSetup.h" +#include + +using namespace testing; + +/// @brief Test fixture to setup and tear down tests +class TestHarnessBase : public Test { +public: + TestHarnessBase(); + virtual ~TestHarnessBase(); + +protected: + void SetUp() override; + void TearDown() override; + + DCCEXProtocol _dccexProtocol; + DCCEXProtocolDelegateMock _delegate; + StreamMock _console; + StreamMock _stream; +}; + +#endif // TESTHARNESSBASE_HPP diff --git a/tests/setup/TurnoutTests.h b/tests/setup/TurnoutTests.h new file mode 100644 index 0000000..7915626 --- /dev/null +++ b/tests/setup/TurnoutTests.h @@ -0,0 +1,36 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef TURNOUTTESTS_H +#define TUNROUTTESTS_H + +#include "TestHarnessBase.hpp" + +/// @brief Test harness for the Turnout class +class TurnoutTests : public TestHarnessBase {}; + +#endif // TURNOUTTESTS_H \ No newline at end of file diff --git a/tests/setup/TurntableTests.h b/tests/setup/TurntableTests.h new file mode 100644 index 0000000..b711822 --- /dev/null +++ b/tests/setup/TurntableTests.h @@ -0,0 +1,36 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#ifndef TURNTABLETESTS_H +#define TURNTABLETESTS_H + +#include "TestHarnessBase.hpp" + +/// @brief Test harness for the Turntable class +class TurntableTests : public TestHarnessBase {}; + +#endif // TURNTABLETESTS_H \ No newline at end of file diff --git a/tests/turnout/test_TurnoutParsing.cpp b/tests/turnout/test_TurnoutParsing.cpp new file mode 100644 index 0000000..4b0f0d7 --- /dev/null +++ b/tests/turnout/test_TurnoutParsing.cpp @@ -0,0 +1,75 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/TurnoutTests.h" + +TEST_F(TurnoutTests, parseEmptyTurnoutList) { + // Received flag should be false to start + EXPECT_FALSE(_dccexProtocol.receivedTurnoutList()); + _dccexProtocol.getLists(false, true, false, false); + EXPECT_EQ(_stream, "\r\n"); + _stream = {}; + + // Empty turnout list response + _stream << ""; + _dccexProtocol.check(); + + // Should be true given turnout list is empty + EXPECT_TRUE(_dccexProtocol.receivedTurnoutList()); +} + +TEST_F(TurnoutTests, parseThreeTurnouts) { + // Received flag should be false to start + EXPECT_FALSE(_dccexProtocol.receivedTurnoutList()); + _dccexProtocol.getLists(false, true, false, false); + EXPECT_EQ(_stream, "\r\n"); + _stream = {}; + + // Empty turnout list response + _stream << ""; + _dccexProtocol.check(); + + // Received flag should still be false + EXPECT_FALSE(_dccexProtocol.receivedTurnoutList()); + + // First turnout response - closed and description + _stream << R"()"; + _dccexProtocol.check(); + + // Second turnout response - thrown and description + _stream << R"()"; + _dccexProtocol.check(); + + // Third turnout response - closed and no description + _stream << R"()"; + // Delegate should call once here + EXPECT_CALL(_delegate, receivedTurnoutList()).Times(Exactly(1)); + _dccexProtocol.check(); + + // Should be true given turnout list is empty + EXPECT_TRUE(_dccexProtocol.receivedTurnoutList()); +} diff --git a/tests/turnout/test_Turnouts.cpp b/tests/turnout/test_Turnouts.cpp new file mode 100644 index 0000000..cff47da --- /dev/null +++ b/tests/turnout/test_Turnouts.cpp @@ -0,0 +1,90 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/TurnoutTests.h" + +TEST_F(TurnoutTests, createSingleTurnout) { + // Create a turnout 100 + Turnout *turnout100 = new Turnout(100, false); + turnout100->setName("Turnout 100"); + + // Validate turnout details + EXPECT_EQ(turnout100->getId(), 100); + EXPECT_STREQ(turnout100->getName(), "Turnout 100"); + EXPECT_FALSE(turnout100->getThrown()); + + // Validate it's in the list by ID + EXPECT_EQ(_dccexProtocol.turnouts->getById(100), turnout100); +} + +TEST_F(TurnoutTests, createTurnoutList) { + // Create three turnouts + Turnout *turnout100 = new Turnout(100, false); + turnout100->setName("Turnout 100"); + Turnout *turnout101 = new Turnout(101, true); + turnout101->setName("Turnout 101"); + Turnout *turnout102 = new Turnout(102, false); + turnout102->setName(""); + + // Validate turnouts are in the list + EXPECT_EQ(_dccexProtocol.turnouts->getById(100), turnout100); + EXPECT_EQ(_dccexProtocol.turnouts->getById(101), turnout101); + EXPECT_EQ(_dccexProtocol.turnouts->getById(102), turnout102); + + // Validate turnout details + EXPECT_EQ(turnout100->getId(), 100); + EXPECT_STREQ(turnout100->getName(), "Turnout 100"); + EXPECT_FALSE(turnout100->getThrown()); + + // Validate turnout details + EXPECT_EQ(turnout101->getId(), 101); + EXPECT_STREQ(turnout101->getName(), "Turnout 101"); + EXPECT_TRUE(turnout101->getThrown()); + + // Validate turnout details + EXPECT_EQ(turnout102->getId(), 102); + EXPECT_STREQ(turnout102->getName(), ""); + EXPECT_FALSE(turnout102->getThrown()); +} + +TEST_F(TurnoutTests, operateTurnout) { + // Create a turnout 100 + Turnout *turnout100 = new Turnout(100, false); + turnout100->setName("Turnout 100"); + + // Close it and validate + turnout100->setThrown(false); + EXPECT_FALSE(turnout100->getThrown()); + + // Throw it and validate + turnout100->setThrown(true); + EXPECT_TRUE(turnout100->getThrown()); + + // Close it and validate + turnout100->setThrown(false); + EXPECT_FALSE(turnout100->getThrown()); +} diff --git a/tests/turntable/test_TurntableParsing.cpp b/tests/turntable/test_TurntableParsing.cpp new file mode 100644 index 0000000..15ab958 --- /dev/null +++ b/tests/turntable/test_TurntableParsing.cpp @@ -0,0 +1,96 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/TurntableTests.h" + +TEST_F(TurntableTests, parseEmptyTurntableList) { + // Received flag should be false to start + EXPECT_FALSE(_dccexProtocol.receivedTurntableList()); + _dccexProtocol.getLists(false, false, false, true); + EXPECT_EQ(_stream, "\r\n"); + _stream = {}; + + // Empty turntable list response + _stream << ""; + _dccexProtocol.check(); + + // Should be true given turntable list is empty + EXPECT_TRUE(_dccexProtocol.receivedTurntableList()); +} + +TEST_F(TurntableTests, parseTwoTurntables) { + // Received flag should be false to start + EXPECT_FALSE(_dccexProtocol.receivedTurntableList()); + _dccexProtocol.getLists(false, false, false, true); + EXPECT_EQ(_stream, "\r\n"); + _stream = {}; + + // Two turntables in response + _stream << ""; + _dccexProtocol.check(); + + // Received should still be false while waiting for details + EXPECT_FALSE(_dccexProtocol.receivedTurntableList()); + + // First turntable response - EX-Turntable at ID 1 with 5 indexes, currently at home position + _stream << R"()"; + + // Second turntable response - DCC Turntable at ID 1 with 6 indexes, currently at position 3 + _stream << R"()"; + + // ID 1 Position responses + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + + // ID 2 Position responses + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + _dccexProtocol.check(); + _stream << R"()"; + + // Delegate should call back once here + EXPECT_CALL(_delegate, receivedTurntableList()).Times(Exactly(1)); + _dccexProtocol.check(); + + // Now the flag should be true + EXPECT_TRUE(_dccexProtocol.receivedTurntableList()); +} diff --git a/tests/turntable/test_Turntables.cpp b/tests/turntable/test_Turntables.cpp new file mode 100644 index 0000000..aff9732 --- /dev/null +++ b/tests/turntable/test_Turntables.cpp @@ -0,0 +1,212 @@ +/* -*- c++ -*- + * + * DCCEXProtocol + * + * This package implements a DCCEX native protocol connection, + * allow a device to communicate with a DCC-EX EX-CommandStation. + * + * Copyright © 2024 Peter Cole + * + * This work is licensed under the Creative Commons Attribution-ShareAlike + * 4.0 International License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to + * Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + * + * Attribution — You must give appropriate credit, provide a link to the + * license, and indicate if changes were made. You may do so in any + * reasonable manner, but not in any way that suggests the licensor + * endorses you or your use. + * + * ShareAlike — If you remix, transform, or build upon the material, you + * must distribute your contributions under the same license as the + * original. + * + * All other rights reserved. + * + */ + +#include "../setup/TurntableTests.h" + +/// @brief Test creating a single turntable index +TEST_F(TurntableTests, createTurntableIndex) { + TurntableIndex *index = new TurntableIndex(1, 0, 900, "Home"); + + // Fatal error if it wasn't created + ASSERT_NE(index, nullptr); + + // Validate details + EXPECT_EQ(index->getTTId(), 1); + EXPECT_EQ(index->getId(), 0); + EXPECT_EQ(index->getAngle(), 900); + EXPECT_STREQ(index->getName(), "Home"); + EXPECT_EQ(index->getNextIndex(), nullptr); + + // Clean up + delete index; +} + +/// @brief Test creating a complete EX-turntable +TEST_F(TurntableTests, createEXTurntable) { + // Create an EX-Turntable with: + // - ID 1 + // - Currently at the home position + // - Has 5 positions including home + // - Name "Test EX-Turntable" + Turntable *turntable1 = new Turntable(1); + // Fatal fail if the turntable is not created + ASSERT_NE(turntable1, nullptr); + + // Set and check details + turntable1->setType(TurntableType::TurntableTypeEXTT); + turntable1->setIndex(0); + turntable1->setNumberOfIndexes(5); + turntable1->setName("Test EX-Turntable"); + EXPECT_EQ(turntable1->getType(), TurntableType::TurntableTypeEXTT); + EXPECT_EQ(turntable1->getIndex(), 0); + EXPECT_EQ(turntable1->getNumberOfIndexes(), 5); + EXPECT_STREQ(turntable1->getName(), "Test EX-Turntable"); + EXPECT_EQ(turntable1->getNext(), nullptr); + + // Create 5 positions and add to list + TurntableIndex *index0 = new TurntableIndex(1, 0, 900, "Home"); + turntable1->addIndex(index0); + TurntableIndex *index1 = new TurntableIndex(1, 1, 450, "EX-Turntable Index 1"); + turntable1->addIndex(index1); + TurntableIndex *index2 = new TurntableIndex(1, 2, 1800, "EX-Turntable Index 2"); + turntable1->addIndex(index2); + TurntableIndex *index3 = new TurntableIndex(1, 3, 2700, "EX-Turntable Index 3"); + turntable1->addIndex(index3); + TurntableIndex *index4 = new TurntableIndex(1, 4, 3000, "EX-Turntable Index 4"); + turntable1->addIndex(index4); + + // Validate we have added all 5 positions + EXPECT_EQ(turntable1->getIndexCount(), 5); + + // Validate the first index is available and correct + EXPECT_EQ(turntable1->getFirstIndex(), index0); + + // Validate various attributes + EXPECT_EQ(index0->getAngle(), 900); + EXPECT_EQ(index1->getId(), 1); + EXPECT_EQ(index2->getTTId(), 1); + EXPECT_STREQ(index3->getName(), "EX-Turntable Index 3"); + EXPECT_EQ(index4->getNextIndex(), nullptr); +} + +/// @brief Test creating a complete DCC turntable +TEST_F(TurntableTests, createDCCTurntable) { + // Create a DCC Turntable with: + // - ID 2 + // - Currently at position 3 + // - Has 6 positions including home + // - Name "Test DCC Turntable" + Turntable *turntable2 = new Turntable(2); + // Fatal fail if the turntable is not created + ASSERT_NE(turntable2, nullptr); + + // Set and check details + turntable2->setType(TurntableType::TurntableTypeDCC); + turntable2->setIndex(3); + turntable2->setNumberOfIndexes(6); + turntable2->setName("Test DCC Turntable"); + EXPECT_EQ(turntable2->getType(), TurntableType::TurntableTypeDCC); + EXPECT_EQ(turntable2->getIndex(), 3); + EXPECT_EQ(turntable2->getNumberOfIndexes(), 6); + EXPECT_STREQ(turntable2->getName(), "Test DCC Turntable"); + EXPECT_EQ(turntable2->getNext(), nullptr); + + // Create 5 positions and add to list + TurntableIndex *index0 = new TurntableIndex(2, 0, 0, "Home"); + turntable2->addIndex(index0); + TurntableIndex *index1 = new TurntableIndex(2, 1, 450, "DCC Turntable Index 1"); + turntable2->addIndex(index1); + TurntableIndex *index2 = new TurntableIndex(2, 2, 1800, "DCC Turntable Index 2"); + turntable2->addIndex(index2); + TurntableIndex *index3 = new TurntableIndex(2, 3, 2700, "DCC Turntable Index 3"); + turntable2->addIndex(index3); + TurntableIndex *index4 = new TurntableIndex(2, 4, 3000, "DCC Turntable Index 4"); + turntable2->addIndex(index4); + TurntableIndex *index5 = new TurntableIndex(2, 4, 3300, "DCC Turntable Index 5"); + turntable2->addIndex(index5); + + // Validate we have added all 5 positions + EXPECT_EQ(turntable2->getIndexCount(), 6); + + // Validate the first index is available and correct + EXPECT_EQ(turntable2->getFirstIndex(), index0); + + // Validate various attributes + EXPECT_EQ(index0->getAngle(), 0); + EXPECT_EQ(index1->getId(), 1); + EXPECT_EQ(index2->getTTId(), 2); + EXPECT_STREQ(index3->getName(), "DCC Turntable Index 3"); + EXPECT_STREQ(index4->getName(), "DCC Turntable Index 4"); + EXPECT_EQ(index5->getNextIndex(), nullptr); +} + +/// @brief Test creating multiple turntables +TEST_F(TurntableTests, createTurntableList) { + // Create three turntables, ignore indexes for this + Turntable *turntable1 = new Turntable(1); + turntable1->setType(TurntableType::TurntableTypeEXTT); + turntable1->setIndex(0); + turntable1->setName("Test EX-Turntable"); + Turntable *turntable2 = new Turntable(2); + turntable2->setType(TurntableType::TurntableTypeDCC); + turntable2->setIndex(3); + turntable2->setName("Test DCC Turntable"); + Turntable *turntable3 = new Turntable(3); + turntable3->setType(TurntableType::TurntableTypeEXTT); + turntable3->setIndex(0); + turntable3->setName("Test EX-Turntable"); + + // Validate all three created and the list makeup + ASSERT_NE(turntable1, nullptr); + ASSERT_NE(turntable2, nullptr); + ASSERT_NE(turntable3, nullptr); + EXPECT_EQ(Turntable::getFirst(), turntable1); + EXPECT_EQ(turntable1->getNext(), turntable2); + EXPECT_EQ(turntable3->getNext(), nullptr); +} + +/// @brief Test operating an EX-Turntable +TEST_F(TurntableTests, operateTurntable) { + // Create an EX-Turntable + Turntable *turntable1 = new Turntable(1); + + // Set details + turntable1->setType(TurntableType::TurntableTypeEXTT); + turntable1->setIndex(0); + turntable1->setNumberOfIndexes(5); + turntable1->setName("Test EX-Turntable"); + + // Create 5 positions and add to list + TurntableIndex *index0 = new TurntableIndex(1, 0, 900, "Home"); + turntable1->addIndex(index0); + TurntableIndex *index1 = new TurntableIndex(1, 1, 450, "EX-Turntable Index 1"); + turntable1->addIndex(index1); + TurntableIndex *index2 = new TurntableIndex(1, 2, 1800, "EX-Turntable Index 2"); + turntable1->addIndex(index2); + TurntableIndex *index3 = new TurntableIndex(1, 3, 2700, "EX-Turntable Index 3"); + turntable1->addIndex(index3); + TurntableIndex *index4 = new TurntableIndex(1, 4, 3000, "EX-Turntable Index 4"); + turntable1->addIndex(index4); + + // Validate current position and state + EXPECT_EQ(turntable1->getIndex(), 0); + EXPECT_FALSE(turntable1->isMoving()); + EXPECT_STREQ(turntable1->getIndexById(turntable1->getIndex())->getName(), "Home"); + + // Set moving to position 3 and validate + turntable1->setIndex(3); + turntable1->setMoving(true); + EXPECT_EQ(turntable1->getIndex(), 3); + EXPECT_TRUE(turntable1->isMoving()); + EXPECT_STREQ(turntable1->getIndexById(turntable1->getIndex())->getName(), "EX-Turntable Index 3"); + + // Move finished + turntable1->setMoving(false); + EXPECT_EQ(turntable1->getIndex(), 3); + EXPECT_FALSE(turntable1->isMoving()); + EXPECT_STREQ(turntable1->getIndexById(turntable1->getIndex())->getName(), "EX-Turntable Index 3"); +} \ No newline at end of file