summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Kandeler <christian.kandeler@digia.com>2013-11-14 11:14:26 +0100
committerChristian Kandeler <christian.kandeler@digia.com>2013-11-20 14:54:32 +0100
commit4bdfb786b4120165615cde287e917a802b32d31d (patch)
treedc74ed7b704971145e07b411f6309dcfccb8df18
parent9d0a5b9ac770ca69fcb198ceaf02a39f3c72f6a2 (diff)
downloadqbs-4bdfb786b4120165615cde287e917a802b32d31d.tar.gz
Add API for adding groups and files.
This is a necessary feature for proper IDE support. Change-Id: I296beb5beae514f70babf3da1b5544726e9df82e Reviewed-by: Joerg Bornemann <joerg.bornemann@digia.com>
-rw-r--r--src/lib/api/api.pri2
-rw-r--r--src/lib/api/project.cpp305
-rw-r--r--src/lib/api/project.h6
-rw-r--r--src/lib/api/projectfileupdater.cpp305
-rw-r--r--src/lib/api/projectfileupdater.h100
-rw-r--r--src/lib/language/language.h10
-rw-r--r--src/lib/language/projectresolver.cpp8
-rw-r--r--src/lib/language/projectresolver.h5
-rw-r--r--src/lib/lib.qbs2
-rw-r--r--src/lib/tools/fileinfo.h6
-rw-r--r--tests/auto/api/testdata/project-editing/existingfile1.txt0
-rw-r--r--tests/auto/api/testdata/project-editing/existingfile2.txt0
-rw-r--r--tests/auto/api/testdata/project-editing/existingfile3.txt0
-rw-r--r--tests/auto/api/testdata/project-editing/file.cpp0
-rw-r--r--tests/auto/api/testdata/project-editing/file.h0
-rw-r--r--tests/auto/api/testdata/project-editing/main.cpp0
-rw-r--r--tests/auto/api/testdata/project-editing/newfile1.txt0
-rw-r--r--tests/auto/api/testdata/project-editing/newfile2.txt0
-rw-r--r--tests/auto/api/testdata/project-editing/newfile3.txt0
-rw-r--r--tests/auto/api/testdata/project-editing/newfile4.txt0
-rw-r--r--tests/auto/api/testdata/project-editing/project.qbs21
-rw-r--r--tests/auto/api/tst_api.cpp192
-rw-r--r--tests/auto/api/tst_api.h14
23 files changed, 932 insertions, 44 deletions
diff --git a/src/lib/api/api.pri b/src/lib/api/api.pri
index 917a54bbe..8a0c93237 100644
--- a/src/lib/api/api.pri
+++ b/src/lib/api/api.pri
@@ -2,6 +2,7 @@ HEADERS += \
$$PWD/changeset.h \
$$PWD/internaljobs.h \
$$PWD/projectdata.h \
+ $$PWD/projectfileupdater.h \
$$PWD/runenvironment.h \
$$PWD/jobs.h \
$$PWD/project.h \
@@ -12,6 +13,7 @@ HEADERS += \
SOURCES += \
$$PWD/changeset.cpp \
$$PWD/internaljobs.cpp \
+ $$PWD/projectfileupdater.cpp \
$$PWD/runenvironment.cpp \
$$PWD/projectdata.cpp \
$$PWD/jobs.cpp \
diff --git a/src/lib/api/project.cpp b/src/lib/api/project.cpp
index 41d4db29c..39ed4dc27 100644
--- a/src/lib/api/project.cpp
+++ b/src/lib/api/project.cpp
@@ -32,13 +32,18 @@
#include "jobs.h"
#include "projectdata.h"
#include "projectdata_p.h"
+#include "projectfileupdater.h"
#include "propertymap_p.h"
#include "runenvironment.h"
#include <buildgraph/artifact.h>
+#include <buildgraph/buildgraph.h>
#include <buildgraph/productbuilddata.h>
#include <buildgraph/productinstaller.h>
+#include <buildgraph/projectbuilddata.h>
+#include <buildgraph/rulesevaluationcontext.h>
#include <buildgraph/timestampsupdater.h>
#include <language/language.h>
+#include <language/projectresolver.h>
#include <logging/logger.h>
#include <logging/translator.h>
#include <tools/cleanoptions.h>
@@ -108,6 +113,17 @@ public:
QList<ResolvedProductPtr> internalProducts(const QList<ProductData> &products) const;
QList<ResolvedProductPtr> allEnabledInternalProducts() const;
ResolvedProductPtr internalProduct(const ProductData &product) const;
+ ProductData findProductData(const QString &productName) const;
+ GroupData findGroupData(const ProductData &product, const QString &groupName) const;
+
+ GroupData createGroupDataFromGroup(const GroupPtr &resolvedGroup);
+
+ void addGroup(const ProductData &product, const QString &groupName);
+ void addFiles(const ProductData &product, const GroupData &group, const QStringList &filePaths);
+ void updateInternalCodeLocations(const ResolvedProjectPtr &project,
+ const CodeLocation &changeLocation, int lineOffset);
+ void updateExternalCodeLocations(const ProjectData &project,
+ const CodeLocation &changeLocation, int lineOffset);
const TopLevelProjectPtr internalProject;
Logger logger;
@@ -220,6 +236,235 @@ ResolvedProductPtr ProjectPrivate::internalProduct(const ProductData &product) c
return internalProductForProject(internalProject, product);
}
+ProductData ProjectPrivate::findProductData(const QString &productName) const
+{
+ foreach (const ProductData &p, m_projectData.allProducts()) {
+ if (p.name() == productName)
+ return p;
+ }
+ return ProductData();
+}
+
+GroupData ProjectPrivate::findGroupData(const ProductData &product, const QString &groupName) const
+{
+ foreach (const GroupData &g, product.groups()) {
+ if (g.name() == groupName)
+ return g;
+ }
+ return GroupData();
+}
+
+GroupData ProjectPrivate::createGroupDataFromGroup(const GroupPtr &resolvedGroup)
+{
+ GroupData group;
+ group.d->name = resolvedGroup->name;
+ group.d->location = resolvedGroup->location;
+ foreach (const SourceArtifactConstPtr &sa, resolvedGroup->files)
+ group.d->filePaths << sa->absoluteFilePath;
+ if (resolvedGroup->wildcards) {
+ foreach (const SourceArtifactConstPtr &sa, resolvedGroup->wildcards->files)
+ group.d->expandedWildcards << sa->absoluteFilePath;
+ }
+ qSort(group.d->filePaths);
+ qSort(group.d->expandedWildcards);
+ group.d->properties.d->m_map = resolvedGroup->properties;
+ group.d->isEnabled = resolvedGroup->enabled;
+ group.d->isValid = true;
+ return group;
+}
+
+void ProjectPrivate::addGroup(const ProductData &product, const QString &groupName)
+{
+ if (groupName.isEmpty())
+ throw ErrorInfo(Tr::tr("Group has an empty name."));
+ if (!product.isValid())
+ throw ErrorInfo(Tr::tr("Product is invalid."));
+ const ResolvedProductPtr resolvedProduct = internalProduct(product);
+ if (!resolvedProduct)
+ throw ErrorInfo(Tr::tr("Product '%1' does not exist.").arg(product.name()));
+
+ // Guard against calls with outdated product data.
+ const ProductData currentProduct= findProductData(product.name());
+ QBS_CHECK(currentProduct.isValid());
+
+ foreach (const GroupPtr &resolvedGroup, resolvedProduct->groups) {
+ if (resolvedGroup->name == groupName) {
+ throw ErrorInfo(Tr::tr("Group '%1' already exists in product '%2'.")
+ .arg(groupName, product.name()), resolvedGroup->location);
+ }
+ }
+
+ ProjectFileGroupInserter groupInserter(currentProduct, groupName);
+ groupInserter.apply();
+
+ m_projectData.d.detach(); // The data we already gave out must stay as it is.
+
+ updateInternalCodeLocations(internalProject, groupInserter.itemPosition(),
+ groupInserter.lineOffset());
+ updateExternalCodeLocations(m_projectData, groupInserter.itemPosition(),
+ groupInserter.lineOffset());
+
+ GroupPtr resolvedGroup = ResolvedGroup::create();
+ resolvedGroup->location = groupInserter.itemPosition();
+ resolvedGroup->enabled = true;
+ resolvedGroup->name = groupName;
+ resolvedGroup->properties = resolvedProduct->properties;
+ resolvedGroup->overrideTags = false;
+ resolvedProduct->groups << resolvedGroup;
+ foreach (const ProductData &newProduct, m_projectData.allProducts()) {
+ if (newProduct.name() == product.name()) {
+ newProduct.d->groups << createGroupDataFromGroup(resolvedGroup);
+ qSort(newProduct.d->groups);
+ break;
+ }
+ }
+}
+
+void ProjectPrivate::addFiles(const ProductData &product, const GroupData &group,
+ const QStringList &filePaths)
+{
+ if (!product.isValid())
+ throw ErrorInfo(Tr::tr("Product is invalid."));
+ const ResolvedProductPtr resolvedProduct = internalProduct(product);
+ if (!resolvedProduct)
+ throw ErrorInfo(Tr::tr("Product '%1' does not exist.").arg(product.name()));
+
+ const ProductData currentProduct = findProductData(product.name());
+ QBS_CHECK(currentProduct.isValid());
+
+ GroupPtr resolvedGroup;
+ GroupData currentGroup;
+ const QString groupName = group.isValid() ? group.name() : product.name();
+ foreach (const GroupPtr &g, resolvedProduct->groups) {
+ if (g->name == groupName) {
+ resolvedGroup = g;
+ break;
+ }
+ }
+ if (!resolvedGroup)
+ throw ErrorInfo(Tr::tr("Group '%1' does not exist.").arg(groupName));
+ currentGroup = findGroupData(currentProduct, groupName);
+ QBS_CHECK(currentGroup.isValid());
+
+ if (filePaths.isEmpty())
+ return; // "No-op". Could also be defined to be an error, I guess.
+
+ if (!resolvedGroup->prefix.isEmpty())
+ throw ErrorInfo(Tr::tr("Cannot add files to a group with a prefix."));
+ QStringList absoluteFilePaths;
+ QString projectFileDir = QFileInfo(product.location().fileName()).dir().absolutePath();
+ foreach (const QString &filePath, filePaths) {
+ const QString absPath = QDir::cleanPath(FileInfo::resolvePath(projectFileDir,
+ filePath));
+ if (absoluteFilePaths.contains(absPath))
+ throw ErrorInfo(Tr::tr("File '%1' appears more than once.").arg(absPath));
+ if (!FileInfo(absPath).exists())
+ throw ErrorInfo(Tr::tr("File '%1' does not exist.").arg(absPath));
+ absoluteFilePaths << absPath;
+ }
+
+ // We do not check for entries in other groups, because such doublettes might be legitimate
+ // due to conditions.
+ foreach (const QString &filePath, absoluteFilePaths) {
+ foreach (const SourceArtifactConstPtr &sa, resolvedGroup->files) {
+ if (sa->absoluteFilePath == filePath) {
+ throw ErrorInfo(Tr::tr("File '%1' already exists in group '%2'.")
+ .arg(filePath, group.name()));
+ }
+ }
+ }
+
+ ProjectFileFilesAdder adder(currentProduct, group.isValid() ? currentGroup : GroupData(),
+ absoluteFilePaths);
+ adder.apply();
+
+ m_projectData.d.detach();
+ updateInternalCodeLocations(internalProject, adder.itemPosition(), adder.lineOffset());
+ updateExternalCodeLocations(m_projectData, adder.itemPosition(), adder.lineOffset());
+
+ QList<SourceArtifactPtr> addedSourceArtifacts;
+ foreach (const QString &file, absoluteFilePaths) {
+ const SourceArtifactPtr artifact = SourceArtifact::create();
+ artifact->absoluteFilePath = file;
+ artifact->properties = resolvedGroup->properties;
+ artifact->fileTags = resolvedGroup->fileTags;
+ artifact->overrideFileTags = resolvedGroup->overrideTags;
+ ProjectResolver::applyFileTaggers(artifact, resolvedProduct, logger);
+ addedSourceArtifacts << artifact;
+ resolvedGroup->files << artifact;
+ }
+ if (resolvedProduct->enabled) {
+ ArtifactsPerFileTagMap artifactsPerFileTag;
+ foreach (const SourceArtifactConstPtr &sa, addedSourceArtifacts) {
+ Artifact * const artifact = createArtifact(resolvedProduct, sa, logger);
+ foreach (const FileTag &ft, artifact->fileTags)
+ artifactsPerFileTag[ft] += artifact;
+ }
+ RulesEvaluationContextPtr &evalContext
+ = resolvedProduct->topLevelProject()->buildData->evaluationContext;
+ evalContext.reset(new RulesEvaluationContext(logger));
+ RulesApplicator(resolvedProduct, artifactsPerFileTag, logger).applyAllRules();
+ addTargetArtifacts(resolvedProduct, artifactsPerFileTag, logger);
+ evalContext.clear();
+ }
+ doSanityChecks(internalProject, logger);
+ currentGroup.d->filePaths << absoluteFilePaths;
+ qSort(currentGroup.d->filePaths);
+}
+
+static void updateLocationIfNecessary(CodeLocation &location, const CodeLocation &changeLocation,
+ int lineOffset)
+{
+ if (location.fileName() == changeLocation.fileName()
+ && location.line() >= changeLocation.line()) {
+ location = CodeLocation(location.fileName(), location.line() + lineOffset,
+ location.column());
+ }
+}
+
+void ProjectPrivate::updateInternalCodeLocations(const ResolvedProjectPtr &project,
+ const CodeLocation &changeLocation, int lineOffset)
+{
+ updateLocationIfNecessary(project->location, changeLocation, lineOffset);
+ foreach (const ResolvedProjectPtr &subProject, project->subProjects)
+ updateInternalCodeLocations(subProject, changeLocation, lineOffset);
+ foreach (const ResolvedProductPtr &product, project->products) {
+ updateLocationIfNecessary(product->location, changeLocation, lineOffset);
+ foreach (const GroupPtr &group, product->groups)
+ updateLocationIfNecessary(group->location, changeLocation, lineOffset);
+ foreach (const RulePtr &rule, product->rules) {
+ updateLocationIfNecessary(rule->script->location, changeLocation, lineOffset);
+ foreach (const RuleArtifactPtr &artifact, rule->artifacts) {
+ for (int i = 0; i < artifact->bindings.count(); ++i) {
+ updateLocationIfNecessary(artifact->bindings[i].location, changeLocation,
+ lineOffset);
+ }
+ }
+ }
+ foreach (const ResolvedTransformerConstPtr &transformer, product->transformers)
+ updateLocationIfNecessary(transformer->transform->location, changeLocation, lineOffset);
+ foreach (const ResolvedModuleConstPtr &module, product->modules) {
+ updateLocationIfNecessary(module->setupBuildEnvironmentScript->location,
+ changeLocation, lineOffset);
+ updateLocationIfNecessary(module->setupRunEnvironmentScript->location,
+ changeLocation, lineOffset);
+ }
+ }
+}
+
+void ProjectPrivate::updateExternalCodeLocations(const ProjectData &project,
+ const CodeLocation &changeLocation, int lineOffset)
+{
+ updateLocationIfNecessary(project.d->location, changeLocation, lineOffset);
+ foreach (const ProjectData &subProject, project.subProjects())
+ updateExternalCodeLocations(subProject, changeLocation, lineOffset);
+ foreach (const ProductData &product, project.products()) {
+ updateLocationIfNecessary(product.d->location, changeLocation, lineOffset);
+ foreach (const GroupData &group, product.groups())
+ updateLocationIfNecessary(group.d->location, changeLocation, lineOffset);
+ }
+}
+
void ProjectPrivate::retrieveProjectData(ProjectData &projectData,
const ResolvedProjectConstPtr &internalProject)
{
@@ -231,23 +476,8 @@ void ProjectPrivate::retrieveProjectData(ProjectData &projectData,
product.d->name = resolvedProduct->name;
product.d->location = resolvedProduct->location;
product.d->isEnabled = resolvedProduct->enabled;
- foreach (const GroupPtr &resolvedGroup, resolvedProduct->groups) {
- GroupData group;
- group.d->name = resolvedGroup->name;
- group.d->location = resolvedGroup->location;
- foreach (const SourceArtifactConstPtr &sa, resolvedGroup->files)
- group.d->filePaths << sa->absoluteFilePath;
- if (resolvedGroup->wildcards) {
- foreach (const SourceArtifactConstPtr &sa, resolvedGroup->wildcards->files)
- group.d->expandedWildcards << sa->absoluteFilePath;
- }
- qSort(group.d->filePaths);
- qSort(group.d->expandedWildcards);
- group.d->properties.d->m_map = resolvedGroup->properties;
- group.d->isEnabled = resolvedGroup->enabled;
- group.d->isValid = true;
- product.d->groups << group;
- }
+ foreach (const GroupPtr &resolvedGroup, resolvedProduct->groups)
+ product.d->groups << createGroupDataFromGroup(resolvedGroup);
if (resolvedProduct->enabled) {
QBS_CHECK(resolvedProduct->buildData);
foreach (const Artifact * const a, resolvedProduct->buildData->targetArtifacts) {
@@ -577,4 +807,45 @@ QSet<QString> Project::buildSystemFiles() const
return d->internalProject->buildSystemFiles;
}
+/*!
+ * \brief Adds a new empty group to the given product.
+ * Returns an \c ErrorInfo object for which \c hasError() is false in case of a success
+ * and true otherwise. In the latter case, the object will have a sensible description.
+ * After calling this function, it is recommended to re-fetch the project data, as other
+ * items can be affected.
+ * \sa qbs::Project::projectData()
+ */
+ErrorInfo Project::addGroup(const ProductData &product, const QString &groupName)
+{
+ try {
+ d->addGroup(product, groupName);
+ return ErrorInfo();
+ } catch (ErrorInfo errorInfo) {
+ errorInfo.prepend(Tr::tr("Failure adding group to product."));
+ return errorInfo;
+ }
+}
+
+/*!
+ * \brief Adds the given files to the given product.
+ * If \c group is a default-constructed object, the files will be added to the product's
+ * "files" property, otherwise to the one of \c group.
+ * The file paths can be absolute or relative to the location of \c product. The project file
+ * will always contain relative paths.
+ * After calling this function, it is recommended to re-fetch the project data, as other
+ * items can be affected.
+ * \sa qbs::Project::projectData()
+ */
+ErrorInfo Project::addFiles(const ProductData &product, const GroupData &group,
+ const QStringList &filePaths)
+{
+ try {
+ d->addFiles(product, group, filePaths);
+ return ErrorInfo();
+ } catch (ErrorInfo errorInfo) {
+ errorInfo.prepend(Tr::tr("Failure adding files to product."));
+ return errorInfo;
+ }
+}
+
} // namespace qbs
diff --git a/src/lib/api/project.h b/src/lib/api/project.h
index 9e85bc417..11df00a08 100644
--- a/src/lib/api/project.h
+++ b/src/lib/api/project.h
@@ -49,6 +49,8 @@ class BuildJob;
class BuildOptions;
class CleanJob;
class CleanOptions;
+class ErrorInfo;
+class GroupData;
class ILogSink;
class InstallableFile;
class InstallJob;
@@ -120,6 +122,10 @@ public:
QSet<QString> buildSystemFiles() const;
+ ErrorInfo addGroup(const ProductData &product, const QString &groupName);
+ ErrorInfo addFiles(const ProductData &product, const GroupData &group,
+ const QStringList &filePaths);
+
private:
Project(const Internal::TopLevelProjectPtr &internalProject, const Internal::Logger &logger);
diff --git a/src/lib/api/projectfileupdater.cpp b/src/lib/api/projectfileupdater.cpp
new file mode 100644
index 000000000..d4c60abad
--- /dev/null
+++ b/src/lib/api/projectfileupdater.cpp
@@ -0,0 +1,305 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Build Suite.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+****************************************************************************/
+
+#include "projectfileupdater.h"
+
+#include "projectdata.h"
+#include "qmljsrewriter.h"
+
+#include <language/asttools.h>
+#include <logging/translator.h>
+#include <parser/qmljsast_p.h>
+#include <parser/qmljsastvisitor_p.h>
+#include <parser/qmljsengine_p.h>
+#include <parser/qmljslexer_p.h>
+#include <parser/qmljsparser_p.h>
+#include <tools/qbsassert.h>
+
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+
+using namespace QbsQmlJS;
+using namespace AST;
+
+namespace qbs {
+namespace Internal {
+
+class ItemFinder : public Visitor
+{
+public:
+ ItemFinder(const CodeLocation &cl) : m_cl(cl), m_item(0) { }
+
+ UiObjectDefinition *item() const { return m_item; }
+
+private:
+ bool visit(UiObjectDefinition *ast)
+ {
+ if (toCodeLocation(m_cl.fileName(), ast->firstSourceLocation()) == m_cl) {
+ m_item = ast;
+ return false;
+ }
+ return true;
+ }
+
+ const CodeLocation m_cl;
+ UiObjectDefinition *m_item;
+};
+
+class FilesBindingFinder : public Visitor
+{
+public:
+ FilesBindingFinder(const UiObjectDefinition *startItem)
+ : m_startItem(startItem), m_binding(0)
+ {
+ }
+
+ UiScriptBinding *binding() const { return m_binding; }
+
+private:
+ bool visit(UiObjectDefinition *ast)
+ {
+ // We start with the direct parent of the binding, so do not descend into any
+ // other item.
+ return ast == m_startItem;
+ }
+
+ bool visit(UiScriptBinding *ast)
+ {
+ if (ast->qualifiedId->name.toString() != QLatin1String("files"))
+ return true;
+ m_binding = ast;
+ return false;
+ }
+
+ const UiObjectDefinition * const m_startItem;
+ UiScriptBinding *m_binding;
+};
+
+
+ProjectFileUpdater::ProjectFileUpdater(const QString &projectFile) : m_projectFile(projectFile)
+{
+}
+
+void ProjectFileUpdater::apply()
+{
+ QFile file(m_projectFile);
+ if (!file.open(QFile::ReadOnly)) {
+ throw ErrorInfo(Tr::tr("File '%1' cannot be opened for reading: %2")
+ .arg(m_projectFile, file.errorString()));
+ }
+ QString content = QString::fromLocal8Bit(file.readAll());
+ file.close();
+ Engine engine;
+ Lexer lexer(&engine);
+ lexer.setCode(content, 1);
+ Parser parser(&engine);
+ if (!parser.parse()) {
+ QList<DiagnosticMessage> parserMessages = parser.diagnosticMessages();
+ if (!parserMessages.isEmpty()) {
+ ErrorInfo errorInfo;
+ errorInfo.append(Tr::tr("Failure parsing project file."));
+ foreach (const DiagnosticMessage &msg, parserMessages)
+ errorInfo.append(msg.message, toCodeLocation(file.fileName(), msg.loc));
+ throw errorInfo;
+ }
+ }
+
+ doApply(content, parser.ast());
+
+ if (!file.open(QFile::WriteOnly)) {
+ throw ErrorInfo(Tr::tr("File '%1' cannot be opened for writing: %2")
+ .arg(m_projectFile, file.errorString()));
+ }
+ file.resize(0);
+ file.write(content.toLocal8Bit());
+}
+
+
+ProjectFileGroupInserter::ProjectFileGroupInserter(const ProductData &product,
+ const QString &groupName)
+ : ProjectFileUpdater(product.location().fileName())
+ , m_product(product)
+ , m_groupName(groupName)
+{
+}
+
+void ProjectFileGroupInserter::doApply(QString &fileContent, UiProgram *ast)
+{
+ ItemFinder itemFinder(m_product.location());
+ ast->accept(&itemFinder);
+ if (!itemFinder.item()) {
+ throw ErrorInfo(Tr::tr("The project file parser failed to find the product item."),
+ CodeLocation(projectFile()));
+ }
+
+ ChangeSet changeSet;
+ Rewriter rewriter(fileContent, &changeSet, QStringList());
+ QString groupItemString;
+ const int productItemIndentation
+ = itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1;
+ const int groupItemIndentation = productItemIndentation + 4;
+ const QString groupItemIndentationString = QString(groupItemIndentation, QLatin1Char(' '));
+ groupItemString += groupItemIndentationString + QLatin1String("Group {\n");
+ groupItemString += groupItemIndentationString + groupItemIndentationString
+ + QLatin1String("name: \"") + m_groupName + QLatin1String("\"\n");
+ groupItemString += groupItemIndentationString + groupItemIndentationString
+ + QLatin1String("files: []\n");
+ groupItemString += groupItemIndentationString + QLatin1Char('}');
+ rewriter.addObject(itemFinder.item()->initializer, groupItemString);
+
+ int lineOffset = 3 + 1; // Our text + a leading newline that is always added by the rewriter.
+ const QList<ChangeSet::EditOp> &editOps = changeSet.operationList();
+ QBS_CHECK(editOps.count() == 1);
+ const ChangeSet::EditOp &insertOp = editOps.first();
+ setLineOffset(lineOffset);
+
+ int insertionLine = fileContent.left(insertOp.pos1).count(QLatin1Char('\n'));
+ for (int i = 0; i < insertOp.text.count() && insertOp.text.at(i) == QLatin1Char('\n'); ++i)
+ ++insertionLine; // To account for newlines prepended by the rewriter.
+ ++insertionLine; // To account for zero-based indexing.
+ setItemPosition(CodeLocation(projectFile(), insertionLine,
+ groupItemIndentation + 1));
+ changeSet.apply(&fileContent);
+}
+
+ProjectFileFilesAdder::ProjectFileFilesAdder(const ProductData &product, const GroupData &group,
+ const QStringList &files)
+ : ProjectFileUpdater(product.location().fileName())
+ , m_product(product)
+ , m_group(group)
+ , m_files(files)
+{
+}
+
+void ProjectFileFilesAdder::doApply(QString &fileContent, UiProgram *ast)
+{
+ // Find the item containing the "files" binding.
+ ItemFinder itemFinder(m_group.isValid() ? m_group.location() : m_product.location());
+ ast->accept(&itemFinder);
+ if (!itemFinder.item()) {
+ throw ErrorInfo(Tr::tr("The project file parser failed to find the item."),
+ CodeLocation(projectFile()));
+ }
+
+ const int itemIndentation
+ = itemFinder.item()->qualifiedTypeNameId->firstSourceLocation().startColumn - 1;
+ const int bindingIndentation = itemIndentation + 4;
+ const int arrayElemIndentation = bindingIndentation + 4;
+ QString newFilesString;
+ const QDir projectFileDir = QFileInfo(projectFile()).dir();
+ foreach (const QString &absFilePath, m_files) {
+ newFilesString += QString(arrayElemIndentation, QLatin1Char(' '));
+ newFilesString += QLatin1Char('"');
+ newFilesString += projectFileDir.relativeFilePath(absFilePath);
+ newFilesString += QLatin1Char('"');
+ newFilesString += QLatin1String(",\n");
+ }
+ newFilesString.chop(2); // Trailing comma and newline.
+
+ // Now get the binding itself.
+ FilesBindingFinder bindingFinder(itemFinder.item());
+ itemFinder.item()->accept(&bindingFinder);
+
+ ChangeSet changeSet;
+ Rewriter rewriter(fileContent, &changeSet, QStringList());
+
+ UiScriptBinding * const filesBinding = bindingFinder.binding();
+ if (filesBinding) {
+ if (filesBinding->statement->kind != Node::Kind_ExpressionStatement)
+ throw ErrorInfo(Tr::tr("JavaScript construct in source file is too complex.")); // TODO: rename, add new and concat.
+ const ExpressionStatement * const exprStatement
+ = static_cast<ExpressionStatement *>(filesBinding->statement);
+ switch (exprStatement->expression->kind) {
+ case Node::Kind_ArrayLiteral: {
+ QString filesString = QLatin1String("[\n");
+ const ElementList *elem
+ = static_cast<ArrayLiteral *>(exprStatement->expression)->elements;
+ while (elem) {
+ const quint32 start = elem->expression->firstSourceLocation().offset;
+ const quint32 end = elem->expression->lastSourceLocation().end();
+ const QString elemRepr = fileContent.mid(start, end - start);
+ filesString += QString(arrayElemIndentation, QLatin1Char(' '));
+ filesString += elemRepr;
+ filesString += QLatin1String(",\n");
+ elem = elem->next;
+ }
+ filesString += newFilesString;
+ filesString += QLatin1Char('\n');
+ filesString += QString(bindingIndentation, QLatin1Char(' '));
+ filesString += QLatin1Char(']');
+ rewriter.changeBinding(itemFinder.item()->initializer, QLatin1String("files"),
+ filesString, Rewriter::ScriptBinding);
+ break;
+ }
+ case Node::Kind_StringLiteral: {
+ const QString existingElement
+ = static_cast<StringLiteral *>(exprStatement->expression)->value.toString();
+ QString filesString = QLatin1String("[\n");
+ filesString += QString(arrayElemIndentation, QLatin1Char(' '));
+ filesString += QLatin1Char('"') + existingElement + QLatin1Char('"');
+ filesString += QLatin1String(",\n");
+ filesString += newFilesString;
+ filesString += QLatin1Char('\n');
+ filesString += QString(bindingIndentation, QLatin1Char(' '));
+ filesString += QLatin1Char(']');
+ rewriter.changeBinding(itemFinder.item()->initializer, QLatin1String("files"),
+ filesString, Rewriter::ScriptBinding);
+ break;
+ }
+ default:
+ qDebug("unsupported kind: %d", exprStatement->expression->kind);
+ // TODO: If it's an identifier, call "concat()".
+ throw ErrorInfo(Tr::tr("Javascript construct in source file is too complex."));
+ }
+ } else { // Can happen for the product itself, for which the "files" binding is not mandatory.
+ newFilesString.prepend(QLatin1String("[\n"));
+ newFilesString += QLatin1Char('\n');
+ newFilesString += QString(bindingIndentation, QLatin1Char(' '));
+ newFilesString += QLatin1Char(']');
+ const QString bindingString = QString(bindingIndentation, QLatin1Char(' '))
+ + QLatin1String("files");
+ rewriter.addBinding(itemFinder.item()->initializer, bindingString, newFilesString,
+ Rewriter::ScriptBinding);
+ }
+
+ const QList<ChangeSet::EditOp> &editOps = changeSet.operationList();
+ QBS_CHECK(editOps.count() == 1);
+ const ChangeSet::EditOp &insertOp = editOps.first();
+ setLineOffset(insertOp.text.count(QLatin1Char('\n')));
+
+ const int insertionLine = fileContent.left(insertOp.pos1 + 1).count(QLatin1Char('\n')) + 2;
+ const int insertionColumn = (filesBinding ? arrayElemIndentation : bindingIndentation) + 1;
+ setItemPosition(CodeLocation(projectFile(), insertionLine, insertionColumn));
+ changeSet.apply(&fileContent);
+}
+
+
+} // namespace Internal
+} // namespace qbs
diff --git a/src/lib/api/projectfileupdater.h b/src/lib/api/projectfileupdater.h
new file mode 100644
index 000000000..a7ce351a5
--- /dev/null
+++ b/src/lib/api/projectfileupdater.h
@@ -0,0 +1,100 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/legal
+**
+** This file is part of the Qt Build Suite.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+****************************************************************************/
+#ifndef QBS_PROJECTFILEUPDATER_H
+#define QBS_PROJECTFILEUPDATER_H
+
+#include "projectdata.h"
+
+#include <tools/error.h>
+#include <tools/codelocation.h>
+
+#include <QStringList>
+
+namespace QbsQmlJS { namespace AST { class UiProgram; } }
+
+namespace qbs {
+namespace Internal {
+
+class ProjectFileUpdater
+{
+public:
+ void apply();
+
+ CodeLocation itemPosition() const { return m_itemPosition; }
+ int lineOffset() const { return m_lineOffset; }
+
+protected:
+ ProjectFileUpdater(const QString &projectFile);
+
+ QString projectFile() const { return m_projectFile; }
+
+ void setLineOffset(int offset) { m_lineOffset = offset; }
+ void setItemPosition(const CodeLocation &cl) { m_itemPosition = cl; }
+
+private:
+ virtual void doApply(QString &fileContent, QbsQmlJS::AST::UiProgram *ast) = 0;
+
+ const QString m_projectFile;
+ CodeLocation m_itemPosition;
+ int m_lineOffset;
+};
+
+
+class ProjectFileGroupInserter : public ProjectFileUpdater
+{
+public:
+ ProjectFileGroupInserter(const ProductData &product, const QString &groupName);
+
+private:
+ void doApply(QString &fileContent, QbsQmlJS::AST::UiProgram *ast);
+
+ const ProductData m_product;
+ const QString m_groupName;
+};
+
+
+class ProjectFileFilesAdder : public ProjectFileUpdater
+{
+public:
+ ProjectFileFilesAdder(const ProductData &product, const GroupData &group,
+ const QStringList &files);
+
+private:
+ void doApply(QString &fileContent, QbsQmlJS::AST::UiProgram *ast);
+
+ const ProductData m_product;
+ const GroupData m_group;
+ const QStringList m_files;
+};
+
+} // namespace Internal
+} // namespace qbs
+
+
+#endif // Include guard.
diff --git a/src/lib/language/language.h b/src/lib/language/language.h
index 334400035..f828b7b33 100644
--- a/src/lib/language/language.h
+++ b/src/lib/language/language.h
@@ -252,8 +252,8 @@ public:
QString name;
QStringList moduleDependencies;
- ScriptFunctionConstPtr setupBuildEnvironmentScript;
- ScriptFunctionConstPtr setupRunEnvironmentScript;
+ ScriptFunctionPtr setupBuildEnvironmentScript;
+ ScriptFunctionPtr setupRunEnvironmentScript;
private:
ResolvedModule() {}
@@ -279,13 +279,13 @@ public:
static RulePtr create() { return RulePtr(new Rule); }
ResolvedModuleConstPtr module;
- ScriptFunctionConstPtr script;
+ ScriptFunctionPtr script;
FileTags inputs;
FileTags auxiliaryInputs;
FileTags usings;
FileTags explicitlyDependsOn;
bool multiplex;
- QList<RuleArtifactConstPtr> artifacts;
+ QList<RuleArtifactPtr> artifacts;
// members that we don't need to save
int ruleGraphId;
@@ -314,7 +314,7 @@ public:
ResolvedModuleConstPtr module;
QStringList inputs;
QList<SourceArtifactPtr> outputs;
- ScriptFunctionConstPtr transform;
+ ScriptFunctionPtr transform;
FileTags explicitlyDependsOn;
private:
diff --git a/src/lib/language/projectresolver.cpp b/src/lib/language/projectresolver.cpp
index 65a874d70..7de84608c 100644
--- a/src/lib/language/projectresolver.cpp
+++ b/src/lib/language/projectresolver.cpp
@@ -831,11 +831,11 @@ void ProjectResolver::postProcess(const ResolvedProductPtr &product,
void ProjectResolver::applyFileTaggers(const ResolvedProductPtr &product) const
{
foreach (const SourceArtifactPtr &artifact, product->allEnabledFiles())
- applyFileTaggers(artifact, product);
+ applyFileTaggers(artifact, product, m_logger);
}
void ProjectResolver::applyFileTaggers(const SourceArtifactPtr &artifact,
- const ResolvedProductConstPtr &product) const
+ const ResolvedProductConstPtr &product, const Logger &logger)
{
if (!artifact->overrideFileTags || artifact->fileTags.isEmpty()) {
const QString fileName = FileInfo::fileName(artifact->absoluteFilePath);
@@ -843,8 +843,8 @@ void ProjectResolver::applyFileTaggers(const SourceArtifactPtr &artifact,
artifact->fileTags.unite(fileTags);
if (artifact->fileTags.isEmpty())
artifact->fileTags.insert(unknownFileTag());
- if (m_logger.traceEnabled())
- m_logger.qbsTrace() << "[PR] adding file tags " << artifact->fileTags
+ if (logger.traceEnabled())
+ logger.qbsTrace() << "[PR] adding file tags " << artifact->fileTags
<< " to " << fileName;
}
}
diff --git a/src/lib/language/projectresolver.h b/src/lib/language/projectresolver.h
index 645aeba77..b23f19341 100644
--- a/src/lib/language/projectresolver.h
+++ b/src/lib/language/projectresolver.h
@@ -61,6 +61,9 @@ public:
TopLevelProjectPtr resolve(ModuleLoaderResult &loadResult,
const SetupProjectParameters &setupParameters);
+ static void applyFileTaggers(const SourceArtifactPtr &artifact,
+ const ResolvedProductConstPtr &product, const Logger &logger);
+
private:
struct ProjectContext
{
@@ -105,8 +108,6 @@ private:
void resolveProductDependencies(ProjectContext *projectContext);
void postProcess(const ResolvedProductPtr &product, ProjectContext *projectContext) const;
void applyFileTaggers(const ResolvedProductPtr &product) const;
- void applyFileTaggers(const SourceArtifactPtr &artifact,
- const ResolvedProductConstPtr &product) const;
QVariantMap evaluateModuleValues(Item *item) const;
void evaluateModuleValues(Item *item, QVariantMap *modulesMap) const;
QVariantMap evaluateProperties(Item *item) const;
diff --git a/src/lib/lib.qbs b/src/lib/lib.qbs
index c45997744..f5d08380d 100644
--- a/src/lib/lib.qbs
+++ b/src/lib/lib.qbs
@@ -49,6 +49,8 @@ Product {
"project.cpp",
"projectdata.cpp",
"projectdata_p.h",
+ "projectfileupdater.cpp",
+ "projectfileupdater.h",
"qmljsrewriter.cpp",
"qmljsrewriter.h",
"propertymap_p.h",
diff --git a/src/lib/tools/fileinfo.h b/src/lib/tools/fileinfo.h
index f7de2d2a0..1f988c867 100644
--- a/src/lib/tools/fileinfo.h
+++ b/src/lib/tools/fileinfo.h
@@ -83,11 +83,11 @@ private:
};
bool removeFileRecursion(const QFileInfo &f, QString *errorMessage);
-bool copyFileRecursion(const QString &sourcePath, const QString &targetPath, bool preserveSymLinks,
- QString *errorMessage);
-// FIXME: Used by tst_blackbox
+// FIXME: Used by tests.
bool QBS_EXPORT removeDirectoryWithContents(const QString &path, QString *errorMessage);
+bool QBS_EXPORT copyFileRecursion(const QString &sourcePath, const QString &targetPath,
+ bool preserveSymLinks, QString *errorMessage);
} // namespace Internal
} // namespace qbs
diff --git a/tests/auto/api/testdata/project-editing/existingfile1.txt b/tests/auto/api/testdata/project-editing/existingfile1.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/existingfile1.txt
diff --git a/tests/auto/api/testdata/project-editing/existingfile2.txt b/tests/auto/api/testdata/project-editing/existingfile2.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/existingfile2.txt
diff --git a/tests/auto/api/testdata/project-editing/existingfile3.txt b/tests/auto/api/testdata/project-editing/existingfile3.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/existingfile3.txt
diff --git a/tests/auto/api/testdata/project-editing/file.cpp b/tests/auto/api/testdata/project-editing/file.cpp
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/file.cpp
diff --git a/tests/auto/api/testdata/project-editing/file.h b/tests/auto/api/testdata/project-editing/file.h
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/file.h
diff --git a/tests/auto/api/testdata/project-editing/main.cpp b/tests/auto/api/testdata/project-editing/main.cpp
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/main.cpp
diff --git a/tests/auto/api/testdata/project-editing/newfile1.txt b/tests/auto/api/testdata/project-editing/newfile1.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/newfile1.txt
diff --git a/tests/auto/api/testdata/project-editing/newfile2.txt b/tests/auto/api/testdata/project-editing/newfile2.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/newfile2.txt
diff --git a/tests/auto/api/testdata/project-editing/newfile3.txt b/tests/auto/api/testdata/project-editing/newfile3.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/newfile3.txt
diff --git a/tests/auto/api/testdata/project-editing/newfile4.txt b/tests/auto/api/testdata/project-editing/newfile4.txt
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/newfile4.txt
diff --git a/tests/auto/api/testdata/project-editing/project.qbs b/tests/auto/api/testdata/project-editing/project.qbs
new file mode 100644
index 000000000..1d7704fe6
--- /dev/null
+++ b/tests/auto/api/testdata/project-editing/project.qbs
@@ -0,0 +1,21 @@
+import qbs
+
+CppApplication {
+ Group {
+ name: "Existing Group 1"
+ files: ["existingfile1.txt"]
+ }
+ property string aFile: "existingfile2.txt"
+ Group {
+ name: "Existing Group 2"
+ files: product.aFile
+ }
+ Group {
+ name: "Existing Group 3"
+ files: {
+ var file = "existingfile3.txt";
+ return file;
+ }
+ }
+ files: "main.cpp"
+}
diff --git a/tests/auto/api/tst_api.cpp b/tests/auto/api/tst_api.cpp
index a60d4795e..1e1e883d3 100644
--- a/tests/auto/api/tst_api.cpp
+++ b/tests/auto/api/tst_api.cpp
@@ -35,7 +35,9 @@
#include <api/project.h>
#include <api/projectdata.h>
#include <logging/ilogsink.h>
+#include <tools/fileinfo.h>
#include <tools/hostosinfo.h>
+#include <tools/buildoptions.h>
#include <tools/installoptions.h>
#include <tools/preferences.h>
#include <tools/setupprojectparameters.h>
@@ -44,6 +46,7 @@
#include <QDir>
#include <QEventLoop>
#include <QScopedPointer>
+#include <QStringList>
#include <QTest>
class LogSink: public qbs::ILogSink
@@ -54,7 +57,22 @@ class LogSink: public qbs::ILogSink
void doPrintMessage(qbs::LoggerLevel, const QString &, const QString &) { }
};
-TestApi::TestApi() : m_logSink(new LogSink)
+class BuildDescriptionReveiver : public QObject
+{
+ Q_OBJECT
+public:
+ QString descriptions;
+
+private slots:
+ void handleDescription(const QString &, const QString &description) {
+ descriptions += description;
+ }
+};
+
+TestApi::TestApi()
+ : m_logSink(new LogSink)
+ , m_sourceDataDir(QDir::cleanPath(SRCDIR "/testdata"))
+ , m_workingDataDir(QCoreApplication::applicationDirPath() + "/../tests/auto/api/testWorkDir")
{
}
@@ -63,6 +81,14 @@ TestApi::~TestApi()
delete m_logSink;
}
+void TestApi::initTestCase()
+{
+ QString errorMessage;
+ qbs::Internal::removeDirectoryWithContents(m_workingDataDir, &errorMessage);
+ QVERIFY2(qbs::Internal::copyFileRecursion(m_sourceDataDir,
+ m_workingDataDir, false, &errorMessage), qPrintable(errorMessage));
+}
+
static void waitForFinished(qbs::AbstractJob *job)
{
QEventLoop loop;
@@ -70,11 +96,160 @@ static void waitForFinished(qbs::AbstractJob *job)
loop.exec();
}
+
+void printProjectData(const qbs::ProjectData &project)
+{
+ foreach (const qbs::ProductData &p, project.products()) {
+ qDebug(" Product '%s' at %s", qPrintable(p.name()), qPrintable(p.location().toString()));
+ foreach (const qbs::GroupData &g, p.groups()) {
+ qDebug(" Group '%s' at %s", qPrintable(g.name()), qPrintable(g.location().toString()));
+ qDebug(" Files: %s", qPrintable(g.filePaths().join(QLatin1String(", "))));
+ }
+ }
+}
+
+qbs::GroupData findGroup(const qbs::ProductData &product, const QString &name)
+{
+ foreach (const qbs::GroupData &g, product.groups()) {
+ if (g.name() == name)
+ return g;
+ }
+ return qbs::GroupData();
+}
+
+void TestApi::changeContent()
+{
+ qbs::SetupProjectParameters setupParams = defaultSetupParameters();
+ setupParams.setProjectFilePath(QDir::cleanPath(m_workingDataDir +
+ "/project-editing/project.qbs"));
+ QScopedPointer<qbs::SetupProjectJob> job(qbs::Project::setupProject(setupParams,
+ m_logSink, 0));
+ waitForFinished(job.data());
+ QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString()));
+ qbs::Project project = job->project();
+ qbs::ProjectData projectData = project.projectData();
+ QCOMPARE(projectData.allProducts().count(), 1);
+ qbs::ProductData product = projectData.allProducts().first();
+ QCOMPARE(product.groups().count(), 4);
+
+ // Error handling: Invalid product.
+ qbs::ErrorInfo errorInfo = project.addGroup(qbs::ProductData(), "blubb");
+ QVERIFY(errorInfo.hasError());
+ QVERIFY(errorInfo.toString().contains("invalid"));
+
+ // Error handling: Empty group name.
+ errorInfo = project.addGroup(product, QString());
+ QVERIFY(errorInfo.hasError());
+ QVERIFY(errorInfo.toString().contains("empty"));
+
+ errorInfo = project.addGroup(product, "New Group 1");
+ QVERIFY2(!errorInfo.hasError(), qPrintable(errorInfo.toString()));
+
+ errorInfo = project.addGroup(product, "New Group 2");
+ QVERIFY2(!errorInfo.hasError(), qPrintable(errorInfo.toString()));
+
+ // Error handling: Group already inserted.
+ errorInfo = project.addGroup(product, "New Group 1");
+ QVERIFY(errorInfo.hasError());
+ QVERIFY(errorInfo.toString().contains("already"));
+
+ // Error handling: Add list of files with double entries.
+ errorInfo = project.addFiles(product, qbs::GroupData(), QStringList() << "file.cpp"
+ << "file.cpp");
+ QVERIFY(errorInfo.hasError());
+ QVERIFY2(errorInfo.toString().contains("more than once"), qPrintable(errorInfo.toString()));
+
+ // Add files to empty array literal.
+ projectData = project.projectData();
+ QVERIFY(projectData.products().count() == 1);
+ product = projectData.products().first();
+ QCOMPARE(product.groups().count(), 6);
+ qbs::GroupData group = findGroup(product, "New Group 1");
+ QVERIFY(group.isValid());
+ errorInfo = project.addFiles(product, group, QStringList() << "file.h" << "file.cpp");
+ QVERIFY2(!errorInfo.hasError(), qPrintable(errorInfo.toString()));
+
+ // Error handling: Add the same file again.
+ projectData = project.projectData();
+ QVERIFY(projectData.products().count() == 1);
+ product = projectData.products().first();
+ QCOMPARE(product.groups().count(), 6);
+ group = findGroup(product, "New Group 1");
+ QVERIFY(group.isValid());
+ errorInfo = project.addFiles(product, group, QStringList() << "file.cpp");
+ QVERIFY(errorInfo.hasError());
+ QVERIFY2(errorInfo.toString().contains("already"), qPrintable(errorInfo.toString()));
+
+ // Add file to non-empty array literal.
+ projectData = project.projectData();
+ QVERIFY(projectData.products().count() == 1);
+ product = projectData.products().first();
+ group = findGroup(product, "Existing Group 1");
+ QVERIFY(group.isValid());
+ errorInfo = project.addFiles(product, group, QStringList() << "newfile1.txt");
+ QVERIFY2(!errorInfo.hasError(), qPrintable(errorInfo.toString()));
+
+ // Add files to list represented as a single string.
+ projectData = project.projectData();
+ QVERIFY(projectData.products().count() == 1);
+ product = projectData.products().first();
+ errorInfo = project.addFiles(product, qbs::GroupData(), QStringList() << "newfile2.txt");
+ QVERIFY2(!errorInfo.hasError(), qPrintable(errorInfo.toString()));
+
+ // Add files to list represented as an identifier (not yet implmented).
+ projectData = project.projectData();
+ QVERIFY(projectData.products().count() == 1);
+ product = projectData.products().first();
+ group = findGroup(product, "Existing Group 2");
+ QVERIFY(group.isValid());
+ errorInfo = project.addFiles(product, group, QStringList() << "newfile3.txt");
+ QVERIFY(errorInfo.hasError());
+ QVERIFY2(errorInfo.toString().contains("complex"), qPrintable(errorInfo.toString()));
+
+ // Add files to list represented as a block of code (not yet implemented).
+ projectData = project.projectData();
+ QVERIFY(projectData.products().count() == 1);
+ product = projectData.products().first();
+ group = findGroup(product, "Existing Group 3");
+ QVERIFY(group.isValid());
+ errorInfo = project.addFiles(product, group, QStringList() << "newfile4.txt");
+ QVERIFY(errorInfo.hasError());
+ QVERIFY2(errorInfo.toString().contains("complex"), qPrintable(errorInfo.toString()));
+
+ // Check whether building will take the newly added cpp file into account.
+ // This must not be moved below the re-resolving test!!!
+ qbs::BuildOptions buildOptions;
+ buildOptions.setDryRun(true);
+ m_logSink->setLogLevel(qbs::LoggerMaxLevel);
+ BuildDescriptionReveiver rcvr;
+ const QScopedPointer<qbs::BuildJob> buildJob(project.buildAllProducts(buildOptions, this));
+ connect(buildJob.data(), SIGNAL(reportCommandDescription(QString, QString)), &rcvr,
+ SLOT(handleDescription(QString,QString)));
+ waitForFinished(buildJob.data());
+ QVERIFY2(!buildJob->error().hasError(), qPrintable(buildJob->error().toString()));
+ QVERIFY(rcvr.descriptions.contains("compiling file.cpp"));
+
+ // Now check whether the data updates were done correctly.
+ projectData = project.projectData();
+ job.reset(qbs::Project::setupProject(setupParams, m_logSink, 0));
+ waitForFinished(job.data());
+ QVERIFY2(!job->error().hasError(), qPrintable(job->error().toString()));
+ const qbs::ProjectData newProjectData = job->project().projectData();
+ const bool projectDataMatches = newProjectData == projectData;
+ if (!projectDataMatches) {
+ qDebug("This is the assumed project:");
+ printProjectData(projectData);
+ qDebug("This is the actual project:");
+ printProjectData(newProjectData);
+ }
+ QVERIFY(projectDataMatches); // Will fail if e.g. code locations don't match.
+}
+
void TestApi::disabledInstallGroup()
{
qbs::SetupProjectParameters setupParams = defaultSetupParameters();
- setupParams.setProjectFilePath(QDir::cleanPath(QLatin1String(SRCDIR "/testdata"
- "/disabled_install_group/project.qbs")));
+ setupParams.setProjectFilePath(QDir::cleanPath(m_workingDataDir +
+ "/disabled_install_group/project.qbs"));
QScopedPointer<qbs::SetupProjectJob> job(qbs::Project::setupProject(setupParams,
m_logSink, 0));
waitForFinished(job.data());
@@ -95,8 +270,8 @@ void TestApi::disabledInstallGroup()
void TestApi::fileTagsFilterOverride()
{
qbs::SetupProjectParameters setupParams = defaultSetupParameters();
- setupParams.setProjectFilePath(QDir::cleanPath(QLatin1String(SRCDIR "/testdata"
- "/filetagsfilter_override/project.qbs")));
+ setupParams.setProjectFilePath(QDir::cleanPath(m_workingDataDir +
+ "/filetagsfilter_override/project.qbs"));
QScopedPointer<qbs::SetupProjectJob> job(qbs::Project::setupProject(setupParams,
m_logSink, 0));
waitForFinished(job.data());
@@ -176,7 +351,7 @@ void TestApi::nonexistingProjectPropertyFromProduct()
{
qbs::SetupProjectParameters setupParams = defaultSetupParameters();
const QString projectDir
- = QDir::cleanPath(QLatin1String(SRCDIR "/testdata/nonexistingprojectproperties"));
+ = QDir::cleanPath(m_workingDataDir + "/nonexistingprojectproperties");
const QString topLevelProjectFile = projectDir + QLatin1String("/invalidaccessfromproduct.qbs");
setupParams.setProjectFilePath(topLevelProjectFile);
QScopedPointer<qbs::SetupProjectJob> job(qbs::Project::setupProject(setupParams,
@@ -191,8 +366,7 @@ void TestApi::nonexistingProjectPropertyFromProduct()
void TestApi::nonexistingProjectPropertyFromCommandLine()
{
qbs::SetupProjectParameters setupParams = defaultSetupParameters();
- const QString projectDir
- = QDir::cleanPath(QLatin1String(SRCDIR "/testdata/nonexistingprojectproperties"));
+ const QString projectDir = QDir::cleanPath(m_workingDataDir + "/nonexistingprojectproperties");
const QString topLevelProjectFile = projectDir + QLatin1String("/project.qbs");
setupParams.setProjectFilePath(topLevelProjectFile);
QVariantMap projectProperties;
@@ -227,3 +401,5 @@ qbs::SetupProjectParameters TestApi::defaultSetupParameters() const
}
QTEST_MAIN(TestApi)
+
+#include "tst_api.moc"
diff --git a/tests/auto/api/tst_api.h b/tests/auto/api/tst_api.h
index 707ccf85e..ffeff6922 100644
--- a/tests/auto/api/tst_api.h
+++ b/tests/auto/api/tst_api.h
@@ -32,10 +32,9 @@
#include <QObject>
-namespace qbs {
-class ILogSink;
-class SetupProjectParameters;
-}
+namespace qbs { class SetupProjectParameters; }
+
+class LogSink;
class TestApi : public QObject
{
@@ -46,6 +45,9 @@ public:
~TestApi();
private slots:
+ void initTestCase();
+
+ void changeContent();
void disabledInstallGroup();
void fileTagsFilterOverride();
void installableFiles();
@@ -56,7 +58,9 @@ private slots:
private:
qbs::SetupProjectParameters defaultSetupParameters() const;
- qbs::ILogSink * const m_logSink;
+ LogSink * const m_logSink;
+ const QString m_sourceDataDir;
+ const QString m_workingDataDir;
};
#endif // Include guard.