diff --git a/src/plugins/cppeditor/cppeditor.pro b/src/plugins/cppeditor/cppeditor.pro index 1fb2568caad..5e8d82d2783 100644 --- a/src/plugins/cppeditor/cppeditor.pro +++ b/src/plugins/cppeditor/cppeditor.pro @@ -47,7 +47,8 @@ OTHER_FILES += CppEditor.mimetypes.xml equals(TEST, 1) { SOURCES += \ cppquickfix_test.cpp \ - cppdoxygen_test.cpp + cppdoxygen_test.cpp \ + fileandtokenactions_test.cpp DEFINES += SRCDIR=\\\"$$PWD\\\" } diff --git a/src/plugins/cppeditor/cppeditor.qbs b/src/plugins/cppeditor/cppeditor.qbs index 24838774146..5e12d69d607 100644 --- a/src/plugins/cppeditor/cppeditor.qbs +++ b/src/plugins/cppeditor/cppeditor.qbs @@ -65,7 +65,8 @@ QtcPlugin { condition: Defaults.testsEnabled(qbs) files: [ "cppquickfix_test.cpp", - "cppdoxygen_test.cpp" + "cppdoxygen_test.cpp", + "fileandtokenactions_test.cpp" ] cpp.defines: outer.concat(['SRCDIR="' + FileInfo.path(filePath) + '"']) diff --git a/src/plugins/cppeditor/cppplugin.h b/src/plugins/cppeditor/cppplugin.h index 705ca705a92..934371514db 100644 --- a/src/plugins/cppeditor/cppplugin.h +++ b/src/plugins/cppeditor/cppplugin.h @@ -86,13 +86,13 @@ public slots: void setSortedOutline(bool sorted); private slots: - void onTaskStarted(const QString &type); void onAllTasksFinished(const QString &type); void currentEditorChanged(Core::IEditor *editor); #ifdef WITH_TESTS private slots: + // The following tests expect that no projects are loaded on start-up. void test_doxygen_comments_qt_style(); void test_doxygen_comments_qt_style_continuation(); void test_doxygen_comments_java_style(); @@ -130,6 +130,19 @@ private slots: void test_quickfix_AddIncludeForUndefinedIdentifier_noinclude(); void test_quickfix_AddIncludeForUndefinedIdentifier_noincludeComment01(); void test_quickfix_AddIncludeForUndefinedIdentifier_noincludeComment02(); + + // The following tests depend on the projects that are loaded on startup + // and will be skipped in case no projects are loaded. + void test_openEachFile(); + void test_switchHeaderSourceOnEachFile(); + void test_moveTokenWiseThroughEveryFile(); + void test_moveTokenWiseThroughEveryFileAndFollowSymbol(); + void test_moveTokenWiseThroughEveryFileAndSwitchDeclarationDefinition(); + void test_moveTokenWiseThroughEveryFileAndFindUsages(); + void test_moveTokenWiseThroughEveryFileAndRenameUsages(); + void test_moveTokenWiseThroughEveryFileAndOpenTypeHierarchy(); + void test_moveTokenWiseThroughEveryFileAndInvokeCompletion(); + void test_moveTokenWiseThroughEveryFileAndTriggerQuickFixes(); #endif // WITH_TESTS private: diff --git a/src/plugins/cppeditor/fileandtokenactions_test.cpp b/src/plugins/cppeditor/fileandtokenactions_test.cpp new file mode 100644 index 00000000000..87adf7dceb0 --- /dev/null +++ b/src/plugins/cppeditor/fileandtokenactions_test.cpp @@ -0,0 +1,560 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of Qt Creator. +** +** 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +/*! + Tests for executing "test actions" for + (1) each file of each loaded project (file actions) or + e.g. "Switch Header/Source" + (2) each word/token of each file of each loaded project (token actions) + e.g. "Trigger and perform all Quick Fixes" + + Purpose: + Detect corner cases for which Qt Creator crashes or outputs QTC_ASSERTS. + Correct behavior is *not* tested. + + Prerequisites: + These tests depend on the projects that are loaded on startup of Qt Creator. + Make sure to load the test projects via command line or the session manager. + The test projects must be properly configured for a Kit. + */ + +using namespace Core; +using namespace CPlusPlus; +using namespace CppEditor; +using namespace CppEditor::Internal; +using namespace CppTools; +using namespace TextEditor; + +namespace { + +class TestActionsTestCase +{ +public: + class AbstractAction + { + public: + /// Every Action is expected to undo its changes. + virtual void run(CPPEditorWidget *editorWidget) = 0; + virtual ~AbstractAction() {} + }; + typedef QSharedPointer ActionPointer; + typedef QList Actions; + +public: + /// Run the given fileActions for each file and the given tokenActions for each token. + /// The cursor is positioned on the very first character of each token. + void run(const Actions &tokenActions = Actions(), + const Actions &fileActions = Actions()); + + /// Simulate pressing ESC, which will close popups, search results pane, etc... + /// This works only if the Qt Creator window is active. + static void escape(); + + /// Undoing changes + static void undoChangesInEditorWidget(BaseTextEditorWidget *editorWidget); + static void undoChangesInAllEditorWidgets(); + + /// Execute actions for the current cursor position of editorWidget. + static void executeActionsOnEditorWidget(CPPEditorWidget *editorWidget, Actions actions); + +private: + /// Move word camel case wise from current cursor position until given token (not included) + /// and execute the tokenActions for each new position. + static void moveWordCamelCaseToToken(TranslationUnit *translationUnit, Token token, + CPPEditor *editor, const Actions &tokenActions); + + static void undoAllChangesAndCloseAllEditors(); +}; + +typedef TestActionsTestCase::Actions Actions; +typedef TestActionsTestCase::ActionPointer ActionPointer; + +void TestActionsTestCase::run(const Actions &tokenActions, const Actions &fileActions) +{ + CppModelManagerInterface *mm = CppModelManagerInterface::instance(); + EditorManager *em = EditorManager::instance(); + + // Collect files to process + QStringList filesToOpen; + const QList projectInfos = mm->projectInfos(); + if (projectInfos.isEmpty()) + QSKIP("No project(s) loaded. Test operates only on loaded projects."); + + foreach (const CppModelManagerInterface::ProjectInfo &info, projectInfos) { + qDebug() << "Project" << info.project()->displayName() << "- files to process:" + << info.sourceFiles().size(); + foreach (const QString &sourceFile, info.sourceFiles()) + filesToOpen << sourceFile; + } + + qSort(filesToOpen); + + // Process all files from the projects + foreach (const QString filePath, filesToOpen) { + // Skip e.g. "" + const QFileInfo fileInfo(filePath); + if (!fileInfo.exists()) + continue; + + qDebug() << " --" << filePath; + + // Clean up + undoAllChangesAndCloseAllEditors(); + + // Open editor + QCOMPARE(em->openedEditors().size(), 0); + CPPEditor *editor = dynamic_cast(em->openEditor(filePath)); + QVERIFY(editor); + QCOMPARE(em->openedEditors().size(), 1); + QVERIFY(mm->isCppEditor(editor)); + QVERIFY(mm->workingCopy().contains(filePath)); + + // Rehighlight + CPPEditorWidget *editorWidget = dynamic_cast(editor->editorWidget()); + QVERIFY(editorWidget); + editorWidget->semanticRehighlight(true); + while (editorWidget->semanticInfo().doc.isNull()) + QApplication::processEvents(); + + // Run all file actions + executeActionsOnEditorWidget(editorWidget, fileActions); + + if (tokenActions.empty()) + continue; + + Snapshot snapshot = mm->snapshot(); + Document::Ptr document = snapshot.preprocessedDocument( + editorWidget->document()->toPlainText(), filePath); + QVERIFY(document); + document->parse(); + TranslationUnit *translationUnit = document->translationUnit(); + QVERIFY(translationUnit); + QVERIFY(translationUnit->tokenCount() >= 2); // First invalid token included + + // Move word camel case wise from the top until the first user visible token + editor->gotoLine(1); + const Token token = translationUnit->tokenAt(1); + moveWordCamelCaseToToken(translationUnit, token, editor, tokenActions); + + // Move token wise through the document + // translationUnit->tokenAt(0) is the invalid token, so start with 1. + for (unsigned i = 1, ei = translationUnit->tokenCount(); i < ei; ++i) { + const Token token = translationUnit->tokenAt(i); + if (token.kind() == T_EOF_SYMBOL) + continue; + + if (token.expanded()) { + // Move word camel case wise until first not expanded token + unsigned j = i + 1; + for (; translationUnit->tokenAt(j).expanded() && j < ei; ++j) ; + if (j == ei) { + if (j == i + 1) + break; + else // All tokens are expanded until end + j = j - 1; + } + const Token token = translationUnit->tokenAt(j); + moveWordCamelCaseToToken(translationUnit, token, editor, tokenActions); + i = j - 1; // Continue with next not expanded token + } else { + // Position the cursor on the token + unsigned line, column; + translationUnit->getPosition(token.offset, &line, &column); + editor->gotoLine(line, column - 1); + QApplication::processEvents(); + + // Run all token actions + executeActionsOnEditorWidget(editorWidget, tokenActions); + } + } // for tokens + } // for files + + // Clean up + undoAllChangesAndCloseAllEditors(); +} + +void TestActionsTestCase::escape() +{ + if (QWidget *w = QApplication::focusWidget()) + QTest::keyClick(w, Qt::Key_Escape); +} + +void TestActionsTestCase::undoChangesInEditorWidget(BaseTextEditorWidget *editorWidget) +{ + QTextDocument * const document = editorWidget->document(); + QVERIFY(document); + while (document->isUndoAvailable()) + editorWidget->undo(); +} + +void TestActionsTestCase::undoChangesInAllEditorWidgets() +{ + EditorManager *em = EditorManager::instance(); + foreach (IEditor *editor, em->openedEditors()) { + BaseTextEditor *baseTextEditor = qobject_cast(editor); + BaseTextEditorWidget *baseTextEditorWidget = baseTextEditor->editorWidget(); + undoChangesInEditorWidget(baseTextEditorWidget); + } +} + +void TestActionsTestCase::executeActionsOnEditorWidget(CPPEditorWidget *editorWidget, + TestActionsTestCase::Actions actions) +{ + foreach (const ActionPointer &action, actions) + action->run(editorWidget); + QApplication::processEvents(); +} + +void TestActionsTestCase::moveWordCamelCaseToToken(TranslationUnit *translationUnit, Token token, + CPPEditor *editor, const Actions &tokenActions) +{ + QVERIFY(translationUnit); + QVERIFY(editor); + CPPEditorWidget *editorWidget = dynamic_cast(editor->editorWidget()); + QVERIFY(editorWidget); + + unsigned line, column; + translationUnit->getPosition(token.offset, &line, &column); + + while (editor->currentLine() < (int) line + || (editor->currentLine() == (int) line + && editor->currentColumn() < (int) column)) { + editorWidget->gotoNextWordCamelCase(); + QApplication::processEvents(); + + // Run all token actions + executeActionsOnEditorWidget(editorWidget, tokenActions); + } +} + +void TestActionsTestCase::undoAllChangesAndCloseAllEditors() +{ + EditorManager *em = EditorManager::instance(); + undoChangesInAllEditorWidgets(); + em->closeAllEditors(/*askAboutModifiedEditors =*/ false); + QApplication::processEvents(); + QCOMPARE(em->openedEditors().size(), 0); +} + +class NoOpTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Do nothing on each token + void run(CPPEditorWidget *) {} +}; + +class FollowSymbolUnderCursorTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Follow symbol under cursor + /// Warning: May block if file does not exists (e.g. a not generated ui_* file). + void run(CPPEditorWidget *editorWidget); +}; + +void FollowSymbolUnderCursorTokenAction::run(CPPEditorWidget *editorWidget) +{ + EditorManager *em = EditorManager::instance(); + + // Follow link + IEditor *editorBefore = em->currentEditor(); + const int originalLine = editorBefore->currentLine(); + const int originalColumn = editorBefore->currentColumn(); + editorWidget->openLinkUnderCursor(); + TestActionsTestCase::escape(); + QApplication::processEvents(); + + // Go back + IEditor *editorAfter = em->currentEditor(); + if (editorAfter != editorBefore) + em->goBackInNavigationHistory(); + else + editorBefore->gotoLine(originalLine, originalColumn); + QApplication::processEvents(); +} + +class SwitchDeclarationDefinitionTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Switch Declaration/Definition on each token + void run(CPPEditorWidget *); +}; + +void SwitchDeclarationDefinitionTokenAction::run(CPPEditorWidget *) +{ + EditorManager *em = EditorManager::instance(); + + // Switch Declaration/Definition + IEditor *editorBefore = em->currentEditor(); + const int originalLine = editorBefore->currentLine(); + const int originalColumn = editorBefore->currentColumn(); + CppEditor::Internal::CppPlugin::instance()->switchDeclarationDefinition(); + QApplication::processEvents(); + + // Go back + IEditor *editorAfter = em->currentEditor(); + if (editorAfter != editorBefore) + em->goBackInNavigationHistory(); + else + editorBefore->gotoLine(originalLine, originalColumn); + QApplication::processEvents(); +} + +class FindUsagesTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Find Usages on each token + void run(CPPEditorWidget *); +}; + +void FindUsagesTokenAction::run(CPPEditorWidget *) +{ + CppEditor::Internal::CppPlugin::instance()->findUsages(); + QApplication::processEvents(); +} + +class RenameSymbolUnderCursorTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Rename Symbol Under Cursor on each token (Renaming is not applied) + void run(CPPEditorWidget *); +}; + +void RenameSymbolUnderCursorTokenAction::run(CPPEditorWidget *) +{ + CppEditor::Internal::CppPlugin::instance()->renameSymbolUnderCursor(); + QApplication::processEvents(); +} + +class OpenTypeHierarchyTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Open Type Hierarchy on each token + void run(CPPEditorWidget *); +}; + +void OpenTypeHierarchyTokenAction::run(CPPEditorWidget *) +{ + CppEditor::Internal::CppPlugin::instance()->openTypeHierarchy(); + QApplication::processEvents(); +} + +class InvokeCompletionTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Invoke completion menu on each token. + /// Warning: May create tool tip artefacts if focus is lost. + void run(CPPEditorWidget *editorWidget); +}; + +void InvokeCompletionTokenAction::run(CPPEditorWidget *editorWidget) +{ + // Invoke assistant and wait until it is finished + QEventLoop loop; + QObject::connect(editorWidget, SIGNAL(assistFinished()), &loop, SLOT(quit())); + editorWidget->invokeAssist(Completion); + loop.exec(); + + // Close the completion popup widget + QApplication::processEvents(); + editorWidget->abortAssist(); + // An alternative way to close the completion pop up is to give the editor widget the focus. + // This works only if the Qt Creator window is always the active window. + // editorWidget->setFocus(); + QApplication::processEvents(); + + TestActionsTestCase::undoChangesInEditorWidget(editorWidget); +} + +class RunAllQuickFixesTokenAction : public TestActionsTestCase::AbstractAction +{ +public: + /// Trigger all Quick Fixes and apply the matching ones + void run(CPPEditorWidget *editorWidget); +}; + +// TODO: Some QuickFixes operate on selections. +void RunAllQuickFixesTokenAction::run(CPPEditorWidget *editorWidget) +{ + // Calling editorWidget->invokeAssist(QuickFix) would be not enough + // since we also want to execute the ones that match. + + const QList quickFixFactories + = ExtensionSystem::PluginManager::getObjects(); + QVERIFY(!quickFixFactories.isEmpty()); + + CppQuickFixInterface qfi(new CppQuickFixAssistInterface(editorWidget, ExplicitlyInvoked)); + // This guard is important since the Quick Fixes expect to get a non-empty path(). + if (qfi->path().isEmpty()) + return; + + foreach (CppQuickFixFactory *quickFixFactory, quickFixFactories) { + TextEditor::QuickFixOperations operations; + quickFixFactory->match(qfi, operations); + foreach (QuickFixOperation::Ptr operation, operations) { + qDebug() << " -- Performing Quick Fix" << operation->description(); + operation->perform(); + TestActionsTestCase::escape(); + TestActionsTestCase::undoChangesInAllEditorWidgets(); + QApplication::processEvents(); + } + } +} + +class SwitchHeaderSourceFileAction : public TestActionsTestCase::AbstractAction +{ +public: + void run(CPPEditorWidget *); +}; + +void SwitchHeaderSourceFileAction::run(CPPEditorWidget *) +{ + EditorManager *em = EditorManager::instance(); + + // Switch Header/Source + IEditor *editorBefore = em->currentEditor(); + CppTools::Internal::CppToolsPlugin::instance()->switchHeaderSource(); + QApplication::processEvents(); + + // Go back + IEditor *editorAfter = em->currentEditor(); + if (editorAfter != editorBefore) + em->goBackInNavigationHistory(); + QApplication::processEvents(); +} + +} // anonymous namespace + +void CppPlugin::test_openEachFile() +{ + TestActionsTestCase test; + test.run(); +} + +void CppPlugin::test_switchHeaderSourceOnEachFile() +{ + Actions fileActions; + fileActions << ActionPointer(new SwitchHeaderSourceFileAction); + + TestActionsTestCase test; + test.run(Actions(), fileActions); +} + +void CppPlugin::test_moveTokenWiseThroughEveryFile() +{ + Actions tokenActions; + tokenActions << ActionPointer(new NoOpTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +} + +/// May block if file does not exists (e.g. a not generated ui_* file). +void CppPlugin::test_moveTokenWiseThroughEveryFileAndFollowSymbol() +{ + Actions tokenActions; + tokenActions << ActionPointer(new FollowSymbolUnderCursorTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +} + +void CppPlugin::test_moveTokenWiseThroughEveryFileAndSwitchDeclarationDefinition() +{ + Actions tokenActions; + tokenActions << ActionPointer(new SwitchDeclarationDefinitionTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +} + +void CppPlugin::test_moveTokenWiseThroughEveryFileAndFindUsages() +{ + Actions tokenActions; + tokenActions << ActionPointer(new FindUsagesTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +} + +void CppPlugin::test_moveTokenWiseThroughEveryFileAndRenameUsages() +{ + Actions tokenActions; + tokenActions << ActionPointer(new RenameSymbolUnderCursorTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +} + +void CppPlugin::test_moveTokenWiseThroughEveryFileAndOpenTypeHierarchy() +{ + Actions tokenActions; + tokenActions << ActionPointer(new OpenTypeHierarchyTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +} + +void CppPlugin::test_moveTokenWiseThroughEveryFileAndInvokeCompletion() +{ + Actions tokenActions; + tokenActions << ActionPointer(new InvokeCompletionTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +} + +void CppPlugin::test_moveTokenWiseThroughEveryFileAndTriggerQuickFixes() +{ + Actions tokenActions; + tokenActions << ActionPointer(new RunAllQuickFixesTokenAction()); + + TestActionsTestCase test; + test.run(tokenActions); +}