diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index b7b433e..20c00eb 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -18,6 +18,12 @@ set(SRC_FILES args.cpp application.cpp ) +set(TEST_HEADER_FILES +) +set(TEST_SRC_FILES + tests/cppunit.cpp + tests/application.cpp +) # find c++utilities find_package(c++utilities 4.14.0 REQUIRED) @@ -31,6 +37,15 @@ list(APPEND CMAKE_MODULE_PATH ${QT_UTILITIES_MODULE_DIRS}) find_package(syncthingconnector ${META_APP_VERSION} REQUIRED) use_syncthingconnector() +find_package(syncthingtesthelper ${META_APP_VERSION} REQUIRED) +if(SYNCTHINGTESTHELPER_HAS_SHARED_LIB) + list(APPEND TEST_LIBRARIES syncthingtesthelper) +elseif(SYNCTHINGTESTHELPER_HAS_STATIC_LIB) + list(APPEND TEST_LIBRARIES syncthingtesthelper_static) +else() + message(WARNING "Unable to build tests. Testhelper not found.") +endif() + # include modules to apply configuration include(BasicConfig) include(JsProviderConfig) @@ -42,6 +57,9 @@ endif() include(QtConfig) include(WindowsResources) include(AppTarget) +if(SYNCTHINGTESTHELPER_HAS_SHARED_LIB OR SYNCTHINGTESTHELPER_HAS_STATIC_LIB) + include(TestTarget) +endif() include(ShellCompletion) include(Doxygen) include(ConfigHeader) diff --git a/cli/testfiles/expected-status.txt b/cli/testfiles/expected-status.txt new file mode 100644 index 0000000..e81345e --- /dev/null +++ b/cli/testfiles/expected-status.txt @@ -0,0 +1,44 @@ +Directories + - test2 + Label Test dir 2 + Path /tmp/some/path/2/ + Status paused + Global 0 file\(s\), 0 dir\(s\), 0 bytes + Local 0 file\(s\), 0 dir\(s\), 0 bytes + Shared with Test dev 2 + Read-only no + Ignore permissions no + Auto-normalize yes + Rescan interval 365 d + + - test1 + Path /tmp/some/path/1/ + Status (idle|scanning) + Global 0 file\(s\), 0 dir\(s\), 0 bytes + Local 0 file\(s\), 0 dir\(s\), 0 bytes + Last scan time \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d.* + Shared with Test dev 2 + Test dev 1 + Read-only no + Ignore permissions no + Auto-normalize yes + Rescan interval 2 h + +Devices + - Test dev 1 + ID 6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4 + Status disconnected + Addresses dynamic + Compression metadata + + - Test dev 2 + ID MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7 + Status paused + Addresses tcp://.*22000 + Compression metadata + + - martchus-arch + ID \w\w\w\w\w\w\w-\w\w\w\w\w\w\w-\w\w\w\w\w\w\w-\w\w\w\w\w\w\w-\w\w\w\w\w\w\w-\w\w\w\w\w\w\w-\w\w\w\w\w\w\w-\w\w\w\w\w\w\w + Status own device + Addresses dynamic + Compression metadata diff --git a/cli/testfiles/testconfig b/cli/testfiles/testconfig new file mode 120000 index 0000000..fbe1030 --- /dev/null +++ b/cli/testfiles/testconfig @@ -0,0 +1 @@ +../../connector/testfiles/testconfig \ No newline at end of file diff --git a/cli/tests/application.cpp b/cli/tests/application.cpp new file mode 100644 index 0000000..61b7ace --- /dev/null +++ b/cli/tests/application.cpp @@ -0,0 +1,212 @@ +#include "../../testhelper/syncthingtestinstance.h" + +#include +#include + +#include + +#include +#include +#include + +#include + +#include +#include + +using namespace std; +using namespace Data; +using namespace ChronoUtilities; +using namespace IoUtilities; +using namespace TestUtilities; +using namespace TestUtilities::Literals; + +using namespace CPPUNIT_NS; + +/*! + * \brief The ApplicationTests class tests the overall CLI application. + */ +class ApplicationTests : public TestFixture, private SyncthingTestInstance { + CPPUNIT_TEST_SUITE(ApplicationTests); +#ifdef PLATFORM_UNIX + CPPUNIT_TEST(test); +#endif + CPPUNIT_TEST_SUITE_END(); + +public: + ApplicationTests(); + + void test(); + + void setUp(); + void tearDown(); + +private: + DateTime m_startTime; +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(ApplicationTests); + +ApplicationTests::ApplicationTests() +{ +} + +/*! + * \brief Ensures Syncthing dirs are empty and starts Syncthing. + */ +void ApplicationTests::setUp() +{ + remove("/tmp/some/path/1/new-file.txt"); + remove("/tmp/some/path/1/newdir/yet-another-file.txt"); + remove("/tmp/some/path/1/newdir/default.profraw"); + rmdir("/tmp/some/path/1/newdir"); + + SyncthingTestInstance::start(); + m_startTime = DateTime::gmtNow(); +} + +/*! + * \brief Terminates Syncthing and prints stdout/stderr from Syncthing. + */ +void ApplicationTests::tearDown() +{ + SyncthingTestInstance::stop(); +} + +#ifdef PLATFORM_UNIX +/*! + * \brief Tests the overall CLI application. + */ +void ApplicationTests::test() +{ + // prepare executing syncthingctl + string stdout, stderr; + const auto apiKey(this->apiKey().toLocal8Bit()); + const auto url(argsToString("http://localhost:", syncthingPort().toInt())); + + // disable colorful output + setenv("ENABLE_ESCAPE_CODES", "0", true); + + // load expected status + const regex expectedStatus(readFile(testFilePath("expected-status.txt"), 2048)); + + // save cwd (to restore later) + char cwd[1024]; + const bool hasCwd(getcwd(cwd, sizeof(cwd))); + + // wait till Syncthing GUI becomes available + cerr << "\nWaiting till Syncthing GUI becomes available ..."; + QByteArray syncthingOutput; + do { + // wait for output + if (!syncthingProcess().bytesAvailable()) { + // fail when already waiting for over 15 seconds + const auto waitingTime(DateTime::gmtNow() - m_startTime); + if (waitingTime.seconds() > 15) { + CPPUNIT_FAIL("Syncthing needs longer than 15 seconds to become available."); + } + syncthingProcess().waitForReadyRead(15000 - waitingTime.milliseconds()); + } + syncthingOutput.append(syncthingProcess().readAll()); + } while (!syncthingOutput.contains("Access the GUI via the following URL")); + + // test status for all dirs and devs + const char *const statusArgs[] = { "syncthingctl", "status", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(statusArgs); + cout << stdout; + CPPUNIT_ASSERT(regex_search(stdout, expectedStatus)); + + // test log + const char *const logArgs[] = { "syncthingctl", "log", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(logArgs); + cout << stdout; + CPPUNIT_ASSERT(stdout.find("syncthing v") != string::npos); + CPPUNIT_ASSERT(stdout.find("My ID") != string::npos); + CPPUNIT_ASSERT(stdout.find("Startup complete") != string::npos); + CPPUNIT_ASSERT(stdout.find("Access the GUI via the following URL") != string::npos); + + // test resume, verify via status for dirs only + const char *const resumeArgs[] = { "syncthingctl", "resume", "--dir", "test2", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + const char *const statusDirsOnlyArgs[] = { "syncthingctl", "status", "--all-dirs", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(resumeArgs); + TESTUTILS_ASSERT_EXEC(statusDirsOnlyArgs); + CPPUNIT_ASSERT(stdout.find("test2") != string::npos); + CPPUNIT_ASSERT(stdout.find("paused") == string::npos); + + // test pause, verify via status on specific dir + const char *const pauseArgs[] = { "syncthingctl", "pause", "--dir", "test2", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + const char *const statusTest2Args[] = { "syncthingctl", "status", "--dir", "test2", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(pauseArgs); + TESTUTILS_ASSERT_EXEC(statusTest2Args); + CPPUNIT_ASSERT(stdout.find("test1") == string::npos); + CPPUNIT_ASSERT(stdout.find("test2") != string::npos); + CPPUNIT_ASSERT(stdout.find("paused") != string::npos); + + // test cat + const char *const catArgs[] = { "syncthingctl", "cat", nullptr }; + TESTUTILS_ASSERT_EXEC(catArgs); + cout << stdout; + QJsonParseError error; + const auto doc(QJsonDocument::fromJson(QByteArray(stdout.data(), stdout.size()), &error)); + CPPUNIT_ASSERT_EQUAL(QJsonParseError::NoError, error.error); + const auto object(doc.object()); + CPPUNIT_ASSERT(object.value(QLatin1String("options")).isObject()); + CPPUNIT_ASSERT(object.value(QLatin1String("devices")).isArray()); + CPPUNIT_ASSERT(object.value(QLatin1String("folders")).isArray()); + + // test edit +#if defined(SYNCTHINGCTL_USE_JSENGINE) || defined(SYNCTHINGCTL_USE_SCRIPT) + const char *const editArgs[] = { "syncthingctl", "edit", "--js-lines", "assignIfPresent(findFolder('test1'), 'rescanIntervalS', 0);", "--api-key", + apiKey.data(), "--url", url.data(), nullptr }; + const char *const statusTest1Args[] = { "syncthingctl", "status", "--dir", "test1", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(editArgs); + cout << stdout; + TESTUTILS_ASSERT_EXEC(statusTest1Args); + cout << stdout; + CPPUNIT_ASSERT(stdout.find("test1") != string::npos); + CPPUNIT_ASSERT(stdout.find("test2") == string::npos); + CPPUNIT_ASSERT(stdout.find("Rescan interval") == string::npos); +#endif + + // test rescan: create new file, trigger rescan, check status + CPPUNIT_ASSERT(ofstream("/tmp/some/path/1/new-file.txt") << "foo"); + const char *const rescanArgs[] = { "syncthingctl", "rescan", "test1", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(rescanArgs); + cout << stdout; + TESTUTILS_ASSERT_EXEC(statusTest1Args); + cout << stdout; + CPPUNIT_ASSERT(stdout.find("test1") != string::npos); + CPPUNIT_ASSERT(stdout.find("Local 1 file(s), 0 dir(s), 3 bytes") != string::npos); + + // test pwd + // -> create and enter new dir, also create a 2nd file in it + chdir("/tmp/some/path/1"); + mkdir("newdir", S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + chdir("newdir"); + CPPUNIT_ASSERT(ofstream("yet-another-file.txt") << "bar"); + // -> change LLVM_PROFILE_FILE to prevent default.profraw file being created in the new directory + const char *const llvmProfileFile(getenv("LLVM_PROFILE_FILE")); + setenv("LLVM_PROFILE_FILE", "/tmp/syncthingctl-%p.profraw", true); + // -> do actual test + const char *const pwdRescanArgs[] = { "syncthingctl", "pwd", "rescan", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(pwdRescanArgs); + cout << stdout; + // -> restore LLVM_PROFILE_FILE + if (llvmProfileFile) { + setenv("LLVM_PROFILE_FILE", llvmProfileFile, true); + } else { + unsetenv("LLVM_PROFILE_FILE"); + } + // -> verify result + const char *const pwdStatusArgs[] = { "syncthingctl", "pwd", "status", "--api-key", apiKey.data(), "--url", url.data(), nullptr }; + TESTUTILS_ASSERT_EXEC(pwdStatusArgs); + cout << stdout; + CPPUNIT_ASSERT(stdout.find("test1") != string::npos); + CPPUNIT_ASSERT(stdout.find("Local 2 file(s), 1 dir(s)") != string::npos); + + // switch back to initial working dir + if (hasCwd) { + chdir(cwd); + } +} +#endif diff --git a/cli/tests/cppunit.cpp b/cli/tests/cppunit.cpp new file mode 100644 index 0000000..67aaee6 --- /dev/null +++ b/cli/tests/cppunit.cpp @@ -0,0 +1 @@ +#include diff --git a/connector/tests/connectiontests.cpp b/connector/tests/connectiontests.cpp index d71be49..71781e0 100644 --- a/connector/tests/connectiontests.cpp +++ b/connector/tests/connectiontests.cpp @@ -1,8 +1,8 @@ #include "../syncthingconnection.h" #include "../syncthingconnectionsettings.h" -#include "../testhelper/helper.h" -#include "../testhelper/syncthingtestinstance.h" +#include "../../testhelper/helper.h" +#include "../../testhelper/syncthingtestinstance.h" #include diff --git a/connector/tests/misctests.cpp b/connector/tests/misctests.cpp index 8a114a9..0b35a2b 100644 --- a/connector/tests/misctests.cpp +++ b/connector/tests/misctests.cpp @@ -10,7 +10,7 @@ #include #include -#include "../testhelper/helper.h" +#include "../../testhelper/helper.h" #include diff --git a/testhelper/syncthingtestinstance.cpp b/testhelper/syncthingtestinstance.cpp index 277862b..76bb2ab 100644 --- a/testhelper/syncthingtestinstance.cpp +++ b/testhelper/syncthingtestinstance.cpp @@ -71,14 +71,16 @@ void SyncthingTestInstance::start() // start st cerr << "\n - Launching Syncthing ..." << endl; - QStringList args; - args.reserve(2); - args << QStringLiteral("-gui-address=http://localhost:") + m_syncthingPort; - args << QStringLiteral("-gui-apikey=") + m_apiKey; - args << QStringLiteral("-home=") + configFile.absolutePath(); - args << QStringLiteral("-no-browser"); - args << QStringLiteral("-verbose"); + // clang-format off + const QStringList args{ + QStringLiteral("-gui-address=http://localhost:") + m_syncthingPort, + QStringLiteral("-gui-apikey=") + m_apiKey, + QStringLiteral("-home=") + configFile.absolutePath(), + QStringLiteral("-no-browser"), + QStringLiteral("-verbose"), + }; m_syncthingProcess.start(syncthingPath, args); + // clang-format on } /*!