overte-JulianGro/tools/nitpick/src/TestRunnerDesktop.cpp
2019-01-25 16:48:29 -08:00

681 lines
25 KiB
C++

//
// TestRunnerDesktop.cpp
//
// Created by Nissim Hadar on 1 Sept 2018.
// Copyright 2013 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "TestRunnerDesktop.h"
#include <QThread>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QFileDialog>
#ifdef Q_OS_WIN
#include <windows.h>
#include <tlhelp32.h>
#endif
#include "Nitpick.h"
extern Nitpick* nitpick;
TestRunnerDesktop::TestRunnerDesktop(
std::vector<QCheckBox*> dayCheckboxes,
std::vector<QCheckBox*> timeEditCheckboxes,
std::vector<QTimeEdit*> timeEdits,
QLabel* workingFolderLabel,
QCheckBox* runServerless,
QCheckBox* runLatest,
QLineEdit* url,
QPushButton* runNow,
QLabel* statusLabel,
QObject* parent
) : QObject(parent)
{
_dayCheckboxes = dayCheckboxes;
_timeEditCheckboxes = timeEditCheckboxes;
_timeEdits = timeEdits;
_workingFolderLabel = workingFolderLabel;
_runServerless = runServerless;
_runLatest = runLatest;
_url = url;
_runNow = runNow;
_statusLabel = statusLabel;
_installerThread = new QThread();
_installerWorker = new InstallerWorker();
_installerWorker->moveToThread(_installerThread);
_installerThread->start();
connect(this, SIGNAL(startInstaller()), _installerWorker, SLOT(runCommand()));
connect(_installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete()));
_interfaceThread = new QThread();
_interfaceWorker = new InterfaceWorker();
_interfaceThread->start();
_interfaceWorker->moveToThread(_interfaceThread);
connect(this, SIGNAL(startInterface()), _interfaceWorker, SLOT(runCommand()));
connect(_interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete()));
}
TestRunnerDesktop::~TestRunnerDesktop() {
delete _installerThread;
delete _installerWorker;
delete _interfaceThread;
delete _interfaceWorker;
if (_timer) {
delete _timer;
}
}
void TestRunnerDesktop::setWorkingFolderAndEnableControls() {
setWorkingFolder(_workingFolderLabel);
#ifdef Q_OS_WIN
_installationFolder = _workingFolder + "/High Fidelity";
#elif defined Q_OS_MAC
_installationFolder = _workingFolder + "/High_Fidelity";
#endif
nitpick->enableRunTabControls();
_timer = new QTimer(this);
connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime()));
_timer->start(30 * 1000); //time specified in ms
#ifdef Q_OS_MAC
// Create MAC shell scripts
QFile script;
// This script waits for a process to start
script.setFileName(_workingFolder + "/waitForStart.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'waitForStart.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("PROCESS=\"$1\"\n");
script.write("until (pgrep -x $PROCESS >nul)\n");
script.write("do\n");
script.write("\techo waiting for \"$1\" to start\n");
script.write("\tsleep 2\n");
script.write("done\n");
script.write("echo \"$1\" \"started\"\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
// The Mac shell command returns immediately. This little script waits for a process to finish
script.setFileName(_workingFolder + "/waitForFinish.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'waitForFinish.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("PROCESS=\"$1\"\n");
script.write("while (pgrep -x $PROCESS >nul)\n");
script.write("do\n");
script.write("\techo waiting for \"$1\" to finish\n");
script.write("\tsleep 2\n");
script.write("done\n");
script.write("echo \"$1\" \"finished\"\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
// Create an AppleScript to resize Interface. This is needed so that snapshots taken
// with the primary camera will be the correct size.
// This will be run from a normal shell script
script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'setInterfaceSizeAndPosition.scpt'");
exit(-1);
}
script.write("set width to 960\n");
script.write("set height to 540\n");
script.write("set x to 100\n");
script.write("set y to 100\n\n");
script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'setInterfaceSizeAndPosition.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
script.write("echo resizing interface\n");
script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str());
script.write("echo resize complete\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
#endif
}
void TestRunnerDesktop::run() {
_runNow->setEnabled(false);
_testStartDateTime = QDateTime::currentDateTime();
_automatedTestIsRunning = true;
// Initial setup
_branch = nitpick->getSelectedBranch();
_user = nitpick->getSelectedUser();
// This will be restored at the end of the tests
saveExistingHighFidelityAppDataFolder();
_statusLabel->setText("Downloading Build XML");
downloadBuildXml((void*)this);
// `downloadComplete` will run after download has completed
}
void TestRunnerDesktop::downloadComplete() {
if (!buildXMLDownloaded) {
// Download of Build XML has completed
buildXMLDownloaded = true;
// Download the High Fidelity installer
QStringList urls;
QStringList filenames;
if (_runLatest->isChecked()) {
parseBuildInformation();
_installerFilename = INSTALLER_FILENAME_LATEST;
urls << _buildInformation.url;
filenames << _installerFilename;
} else {
QString urlText = _url->text();
urls << urlText;
_installerFilename = getInstallerNameFromURL(urlText);
filenames << _installerFilename;
}
_statusLabel->setText("Downloading installer");
nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this);
// `downloadComplete` will run again after download has completed
} else {
// Download of Installer has completed
appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" +
QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " +
_testStartDateTime.date().toString("ddd, MMM d, yyyy"));
_statusLabel->setText("Installing");
// Kill any existing processes that would interfere with installation
killProcesses();
runInstaller();
}
}
void TestRunnerDesktop::runInstaller() {
// Qt cannot start an installation process using QProcess::start (Qt Bug 9761)
// To allow installation, the installer is run using the `system` command
QStringList arguments{ QStringList() << QString("/S") << QString("/D=") + QDir::toNativeSeparators(_installationFolder) };
QString installerFullPath = _workingFolder + "/" + _installerFilename;
QString commandLine;
#ifdef Q_OS_WIN
commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder);
#elif defined Q_OS_MAC
// Create installation shell script
QFile script;
script.setFileName(_workingFolder + "/install_app.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'install_app.sh'");
exit(-1);
}
if (!QDir().exists(_installationFolder)) {
QDir().mkdir(_installationFolder);
}
// This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end
script.write("#!/bin/sh\n\n");
script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n");
QString folderName {"High Fidelity"};
if (!_runLatest->isChecked()) {
folderName += QString(" - ") + getPRNumberFromURL(_url->text());
}
script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
script.write("hdiutil detach \"$VOLUME\"\n");
script.write("killall yes\n");
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath;
#endif
appendLog(commandLine);
_installerWorker->setCommandLine(commandLine);
emit startInstaller();
}
void TestRunnerDesktop::installationComplete() {
verifyInstallationSucceeded();
createSnapshotFolder();
_statusLabel->setText("Running tests");
if (!_runServerless->isChecked()) {
startLocalServerProcesses();
}
runInterfaceWithTestScript();
}
void TestRunnerDesktop::verifyInstallationSucceeded() {
// Exit if the executables are missing.
// On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error
#ifdef Q_OS_WIN
QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe");
QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe");
QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe");
if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) {
QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled");
exit(-1);
}
#endif
}
void TestRunnerDesktop::saveExistingHighFidelityAppDataFolder() {
QString dataDirectory{ "NOT FOUND" };
#ifdef Q_OS_WIN
dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming";
#elif defined Q_OS_MAC
dataDirectory = QDir::homePath() + "/Library/Application Support";
#endif
if (_runLatest->isChecked()) {
_appDataFolder = dataDirectory + "/High Fidelity";
} else {
// We are running a PR build
_appDataFolder = dataDirectory + "/High Fidelity - " + getPRNumberFromURL(_url->text());
}
_savedAppDataFolder = dataDirectory + "/" + UNIQUE_FOLDER_NAME;
if (QDir(_savedAppDataFolder).exists()) {
_savedAppDataFolder.removeRecursively();
}
if (_appDataFolder.exists()) {
// The original folder is saved in a unique name
_appDataFolder.rename(_appDataFolder.path(), _savedAppDataFolder.path());
}
// Copy an "empty" AppData folder (i.e. no entities)
QDir canonicalAppDataFolder;
#ifdef Q_OS_WIN
canonicalAppDataFolder = QDir::currentPath() + "/AppDataHighFidelity";
#elif defined Q_OS_MAC
canonicalAppDataFolder = QCoreApplication::applicationDirPath() + "/AppDataHighFidelity";
#endif
if (canonicalAppDataFolder.exists()) {
copyFolder(canonicalAppDataFolder.path(), _appDataFolder.path());
} else {
QMessageBox::critical(0, "Internal error", "The nitpick AppData folder cannot be found at:\n" + canonicalAppDataFolder.path());
exit(-1);
}
}
void TestRunnerDesktop::createSnapshotFolder() {
_snapshotFolder = _workingFolder + "/" + SNAPSHOT_FOLDER_NAME;
// Just delete all PNGs from the folder if it already exists
if (QDir(_snapshotFolder).exists()) {
// Note that we cannot use just a `png` filter, as the filenames include periods
// Also, delete any `jpg` and `txt` files
// The idea is to leave only previous zipped result folders
QDirIterator it(_snapshotFolder);
while (it.hasNext()) {
QString filename = it.next();
if (filename.right(4) == ".png" || filename.right(4) == ".jpg" || filename.right(4) == ".txt") {
QFile::remove(filename);
}
}
} else {
QDir().mkdir(_snapshotFolder);
}
}
void TestRunnerDesktop::killProcesses() {
#ifdef Q_OS_WIN
try {
QStringList processesToKill = QStringList() << "interface.exe"
<< "assignment-client.exe"
<< "domain-server.exe"
<< "server-console.exe";
// Loop until all pending processes to kill have actually died
QStringList pendingProcessesToKill;
do {
pendingProcessesToKill.clear();
// Get list of running tasks
HANDLE processSnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (processSnapHandle == INVALID_HANDLE_VALUE) {
throw("Process snapshot creation failure");
}
PROCESSENTRY32 processEntry32;
processEntry32.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(processSnapHandle, &processEntry32)) {
CloseHandle(processSnapHandle);
throw("Process32First failed");
}
// Kill any task in the list
do {
foreach (QString process, processesToKill)
if (QString(processEntry32.szExeFile) == process) {
QString commandLine = "taskkill /im " + process + " /f >nul";
system(commandLine.toStdString().c_str());
pendingProcessesToKill << process;
}
} while (Process32Next(processSnapHandle, &processEntry32));
QThread::sleep(2);
} while (!pendingProcessesToKill.isEmpty());
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
#elif defined Q_OS_MAC
QString commandLine;
commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface";
system(commandLine.toStdString().c_str());
commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox";
system(commandLine.toStdString().c_str());
commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console";
system(commandLine.toStdString().c_str());
#endif
}
void TestRunnerDesktop::startLocalServerProcesses() {
QString commandLine;
#ifdef Q_OS_WIN
commandLine =
"start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\"";
system(commandLine.toStdString().c_str());
commandLine =
"start \"assignment-client.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe\" -n 6";
system(commandLine.toStdString().c_str());
#elif defined Q_OS_MAC
commandLine = "open \"" +_installationFolder + "/Sandbox.app\"";
system(commandLine.toStdString().c_str());
#endif
// Give server processes time to stabilize
QThread::sleep(20);
}
void TestRunnerDesktop::runInterfaceWithTestScript() {
QString url = QString("hifi://localhost");
if (_runServerless->isChecked()) {
// Move to an empty area
url = "file:///~serverless/tutorial.json";
} else {
url = "hifi://localhost";
}
QString deleteScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js";
QString testScript =
QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js";
QString commandLine;
#ifdef Q_OS_WIN
QString exeFile;
// First, run script to delete any entities in test area
// Note that this will run to completion before continuing
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = "start /wait \"\" " + exeFile +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished";
system(commandLine.toStdString().c_str());
// Now run the test suite
exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\"";
commandLine = exeFile +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation " + _snapshotFolder;
_interfaceWorker->setCommandLine(commandLine);
emit startInterface();
#elif defined Q_OS_MAC
QFile script;
script.setFileName(_workingFolder + "/runInterfaceTests.sh");
if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
"Could not open 'runInterfaceTests.sh'");
exit(-1);
}
script.write("#!/bin/sh\n\n");
// First, run script to delete any entities in test area
commandLine =
"open -W \"" +_installationFolder + "/interface.app\" --args" +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + deleteScript + " quitWhenFinished\n";
script.write(commandLine.toStdString().c_str());
// On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process
// has started.
// Before starting interface, start a process that will resize interface 10s after it opens
commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n";
script.write(commandLine.toStdString().c_str());
commandLine =
"open \"" +_installationFolder + "/interface.app\" --args" +
" --url " + url +
" --no-updater" +
" --no-login-suggestion"
" --testScript " + testScript + " quitWhenFinished" +
" --testResultsLocation " + _snapshotFolder +
" && " + _workingFolder +"/waitForFinish.sh interface\n";
script.write(commandLine.toStdString().c_str());
script.close();
script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner);
commandLine = _workingFolder + "/runInterfaceTests.sh";
_interfaceWorker->setCommandLine(commandLine);
emit startInterface();
#endif
// Helpful for debugging
appendLog(commandLine);
}
void TestRunnerDesktop::interfaceExecutionComplete() {
QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt");
if (!testCompleted.exists()) {
QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated");
}
evaluateResults();
killProcesses();
// The High Fidelity AppData folder will be restored after evaluation has completed
}
void TestRunnerDesktop::evaluateResults() {
_statusLabel->setText("Evaluating results");
nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user);
}
void TestRunnerDesktop::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) {
addBuildNumberToResults(zippedFolder);
restoreHighFidelityAppDataFolder();
_statusLabel->setText("Testing complete");
QDateTime currentDateTime = QDateTime::currentDateTime();
QString completionText = QString("Tests completed at ") + QString::number(currentDateTime.time().hour()) + ":" +
QString("%1").arg(currentDateTime.time().minute(), 2, 10, QChar('0')) + ", on " +
currentDateTime.date().toString("ddd, MMM d, yyyy");
if (numberOfFailures == 0) {
completionText += "; no failures";
} else if (numberOfFailures == 1) {
completionText += "; 1 failure";
} else {
completionText += QString("; ") + QString::number(numberOfFailures) + " failures";
}
appendLog(completionText);
_automatedTestIsRunning = false;
_runNow->setEnabled(true);
}
void TestRunnerDesktop::addBuildNumberToResults(QString zippedFolderName) {
QString augmentedFilename;
if (!_runLatest->isChecked()) {
augmentedFilename = zippedFolderName.replace("local", getPRNumberFromURL(_url->text()));
} else {
augmentedFilename = zippedFolderName.replace("local", _buildInformation.build);
}
QFile::rename(zippedFolderName, augmentedFilename);
}
void TestRunnerDesktop::restoreHighFidelityAppDataFolder() {
_appDataFolder.removeRecursively();
if (_savedAppDataFolder != QDir()) {
_appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path());
}
}
// Copies a folder recursively
void TestRunnerDesktop::copyFolder(const QString& source, const QString& destination) {
try {
if (!QFileInfo(source).isDir()) {
// just a file copy
QFile::copy(source, destination);
} else {
QDir destinationDir(destination);
if (!destinationDir.cdUp()) {
throw("'source '" + source + "'seems to be a root folder");
}
if (!destinationDir.mkdir(QFileInfo(destination).fileName())) {
throw("Could not create destination folder '" + destination + "'");
}
QStringList fileNames =
QDir(source).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
foreach (const QString& fileName, fileNames) {
copyFolder(QString(source + "/" + fileName), QString(destination + "/" + fileName));
}
}
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}
void TestRunnerDesktop::checkTime() {
// No processing is done if a test is running
if (_automatedTestIsRunning) {
return;
}
QDateTime now = QDateTime::currentDateTime();
// Check day of week
if (!_dayCheckboxes.at(now.date().dayOfWeek() - 1)->isChecked()) {
return;
}
// Check the time
bool timeToRun{ false };
for (size_t i = 0; i < std::min(_timeEditCheckboxes.size(), _timeEdits.size()); ++i) {
if (_timeEditCheckboxes[i]->isChecked() && (_timeEdits[i]->time().hour() == now.time().hour()) &&
(_timeEdits[i]->time().minute() == now.time().minute())) {
timeToRun = true;
break;
}
}
if (timeToRun) {
run();
}
}
QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) {
try {
QStringList urlParts = url.split("/");
QStringList filenameParts = urlParts[urlParts.size() - 1].split("-");
if (filenameParts.size() <= 3) {
#ifdef Q_OS_WIN
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`";
#elif defined Q_OS_MAC
throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`";
#endif
}
return filenameParts[filenameParts.size() - 2];
} catch (QString errorMessage) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage);
exit(-1);
} catch (...) {
QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error");
exit(-1);
}
}