diff --git a/libraries/recording/src/recording/Clip.cpp b/libraries/recording/src/recording/Clip.cpp index ef59532f09..09acf0579f 100644 --- a/libraries/recording/src/recording/Clip.cpp +++ b/libraries/recording/src/recording/Clip.cpp @@ -16,33 +16,34 @@ using namespace recording; Clip::Pointer Clip::fromFile(const QString& filePath) { - return std::make_shared(filePath); + auto result = std::make_shared(filePath); + if (result->frameCount() == 0) { + return Clip::Pointer(); + } + return result; } -void Clip::toFile(Clip::Pointer clip, const QString& filePath) { - // FIXME +void Clip::toFile(const QString& filePath, Clip::Pointer clip) { + FileClip::write(filePath, clip->duplicate()); +} + +Clip::Pointer Clip::newClip() { + return std::make_shared(); } Clip::Pointer Clip::duplicate() { Clip::Pointer result = std::make_shared(); + Locker lock(_mutex); float currentPosition = position(); seek(0); Frame::Pointer frame = nextFrame(); while (frame) { - result->appendFrame(frame); + result->addFrame(frame); + frame = nextFrame(); } seek(currentPosition); return result; } - -#if 0 -Clip::Pointer Clip::fromIODevice(QIODevice * device) { - return std::make_shared(device); -} - -void Clip::fromIODevice(Clip::Pointer clip, QIODevice * device) { -} -#endif \ No newline at end of file diff --git a/libraries/recording/src/recording/Clip.h b/libraries/recording/src/recording/Clip.h index ca77ba8969..e7034ef077 100644 --- a/libraries/recording/src/recording/Clip.h +++ b/libraries/recording/src/recording/Clip.h @@ -12,35 +12,44 @@ #include "Forward.h" +#include + #include class QIODevice; namespace recording { -class Clip : public QObject { +class Clip { public: using Pointer = std::shared_ptr; - Clip(QObject* parent = nullptr) : QObject(parent) {} virtual ~Clip() {} Pointer duplicate(); + virtual float duration() const = 0; + virtual size_t frameCount() const = 0; + virtual void seek(float offset) = 0; virtual float position() const = 0; virtual FramePointer peekFrame() const = 0; virtual FramePointer nextFrame() = 0; virtual void skipFrame() = 0; - virtual void appendFrame(FramePointer) = 0; - + virtual void addFrame(FramePointer) = 0; static Pointer fromFile(const QString& filePath); - static void toFile(Pointer clip, const QString& filePath); + static void toFile(const QString& filePath, Pointer clip); + static Pointer newClip(); protected: + using Mutex = std::recursive_mutex; + using Locker = std::unique_lock; + virtual void reset() = 0; + + mutable Mutex _mutex; }; } diff --git a/libraries/recording/src/recording/Frame.h b/libraries/recording/src/recording/Frame.h index 2834637a6b..0fb95c4b2e 100644 --- a/libraries/recording/src/recording/Frame.h +++ b/libraries/recording/src/recording/Frame.h @@ -29,6 +29,10 @@ public: float timeOffset { 0 }; QByteArray data; + Frame() {} + Frame(FrameType type, float timeOffset, const QByteArray& data) + : type(type), timeOffset(timeOffset), data(data) {} + static FrameType registerFrameType(const QString& frameTypeName); static QMap getFrameTypes(); static QMap getFrameTypeNames(); diff --git a/libraries/recording/src/recording/Logging.cpp b/libraries/recording/src/recording/Logging.cpp new file mode 100644 index 0000000000..5673e6e175 --- /dev/null +++ b/libraries/recording/src/recording/Logging.cpp @@ -0,0 +1,11 @@ +// +// Created by Bradley Austin Davis 2015/10/11 +// Copyright 2015 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 "Logging.h" + +Q_LOGGING_CATEGORY(recordingLog, "hifi.recording") diff --git a/libraries/recording/src/recording/Logging.h b/libraries/recording/src/recording/Logging.h new file mode 100644 index 0000000000..a1b28329d7 --- /dev/null +++ b/libraries/recording/src/recording/Logging.h @@ -0,0 +1,16 @@ +// +// Created by Bradley Austin Davis 2015/10/11 +// Copyright 2015 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 +// + +#ifndef hifi_Controllers_Logging_h +#define hifi_Controllers_Logging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(recordingLog) + +#endif diff --git a/libraries/recording/src/recording/Recorder.cpp b/libraries/recording/src/recording/Recorder.cpp index a38e4252b9..b2e7399cd4 100644 --- a/libraries/recording/src/recording/Recorder.cpp +++ b/libraries/recording/src/recording/Recorder.cpp @@ -51,7 +51,7 @@ void Recorder::recordFrame(FrameType type, QByteArray frameData) { frame->type = type; frame->data = frameData; frame->timeOffset = (float)(_elapsed + _timer.elapsed()) / MSECS_PER_SECOND; - _clip->appendFrame(frame); + _clip->addFrame(frame); } ClipPointer Recorder::getClip() { diff --git a/libraries/recording/src/recording/impl/BufferClip.cpp b/libraries/recording/src/recording/impl/BufferClip.cpp index 378fee2558..4d5a910d42 100644 --- a/libraries/recording/src/recording/impl/BufferClip.cpp +++ b/libraries/recording/src/recording/impl/BufferClip.cpp @@ -51,11 +51,15 @@ FramePointer BufferClip::nextFrame() { return result; } -void BufferClip::appendFrame(FramePointer newFrame) { +void BufferClip::addFrame(FramePointer newFrame) { + if (newFrame->timeOffset < 0.0f) { + throw std::runtime_error("Frames may not have negative time offsets"); + } auto currentPosition = position(); seek(newFrame->timeOffset); { Locker lock(_mutex); + _frames.insert(_frames.begin() + _frameIndex, newFrame); } seek(currentPosition); @@ -72,3 +76,15 @@ void BufferClip::reset() { Locker lock(_mutex); _frameIndex = 0; } + +float BufferClip::duration() const { + if (_frames.empty()) { + return 0; + } + return (*_frames.rbegin())->timeOffset; +} + +size_t BufferClip::frameCount() const { + return _frames.size(); +} + diff --git a/libraries/recording/src/recording/impl/BufferClip.h b/libraries/recording/src/recording/impl/BufferClip.h index 8fd78a9091..b40687a4ec 100644 --- a/libraries/recording/src/recording/impl/BufferClip.h +++ b/libraries/recording/src/recording/impl/BufferClip.h @@ -20,25 +20,23 @@ class BufferClip : public Clip { public: using Pointer = std::shared_ptr; - BufferClip(QObject* parent = nullptr) : Clip(parent) {} virtual ~BufferClip() {} + virtual float duration() const override; + virtual size_t frameCount() const override; + virtual void seek(float offset) override; virtual float position() const override; virtual FramePointer peekFrame() const override; virtual FramePointer nextFrame() override; virtual void skipFrame() override; - virtual void appendFrame(FramePointer) override; + virtual void addFrame(FramePointer) override; private: - using Mutex = std::mutex; - using Locker = std::unique_lock; - virtual void reset() override; std::vector _frames; - mutable Mutex _mutex; mutable size_t _frameIndex { 0 }; }; diff --git a/libraries/recording/src/recording/impl/FileClip.cpp b/libraries/recording/src/recording/impl/FileClip.cpp index 354a97c5db..be7230e3f8 100644 --- a/libraries/recording/src/recording/impl/FileClip.cpp +++ b/libraries/recording/src/recording/impl/FileClip.cpp @@ -8,42 +8,197 @@ #include "FileClip.h" -#include "../Frame.h" - #include +#include +#include +#include + +#include + +#include "../Frame.h" +#include "../Logging.h" + + using namespace recording; -static const qint64 MINIMUM_FRAME_SIZE = sizeof(FrameType) + sizeof(float) + sizeof(uint16_t) + 1; +static const qint64 MINIMUM_FRAME_SIZE = sizeof(FrameType) + sizeof(float) + sizeof(uint16_t); -FileClip::FileClip(const QString& fileName, QObject* parent) : Clip(parent), _file(fileName) { - auto size = _file.size(); - _map = _file.map(0, size, QFile::MapPrivateOption); +static const QString FRAME_TYPE_MAP = QStringLiteral("frameTypes"); - auto current = _map; +using FrameHeaderList = std::list; +using FrameTranslationMap = QMap; + +FrameTranslationMap parseTranslationMap(const QJsonDocument& doc) { + FrameTranslationMap results; + auto headerObj = doc.object(); + if (headerObj.contains(FRAME_TYPE_MAP)) { + auto frameTypeObj = headerObj[FRAME_TYPE_MAP].toObject(); + auto currentFrameTypes = Frame::getFrameTypes(); + for (auto frameTypeName : frameTypeObj.keys()) { + qDebug() << frameTypeName; + if (!currentFrameTypes.contains(frameTypeName)) { + continue; + } + FrameType currentTypeEnum = currentFrameTypes[frameTypeName]; + FrameType storedTypeEnum = static_cast(frameTypeObj[frameTypeName].toInt()); + results[storedTypeEnum] = currentTypeEnum; + } + } + return results; +} + + +FrameHeaderList parseFrameHeaders(uchar* const start, const qint64& size) { + using FrameHeader = FileClip::FrameHeader; + FrameHeaderList results; + auto current = start; auto end = current + size; // Read all the frame headers - while (end - current < MINIMUM_FRAME_SIZE) { + // FIXME move to Frame::readHeader? + while (end - current >= MINIMUM_FRAME_SIZE) { FrameHeader header; memcpy(&(header.type), current, sizeof(FrameType)); current += sizeof(FrameType); - memcpy(&(header.timeOffset), current, sizeof(FrameType)); + memcpy(&(header.timeOffset), current, sizeof(float)); current += sizeof(float); memcpy(&(header.size), current, sizeof(uint16_t)); current += sizeof(uint16_t); - header.fileOffset = current - _map; + header.fileOffset = current - start; if (end - current < header.size) { + current = end; break; } - - _frameHeaders.push_back(header); + current += header.size; + results.push_back(header); } + return results; +} + + +FileClip::FileClip(const QString& fileName) : _file(fileName) { + auto size = _file.size(); + bool opened = _file.open(QIODevice::ReadOnly); + if (!opened) { + qCWarning(recordingLog) << "Unable to open file " << fileName; + return; + } + _map = _file.map(0, size, QFile::MapPrivateOption); + if (!_map) { + qCWarning(recordingLog) << "Unable to map file " << fileName; + return; + } + + FrameHeaderList parsedFrameHeaders = parseFrameHeaders(_map, size); + + // Verify that at least one frame exists and that the first frame is a header + if (0 == parsedFrameHeaders.size()) { + qWarning() << "No frames found, invalid file"; + return; + } + + // Grab the file header + { + auto fileHeaderFrameHeader = *parsedFrameHeaders.begin(); + parsedFrameHeaders.pop_front(); + if (fileHeaderFrameHeader.type != Frame::TYPE_HEADER) { + qWarning() << "Missing header frame, invalid file"; + return; + } + + QByteArray fileHeaderData((char*)_map + fileHeaderFrameHeader.fileOffset, fileHeaderFrameHeader.size); + _fileHeader = QJsonDocument::fromBinaryData(fileHeaderData); + } + + // Find the type enum translation map and fix up the frame headers + { + FrameTranslationMap translationMap = parseTranslationMap(_fileHeader); + if (translationMap.empty()) { + qWarning() << "Header missing frame type map, invalid file"; + return; + } + + // Update the loaded headers with the frame data + _frameHeaders.reserve(parsedFrameHeaders.size()); + for (auto& frameHeader : parsedFrameHeaders) { + if (!translationMap.contains(frameHeader.type)) { + continue; + } + frameHeader.type = translationMap[frameHeader.type]; + _frameHeaders.push_back(frameHeader); + } + } +} + +// FIXME move to frame? +bool writeFrame(QIODevice& output, const Frame& frame) { + auto written = output.write((char*)&(frame.type), sizeof(FrameType)); + if (written != sizeof(FrameType)) { + return false; + } + written = output.write((char*)&(frame.timeOffset), sizeof(float)); + if (written != sizeof(float)) { + return false; + } + uint16_t dataSize = frame.data.size(); + written = output.write((char*)&dataSize, sizeof(uint16_t)); + if (written != sizeof(uint16_t)) { + return false; + } + if (dataSize != 0) { + written = output.write(frame.data); + if (written != dataSize) { + return false; + } + } + return true; +} + +bool FileClip::write(const QString& fileName, Clip::Pointer clip) { + qCDebug(recordingLog) << "Writing clip to file " << fileName; + + if (0 == clip->frameCount()) { + return false; + } + + QFile outputFile(fileName); + if (!outputFile.open(QFile::Truncate | QFile::WriteOnly)) { + return false; + } + + Finally closer([&] { outputFile.close(); }); + { + auto frameTypes = Frame::getFrameTypes(); + QJsonObject frameTypeObj; + for (const auto& frameTypeName : frameTypes.keys()) { + frameTypeObj[frameTypeName] = frameTypes[frameTypeName]; + } + + QJsonObject rootObject; + rootObject.insert(FRAME_TYPE_MAP, frameTypeObj); + QByteArray headerFrameData = QJsonDocument(rootObject).toBinaryData(); + if (!writeFrame(outputFile, Frame({ Frame::TYPE_HEADER, 0, headerFrameData }))) { + return false; + } + } + + clip->seek(0); + for (auto frame = clip->nextFrame(); frame; frame = clip->nextFrame()) { + if (!writeFrame(outputFile, *frame)) { + return false; + } + } + outputFile.close(); + return true; } FileClip::~FileClip() { Locker lock(_mutex); _file.unmap(_map); _map = nullptr; + if (_file.isOpen()) { + _file.close(); + } } void FileClip::seek(float offset) { @@ -72,7 +227,9 @@ FramePointer FileClip::readFrame(uint32_t frameIndex) const { const FrameHeader& header = _frameHeaders[frameIndex]; result->type = header.type; result->timeOffset = header.timeOffset; - result->data.insert(0, reinterpret_cast(_map)+header.fileOffset, header.size); + if (header.size) { + result->data.insert(0, reinterpret_cast(_map)+header.fileOffset, header.size); + } } return result; } @@ -99,7 +256,18 @@ void FileClip::reset() { _frameIndex = 0; } -void FileClip::appendFrame(FramePointer) { +void FileClip::addFrame(FramePointer) { throw std::runtime_error("File clips are read only"); } +float FileClip::duration() const { + if (_frameHeaders.empty()) { + return 0; + } + return _frameHeaders.rbegin()->timeOffset; +} + +size_t FileClip::frameCount() const { + return _frameHeaders.size(); +} + diff --git a/libraries/recording/src/recording/impl/FileClip.h b/libraries/recording/src/recording/impl/FileClip.h index 9b13adc9ef..08eacd8337 100644 --- a/libraries/recording/src/recording/impl/FileClip.h +++ b/libraries/recording/src/recording/impl/FileClip.h @@ -13,6 +13,7 @@ #include "../Clip.h" #include +#include #include @@ -22,22 +23,25 @@ class FileClip : public Clip { public: using Pointer = std::shared_ptr; - FileClip(const QString& file, QObject* parent = nullptr); + FileClip(const QString& file); virtual ~FileClip(); + virtual float duration() const override; + virtual size_t frameCount() const override; + virtual void seek(float offset) override; virtual float position() const override; virtual FramePointer peekFrame() const override; virtual FramePointer nextFrame() override; - virtual void appendFrame(FramePointer) override; virtual void skipFrame() override; + virtual void addFrame(FramePointer) override; -private: - using Mutex = std::mutex; - using Locker = std::unique_lock; + const QJsonDocument& getHeader() { + return _fileHeader; + } - virtual void reset() override; + static bool write(const QString& filePath, Clip::Pointer clip); struct FrameHeader { FrameType type; @@ -46,15 +50,20 @@ private: quint64 fileOffset; }; - using FrameHeaders = std::vector; +private: + + virtual void reset() override; + + + using FrameHeaderVector = std::vector; FramePointer readFrame(uint32_t frameIndex) const; - mutable Mutex _mutex; + QJsonDocument _fileHeader; QFile _file; uint32_t _frameIndex { 0 }; - uchar* _map; - FrameHeaders _frameHeaders; + uchar* _map { nullptr }; + FrameHeaderVector _frameHeaders; }; } diff --git a/tests/recording/CMakeLists.txt b/tests/recording/CMakeLists.txt index f1e130956d..a523947f52 100644 --- a/tests/recording/CMakeLists.txt +++ b/tests/recording/CMakeLists.txt @@ -1,10 +1,16 @@ +set(TARGET_NAME recording-test) +# This is not a testcase -- just set it up as a regular hifi project +setup_hifi_project(Test) +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") +link_hifi_libraries(shared recording) +copy_dlls_beside_windows_executable() +# FIXME convert to unit tests # Declare dependencies -macro (setup_testcase_dependencies) - # link in the shared libraries - link_hifi_libraries(shared recording) - - copy_dlls_beside_windows_executable() -endmacro () - -setup_hifi_testcase() +#macro (setup_testcase_dependencies) +# # link in the shared libraries +# link_hifi_libraries(shared recording) +# +# copy_dlls_beside_windows_executable() +#endmacro () +#setup_hifi_testcase() diff --git a/tests/recording/src/Constants.h b/tests/recording/src/Constants.h index 37758bfd68..2aa19fe786 100644 --- a/tests/recording/src/Constants.h +++ b/tests/recording/src/Constants.h @@ -11,6 +11,8 @@ #ifndef hifi_Constants_h #define hifi_Constants_h +#include + static const QString HEADER_NAME = "com.highfidelity.recording.Header"; static const QString TEST_NAME = "com.highfidelity.recording.Test"; diff --git a/tests/recording/src/FrameTests.cpp b/tests/recording/src/FrameTests.cpp index f61d45c197..ebd7f90e31 100644 --- a/tests/recording/src/FrameTests.cpp +++ b/tests/recording/src/FrameTests.cpp @@ -8,6 +8,9 @@ #include "FrameTests.h" #include "Constants.h" + +#if 0 + #include "../QTestExtensions.h" #include @@ -27,3 +30,4 @@ void FrameTests::registerFrameTypeTest() { QCOMPARE(backMap[recording::Frame::TYPE_HEADER], HEADER_NAME); } +#endif diff --git a/tests/recording/src/FrameTests.h b/tests/recording/src/FrameTests.h index 04f73532a5..bdf4542846 100644 --- a/tests/recording/src/FrameTests.h +++ b/tests/recording/src/FrameTests.h @@ -10,6 +10,7 @@ #ifndef hifi_FrameTests_h #define hifi_FrameTests_h +#if 0 #include class FrameTests : public QObject { @@ -18,4 +19,6 @@ private slots: void registerFrameTypeTest(); }; +#endif + #endif // hifi_FrameTests_h diff --git a/tests/recording/src/RecorderTests.cpp b/tests/recording/src/RecorderTests.cpp index 76b4b46577..b102a3c931 100644 --- a/tests/recording/src/RecorderTests.cpp +++ b/tests/recording/src/RecorderTests.cpp @@ -8,6 +8,9 @@ #include "RecorderTests.h" #include "Constants.h" + +#if 0 + #include "../QTestExtensions.h" #include @@ -23,3 +26,4 @@ void RecorderTests::recorderTest() { //QCOMPARE(recoreder.isRecording(), false); } +#endif diff --git a/tests/recording/src/RecorderTests.h b/tests/recording/src/RecorderTests.h index 8e97a828a2..9bfd8e2d10 100644 --- a/tests/recording/src/RecorderTests.h +++ b/tests/recording/src/RecorderTests.h @@ -10,6 +10,8 @@ #ifndef hifi_RecorderTests_h #define hifi_RecorderTests_h +#if 0 + #include class RecorderTests : public QObject { @@ -19,3 +21,5 @@ private slots: }; #endif + +#endif diff --git a/tests/recording/src/main.cpp b/tests/recording/src/main.cpp new file mode 100644 index 0000000000..836d8b5ac1 --- /dev/null +++ b/tests/recording/src/main.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include + +#ifdef Q_OS_WIN32 +#include +#endif + +#include +#include + +#include "Constants.h" + +#define QVERIFY Q_ASSERT + +using namespace recording; +FrameType TEST_FRAME_TYPE { Frame::TYPE_INVALID }; + +void testFrameTypeRegistration() { + TEST_FRAME_TYPE = Frame::registerFrameType(TEST_NAME); + QVERIFY(TEST_FRAME_TYPE != Frame::TYPE_INVALID); + QVERIFY(TEST_FRAME_TYPE != Frame::TYPE_HEADER); + + auto forwardMap = recording::Frame::getFrameTypes(); + QVERIFY(forwardMap.count(TEST_NAME) == 1); + QVERIFY(forwardMap[TEST_NAME] == TEST_FRAME_TYPE); + QVERIFY(forwardMap[HEADER_NAME] == recording::Frame::TYPE_HEADER); + + auto backMap = recording::Frame::getFrameTypeNames(); + QVERIFY(backMap.count(TEST_FRAME_TYPE) == 1); + QVERIFY(backMap[TEST_FRAME_TYPE] == TEST_NAME); + QVERIFY(backMap[recording::Frame::TYPE_HEADER] == HEADER_NAME); +} + +void testFilePersist() { + QTemporaryFile file; + QString fileName; + if (file.open()) { + fileName = file.fileName(); + file.close(); + } + auto readClip = Clip::fromFile(fileName); + QVERIFY(Clip::Pointer() == readClip); + auto writeClip = Clip::newClip(); + writeClip->addFrame(std::make_shared(TEST_FRAME_TYPE, 5.0f, QByteArray())); + QVERIFY(writeClip->frameCount() == 1); + QVERIFY(writeClip->duration() == 5.0f); + + Clip::toFile(fileName, writeClip); + readClip = Clip::fromFile(fileName); + QVERIFY(readClip != Clip::Pointer()); + QVERIFY(readClip->frameCount() == 1); + QVERIFY(readClip->duration() == 5.0f); + readClip->seek(0); + writeClip->seek(0); + + size_t count = 0; + for (auto readFrame = readClip->nextFrame(), writeFrame = writeClip->nextFrame(); readFrame && writeFrame; + readFrame = readClip->nextFrame(), writeFrame = writeClip->nextFrame(), ++count) { + QVERIFY(readFrame->type == writeFrame->type); + QVERIFY(readFrame->timeOffset == writeFrame->timeOffset); + QVERIFY(readFrame->data == writeFrame->data); + } + QVERIFY(readClip->frameCount() == count); + + + writeClip = Clip::newClip(); + writeClip->addFrame(std::make_shared(TEST_FRAME_TYPE, 5.0f, QByteArray())); + // Simulate an unknown frametype + writeClip->addFrame(std::make_shared(Frame::TYPE_INVALID - 1, 10.0f, QByteArray())); + QVERIFY(writeClip->frameCount() == 2); + QVERIFY(writeClip->duration() == 10.0f); + Clip::toFile(fileName, writeClip); + + // Verify that the read version of the clip ignores the unknown frame type + readClip = Clip::fromFile(fileName); + QVERIFY(readClip != Clip::Pointer()); + QVERIFY(readClip->frameCount() == 1); + QVERIFY(readClip->duration() == 5.0f); +} + +void testClipOrdering() { + auto writeClip = Clip::newClip(); + // simulate our of order addition of frames + writeClip->addFrame(std::make_shared(TEST_FRAME_TYPE, 10.0f, QByteArray())); + writeClip->addFrame(std::make_shared(TEST_FRAME_TYPE, 5.0f, QByteArray())); + QVERIFY(writeClip->frameCount() == 2); + QVERIFY(writeClip->duration() == 10.0f); + + QVERIFY(std::numeric_limits::max() == writeClip->position()); + writeClip->seek(0); + QVERIFY(5.0f == writeClip->position()); + float lastFrameTimeOffset { 0 }; + for (auto writeFrame = writeClip->nextFrame(); writeFrame; writeFrame = writeClip->nextFrame()) { + QVERIFY(writeClip->position() >= lastFrameTimeOffset); + } +} + +#ifdef Q_OS_WIN32 +void myMessageHandler(QtMsgType type, const QMessageLogContext & context, const QString & msg) { + OutputDebugStringA(msg.toLocal8Bit().toStdString().c_str()); + OutputDebugStringA("\n"); +} +#endif + +int main(int, const char**) { +#ifdef Q_OS_WIN32 + qInstallMessageHandler(myMessageHandler); +#endif + testFrameTypeRegistration(); + testFilePersist(); + testClipOrdering(); +} \ No newline at end of file