Split integration test file to several files.

This commit is contained in:
Alexey Sokolov
2018-02-13 20:53:05 +00:00
parent e3a1308a04
commit 70dabc07dc
12 changed files with 1053 additions and 827 deletions
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2004-2016 ZNC, see the NOTICE file for details.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gmock/gmock.h>
#include "base.h"
using testing::AnyOf;
using testing::Eq;
namespace znc_inttest {
Process::Process(QString cmd, QStringList args,
std::function<void(QProcess*)> setup)
: IO(&m_proc, true) {
auto env = QProcessEnvironment::systemEnvironment();
env.insert("ZNC_DEBUG_TIMER", "1");
// Default exit codes of sanitizers upon error:
// ASAN - 1
// LSAN - 23 (part of ASAN, but uses a different value)
// TSAN - 66
//
// ZNC uses 1 too to report startup failure.
// But we don't want to confuse expected startup failure with ASAN
// error.
env.insert("ASAN_OPTIONS", "exitcode=57");
m_proc.setProcessEnvironment(env);
setup(&m_proc);
m_proc.start(cmd, args);
EXPECT_TRUE(m_proc.waitForStarted())
<< "Failed to start ZNC, did you install it?";
}
Process::~Process() {
if (m_kill) m_proc.terminate();
[this]() {
ASSERT_TRUE(m_proc.waitForFinished());
if (!m_allowDie) {
ASSERT_EQ(QProcess::NormalExit, m_proc.exitStatus());
if (m_allowLeak) {
ASSERT_THAT(m_proc.exitStatus(), AnyOf(Eq(23), Eq(m_exit)));
} else {
ASSERT_EQ(m_exit, m_proc.exitCode());
}
}
}();
}
} // namespace znc_inttest
+192
View File
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2004-2016 ZNC, see the NOTICE file for details.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QDateTime>
#include <QProcess>
#include <QTcpSocket>
#include <memory>
namespace znc_inttest {
template <typename Device>
class IO {
public:
IO(Device* device, bool verbose = false)
: m_device(device), m_verbose(verbose) {}
virtual ~IO() {}
void ReadUntil(QByteArray pattern);
/*
* Reads from Device until pattern is matched and returns this pattern
* up to and excluding the first newline. Pattern itself can contain a newline.
* Have to use second param as the ASSERT_*'s return a non-QByteArray.
*/
void ReadUntilAndGet(QByteArray pattern, QByteArray& match);
void Write(QByteArray s = "", bool new_line = true);
void Close();
private:
// Need to flush QTcpSocket, and QIODevice doesn't have flush at all...
static void FlushIfCan(QIODevice*) {}
static void FlushIfCan(QTcpSocket* sock) { sock->flush(); }
Device* m_device;
bool m_verbose;
QByteArray m_readed;
};
template <typename Device>
IO<Device> WrapIO(Device* d) {
return IO<Device>(d);
}
using Socket = IO<QTcpSocket>;
class Process : public IO<QProcess> {
public:
Process(QString cmd, QStringList args,
std::function<void(QProcess*)> setup = [](QProcess*) {});
~Process() override;
void ShouldFinishItself(int code = 0) {
m_kill = false;
m_exit = code;
}
void CanDie() { m_allowDie = true; }
// I can't do much about SWIG...
void CanLeak() { m_allowLeak = true; }
private:
bool m_kill = true;
int m_exit = 0;
bool m_allowDie = false;
bool m_allowLeak = false;
QProcess m_proc;
};
// Can't use QEventLoop without existing QCoreApplication
class App {
public:
App() : m_argv(new char{}), m_app(m_argc, &m_argv) {}
~App() { delete m_argv; }
private:
int m_argc = 1;
char* m_argv;
QCoreApplication m_app;
};
// Implementation
template <typename Device>
void IO<Device>::ReadUntil(QByteArray pattern) {
auto deadline = QDateTime::currentDateTime().addSecs(60);
while (true) {
int search = m_readed.indexOf(pattern);
if (search != -1) {
m_readed.remove(0, search + pattern.length());
return;
}
if (m_readed.length() > pattern.length()) {
m_readed = m_readed.right(pattern.length());
}
const int timeout_ms =
QDateTime::currentDateTime().msecsTo(deadline);
ASSERT_GT(timeout_ms, 0) << "Wanted:" << pattern.toStdString();
ASSERT_TRUE(m_device->waitForReadyRead(timeout_ms))
<< "Wanted: " << pattern.toStdString();
QByteArray chunk = m_device->readAll();
if (m_verbose) {
std::cout << chunk.toStdString() << std::flush;
}
m_readed += chunk;
}
}
template <typename Device>
void IO<Device>::ReadUntilAndGet(QByteArray pattern, QByteArray& match) {
auto deadline = QDateTime::currentDateTime().addSecs(60);
while (true) {
int search = m_readed.indexOf(pattern);
if (search != -1) {
int start = 0;
/* Don't look for what we've already found */
if (pattern != "\n") {
int patlen = pattern.length();
start = search;
pattern = QByteArray("\n");
search = m_readed.indexOf(pattern, start + patlen);
}
if (search != -1) {
match += m_readed.mid(start, search - start);
m_readed.remove(0, search + 1);
return;
}
/* No newline yet, add to retvalue and trunc output */
match += m_readed.mid(start);
m_readed.resize(0);
}
if (m_readed.length() > pattern.length()) {
m_readed = m_readed.right(pattern.length());
}
const int timeout_ms =
QDateTime::currentDateTime().msecsTo(deadline);
ASSERT_GT(timeout_ms, 0) << "Wanted:" << pattern.toStdString();
ASSERT_TRUE(m_device->waitForReadyRead(timeout_ms))
<< "Wanted: " << pattern.toStdString();
QByteArray chunk = m_device->readAll();
if (m_verbose) {
std::cout << chunk.toStdString() << std::flush;
}
m_readed += chunk;
}
}
template <typename Device>
void IO<Device>::Write(QByteArray s, bool new_line) {
if (!m_device) return;
if (m_verbose) {
std::cout << s.toStdString() << std::flush;
if (new_line) {
std::cout << std::endl;
}
}
s += "\n";
while (!s.isEmpty()) {
auto res = m_device->write(s);
ASSERT_NE(res, -1);
s.remove(0, res);
}
FlushIfCan(m_device);
}
template <typename Device>
void IO<Device>::Close() {
#ifdef __CYGWIN__
// Qt on cygwin silently doesn't send the rest of buffer from socket
// without this line
sleep(1);
#endif
m_device->disconnectFromHost();
}
} // namespace znc_inttest
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2004-2016 ZNC, see the NOTICE file for details.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtest/gtest.h>
class ThrowListener : public testing::EmptyTestEventListener {
void OnTestPartResult(const testing::TestPartResult& result) override {
if (result.type() == testing::TestPartResult::kFatalFailure &&
!std::uncaught_exception()) {
throw testing::AssertionException(result);
}
}
};
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener);
return RUN_ALL_TESTS();
}
+177
View File
@@ -0,0 +1,177 @@
/*
* Copyright (C) 2004-2016 ZNC, see the NOTICE file for details.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "znctest.h"
#ifndef ZNC_BIN_DIR
#define ZNC_BIN_DIR ""
#endif
namespace znc_inttest {
void WriteConfig(QString path) {
// clang-format off
Process p(ZNC_BIN_DIR "/znc", QStringList() << "--debug"
<< "--datadir" << path
<< "--makeconf");
p.ReadUntil("Listen on port"); p.Write("12345");
p.ReadUntil("Listen using SSL"); p.Write();
p.ReadUntil("IPv6"); p.Write();
p.ReadUntil("Username"); p.Write("user");
p.ReadUntil("password"); p.Write("hunter2", false);
p.ReadUntil("Confirm"); p.Write("hunter2", false);
p.ReadUntil("Nick [user]"); p.Write();
p.ReadUntil("Alternate nick [user_]"); p.Write();
p.ReadUntil("Ident [user]"); p.Write();
p.ReadUntil("Real name"); p.Write();
p.ReadUntil("Bind host"); p.Write();
p.ReadUntil("Set up a network?"); p.Write();
p.ReadUntil("Name [freenode]"); p.Write("test");
p.ReadUntil("Server host (host only)"); p.Write("127.0.0.1");
p.ReadUntil("Server uses SSL?"); p.Write();
p.ReadUntil("6667"); p.Write();
p.ReadUntil("password"); p.Write();
p.ReadUntil("channels"); p.Write();
p.ReadUntil("Launch ZNC now?"); p.Write("no");
p.ShouldFinishItself();
// clang-format on
}
void ZNCTest::SetUp() {
WriteConfig(m_dir.path());
ASSERT_TRUE(m_server.listen(QHostAddress::LocalHost, 6667))
<< m_server.errorString().toStdString();
}
Socket ZNCTest::ConnectIRCd() {
[this] {
ASSERT_TRUE(m_server.waitForNewConnection(30000 /* msec */));
}();
return WrapIO(m_server.nextPendingConnection());
}
Socket ZNCTest::ConnectClient() {
m_clients.emplace_back();
QTcpSocket& sock = m_clients.back();
sock.connectToHost("127.0.0.1", 12345);
[&] {
ASSERT_TRUE(sock.waitForConnected())
<< sock.errorString().toStdString();
}();
return WrapIO(&sock);
}
Socket ZNCTest::LoginClient() {
auto client = ConnectClient();
client.Write("PASS :hunter2");
client.Write("NICK nick");
client.Write("USER user/test x x :x");
return client;
}
std::unique_ptr<Process> ZNCTest::Run() {
return std::unique_ptr<Process>(new Process(
ZNC_BIN_DIR "/znc", QStringList() << "--debug"
<< "--datadir" << m_dir.path(),
[](QProcess* proc) {
proc->setProcessChannelMode(QProcess::ForwardedChannels);
}));
}
std::unique_ptr<QNetworkReply> ZNCTest::HttpGet(QNetworkRequest request) {
return HandleHttp(m_network.get(request));
}
std::unique_ptr<QNetworkReply> ZNCTest::HttpPost(
QNetworkRequest request, QList<QPair<QString, QString>> data) {
request.setHeader(QNetworkRequest::ContentTypeHeader,
"application/x-www-form-urlencoded");
QUrlQuery q;
q.setQueryItems(data);
return HandleHttp(m_network.post(request, q.toString().toUtf8()));
}
std::unique_ptr<QNetworkReply> ZNCTest::HandleHttp(QNetworkReply* reply) {
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, [&]() {
std::cout << "Got HTTP reply" << std::endl;
loop.quit();
});
QObject::connect(
reply,
static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(
&QNetworkReply::error),
[&](QNetworkReply::NetworkError e) {
ADD_FAILURE() << reply->errorString().toStdString();
});
QTimer::singleShot(30000 /* msec */, &loop, [&]() {
ADD_FAILURE() << "connection timeout";
loop.quit();
});
std::cout << "Start HTTP loop.exec()" << std::endl;
loop.exec();
std::cout << "Finished HTTP loop.exec()" << std::endl;
return std::unique_ptr<QNetworkReply>(reply);
}
void ZNCTest::InstallModule(QString name, QString content) {
QDir dir(m_dir.path());
ASSERT_TRUE(dir.mkpath("modules"));
ASSERT_TRUE(dir.cd("modules"));
if (name.endsWith(".cpp")) {
// Compile
QTemporaryDir srcdir;
QFile file(QDir(srcdir.path()).filePath(name));
ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&file);
out << content;
file.close();
Process p(
ZNC_BIN_DIR "/znc-buildmod", QStringList() << file.fileName(),
[&](QProcess* proc) {
proc->setWorkingDirectory(dir.absolutePath());
proc->setProcessChannelMode(QProcess::ForwardedChannels);
});
p.ShouldFinishItself();
} else if (name.endsWith(".py")) {
// Dedent
QStringList lines = content.split("\n");
int maxoffset = -1;
for (const QString& line : lines) {
int nonspace = line.indexOf(QRegExp("\\S"));
if (nonspace == -1) continue;
if (nonspace < maxoffset || maxoffset == -1)
maxoffset = nonspace;
}
if (maxoffset == -1) maxoffset = 0;
QFile file(dir.filePath(name));
ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&file);
for (const QString& line : lines) {
// QTextStream::operator<<(const QStringRef &string) was
// introduced in Qt 5.6; let's keep minimum required version
// less than that for now.
out << line.mid(maxoffset) << "\n";
}
} else {
// Write as is
QFile file(dir.filePath(name));
ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&file);
out << content;
}
}
} // namespace znc_inttest
+58
View File
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2004-2016 ZNC, see the NOTICE file for details.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include "base.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QTcpServer>
#include <QTemporaryDir>
#include <QTextStream>
#include <QTimer>
#include <QUrl>
#include <QUrlQuery>
namespace znc_inttest {
void WriteConfig(QString path);
class ZNCTest : public testing::Test {
protected:
void SetUp() override;
Socket ConnectIRCd();
Socket ConnectClient();
Socket LoginClient();
std::unique_ptr<Process> Run();
std::unique_ptr<QNetworkReply> HttpGet(QNetworkRequest request);
std::unique_ptr<QNetworkReply> HttpPost(
QNetworkRequest request, QList<QPair<QString, QString>> data);
std::unique_ptr<QNetworkReply> HandleHttp(QNetworkReply* reply);
void InstallModule(QString name, QString content);
App m_app;
QNetworkAccessManager m_network;
QTemporaryDir m_dir;
QTcpServer m_server;
std::list<QTcpSocket> m_clients;
};
} // namespace znc_inttest