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

Add tests to prevent project corruption on color/title/image clip resize as happened in 21.08.3

parent 90b1e4f5
Pipeline #105103 passed with stage
in 8 minutes and 44 seconds
......@@ -2910,13 +2910,7 @@ QStringList Bin::getBinFolderClipIds(const QString &id) const
std::shared_ptr<ProjectClip> Bin::getBinClip(const QString &id)
{
std::shared_ptr<ProjectClip> clip = nullptr;
if (id.contains(QLatin1Char('_'))) {
clip = m_itemModel->getClipByBinID(id.section(QLatin1Char('_'), 0, 0));
} else if (!id.isEmpty()) {
clip = m_itemModel->getClipByBinID(id);
}
return clip;
return m_itemModel->getClipByBinID(id);
}
const QString Bin::getBinClipName(const QString &id) const
......
......@@ -120,7 +120,7 @@ void SpeechDialog::slotProcessSpeech(QPoint zone)
audio = m_tmpAudio->fileName();
}
m_tmpAudio->close();
pCore->getMonitor(Kdenlive::ProjectMonitor)->sceneList(QDir::temp().absolutePath(), sceneList);
m_timeline->sceneList(QDir::temp().absolutePath(), sceneList);
Mlt::Producer producer(*m_timeline->tractor()->profile(), "xml", sceneList.toUtf8().constData());
qDebug()<<"=== STARTING RENDER B";
Mlt::Consumer xmlConsumer(*m_timeline->tractor()->profile(), "avformat", audio.toUtf8().constData());
......
......@@ -1289,6 +1289,11 @@ void KdenliveDoc::slotProxyCurrentItem(bool doProxy, QList<std::shared_ptr<Proje
}
}
double KdenliveDoc::getDocumentVersion() const
{
return DOCUMENTVERSION;
}
QMap<QString, QString> KdenliveDoc::documentProperties()
{
m_documentProperties.insert(QStringLiteral("version"), QString::number(DOCUMENTVERSION));
......
......@@ -167,6 +167,10 @@ public:
void initializeSubtitles(const std::shared_ptr<SubtitleModel> m_subtitle);
/** @brief Returns a path for current document's subtitle file. If final is true, this will be the project filename with ".srt" appended. Otherwise a file in /tmp */
const QString subTitlePath(bool final);
/** @brief Creates a new project. */
QDomDocument createEmptyDocument(int videotracks, int audiotracks);
/** @brief Return the document version. */
double getDocumentVersion() const;
private:
QUrl m_url;
......@@ -204,7 +208,6 @@ private:
QString searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const;
/** @brief Creates a new project. */
QDomDocument createEmptyDocument(int videotracks, int audiotracks);
QDomDocument createEmptyDocument(const QList<TrackInfo> &tracks);
/** @brief Updates the project folder location entry in the kdenlive file dialogs to point to the current project folder. */
......
......@@ -1383,39 +1383,6 @@ void GLWidget::resetConsumer(bool fullReset)
reconfigure();
}
const QString GLWidget::sceneList(const QString &root, const QString &fullPath, QString filterData)
{
LocaleHandling::resetLocale();
QString playlist;
Mlt::Consumer xmlConsumer(pCore->getCurrentProfile()->profile(), "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData());
if (!root.isEmpty()) {
xmlConsumer.set("root", root.toUtf8().constData());
}
if (!xmlConsumer.is_valid()) {
return QString();
}
xmlConsumer.set("store", "kdenlive");
xmlConsumer.set("time_format", "clock");
// Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc)
// And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening
// xmlConsumer.set("no_meta", 1);
Mlt::Service s(m_producer->get_service());
std::unique_ptr<Mlt::Filter> filter = nullptr;
if (!filterData.isEmpty()) {
filter = std::make_unique<Mlt::Filter>(pCore->getCurrentProfile()->profile(), QString("dynamictext:%1").arg(filterData).toUtf8().constData());
filter->set("fgcolour", "#ffffff");
filter->set("bgcolour", "#bb333333");
s.attach(*filter.get());
}
xmlConsumer.connect(s);
xmlConsumer.run();
if (filter) {
s.detach(*filter.get());
}
playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath;
return playlist;
}
void GLWidget::updateTexture(GLuint yName, GLuint uName, GLuint vName)
{
m_texture[0] = yName;
......
......@@ -76,9 +76,6 @@ public:
// TODO: currently unused
int reconfigureMulti(const QString &params, const QString &path, Mlt::Profile *profile);
void stopCapture();
/** @brief Get the current MLT producer playlist.
* @return A string describing the playlist */
const QString sceneList(const QString &root, const QString &fullPath = QString(), QString filterData = QString());
int displayWidth() const { return m_rect.width(); }
void updateAudioForAnalysis();
......
......@@ -1931,11 +1931,6 @@ void Monitor::resetConsumer(bool fullReset)
m_glMonitor->resetConsumer(fullReset);
}
const QString Monitor::sceneList(const QString &root, const QString &fullPath, const QString overlayData)
{
return m_glMonitor->sceneList(root, fullPath, overlayData);
}
void Monitor::updateClipZone(const QPoint zone)
{
if (m_controller == nullptr) {
......
......@@ -70,7 +70,6 @@ public:
void resetConsumer(bool fullReset);
void setCustomProfile(const QString &profile, const Timecode &tc);
void setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu = nullptr, QAction *loopClip = nullptr);
const QString sceneList(const QString &root, const QString &fullPath = QString(), const QString overlayData = QString());
const QString activeClipId();
int position();
void updateTimecodeFormat();
......
......@@ -238,6 +238,35 @@ void ProjectManager::newFile(QString profileName, bool showProjectSettings)
m_lastSave.start();
}
void ProjectManager::testSetActiveDocument(KdenliveDoc *doc, std::shared_ptr<TimelineItemModel> timeline)
{
m_project = doc;
m_mainTimelineModel = timeline;
}
bool ProjectManager::testSaveFileAs(const QString &outputFileName)
{
QString saveFolder = QFileInfo(outputFileName).absolutePath();
QMap<QString,QString>docProperties;
docProperties.insert(QStringLiteral("version"), QString::number(m_project->getDocumentVersion()));
pCore->projectItemModel()->saveDocumentProperties(docProperties, QMap<QString,QString>(),
m_project->getGuideModel());
QString scene = m_mainTimelineModel->sceneList(saveFolder);
QSaveFile file(outputFileName);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
qDebug()<<"////// ERROR writing to file: " << outputFileName;
return false;
}
file.write(scene.toUtf8());
if (!file.commit()) {
qDebug()<<"Cannot write to file %1";
return false;
}
return true;
}
bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit)
{
// Disable autosave
......@@ -746,7 +775,7 @@ QString ProjectManager::projectSceneList(const QString &outputFolder, const QStr
pCore->window()->getMainTimeline()->controller()->requestEndTrimmingMode();
}
pCore->mixer()->pauseMonitoring(true);
QString scene = pCore->monitorManager()->projectMonitor()->sceneList(outputFolder, QString(), overlayData);
QString scene = m_mainTimelineModel->sceneList(outputFolder, QString(), overlayData);
pCore->mixer()->pauseMonitoring(false);
if (isMultiTrack) {
pCore->window()->getMainTimeline()->controller()->slotMultitrackView(true, false);
......@@ -762,7 +791,9 @@ QString ProjectManager::projectSceneList(const QString &outputFolder, const QStr
void ProjectManager::setDocumentNotes(const QString &notes)
{
m_notesPlugin->widget()->setHtml(notes);
if (m_notesPlugin) {
m_notesPlugin->widget()->setHtml(notes);
}
}
QString ProjectManager::documentNotes() const
......
......@@ -94,6 +94,12 @@ public:
/** @brief Add requested audio tracks number to project.
*/
void addAudioTracks(int tracksCount);
/** @brief This method is only there for tests, do not use in real app.
*/
void testSetActiveDocument(KdenliveDoc *doc, std::shared_ptr<TimelineItemModel> timeline);
/** @brief This method is only there for tests, do not use in real app.
*/
bool testSaveFileAs(const QString &outputFileName);
public slots:
void newFile(QString profileName, bool showProjectSettings = true);
......
......@@ -47,7 +47,6 @@ bool constructTimelineFromMelt(const std::shared_ptr<TimelineItemModel> &timelin
std::unordered_map<QString, QString> binIdCorresp;
QStringList expandedFolders;
pCore->projectItemModel()->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp, expandedFolders, progressDialog);
pCore->bin()->checkMissingProxies();
QStringList foldersToExpand;
// Find updated ids for expanded folders
for (const QString &folderId : expandedFolders) {
......@@ -55,7 +54,10 @@ bool constructTimelineFromMelt(const std::shared_ptr<TimelineItemModel> &timelin
foldersToExpand << binIdCorresp.at(folderId);
}
}
pCore->bin()->loadFolderState(foldersToExpand);
if (pCore->window()) {
pCore->bin()->checkMissingProxies();
pCore->bin()->loadFolderState(foldersToExpand);
}
QSet<QString> reserved_names{QLatin1String("playlistmain"), QLatin1String("timeline_preview"), QLatin1String("timeline_overlay"), QLatin1String("black_track"), QLatin1String("overlay_track")};
bool ok = true;
......@@ -373,7 +375,7 @@ bool constructTrackFromMelt(const std::shared_ptr<TimelineItemModel> &timeline,
}
bool ok = false;
int cid = -1;
if (pCore->bin()->getBinClip(binId)) {
if (pCore->projectItemModel()->getClipByBinID(binId)) {
PlaylistState::ClipState st = inferState(clip, audioTrack);
bool enforceTopPlaylist = false;
if (playlist > 0) {
......
......@@ -39,6 +39,7 @@
#include <set>
#include "macros.hpp"
#include <localeHandling.h>
#ifdef CRASH_AUTO_TEST
#include "logger.hpp"
......@@ -3280,6 +3281,9 @@ int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logU
finalSize = qMax(0, getItemPosition(id)) + getItemPlaytime(id) - finalPos;
}
result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo);
if (!result) {
break;
}
if (id == itemId) {
size = finalSize;
}
......@@ -5239,6 +5243,39 @@ std::shared_ptr<Mlt::Producer> TimelineModel::producer()
return std::make_shared<Mlt::Producer>(tractor());
}
const QString TimelineModel::sceneList(const QString &root, const QString &fullPath, QString filterData)
{
LocaleHandling::resetLocale();
QString playlist;
Mlt::Consumer xmlConsumer(*m_profile, "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData());
if (!root.isEmpty()) {
xmlConsumer.set("root", root.toUtf8().constData());
}
if (!xmlConsumer.is_valid()) {
return QString();
}
xmlConsumer.set("store", "kdenlive");
xmlConsumer.set("time_format", "clock");
// Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc)
// And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening
// xmlConsumer.set("no_meta", 1);
Mlt::Service s(m_tractor->get_service());
std::unique_ptr<Mlt::Filter> filter = nullptr;
if (!filterData.isEmpty()) {
filter = std::make_unique<Mlt::Filter>(*m_profile, QString("dynamictext:%1").arg(filterData).toUtf8().constData());
filter->set("fgcolour", "#ffffff");
filter->set("bgcolour", "#bb333333");
s.attach(*filter.get());
}
xmlConsumer.connect(s);
xmlConsumer.run();
if (filter) {
s.detach(*filter.get());
}
playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath;
return playlist;
}
void TimelineModel::checkRefresh(int start, int end)
{
if (m_blockRefresh) {
......
......@@ -442,6 +442,9 @@ public:
/** @brief Returns a list of proxied clips at position pos
*/
QStringList getProxiesAt(int position);
/** @brief Returns the current project xml playlist for saving
*/
const QString sceneList(const QString &root, const QString &fullPath = QString(), QString filterData = QString());
protected:
/** @brief Creates a new clip instance without inserting it.
......
......@@ -6,12 +6,14 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "previewmanager.h"
#include "core.h"
#include "mainwindow.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
#include "kdenlivesettings.h"
#include "monitor/monitor.h"
#include "profiles/profilemodel.hpp"
#include "timeline2/view/timelinecontroller.h"
#include "timeline2/view/timelinewidget.h"
#include <KLocalizedString>
#include <QProcess>
......@@ -516,7 +518,7 @@ void PreviewManager::startPreviewRender()
// clear log
m_errorLog.clear();
const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt"));
pCore->getMonitor(Kdenlive::ProjectMonitor)->sceneList(m_cacheDir.absolutePath(), sceneList);
pCore->window()->getMainTimeline()->model()->sceneList(m_cacheDir.absolutePath(), sceneList);
m_previewTimer.stop();
doPreviewRender(sceneList);
}
......
......@@ -5,6 +5,7 @@ add_executable(runTests
abortutil.cpp
compositiontest.cpp
effectstest.cpp
filetest.cpp
mixtest.cpp
groupstest.cpp
keyframetest.cpp
......
#include "doc/kdenlivedoc.h"
#include "timeline2/model/builders/meltBuilder.hpp"
#include "test_utils.hpp"
using namespace fakeit;
Mlt::Profile profile_file;
TEST_CASE("Save File", "[SF]")
{
auto binModel = pCore->projectItemModel();
binModel->clean();
std::shared_ptr<DocUndoStack> undoStack = std::make_shared<DocUndoStack>(nullptr);
std::shared_ptr<MarkerListModel> guideModel = std::make_shared<MarkerListModel>(undoStack);
// Here we do some trickery to enable testing.
// 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",
doc.toString().toUtf8()));
Mlt::Service s(*xmlProd);
Mlt::Tractor tractor(s);
binModel->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp, expandedFolders, nullptr);
RESET(timMock)
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);
// 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")
{
// Insert 2 clips (length=20, pos = 80 / 100)
int cid1 = -1;
REQUIRE(timeline->requestClipInsertion(binId2, tid1, 80, cid1, true, true, false));
int l = timeline->getClipPlaytime(cid1);
int cid2 = -1;
REQUIRE(timeline->requestClipInsertion(binId2, tid1, 80 + l, cid2, true, true, false));
// Resize first clip (length=100)
REQUIRE(timeline->requestItemResize(cid1, 100, false) == 100);
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPosition(cid2) == 100);
REQUIRE(timeline->getClipPlaytime(cid1) == 100);
REQUIRE(timeline->getClipPlaytime(cid2) == 20);
};
state();
mocked.testSaveFileAs(dir.absoluteFilePath(QStringLiteral("test.kdenlive")));
// Undo resize
undoStack->undo();
// Undo first insert
undoStack->undo();
// Undo second insert
undoStack->undo();
}
binModel->clean();
SECTION("Reopen and check in/out points")
{
QString path = dir.absoluteFilePath(QStringLiteral("test.kdenlive"));
QFile file(path);
REQUIRE(file.open(QIODevice::ReadOnly | QIODevice::Text) == true);
QByteArray playlist = file.readAll();
file.close();
QScopedPointer<Mlt::Producer> xmlProd(new Mlt::Producer(profile_file, "xml-string",
playlist));
Mlt::Service s(*xmlProd);
Mlt::Tractor tractor(s);
bool projectErrors;
constructTimelineFromMelt(timeline, tractor, nullptr, QString(), &projectErrors);
int tid1 = timeline->getTrackIndexFromPosition(3);
int cid1 = timeline->getClipByStartPosition(tid1, 0);
int cid2 = timeline->getClipByStartPosition(tid1, 100);
REQUIRE(cid1 > -1);
REQUIRE(cid2 > -1);
auto state = [&]() {
REQUIRE(timeline->checkConsistency());
REQUIRE(timeline->getTrackClipsCount(tid1) == 2);
REQUIRE(timeline->getClipTrackId(cid1) == tid1);
REQUIRE(timeline->getClipTrackId(cid2) == tid1);
REQUIRE(timeline->getClipPosition(cid1) == 0);
REQUIRE(timeline->getClipPosition(cid2) == 100);
REQUIRE(timeline->getClipPlaytime(cid1) == 100);
REQUIRE(timeline->getClipPlaytime(cid2) == 20);
};
state();
QFile::remove(dir.absoluteFilePath(QStringLiteral("test.kdenlive")));
}
binModel->clean();
pCore->m_projectManager = nullptr;
}
Markdown is supported
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