395 lines
16 KiB
C++
395 lines
16 KiB
C++
#include "./helper.h"
|
|
|
|
#include "../aspectratio.h"
|
|
#include "../backuphelper.h"
|
|
#include "../diagnostics.h"
|
|
#include "../exceptions.h"
|
|
#include "../margin.h"
|
|
#include "../mediafileinfo.h"
|
|
#include "../mediaformat.h"
|
|
#include "../positioninset.h"
|
|
#include "../progressfeedback.h"
|
|
#include "../signature.h"
|
|
#include "../size.h"
|
|
#include "../tagtarget.h"
|
|
|
|
#include <c++utilities/conversion/stringbuilder.h>
|
|
#include <c++utilities/tests/testutils.h>
|
|
using namespace CppUtilities;
|
|
|
|
#include <cppunit/TestFixture.h>
|
|
#include <cppunit/extensions/HelperMacros.h>
|
|
|
|
#include <cstdio>
|
|
#include <regex>
|
|
|
|
#include <unistd.h>
|
|
|
|
using namespace std;
|
|
using namespace CppUtilities::Literals;
|
|
using namespace TagParser;
|
|
using namespace CPPUNIT_NS;
|
|
|
|
/*!
|
|
* \brief The UtilitiesTests class tests various utility classes and functions of the tagparser library.
|
|
*/
|
|
class UtilitiesTests : public TestFixture {
|
|
CPPUNIT_TEST_SUITE(UtilitiesTests);
|
|
CPPUNIT_TEST(testSize);
|
|
CPPUNIT_TEST(testTagTarget);
|
|
CPPUNIT_TEST(testSignature);
|
|
CPPUNIT_TEST(testMargin);
|
|
CPPUNIT_TEST(testAspectRatio);
|
|
CPPUNIT_TEST(testMediaFormat);
|
|
CPPUNIT_TEST(testPositionInSet);
|
|
CPPUNIT_TEST(testProgressFeedback);
|
|
CPPUNIT_TEST(testAbortableProgressFeedback);
|
|
CPPUNIT_TEST(testDiagnostics);
|
|
CPPUNIT_TEST(testBackupFile);
|
|
CPPUNIT_TEST_SUITE_END();
|
|
|
|
public:
|
|
void setUp() override;
|
|
void tearDown() override;
|
|
|
|
void testSize();
|
|
void testStatusProvider();
|
|
void testTagTarget();
|
|
void testSignature();
|
|
void testMargin();
|
|
void testAspectRatio();
|
|
void testMediaFormat();
|
|
void testPositionInSet();
|
|
void testProgressFeedback();
|
|
void testAbortableProgressFeedback();
|
|
void testDiagnostics();
|
|
void testBackupFile();
|
|
};
|
|
|
|
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::testTagTarget()
|
|
{
|
|
TagTarget target;
|
|
CPPUNIT_ASSERT(target.isEmpty());
|
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("default level is 50", static_cast<std::uint64_t>(50), 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 = [](std::uint64_t 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<std::uint16_t>(16), ratio.numerator);
|
|
CPPUNIT_ASSERT_EQUAL(static_cast<std::uint16_t>(11), ratio.denominator);
|
|
const AspectRatio ratio2(77);
|
|
CPPUNIT_ASSERT_EQUAL(static_cast<std::uint16_t>(0), ratio2.numerator);
|
|
CPPUNIT_ASSERT_EQUAL(static_cast<std::uint16_t>(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()));
|
|
}
|
|
|
|
void UtilitiesTests::testPositionInSet()
|
|
{
|
|
const PositionInSet empty;
|
|
CPPUNIT_ASSERT(empty.isNull());
|
|
CPPUNIT_ASSERT_EQUAL(0, empty.position());
|
|
CPPUNIT_ASSERT_EQUAL(0, empty.total());
|
|
CPPUNIT_ASSERT_EQUAL(""s, empty.toString());
|
|
|
|
const PositionInSet oneOfThree(1, 3);
|
|
CPPUNIT_ASSERT(!oneOfThree.isNull());
|
|
CPPUNIT_ASSERT_EQUAL(1, oneOfThree.position());
|
|
CPPUNIT_ASSERT_EQUAL(3, oneOfThree.total());
|
|
CPPUNIT_ASSERT_EQUAL("1/3"s, oneOfThree.toString());
|
|
|
|
const PositionInSet posOnly(5, 0);
|
|
CPPUNIT_ASSERT(!posOnly.isNull());
|
|
CPPUNIT_ASSERT_EQUAL(5, posOnly.position());
|
|
CPPUNIT_ASSERT_EQUAL(0, posOnly.total());
|
|
CPPUNIT_ASSERT_EQUAL("5"s, posOnly.toString());
|
|
|
|
const PositionInSet totalOnly(0, 5);
|
|
CPPUNIT_ASSERT(!totalOnly.isNull());
|
|
CPPUNIT_ASSERT_EQUAL(0, totalOnly.position());
|
|
CPPUNIT_ASSERT_EQUAL(5, totalOnly.total());
|
|
CPPUNIT_ASSERT_EQUAL("/5"s, totalOnly.toString());
|
|
}
|
|
|
|
void UtilitiesTests::testProgressFeedback()
|
|
{
|
|
unsigned int steps = 0;
|
|
string step;
|
|
unsigned int stepPercentage;
|
|
unsigned int overallPercentage = 0;
|
|
|
|
ProgressFeedback progress(
|
|
[&](const ProgressFeedback &progress) {
|
|
++steps;
|
|
step = progress.step();
|
|
stepPercentage = progress.stepPercentage();
|
|
overallPercentage = progress.overallPercentage();
|
|
},
|
|
[&](const ProgressFeedback &progress) {
|
|
stepPercentage = progress.stepPercentage();
|
|
overallPercentage = progress.overallPercentage();
|
|
});
|
|
CPPUNIT_ASSERT_EQUAL(0u, steps);
|
|
progress.updateOverallPercentage(25);
|
|
CPPUNIT_ASSERT_EQUAL(0u, steps);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
progress.updateStep("foo", 45);
|
|
CPPUNIT_ASSERT_EQUAL(1u, steps);
|
|
CPPUNIT_ASSERT_EQUAL("foo"s, step);
|
|
CPPUNIT_ASSERT_EQUAL(45u, stepPercentage);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
progress.updateStepPercentage(60);
|
|
CPPUNIT_ASSERT_EQUAL(1u, steps);
|
|
CPPUNIT_ASSERT_EQUAL("foo"s, step);
|
|
CPPUNIT_ASSERT_EQUAL(60u, stepPercentage);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
progress.updateStepPercentageFromFraction(0.75);
|
|
CPPUNIT_ASSERT_EQUAL(1u, steps);
|
|
CPPUNIT_ASSERT_EQUAL("foo"s, step);
|
|
CPPUNIT_ASSERT_EQUAL(75u, stepPercentage);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
}
|
|
|
|
void UtilitiesTests::testAbortableProgressFeedback()
|
|
{
|
|
unsigned int steps = 0;
|
|
string step;
|
|
unsigned int stepPercentage;
|
|
unsigned int overallPercentage = 0;
|
|
|
|
AbortableProgressFeedback progress(
|
|
[&](const AbortableProgressFeedback &progress) {
|
|
++steps;
|
|
step = progress.step();
|
|
stepPercentage = progress.stepPercentage();
|
|
overallPercentage = progress.overallPercentage();
|
|
},
|
|
[&](const AbortableProgressFeedback &progress) {
|
|
stepPercentage = progress.stepPercentage();
|
|
overallPercentage = progress.overallPercentage();
|
|
});
|
|
CPPUNIT_ASSERT(!progress.isAborted());
|
|
CPPUNIT_ASSERT_NO_THROW_MESSAGE("stop does nothing if not aborted", progress.stopIfAborted());
|
|
CPPUNIT_ASSERT_EQUAL(0u, steps);
|
|
progress.updateOverallPercentage(25);
|
|
CPPUNIT_ASSERT_EQUAL(0u, steps);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
progress.updateStep("foo", 45);
|
|
CPPUNIT_ASSERT_EQUAL(1u, steps);
|
|
CPPUNIT_ASSERT_EQUAL("foo"s, step);
|
|
CPPUNIT_ASSERT_EQUAL(45u, stepPercentage);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
CPPUNIT_ASSERT_NO_THROW_MESSAGE("next step continues if not aborted", progress.nextStepOrStop("bar", 33));
|
|
CPPUNIT_ASSERT_EQUAL(2u, steps);
|
|
CPPUNIT_ASSERT_EQUAL("bar"s, step);
|
|
CPPUNIT_ASSERT_EQUAL(33u, stepPercentage);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
progress.tryToAbort();
|
|
CPPUNIT_ASSERT(progress.isAborted());
|
|
CPPUNIT_ASSERT_THROW(progress.nextStepOrStop("not going to happen", 33), OperationAbortedException);
|
|
CPPUNIT_ASSERT_EQUAL(2u, steps);
|
|
CPPUNIT_ASSERT_EQUAL("bar"s, step);
|
|
CPPUNIT_ASSERT_EQUAL(33u, stepPercentage);
|
|
CPPUNIT_ASSERT_EQUAL(25u, overallPercentage);
|
|
}
|
|
|
|
void UtilitiesTests::testDiagnostics()
|
|
{
|
|
Diagnostics diag;
|
|
CPPUNIT_ASSERT_EQUAL(DiagLevel::None, diag.level());
|
|
diag.emplace_back(DiagLevel::Warning, "warning msg", "context");
|
|
CPPUNIT_ASSERT_EQUAL(DiagLevel::Warning, diag.level());
|
|
CPPUNIT_ASSERT(!diag.has(DiagLevel::Critical));
|
|
diag.emplace_back(DiagLevel::Critical, "critical msg", "context");
|
|
CPPUNIT_ASSERT_EQUAL(DiagLevel::Critical, diag.level());
|
|
CPPUNIT_ASSERT(diag.has(DiagLevel::Critical));
|
|
}
|
|
|
|
void UtilitiesTests::testBackupFile()
|
|
{
|
|
using namespace BackupHelper;
|
|
|
|
// setup testfile
|
|
MediaFileInfo file(workingCopyPath("unsupported.bin"));
|
|
file.setBackupDirectory(string()); // ensure backup directory is empty, so backups will be created in the same directory as the original file
|
|
const auto workingDir(file.containingDirectory());
|
|
file.open();
|
|
|
|
// create backup file
|
|
string backupPath1, backupPath2;
|
|
NativeFileStream backupStream1, backupStream2;
|
|
createBackupFile(string(), file.path(), backupPath1, file.stream(), backupStream1);
|
|
CPPUNIT_ASSERT_EQUAL(workingDir + "/unsupported.bin.bak", backupPath1);
|
|
|
|
// recreate original file (like the 'make' methods would do to apply changes)
|
|
file.stream().open(file.path(), ios_base::out);
|
|
file.stream() << "test1" << endl;
|
|
|
|
// create a 2nd backup which should not override the first one
|
|
createBackupFile(string(), 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
|
|
try {
|
|
createBackupFile("bak", file.path(), backupPath2, file.stream(), backupStream2);
|
|
CPPUNIT_FAIL("renaming failed because backup dir does not exist");
|
|
} catch (const std::ios_base::failure &failure) {
|
|
TESTUTILS_ASSERT_LIKE("renaming error", "Unable to rename original file before rewriting it: .*"s, string(failure.what()));
|
|
}
|
|
backupStream2.clear();
|
|
workingCopyPath("bak/unsupported.bin", WorkingCopyMode::NoCopy);
|
|
createBackupFile("bak", file.path(), backupPath2, file.stream(), backupStream2);
|
|
CPPUNIT_ASSERT_EQUAL(workingDir + "/bak/unsupported.bin", backupPath2);
|
|
|
|
// get rid of 2nd backup (again)
|
|
backupStream2.close();
|
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("remove " + backupPath2, 0, remove(backupPath2.data()));
|
|
const auto backupDir(workingDir + "/bak");
|
|
CPPUNIT_ASSERT_EQUAL_MESSAGE("remove " + backupDir, 0, rmdir(backupDir.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(0x34_st, static_cast<size_t>(file.stream().get()));
|
|
file.close();
|
|
|
|
// restore after user aborted
|
|
createBackupFile(string(), file.path(), backupPath1, file.stream(), backupStream1);
|
|
try {
|
|
throw OperationAbortedException();
|
|
} catch (...) {
|
|
Diagnostics diag;
|
|
CPPUNIT_ASSERT_THROW(
|
|
handleFailureAfterFileModified(file, backupPath1, file.stream(), backupStream1, diag, "test"), OperationAbortedException);
|
|
CPPUNIT_ASSERT(diag.level() < DiagLevel::Critical);
|
|
CPPUNIT_ASSERT(!diag.empty());
|
|
CPPUNIT_ASSERT_EQUAL("Rewriting the file to apply changed tag information has been aborted."s, diag.front().message());
|
|
CPPUNIT_ASSERT_EQUAL("The original file has been restored."s, diag.back().message());
|
|
}
|
|
|
|
// restore after error
|
|
createBackupFile(string(), file.path(), backupPath1, file.stream(), backupStream1);
|
|
try {
|
|
throw Failure();
|
|
} catch (...) {
|
|
Diagnostics diag;
|
|
CPPUNIT_ASSERT_THROW(handleFailureAfterFileModified(file, backupPath1, file.stream(), backupStream1, diag, "test"), Failure);
|
|
CPPUNIT_ASSERT(diag.level() >= DiagLevel::Critical);
|
|
CPPUNIT_ASSERT_EQUAL("Rewriting the file to apply changed tag information failed."s, diag.front().message());
|
|
CPPUNIT_ASSERT_EQUAL("The original file has been restored."s, diag.back().message());
|
|
}
|
|
|
|
// restore after io failure
|
|
createBackupFile(string(), file.path(), backupPath1, file.stream(), backupStream1);
|
|
try {
|
|
throw std::ios_base::failure("simulated IO failure");
|
|
} catch (const std::ios_base::failure &) {
|
|
Diagnostics diag;
|
|
CPPUNIT_ASSERT_THROW_MESSAGE("IO failure re-thrown",
|
|
handleFailureAfterFileModified(file, backupPath1, file.stream(), backupStream1, diag, "test"), std::ios_base::failure);
|
|
CPPUNIT_ASSERT(diag.level() >= DiagLevel::Critical);
|
|
CPPUNIT_ASSERT_EQUAL("An IO error occurred when rewriting the file to apply changed tag information."s, diag.front().message());
|
|
CPPUNIT_ASSERT_EQUAL("The original file has been restored."s, diag.back().message());
|
|
}
|
|
|
|
CPPUNIT_ASSERT_EQUAL(0, remove(file.path().data()));
|
|
}
|