diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 35739a05f..c57135f3d 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -58,6 +58,7 @@ jobs: git config --global --add safe.directory /home/runner/work/megaglest-source/megaglest-source cd mk/linux ./build-mg.sh -f + ctest --test-dir build --output-on-failure build-linux: strategy: @@ -91,6 +92,10 @@ jobs: mk/linux/build-mg.sh -m ${EXTRA_OPTS} make -C mk/linux/build -j$(nproc) VERBOSE=1 + - name: Run tests + if: ${{ matrix.release != true && matrix.compiler == 'gcc' }} + run: ctest --test-dir mk/linux/build --output-on-failure + - name: Build Release if: ${{ matrix.release == true }} run: | diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 15308ecd8..718228a68 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -33,6 +33,9 @@ jobs: mk/macos/build-mg.sh -m make -C mk/macos/build -j$(sysctl -n hw.ncpu) VERBOSE=1 + - name: Run tests + run: ctest --test-dir mk/macos/build --output-on-failure + - name: Git Hash if: ${{ github.ref == 'refs/heads/develop' }} run: | diff --git a/BUILD.md b/BUILD.md index e89e183da..99e55a567 100644 --- a/BUILD.md +++ b/BUILD.md @@ -97,6 +97,33 @@ cd builddir cmake -LH ``` +## Unit tests + +Unit tests are built by default (enabled by `-DBUILD_MEGAGLEST_TESTS=ON` in +`build-mg.sh`). After a successful build the test binary is at: + + mk/linux/megaglest_tests # Linux + mk/macos/megaglest_tests # macOS + +Run all tests via CTest: + + ctest --test-dir mk/linux/build --output-on-failure + +Or run the binary directly, optionally filtering by suite or test name: + + ./mk/linux/megaglest_tests + ./mk/linux/megaglest_tests SocketTest + ./mk/linux/megaglest_tests SocketTest::test_ip_ipv6_cursor_stripped + +To run tests as part of the build script: + + ./mk/linux/build-mg.sh -t # build then run tests + +Tests live under `source/tests/`. To add tests for a new subsystem, create a +subdirectory under `source/tests/shared_lib/` and add it to `DIRS_WITH_SRC` in +`source/tests/CMakeLists.txt` — the glob will pick up any `.cpp` files there +automatically. + ## Installing from manual build > [!CAUTION] diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e0d402e5..d30477922 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,8 @@ ENDIF() PROJECT( MegaGlest ) +enable_testing() + # This helps prevent certain errors such as what's mentioned in # https://github.com/MegaGlest/megaglest-source/pull/300 # and also some consider it good practice to specify a standard diff --git a/mk/linux/build-mg.sh b/mk/linux/build-mg.sh index 829468332..081a9dc12 100755 --- a/mk/linux/build-mg.sh +++ b/mk/linux/build-mg.sh @@ -28,9 +28,10 @@ LUA_FORCED_VERSION=0 FORCE_32BIT_CROSS_COMPILE=0 COMPILATION_WITHOUT=0 BUILD_MEGAGLEST_TESTS="ON" +RUN_TESTS=0 SHOW_CMAKE_OPTIONS=0 -while getopts "B:c:defg:hl:mnopswx" option; do +while getopts "B:c:defg:hl:mnopswxt" option; do case "${option}" in B) BUILD_DIR=${OPTARG} @@ -72,6 +73,7 @@ while getopts "B:c:defg:hl:mnopswx" option; do echo " -w : Force compilation 'Without using wxWidgets'" echo " -x : Force cross compiling on x64 linux to produce an x86 32 bit binary" + echo " -t : Run unit tests after build (requires -DBUILD_MEGAGLEST_TESTS=ON)" echo " -o : Show available cmake options" echo " -h : Display this help usage" echo " -- : Pass remaining arguments verbatim to cmake" @@ -111,6 +113,9 @@ while getopts "B:c:defg:hl:mnopswx" option; do FORCE_32BIT_CROSS_COMPILE=1 # echo "${option} value: ${OPTARG}" ;; + t) + RUN_TESTS=1 + ;; \?) echo "Script Invalid option: -$OPTARG" >&2 @@ -376,6 +381,14 @@ else echo 'ERROR: MAKE failed.' >&2; exit 2 fi + if [ $RUN_TESTS = 1 ]; then + echo "==================> Running unit tests... <==================================" + ctest --output-on-failure + if [ $? -ne 0 ]; then + echo 'ERROR: Tests failed.' >&2; exit 3 + fi + fi + cd .. echo '' echo 'BUILD COMPLETE.' diff --git a/mk/macos/build-mg.sh b/mk/macos/build-mg.sh index 4f0fb1831..1ad624511 100755 --- a/mk/macos/build-mg.sh +++ b/mk/macos/build-mg.sh @@ -21,6 +21,7 @@ WANT_STATIC_LIBS="-DWANT_STATIC_LIBS=ON" FORCE_EMBEDDED_LIBS=0 LUA_FORCED_VERSION=0 COMPILATION_WITHOUT=0 +RUN_TESTS=0 SHOW_CMAKE_OPTIONS=0 # Some brew things don't appear to link correctly by themselves. @@ -54,7 +55,7 @@ then fi fi -while getopts "B:c:defhl:mnopwxb" option; do +while getopts "B:c:defhl:mnoptwxb" option; do case "${option}" in B) BUILD_DIR=${OPTARG};; c) CPU_COUNT=${OPTARG};; @@ -73,6 +74,7 @@ while getopts "B:c:defhl:mnopwxb" option; do echo " -l x : Force using LUA version x - example: -l 5.3" echo " -m : Force running CMAKE only to create Make files (do not compile)" echo " -n : Force running MAKE only to compile (assume CMAKE already built make files)" + echo " -t : Run unit tests after build (requires -DBUILD_MEGAGLEST_TESTS=ON)" echo " -w : Force compilation 'Without using wxWidgets'" echo " -x : Force usage of Xcode and xcodebuild" echo " -o : Show available cmake options" @@ -90,6 +92,7 @@ while getopts "B:c:defhl:mnopwxb" option; do fi SHOW_CMAKE_OPTIONS=1 CMAKE_ONLY=1;; + t) RUN_TESTS=1;; w) COMPILATION_WITHOUT=1;; x) USE_XCODE=1;; b) BUILD_BUNDLE=1 @@ -237,14 +240,7 @@ fi if [ "$MAKE_ONLY" -eq "0" ]; then EXTRA_CMAKE_OPTIONS="${EXTRA_CMAKE_OPTIONS} -DWANT_DEV_OUTPATH=ON $WANT_STATIC_LIBS -DBREAKPAD_ROOT=$BREAKPAD_ROOT" if [ "$BUILD_BUNDLE" -ne "1" ]; then - EXTRA_CMAKE_OPTIONS="${EXTRA_CMAKE_OPTIONS} -DCMAKE_INSTALL_PREFIX=''" - if [ "$GCC_FORCED" -ne "1" ] || [ "$USE_XCODE" -eq "1" ]; then : - #^ Remove this condition when it V will start working on gcc - #EXTRA_CMAKE_OPTIONS="${EXTRA_CMAKE_OPTIONS} -DBUILD_MEGAGLEST_TESTS=ON" - #^ Uncomment when it will start working on clang - else - rm -f ../megaglest_tests - fi + EXTRA_CMAKE_OPTIONS="${EXTRA_CMAKE_OPTIONS} -DCMAKE_INSTALL_PREFIX='' -DBUILD_MEGAGLEST_TESTS=ON" rm -f ../MegaGlest*.dmg else EXTRA_CMAKE_OPTIONS="${EXTRA_CMAKE_OPTIONS} -DCPACK_GENERATOR=Bundle -DWANT_SINGLE_INSTALL_DIRECTORY=ON" @@ -276,6 +272,12 @@ else echo "==================> About to call make with $NUMCORES cores... <==================" make -j$NUMCORES if [ "$?" -ne "0" ]; then echo 'ERROR: MAKE failed.' >&2; exit 2; fi + + if [ "$RUN_TESTS" -eq "1" ]; then + echo "==================> Running unit tests... <==================================" + ctest --output-on-failure + if [ "$?" -ne "0" ]; then echo 'ERROR: Tests failed.' >&2; exit 3; fi + fi fi if [ -d "../Debug" ]; then mv -f ../Debug/megaglest* "$SCRIPTDIR"; rm -rf ../Debug diff --git a/mk/windoze/build-mg-vs-cmake.ps1 b/mk/windoze/build-mg-vs-cmake.ps1 index 2554a154c..b4bbb3a70 100644 --- a/mk/windoze/build-mg-vs-cmake.ps1 +++ b/mk/windoze/build-mg-vs-cmake.ps1 @@ -1,6 +1,6 @@ # Build MegaGlest on Windows. # Author: James Sherratt. -param(${vcpkg-location}, ${buildtype}, [string[]]${cmake-options}, [switch]${show-options}) +param(${vcpkg-location}, ${buildtype}, [string[]]${cmake-options}, [switch]${show-options}, [switch]${run-tests}) $sword = [char]::ConvertFromUtf32(0x2694) Write-Output "=====$sword MegaGlest $sword=====" @@ -145,6 +145,11 @@ cmake --build "$buildFolder" --config $buildtype --target ALL_BUILD if ($?) { "Build succeeded. megaglest.exe, megaglest_editor.exe and megaglest_g3dviewer.exe can be found in mk/windoze/." + if (${run-tests}) { + Write-Title "Running unit tests" + ctest --test-dir "$buildFolder" --build-config $buildtype --output-on-failure + if (!$?) { "Tests failed."; Exit 3 } + } } else { "Build failed. Please make sure you have installed VS C++ tools (2019 or 2022): https://visualstudio.microsoft.com/downloads ." diff --git a/source/glest_game/main/main.cpp b/source/glest_game/main/main.cpp index c609ae0dc..fbd0fecab 100644 --- a/source/glest_game/main/main.cpp +++ b/source/glest_game/main/main.cpp @@ -5321,12 +5321,7 @@ int glestMain(int argc, char **argv) { string autoConnectServer = paramPartTokens[1]; int port = config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); - vector paramPartTokens2; - Tokenize(autoConnectServer, paramPartTokens2, ":"); - autoConnectServer = paramPartTokens2[0]; - if (paramPartTokens2.size() >= 2 && paramPartTokens2[1].length() > 0) { - port = strToInt(paramPartTokens2[1]); - } + Ip::parseHostPort(autoConnectServer, port); printf("Connecting to host [%s] using port: %d\n", autoConnectServer.c_str(), port); if (autoConnectServer == "auto-connect") { diff --git a/source/glest_game/menu/menu_state_join_game.cpp b/source/glest_game/menu/menu_state_join_game.cpp index e6737bae0..14db0a3ca 100644 --- a/source/glest_game/menu/menu_state_join_game.cpp +++ b/source/glest_game/menu/menu_state_join_game.cpp @@ -70,6 +70,7 @@ void MenuStateJoinGame::CommonInit(bool connect, Ip serverIp, int portNumberOver containerName = "JoinGame"; abortAutoFind = false; autoConnectToServer = false; + serverPortOverride = portNumberOverride; Lang &lang = Lang::getInstance(); Config &config = Config::getInstance(); NetworkManager &networkManager = NetworkManager::getInstance(); @@ -165,13 +166,7 @@ void MenuStateJoinGame::CommonInit(bool connect, Ip serverIp, int portNumberOver string host = labelServerIp.getText(); int portNumber = config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); - std::vector hostPartsList; - Tokenize(host, hostPartsList, ":"); - if (hostPartsList.size() > 1) { - host = hostPartsList[0]; - replaceAll(hostPartsList[1], "_", ""); - portNumber = strToInt(hostPartsList[1]); - } + Ip::parseHostPort(host, portNumber); string port = " (" + intToStr(portNumber) + ")"; labelServerPort.setText(port); @@ -187,34 +182,17 @@ void MenuStateJoinGame::CommonInit(bool connect, Ip serverIp, int portNumberOver connected = false; playerIndex = -1; - // server ip + // server ip — label stores only the address; port is tracked via serverPortOverride if (connect == true) { - string hostIP = serverIp.getString(); - if (portNumberOverride > 0) { - hostIP += ":" + intToStr(portNumberOverride); - } - - labelServerIp.setText(hostIP + "_"); - + labelServerIp.setText(serverIp.getString() + "_"); autoConnectToServer = true; } else { - string hostIP = config.getString("ServerIp"); - if (portNumberOverride > 0) { - hostIP += ":" + intToStr(portNumberOverride); - } - - labelServerIp.setText(hostIP + "_"); + labelServerIp.setText(config.getString("ServerIp") + "_"); } host = labelServerIp.getText(); - portNumber = config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); - hostPartsList.clear(); - Tokenize(host, hostPartsList, ":"); - if (hostPartsList.size() > 1) { - host = hostPartsList[0]; - replaceAll(hostPartsList[1], "_", ""); - portNumber = strToInt(hostPartsList[1]); - } + portNumber = serverPortOverride > 0 ? serverPortOverride : config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); + Ip::parseHostPort(host, portNumber); port = " (" + intToStr(portNumber) + ")"; labelServerPort.setText(port); @@ -247,14 +225,8 @@ void MenuStateJoinGame::reloadUI() { labelServerPortLabel.setText(lang.getString("ServerPort")); string host = labelServerIp.getText(); - int portNumber = config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); - std::vector hostPartsList; - Tokenize(host, hostPartsList, ":"); - if (hostPartsList.size() > 1) { - host = hostPartsList[0]; - replaceAll(hostPartsList[1], "_", ""); - portNumber = strToInt(hostPartsList[1]); - } + int portNumber = serverPortOverride > 0 ? serverPortOverride : config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); + Ip::parseHostPort(host, portNumber); string port = " (" + intToStr(portNumber) + ")"; labelServerPort.setText(port); @@ -370,14 +342,8 @@ void MenuStateJoinGame::mouseClick(int x, int y, MouseButton mouseButton) { string host = labelServerIp.getText(); Config &config = Config::getInstance(); - int portNumber = config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); - std::vector hostPartsList; - Tokenize(host, hostPartsList, ":"); - if (hostPartsList.size() > 1) { - host = hostPartsList[0]; - replaceAll(hostPartsList[1], "_", ""); - portNumber = strToInt(hostPartsList[1]); - } + int portNumber = serverPortOverride > 0 ? serverPortOverride : config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); + Ip::parseHostPort(host, portNumber); string port = " (" + intToStr(portNumber) + ")"; labelServerPort.setText(port); @@ -585,16 +551,10 @@ void MenuStateJoinGame::update() { labelInfo.setText(lang.getString("WaitingHost")); string host = labelServerIp.getText(); - std::vector hostPartsList; - Tokenize(host, hostPartsList, ":"); - if (hostPartsList.size() > 1) { - host = hostPartsList[0]; - replaceAll(hostPartsList[1], "_", ""); - } - string saveHost = Ip(host).getString(); - if (hostPartsList.size() > 1) { - saveHost += ":" + hostPartsList[1]; - } + Config &config = Config::getInstance(); + int portNumber = serverPortOverride > 0 ? serverPortOverride : config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); + Ip::parseHostPort(host, portNumber); + string saveHost = Ip::buildHostDisplay(Ip(host).getString(), portNumber); servers.setString(clientInterface->getServerName(), saveHost); } @@ -753,16 +713,10 @@ bool MenuStateJoinGame::connectToServer() { Config &config = Config::getInstance(); string host = labelServerIp.getText(); - int port = config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); - std::vector hostPartsList; - Tokenize(host, hostPartsList, ":"); - if (hostPartsList.size() > 1) { - host = hostPartsList[0]; - replaceAll(hostPartsList[1], "_", ""); - port = strToInt(hostPartsList[1]); - } + int port = serverPortOverride > 0 ? serverPortOverride : config.getInt("PortServer", intToStr(GameConstants::serverPort).c_str()); + Ip::parseHostPort(host, port); + serverPortOverride = port; Ip serverIp(host); - ClientInterface *clientInterface = NetworkManager::getInstance().getClientInterface(); clientInterface->connect(serverIp, port); @@ -788,10 +742,7 @@ bool MenuStateJoinGame::connectToServer() { } } if (clientInterface->isConnected() == true && clientInterface->getIntroDone() == true) { - string saveHost = Ip(host).getString(); - if (hostPartsList.size() > 1) { - saveHost += ":" + hostPartsList[1]; - } + string saveHost = Ip::buildHostDisplay(Ip(host).getString(), port); servers.setString(clientInterface->getServerName(), saveHost); servers.save(serversSavedFile); diff --git a/source/glest_game/menu/menu_state_join_game.h b/source/glest_game/menu/menu_state_join_game.h index 7e45d5571..2c5e629fb 100644 --- a/source/glest_game/menu/menu_state_join_game.h +++ b/source/glest_game/menu/menu_state_join_game.h @@ -68,6 +68,7 @@ class MenuStateJoinGame : public MenuState, public DiscoveredServersInterface { string serversSavedFile; bool abortAutoFind; bool autoConnectToServer; + int serverPortOverride; public: MenuStateJoinGame(Program *program, MainMenu *mainMenu, bool connect = false, Ip serverIp = Ip(), int portNumberOverride = -1); diff --git a/source/shared_lib/include/platform/posix/socket.h b/source/shared_lib/include/platform/posix/socket.h index 0db7b3bc3..50ffbf6e4 100644 --- a/source/shared_lib/include/platform/posix/socket.h +++ b/source/shared_lib/include/platform/posix/socket.h @@ -97,6 +97,7 @@ class DiscoveredServersInterface { class Ip { private: unsigned char bytes[4]; + std::string addrStr; // stores IPv6 or IPv4 strings; overrides bytes when non-empty public: Ip(); @@ -106,6 +107,17 @@ class Ip { unsigned char getByte(int byteIndex) { return bytes[byteIndex]; } string getString() const; + + // Parse "host", "host:port", or "[ipv6]:port" from a UI label string. + // Strips trailing '_' cursor characters. Updates host and port in-place. + // Bare IPv6 addresses (multiple colons, no brackets) leave host unchanged + // and do not modify port. + static void parseHostPort(std::string &host, int &port); + + // Format host:port for display. Uses "[host]:port" bracket notation when + // host is an IPv6 address (contains a colon). Returns host unchanged when + // port <= 0. + static std::string buildHostDisplay(const std::string &host, int port); }; // ===================================================== @@ -126,6 +138,7 @@ class Socket { // static SocketManager wsaManager; // #endif PLATFORM_SOCKET sock; + int socketFamily; // AF_INET or AF_INET6 time_t lastDebugEvent; static int broadcast_portno; std::string ipAddress; @@ -202,6 +215,7 @@ class Socket { virtual std::string getIpAddress(); virtual void setIpAddress(std::string value) { ipAddress = value; } + int getSocketFamily() const { return socketFamily; } uint32 getConnectedIPAddress(string IP = ""); diff --git a/source/shared_lib/sources/platform/posix/socket.cpp b/source/shared_lib/sources/platform/posix/socket.cpp index 772d9507a..309f55419 100644 --- a/source/shared_lib/sources/platform/posix/socket.cpp +++ b/source/shared_lib/sources/platform/posix/socket.cpp @@ -286,23 +286,63 @@ Ip::Ip(unsigned char byte0, unsigned char byte1, unsigned char byte2, unsigned c } Ip::Ip(const string &ipString) { - size_t offset = 0; - int byteIndex = 0; + bytes[0] = 0; + bytes[1] = 0; + bytes[2] = 0; + bytes[3] = 0; - if (ipString.empty() == false) { - for (byteIndex = 0; byteIndex < 4; ++byteIndex) { - size_t dotPos = ipString.find_first_of('.', offset); + // Strip trailing '_' cursor characters appended by the UI text labels + string clean = ipString; + while (!clean.empty() && clean.back() == '_') { + clean.pop_back(); + } + addrStr = clean; - bytes[byteIndex] = atoi(ipString.substr(offset, dotPos - offset).c_str()); + // For IPv4 dotted-decimal, also populate the bytes array for backwards compat + if (clean.find(':') == string::npos && clean.find('.') != string::npos) { + size_t offset = 0; + for (int byteIndex = 0; byteIndex < 4; ++byteIndex) { + size_t dotPos = clean.find_first_of('.', offset); + bytes[byteIndex] = (unsigned char)atoi(clean.substr(offset, dotPos - offset).c_str()); offset = dotPos + 1; } } } string Ip::getString() const { + if (!addrStr.empty()) { + return addrStr; + } return intToStr(bytes[0]) + "." + intToStr(bytes[1]) + "." + intToStr(bytes[2]) + "." + intToStr(bytes[3]); } +void Ip::parseHostPort(string &host, int &port) { + replaceAll(host, "_", ""); + if (!host.empty() && host[0] == '[') { + size_t close = host.find(']'); + if (close != string::npos) { + if (close + 2 <= host.size() && host[close + 1] == ':') { + port = strToInt(host.substr(close + 2)); + } + host = host.substr(1, close - 1); + } + } else { + size_t firstColon = host.find(':'); + if (firstColon != string::npos && host.find(':', firstColon + 1) == string::npos) { + port = strToInt(host.substr(firstColon + 1)); + host = host.substr(0, firstColon); + } + } +} + +string Ip::buildHostDisplay(const string &host, int port) { + if (port <= 0) return host; + if (host.find(':') != string::npos) { + return "[" + host + "]:" + intToStr(port); + } + return host + ":" + intToStr(port); +} + // =============================================== // class Socket // =============================================== @@ -677,21 +717,42 @@ std::vector Socket::getLocalIPAddressList() { char myhostname[101] = ""; gethostname(myhostname, 100); - struct hostent *myhostent = gethostbyname(myhostname); - if (myhostent) { - // get all host IP addresses (Except for loopback) - char myhostaddr[101] = ""; - for (int ipIdx = 0; myhostent->h_addr_list[ipIdx] != NULL; ++ipIdx) { - Ip::Inet_NtoA(SockAddrToUint32((struct in_addr *)myhostent->h_addr_list[ipIdx]), myhostaddr); - - // printf("ipIdx = %d [%s]\n",ipIdx,myhostaddr); - if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) - SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] myhostaddr = [%s]\n", __FILE__, __FUNCTION__, __LINE__, myhostaddr); - - if (strlen(myhostaddr) > 0 && strncmp(myhostaddr, "127.", 4) != 0 && strncmp(myhostaddr, "0.", 2) != 0) { - ipList.push_back(myhostaddr); + // Use getaddrinfo to enumerate host addresses (supports both IPv4 and IPv6) + struct addrinfo hints, *res = NULL, *rp = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + if (getaddrinfo(myhostname, NULL, &hints, &res) == 0) { + for (rp = res; rp != NULL; rp = rp->ai_next) { + char myhostaddr[INET6_ADDRSTRLEN] = ""; + if (rp->ai_family == AF_INET) { + struct sockaddr_in *sa = (struct sockaddr_in *)rp->ai_addr; + inet_ntop(AF_INET, &sa->sin_addr, myhostaddr, sizeof(myhostaddr)); + if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] myhostaddr (v4) = [%s]\n", __FILE__, __FUNCTION__, __LINE__, + myhostaddr); + if (strlen(myhostaddr) > 0 && strncmp(myhostaddr, "127.", 4) != 0 && strncmp(myhostaddr, "0.", 2) != 0) { + if (std::find(ipList.begin(), ipList.end(), myhostaddr) == ipList.end()) { + ipList.push_back(myhostaddr); + } + } + } else if (rp->ai_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)rp->ai_addr; + if (IN6_IS_ADDR_LOOPBACK(&sa6->sin6_addr) || IN6_IS_ADDR_LINKLOCAL(&sa6->sin6_addr)) { + continue; + } + inet_ntop(AF_INET6, &sa6->sin6_addr, myhostaddr, sizeof(myhostaddr)); + if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] myhostaddr (v6) = [%s]\n", __FILE__, __FUNCTION__, __LINE__, + myhostaddr); + if (strlen(myhostaddr) > 0) { + if (std::find(ipList.begin(), ipList.end(), myhostaddr) == ipList.end()) { + ipList.push_back(myhostaddr); + } + } } } + freeaddrinfo(res); } Socket::getLocalIPAddressListForPlatform(ipList); @@ -700,25 +761,40 @@ std::vector Socket::getLocalIPAddressList() { #ifndef WIN32 void Socket::getLocalIPAddressListForPlatform(std::vector &ipList) { - // Now check all linux network devices + // Now check all network devices, including IPv6 struct ifaddrs *ifap = NULL; getifaddrs(&ifap); for (struct ifaddrs *ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) { if (!ifa->ifa_addr) { continue; } - if (ifa->ifa_addr->sa_family == AF_INET) { // check it is IP4 - // is a valid IP4 Address + char addrBuf[INET6_ADDRSTRLEN] = ""; + if (ifa->ifa_addr->sa_family == AF_INET) { struct sockaddr_in *sa = (struct sockaddr_in *)ifa->ifa_addr; - char *addr = inet_ntoa(sa->sin_addr); - // printf("Interface: %s\tAddress: %s\n", ifa->ifa_name, addr); + inet_ntop(AF_INET, &sa->sin_addr, addrBuf, sizeof(addrBuf)); + if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) { + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] Interface: [%s] IPv4 address: [%s]\n", __FILE__, __FUNCTION__, + __LINE__, ifa->ifa_name, addrBuf); + } + if (strlen(addrBuf) > 0 && strncmp(addrBuf, "127.", 4) != 0 && strncmp(addrBuf, "0.", 2) != 0) { + if (std::find(ipList.begin(), ipList.end(), addrBuf) == ipList.end()) { + ipList.push_back(addrBuf); + } + } + } else if (ifa->ifa_addr->sa_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)ifa->ifa_addr; + // Skip loopback (::1) and link-local (fe80::/10) + if (IN6_IS_ADDR_LOOPBACK(&sa6->sin6_addr) || IN6_IS_ADDR_LINKLOCAL(&sa6->sin6_addr)) { + continue; + } + inet_ntop(AF_INET6, &sa6->sin6_addr, addrBuf, sizeof(addrBuf)); if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) { - SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] Interface: [%s] address: [%s]\n", __FILE__, __FUNCTION__, __LINE__, - ifa->ifa_name, addr); + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] Interface: [%s] IPv6 address: [%s]\n", __FILE__, __FUNCTION__, + __LINE__, ifa->ifa_name, addrBuf); } - if (strlen(addr) > 0 && strncmp(addr, "127.", 4) != 0 && strncmp(addr, "0.", 2) != 0) { - if (std::find(ipList.begin(), ipList.end(), addr) == ipList.end()) { - ipList.push_back(addr); + if (strlen(addrBuf) > 0) { + if (std::find(ipList.begin(), ipList.end(), addrBuf) == ipList.end()) { + ipList.push_back(addrBuf); } } } @@ -801,15 +877,13 @@ void Socket::getLocalIPAddressListForPlatform(std::vector &ipList) #ifdef WIN32 void Socket::getLocalIPAddressListForPlatform(std::vector &ipList) { ULONG outBufLen = 0; - GetAdaptersAddresses(AF_INET, 0, NULL, NULL, &outBufLen); + GetAdaptersAddresses(AF_UNSPEC, 0, NULL, NULL, &outBufLen); PIP_ADAPTER_ADDRESSES pAddresses = (IP_ADAPTER_ADDRESSES *)malloc(outBufLen); - GetAdaptersAddresses(AF_INET, GAA_FLAG_SKIP_ANYCAST, NULL, pAddresses, &outBufLen); + GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_SKIP_ANYCAST, NULL, pAddresses, &outBufLen); PIP_ADAPTER_ADDRESSES pCurrAddresses = NULL; PIP_ADAPTER_UNICAST_ADDRESS pUnicast = NULL; LPSOCKADDR addr = NULL; pCurrAddresses = pAddresses; - // char buff[100]; - DWORD bufflen = 100; while (pCurrAddresses) { if (pCurrAddresses->OperStatus != IfOperStatusUp) { pCurrAddresses = pCurrAddresses->Next; @@ -818,16 +892,29 @@ void Socket::getLocalIPAddressListForPlatform(std::vector &ipList) pUnicast = pCurrAddresses->FirstUnicastAddress; while (pUnicast) { addr = pUnicast->Address.lpSockaddr; + char addrBuf[INET6_ADDRSTRLEN] = ""; if (addr->sa_family == AF_INET && pCurrAddresses->IfType != MIB_IF_TYPE_LOOPBACK) { sockaddr_in *sa_in = (sockaddr_in *)addr; - char *strIP = ::inet_ntoa((sa_in->sin_addr)); - // printf("\tIPV4:%s\n", strIP); + inet_ntop(AF_INET, &sa_in->sin_addr, addrBuf, sizeof(addrBuf)); if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) { - SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] strIP [%s]\n", __FILE__, __FUNCTION__, __LINE__, strIP); + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] IPv4 [%s]\n", __FILE__, __FUNCTION__, __LINE__, addrBuf); + } + if (strlen(addrBuf) > 0 && strncmp(addrBuf, "127.", 4) != 0 && strncmp(addrBuf, "0.", 2) != 0) { + if (std::find(ipList.begin(), ipList.end(), addrBuf) == ipList.end()) { + ipList.push_back(addrBuf); + } } - if (strlen(strIP) > 0 && strncmp(strIP, "127.", 4) != 0 && strncmp(strIP, "0.", 2) != 0) { - if (std::find(ipList.begin(), ipList.end(), strIP) == ipList.end()) { - ipList.push_back(strIP); + } else if (addr->sa_family == AF_INET6 && pCurrAddresses->IfType != MIB_IF_TYPE_LOOPBACK) { + sockaddr_in6 *sa_in6 = (sockaddr_in6 *)addr; + if (!IN6_IS_ADDR_LOOPBACK(&sa_in6->sin6_addr) && !IN6_IS_ADDR_LINKLOCAL(&sa_in6->sin6_addr)) { + inet_ntop(AF_INET6, &sa_in6->sin6_addr, addrBuf, sizeof(addrBuf)); + if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) { + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] IPv6 [%s]\n", __FILE__, __FUNCTION__, __LINE__, addrBuf); + } + if (strlen(addrBuf) > 0) { + if (std::find(ipList.begin(), ipList.end(), addrBuf) == ipList.end()) { + ipList.push_back(addrBuf); + } } } } @@ -878,6 +965,7 @@ Socket::Socket(PLATFORM_SOCKET sock) { dataSynchAccessorWrite->setOwnerId(CODE_AT_LINE); this->sock = sock; + this->socketFamily = AF_INET; this->isSocketBlocking = true; this->connectedIpAddress = ""; } @@ -901,6 +989,7 @@ Socket::Socket() { // this->pingThread = NULL; this->connectedIpAddress = ""; + this->socketFamily = AF_INET; sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (isSocketValid() == false) { @@ -2141,20 +2230,37 @@ string Socket::getHostName() { } string Socket::getIp() { - hostent *info = gethostbyname(getHostName().c_str()); - unsigned char *address; - - if (info == NULL) { - throw megaglest_runtime_error("Error getting host by name"); - } - - address = reinterpret_cast(info->h_addr_list[0]); - - if (address == NULL) { + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; // prefer IPv4 for backwards compat + hints.ai_socktype = SOCK_STREAM; + + int err = getaddrinfo(getHostName().c_str(), NULL, &hints, &res); + if (err != 0 || res == NULL) { + // Fall back to any address family + if (res) freeaddrinfo(res); + hints.ai_family = AF_UNSPEC; + err = getaddrinfo(getHostName().c_str(), NULL, &hints, &res); + if (err != 0 || res == NULL) { + if (res) freeaddrinfo(res); + throw megaglest_runtime_error("Error getting host ip"); + } + } + + char addrBuf[INET6_ADDRSTRLEN] = ""; + if (res->ai_family == AF_INET) { + struct sockaddr_in *sa = (struct sockaddr_in *)res->ai_addr; + inet_ntop(AF_INET, &sa->sin_addr, addrBuf, sizeof(addrBuf)); + } else if (res->ai_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)res->ai_addr; + inet_ntop(AF_INET6, &sa6->sin6_addr, addrBuf, sizeof(addrBuf)); + } + freeaddrinfo(res); + + if (addrBuf[0] == '\0') { throw megaglest_runtime_error("Error getting host ip"); } - - return intToStr(address[0]) + "." + intToStr(address[1]) + "." + intToStr(address[2]) + "." + intToStr(address[3]); + return addrBuf; } void Socket::throwException(string str) { @@ -2224,29 +2330,69 @@ void ClientSocket::discoverServers(DiscoveredServersInterface *cb) { } void ClientSocket::connect(const Ip &ip, int port) { - sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = inet_addr(ip.getString().c_str()); - addr.sin_port = htons(port); + string ipStr = ip.getString(); if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) - SystemFlags::OutputDebug(SystemFlags::debugNetwork, "Connecting to host [%s] on port = %d\n", ip.getString().c_str(), port); - if (SystemFlags::VERBOSE_MODE_ENABLED) printf("Connecting to host [%s] on port = %d\n", ip.getString().c_str(), port); + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "Connecting to host [%s] on port = %d\n", ipStr.c_str(), port); + if (SystemFlags::VERBOSE_MODE_ENABLED) printf("Connecting to host [%s] on port = %d\n", ipStr.c_str(), port); + + // Resolve the address using getaddrinfo, which supports both IPv4 and IPv6. + char portStr[16]; + snprintf(portStr, sizeof(portStr), "%d", port); + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + int gai_err = getaddrinfo(ipStr.c_str(), portStr, &hints, &res); + if (gai_err != 0 || res == NULL) { + if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] getaddrinfo failed for [%s]: %s\n", __FILE__, __FUNCTION__, __LINE__, + ipStr.c_str(), gai_strerror(gai_err)); + if (res) freeaddrinfo(res); + disconnectSocket(); + return; + } + // If the resolved family differs from the current socket family, recreate + // the socket so it matches (e.g. upgrade AF_INET to AF_INET6). + if (res->ai_family != socketFamily) { + disconnectSocket(); + sock = socket(res->ai_family, SOCK_STREAM, IPPROTO_TCP); + if (isSocketValid() == false) { + freeaddrinfo(res); + throwException("Error creating socket for connect"); + } + socketFamily = res->ai_family; + setBlock(false); +#ifdef __APPLE__ + { + int set = 1; + setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int)); + } +#endif + if (Socket::disableNagle == true) { + int flag = 1; + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag)); + } + } + + struct sockaddr_storage addr; + memcpy(&addr, res->ai_addr, res->ai_addrlen); + socklen_t addrlen = (socklen_t)res->ai_addrlen; + freeaddrinfo(res); connectedIpAddress = ""; - int err = ::connect(sock, reinterpret_cast(&addr), sizeof(addr)); + int err = ::connect(sock, reinterpret_cast(&addr), addrlen); if (err < 0) { if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] #2 Error connecting socket for IP: %s for " "Port: %d err = %d error = %s\n", - __FILE__, __FUNCTION__, __LINE__, ip.getString().c_str(), port, err, getLastSocketErrorFormattedText().c_str()); + __FILE__, __FUNCTION__, __LINE__, ipStr.c_str(), port, err, getLastSocketErrorFormattedText().c_str()); if (SystemFlags::VERBOSE_MODE_ENABLED) printf("In [%s::%s Line: %d] #2 Error connecting socket for IP: %s for " "Port: %d err = %d error = %s\n", - __FILE__, __FUNCTION__, __LINE__, ip.getString().c_str(), port, err, getLastSocketErrorFormattedText().c_str()); + __FILE__, __FUNCTION__, __LINE__, ipStr.c_str(), port, err, getLastSocketErrorFormattedText().c_str()); int lastSocketError = getLastSocketError(); if (lastSocketError == PLATFORM_SOCKET_INPROGRESS || lastSocketError == PLATFORM_SOCKET_TRY_AGAIN) { @@ -2362,17 +2508,16 @@ void ClientSocket::connect(const Ip &ip, int port) { printf("In [%s::%s Line: %d] Valid recovery for connection sock " "= " PLATFORM_SOCKET_FORMAT_TYPE ", err = %d, error = %s\n", __FILE__, __FUNCTION__, __LINE__, sock, err, getLastSocketErrorFormattedText().c_str()); - connectedIpAddress = ip.getString(); + connectedIpAddress = ipStr; } } else { if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) - SystemFlags::OutputDebug(SystemFlags::debugNetwork, "Connected to host [%s] on port = %d sock = %d err = %d", ip.getString().c_str(), port, sock, - err); + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "Connected to host [%s] on port = %d sock = %d err = %d", ipStr.c_str(), port, sock, err); if (SystemFlags::VERBOSE_MODE_ENABLED) printf("Connected to host [%s] on port = %d sock " "= " PLATFORM_SOCKET_FORMAT_TYPE " err = %d", - ip.getString().c_str(), port, sock, err); - connectedIpAddress = ip.getString(); + ipStr.c_str(), port, sock, err); + connectedIpAddress = ipStr; } } @@ -2681,34 +2826,78 @@ void ServerSocket::bind(int port) { boundPort = port; - if (isSocketValid() == false) { + // Close any existing socket so we can choose the right address family. + if (isSocketValid()) { + disconnectSocket(); + portBound = false; + } + + // When no specific address is requested, try an IPv6 dual-stack socket + // (IPV6_V6ONLY=0) so the server accepts both IPv4 and IPv6 connections on + // a single socket. If the platform does not support dual-stack (e.g. + // OpenBSD where IPV6_V6ONLY cannot be cleared), fall back to IPv4. + bool useIPv6 = (this->bindSpecificAddress == ""); + + if (useIPv6) { + sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); + if (isSocketValid()) { + int v6only = 0; +#ifndef WIN32 + if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)) != 0) { +#else + if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&v6only, sizeof(v6only)) != 0) { +#endif + // Cannot clear IPV6_V6ONLY — fall back to IPv4-only + disconnectSocket(); + portBound = false; + useIPv6 = false; + } + } else { + useIPv6 = false; + } + if (useIPv6) { + socketFamily = AF_INET6; + } + } + + if (!useIPv6) { sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (isSocketValid() == false) { throwException("Error creating socket"); } - setBlock(false); + socketFamily = AF_INET; } - // sockaddr structure - sockaddr_in addr; - addr.sin_family = AF_INET; - if (this->bindSpecificAddress != "") { - addr.sin_addr.s_addr = inet_addr(this->bindSpecificAddress.c_str()); - } else { - addr.sin_addr.s_addr = INADDR_ANY; - } - addr.sin_port = htons(port); - addr.sin_zero[0] = 0; + setBlock(false); int val = 1; - #ifndef WIN32 int opt_result = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); #else int opt_result = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val)); #endif - int err = ::bind(sock, reinterpret_cast(&addr), sizeof(addr)); + int err = -1; + if (useIPv6) { + sockaddr_in6 addr6; + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_addr = in6addr_any; + addr6.sin6_port = htons(port); + err = ::bind(sock, reinterpret_cast(&addr6), sizeof(addr6)); + } else { + sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + if (this->bindSpecificAddress != "") { + addr.sin_addr.s_addr = inet_addr(this->bindSpecificAddress.c_str()); + } else { + addr.sin_addr.s_addr = INADDR_ANY; + } + addr.sin_port = htons(port); + err = ::bind(sock, reinterpret_cast(&addr), sizeof(addr)); + } + if (err < 0) { char szBuf[8096] = ""; snprintf(szBuf, 8096, @@ -2723,8 +2912,8 @@ void ServerSocket::bind(int port) { portBound = true; if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) - SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s] Line: %d port = %d, portBound = %d END\n", __FILE__, __FUNCTION__, __LINE__, port, - portBound); + SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s] Line: %d port = %d, portBound = %d (IPv6=%d) END\n", __FILE__, __FUNCTION__, __LINE__, + port, portBound, useIPv6 ? 1 : 0); } void ServerSocket::disconnectSocket() { @@ -2794,7 +2983,7 @@ Socket *ServerSocket::accept(bool errorOnFail) { char client_host[100] = ""; // const int max_attempts = 100; // for(int attempt = 0; attempt < max_attempts; ++attempt) { - struct sockaddr_in cli_addr; + struct sockaddr_storage cli_addr; socklen_t clilen = sizeof(cli_addr); client_host[0] = '\0'; MutexSafeWrapper safeMutex(dataSynchAccessorRead, CODE_AT_LINE); @@ -2812,12 +3001,7 @@ Socket *ServerSocket::accept(bool errorOnFail) { int lastSocketError = getLastSocketError(); if (lastSocketError == PLATFORM_SOCKET_TRY_AGAIN) { - // if(attempt+1 >= max_attempts) { - // return NULL; - // } - // else { sleep(0); - //} } if (errorOnFail == true) { throwException(szBuf); @@ -2834,7 +3018,24 @@ Socket *ServerSocket::accept(bool errorOnFail) { } } else { - Ip::Inet_NtoA(SockAddrToUint32((struct sockaddr *)&cli_addr), client_host); + // Extract client address string, supporting both IPv4 and IPv6. + // When the server socket is dual-stack (AF_INET6 + IPV6_V6ONLY=0), + // IPv4 clients appear as IPv4-mapped IPv6 addresses (::ffff:x.x.x.x). + // Strip the prefix so the rest of the code sees a plain IPv4 address. + if (cli_addr.ss_family == AF_INET6) { + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&cli_addr; + if (IN6_IS_ADDR_V4MAPPED(&sa6->sin6_addr)) { + // Extract the embedded IPv4 address + struct in_addr v4addr; + memcpy(&v4addr, sa6->sin6_addr.s6_addr + 12, 4); + inet_ntop(AF_INET, &v4addr, client_host, sizeof(client_host)); + } else { + inet_ntop(AF_INET6, &sa6->sin6_addr, client_host, sizeof(client_host)); + } + } else { + struct sockaddr_in *sa4 = (struct sockaddr_in *)&cli_addr; + inet_ntop(AF_INET, &sa4->sin_addr, client_host, sizeof(client_host)); + } if (SystemFlags::getSystemSettingType(SystemFlags::debugNetwork).enabled) SystemFlags::OutputDebug(SystemFlags::debugNetwork, "In [%s::%s Line: %d] got connection, newSock = " diff --git a/source/tests/CMakeLists.txt b/source/tests/CMakeLists.txt index f8b1c6676..a9f961075 100644 --- a/source/tests/CMakeLists.txt +++ b/source/tests/CMakeLists.txt @@ -72,6 +72,7 @@ IF(BUILD_MEGAGLEST_TESTS) SET(DIRS_WITH_SRC ./ shared_lib/graphics + shared_lib/platform shared_lib/util shared_lib/xml) @@ -205,19 +206,14 @@ IF(BUILD_MEGAGLEST_TESTS) ENDIF() TARGET_LINK_LIBRARIES(${TARGET_NAME} ${EXTERNAL_LIBS}) - IF(NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode") - # Run the unit tests after build - IF(EXISTS ${XVFB_EXEC}) - MESSAGE("***-- Found xvfb-run: ${XVFB_EXEC} will run tests with it.") - - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${XVFB_EXEC} --auto-servernum --server-num=770 ${EXECUTABLE_OUTPUT_PATH}${TARGET_NAME} - COMMENT "***-- Found MegaGlest test runner: ${TARGET_NAME} about to run unit tests via xvfb...") - ELSE() - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${EXECUTABLE_OUTPUT_PATH}${TARGET_NAME} - COMMENT "***-- Found MegaGlest test runner: ${TARGET_NAME} about to run unit tests...") - ENDIF() + # Register with CTest so tests run via `ctest` / `cmake --build . --target test` + IF(EXISTS ${XVFB_EXEC}) + MESSAGE("***-- Found xvfb-run: ${XVFB_EXEC} will run tests with it.") + add_test(NAME megaglest_tests + COMMAND ${XVFB_EXEC} --auto-servernum --server-num=770 ${EXECUTABLE_OUTPUT_PATH}${TARGET_NAME}) + ELSE() + add_test(NAME megaglest_tests + COMMAND ${EXECUTABLE_OUTPUT_PATH}${TARGET_NAME}) ENDIF() ENDIF() diff --git a/source/tests/shared_lib/platform/network_test.cpp b/source/tests/shared_lib/platform/network_test.cpp new file mode 100644 index 000000000..8137eb6fd --- /dev/null +++ b/source/tests/shared_lib/platform/network_test.cpp @@ -0,0 +1,132 @@ +// ============================================================== +// This file is part of MegaGlest Unit Tests (www.megaglest.org) +// +// You can redistribute this code and/or modify it under +// the terms of the GNU General Public License as published +// by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version +// ============================================================== + +#include +#include "socket.h" +#include + +using namespace Shared::Platform; + +// Base port for these tests; chosen to avoid well-known services. +static const int TEST_PORT_BASE = 34527; + +// Poll server.accept() for up to ~100 ms (50 × 2 ms sleeps). +static Socket *pollAccept(ServerSocket &server, int tries = 50) { + for (int i = 0; i < tries; ++i) { + Socket *s = server.accept(false); + if (s != nullptr) return s; + usleep(2000); + } + return nullptr; +} + +// Returns true if this platform supports IPv6 dual-stack: an AF_INET6 socket +// can be created and IPV6_V6ONLY can be cleared. ServerSocket::bind() already +// does this probe and falls back to IPv4 silently, so we just check which +// family was actually chosen. +static bool platformHasIPv6DualStack() { + ServerSocket probe; + probe.bind(TEST_PORT_BASE + 9); + return probe.getSocketFamily() == AF_INET6; +} + +class NetworkTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(NetworkTest); + CPPUNIT_TEST(test_server_bind); + CPPUNIT_TEST(test_ipv4_client_connect); + CPPUNIT_TEST(test_ipv4_client_address_on_accept); + CPPUNIT_TEST(test_ipv6_client_connect); + CPPUNIT_TEST(test_ipv6_client_address_on_accept); + CPPUNIT_TEST_SUITE_END(); + + public: + // Server binds on a port and reports the port as bound + void test_server_bind() { + ServerSocket server; + server.bind(TEST_PORT_BASE); + server.listen(1); + CPPUNIT_ASSERT(server.isPortBound()); + CPPUNIT_ASSERT(server.isSocketValid()); + } + + // An IPv4 client can connect to the listening server + void test_ipv4_client_connect() { + ServerSocket server; + server.bind(TEST_PORT_BASE + 1); + server.listen(1); + + ClientSocket client; + client.connect(Ip("127.0.0.1"), TEST_PORT_BASE + 1); + + Socket *accepted = pollAccept(server); + CPPUNIT_ASSERT_MESSAGE("IPv4 connection was not accepted", accepted != nullptr); + CPPUNIT_ASSERT(accepted->isSocketValid()); + delete accepted; + } + + // When an IPv4 client connects to a dual-stack server the accepted socket's + // IP is a plain IPv4 string, not the mapped form "::ffff:127.0.0.1" + void test_ipv4_client_address_on_accept() { + ServerSocket server; + server.bind(TEST_PORT_BASE + 2); + server.listen(1); + + ClientSocket client; + client.connect(Ip("127.0.0.1"), TEST_PORT_BASE + 2); + + Socket *accepted = pollAccept(server); + CPPUNIT_ASSERT_MESSAGE("IPv4 connection was not accepted", accepted != nullptr); + CPPUNIT_ASSERT_EQUAL(std::string("127.0.0.1"), accepted->getIpAddress()); + delete accepted; + } + + // An IPv6 client can connect to the dual-stack server. + // Skipped on platforms without IPv6 support. + void test_ipv6_client_connect() { + if (!platformHasIPv6DualStack()) { + std::cout << " [skipped: no IPv6 dual-stack] "; + return; + } + + ServerSocket server; + server.bind(TEST_PORT_BASE + 3); + server.listen(1); + + ClientSocket client; + client.connect(Ip("::1"), TEST_PORT_BASE + 3); + + Socket *accepted = pollAccept(server); + CPPUNIT_ASSERT_MESSAGE("IPv6 connection was not accepted", accepted != nullptr); + CPPUNIT_ASSERT(accepted->isSocketValid()); + delete accepted; + } + + // When an IPv6 client connects, the accepted socket reports the IPv6 + // address of the client + void test_ipv6_client_address_on_accept() { + if (!platformHasIPv6DualStack()) { + std::cout << " [skipped: no IPv6 dual-stack] "; + return; + } + + ServerSocket server; + server.bind(TEST_PORT_BASE + 4); + server.listen(1); + + ClientSocket client; + client.connect(Ip("::1"), TEST_PORT_BASE + 4); + + Socket *accepted = pollAccept(server); + CPPUNIT_ASSERT_MESSAGE("IPv6 connection was not accepted", accepted != nullptr); + CPPUNIT_ASSERT_EQUAL(std::string("::1"), accepted->getIpAddress()); + delete accepted; + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(NetworkTest); diff --git a/source/tests/shared_lib/platform/socket_test.cpp b/source/tests/shared_lib/platform/socket_test.cpp new file mode 100644 index 000000000..0de0002ba --- /dev/null +++ b/source/tests/shared_lib/platform/socket_test.cpp @@ -0,0 +1,204 @@ +// ============================================================== +// This file is part of MegaGlest Unit Tests (www.megaglest.org) +// +// You can redistribute this code and/or modify it under +// the terms of the GNU General Public License as published +// by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version +// ============================================================== + +#include +#include "socket.h" + +using namespace Shared::Platform; + +class SocketTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(SocketTest); + + CPPUNIT_TEST(test_ip_ipv4_string); + CPPUNIT_TEST(test_ip_ipv4_cursor_stripped); + CPPUNIT_TEST(test_ip_ipv4_bytes); + CPPUNIT_TEST(test_ip_ipv6_string); + CPPUNIT_TEST(test_ip_ipv6_cursor_stripped); + CPPUNIT_TEST(test_ip_empty_string); + + CPPUNIT_TEST(test_parseHostPort_ipv4_with_port); + CPPUNIT_TEST(test_parseHostPort_ipv4_no_port); + CPPUNIT_TEST(test_parseHostPort_hostname_with_port); + CPPUNIT_TEST(test_parseHostPort_ipv6_bracket_with_port); + CPPUNIT_TEST(test_parseHostPort_ipv6_bracket_no_port); + CPPUNIT_TEST(test_parseHostPort_ipv6_bare_no_port_extracted); + CPPUNIT_TEST(test_parseHostPort_cursor_stripped); + CPPUNIT_TEST(test_parseHostPort_ipv6_bracket_cursor_stripped); + + CPPUNIT_TEST(test_buildHostDisplay_ipv4_with_port); + CPPUNIT_TEST(test_buildHostDisplay_ipv4_no_port); + CPPUNIT_TEST(test_buildHostDisplay_ipv6_with_port); + CPPUNIT_TEST(test_buildHostDisplay_ipv6_no_port); + CPPUNIT_TEST(test_buildHostDisplay_hostname_with_port); + CPPUNIT_TEST(test_buildHostDisplay_roundtrip_ipv6); + CPPUNIT_TEST(test_buildHostDisplay_roundtrip_ipv4); + + CPPUNIT_TEST_SUITE_END(); + + public: + // Plain IPv4 address round-trips through the string constructor + void test_ip_ipv4_string() { + Ip ip("127.0.0.1"); + CPPUNIT_ASSERT_EQUAL(std::string("127.0.0.1"), ip.getString()); + } + + // The UI appends '_' as a cursor — it must be stripped + void test_ip_ipv4_cursor_stripped() { + Ip ip("192.168.1.100_"); + CPPUNIT_ASSERT_EQUAL(std::string("192.168.1.100"), ip.getString()); + } + + // Byte constructor still works (used by legacy code) + void test_ip_ipv4_bytes() { + Ip ip(10, 0, 0, 1); + CPPUNIT_ASSERT_EQUAL(std::string("10.0.0.1"), ip.getString()); + } + + // IPv6 loopback address is stored and returned unchanged + void test_ip_ipv6_string() { + Ip ip("::1"); + CPPUNIT_ASSERT_EQUAL(std::string("::1"), ip.getString()); + } + + // UI cursor character is stripped from IPv6 addresses too + void test_ip_ipv6_cursor_stripped() { + Ip ip("::1_"); + CPPUNIT_ASSERT_EQUAL(std::string("::1"), ip.getString()); + } + + // Empty string gives the zero address + void test_ip_empty_string() { + Ip ip(""); + CPPUNIT_ASSERT_EQUAL(std::string("0.0.0.0"), ip.getString()); + } + + // --- parseHostPort --- + + // IPv4 address with port: extracts both + void test_parseHostPort_ipv4_with_port() { + std::string host = "192.168.1.1:6234"; + int port = 0; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("192.168.1.1"), host); + CPPUNIT_ASSERT_EQUAL(6234, port); + } + + // IPv4 address without port: host unchanged, port unchanged + void test_parseHostPort_ipv4_no_port() { + std::string host = "192.168.1.1"; + int port = 9999; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("192.168.1.1"), host); + CPPUNIT_ASSERT_EQUAL(9999, port); + } + + // Hostname with port + void test_parseHostPort_hostname_with_port() { + std::string host = "example.com:61367"; + int port = 0; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("example.com"), host); + CPPUNIT_ASSERT_EQUAL(61367, port); + } + + // IPv6 in bracket notation with port: the crash case + void test_parseHostPort_ipv6_bracket_with_port() { + std::string host = "[2a04:1c43:31da:0:7bb0:7ebc:5759:9a6b]:61367"; + int port = 0; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("2a04:1c43:31da:0:7bb0:7ebc:5759:9a6b"), host); + CPPUNIT_ASSERT_EQUAL(61367, port); + } + + // IPv6 in bracket notation without port + void test_parseHostPort_ipv6_bracket_no_port() { + std::string host = "[::1]"; + int port = 9999; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("::1"), host); + CPPUNIT_ASSERT_EQUAL(9999, port); + } + + // Bare IPv6 (no brackets): host is left intact, port is not changed. + // This is the exact input that caused the strToInt crash on "1c43". + void test_parseHostPort_ipv6_bare_no_port_extracted() { + std::string host = "2a04:1c43:31da:0:7bb0:7ebc:5759:9a6b"; + int port = 9999; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("2a04:1c43:31da:0:7bb0:7ebc:5759:9a6b"), host); + CPPUNIT_ASSERT_EQUAL(9999, port); + } + + // UI cursor '_' is stripped before parsing + void test_parseHostPort_cursor_stripped() { + std::string host = "192.168.0.5:6234_"; + int port = 0; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("192.168.0.5"), host); + CPPUNIT_ASSERT_EQUAL(6234, port); + } + + // UI cursor stripped from bracketed IPv6 label text + void test_parseHostPort_ipv6_bracket_cursor_stripped() { + std::string host = "[::1]:61367_"; + int port = 0; + Ip::parseHostPort(host, port); + CPPUNIT_ASSERT_EQUAL(std::string("::1"), host); + CPPUNIT_ASSERT_EQUAL(61367, port); + } + + // --- buildHostDisplay --- + + // IPv4 with positive port: plain "host:port" + void test_buildHostDisplay_ipv4_with_port() { CPPUNIT_ASSERT_EQUAL(std::string("192.168.1.1:6234"), Ip::buildHostDisplay("192.168.1.1", 6234)); } + + // IPv4 with port <= 0: host returned as-is + void test_buildHostDisplay_ipv4_no_port() { CPPUNIT_ASSERT_EQUAL(std::string("192.168.1.1"), Ip::buildHostDisplay("192.168.1.1", 0)); } + + // IPv6 with port: bracket notation "[addr]:port" + void test_buildHostDisplay_ipv6_with_port() { + CPPUNIT_ASSERT_EQUAL(std::string("[2a04:1c43:31da:0:7bb0:7ebc:5759:9a6b]:61367"), Ip::buildHostDisplay("2a04:1c43:31da:0:7bb0:7ebc:5759:9a6b", 61367)); + } + + // IPv6 with port <= 0: host returned as-is (no brackets added) + void test_buildHostDisplay_ipv6_no_port() { CPPUNIT_ASSERT_EQUAL(std::string("::1"), Ip::buildHostDisplay("::1", 0)); } + + // Hostname with port: plain "host:port" + void test_buildHostDisplay_hostname_with_port() { CPPUNIT_ASSERT_EQUAL(std::string("example.com:6234"), Ip::buildHostDisplay("example.com", 6234)); } + + // buildHostDisplay output fed back into parseHostPort recovers the same + // host and port — this is the round-trip that connectToServer() relies on + void test_buildHostDisplay_roundtrip_ipv6() { + const std::string addr = "2a04:1c43:31da:0:7bb0:7ebc:5759:9a6b"; + const int origPort = 61367; + std::string display = Ip::buildHostDisplay(addr, origPort) + '_'; + + std::string host = display; + int port = 9999; + Ip::parseHostPort(host, port); + + CPPUNIT_ASSERT_EQUAL(addr, host); + CPPUNIT_ASSERT_EQUAL(origPort, port); + } + + void test_buildHostDisplay_roundtrip_ipv4() { + const std::string addr = "192.168.1.1"; + const int origPort = 6234; + std::string display = Ip::buildHostDisplay(addr, origPort) + '_'; + + std::string host = display; + int port = 9999; + Ip::parseHostPort(host, port); + + CPPUNIT_ASSERT_EQUAL(addr, host); + CPPUNIT_ASSERT_EQUAL(origPort, port); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(SocketTest); diff --git a/source/tests/test_runner.cpp b/source/tests/test_runner.cpp index 4a11c1e00..94df47158 100644 --- a/source/tests/test_runner.cpp +++ b/source/tests/test_runner.cpp @@ -23,8 +23,11 @@ int main(int argc, char *argv[]) { // Change the default outputter to a compiler error format outputter runner.setOutputter(new CppUnit::CompilerOutputter(&runner.result(), std::cerr)); - // Run the tests. - bool wasSucessful = runner.run(); + // Run the tests. An optional command-line argument selects a specific + // suite or test by path, e.g. "SocketTest" or + // "SocketTest::test_ip_ipv6_cursor_stripped". + std::string testPath = (argc > 1) ? argv[1] : ""; + bool wasSucessful = runner.run(testPath); // Return error code 1 if the one of test failed. return wasSucessful ? 0 : 1;