363 lines
14 KiB
C++
363 lines
14 KiB
C++
|
#include "./helper.h"
|
||
|
|
||
|
#include "../size.h"
|
||
|
#include "../statusprovider.h"
|
||
|
#include "../tagtarget.h"
|
||
|
#include "../signature.h"
|
||
|
#include "../margin.h"
|
||
|
#include "../aspectratio.h"
|
||
|
#include "../mediaformat.h"
|
||
|
#include "../mediafileinfo.h"
|
||
|
#include "../exceptions.h"
|
||
|
#include "../backuphelper.h"
|
||
|
|
||
|
#include <c++utilities/io/catchiofailure.h>
|
||
|
#include <c++utilities/tests/testutils.h>
|
||
|
using namespace TestUtilities;
|
||
|
|
||
|
#include <cppunit/TestFixture.h>
|
||
|
#include <cppunit/extensions/HelperMacros.h>
|
||
|
|
||
|
#include <cstdio>
|
||
|
|
||
|
using namespace std;
|
||
|
using namespace Media;
|
||
|
using namespace IoUtilities;
|
||
|
using namespace TestUtilities::Literals;
|
||
|
|
||
|
using namespace CPPUNIT_NS;
|
||
|
|
||
|
class TestStatusProvider : public StatusProvider
|
||
|
{
|
||
|
public:
|
||
|
TestStatusProvider();
|
||
|
};
|
||
|
|
||
|
TestStatusProvider::TestStatusProvider()
|
||
|
{
|
||
|
}
|
||
|
|
||
|
class UtilitiesTests : public TestFixture {
|
||
|
CPPUNIT_TEST_SUITE(UtilitiesTests);
|
||
|
CPPUNIT_TEST(testSize);
|
||
|
CPPUNIT_TEST(testStatusProvider);
|
||
|
CPPUNIT_TEST(testTagTarget);
|
||
|
CPPUNIT_TEST(testSignature);
|
||
|
CPPUNIT_TEST(testMargin);
|
||
|
CPPUNIT_TEST(testAspectRatio);
|
||
|
CPPUNIT_TEST(testMediaFormat);
|
||
|
#ifdef PLATFORM_UNIX
|
||
|
CPPUNIT_TEST(testBackupFile);
|
||
|
#endif
|
||
|
CPPUNIT_TEST_SUITE_END();
|
||
|
|
||
|
public:
|
||
|
void setUp();
|
||
|
void tearDown();
|
||
|
|
||
|
void testSize();
|
||
|
void testStatusProvider();
|
||
|
void testTagTarget();
|
||
|
void testSignature();
|
||
|
void testMargin();
|
||
|
void testAspectRatio();
|
||
|
void testMediaFormat();
|
||
|
#ifdef PLATFORM_UNIX
|
||
|
void testBackupFile();
|
||
|
#endif
|
||
|
};
|
||
|
|
||
|
CPPUNIT_TEST_SUITE_REGISTRATION(UtilitiesTests);
|
||
|
|
||
|
void UtilitiesTests::setUp()
|
||
|
{
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::tearDown()
|
||
|
{
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::testSize()
|
||
|
{
|
||
|
static_assert(Size().isNull(), "Size::isNull()");
|
||
|
static_assert(!Size(3, 4).isNull(), "Size::isNull()");
|
||
|
static_assert(Size(3, 4).resolution() == 12, "Size::resolution");
|
||
|
|
||
|
Size size(1920, 1080);
|
||
|
CPPUNIT_ASSERT_EQUAL("width: 1920, height: 1080"s, size.toString());
|
||
|
CPPUNIT_ASSERT_EQUAL("1080p"s, string(size.abbreviation()));
|
||
|
size.setWidth(1280);
|
||
|
size.setHeight(720);
|
||
|
CPPUNIT_ASSERT_EQUAL("720p"s, string(size.abbreviation()));
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::testStatusProvider()
|
||
|
{
|
||
|
const string context("unit tests");
|
||
|
TestStatusProvider status, status2;
|
||
|
|
||
|
// notifications
|
||
|
CPPUNIT_ASSERT(!status.hasNotifications());
|
||
|
CPPUNIT_ASSERT_EQUAL(NotificationType::None, status.worstNotificationType());
|
||
|
status.addNotification(NotificationType::Debug, "debug notification", context);
|
||
|
CPPUNIT_ASSERT_EQUAL(NotificationType::Debug, status.worstNotificationType());
|
||
|
CPPUNIT_ASSERT(!status.hasCriticalNotifications());
|
||
|
status.addNotification(NotificationType::Warning, "warning", context);
|
||
|
CPPUNIT_ASSERT_EQUAL(NotificationType::Warning, status.worstNotificationType());
|
||
|
CPPUNIT_ASSERT_EQUAL("warning"s, status.notifications().back().message());
|
||
|
CPPUNIT_ASSERT(!status.hasCriticalNotifications());
|
||
|
status.addNotification(NotificationType::Critical, "error", context);
|
||
|
CPPUNIT_ASSERT_EQUAL(NotificationType::Critical, status.worstNotificationType());
|
||
|
CPPUNIT_ASSERT(status.hasCriticalNotifications());
|
||
|
CPPUNIT_ASSERT_EQUAL(3_st, status.notifications().size());
|
||
|
CPPUNIT_ASSERT(status.hasNotifications());
|
||
|
status2.addNotifications(status);
|
||
|
status.invalidateNotifications();
|
||
|
CPPUNIT_ASSERT(!status.hasNotifications());
|
||
|
CPPUNIT_ASSERT(!status.hasCriticalNotifications());
|
||
|
CPPUNIT_ASSERT_EQUAL(3_st, status2.notifications().size());
|
||
|
status.addNotification(status2.notifications().back());
|
||
|
CPPUNIT_ASSERT(status.hasCriticalNotifications());
|
||
|
|
||
|
// status and percentage
|
||
|
CPPUNIT_ASSERT_EQUAL(string(), status.currentStatus());
|
||
|
CPPUNIT_ASSERT_EQUAL(0.0, status.currentPercentage());
|
||
|
CPPUNIT_ASSERT(!status.isAborted());
|
||
|
bool statusUpdateReceived = false, firstStatusUpdate = true;
|
||
|
const auto callbackId = status.registerCallback([&status, &statusUpdateReceived, &firstStatusUpdate] (StatusProvider &sender) {
|
||
|
CPPUNIT_ASSERT(&status == &sender);
|
||
|
if(firstStatusUpdate) {
|
||
|
CPPUNIT_ASSERT_EQUAL("test"s, sender.currentStatus());
|
||
|
CPPUNIT_ASSERT_EQUAL(0.5, sender.currentPercentage());
|
||
|
firstStatusUpdate = false;
|
||
|
}
|
||
|
sender.tryToAbort();
|
||
|
statusUpdateReceived = true;
|
||
|
});
|
||
|
status.updateStatus("test", 0.5);
|
||
|
CPPUNIT_ASSERT_MESSAGE("status update for updated status received", statusUpdateReceived);
|
||
|
CPPUNIT_ASSERT(status.isAborted());
|
||
|
statusUpdateReceived = false;
|
||
|
status.updatePercentage(0.625);
|
||
|
CPPUNIT_ASSERT_MESSAGE("status update for updated percentage received", statusUpdateReceived);
|
||
|
statusUpdateReceived = false;
|
||
|
status.addNotification(status2.notifications().front());
|
||
|
CPPUNIT_ASSERT_MESSAGE("status update for new notification received", statusUpdateReceived);
|
||
|
statusUpdateReceived = false;
|
||
|
status.unregisterCallback(callbackId);
|
||
|
status.updatePercentage(0.65);
|
||
|
CPPUNIT_ASSERT_MESSAGE("no status update received after callback unregistered", !statusUpdateReceived);
|
||
|
|
||
|
// forwarding
|
||
|
TestStatusProvider forwardReceiver;
|
||
|
status.forwardStatusUpdateCalls(&forwardReceiver);
|
||
|
statusUpdateReceived = false;
|
||
|
forwardReceiver.registerCallback([&status, &statusUpdateReceived] (StatusProvider &sender) {
|
||
|
CPPUNIT_ASSERT(&status == &sender);
|
||
|
CPPUNIT_ASSERT_EQUAL("test2"s, sender.currentStatus());
|
||
|
CPPUNIT_ASSERT_EQUAL(0.75, sender.currentPercentage());
|
||
|
statusUpdateReceived = true;
|
||
|
});
|
||
|
status.updateStatus("test2", 0.75);
|
||
|
CPPUNIT_ASSERT(statusUpdateReceived);
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::testTagTarget()
|
||
|
{
|
||
|
TagTarget target;
|
||
|
CPPUNIT_ASSERT(target.isEmpty());
|
||
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("default level is 50", 50ul, target.level());
|
||
|
CPPUNIT_ASSERT_EQUAL("level 50"s, target.toString(TagTargetLevel::Unspecified));
|
||
|
target = TagTarget(30, {1, 2, 3}, {4}, {5, 6}, {7, 8, 9});
|
||
|
CPPUNIT_ASSERT(!target.isEmpty());
|
||
|
const auto mapping = [] (uint64 level) {
|
||
|
return level == 30 ? TagTargetLevel::Track : TagTargetLevel::Unspecified;
|
||
|
};
|
||
|
CPPUNIT_ASSERT_EQUAL("level 30 'track, song, chapter', track 1, track 2, track 3, chapter 4, edition 5, edition 6, attachment 7, attachment 8, attachment 9"s, target.toString(mapping));
|
||
|
target.setLevel(40);
|
||
|
CPPUNIT_ASSERT_EQUAL("level 40, track 1, track 2, track 3, chapter 4, edition 5, edition 6, attachment 7, attachment 8, attachment 9"s, target.toString(mapping));
|
||
|
target.setLevelName("test");
|
||
|
CPPUNIT_ASSERT_EQUAL("level 40 'test', track 1, track 2, track 3, chapter 4, edition 5, edition 6, attachment 7, attachment 8, attachment 9"s, target.toString(mapping));
|
||
|
CPPUNIT_ASSERT(target == TagTarget(40, {1, 2, 3}, {4}, {5, 6}, {7, 8, 9}));
|
||
|
target.clear();
|
||
|
CPPUNIT_ASSERT(target.isEmpty());
|
||
|
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::testSignature()
|
||
|
{
|
||
|
const unsigned char xzHead[12] = {
|
||
|
0xfd, 0x37, 0x7a, 0x58,
|
||
|
0x5a, 0x00, 0x00, 0x04,
|
||
|
0xe6, 0xd6, 0xb4, 0x46
|
||
|
};
|
||
|
|
||
|
// truncated buffer
|
||
|
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Unknown, parseSignature(reinterpret_cast<const char *>(xzHead), 3));
|
||
|
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Unknown, parseSignature(reinterpret_cast<const char *>(xzHead), 2));
|
||
|
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Unknown, parseSignature(reinterpret_cast<const char *>(xzHead), 0));
|
||
|
|
||
|
const auto containerFormat = parseSignature(reinterpret_cast<const char *>(xzHead), sizeof(xzHead));
|
||
|
CPPUNIT_ASSERT_EQUAL(ContainerFormat::Xz, containerFormat);
|
||
|
CPPUNIT_ASSERT_EQUAL("xz compressed file"s, string(containerFormatName(containerFormat)));
|
||
|
CPPUNIT_ASSERT_EQUAL("xz"s, string(containerFormatAbbreviation(containerFormat)));
|
||
|
CPPUNIT_ASSERT_EQUAL(string(), string(containerFormatSubversion(containerFormat)));
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::testMargin()
|
||
|
{
|
||
|
static_assert(Margin().isNull(), "empty margin");
|
||
|
static_assert(!Margin(0, 2).isNull(), "non-empty margin");
|
||
|
|
||
|
CPPUNIT_ASSERT_EQUAL("top: 1; left: 2; bottom: 3; right: 4"s, Margin(1, 2, 3, 4).toString());
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::testAspectRatio()
|
||
|
{
|
||
|
static_assert(!AspectRatio().isValid(), "invalid aspect ratio");
|
||
|
static_assert(AspectRatio(16, 9).isValid(), "valid aspect ratio");
|
||
|
static_assert(AspectRatio(16, 9).isExtended(), "extended aspect ratio");
|
||
|
|
||
|
const AspectRatio ratio(4);
|
||
|
CPPUNIT_ASSERT_EQUAL(static_cast<uint16>(16), ratio.numerator);
|
||
|
CPPUNIT_ASSERT_EQUAL(static_cast<uint16>(11), ratio.denominator);
|
||
|
const AspectRatio ratio2(77);
|
||
|
CPPUNIT_ASSERT_EQUAL(static_cast<uint16>(0), ratio2.numerator);
|
||
|
CPPUNIT_ASSERT_EQUAL(static_cast<uint16>(0), ratio2.denominator);
|
||
|
}
|
||
|
|
||
|
void UtilitiesTests::testMediaFormat()
|
||
|
{
|
||
|
// unspecific format
|
||
|
MediaFormat aac(GeneralMediaFormat::Aac);
|
||
|
CPPUNIT_ASSERT_EQUAL("Advanced Audio Coding"s, string(aac.name()));
|
||
|
CPPUNIT_ASSERT_EQUAL("AAC"s, string(aac.abbreviation()));
|
||
|
CPPUNIT_ASSERT_EQUAL("AAC"s, string(aac.shortAbbreviation()));
|
||
|
|
||
|
// specific format
|
||
|
aac += MediaFormat(GeneralMediaFormat::Aac, SubFormats::AacMpeg4LowComplexityProfile, ExtensionFormats::SpectralBandReplication);
|
||
|
CPPUNIT_ASSERT(aac == GeneralMediaFormat::Aac);
|
||
|
CPPUNIT_ASSERT(aac != GeneralMediaFormat::Mpeg1Audio);
|
||
|
CPPUNIT_ASSERT_EQUAL("Advanced Audio Coding Low Complexity Profile"s, string(aac.name()));
|
||
|
CPPUNIT_ASSERT_EQUAL("MPEG-4 AAC-LC"s, string(aac.abbreviation()));
|
||
|
CPPUNIT_ASSERT_EQUAL("HE-AAC"s, string(aac.shortAbbreviation()));
|
||
|
CPPUNIT_ASSERT_EQUAL("Spectral Band Replication / HE-AAC"s, string(aac.extensionName()));
|
||
|
}
|
||
|
|
||
|
#ifdef PLATFORM_UNIX
|
||
|
void UtilitiesTests::testBackupFile()
|
||
|
{
|
||
|
using namespace BackupHelper;
|
||
|
|
||
|
// ensure backup directory is empty, so backups will be created in the same directory
|
||
|
// as the original file
|
||
|
backupDirectory().clear();
|
||
|
|
||
|
// setup testfile
|
||
|
MediaFileInfo file(workingCopyPath("unsupported.bin"));
|
||
|
const string workingDir(file.containingDirectory());
|
||
|
file.open();
|
||
|
|
||
|
// create backup file
|
||
|
string backupPath1, backupPath2;
|
||
|
NativeFileStream backupStream1, backupStream2;
|
||
|
createBackupFile(file.path(), backupPath1, file.stream(), backupStream1);
|
||
|
CPPUNIT_ASSERT_EQUAL(workingDir + "/unsupported.bin.bak", backupPath1);
|
||
|
|
||
|
// recreate original file
|
||
|
file.stream().open(file.path(), ios_base::out);
|
||
|
file.stream() << "test1" << endl;
|
||
|
|
||
|
// create a 2nd backup which should not override the first one
|
||
|
createBackupFile(file.path(), backupPath2, file.stream(), backupStream2);
|
||
|
CPPUNIT_ASSERT_EQUAL(workingDir + "/unsupported.bin.1.bak", backupPath2);
|
||
|
|
||
|
// get rid of 2nd backup, recreate original file
|
||
|
backupStream2.close();
|
||
|
remove(backupPath2.data());
|
||
|
file.stream().open(file.path(), ios_base::out);
|
||
|
file.stream() << "test2" << endl;
|
||
|
|
||
|
// create backup under another location
|
||
|
backupDirectory() = workingDir + "/bak";
|
||
|
try {
|
||
|
createBackupFile(file.path(), backupPath2, file.stream(), backupStream2);
|
||
|
CPPUNIT_FAIL("renaming failed because backup dir does not exist");
|
||
|
} catch(...) {
|
||
|
const char *what = catchIoFailure();
|
||
|
CPPUNIT_ASSERT(strstr(what, "Unable to rename original file before rewriting it."));
|
||
|
}
|
||
|
backupStream2.clear();
|
||
|
workingCopyPathMode("bak/unsupported.bin", WorkingCopyMode::NoCopy);
|
||
|
createBackupFile(file.path(), backupPath2, file.stream(), backupStream2);
|
||
|
CPPUNIT_ASSERT_EQUAL(workingDir + "/bak/unsupported.bin", backupPath2);
|
||
|
|
||
|
// get rid of 2nd backup (again)
|
||
|
backupStream2.close();
|
||
|
remove(backupPath2.data());
|
||
|
remove(backupDirectory().data());
|
||
|
|
||
|
// should be able to use backup stream, eg. seek to the end
|
||
|
backupStream1.seekg(0, ios_base::end);
|
||
|
CPPUNIT_ASSERT_EQUAL(41_st, static_cast<size_t>(backupStream1.tellg()));
|
||
|
|
||
|
// restore backup
|
||
|
restoreOriginalFileFromBackupFile(file.path(), backupPath1, file.stream(), backupStream1);
|
||
|
|
||
|
// check restored backup
|
||
|
file.open(true);
|
||
|
file.stream().seekg(0x1D);
|
||
|
CPPUNIT_ASSERT_EQUAL(static_cast<ios::int_type>(0x34), file.stream().get());
|
||
|
file.close();
|
||
|
|
||
|
CPPUNIT_ASSERT_MESSAGE("file has no critical notifications yet", !file.hasCriticalNotifications());
|
||
|
|
||
|
// reset backup dir again
|
||
|
backupDirectory().clear();
|
||
|
|
||
|
// restore after user aborted
|
||
|
createBackupFile(file.path(), backupPath1, file.stream(), backupStream1);
|
||
|
try {
|
||
|
throw OperationAbortedException();
|
||
|
} catch(...) {
|
||
|
CPPUNIT_ASSERT_THROW(handleFailureAfterFileModified(file, backupPath1, file.stream(), backupStream1, "test"), OperationAbortedException);
|
||
|
}
|
||
|
CPPUNIT_ASSERT(!file.hasCriticalNotifications());
|
||
|
CPPUNIT_ASSERT(file.hasNotifications());
|
||
|
CPPUNIT_ASSERT_EQUAL("Rewriting the file to apply changed tag information has been aborted."s, file.notifications().front().message());
|
||
|
CPPUNIT_ASSERT_EQUAL("The original file has been restored."s, file.notifications().back().message());
|
||
|
file.invalidateNotifications();
|
||
|
|
||
|
// restore after error
|
||
|
createBackupFile(file.path(), backupPath1, file.stream(), backupStream1);
|
||
|
try {
|
||
|
throw Failure();
|
||
|
} catch(...) {
|
||
|
CPPUNIT_ASSERT_THROW(handleFailureAfterFileModified(file, backupPath1, file.stream(), backupStream1, "test"), Failure);
|
||
|
}
|
||
|
CPPUNIT_ASSERT(file.hasCriticalNotifications());
|
||
|
CPPUNIT_ASSERT_EQUAL("Rewriting the file to apply changed tag information failed."s, file.notifications().front().message());
|
||
|
CPPUNIT_ASSERT_EQUAL("The original file has been restored."s, file.notifications().back().message());
|
||
|
file.invalidateNotifications();
|
||
|
|
||
|
// restore after io failure
|
||
|
createBackupFile(file.path(), backupPath1, file.stream(), backupStream1);
|
||
|
try {
|
||
|
throwIoFailure("simulated IO failure");
|
||
|
} catch(...) {
|
||
|
try {
|
||
|
handleFailureAfterFileModified(file, backupPath1, file.stream(), backupStream1, "test");
|
||
|
CPPUNIT_FAIL("IO failure rethrown");
|
||
|
} catch(...) {
|
||
|
catchIoFailure();
|
||
|
}
|
||
|
}
|
||
|
CPPUNIT_ASSERT(file.hasCriticalNotifications());
|
||
|
CPPUNIT_ASSERT_EQUAL("An IO error occured when rewriting the file to apply changed tag information."s, file.notifications().front().message());
|
||
|
CPPUNIT_ASSERT_EQUAL("The original file has been restored."s, file.notifications().back().message());
|
||
|
file.invalidateNotifications();
|
||
|
|
||
|
remove(file.path().data());
|
||
|
}
|
||
|
#endif
|