2023-08-02 17:35:41 +02:00
|
|
|
#include "../../testhelper/helper.h"
|
2018-05-01 01:10:46 +02:00
|
|
|
#include "../../testhelper/syncthingtestinstance.h"
|
|
|
|
|
|
|
|
#include <c++utilities/io/misc.h>
|
|
|
|
#include <c++utilities/tests/testutils.h>
|
|
|
|
|
|
|
|
#include <cppunit/TestFixture.h>
|
|
|
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
#include <QJsonObject>
|
|
|
|
#include <QJsonValue>
|
|
|
|
|
2023-02-11 15:55:45 +01:00
|
|
|
#include <filesystem>
|
2018-05-01 01:10:46 +02:00
|
|
|
#include <regex>
|
|
|
|
|
2023-08-19 19:15:09 +02:00
|
|
|
#ifdef stdout
|
|
|
|
#undef stdout
|
|
|
|
#endif
|
|
|
|
#ifdef stderr
|
|
|
|
#undef stderr
|
|
|
|
#endif
|
|
|
|
|
2018-05-01 01:10:46 +02:00
|
|
|
using namespace std;
|
|
|
|
using namespace Data;
|
2019-06-10 22:48:26 +02:00
|
|
|
using namespace CppUtilities;
|
|
|
|
using namespace CppUtilities::Literals;
|
2018-05-01 01:10:46 +02:00
|
|
|
|
|
|
|
using namespace CPPUNIT_NS;
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief The ApplicationTests class tests the overall CLI application.
|
|
|
|
*/
|
|
|
|
class ApplicationTests : public TestFixture, private SyncthingTestInstance {
|
|
|
|
CPPUNIT_TEST_SUITE(ApplicationTests);
|
2023-08-19 19:15:09 +02:00
|
|
|
#if defined(PLATFORM_UNIX) || defined(CPP_UTILITIES_HAS_EXEC_APP)
|
2018-05-01 01:10:46 +02:00
|
|
|
CPPUNIT_TEST(test);
|
|
|
|
#endif
|
|
|
|
CPPUNIT_TEST_SUITE_END();
|
|
|
|
|
|
|
|
public:
|
|
|
|
ApplicationTests();
|
|
|
|
|
|
|
|
void test();
|
|
|
|
|
2019-10-13 18:28:32 +02:00
|
|
|
void setUp() override;
|
|
|
|
void tearDown() override;
|
2018-05-01 01:10:46 +02:00
|
|
|
|
|
|
|
private:
|
|
|
|
DateTime m_startTime;
|
2023-02-11 15:55:45 +01:00
|
|
|
std::error_code m_ecCwd;
|
|
|
|
std::filesystem::path m_initialCwd;
|
|
|
|
std::filesystem::path m_tempPath;
|
2018-05-01 01:10:46 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
CPPUNIT_TEST_SUITE_REGISTRATION(ApplicationTests);
|
|
|
|
|
|
|
|
ApplicationTests::ApplicationTests()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Ensures Syncthing dirs are empty and starts Syncthing.
|
|
|
|
*/
|
|
|
|
void ApplicationTests::setUp()
|
|
|
|
{
|
2023-02-11 15:55:45 +01:00
|
|
|
auto ec = std::error_code();
|
|
|
|
std::filesystem::remove_all((m_tempPath = std::filesystem::temp_directory_path()) / "some/path/1", ec);
|
|
|
|
if (ec && ec != std::errc::no_such_file_or_directory) {
|
|
|
|
CPPUNIT_FAIL(argsToString("Unable to clean-up temporary directory \"", m_tempPath.string(), "/some/path/1\": ", ec.message()));
|
|
|
|
}
|
2018-05-01 01:10:46 +02:00
|
|
|
|
|
|
|
SyncthingTestInstance::start();
|
2023-02-11 15:55:45 +01:00
|
|
|
m_initialCwd = std::filesystem::current_path(m_ecCwd);
|
2018-05-01 01:10:46 +02:00
|
|
|
m_startTime = DateTime::gmtNow();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* \brief Terminates Syncthing and prints stdout/stderr from Syncthing.
|
|
|
|
*/
|
|
|
|
void ApplicationTests::tearDown()
|
|
|
|
{
|
2023-02-11 15:55:45 +01:00
|
|
|
// restore initial cwd
|
|
|
|
if (!m_initialCwd.empty() && !m_ecCwd) {
|
|
|
|
std::filesystem::current_path(m_initialCwd);
|
|
|
|
}
|
2018-05-01 01:10:46 +02:00
|
|
|
SyncthingTestInstance::stop();
|
|
|
|
}
|
|
|
|
|
2023-08-19 19:15:09 +02:00
|
|
|
#if defined(PLATFORM_UNIX) || defined(CPP_UTILITIES_HAS_EXEC_APP)
|
2018-05-01 01:10:46 +02:00
|
|
|
/*!
|
|
|
|
* \brief Tests the overall CLI application.
|
2018-10-20 23:48:24 +02:00
|
|
|
* \remarks Tests for rescan are currently disabled for release mode because they sometimes fail.
|
2018-05-01 01:10:46 +02:00
|
|
|
*/
|
|
|
|
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
|
2023-08-19 19:15:09 +02:00
|
|
|
qputenv("ENABLE_ESCAPE_CODES", "0");
|
2018-05-01 01:10:46 +02:00
|
|
|
|
|
|
|
// load expected status
|
2023-08-02 17:35:41 +02:00
|
|
|
const auto expectedStatusData = [] {
|
|
|
|
auto data = readFile(testFilePath("expected-status.txt"), 4000);
|
|
|
|
findAndReplace(data, "/tmp/", tempDirectory());
|
|
|
|
return data;
|
|
|
|
}();
|
|
|
|
const auto expectedStatusLines = splitString<vector<string>>(expectedStatusData, "\n");
|
|
|
|
const auto expectedStatusPatterns = [&] {
|
|
|
|
auto regex = std::vector<std::regex>();
|
|
|
|
regex.reserve(expectedStatusLines.size());
|
|
|
|
for (const auto &line : expectedStatusLines) {
|
|
|
|
regex.emplace_back(line);
|
|
|
|
}
|
|
|
|
CPPUNIT_ASSERT(!regex.empty());
|
|
|
|
return regex;
|
|
|
|
}();
|
2018-05-01 01:10:46 +02:00
|
|
|
|
|
|
|
// wait till Syncthing GUI becomes available
|
2018-10-25 18:22:56 +02:00
|
|
|
{
|
2021-07-01 17:25:45 +02:00
|
|
|
cerr << "\nWaiting till Syncthing GUI becomes available ...\n";
|
2018-10-25 18:22:56 +02:00
|
|
|
QByteArray syncthingOutput;
|
2019-01-13 23:39:37 +01:00
|
|
|
constexpr auto syncthingCheckInterval = TimeSpan::fromMilliseconds(200.0);
|
|
|
|
const auto maxSyncthingStartupTime = TimeSpan::fromSeconds(15.0 * max(timeoutFactor, 5.0));
|
|
|
|
auto remainingTimeForSyncthingToComeUp = maxSyncthingStartupTime;
|
2018-10-25 18:22:56 +02:00
|
|
|
do {
|
|
|
|
// wait for output
|
|
|
|
if (!syncthingProcess().bytesAvailable()) {
|
2019-01-13 23:39:37 +01:00
|
|
|
// consider test failed if Syncthing takes too long to come up (or we fail to connect)
|
|
|
|
if ((remainingTimeForSyncthingToComeUp -= syncthingCheckInterval).isNegative()) {
|
|
|
|
CPPUNIT_FAIL(
|
|
|
|
argsToString("unable to connect to Syncthing within ", maxSyncthingStartupTime.toString(TimeSpanOutputFormat::WithMeasures)));
|
2018-10-25 18:22:56 +02:00
|
|
|
}
|
2019-01-13 23:39:37 +01:00
|
|
|
syncthingProcess().waitForReadyRead(static_cast<int>(syncthingCheckInterval.totalMilliseconds()));
|
2018-05-01 01:10:46 +02:00
|
|
|
}
|
2021-06-17 20:32:25 +02:00
|
|
|
const auto newOutput = syncthingProcess().readAll();
|
|
|
|
clog.write(newOutput.data(), newOutput.size());
|
|
|
|
syncthingOutput.append(newOutput);
|
2018-10-25 18:22:56 +02:00
|
|
|
} while (!syncthingOutput.contains("Access the GUI via the following URL"));
|
2018-05-01 01:10:46 +02:00
|
|
|
|
2018-10-25 18:22:56 +02:00
|
|
|
setInterleavedOutputEnabledFromEnv();
|
|
|
|
cout.flush();
|
|
|
|
}
|
2018-10-20 23:48:24 +02:00
|
|
|
|
2018-05-01 01:10:46 +02:00
|
|
|
// test status for all dirs and devs
|
2018-10-20 23:48:24 +02:00
|
|
|
const char *const statusArgs[] = { "syncthingctl", "status", "--api-key", apiKey.data(), "--url", url.data(), "--no-color", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(statusArgs);
|
2018-10-25 18:22:56 +02:00
|
|
|
cout << stderr;
|
2018-05-01 01:10:46 +02:00
|
|
|
cout << stdout;
|
2018-10-20 23:48:24 +02:00
|
|
|
const auto statusLines(splitString(stdout, "\n"));
|
|
|
|
auto currentStatusLine = statusLines.cbegin();
|
|
|
|
auto currentPattern = 0_st;
|
|
|
|
for (const auto &expectedPattern : expectedStatusPatterns) {
|
|
|
|
bool patternFound = false;
|
|
|
|
while (currentStatusLine != statusLines.cend()) {
|
|
|
|
patternFound = regex_search(*currentStatusLine, expectedPattern);
|
|
|
|
++currentStatusLine;
|
|
|
|
if (patternFound) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!patternFound) {
|
|
|
|
CPPUNIT_FAIL(argsToString("Line ", expectedStatusLines[currentPattern], " could not be found in output."));
|
|
|
|
}
|
|
|
|
++currentPattern;
|
2018-05-01 01:32:27 +02:00
|
|
|
}
|
2018-05-01 01:10:46 +02:00
|
|
|
|
|
|
|
// test log
|
|
|
|
const char *const logArgs[] = { "syncthingctl", "log", "--api-key", apiKey.data(), "--url", url.data(), nullptr };
|
|
|
|
TESTUTILS_ASSERT_EXEC(logArgs);
|
|
|
|
cout << stdout;
|
2019-08-08 22:36:47 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find("My ID") != string::npos || stdout.find("My name") != string::npos);
|
2018-05-01 01:10:46 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find("Startup complete") != string::npos);
|
|
|
|
CPPUNIT_ASSERT(stdout.find("Access the GUI via the following URL") != string::npos);
|
|
|
|
|
2018-05-01 01:32:27 +02:00
|
|
|
// use environment variables to specify API-key and URL
|
2023-08-19 19:15:09 +02:00
|
|
|
qputenv("SYNCTHING_CTL_API_KEY", apiKey);
|
2024-02-16 19:17:51 +01:00
|
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
2023-08-19 19:15:09 +02:00
|
|
|
qputenv("SYNCTHING_CTL_URL", url);
|
2023-09-01 21:51:58 +02:00
|
|
|
#else
|
|
|
|
qputenv("SYNCTHING_CTL_URL", QByteArray(url.data(), static_cast<int>(url.size())));
|
|
|
|
#endif
|
2018-05-01 01:32:27 +02:00
|
|
|
|
2018-05-01 01:10:46 +02:00
|
|
|
// test resume, verify via status for dirs only
|
2018-05-01 01:32:27 +02:00
|
|
|
const char *const resumeArgs[] = { "syncthingctl", "resume", "--dir", "test2", nullptr };
|
|
|
|
const char *const statusDirsOnlyArgs[] = { "syncthingctl", "status", "--all-dirs", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(resumeArgs);
|
|
|
|
TESTUTILS_ASSERT_EXEC(statusDirsOnlyArgs);
|
2023-01-28 20:40:00 +01:00
|
|
|
CPPUNIT_ASSERT(stdout.find(" - Test dir 2") != string::npos);
|
2018-05-01 01:10:46 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find("paused") == string::npos);
|
|
|
|
|
|
|
|
// test pause, verify via status on specific dir
|
2018-05-01 01:32:27 +02:00
|
|
|
const char *const pauseArgs[] = { "syncthingctl", "pause", "--dir", "test2", nullptr };
|
|
|
|
const char *const statusTest2Args[] = { "syncthingctl", "status", "--dir", "test2", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(pauseArgs);
|
2018-10-20 23:48:24 +02:00
|
|
|
cout << stdout;
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(statusTest2Args);
|
2018-10-20 23:48:24 +02:00
|
|
|
cout << stdout;
|
|
|
|
CPPUNIT_ASSERT(stdout.find(" - test1") == string::npos);
|
2023-01-28 20:40:00 +01:00
|
|
|
CPPUNIT_ASSERT(stdout.find(" - Test dir 2") != string::npos);
|
2018-05-01 01:10:46 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find("paused") != string::npos);
|
|
|
|
|
|
|
|
// test cat
|
|
|
|
const char *const catArgs[] = { "syncthingctl", "cat", nullptr };
|
|
|
|
TESTUTILS_ASSERT_EXEC(catArgs);
|
|
|
|
cout << stdout;
|
|
|
|
QJsonParseError error;
|
2021-03-20 22:39:40 +01:00
|
|
|
const auto doc(QJsonDocument::fromJson(QByteArray(stdout.data(), static_cast<QByteArray::size_type>(stdout.size())), &error));
|
2018-05-01 01:10:46 +02:00
|
|
|
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
|
2019-04-10 20:34:25 +02:00
|
|
|
const char *const statusTest1Args[] = { "syncthingctl", "status", "--dir", "test1", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
#if defined(SYNCTHINGCTL_USE_JSENGINE) || defined(SYNCTHINGCTL_USE_SCRIPT)
|
2018-05-01 01:32:27 +02:00
|
|
|
const char *const editArgs[] = { "syncthingctl", "edit", "--js-lines", "assignIfPresent(findFolder('test1'), 'rescanIntervalS', 0);", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(editArgs);
|
|
|
|
cout << stdout;
|
|
|
|
TESTUTILS_ASSERT_EXEC(statusTest1Args);
|
|
|
|
cout << stdout;
|
2018-10-20 23:48:24 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find(" - test1") != string::npos);
|
2023-01-28 20:40:00 +01:00
|
|
|
CPPUNIT_ASSERT(stdout.find(" - Test dir 2") == string::npos);
|
2018-09-15 18:18:15 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find("Rescan interval file system watcher and periodic rescan disabled") != string::npos);
|
2018-05-01 01:10:46 +02:00
|
|
|
#endif
|
|
|
|
|
|
|
|
// test rescan: create new file, trigger rescan, check status
|
2023-02-11 15:55:45 +01:00
|
|
|
CPPUNIT_ASSERT(std::ofstream(m_tempPath / "some/path/1/new-file.txt") << "foo");
|
2018-05-01 01:32:27 +02:00
|
|
|
const char *const rescanArgs[] = { "syncthingctl", "rescan", "test1", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(rescanArgs);
|
|
|
|
cout << stdout;
|
|
|
|
TESTUTILS_ASSERT_EXEC(statusTest1Args);
|
|
|
|
cout << stdout;
|
2018-10-20 23:48:24 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find(" - test1") != string::npos);
|
2018-05-01 01:10:46 +02:00
|
|
|
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
|
2023-02-11 15:55:45 +01:00
|
|
|
std::filesystem::current_path(m_tempPath / "some/path/1");
|
|
|
|
std::filesystem::create_directory("newdir");
|
|
|
|
std::filesystem::current_path("newdir");
|
2018-05-01 01:10:46 +02:00
|
|
|
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"));
|
2023-08-19 19:15:09 +02:00
|
|
|
qputenv("LLVM_PROFILE_FILE", "/tmp/syncthingctl-%p.profraw");
|
2018-05-01 01:10:46 +02:00
|
|
|
// -> do actual test
|
2018-05-01 01:32:27 +02:00
|
|
|
const char *const pwdRescanArgs[] = { "syncthingctl", "pwd", "rescan", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(pwdRescanArgs);
|
|
|
|
cout << stdout;
|
|
|
|
// -> restore LLVM_PROFILE_FILE
|
|
|
|
if (llvmProfileFile) {
|
2023-08-19 19:15:09 +02:00
|
|
|
qputenv("LLVM_PROFILE_FILE", llvmProfileFile);
|
2018-05-01 01:10:46 +02:00
|
|
|
} else {
|
2023-08-19 19:15:09 +02:00
|
|
|
qunsetenv("LLVM_PROFILE_FILE");
|
2018-05-01 01:10:46 +02:00
|
|
|
}
|
|
|
|
// -> verify result
|
2018-05-01 01:32:27 +02:00
|
|
|
const char *const pwdStatusArgs[] = { "syncthingctl", "pwd", "status", nullptr };
|
2018-05-01 01:10:46 +02:00
|
|
|
TESTUTILS_ASSERT_EXEC(pwdStatusArgs);
|
|
|
|
cout << stdout;
|
2018-10-20 23:48:24 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find(" - test1") != string::npos);
|
2018-05-01 01:10:46 +02:00
|
|
|
CPPUNIT_ASSERT(stdout.find("Local 2 file(s), 1 dir(s)") != string::npos);
|
|
|
|
}
|
|
|
|
#endif
|