Commit 81147434 authored by Andreas Cord-Landwehr's avatar Andreas Cord-Landwehr
Browse files

Make ContributorRepository with IRepository API contract

First steps for fulfilling the API contract:
- create test
- check course loading handling
- check signals
parent 7f593153
......@@ -32,6 +32,7 @@ include_directories(
# copy test data
file(COPY testdata/courses/de.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses/de/) # copy test files
file(COPY testdata/courses/fr.xml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/courses/fr/) # copy test files
file(COPY testdata/contributorrepository/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/contributorrepository/) # copy test files
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR})
......@@ -46,6 +47,17 @@ target_link_libraries(test_resourcerepository
add_test(test_resourcerepository test_resourcerepository)
ecm_mark_as_test(test_resourcerepository)
# integration tests for iresource repository interface derived classes
set(TestIResourceRepository_SRCS iresourcerepository_integration/test_iresourcerepository.cpp)
qt5_add_resources(TestIResourceRepository_SRCS ../data/languages.qrc)
add_executable(test_iresourcerepository_integration ${TestIResourceRepository_SRCS})
target_link_libraries(test_iresourcerepository_integration
artikulatecore
Qt5::Test
)
add_test(test_iresourcerepository_integration test_iresourcerepository_integration)
ecm_mark_as_test(test_iresourcerepository_integration)
# training session tests
set(TestTrainingSession_SRCS trainingsession/test_trainingsession.cpp)
add_executable(test_trainingsession ${TestTrainingSession_SRCS})
......
/*
* Copyright 2019 Andreas Cord-Landwehr <cordlandwehr@kde.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "test_iresourcerepository.h"
#include <QTest>
#include <QSignalSpy>
#include <QObject>
#include "src/core/resourcerepository.h"
#include "src/core/contributorrepository.h"
#include "src/core/language.h"
#include "../src/settings.h"
void TestIResourceRepository::init()
{
// check that test data is deployed at the expected location
QVERIFY(QFile::exists("data/courses/de/de.xml"));
QVERIFY(QFile::exists("data/courses/fr/fr.xml"));
}
void TestIResourceRepository::resourceRepository()
{
ResourceRepository repository(QUrl::fromLocalFile("data/courses/"));
QCOMPARE(repository.storageLocation(), "data/courses/");
performInterfaceTests(&repository);
}
void TestIResourceRepository::contributorRepository()
{
ContributorRepository repository;
repository.setStorageLocation("data/contributorrepository/"); // contributor repository requires subdirectory "courses"
QCOMPARE(repository.storageLocation(), "data/contributorrepository/");
performInterfaceTests(&repository);
}
void TestIResourceRepository::performInterfaceTests(IResourceRepository *interface)
{
QVERIFY(interface->languages().count() > 0); // automatically load languages
QCOMPARE(interface->courses().count(), 0); // load courses only on demand
// test adding
QSignalSpy spyAboutToBeAdded(dynamic_cast<QObject *>(interface), SIGNAL(courseAboutToBeAdded(ICourse*, int)));
QSignalSpy spyAdded(dynamic_cast<QObject *>(interface), SIGNAL(courseAdded()));
QCOMPARE(spyAboutToBeAdded.count(), 0);
QCOMPARE(spyAdded.count(), 0);
interface->reloadCourses(); // initial loading of courses
QCOMPARE(interface->courses().count(), 2);
QCOMPARE(spyAboutToBeAdded.count(), 2);
QCOMPARE(spyAdded.count(), 2);
// test reloading of courses
interface->reloadCourses(); // initial loading of courses
QCOMPARE(interface->courses().count(), 2);
// test removal
// note: repository does not provide removal of courses, yet
// test access of courses grouped by language
auto languages = interface->languages();
Language *german = nullptr;
for (auto language : interface->languages()) {
if (language->id() == "de") {
german = language;
break;
}
}
QVERIFY(german != nullptr); // ensure that German language was found
QCOMPARE(interface->courses(german).count(), 1); // there is exactly one German course
QCOMPARE(interface->courses(nullptr).count(), 2); // all courses in total are 2
}
QTEST_GUILESS_MAIN(TestIResourceRepository)
/*
* Copyright 2019 Andreas Cord-Landwehr <cordlandwehr@kde.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef TEST_IRESOURCEREPOSITORY_H
#define TEST_IRESOURCEREPOSITORY_H
#include <QObject>
class IResourceRepository;
class TestIResourceRepository : public QObject
{
Q_OBJECT
public:
TestIResourceRepository() = default;
private Q_SLOTS:
/**
* Called before every test case.
*/
void init();
/**
* @brief integration test of ResourceRepository for IResourceRepository interface contract
* Test that expectations of the IResourceRepository interface are implemented by
* ResourceRepository class.
*/
void resourceRepository();
/**
* @brief integration test of ContributorRepository for IResourceRepository interface contract
* Test that expectations of the IResourceRepository interface are implemented by
* ContributorRepository class.
*/
void contributorRepository();
private:
void performInterfaceTests(IResourceRepository *repository);
};
#endif
<?xml version="1.0"?>
<course>
<id>de</id>
<foreignId>artikulate-basic</foreignId>
<title>Artikulate Deutsch</title>
<description>Ein Kurs in (hoch-)deutscher Aussprache.</description>
<language>de</language>
<units>
<unit>
<id>1</id>
<foreignId>{dd60f04a-eb37-44b7-9787-67aaf7d3578d}</foreignId>
<title>Auf der Straße</title>
<phrases>
<phrase>
<id>1</id>
<foreignId>{3a4c1926-60d7-44c6-80d1-03165a641c75}</foreignId>
<text>Guten Tag.</text>
<soundFile>de_01.ogg</soundFile>
<type>sentence</type>
<phonemes></phonemes>
</phrase>
<phrase>
<id>2</id>
<foreignId></foreignId>
<text>Auf Wiedersehen.</text>
<soundFile>de_02.ogg</soundFile>
<type>sentence</type>
<phonemes></phonemes>
</phrase>
<phrase>
<id>3</id>
<foreignId>{56b0d0a2-8505-4a9e-89f7-a933824fac89}</foreignId>
<text>Wie geht es dir?</text>
<soundFile></soundFile>
<type>sentence</type>
<phonemes></phonemes>
</phrase>
</phrases>
</unit>
</units>
</course>
<?xml version="1.0"?>
<course>
<id>fr</id>
<title>ArtiKulate Français</title>
<description>Course française.</description>
<language>fr</language>
<units>
<unit>
<id>1</id>
<title>Cuisine Français</title>
<phrases>
<phrase>
<id>1</id>
<text>Qu'est-ce que vous avez choisi?</text>
<soundFile></soundFile>
<type>sentence</type>
<phonemes></phonemes>
</phrase>
<phrase>
<id>2</id>
<text>Moi, comme entrée, une salade exotique.</text>
<soundFile></soundFile>
<type>sentence</type>
<phonemes></phonemes>
</phrase>
<phrase>
<id>3</id>
<text>eau</text>
<soundFile></soundFile>
<type>word</type>
<phonemes>
<phonemeID>oh</phonemeID>
</phonemes>
</phrase>
</phrases>
</unit>
</units>
</course>
......@@ -28,7 +28,6 @@
#include "resources/languageresource.h"
#include "resources/editablecourseresource.h"
#include "resources/skeletonresource.h"
#include "settings.h"
#include "liblearnerprofile/src/profilemanager.h"
#include "liblearnerprofile/src/learninggoal.h"
......@@ -48,83 +47,7 @@
ContributorRepository::ContributorRepository(QObject *parent)
: IResourceRepository()
{
}
void ContributorRepository::loadCourseResources()
{
//TODO fix this method such that it may be called many times of e.g. updating
// reload config, could be changed in dialogs
Settings::self()->load();
// register skeleton resources
QDir skeletonRepository = QDir(Settings::courseRepositoryPath());
skeletonRepository.setFilter(QDir::Files | QDir::Hidden);
if (!skeletonRepository.cd(QStringLiteral("skeletons"))) {
qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonRepository.path()
<< " cannot load skeletons.";
} else {
// read skeletons
QFileInfoList list = skeletonRepository.entryInfoList();
for (int i = 0; i < list.size(); ++i) {
QFileInfo fileInfo = list.at(i);
addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
}
}
// register contributor course files
QDir courseRepository = QDir(Settings::courseRepositoryPath());
if (!courseRepository.cd(QStringLiteral("courses"))) {
qCritical() << "There is no subdirectory \"courses\" in directory " << courseRepository.path()
<< " cannot load courses.";
} else {
// find courses
courseRepository.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
QFileInfoList courseDirList = courseRepository.entryInfoList();
// traverse all course directories
foreach (const QFileInfo &info, courseDirList) {
QDir courseDir = QDir(info.absoluteFilePath());
courseDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
QFileInfoList courseLangDirList = courseDir.entryInfoList();
// traverse all language directories for each course
foreach (const QFileInfo &langInfo, courseLangDirList) {
QDir courseLangDir = QDir(langInfo.absoluteFilePath());
courseLangDir.setFilter(QDir::Files);
QStringList nameFilters;
nameFilters.append(QStringLiteral("*.xml"));
QFileInfoList courses = courseLangDir.entryInfoList(nameFilters);
// find and add course files
foreach (const QFileInfo &courseInfo, courses) {
addCourse(QUrl::fromLocalFile(courseInfo.filePath()));
}
}
}
}
// register GHNS course resources
QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::DataLocation);
foreach (const QString &testdir, dirs) {
QDirIterator it(testdir + "/courses/", QDirIterator::Subdirectories);
while (it.hasNext()) {
QDir dir(it.next());
dir.setFilter(QDir::Files | QDir::NoSymLinks);
QFileInfoList list = dir.entryInfoList();
for (int i = 0; i < list.size(); ++i) {
QFileInfo fileInfo = list.at(i);
if (fileInfo.completeSuffix() != QLatin1String("xml")) {
continue;
}
addCourse(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
}
}
}
//TODO this signal should only be emitted when repository was added/removed
// yet the call to this method is very seldom and emitting it too often is not that harmful
emit repositoryChanged();
loadLanguageResources();
}
void ContributorRepository::loadLanguageResources()
......@@ -188,14 +111,14 @@ void ContributorRepository::addLanguage(const QUrl &languageFile)
emit languageResourceAdded();
}
bool ContributorRepository::isRepositoryManager() const
QString ContributorRepository::storageLocation() const
{
return !Settings::courseRepositoryPath().isEmpty();
return m_storageLocation;
}
QString ContributorRepository::storageLocation() const
void ContributorRepository::setStorageLocation(const QString &path)
{
return Settings::courseRepositoryPath();
m_storageLocation = path;
}
QList< LanguageResource* > ContributorRepository::languageResources() const
......@@ -254,12 +177,28 @@ QList<EditableCourseResource *> ContributorRepository::courseResources(Language
QVector<ICourse *> ContributorRepository::courses() const
{
return QVector<ICourse *>(); //TODO and check if overload for editable is needed
QVector<ICourse *> courses;
for (const auto &courseList : m_courses) {
for (const auto &course : courseList) {
courses.append(course);
}
}
return courses;
}
QVector<ICourse *> ContributorRepository::courses(Language *language) const
{
return QVector<ICourse *>(); //TODO and check if overload for editable is needed
if (language == nullptr) {
return courses();
}
QVector<ICourse *> courses;
if (m_courses.contains(language->id())) {
for (const auto &course : m_courses[language->id()]) {
courses.append(course);
}
}
return courses;
}
EditableCourseResource * ContributorRepository::course(Language *language, int index) const
......@@ -299,6 +238,60 @@ void ContributorRepository::reloadCourseOrSkeleton(ICourse *courseOrSkeleton)
}
}
void ContributorRepository::reloadCourses()
{
// register skeleton resources
QDir skeletonDirectory = QDir(storageLocation());
skeletonDirectory.setFilter(QDir::Files | QDir::Hidden);
if (!skeletonDirectory.cd(QStringLiteral("skeletons"))) {
qCritical() << "There is no subdirectory \"skeletons\" in directory " << skeletonDirectory.path()
<< " cannot load skeletons.";
} else {
// read skeletons
QFileInfoList list = skeletonDirectory.entryInfoList();
for (int i = 0; i < list.size(); ++i) {
QFileInfo fileInfo = list.at(i);
addSkeleton(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
}
}
// register contributor course files
QDir courseDirectory(storageLocation());
if (!courseDirectory.cd(QStringLiteral("courses"))) {
qCritical() << "There is no subdirectory \"courses\" in directory " << courseDirectory.path()
<< " cannot load courses.";
} else {
// find courses
courseDirectory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
QFileInfoList courseDirList = courseDirectory.entryInfoList();
// traverse all course directories
for (const QFileInfo &info : courseDirList) {
QDir courseDir = QDir(info.absoluteFilePath());
courseDir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
QFileInfoList courseLangDirList = courseDir.entryInfoList();
// traverse all language directories for each course
for (const QFileInfo &langInfo : courseLangDirList) {
QDir courseLangDir = QDir(langInfo.absoluteFilePath());
courseLangDir.setFilter(QDir::Files);
QStringList nameFilters;
nameFilters.append(QStringLiteral("*.xml"));
QFileInfoList courses = courseLangDir.entryInfoList(nameFilters);
// find and add course files
for (const QFileInfo &courseInfo : courses) {
addCourse(QUrl::fromLocalFile(courseInfo.filePath()));
}
}
}
}
//TODO this signal should only be emitted when repository was added/removed
// yet the call to this method is very seldom and emitting it too often is not that harmful
emit repositoryChanged();
}
void ContributorRepository::updateCourseFromSkeleton(EditableCourseResource *course)
{
//TODO implement status information that are shown at mainwindow
......@@ -397,13 +390,10 @@ void ContributorRepository::addCourseResource(EditableCourseResource *resource)
{
Q_ASSERT(m_courses.contains(resource->language()->id()));
if (m_courses.contains(resource->language()->id())) {
// emit courseResourceAboutToBeAdded(resource, m_courses[resource->language()].count()); //FIXME
}
else {
emit courseAboutToBeAdded(resource, 0);
if (!m_courses.contains(resource->language()->id())) {
m_courses.insert(resource->language()->id(), QList<EditableCourseResource*>());
}
emit courseAboutToBeAdded(resource, m_courses[resource->language()->id()].count());
m_courses[resource->language()->id()].append(resource);
emit courseAdded();
}
......@@ -425,7 +415,7 @@ EditableCourseResource * ContributorRepository::createCourse(Language *language,
{
// set path
QString path = QStringLiteral("%1/%2/%3/%4/%4.xml")
.arg(Settings::courseRepositoryPath(),
.arg(storageLocation(),
QStringLiteral("courses"),
skeleton->id(),
language->id());
......
......@@ -36,13 +36,8 @@ class LanguageResource;
class Skeleton;
class Language;
class ICourse;
class ProfileManager;
class QUrl;
namespace LearnerProfile {
class ProfileManager;
}
/**
* @class ContributorRepository
* This class handles the resources of a contributor.
......@@ -57,21 +52,6 @@ class ARTIKULATECORE_EXPORT ContributorRepository : public IResourceRepository
public:
explicit ContributorRepository(QObject *parent = nullptr);
/**
* Load all course resources.
* This loading is very fast, since course files are only partly (~20 top lines) parsed and
* the complete parsing is postponed until first access.
*
* This method is safe to be called several times for incremental updates.
*/
Q_INVOKABLE void loadCourseResources();
/**
* This method loads all language files that are provided in the standard directories
* for this application.
*/
void loadLanguageResources();
/**
* save all changes to course resources
*/
......@@ -83,14 +63,15 @@ public:
bool modified() const;
/**
* \return \c true if a repository is used, else \c false
* \return path to working repository, if one is set
*/
Q_INVOKABLE bool isRepositoryManager() const;
QString storageLocation() const override;
/**
* \return path to working repository, if one is set
* Set path to central storage location
* \param path the path to the storage location directory
*/
QString storageLocation() const override;
void setStorageLocation(const QString &path);
/**
* \return list of all available language specifications
......@@ -129,8 +110,10 @@ public:
*/
Q_INVOKABLE void reloadCourseOrSkeleton(ICourse *course);
//TODO implement some logic
void reloadCourses() override {}
/**
* @brief Implementation of course resource reloading
*/
void reloadCourses() override;
/**
* Imports units and phrases from skeleton, deassociates removed ones.
......@@ -221,6 +204,12 @@ Q_SIGNALS:
void languageCoursesChanged();
private:
/**
* This method loads all language files that are provided in the standard directories
* for this application.
*/
void loadLanguageResources();
QString m_storageLocation;
QList<LanguageResource *> m_languageResources;
QMap<QString, QList<EditableCourseResource *> > m_courses; //!> (language-id, course-resource)
QList<SkeletonResource *> m_skeletonResources;
......
......@@ -73,11 +73,10 @@ MainWindowEditor::MainWindowEditor(ContributorRepository *repository)
OutputDeviceController::self().setVolume(Settings::audioOutputVolume());
// load resources
m_repository->loadLanguageResources();
if (m_repository->languageResources().count() == 0) {
if (m_repository->languages().count() == 0) {
qFatal("No language resources found, cannot start application.");
}
m_repository->loadCourseResources();
m_repository->reloadCourses();
// create menu
setupActions();
......
......@@ -65,5 +65,5 @@ void ResourcesDialogPage::saveSettings()
Settings::setCourseRepositoryPath(ui->kcfg_CourseRepositoryPath->text());
Settings::self()->save();
// reloading resources
m_repository->loadCourseResources();
m_repository->reloadCourses();
}
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