/************************************************************************** ** ** This file is part of Qt Creator ** ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies). ** ** Contact: Nokia Corporation (info@qt.nokia.com) ** ** ** GNU Lesser General Public License Usage ** ** 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, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** Other Usage ** ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** ** If you have questions regarding the use of this file, please contact ** Nokia at info@qt.nokia.com. ** **************************************************************************/ #include "resultsview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum { RES_SIZE = 100, RESULT_POS = 0, DETAILS_POS = 1, REASON_POS = 2, SCREEN_POS = 3 }; // Role for data which holds the identifier for use with m_pendingScreenshots static const int ScreenshotIdRole = Qt::UserRole + 10; // Role for the link to the .png file of a failure screenshot static const int ScreenshotLinkRole = Qt::UserRole + 100; static QBrush passBrush(QColor("lightgreen")); static QBrush failBrush(QColor("orangered")); static QBrush unexpectedBrush(QColor("orange")); /* Constructs a screenshot ID for the test failure at the given \a file and \a line. */ static QString screenshotId(const QString &file, int line) { return QString::fromLatin1("%1 %2").arg(QFileInfo(file).canonicalFilePath()).arg(line); } ResultsView::ResultsView(QWidget *parent, const char *name) : QTableWidget(parent) { setObjectName(name); setColumnCount(3); setGridStyle(Qt::NoPen); m_showPassing = m_testSettings.showPassedResults(); m_showDebug = m_testSettings.showDebugResults(); m_showSkipped = m_testSettings.showSkippedResults(); setHorizontalHeaderLabels(QStringList() << tr("Result") << tr("Test Details") << tr("Description")); resize(width()); setSelectionMode(QAbstractItemView::SingleSelection); setSelectionBehavior(QAbstractItemView::SelectRows); setSortingEnabled(false); setAlternatingRowColors(true); setEditTriggers(QAbstractItemView::NoEditTriggers); verticalHeader()->hide(); horizontalHeader()->show(); m_lastRow = -1; m_ignoreEvent = false; m_userLock = false; connect(this, SIGNAL(currentCellChanged(int,int,int,int)), this, SLOT(onChanged()), Qt::DirectConnection); connect(this, SIGNAL(itemClicked(QTableWidgetItem*)), this, SLOT(onItemClicked(QTableWidgetItem*)), Qt::DirectConnection); setWordWrap(true); } ResultsView::~ResultsView() { } void ResultsView::resize(int width) { resizeColumnToContents(RESULT_POS); if (columnWidth(RESULT_POS) < RES_SIZE) setColumnWidth(RESULT_POS, RES_SIZE); resizeColumnToContents(DETAILS_POS); if (columnWidth(DETAILS_POS) < RES_SIZE) setColumnWidth(DETAILS_POS, RES_SIZE); setColumnWidth(DETAILS_POS, columnWidth(DETAILS_POS) + 20); setColumnWidth(REASON_POS, width - columnWidth(RESULT_POS) - columnWidth(DETAILS_POS)); } void ResultsView::resizeEvent(QResizeEvent *event) { resize(event->size().width()); } void ResultsView::clear() { setUpdatesEnabled(false); disconnect(this, SIGNAL(currentCellChanged(int,int,int,int)), this, SLOT(onChanged())); clearContents(); setRowCount(0); connect(this, SIGNAL(currentCellChanged(int,int,int,int)), this, SLOT(onChanged()), Qt::DirectConnection); setUpdatesEnabled(true); m_ignoreEvent = false; m_userLock = false; m_failedTests.clear(); } /* Called when the test runner tells us it has taken a screenshot due to failure. Note that, since we determine test failures by parsing output, this could happen before or after we see the actual test failure. */ void ResultsView::addScreenshot(const QString &screenshot, const QString &testfunction, const QString& file, int line) { Q_UNUSED(testfunction); QString id = screenshotId(file, line); // We may have received the screenshot before the test result it matches. m_pendingScreenshots[id] = screenshot; updateScreenshots(); } /* Iterates through the test results and generates links to screenshots for any that have a screenshot available. */ void ResultsView::updateScreenshots() { for (int row = 0; row < rowCount(); ++row) { QTableWidgetItem *result = item(row, RESULT_POS); if (!result) continue; // If there is a screenshot for this result, put a link to it in the table. QString id = result->data(ScreenshotIdRole).toString(); if (!m_pendingScreenshots.contains(id)) continue; QString screenshot = m_pendingScreenshots.take(id); QTableWidgetItem* shot = new QTableWidgetItem(QIcon(QPixmap(QLatin1String(":/testrun.png"))), QString()); shot->setData(ScreenshotLinkRole, screenshot); shot->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter); setItem(row, SCREEN_POS, shot); } } /* Opens any screenshot linked to from \a item. Called when \a item is clicked. */ void ResultsView::onItemClicked(QTableWidgetItem* item) { QString shot = item->data(ScreenshotLinkRole).toString(); if (shot.isEmpty()) return; // Open the screenshot using the preferred image viewer. QDesktopServices::openUrl(QUrl::fromLocalFile(shot)); } void ResultsView::append(const QString &res, const QString &test, const QString &reason, const QString &dataTag, const QString &file, const QString &line) { int row = rowCount(); insertRow(row); QTableWidgetItem* result = new QTableWidgetItem(res); result->setTextAlignment(Qt::AlignCenter); if (res.startsWith(QLatin1String("PASS"))) { result->setBackground(passBrush); setRowHidden(row, !m_showPassing); } else if (res.startsWith(QLatin1String("FAIL"))) { result->setBackground(failBrush); } else if (res.startsWith(QLatin1String("QDEBUG"))) { setRowHidden(row, !m_showDebug); } else if (res.startsWith(QLatin1String("SKIP"))) { setRowHidden(row, !m_showSkipped); } else if (res.startsWith(QLatin1String("XFAIL")) || res.startsWith(QLatin1String("XPASS"))) { result->setBackground(unexpectedBrush); } if ((res.contains(QLatin1String("FAIL")) || res.startsWith(QLatin1String("XPASS"))) && !m_failedTests.contains(test)) m_failedTests.append(test); // Construct a result id for use in mapping test failures to screenshots. QString resultId = screenshotId(file, line.toInt()); result->setData(ScreenshotIdRole, resultId); QTableWidgetItem *testDetails = new QTableWidgetItem(formatTestDetails(test, dataTag)); testDetails->setToolTip(formatLocation(file, line)); setItem(row, RESULT_POS, result); setItem(row, DETAILS_POS, testDetails); setItem(row, REASON_POS, new QTableWidgetItem(reason)); resize(width()); resizeRowToContents(row); if (currentRow() == -1) scrollToItem(item(row, REASON_POS)); m_resultsWindow->navigateStateChanged(); updateScreenshots(); } QString ResultsView::xmlDequote(const QString &input) { QString result = input; return result.replace(QLatin1String(">"), QLatin1String(">")) .replace(QLatin1String("<"), QLatin1String("<")) .replace(QLatin1String("'"), QLatin1String("'")) .replace(QLatin1String("""), QLatin1String("\"")) .replace(QLatin1String("&"), QLatin1String("&")) .replace(QLatin1String("-"), QLatin1String("-")); } QString ResultsView::htmlQuote(const QString &input) { QString result=input; return result.replace(QLatin1String("&"), QLatin1String("&")) .replace(QLatin1String(">"), QLatin1String(">")) .replace(QLatin1String("<"), QLatin1String("<")) .replace(QLatin1String("\""), QLatin1String(""")) .replace(QLatin1String("\n"), QLatin1String("
\n")); } QString ResultsView::formatTestDetails(const QString &test, const QString &dataTag) { QString ret = test; if (!dataTag.isEmpty() && dataTag != QLatin1String("...")) { ret += QLatin1String(" ("); ret += dataTag; ret += QLatin1Char(')'); } return ret; } QString ResultsView::formatLocation(const QString &file, const QString &line) { QString description; QString _file = file; if (_file.startsWith(QLatin1Char('['))) _file = _file.mid(1); if (!_file.isEmpty() || !line.isEmpty()) { description += xmlDequote(file); if (line != QLatin1String("-1")) { description += QLatin1Char(':'); description += line; } } return description; } QString ResultsView::result(int row) { if (row >= rowCount()) return QString(); return item(row, RESULT_POS)->text().simplified(); } QString ResultsView::reason(int row) { if (row >= rowCount()) return QString(); QString txt = item(row, REASON_POS)->text(); const int pos = txt.indexOf(QLatin1Char('\n')); if (pos > 0) txt.truncate(pos); return txt.simplified(); } QString ResultsView::location(int row) { if (row >= rowCount()) return QString(); return item(row, DETAILS_POS)->toolTip(); } QString ResultsView::file(int row) { QString txt = location(row); if (!txt.isEmpty()) { const int pos = txt.indexOf(QLatin1Char(':')); if (pos >= 0) txt.truncate(pos); return txt.simplified(); } return QString(); } QString ResultsView::line(int row) { QString txt = location(row); if (!txt.isEmpty()) { const int pos = txt.indexOf(QLatin1Char(':')); if (pos >= 0) { txt = txt.mid(pos + 1); } else { txt.clear(); } return txt.simplified(); } return QString(); } int ResultsView::intLine(int row) { if (row >= rowCount()) return false; return line(row).toInt(); } void ResultsView::setResult(const QString &result, const QString &test, const QString &reason, const QString &dataTag, const QString &file, int line) { int row; QString lineStr(QString::number(line)); append(result, test, reason, dataTag, file, lineStr); row = rowCount()-1; } void ResultsView::onChanged() { if (!m_ignoreEvent) { if (currentColumn() == 0) m_userLock = false; else m_userLock = true; } int row = currentRow(); m_lastRow = row; if (m_ignoreEvent || row >= rowCount()) return; m_resultsWindow->navigateStateChanged(); QTimer::singleShot(0, this, SLOT(emitCurSelection())); } void ResultsView::emitCurSelection() { emitSelection(m_lastRow); } void ResultsView::emitSelection(int row) { if (row < 0) return; TestCaseRec rec; rec.m_testFunction = ""; rec.m_line = intLine(row); TestCode *tmp = m_testCollection.findCode(file(row), "", ""); rec.m_code = tmp; emit defectSelected(rec); } void ResultsView::setResultsWindow(TestResultsWindow *window) { m_resultsWindow = window; } TestResultsWindow* ResultsView::resultsWindow() const { return m_resultsWindow; } void ResultsView::showPassing(bool show) { m_showPassing = show; updateHidden(QLatin1String("PASS"), show); m_testSettings.setShowPassedResults(show); } void ResultsView::showDebugMessages(bool show) { m_showDebug = show; updateHidden(QLatin1String("QDEBUG"), show); m_testSettings.setShowDebugResults(show); } void ResultsView::showSkipped(bool show) { m_showSkipped = show; updateHidden(QLatin1String("SKIP"), show); m_testSettings.setShowSkippedResults(show); } void ResultsView::updateHidden(const QString &result, bool show) { for (int row = 0; row < rowCount(); ++row) { QTableWidgetItem *resultItem = item(row, RESULT_POS); if (resultItem && resultItem->text().startsWith(result)) setRowHidden(row, !show); } resize(width()); m_resultsWindow->navigateStateChanged(); } // ********************************************************** ResultsView *testResultsPane() { return TestResultsWindow::instance()->resultsView(); } TestResultsWindow *_testResultsInstance = 0; TestResultsWindow *TestResultsWindow::instance() { if (!_testResultsInstance) { _testResultsInstance = new TestResultsWindow(); } return _testResultsInstance; } TestResultsWindow::TestResultsWindow() : m_stopAction(new QAction(QIcon(QLatin1String(ProjectExplorer::Constants::ICON_STOP)), tr("Stop Testing"), this)), m_retryAction(new QAction(QIcon(QLatin1String(":/reload.png")), tr("Retry Failed Tests"), this)), m_copyAction(new QAction(QIcon(QLatin1String(Core::Constants::ICON_COPY)), tr("Copy Results"), this)), m_stopButton(new QToolButton), m_retryButton(new QToolButton), m_copyButton(new QToolButton), m_filterButton(new QToolButton) { m_resultsView = new ResultsView; m_resultsView->setResultsWindow(this); m_resultsView->setFrameStyle(QFrame::NoFrame); connect(&m_testSettings, SIGNAL(changed()), this, SLOT(onSettingsChanged())); m_stopAction->setToolTip(tr("Stop Testing")); m_stopAction->setEnabled(false); m_retryAction->setToolTip(tr("Retry Failed Tests")); m_retryAction->setEnabled(false); m_copyAction->setToolTip(tr("Copy Results")); m_copyAction->setEnabled(false); m_filterButton->setIcon(QIcon(Core::Constants::ICON_FILTER)); m_filterButton->setToolTip(tr("Filter Results")); m_filterButton->setPopupMode(QToolButton::InstantPopup); m_showPassingAction = new QAction(tr("Show Passing Tests"), this); m_showPassingAction->setCheckable(true); m_showPassingAction->setChecked(m_testSettings.showPassedResults()); connect(m_showPassingAction, SIGNAL(toggled(bool)), m_resultsView, SLOT(showPassing(bool))); m_showDebugAction = new QAction(tr("Show Debug Messages"), this); m_showDebugAction->setCheckable(true); m_showDebugAction->setChecked(m_testSettings.showDebugResults()); connect(m_showDebugAction, SIGNAL(toggled(bool)), m_resultsView, SLOT(showDebugMessages(bool))); m_showSkipAction = new QAction(tr("Show Skipped Tests"), this); m_showSkipAction->setCheckable(true); m_showSkipAction->setChecked(m_testSettings.showSkippedResults()); connect(m_showSkipAction, SIGNAL(toggled(bool)), m_resultsView, SLOT(showSkipped(bool))); QMenu *filterMenu = new QMenu(m_filterButton); filterMenu->addAction(m_showPassingAction); filterMenu->addAction(m_showDebugAction); filterMenu->addAction(m_showSkipAction); m_filterButton->setMenu(filterMenu); Core::ActionManager *am = Core::ICore::instance()->actionManager(); Core::Context globalcontext(Core::Constants::C_GLOBAL); Core::Command *stopCmd = am->registerAction(m_stopAction, Core::Id("Qt4Test.StopTest"), globalcontext); Core::Command *retryCmd = am->registerAction(m_retryAction, Core::Id("Qt4Test.RetryFailed"), globalcontext); Core::Command *copyCmd = am->registerAction(m_copyAction, Core::Id("Qt4Test.CopyResults"), globalcontext); m_stopButton->setDefaultAction(stopCmd->action()); m_stopButton->setAutoRaise(true); m_retryButton->setDefaultAction(retryCmd->action()); m_retryButton->setAutoRaise(true); m_copyButton->setDefaultAction(copyCmd->action()); m_copyButton->setAutoRaise(true); connect(m_stopAction, SIGNAL(triggered()), this, SIGNAL(stopTest())); connect(m_retryAction, SIGNAL(triggered()), this, SLOT(retryFailed())); connect(m_copyAction, SIGNAL(triggered()), this, SLOT(copyResults())); } TestResultsWindow::~TestResultsWindow() { delete m_resultsView; _testResultsInstance = 0; } bool TestResultsWindow::hasFocus() { return m_resultsView->hasFocus(); } bool TestResultsWindow::canFocus() { return true; } void TestResultsWindow::setFocus() { m_resultsView->setFocus(); } void TestResultsWindow::clearContents() { m_resultsView->clear(); m_retryAction->setEnabled(false); m_copyAction->setEnabled(false); navigateStateChanged(); } QWidget *TestResultsWindow::outputWidget(QWidget *parent) { m_resultsView->setParent(parent); return m_resultsView; } QString TestResultsWindow::name() const { return tr("Test Results"); } void TestResultsWindow::visibilityChanged(bool /*b*/) { } int TestResultsWindow::priorityInStatusBar() const { return 50; } bool TestResultsWindow::canNext() { for (int i = m_resultsView->currentRow() + 1; i < m_resultsView->rowCount(); ++i) { if (!m_resultsView->isRowHidden(i)) return true; } return false; } bool TestResultsWindow::canPrevious() { for (int i = m_resultsView->currentRow() - 1; i >= 0; --i) { if (!m_resultsView->isRowHidden(i)) return true; } return false; } void TestResultsWindow::goToNext() { for (int i = m_resultsView->currentRow() + 1; i < m_resultsView->rowCount(); ++i) { if (!m_resultsView->isRowHidden(i)) { m_resultsView->setCurrentCell(i, 0); return; } } } void TestResultsWindow::goToPrev() { for (int i = m_resultsView->currentRow() - 1; i >= 0; --i) { if (!m_resultsView->isRowHidden(i)) { m_resultsView->setCurrentCell(i, 0); return; } } } bool TestResultsWindow::canNavigate() { return true; } QList TestResultsWindow::toolBarWidgets() const { return QList() << m_filterButton << m_stopButton << m_retryButton << m_copyButton; } void TestResultsWindow::addResult(const QString &result, const QString &test, const QString &reason, const QString &dataTag, const QString &file, int line) { m_resultsView->setResult(result, test, reason, dataTag, file, line); } void TestResultsWindow::onTestStarted() { m_stopAction->setEnabled(true); m_retryAction->setEnabled(false); m_copyAction->setEnabled(false); } void TestResultsWindow::onTestStopped() { m_stopAction->setEnabled(false); m_retryAction->setEnabled(false); m_copyAction->setEnabled(false); } void TestResultsWindow::onTestFinished() { m_stopAction->setEnabled(false); m_retryAction->setEnabled(!m_resultsView->failedTests().isEmpty()); m_copyAction->setEnabled(true); } void TestResultsWindow::retryFailed() { emit retryFailedTests(m_resultsView->failedTests()); } void TestResultsWindow::copyResults() { m_resultsView->copyResults(); } void ResultsView::copyResults() { QMimeData *md = new QMimeData(); QString html = QLatin1String(""); QString text; for (int row = 0; row < rowCount(); ++row) { QString result = item(row, RESULT_POS)->text().trimmed(); QString detail = item(row, DETAILS_POS)->text(); QString location = item(row, DETAILS_POS)->toolTip(); QString reason = item(row, REASON_POS)->text(); html += QString::fromLatin1("") .arg(result).arg(detail).arg(location).arg(htmlQuote(reason)); text += QString::fromLatin1("%1\n%2\n%3\n%4\n").arg(result).arg(detail).arg(location).arg(reason); } html += QLatin1String("
%1%2%3%4
"); md->setHtml(html); md->setText(text); QApplication::clipboard()->setMimeData(md); } void TestResultsWindow::onSettingsChanged() { if (m_showPassingAction->isChecked() != m_testSettings.showPassedResults()) m_showPassingAction->setChecked(m_testSettings.showPassedResults()); if (m_showDebugAction->isChecked() != m_testSettings.showDebugResults()) m_showDebugAction->setChecked(m_testSettings.showDebugResults()); if (m_showSkipAction->isChecked() != m_testSettings.showSkippedResults()) m_showSkipAction->setChecked(m_testSettings.showSkippedResults()); }