libreoffice-online/common/Unit.cpp
Ashod Nakashian db50626aff wsd: test: refactor BaseUnit self-test
The BaseUnit test didn't belong to UnitTimeout
and, more important, it needed access to
private members to both validate their state
and to reset them (since the test is artificially
initializing both WSD and Kit tests and cannot
uninitialize them, lest we unload ourselves).

As such, the self-test is now internal to
BaseUnit, with the added bonus that it
is called on all tests and not just UnitTimeout.

Also, more assertions have been added.

Change-Id: Ieaf60594f39e978a7250407262bd8bbc9b642c43
Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2023-01-23 11:07:55 -04:00

648 lines
18 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <config.h>
#include "Unit.hpp"
#include <iostream>
#include <cassert>
#include <dlfcn.h>
#include <fstream>
#include <mutex>
#include <sstream>
#include <sysexits.h>
#include <thread>
#include <Poco/JSON/Object.h>
#include <Poco/JSON/Parser.h>
#include <Poco/Util/LayeredConfiguration.h>
#include <Poco/Util/Application.h>
#include "Log.hpp"
#include "Util.hpp"
#include "test/testlog.hpp"
#include <common/SigUtil.hpp>
#include <common/StringVector.hpp>
#include <common/Message.hpp>
UnitKit *GlobalKit = nullptr;
UnitWSD *GlobalWSD = nullptr;
UnitTool *GlobalTool = nullptr;
UnitBase** UnitBase::GlobalArray = nullptr;
int UnitBase::GlobalIndex = -1;
char* UnitBase::UnitLibPath = nullptr;
void* UnitBase::DlHandle = nullptr;
UnitBase::TestOptions UnitBase::GlobalTestOptions;
UnitBase::TestResult UnitBase::GlobalResult = UnitBase::TestResult::Ok;
static std::thread TimeoutThread;
static std::atomic<bool> TimeoutThreadRunning(false);
std::timed_mutex TimeoutThreadMutex;
/// Controls whether experimental features/behavior is enabled or not.
bool EnableExperimental = false;
UnitBase** UnitBase::linkAndCreateUnit(UnitType type, const std::string& unitLibPath)
{
#if !MOBILEAPP
DlHandle = dlopen(unitLibPath.c_str(), RTLD_GLOBAL|RTLD_NOW);
if (!DlHandle)
{
LOG_ERR("Failed to load " << unitLibPath << ": " << dlerror());
return nullptr;
}
// avoid std:string de-allocation during failure / exit.
UnitLibPath = strdup(unitLibPath.c_str());
const char *symbol = nullptr;
switch (type)
{
case UnitType::Wsd:
{
// Try the multi-test version first.
CreateUnitHooksFunctionMulti* createHooksMulti =
reinterpret_cast<CreateUnitHooksFunctionMulti*>(
dlsym(DlHandle, "unit_create_wsd_multi"));
if (createHooksMulti)
{
UnitBase** hooks = createHooksMulti();
if (hooks)
{
std::ostringstream oss;
oss << "Loaded UnitTest [" << unitLibPath << "] with: ";
for (int i = 0; hooks[i] != nullptr; ++i)
{
if (i)
oss << ", ";
oss << hooks[i]->getTestname();
}
LOG_INF(oss.str());
return hooks;
}
}
// Fallback.
symbol = "unit_create_wsd";
break;
}
case UnitType::Kit:
symbol = "unit_create_kit";
break;
case UnitType::Tool:
symbol = "unit_create_tool";
break;
}
// Internal consistency sanity check.
selfTest();
CreateUnitHooksFunction* createHooks =
reinterpret_cast<CreateUnitHooksFunction*>(dlsym(DlHandle, symbol));
if (!createHooks)
{
LOG_ERR("No " << symbol << " symbol in " << unitLibPath);
return nullptr;
}
UnitBase* hooks = createHooks();
if (hooks)
return new UnitBase* [2] { hooks, nullptr };
LOG_ERR("No wsd unit-tests found in " << unitLibPath);
#endif
return nullptr;
}
void UnitBase::initTestSuiteOptions()
{
static const char* TestOptions = getenv("COOL_TEST_OPTIONS");
if (TestOptions == nullptr)
return;
StringVector tokens = StringVector::tokenize(std::string(TestOptions), ':');
for (const auto& token : tokens)
{
// Expect name=value pairs.
const auto pair = Util::split(tokens.getParam(token), '=');
// If there is no value, assume it's a filter string.
if (pair.second.empty())
{
const std::string filter = Util::toLower(pair.first);
LOG_INF("Setting the 'filter' test option to [" << filter << ']');
GlobalTestOptions.setFilter(filter);
}
else if (pair.first == "keepgoing")
{
const bool keepgoing = pair.second == "1" || pair.second == "true";
LOG_INF("Setting the 'keepgoing' test option to " << keepgoing);
GlobalTestOptions.setKeepgoing(keepgoing);
}
}
}
void UnitBase::filter()
{
const auto& filter = GlobalTestOptions.getFilter();
for (; GlobalArray[GlobalIndex] != nullptr; ++GlobalIndex)
{
const std::string& name = GlobalArray[GlobalIndex]->getTestname();
if (strstr(Util::toLower(name).c_str(), filter.c_str()))
break;
LOG_INF("Skipping test [" << name << "] per filter [" << filter << ']');
}
}
void UnitBase::selfTest()
{
assert(init(UnitType::Wsd, std::string()));
assert(!UnitBase::get().isFinished());
assert(!UnitWSD::get().isFinished());
assert(GlobalArray);
assert(GlobalIndex == 0);
assert(&UnitBase::get() == GlobalArray[0]);
delete GlobalArray[0];
delete[] GlobalArray;
GlobalArray = nullptr;
GlobalIndex = -1;
GlobalKit = nullptr;
GlobalWSD = nullptr;
GlobalTool = nullptr;
assert(init(UnitType::Kit, std::string()));
assert(!UnitBase::get().isFinished());
assert(!UnitKit::get().isFinished());
assert(GlobalArray);
assert(GlobalIndex == 0);
assert(&UnitBase::get() == GlobalArray[0]);
delete GlobalArray[0];
delete[] GlobalArray;
GlobalArray = nullptr;
GlobalIndex = -1;
GlobalKit = nullptr;
GlobalWSD = nullptr;
GlobalTool = nullptr;
}
bool UnitBase::init(UnitType type, const std::string &unitLibPath)
{
#if !MOBILEAPP
LOG_ASSERT(!get(type));
#else
// The COOLWSD initialization is called in a loop on mobile, allow reuse
if (get(type))
return true;
#endif
LOG_ASSERT(GlobalArray == nullptr);
LOG_ASSERT(GlobalIndex == -1);
GlobalArray = nullptr;
GlobalIndex = -1;
GlobalKit = nullptr;
GlobalWSD = nullptr;
GlobalTool = nullptr;
if (!unitLibPath.empty())
{
GlobalArray = linkAndCreateUnit(type, unitLibPath);
if (GlobalArray)
{
initTestSuiteOptions();
// Filter tests.
GlobalIndex = 0;
filter();
UnitBase* instance = GlobalArray[GlobalIndex];
if (instance)
{
rememberInstance(type, instance);
TST_LOG_NAME("UnitBase",
"Starting test #1: " << GlobalArray[GlobalIndex]->getTestname());
instance->initialize();
if (instance && type == UnitType::Kit)
{
TimeoutThreadMutex.lock();
TimeoutThread = std::thread(
[instance]
{
TimeoutThreadRunning = true;
Util::setThreadName("unit timeout");
if (TimeoutThreadMutex.try_lock_for(instance->_timeoutMilliSeconds))
{
LOG_DBG(instance->getTestname() << ": Unit test finished in time");
TimeoutThreadMutex.unlock();
}
else
{
LOG_ERR(instance->getTestname() << ": Unit test timeout after "
<< instance->_timeoutMilliSeconds);
instance->timeout();
}
TimeoutThreadRunning = false;
});
}
return get(type) != nullptr;
}
}
}
// Fallback.
switch (type)
{
case UnitType::Wsd:
rememberInstance(UnitType::Wsd, new UnitWSD("UnitWSD"));
GlobalArray = new UnitBase* [2] { GlobalWSD, nullptr };
GlobalIndex = 0;
break;
case UnitType::Kit:
rememberInstance(UnitType::Kit, new UnitKit("UnitKit"));
GlobalArray = new UnitBase* [2] { GlobalKit, nullptr };
GlobalIndex = 0;
break;
case UnitType::Tool:
rememberInstance(UnitType::Tool, new UnitTool("UnitTool"));
GlobalArray = new UnitBase* [2] { GlobalTool, nullptr };
GlobalIndex = 0;
break;
default:
assert(false);
break;
}
return get(type) != nullptr;
}
UnitBase* UnitBase::get(UnitType type)
{
switch (type)
{
case UnitType::Wsd:
return GlobalWSD;
break;
case UnitType::Kit:
return GlobalKit;
break;
case UnitType::Tool:
return GlobalTool;
break;
default:
assert(false);
break;
}
return nullptr;
}
void UnitBase::rememberInstance(UnitType type, UnitBase* instance)
{
assert(instance->_type == type);
assert(GlobalWSD == nullptr);
assert(GlobalKit == nullptr);
assert(GlobalTool == nullptr);
switch (type)
{
case UnitType::Wsd:
GlobalWSD = static_cast<UnitWSD*>(instance);
break;
case UnitType::Kit:
GlobalKit = static_cast<UnitKit*>(instance);
break;
case UnitType::Tool:
GlobalTool = static_cast<UnitTool*>(instance);
break;
default:
assert(false);
break;
}
}
int UnitBase::uninit()
{
TST_LOG_NAME("UnitBase", "Uninitializing unit-tests: "
<< (GlobalResult == TestResult::Ok ? "SUCCESS" : "FAILED"));
if (GlobalArray)
{
// By default, this will check _setRetValue and copy _retValue to the arg.
// But we call it to trigger overrides and to perform cleanups.
int retValue = GlobalResult == TestResult::Ok ? EX_OK : EX_SOFTWARE;
if (GlobalArray[GlobalIndex] != nullptr)
GlobalArray[GlobalIndex]->returnValue(retValue);
if (retValue)
GlobalResult = TestResult::Failed;
for (int i = 0; GlobalArray[i] != nullptr; ++i)
{
delete GlobalArray[i];
}
delete[] GlobalArray;
GlobalArray = nullptr;
}
GlobalIndex = -1;
free(UnitBase::UnitLibPath);
UnitBase::UnitLibPath = nullptr;
GlobalKit = nullptr;
GlobalWSD = nullptr;
GlobalTool = nullptr;
// Close the DLL last, after deleting the test instances.
if (DlHandle)
dlclose(DlHandle);
DlHandle = nullptr;
return GlobalResult == TestResult::Ok ? EX_OK : EX_SOFTWARE;
}
void UnitBase::initialize()
{
assert(DlHandle != nullptr && "Invalid handle to set");
LOG_TST("==================== Starting [" << getTestname() << "] ====================");
_socketPoll->startThread();
}
bool UnitBase::isUnitTesting()
{
return DlHandle;
}
void UnitBase::setTimeout(std::chrono::milliseconds timeoutMilliSeconds)
{
assert(!TimeoutThreadRunning);
_timeoutMilliSeconds = timeoutMilliSeconds;
LOG_TST(getTestname() << ": setTimeout: " << _timeoutMilliSeconds);
}
UnitBase::~UnitBase()
{
LOG_TST(getTestname() << ": ~UnitBase: " << (failed() ? "FAILED" : "SUCCESS"));
_socketPoll->joinThread();
}
bool UnitBase::filterLOKitMessage(const std::shared_ptr<Message>& message)
{
return onFilterLOKitMessage(message);
}
bool UnitBase::filterSendWebSocketMessage(const char* data, const std::size_t len,
const WSOpCode code, const bool flush, int& unitReturn)
{
const std::string message(data, len);
if (Util::startsWith(message, "unocommandresult:"))
{
const std::size_t index = message.find_first_of('{');
if (index != std::string::npos)
{
try
{
const std::string stringJSON = message.substr(index);
Poco::JSON::Parser parser;
const Poco::Dynamic::Var parsedJSON = parser.parse(stringJSON);
const auto& object = parsedJSON.extract<Poco::JSON::Object::Ptr>();
if (object->get("commandName").toString() == ".uno:Save")
{
const bool success = object->get("success").toString() == "true";
std::string result;
if (object->has("result"))
{
const Poco::Dynamic::Var parsedResultJSON = object->get("result");
const auto& resultObj = parsedResultJSON.extract<Poco::JSON::Object::Ptr>();
if (resultObj->get("type").toString() == "string")
result = resultObj->get("value").toString();
}
if (onDocumentSaved(message, success, result))
return false;
}
}
catch (const std::exception& exception)
{
LOG_TST("unocommandresult parsing failure: " << exception.what());
}
}
else
{
LOG_TST("Expected json unocommandresult. Ignoring: " << message);
}
}
else if (Util::startsWith(message, "status:"))
{
if (onDocumentLoaded(message))
return false;
}
else if (message == "statechanged: .uno:ModifiedStatus=true")
{
if (onDocumentModified(message))
return false;
}
else if (Util::startsWith(message, "statechanged:"))
{
if (onDocumentStateChanged(message))
return false;
}
else if (Util::startsWith(message, "error:"))
{
if (onDocumentError(message))
return false;
}
return onFilterSendWebSocketMessage(data, len, code, flush, unitReturn);
}
UnitWSD::UnitWSD(const std::string& name)
: UnitBase(name, UnitType::Wsd)
, _hasKitHooks(false)
{
}
UnitWSD::~UnitWSD()
{
}
void UnitWSD::configure(Poco::Util::LayeredConfiguration &config)
{
if (isUnitTesting())
{
// Force HTTP - helps stracing.
config.setBool("ssl.enable", false);
// Use http:// everywhere.
config.setBool("ssl.termination", false);
// Force console output - easier to debug.
config.setBool("logging.file[@enable]", false);
}
}
void UnitWSD::lookupTile(int part, int mode, int width, int height, int tilePosX, int tilePosY,
int tileWidth, int tileHeight,
std::shared_ptr<TileData> &tile)
{
if (isUnitTesting())
{
if (tile)
onTileCacheHit(part, mode, width, height, tilePosX, tilePosY, tileWidth, tileHeight);
else
onTileCacheMiss(part, mode, width, height, tilePosX, tilePosY, tileWidth, tileHeight);
}
}
UnitWSD& UnitWSD::get()
{
assert(GlobalWSD);
return *GlobalWSD;
}
UnitKit::UnitKit(const std::string& name)
: UnitBase(name, UnitType::Kit)
{
}
UnitKit::~UnitKit()
{
}
UnitKit& UnitKit::get()
{
#if MOBILEAPP
if (!GlobalKit)
GlobalKit = new UnitKit("UnitKit");
#endif
assert(GlobalKit);
return *GlobalKit;
}
void UnitBase::exitTest(TestResult result, const std::string& reason)
{
// We could be called from either a SocketPoll (websrv_poll)
// or from invokeTest (coolwsd main).
std::lock_guard<std::mutex> guard(_lock);
if (isFinished())
{
if (result != _result)
LOG_TST("exitTest got " << name(result) << " but is already finished with "
<< name(_result));
return;
}
_result = result;
endTest(reason);
_setRetValue = true;
if (result == TestResult::Ok)
{
LOG_TST("SUCCESS: exitTest: " << name(result) << (reason.empty() ? "" : ": " + reason));
}
else
{
LOG_TST("ERROR: FAILURE: exitTest: " << name(result)
<< (reason.empty() ? "" : ": " + reason));
if (GlobalResult == TestResult::Ok)
GlobalResult = result;
if (!GlobalTestOptions.getKeepgoing() && haveMoreTests())
{
LOG_TST("Failing fast per options, even though there are more tests");
#if !MOBILEAPP
LOG_TST("Setting TerminationFlag as the Test Suite failed");
SigUtil::setTerminationFlag(); // And wakupWorld.
#else
SocketPoll::wakeupWorld();
#endif
return;
}
}
// Check if we have more tests, but keep the current index if it's the last.
if (haveMoreTests())
{
// We have more tests.
++GlobalIndex;
filter();
// Clear the shortcuts.
GlobalKit = nullptr;
GlobalWSD = nullptr;
GlobalTool = nullptr;
if (GlobalArray[GlobalIndex] != nullptr)
{
rememberInstance(_type, GlobalArray[GlobalIndex]);
LOG_TST("Starting test #" << GlobalIndex + 1 << ": "
<< GlobalArray[GlobalIndex]->getTestname());
if (GlobalWSD)
GlobalWSD->configure(Poco::Util::Application::instance().config());
GlobalArray[GlobalIndex]->initialize();
// Wake-up so the previous test stops.
SocketPoll::wakeupWorld();
return;
}
}
// We are done with all the tests.
TST_LOG_NAME("UnitBase", getTestname()
<< " was the last test. Finishing "
<< (GlobalResult == TestResult::Ok ? "SUCCESS" : "FAILED"));
#if !MOBILEAPP
LOG_TST("Setting TerminationFlag as there are no more tests");
SigUtil::setTerminationFlag(); // And wakupWorld.
#else
SocketPoll::wakeupWorld();
#endif
}
void UnitBase::timeout()
{
// Don't timeout if we had already finished.
if (isUnitTesting() && !isFinished())
{
LOG_TST("ERROR: Timed out waiting for unit test to complete within "
<< _timeoutMilliSeconds);
exitTest(TestResult::TimedOut);
}
}
void UnitBase::returnValue(int &retValue)
{
if (_setRetValue)
retValue = (_result == TestResult::Ok ? EX_OK : EX_SOFTWARE);
}
void UnitBase::endTest(const std::string& reason)
{
LOG_TST("Ending test by stopping SocketPoll [" << _socketPoll->name() << "]: " << reason);
_socketPoll->joinThread();
// tell the timeout thread that the work has finished
TimeoutThreadMutex.unlock();
if (TimeoutThread.joinable())
TimeoutThread.join();
LOG_TST("==================== Finished [" << getTestname() << "] ====================");
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */