diff --git a/autotests/unittests/courseresource/test_courseresource.cpp b/autotests/unittests/courseresource/test_courseresource.cpp index 1403c7d..d0e9aca 100644 --- a/autotests/unittests/courseresource/test_courseresource.cpp +++ b/autotests/unittests/courseresource/test_courseresource.cpp @@ -1,191 +1,192 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_courseresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/phonemegroup.h" #include "core/resources/courseresource.h" #include "../mocks/languagestub.h" #include #include #include #include #include #include #include #include #include TestCourseResource::TestCourseResource() { } void TestCourseResource::init() { } void TestCourseResource::cleanup() { } void TestCourseResource::loadCourseResource() { std::shared_ptr language(new LanguageStub("de")); auto group = std::static_pointer_cast(language)->addPhonemeGroup("id", "title"); group->addPhoneme("g", "G"); group->addPhoneme("u", "U"); std::vector> languages; languages.push_back(language); ResourceRepositoryStub repository(languages); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); QCOMPARE(course->file().toLocalFile(), courseFile); QCOMPARE(course->id(), "de"); QCOMPARE(course->foreignId(), "artikulate-basic"); QCOMPARE(course->title(), "Artikulate Deutsch"); QCOMPARE(course->description(), "Ein Kurs in (hoch-)deutscher Aussprache."); QVERIFY(course->language() != nullptr); QCOMPARE(course->language()->id(), "de"); QCOMPARE(course->units().count(), 1); QCOMPARE(course->units().first()->course(), course.get()); const auto unit = course->units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "1"); QCOMPARE(unit->title(), QStringLiteral("Auf der Straße")); QCOMPARE(unit->foreignId(), "{dd60f04a-eb37-44b7-9787-67aaf7d3578d}"); + QCOMPARE(unit->course(), course.get()); QCOMPARE(unit->phraseList().count(), 3); // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phraseList().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "1"); QCOMPARE(firstPhrase->foreignId(), "{3a4c1926-60d7-44c6-80d1-03165a641c75}"); QCOMPARE(firstPhrase->text(), "Guten Tag."); QCOMPARE(firstPhrase->soundFileUrl(), courseDirectory + "de_01.ogg"); QCOMPARE(firstPhrase->type(), Phrase::Type::Sentence); QCOMPARE(firstPhrase->phonemes().count(), 2); } void TestCourseResource::unitAddAndRemoveHandling() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); const int initialUnitNumber = course->units().count(); QCOMPARE(initialUnitNumber, 1); QSignalSpy spyAboutToBeAdded(course.get(), SIGNAL(unitAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(course.get(), SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); auto sharedUnit = course->addUnit(std::move(unit)); QCOMPARE(course->units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); QCOMPARE(sharedUnit->course(), course.get()); } void TestCourseResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/courses/de/"; const QString courseFile = courseDirectory + "de.xml"; auto course = CourseResource::create(QUrl::fromLocalFile(courseFile), &repository); // id { const QString value = "newId"; QSignalSpy spy(course.get(), SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); course->setId(value); QCOMPARE(course->id(), value); QCOMPARE(spy.count(), 1); } // foreign id { const QString value = "newForeignId"; QSignalSpy spy(course.get(), SIGNAL(foreignIdChanged())); QCOMPARE(spy.count(), 0); course->setForeignId(value); QCOMPARE(course->foreignId(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(course.get(), SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); course->setTitle(value); QCOMPARE(course->title(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newI18nTitle"; QSignalSpy spy(course.get(), SIGNAL(i18nTitleChanged())); QCOMPARE(spy.count(), 0); course->setI18nTitle(value); QCOMPARE(course->i18nTitle(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(course.get(), SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); course->setDescription(value); QCOMPARE(course->description(), value); QCOMPARE(spy.count(), 1); } // language { std::shared_ptr testLanguage; QSignalSpy spy(course.get(), SIGNAL(languageChanged())); QCOMPARE(spy.count(), 0); course->setLanguage(testLanguage); QCOMPARE(course->language(), testLanguage); QCOMPARE(spy.count(), 1); } } QTEST_GUILESS_MAIN(TestCourseResource) diff --git a/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp index 4435515..4d815a4 100644 --- a/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp +++ b/autotests/unittests/editablecourseresource/test_editablecourseresource.cpp @@ -1,363 +1,364 @@ /* * Copyright 2013-2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_editablecourseresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/resources/courseparser.h" #include "core/resources/editablecourseresource.h" #include "../mocks/languagestub.h" #include "../mocks/coursestub.h" #include #include #include #include #include #include #include #include #include #include TestEditableCourseResource::TestEditableCourseResource() { } void TestEditableCourseResource::init() { } void TestEditableCourseResource::cleanup() { } void TestEditableCourseResource::loadCourseResource() { std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QCOMPARE(course->file().toLocalFile(), ":/courses/de.xml"); QCOMPARE(course->id(), "de"); QCOMPARE(course->foreignId(), "artikulate-basic"); QCOMPARE(course->title(), "Artikulate Deutsch"); QCOMPARE(course->description(), "Ein Kurs in (hoch-)deutscher Aussprache."); QVERIFY(course->language() != nullptr); QCOMPARE(course->language()->id(), "de"); QCOMPARE(course->units().count(), 1); QCOMPARE(course->units().first()->course(), course.get()); const auto unit = course->units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "1"); QCOMPARE(unit->title(), QStringLiteral("Auf der Straße")); QCOMPARE(unit->foreignId(), "{dd60f04a-eb37-44b7-9787-67aaf7d3578d}"); + QCOMPARE(unit->course(), course.get()); QCOMPARE(unit->phraseList().count(), 3); // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phraseList().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "1"); QCOMPARE(firstPhrase->foreignId(), "{3a4c1926-60d7-44c6-80d1-03165a641c75}"); QCOMPARE(firstPhrase->text(), "Guten Tag."); QCOMPARE(firstPhrase->soundFileUrl(), ":/courses/de_01.ogg"); QCOMPARE(firstPhrase->type(), Phrase::Type::Sentence); QVERIFY(firstPhrase->phonemes().isEmpty()); } void TestEditableCourseResource::unitAddAndRemoveHandling() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); const int initialUnitNumber = course->units().count(); QCOMPARE(initialUnitNumber, 1); QSignalSpy spyAboutToBeAdded(course.get(), SIGNAL(unitAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(course.get(), SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); auto sharedUnit = course->addUnit(std::move(unit)); QCOMPARE(course->units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); QCOMPARE(sharedUnit->course(), course.get()); } void TestEditableCourseResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = CourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); // id { const QString value = "newId"; QSignalSpy spy(course.get(), SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); course->setId(value); QCOMPARE(course->id(), value); QCOMPARE(spy.count(), 1); } // foreign id { const QString value = "newForeignId"; QSignalSpy spy(course.get(), SIGNAL(foreignIdChanged())); QCOMPARE(spy.count(), 0); course->setForeignId(value); QCOMPARE(course->foreignId(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(course.get(), SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); course->setTitle(value); QCOMPARE(course->title(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newI18nTitle"; QSignalSpy spy(course.get(), SIGNAL(i18nTitleChanged())); QCOMPARE(spy.count(), 0); course->setI18nTitle(value); QCOMPARE(course->i18nTitle(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(course.get(), SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); course->setDescription(value); QCOMPARE(course->description(), value); QCOMPARE(spy.count(), 1); } // language { std::shared_ptr testLanguage; QSignalSpy spy(course.get(), SIGNAL(languageChanged())); QCOMPARE(spy.count(), 0); course->setLanguage(testLanguage); QCOMPARE(course->language(), testLanguage); QCOMPARE(spy.count(), 1); } } void TestEditableCourseResource::fileLoadSaveCompleteness() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); // note: this only works, since the resource manager not checks uniqueness of course ids! auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); // test that we actually call the different files QVERIFY(course->file().toLocalFile() != loadedCourse->file().toLocalFile()); QVERIFY(course->id() == loadedCourse->id()); QVERIFY(course->foreignId() == loadedCourse->foreignId()); QVERIFY(course->title() == loadedCourse->title()); QVERIFY(course->description() == loadedCourse->description()); QVERIFY(course->language()->id() == loadedCourse->language()->id()); QVERIFY(course->units().count() == loadedCourse->units().count()); auto testUnit = course->units().constFirst(); auto compareUnit = loadedCourse->units().constFirst(); QVERIFY(testUnit->id() == compareUnit->id()); QVERIFY(testUnit->foreignId() == compareUnit->foreignId()); QVERIFY(testUnit->title() == compareUnit->title()); QVERIFY(testUnit->phraseList().count() == compareUnit->phraseList().count()); Phrase *testPhrase = testUnit->phraseList().constFirst(); Phrase *comparePhrase = new Phrase(this); // note that this actually means that we DO NOT respect phrase orders by list order for (Phrase *phrase : compareUnit->phraseList()) { if (testPhrase->id() == phrase->id()) { comparePhrase = phrase; break; } } QVERIFY(testPhrase->id() == comparePhrase->id()); QVERIFY(testPhrase->foreignId() == comparePhrase->foreignId()); QVERIFY(testPhrase->text() == comparePhrase->text()); QVERIFY(testPhrase->type() == comparePhrase->type()); QVERIFY(testPhrase->sound().fileName() == comparePhrase->sound().fileName()); QVERIFY(testPhrase->phonemes().count() == comparePhrase->phonemes().count()); } void TestEditableCourseResource::modifiedStatus() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); { // initial file loading QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); } { // set ID QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setId("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set title QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setTitle("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set i18n title QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setI18nTitle("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set description QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); loadedCourse->setDescription("ASDF"); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // set language QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); std::shared_ptr newLanguage(new LanguageStub("en")); loadedCourse->setLanguage(newLanguage); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } { // add unit QTemporaryFile outputFile; outputFile.open(); course->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); auto loadedCourse = EditableCourseResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); QCOMPARE(loadedCourse->isModified(), false); std::unique_ptr unit(new Unit); unit->setId("testunit"); loadedCourse->addUnit(std::move(unit)); QCOMPARE(loadedCourse->isModified(), true); loadedCourse->sync(); QCOMPARE(loadedCourse->isModified(), false); } } void TestEditableCourseResource::skeletonUpdate() { std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); auto course = EditableCourseResource::create(QUrl::fromLocalFile(":/courses/de.xml"), &repository); QCOMPARE(course->units().count(), 1); // create skeleton stub auto importPhrase = new Phrase; importPhrase->setId("importPhraseId"); importPhrase->setText("phraseText"); importPhrase->setType(Phrase::Sentence); auto importUnit = std::shared_ptr(new Unit); importUnit->setId("importId"); importUnit->addPhrase(importPhrase); auto skeleton = CourseStub::create(language, {importUnit}); // test import course->updateFrom(skeleton); QCOMPARE(course->units().count(), 2); { std::shared_ptr importedUnit; for (auto unit : course->units()) { if (unit->foreignId() == importUnit->id()) { importedUnit = unit; break; } } QVERIFY(importedUnit != nullptr); QCOMPARE(importedUnit->foreignId(), importUnit->id()); QCOMPARE(importedUnit->id(), importUnit->id()); QCOMPARE(importedUnit->title(), importUnit->title()); QCOMPARE(importedUnit->phraseList().count(), 1); auto importedPhrase = importedUnit->phraseList().first(); QCOMPARE(importedPhrase->id(), importPhrase->id()); QCOMPARE(importedPhrase->foreignId(), importPhrase->id()); QCOMPARE(importedPhrase->text(), importPhrase->text()); QCOMPARE(importedPhrase->type(), importPhrase->type()); } // test that re-import does not change course course->updateFrom(skeleton); QCOMPARE(course->units().count(), 2); } QTEST_GUILESS_MAIN(TestEditableCourseResource) diff --git a/autotests/unittests/skeletonresource/test_skeletonresource.cpp b/autotests/unittests/skeletonresource/test_skeletonresource.cpp index a23ebf3..af1bf6c 100644 --- a/autotests/unittests/skeletonresource/test_skeletonresource.cpp +++ b/autotests/unittests/skeletonresource/test_skeletonresource.cpp @@ -1,200 +1,204 @@ /* * Copyright 2019 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_skeletonresource.h" #include "resourcerepositorystub.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "core/resources/skeletonresource.h" #include "../mocks/languagestub.h" #include #include #include #include #include #include #include #include #include TestSkeletonResource::TestSkeletonResource() { } void TestSkeletonResource::init() { } void TestSkeletonResource::cleanup() { } void TestSkeletonResource::loadSkeletonResource() { std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); QCOMPARE(skeleton->file().toLocalFile(), courseFile); QCOMPARE(skeleton->id(), "skeleton-testdata"); QCOMPARE(skeleton->foreignId(), "skeleton-testdata"); // always same as ID QCOMPARE(skeleton->title(), "Artikulate Test Course Title"); QCOMPARE(skeleton->description(), "Artikulate Test Course Description"); QVERIFY(skeleton->language() == nullptr); // a skeleton must not have a language QCOMPARE(skeleton->units().count(), 2); const auto unit = skeleton->units().first(); QVERIFY(unit != nullptr); QCOMPARE(unit->id(), "{11111111-b885-4833-97ff-27cb1ca2f543}"); QCOMPARE(unit->title(), QStringLiteral("Numbers")); QCOMPARE(unit->phraseList().count(), 2); + QVERIFY(unit->course() != nullptr); + QCOMPARE(unit->course(), skeleton.get()); + // note: this test takes the silent assumption that phrases are added to the list in same // order as they are defined in the file. This assumption should be made explicit or dropped const auto firstPhrase = unit->phraseList().first(); QVERIFY(firstPhrase != nullptr); QCOMPARE(firstPhrase->id(), "{22222222-9234-4da5-a6fe-dbd5104f57d5}"); QCOMPARE(firstPhrase->text(), "0"); QCOMPARE(firstPhrase->type(), Phrase::Type::Word); const auto secondPhrase = unit->phraseList().at(1); QVERIFY(secondPhrase != nullptr); QCOMPARE(secondPhrase->id(), "{333333333-b4a9-4264-9a26-75a55aa5d302}"); QCOMPARE(secondPhrase->text(), "1"); QCOMPARE(secondPhrase->type(), Phrase::Type::Word); } void TestSkeletonResource::unitAddAndRemoveHandling() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); // begin of test std::unique_ptr unit(new Unit); unit->setId("testunit"); const int initialUnitNumber = skeleton->units().count(); QCOMPARE(initialUnitNumber, 2); QSignalSpy spyAboutToBeAdded(skeleton.get(), SIGNAL(unitAboutToBeAdded(std::shared_ptr, int))); QSignalSpy spyAdded(skeleton.get(), SIGNAL(unitAdded())); QCOMPARE(spyAboutToBeAdded.count(), 0); QCOMPARE(spyAdded.count(), 0); - skeleton->addUnit(std::move(unit)); + auto sharedUnit = skeleton->addUnit(std::move(unit)); QCOMPARE(skeleton->units().count(), initialUnitNumber + 1); QCOMPARE(spyAboutToBeAdded.count(), 1); QCOMPARE(spyAdded.count(), 1); + QCOMPARE(sharedUnit->course(), skeleton.get()); } void TestSkeletonResource::coursePropertyChanges() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); // id { const QString value = "newId"; QSignalSpy spy(skeleton.get(), SIGNAL(idChanged())); QCOMPARE(spy.count(), 0); skeleton->setId(value); QCOMPARE(skeleton->id(), value); QCOMPARE(spy.count(), 1); } // title { const QString value = "newTitle"; QSignalSpy spy(skeleton.get(), SIGNAL(titleChanged())); QCOMPARE(spy.count(), 0); skeleton->setTitle(value); QCOMPARE(skeleton->title(), value); QCOMPARE(spy.count(), 1); } // description { const QString value = "newDescription"; QSignalSpy spy(skeleton.get(), SIGNAL(descriptionChanged())); QCOMPARE(spy.count(), 0); skeleton->setDescription(value); QCOMPARE(skeleton->description(), value); QCOMPARE(spy.count(), 1); } } void TestSkeletonResource::fileLoadSaveCompleteness() { // boilerplate std::shared_ptr language(new LanguageStub("de")); ResourceRepositoryStub repository({language}); const QString courseDirectory = "data/contributorrepository/skeletons/"; const QString courseFile = courseDirectory + "skeleton.xml"; auto skeleton = SkeletonResource::create(QUrl::fromLocalFile(courseFile), &repository); QTemporaryFile outputFile; outputFile.open(); skeleton->exportToFile(QUrl::fromLocalFile(outputFile.fileName())); // note: this only works, since the resource manager not checks uniqueness of course ids! auto loadedSkeleton = SkeletonResource::create(QUrl::fromLocalFile(outputFile.fileName()), &repository); // test that we actually call the different files QVERIFY(skeleton->file().toLocalFile() != loadedSkeleton->file().toLocalFile()); QCOMPARE(loadedSkeleton->id(), skeleton->id()); QCOMPARE(loadedSkeleton->foreignId(), skeleton->foreignId()); QCOMPARE(loadedSkeleton->title(), skeleton->title()); QCOMPARE(loadedSkeleton->description(), skeleton->description()); QCOMPARE(loadedSkeleton->language(), skeleton->language()); QCOMPARE(loadedSkeleton->units().count(), skeleton->units().count()); auto testUnit = skeleton->units().constFirst(); auto compareUnit = loadedSkeleton->units().constFirst(); QCOMPARE(testUnit->id(), compareUnit->id()); QCOMPARE(testUnit->foreignId(), compareUnit->foreignId()); QCOMPARE(testUnit->title(), compareUnit->title()); QCOMPARE(testUnit->phraseList().count(), compareUnit->phraseList().count()); Phrase *testPhrase = testUnit->phraseList().constFirst(); Phrase *comparePhrase = new Phrase(this); // note that this actually means that we DO NOT respect phrase orders by list order for (Phrase *phrase : compareUnit->phraseList()) { if (testPhrase->id() == phrase->id()) { comparePhrase = phrase; break; } } QVERIFY(testPhrase->id() == comparePhrase->id()); QVERIFY(testPhrase->foreignId() == comparePhrase->foreignId()); QVERIFY(testPhrase->text() == comparePhrase->text()); QVERIFY(testPhrase->type() == comparePhrase->type()); QVERIFY(testPhrase->sound().fileName() == comparePhrase->sound().fileName()); QVERIFY(testPhrase->phonemes().count() == comparePhrase->phonemes().count()); } QTEST_GUILESS_MAIN(TestSkeletonResource) diff --git a/src/core/editorsession.cpp b/src/core/editorsession.cpp index 4ae4c93..70b4457 100644 --- a/src/core/editorsession.cpp +++ b/src/core/editorsession.cpp @@ -1,336 +1,338 @@ /* * Copyright 2013-2015 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "editorsession.h" #include "core/language.h" #include "core/resources/editablecourseresource.h" #include "core/resources/skeletonresource.h" #include "core/unit.h" #include "core/phrase.h" #include "core/contributorrepository.h" #include "artikulate_debug.h" EditorSession::EditorSession(QObject *parent) : QObject(parent) { connect(this, &EditorSession::skeletonChanged, this, &EditorSession::displayedCourseChanged); connect(this, &EditorSession::courseChanged, this, &EditorSession::displayedCourseChanged); connect(this, &EditorSession::editSkeletonChanged, this, &EditorSession::displayedCourseChanged); connect(this, &EditorSession::displayedCourseChanged, this, &EditorSession::updateDisplayedUnit); connect(this, &EditorSession::courseChanged, this, &EditorSession::skeletonModeChanged); } void EditorSession::setRepository(IEditableRepository *repository) { m_repository = repository; } bool EditorSession::skeletonMode() const { return m_skeleton != nullptr; } void EditorSession::setEditSkeleton(bool enabled) { if (m_editSkeleton == enabled) { return; } m_editSkeleton = enabled; emit editSkeletonChanged(); } bool EditorSession::isEditSkeleton() const { return m_editSkeleton; } IEditableCourse * EditorSession::skeleton() const { return m_skeleton; } void EditorSession::setSkeleton(IEditableCourse *skeleton) { if (m_skeleton == skeleton) { return; } m_skeleton = skeleton; IEditableCourse *newCourse{ nullptr }; if (m_skeleton && m_repository) { for (const auto &course : m_repository->editableCourses()) { if (course->foreignId() == m_skeleton->id()) { newCourse = course.get(); break; } } } setCourse(newCourse); emit skeletonChanged(); } ILanguage * EditorSession::language() const { return m_language; } IEditableCourse * EditorSession::course() const { return m_course; } void EditorSession::setCourse(IEditableCourse *course) { if (m_course == course) { return; } m_course = course; if (m_course != nullptr) { // update skeleton IEditableCourse * newSkeleton{ nullptr }; if (m_skeleton == nullptr || m_skeleton->id() != course->foreignId()) { for (const auto &skeleton : m_repository->skeletons()) { if (skeleton->id() == course->foreignId()) { newSkeleton = skeleton.get(); break; } } m_skeleton = newSkeleton; emit skeletonChanged(); } // update language m_language = m_course->language().get(); } else { m_language = nullptr; } emit languageChanged(); emit courseChanged(); } void EditorSession::setCourseByLanguage(ILanguage *language) { if (!skeletonMode() || m_skeleton == nullptr) { qDebug() << "Course selection by language is only available in skeleton mode"; return; } if (language == nullptr || m_repository == nullptr) { return; } IEditableCourse *newCourse{ nullptr }; QString languageId; if (language) { languageId = language->id(); } for (auto course : m_repository->editableCourses()) { if (course->foreignId() == m_skeleton->id() && course->language()->id() == language->id()) { newCourse = course.get(); break; } } setCourse(newCourse); } IEditableCourse * EditorSession::displayedCourse() const { IEditableCourse * course{ nullptr }; if (m_editSkeleton) { course = m_skeleton; } else { course = m_course; } return course; } void EditorSession::updateDisplayedUnit() { auto course = displayedCourse(); if (course != nullptr) { auto units = course->units(); if (!units.isEmpty()) { setUnit(units.constFirst().get()); return; } } } Unit * EditorSession::unit() const { return m_unit; } Unit * EditorSession::activeUnit() const { return m_unit; } void EditorSession::setUnit(Unit *unit) { if (m_unit == unit) { return; } m_unit = unit; if (!m_unit->phraseList().isEmpty()) { setPhrase(m_unit->phraseList().first()); } else { setPhrase(nullptr); } emit unitChanged(); } void EditorSession::setPhrase(Phrase *phrase) { if (m_phrase == phrase) { return; } if (phrase) { setUnit(phrase->unit()); } m_phrase = phrase; emit phraseChanged(); } Phrase * EditorSession::activePhrase() const { return m_phrase; } Phrase * EditorSession::phrase() const { return m_phrase; } Phrase * EditorSession::previousPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); if (index > 0) { return m_phrase->unit()->phraseList().at(index - 1); } else { auto unit = m_phrase->unit(); int uIndex{ -1 }; for (int i = 0; i < unit->course()->units().size(); ++i) { auto testUnit = unit->course()->units().at(i); if (testUnit.get() == unit) { uIndex = i; break; } } if (uIndex > 0) { return unit->course()->units().at(uIndex - 1)->phraseList().last(); } } return m_phrase; } Phrase * EditorSession::nextPhrase() const { if (!m_phrase) { return nullptr; } const int index = m_phrase->unit()->phraseList().indexOf(m_phrase); if (index < m_phrase->unit()->phraseList().length() - 1) { return m_phrase->unit()->phraseList().at(index + 1); } else { Unit *unit = m_phrase->unit(); int uIndex{ -1 }; for (int i = 0; i < unit->course()->units().size(); ++i) { auto testUnit = unit->course()->units().at(i); if (testUnit.get() == unit) { uIndex = i; break; } } if (uIndex < unit->course()->units().length() - 1) { auto nextUnit = unit->course()->units().at(uIndex + 1); if (nextUnit->phraseList().isEmpty()) { return nullptr; } return nextUnit->phraseList().constFirst(); } } return m_phrase; } void EditorSession::switchToPreviousPhrase() { if (hasPreviousPhrase()) { setPhrase(previousPhrase()); } } void EditorSession::switchToNextPhrase() { if (hasNextPhrase()) { setPhrase(nextPhrase()); } } bool EditorSession::hasPreviousPhrase() const { - if (!m_course || !m_unit || !m_phrase) { + if (!m_unit || !m_phrase) { return false; } + Q_ASSERT(m_unit->course() != nullptr); const int phraseIndex = m_phrase->unit()->phraseList().indexOf(m_phrase); int unitIndex = -1; for (int i = 0; i < m_unit->course()->units().size(); ++i) { if (m_unit->course()->units().at(i).get() == m_unit) { unitIndex = i; break; } } if (unitIndex > 0 || phraseIndex > 0) { return true; } return false; } bool EditorSession::hasNextPhrase() const { - if (!m_course || !m_unit || !m_phrase) { + if (!m_unit || !m_phrase) { return false; } + Q_ASSERT(m_unit->course() != nullptr); const int phraseIndex = m_phrase->unit()->phraseList().indexOf(m_phrase); int unitIndex = -1; for (int i = 0; i < m_unit->course()->units().size(); ++i) { if (m_unit->course()->units().at(i).get() == m_unit) { unitIndex = i; break; } } if ((unitIndex >= 0 && unitIndex < m_course->units().size() - 1) || (phraseIndex >= 0 && phraseIndex < m_unit->phraseList().size() - 1)) { return true; } return false; } void EditorSession::updateCourseFromSkeleton() { if (!m_course) { qCritical() << "Not updating course from skeleton, no one set."; return; } m_repository->updateCourseFromSkeleton(m_course->self()); } diff --git a/src/core/resources/skeletonresource.cpp b/src/core/resources/skeletonresource.cpp index f131663..cce07c0 100644 --- a/src/core/resources/skeletonresource.cpp +++ b/src/core/resources/skeletonresource.cpp @@ -1,365 +1,370 @@ /* * Copyright 2013 Andreas Cord-Landwehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "skeletonresource.h" #include "courseparser.h" #include "core/language.h" #include "core/unit.h" #include "core/phrase.h" #include "editablecourseresource.h" #include "core/phoneme.h" #include "core/phonemegroup.h" #include #include #include #include #include #include #include #include "artikulate_debug.h" class SkeletonResourcePrivate { public: SkeletonResourcePrivate(const QUrl &path) : m_path(path) { // load basic information from language file, but does not parse everything QXmlStreamReader xml; QFile file(path.toLocalFile()); if (file.open(QIODevice::ReadOnly)) { xml.setDevice(&file); xml.readNextStartElement(); while (xml.readNext() && !xml.atEnd()) { if (xml.name() == "id") { m_identifier = xml.readElementText(); continue; } if (xml.name() == "title") { m_title = xml.readElementText(); continue; } if (xml.name() == "description") { m_description = xml.readElementText(); continue; } // quit reading when basic elements are read if (!m_identifier.isEmpty() && !m_title.isEmpty() && !m_description.isEmpty() ) { break; } } if (xml.hasError()) { qCritical() << "Error occurred when reading Course XML file:" << path.toLocalFile(); } } else { qCCritical(ARTIKULATE_CORE()) << "Could not open course file" << path.toLocalFile(); } xml.clear(); file.close(); m_modified = false; } QVector> units(); std::shared_ptr appendUnit(std::shared_ptr unit); /** * @return the skeleton resource as serialized byte array */ QDomDocument serializedSkeleton(); std::weak_ptr m_self; QUrl m_path; QString m_identifier; QString m_title; QString m_description; bool m_unitsParsed{ false }; bool m_modified{ false }; protected: QVector> m_units; ///!< the units variable is loaded lazily and shall never be access directly }; QVector> SkeletonResourcePrivate::units() { if (m_unitsParsed) { return m_units; } auto units = CourseParser::parseUnits(m_path); for (auto &unit : units) { + Q_ASSERT(m_self.lock() != nullptr); + unit->setCourse(m_self.lock().get()); m_units.append(std::move(unit)); } m_unitsParsed = true; return m_units; } -std::shared_ptr SkeletonResourcePrivate::appendUnit(std::shared_ptr unit) { +std::shared_ptr SkeletonResourcePrivate::appendUnit(std::shared_ptr unit) +{ units(); // ensure that units are parsed m_units.append(unit); m_modified = true; + Q_ASSERT(m_self.lock() != nullptr); + unit->setCourse(m_self.lock().get()); return m_units.last(); } QDomDocument SkeletonResourcePrivate::serializedSkeleton() { QDomDocument document; // prepare xml header QDomProcessingInstruction header = document.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\"")); document.appendChild(header); // create main element QDomElement root = document.createElement(QStringLiteral("skeleton")); document.appendChild(root); QDomElement idElement = document.createElement(QStringLiteral("id")); QDomElement titleElement = document.createElement(QStringLiteral("title")); QDomElement descriptionElement = document.createElement(QStringLiteral("description")); idElement.appendChild(document.createTextNode(m_identifier)); titleElement.appendChild(document.createTextNode(m_title)); descriptionElement.appendChild(document.createTextNode(m_description)); QDomElement unitListElement = document.createElement(QStringLiteral("units")); // create units for (auto unit : units()) { QDomElement unitElement = document.createElement(QStringLiteral("unit")); QDomElement unitIdElement = document.createElement(QStringLiteral("id")); QDomElement unitTitleElement = document.createElement(QStringLiteral("title")); QDomElement unitPhraseListElement = document.createElement(QStringLiteral("phrases")); unitIdElement.appendChild(document.createTextNode(unit->id())); unitTitleElement.appendChild(document.createTextNode(unit->title())); // construct phrases for (Phrase *phrase : unit->phraseList()) { QDomElement phraseElement = document.createElement(QStringLiteral("phrase")); QDomElement phraseIdElement = document.createElement(QStringLiteral("id")); QDomElement phraseTextElement = document.createElement(QStringLiteral("text")); QDomElement phraseTypeElement = document.createElement(QStringLiteral("type")); phraseIdElement.appendChild(document.createTextNode(phrase->id())); phraseTextElement.appendChild(document.createTextNode(phrase->text())); phraseTypeElement.appendChild(document.createTextNode(phrase->typeString())); phraseElement.appendChild(phraseIdElement); phraseElement.appendChild(phraseTextElement); phraseElement.appendChild(phraseTypeElement); unitPhraseListElement.appendChild(phraseElement); } // construct the unit element unitElement.appendChild(unitIdElement); unitElement.appendChild(unitTitleElement); unitElement.appendChild(unitPhraseListElement); unitListElement.appendChild(unitElement); } root.appendChild(idElement); root.appendChild(titleElement); root.appendChild(descriptionElement); root.appendChild(unitListElement); return document; } std::shared_ptr SkeletonResource::create(const QUrl &path, IResourceRepository *repository) { std::shared_ptr course(new SkeletonResource(path, repository)); course->setSelf(course); return course; } SkeletonResource::SkeletonResource(const QUrl &path, IResourceRepository *repository) : IEditableCourse() , d(new SkeletonResourcePrivate(path)) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); connect(this, &SkeletonResource::idChanged, this, [=]() { d->m_modified = true; }); connect(this, &SkeletonResource::titleChanged, this, [=]() { d->m_modified = true; }); connect(this, &SkeletonResource::descriptionChanged, this, [=]() { d->m_modified = true; }); Q_UNUSED(repository); } SkeletonResource::~SkeletonResource() = default; void SkeletonResource::setSelf(std::shared_ptr self) { d->m_self = self; } std::shared_ptr SkeletonResource::self() const { return std::static_pointer_cast(d->m_self.lock()); } QString SkeletonResource::id() const { return d->m_identifier; } void SkeletonResource::setId(QString id) { if (d->m_identifier == id) { return; } d->m_identifier = id; emit idChanged(); } QString SkeletonResource::foreignId() const { return id(); } void SkeletonResource::setForeignId(QString id) { Q_UNUSED(id); Q_UNREACHABLE(); } QString SkeletonResource::title() const { return d->m_title; } void SkeletonResource::setTitle(QString title) { if (d->m_title == title) { return; } d->m_title = title; emit titleChanged(); } QString SkeletonResource::i18nTitle() const { // there are no localized titles available return title(); } void SkeletonResource::setI18nTitle(QString title) { Q_UNUSED(title); Q_UNREACHABLE(); } QString SkeletonResource::description() const { return d->m_description; } void SkeletonResource::setDescription(QString description) { if (d->m_description == description) { return; } d->m_description = description; emit descriptionChanged(); } bool SkeletonResource::exportToFile(const QUrl &filePath) const { // write back to file // create directories if necessary QFileInfo info(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); if (!info.exists()) { qCDebug(ARTIKULATE_LOG()) << "create xml output file directory, not existing"; QDir dir; dir.mkpath(filePath.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); } //TODO port to atomic file swap QFile file(filePath.toLocalFile()); if (!file.open(QIODevice::WriteOnly)) { qCWarning(ARTIKULATE_LOG()) << "Unable to open file " << filePath << " in write mode, aborting."; return false; } file.write(d->serializedSkeleton().toByteArray()); return true; } std::shared_ptr SkeletonResource::addUnit(std::unique_ptr unit) { std::shared_ptr storedUnit(std::move(unit)); emit unitAboutToBeAdded(storedUnit, d->units().count() - 1); d->appendUnit(storedUnit); emit unitAdded(); return storedUnit; } bool SkeletonResource::sync() { if (!d->m_modified) { qCDebug(ARTIKULATE_LOG()) << "Aborting sync, skeleton was not modified."; return false; } bool ok = exportToFile(file()); if (ok) { d->m_modified = false; } return ok; } void SkeletonResource::updateFrom(std::shared_ptr) { // not supported } bool SkeletonResource::isModified() const { return d->m_modified; } std::shared_ptr SkeletonResource::language() const { // skeleton must not have a dedicated language return std::shared_ptr(); } void SkeletonResource::setLanguage(std::shared_ptr language) { Q_UNUSED(language); Q_UNREACHABLE(); } QVector> SkeletonResource::units() { return d->units(); } QUrl SkeletonResource::file() const { return d->m_path; }