Commit 88acd303 authored by Elvis Angelaccio's avatar Elvis Angelaccio
Browse files

Refactor archive loading

Ark currently loads an archive by using `Archive *Archive::create()` first
and then `ListJob *archive->list()`. If an archive property is read
*before* list() is called, the archive is listed in the background with
listIfNotListed().
This design is responsible for a lot or problems (see T1877, T3296 and T330).

This commit refactors ListJob in a new LoadJob class. Is not possible
anymore to create an archive and then list() it. Instead, a LoadJob is
started first and then the archive can be retrieved at the end of the
job.

Differential Revision: D2811
parent 8cb2ca5f
......@@ -28,18 +28,16 @@
#include "batchextract.h"
#include "ark_debug.h"
#include "kerfuffle/archive_kerfuffle.h"
#include "kerfuffle/extractiondialog.h"
#include "kerfuffle/jobs.h"
#include "kerfuffle/queries.h"
#include <KIO/JobTracker>
#include <KLocalizedString>
#include <KMessageBox>
#include <KRun>
#include <KIO/RenameDialog>
#include <kwidgetjobtracker.h>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QPointer>
......@@ -64,46 +62,21 @@ BatchExtract::~BatchExtract()
}
}
void BatchExtract::addExtraction(Kerfuffle::Archive* archive)
void BatchExtract::addExtraction(const QUrl& url)
{
QString destination = destinationFolder();
const bool isSingleFolderRPM = (archive->isSingleFolderArchive() &&
(archive->mimeType().name() == QLatin1String("application/x-rpm")));
if ((autoSubfolder()) && (!archive->isSingleFolderArchive() || isSingleFolderRPM)) {
const QDir d(destination);
QString subfolderName = archive->subfolderName();
// Special case for single folder RPM archives.
// We don't want the autodetected folder to have a meaningless "usr" name.
if (isSingleFolderRPM && subfolderName == QStringLiteral("usr")) {
qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name";
subfolderName = QFileInfo(archive->fileName()).completeBaseName();
}
if (d.exists(subfolderName)) {
subfolderName = KIO::suggestName(QUrl::fromUserInput(destination, QString(), QUrl::AssumeLocalFile), subfolderName);
}
d.mkdir(subfolderName);
destination += QLatin1Char( '/' ) + subfolderName;
}
auto job = Kerfuffle::Archive::batchExtract(url.toLocalFile(), destination, autoSubfolder(), preservePaths());
Kerfuffle::ExtractionOptions options;
options[QStringLiteral("PreservePaths")] = preservePaths();
Kerfuffle::ExtractJob *job = archive->extractFiles(QList<Kerfuffle::Archive::Entry*>(), destination, options);
qCDebug(ARK) << QString(QStringLiteral("Registering job from archive %1, to %2, preservePaths %3")).arg(archive->fileName(), destination, QString::number(preservePaths()));
qCDebug(ARK) << QString(QStringLiteral("Registering job from archive %1, to %2, preservePaths %3")).arg(url.toLocalFile(), destination, QString::number(preservePaths()));
addSubjob(job);
m_fileNames[job] = qMakePair(archive->fileName(), destination);
m_fileNames[job] = qMakePair(url.toLocalFile(), destination);
connect(job, SIGNAL(percent(KJob*,ulong)),
this, SLOT(forwardProgress(KJob*,ulong)));
connect(job, &Kerfuffle::Job::userQuery,
connect(job, &Kerfuffle::BatchExtractJob::userQuery,
this, &BatchExtract::slotUserQuery);
}
......@@ -129,14 +102,13 @@ void BatchExtract::start()
void BatchExtract::slotStartJob()
{
// If none of the archives could be loaded, there is no subjob to run
if (m_inputs.isEmpty()) {
emitResult();
return;
}
foreach(Kerfuffle::Archive *archive, m_inputs) {
addExtraction(archive);
foreach (const auto& url, m_inputs) {
addExtraction(url);
}
KIO::getJobTracker()->registerJob(this);
......@@ -167,25 +139,24 @@ void BatchExtract::slotResult(KJob *job)
// TODO: The user must be informed about which file caused the error, and that the other files
// in the queue will not be extracted.
if (job->error()) {
qCDebug(ARK) << "There was en error:" << job->error() << ", errorText:" << job->errorText();
qCDebug(ARK) << "There was en error:" << job->error() << ", errorText:" << job->errorString();
setErrorText(job->errorText());
setErrorText(job->errorString());
setError(job->error());
removeSubjob(job);
if (job->error() != KJob::KilledJobError) {
KMessageBox::error(NULL, job->errorText().isEmpty() ?
i18n("There was an error during extraction.") : job->errorText());
KMessageBox::error(Q_NULLPTR, job->errorString().isEmpty() ?
i18n("There was an error during extraction.") : job->errorString());
}
emitResult();
return;
} else {
removeSubjob(job);
}
removeSubjob(job);
if (!hasSubjobs()) {
if (openDestinationAfterExtraction()) {
QUrl destination(destinationFolder());
......@@ -213,21 +184,16 @@ void BatchExtract::forwardProgress(KJob *job, unsigned long percent)
setPercent(jobPart *(m_initialJobCount - subjobs().size()) + percent / m_initialJobCount);
}
bool BatchExtract::addInput(const QUrl& url)
void BatchExtract::addInput(const QUrl& url)
{
qCDebug(ARK) << "Adding archive" << url.toLocalFile();
Kerfuffle::Archive *archive = Kerfuffle::Archive::create(url.toLocalFile(), this);
Q_ASSERT(archive);
if (!QFileInfo::exists(url.toLocalFile())) {
m_failedFiles.append(url.fileName());
return false;
return;
}
m_inputs.append(archive);
return true;
m_inputs.append(url);
}
bool BatchExtract::openDestinationAfterExtraction() const
......@@ -280,14 +246,36 @@ bool BatchExtract::showExtractDialog()
dialog.data()->setCurrentUrl(QUrl::fromUserInput(destinationFolder(), QString(), QUrl::AssumeLocalFile));
dialog.data()->setPreservePaths(preservePaths());
// Only one archive, we need a LoadJob to get the single-folder and subfolder properties.
// TODO: find a better way (e.g. let the dialog handle everything), otherwise we list
// the archive twice (once here and once in the following BatchExtractJob).
Kerfuffle::LoadJob *loadJob = Q_NULLPTR;
if (m_inputs.size() == 1) {
if (m_inputs.at(0)->isSingleFolderArchive()) {
dialog.data()->setSingleFolderArchive(true);
}
dialog.data()->setSubfolder(m_inputs.at(0)->subfolderName());
loadJob = Kerfuffle::Archive::load(m_inputs.at(0).toLocalFile(), this);
// We need to access the job after result has been emitted, if the user rejects the dialog.
loadJob->setAutoDelete(false);
connect(loadJob, &KJob::result, this, [=](KJob *job) {
if (job->error()) {
return;
}
auto archive = qobject_cast<Kerfuffle::LoadJob*>(job)->archive();
dialog->setSingleFolderArchive(archive->isSingleFolder());
dialog->setSubfolder(archive->subfolderName());
});
connect(loadJob, &KJob::result, dialog.data(), &Kerfuffle::ExtractionDialog::setReadyGui);
dialog->setBusyGui();
// NOTE: we exploit the dialog->exec() below to run this job.
loadJob->start();
}
if (!dialog.data()->exec()) {
if (loadJob) {
loadJob->kill();
loadJob->deleteLater();
}
delete dialog.data();
return false;
}
......
......@@ -29,9 +29,10 @@
#ifndef BATCHEXTRACT_H
#define BATCHEXTRACT_H
#include <kcompositejob.h>
#include <KCompositeJob>
#include <QMap>
#include <QVector>
namespace Kerfuffle
{
......@@ -63,15 +64,15 @@ public:
virtual ~BatchExtract();
/**
* Creates an ExtractJob for the given @p archive and puts it on the queue.
* Creates a BatchExtractJob for the given @p url and puts it on the queue.
*
* If necessary, the destination directory for the archive is created.
* If necessary, the destination directory for the archive is created by the job.
*
* @param archive The archive that will be extracted.
* @param url The url of the archive that will be extracted.
*
* @see setAutoSubfolder
*/
void addExtraction(Kerfuffle::Archive* archive);
void addExtraction(const QUrl& url);
/**
* A wrapper that calls slotStartJob() when the event loop has started.
......@@ -106,13 +107,8 @@ public:
* Adds a file to the list of files that will be extracted.
*
* @param url The file that will be added to the list.
*
* @return @c true The file exists and a suitable plugin
* could be found for it.
* @return @c false The file does not exist or a suitable
* plugin could not be found.
*/
bool addInput(const QUrl& url);
void addInput(const QUrl& url);
/**
* Shows the extract options dialog before extracting the files.
......@@ -220,7 +216,7 @@ private:
QMap<KJob*, QPair<QString, QString> > m_fileNames;
bool m_autoSubfolder;
QList<Kerfuffle::Archive*> m_inputs;
QVector<QUrl> m_inputs;
QString m_destinationFolder;
QStringList m_failedFiles;
bool m_preservePaths;
......
......@@ -23,7 +23,7 @@
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "autotests/testhelper/testhelper.h"
#include "testhelper.h"
using namespace Kerfuffle;
......@@ -106,7 +106,12 @@ void AddTest::testAdding()
QFETCH(QString, archiveName);
const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName;
Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath));
Archive *archive = Archive::create(archivePath, this);
auto loadJob = Archive::load(archivePath);
QVERIFY(loadJob);
TestHelper::startAndWaitForResult(loadJob);
auto archive = loadJob->archive();
QVERIFY(archive);
if (!archive->isValid()) {
......
......@@ -23,11 +23,10 @@
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "kerfuffle/addtoarchive.h"
#include "kerfuffle/archive_kerfuffle.h"
#include "kerfuffle/pluginmanager.h"
#include "addtoarchive.h"
#include "pluginmanager.h"
#include "testhelper.h"
#include <QEventLoop>
#include <QMimeDatabase>
#include <QStandardPaths>
#include <QTest>
......@@ -43,7 +42,6 @@ private Q_SLOTS:
void init();
void testCompressHere_data();
void testCompressHere();
void testCreateEncryptedArchive();
};
void AddToArchiveTest::init()
......@@ -55,6 +53,7 @@ void AddToArchiveTest::init()
void AddToArchiveTest::testCompressHere_data()
{
QTest::addColumn<QString>("expectedSuffix");
QTest::addColumn<Archive::EncryptionType>("expectedEncryptionType");
QTest::addColumn<QStringList>("inputFiles");
QTest::addColumn<QString>("expectedArchiveName");
QTest::addColumn<qulonglong>("expectedNumberOfFiles");
......@@ -62,6 +61,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as TAR) - dir with files")
<< QStringLiteral("tar.gz")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdir")}
<< QStringLiteral("testdir.tar.gz")
<< 2ULL
......@@ -69,6 +69,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as TAR) - dir with subdirs")
<< QStringLiteral("tar.gz")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdirwithsubdirs")}
<< QStringLiteral("testdirwithsubdirs.tar.gz")
<< 4ULL
......@@ -76,6 +77,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as TAR) - dir with empty subdir")
<< QStringLiteral("tar.gz")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")}
<< QStringLiteral("testdirwithemptysubdir.tar.gz")
<< 2ULL
......@@ -83,6 +85,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as TAR) - single file")
<< QStringLiteral("tar.gz")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testfile.txt")}
<< QStringLiteral("testfile.tar.gz")
<< 1ULL
......@@ -90,6 +93,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as TAR) - file + folder")
<< QStringLiteral("tar.gz")
<< Archive::Unencrypted
<< QStringList {
QFINDTESTDATA("data/testdir"),
QFINDTESTDATA("data/testfile.txt")
......@@ -100,6 +104,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as TAR) - bug #362690")
<< QStringLiteral("tar.gz")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/test-3.4.0")}
<< QStringLiteral("test-3.4.0.tar.gz")
<< 1ULL
......@@ -108,6 +113,7 @@ void AddToArchiveTest::testCompressHere_data()
if (!PluginManager().preferredWritePluginsFor(QMimeDatabase().mimeTypeForName(QStringLiteral("application/zip"))).isEmpty()) {
QTest::newRow("compress here (as ZIP) - dir with files")
<< QStringLiteral("zip")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdir")}
<< QStringLiteral("testdir.zip")
<< 2ULL
......@@ -115,6 +121,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as ZIP) - dir with subdirs")
<< QStringLiteral("zip")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdirwithsubdirs")}
<< QStringLiteral("testdirwithsubdirs.zip")
<< 4ULL
......@@ -122,6 +129,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as ZIP) - dir with empty subdir")
<< QStringLiteral("zip")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")}
<< QStringLiteral("testdirwithemptysubdir.zip")
<< 2ULL
......@@ -129,6 +137,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as ZIP) - single file")
<< QStringLiteral("zip")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testfile.txt")}
<< QStringLiteral("testfile.zip")
<< 1ULL
......@@ -136,6 +145,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as ZIP) - file + folder")
<< QStringLiteral("zip")
<< Archive::Unencrypted
<< QStringList {
QFINDTESTDATA("data/testdir"),
QFINDTESTDATA("data/testfile.txt")
......@@ -146,6 +156,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as TAR) - dir with special name (see #365798)")
<< QStringLiteral("tar.gz")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/test%dir")}
<< QStringLiteral("test%dir.tar.gz")
<< 2ULL
......@@ -157,6 +168,7 @@ void AddToArchiveTest::testCompressHere_data()
if (!PluginManager().preferredWritePluginsFor(QMimeDatabase().mimeTypeForName(QStringLiteral("application/vnd.rar"))).isEmpty()) {
QTest::newRow("compress here (as RAR) - dir with files")
<< QStringLiteral("rar")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdir")}
<< QStringLiteral("testdir.rar")
<< 2ULL
......@@ -164,6 +176,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as RAR) - dir with subdirs")
<< QStringLiteral("rar")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdirwithsubdirs")}
<< QStringLiteral("testdirwithsubdirs.rar")
<< 4ULL
......@@ -171,6 +184,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as RAR) - dir with empty subdir")
<< QStringLiteral("rar")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")}
<< QStringLiteral("testdirwithemptysubdir.rar")
<< 2ULL
......@@ -178,6 +192,7 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as RAR) - single file")
<< QStringLiteral("rar")
<< Archive::Unencrypted
<< QStringList {QFINDTESTDATA("data/testfile.txt")}
<< QStringLiteral("testfile.rar")
<< 1ULL
......@@ -185,6 +200,18 @@ void AddToArchiveTest::testCompressHere_data()
QTest::newRow("compress here (as RAR) - file + folder")
<< QStringLiteral("rar")
<< Archive::Unencrypted
<< QStringList {
QFINDTESTDATA("data/testdir"),
QFINDTESTDATA("data/testfile.txt")
}
<< QStringLiteral("data.rar")
<< 3ULL
<< 1ULL;
QTest::newRow("compress to encrypted RAR - file + folder")
<< QStringLiteral("rar")
<< Archive::Encrypted
<< QStringList {
QFINDTESTDATA("data/testdir"),
QFINDTESTDATA("data/testfile.txt")
......@@ -205,25 +232,33 @@ void AddToArchiveTest::testCompressHere()
QFETCH(QString, expectedSuffix);
addToArchiveJob->setAutoFilenameSuffix(expectedSuffix);
QFETCH(Archive::EncryptionType, expectedEncryptionType);
if (expectedEncryptionType == Archive::Encrypted) {
addToArchiveJob->setPassword(QLatin1String("1234"));
}
QFETCH(QStringList, inputFiles);
foreach (const QString &file, inputFiles) {
addToArchiveJob->addInput(QUrl::fromUserInput(file));
}
// Run the job in the following event loop.
QEventLoop eventLoop(this);
connect(addToArchiveJob, &KJob::result, &eventLoop, &QEventLoop::quit);
addToArchiveJob->start();
eventLoop.exec(); // krazy:exclude=crashy
// Run the job.
TestHelper::startAndWaitForResult(addToArchiveJob);
// Check the properties of the generated test archive, then remove it.
QFETCH(QString, expectedArchiveName);
Archive *archive = Archive::create(QFINDTESTDATA(QStringLiteral("data/%1").arg(expectedArchiveName)));
auto loadJob = Archive::load(QFINDTESTDATA(QStringLiteral("data/%1").arg(expectedArchiveName)));
QVERIFY(loadJob);
TestHelper::startAndWaitForResult(loadJob);
auto archive = loadJob->archive();
QVERIFY(archive);
QVERIFY(archive->isValid());
QCOMPARE(archive->completeBaseName() + QLatin1Char('.') + expectedSuffix, expectedArchiveName);
QCOMPARE(archive->encryptionType(), expectedEncryptionType);
QFETCH(qulonglong, expectedNumberOfFiles);
QCOMPARE(archive->numberOfFiles(), expectedNumberOfFiles);
......@@ -231,21 +266,6 @@ void AddToArchiveTest::testCompressHere()
QCOMPARE(archive->numberOfFolders(), expectedNumberOfFolders);
QVERIFY(QFile(archive->fileName()).remove());
}
void AddToArchiveTest::testCreateEncryptedArchive()
{
Archive *archive = Archive::create(QStringLiteral("foo.zip"));
QVERIFY(archive);
if (!archive->isValid()) {
QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle);
}
QCOMPARE(archive->encryptionType(), Archive::Unencrypted);
archive->encrypt(QStringLiteral("1234"), false);
QCOMPARE(archive->encryptionType(), Archive::Encrypted);
archive->deleteLater();
}
......
......@@ -23,7 +23,7 @@
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "autotests/testhelper/testhelper.h"
#include "testhelper.h"
using namespace Kerfuffle;
......@@ -173,7 +173,12 @@ void CopyTest::testCopying()
QFETCH(QString, archiveName);
const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName;
Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath));
Archive *archive = Archive::create(archivePath, this);
auto loadJob = Archive::load(archivePath);
QVERIFY(loadJob);
TestHelper::startAndWaitForResult(loadJob);
auto archive = loadJob->archive();
QVERIFY(archive);
if (!archive->isValid()) {
......
......@@ -24,8 +24,7 @@
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "kerfuffle/archive_kerfuffle.h"
#include "kerfuffle/jobs.h"
#include "testhelper.h"
#include <QDirIterator>
#include <QStandardPaths>
......@@ -62,8 +61,8 @@ void ExtractTest::testProperties_data()
QTest::newRow("non-existent tar archive")
<< QStringLiteral("/tmp/foo.tar.gz")
<< QStringLiteral("foo")
<< false << false << false << false << 0 << Archive::Unencrypted
<< QString();
<< false << false << true << false << 0 << Archive::Unencrypted
<< QStringLiteral("foo");
// Test non-archive file
QTest::newRow("not an archive")
......@@ -195,7 +194,11 @@ void ExtractTest::testProperties_data()
void ExtractTest::testProperties()
{
QFETCH(QString, archivePath);
Archive *archive = Archive::create(archivePath, this);
auto loadJob = Archive::load(archivePath, this);
QVERIFY(loadJob);
TestHelper::startAndWaitForResult(loadJob);
auto archive = loadJob->archive();
QVERIFY(archive);
if (!archive->isValid()) {
......@@ -218,7 +221,7 @@ void ExtractTest::testProperties()
}
QFETCH(bool, isSingleFolder);
QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder);
QCOMPARE(archive->isSingleFolder(), isSingleFolder);
QFETCH(bool, isMultiVolume);
QCOMPARE(archive->isMultiVolume(), isMultiVolume);
......@@ -589,7 +592,12 @@ void ExtractTest::testExtraction_data()
void ExtractTest::testExtraction()
{
QFETCH(QString, archivePath);
Archive *archive = Archive::create(archivePath, this);
auto loadJob = Archive::load(archivePath, this);
QVERIFY(loadJob);
Archive *archive = Q_NULLPTR;
TestHelper::startAndWaitForResult(loadJob);
archive = loadJob->archive();
QVERIFY(archive);
if (!archive->isValid()) {
......@@ -605,10 +613,7 @@ void ExtractTest::testExtraction()
QFETCH(ExtractionOptions, extractionOptions);
auto extractionJob = archive->extractFiles(entriesToExtract, destDir.path(), extractionOptions);
QEventLoop eventLoop(this);
connect(extractionJob, &KJob::result, &eventLoop, &QEventLoop::quit);
extractionJob->start();
eventLoop.exec(); // krazy:exclude=crashy
TestHelper::startAndWaitForResult(extractionJob);
QFETCH(int, expectedExtractedEntriesCount);
int extractedEntriesCount = 0;
......
......@@ -47,8 +47,8 @@ protected Q_SLOTS:
private Q_SLOTS:
// ListJob-related tests
void testListJob_data();
void testListJob();
void testLoadJob_data();
void testLoadJob();
// ExtractJob-related tests
void testExtractJobAccessors();
......@@ -104,11 +104,11 @@ QList<Archive::Entry*> JobsTest::listEntries(JSONArchiveInterface *iface)
{
m_entries.clear();
ListJob *listJob = new ListJob(iface);
connect(listJob, &Job::newEntry,
auto job = new LoadJob(iface);
connect(job, &Job::newEntry,
this, &JobsTest::slotNewEntry);
startAndWaitForResult(listJob);
startAndWaitForResult(job);
return m_entries;
}
......@@ -120,7 +120,7 @@ void JobsTest::startAndWaitForResult(KJob *job)
m_eventLoop.exec();
}
void JobsTest::testListJob_data()
void JobsTest::testLoadJob_data()