Commit a0663d90 authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Update file test, add a timeline hash function to check if a document is...

Update file test, add a timeline hash function to check if a document is identical before / after save
parent e9cc8924
Pipeline #106516 passed with stage
in 9 minutes and 14 seconds
......@@ -249,6 +249,7 @@ bool ProjectManager::testSaveFileAs(const QString &outputFileName)
QString saveFolder = QFileInfo(outputFileName).absolutePath();
QMap<QString,QString>docProperties;
docProperties.insert(QStringLiteral("version"), QString::number(m_project->getDocumentVersion()));
docProperties.insert(QStringLiteral("timelineHash"), m_mainTimelineModel->timelineHash().toHex());
pCore->projectItemModel()->saveDocumentProperties(docProperties, QMap<QString,QString>(),
m_project->getGuideModel());
QString scene = m_mainTimelineModel->sceneList(saveFolder);
......
......@@ -11,6 +11,7 @@
#include "compositionmodel.hpp"
#include "core.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
#include "effects/effectsrepository.hpp"
#include "bin/model/subtitlemodel.hpp"
#include "effects/effectstack/model/effectstackmodel.hpp"
......@@ -6366,3 +6367,26 @@ QStringList TimelineModel::getProxiesAt(int position)
}
return proxied;
}
QByteArray TimelineModel::timelineHash()
{
QByteArray fileData;
// Get track hashes
auto it = m_allTracks.begin();
while (it != m_allTracks.end()) {
fileData.append((*it)->trackHash());
++it;
}
// Compositions hash
for (auto &compo : m_allCompositions) {
int track = getTrackPosition(compo.second->getCurrentTrackId());
QString compoData = QString("%1 %2 %3 %4").arg(QString::number(compo.second->getATrack()), QString::number(track), QString::number(compo.second->getPosition()), QString::number(compo.second->getPlaytime()));
compoData.append(compo.second->getAssetId());
fileData.append(compoData.toLatin1());
}
// Guides
QString guidesData = pCore->currentDoc()->getGuideModel()->toJson();
fileData.append(guidesData.toUtf8().constData());
QByteArray fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
return fileHash;
}
......@@ -604,6 +604,9 @@ public:
Must be called for example when the doc change
*/
void setUndoStack(std::weak_ptr<DocUndoStack> undo_stack);
/** @brief Calculate timeline hash based on clips, mixes and compositions
*/
QByteArray timelineHash();
protected:
/** @brief Requests the best snapped position for a clip
......
......@@ -1095,10 +1095,10 @@ bool TrackModel::checkConsistency()
// Check that the mix has correct in/out
int mainId = -1;
int mixIn = t.get_in();
for (auto & m_sameComposition : m_sameCompositions) {
if (static_cast<Mlt::Transition *>(m_sameComposition.second->getAsset())->get_in() == mixIn) {
for (auto &sameComposition : m_sameCompositions) {
if (static_cast<Mlt::Transition *>(sameComposition.second->getAsset())->get_in() == mixIn) {
// Found mix in list
mainId = m_sameComposition.first;
mainId = sameComposition.first;
break;
}
}
......@@ -2590,3 +2590,21 @@ bool TrackModel::hasClipStart(int pos)
}
return false;
}
QByteArray TrackModel::trackHash()
{
QByteArray fileData;
// Parse clips
for (auto &clip : m_allClips) {
QString clipData = QString("%1 %2 %3 %4").arg(QString::number(clip.second->getPosition()), QString::number(clip.second->getPlaytime()), QString::number(clip.second->getSubPlaylistIndex()), clip.second->effectNames());
fileData.append(clipData.toLatin1());
}
// Parse mixes
for (auto &sameComposition : m_sameCompositions) {
Mlt::Transition *tr = static_cast<Mlt::Transition *>(sameComposition.second->getAsset());
QString mixData = QString("%1 %2 %3").arg(QString::number(tr->get_in()), QString::number(tr->get_out()), sameComposition.second->getAssetId());
fileData.append(mixData.toLatin1());
}
return fileData;
}
......@@ -142,6 +142,8 @@ public:
QVariantList stackZones() const;
/** @brief Return true if a clip starts at pos in one of the trak playlists */
bool hasClipStart(int pos);
/** @brief Calculate a hash based on all clips an d mixes positions/playtime */
QByteArray trackHash();
protected:
/** @brief This will lock the track: it will no longer allow insertion/deletion/resize of items
......
......@@ -11,6 +11,7 @@
#include "src/effects/effectsrepository.hpp"
#include "src/mltcontroller/clipcontroller.h"
#include "bin/projectitemmodel.h"
/* This file is intended to remain empty.
Write your tests in a file with a name corresponding to what you're testing */
......
#include "test_utils.hpp"
#define private public
#define protected public
#include "doc/kdenlivedoc.h"
#include "timeline2/model/builders/meltBuilder.hpp"
#include "test_utils.hpp"
#include "bin/binplaylist.hpp"
#include "xml/xml.hpp"
using namespace fakeit;
Mlt::Profile profile_file;
......@@ -17,59 +22,60 @@ TEST_CASE("Save File", "[SF]")
// we mock a doc to stub the getDocumentProperty
Mock<KdenliveDoc> docMock;
When(Method(docMock, getDocumentProperty)).AlwaysDo([](const QString &name, const QString &defaultValue) {
Q_UNUSED(name) Q_UNUSED(defaultValue)
qDebug() << "Intercepted call";
return QStringLiteral("dummyId");
});
KdenliveDoc &mockedDoc = docMock.get();
// We mock the project class so that the undoStack function returns our undoStack, and our mocked document
Mock<ProjectManager> pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
When(Method(pmMock, cacheDir)).AlwaysReturn(QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
When(Method(pmMock, current)).AlwaysReturn(&mockedDoc);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_file, undoStack);
Mock<TimelineItemModel> timMock(tim);
auto timeline = std::shared_ptr<TimelineItemModel>(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
mocked.testSetActiveDocument(&mockedDoc, timeline);
QDir dir = QDir::temp();
std::unordered_map<QString, QString> binIdCorresp;
QStringList expandedFolders;
QDomDocument doc = mockedDoc.createEmptyDocument(2,2);
QScopedPointer<Mlt::Producer> xmlProd(new Mlt::Producer(profile_file, "xml-string",
SECTION("Simple insert and save")
{
// Create document
Mock<KdenliveDoc> docMock;
/*When(Method(docMock, getDocumentProperty)).AlwaysDo([](const QString &name, const QString &defaultValue) {
Q_UNUSED(name) Q_UNUSED(defaultValue)
qDebug() << "Intercepted call";
return QStringLiteral("dummyId");
});*/
KdenliveDoc &mockedDoc = docMock.get();
// We mock the project class so that the undoStack function returns our undoStack, and our mocked document
Mock<ProjectManager> pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
When(Method(pmMock, cacheDir)).AlwaysReturn(QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
When(Method(pmMock, current)).AlwaysReturn(&mockedDoc);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
pCore->m_projectManager->m_project = &mockedDoc;
pCore->m_projectManager->m_project->m_guideModel = guideModel;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_file, undoStack);
Mock<TimelineItemModel> timMock(tim);
auto timeline = std::shared_ptr<TimelineItemModel>(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
mocked.testSetActiveDocument(&mockedDoc, timeline);
QDir dir = QDir::temp();
std::unordered_map<QString, QString> binIdCorresp;
QStringList expandedFolders;
QDomDocument doc = mockedDoc.createEmptyDocument(2,2);
QScopedPointer<Mlt::Producer> xmlProd(new Mlt::Producer(profile_file, "xml-string",
doc.toString().toUtf8()));
Mlt::Service s(*xmlProd);
Mlt::Tractor tractor(s);
binModel->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp, expandedFolders, nullptr);
Mlt::Service s(*xmlProd);
Mlt::Tractor tractor(s);
binModel->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp, expandedFolders, nullptr);
RESET(timMock)
RESET(timMock)
QString binId = createProducerWithSound(profile_file, binModel);
QString binId2 = createProducer(profile_file, "red", binModel, 20, false);
QString binId = createProducerWithSound(profile_file, binModel);
QString binId2 = createProducer(profile_file, "red", binModel, 20, false);
int tid2b = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid2 = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid1 = TrackModel::construct(timeline);
int tid1b = TrackModel::construct(timeline);
int tid2b = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid2 = TrackModel::construct(timeline, -1, -1, QString(), true);
int tid1 = TrackModel::construct(timeline);
int tid1b = TrackModel::construct(timeline);
// Setup timeline audio drop info
QMap <int, QString>audioInfo;
audioInfo.insert(1,QStringLiteral("stream1"));
timeline->m_binAudioTargets = audioInfo;
timeline->m_videoTarget = tid1;
SECTION("Simple insert and save")
{
// Setup timeline audio drop info
QMap <int, QString>audioInfo;
audioInfo.insert(1,QStringLiteral("stream1"));
timeline->m_binAudioTargets = audioInfo;
timeline->m_videoTarget = tid1;
// Insert 2 clips (length=20, pos = 80 / 100)
int cid1 = -1;
REQUIRE(timeline->requestClipInsertion(binId2, tid1, 80, cid1, true, true, false));
......@@ -90,6 +96,7 @@ TEST_CASE("Save File", "[SF]")
REQUIRE(timeline->getClipPlaytime(cid2) == 20);
};
state();
REQUIRE(timeline->checkConsistency());
mocked.testSaveFileAs(dir.absoluteFilePath(QStringLiteral("test.kdenlive")));
// Undo resize
undoStack->undo();
......@@ -99,9 +106,32 @@ TEST_CASE("Save File", "[SF]")
undoStack->undo();
}
binModel->clean();
SECTION("Reopen and check in/out points")
{
// Create new document
Mock<KdenliveDoc> docMock;
KdenliveDoc &mockedDoc = docMock.get();
// We mock the project class so that the undoStack function returns our undoStack, and our mocked document
Mock<ProjectManager> pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
When(Method(pmMock, cacheDir)).AlwaysReturn(QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
When(Method(pmMock, current)).AlwaysReturn(&mockedDoc);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
pCore->m_projectManager->m_project = &mockedDoc;
pCore->m_projectManager->m_project->m_guideModel = guideModel;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_file, undoStack);
Mock<TimelineItemModel> timMock(tim);
auto timeline = std::shared_ptr<TimelineItemModel>(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
mocked.testSetActiveDocument(&mockedDoc, timeline);
QDir dir = QDir::temp();
RESET(timMock)
QString path = dir.absoluteFilePath(QStringLiteral("test.kdenlive"));
QFile file(path);
REQUIRE(file.open(QIODevice::ReadOnly | QIODevice::Text) == true);
......@@ -109,11 +139,18 @@ TEST_CASE("Save File", "[SF]")
file.close();
QScopedPointer<Mlt::Producer> xmlProd(new Mlt::Producer(profile_file, "xml-string",
playlist));
QDomDocument doc;
doc.setContent(playlist);
QDomNodeList list = doc.elementsByTagName(QStringLiteral("playlist"));
QDomElement pl = list.at(0).toElement();
QString hash = Xml::getXmlProperty(pl, QStringLiteral("kdenlive:docproperties.timelineHash"));
Mlt::Service s(*xmlProd);
Mlt::Tractor tractor(s);
bool projectErrors;
constructTimelineFromMelt(timeline, tractor, nullptr, QString(), &projectErrors);
int tid1 = timeline->getTrackIndexFromPosition(3);
REQUIRE(timeline->checkConsistency());
int tid1 = timeline->getTrackIndexFromPosition(2);
int cid1 = timeline->getClipByStartPosition(tid1, 0);
int cid2 = timeline->getClipByStartPosition(tid1, 100);
REQUIRE(cid1 > -1);
......@@ -129,6 +166,8 @@ TEST_CASE("Save File", "[SF]")
REQUIRE(timeline->getClipPlaytime(cid2) == 20);
};
state();
QByteArray updatedHex = timeline->timelineHash().toHex();
REQUIRE(updatedHex == hash);
QFile::remove(dir.absoluteFilePath(QStringLiteral("test.kdenlive")));
}
binModel->clean();
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment