diff --git a/src/parsers/perf/perfparser.cpp b/src/parsers/perf/perfparser.cpp index f5efdd569..5fa8c3f6b 100644 --- a/src/parsers/perf/perfparser.cpp +++ b/src/parsers/perf/perfparser.cpp @@ -1475,6 +1475,7 @@ PerfParser::~PerfParser() = default; bool PerfParser::initParserArgs(const QString& path) { + // check for common file issues const auto info = QFileInfo(path); if (!info.exists()) { emit parsingFailed(tr("File '%1' does not exist.").arg(path)); @@ -1489,6 +1490,22 @@ bool PerfParser::initParserArgs(const QString& path) return false; } + // peek into file header + const auto filename = decompressIfNeeded(path); + QFile file(filename); + file.open(QIODevice::ReadOnly); + if (file.peek(8) != "PERFILE2" && file.peek(11) != "QPERFSTREAM") { + if (file.peek(8) == "PERFFILE") { + emit parsingFailed(tr("Failed to parse file %1: %2").arg(path, tr("Unsupported V1 perf data"))); + } else { + emit parsingFailed(tr("Failed to parse file %1: %2").arg(path, tr("File format unknown"))); + } + file.close(); + return false; + } + file.close(); + + // check perfparser and set initial values auto parserBinary = Util::perfParserBinaryPath(); if (parserBinary.isEmpty()) { emit parsingFailed(tr("Failed to find hotspot-perfparser binary.")); @@ -1497,8 +1514,8 @@ bool PerfParser::initParserArgs(const QString& path) auto parserArgs = [this](const QString& filename) { const auto settings = Settings::instance(); - QStringList parserArgs = {QStringLiteral("--input"), decompressIfNeeded(filename), - QStringLiteral("--max-frames"), QStringLiteral("1024")}; + QStringList parserArgs = {QStringLiteral("--input"), filename, QStringLiteral("--max-frames"), + QStringLiteral("1024")}; const auto sysroot = settings->sysroot(); if (!sysroot.isEmpty()) { parserArgs += {QStringLiteral("--sysroot"), sysroot}; @@ -1530,7 +1547,7 @@ bool PerfParser::initParserArgs(const QString& path) return parserArgs; }; - m_parserArgs = parserArgs(path); + m_parserArgs = parserArgs(filename); m_parserBinary = parserBinary; return true; } @@ -1583,22 +1600,23 @@ void PerfParser::startParseFile(const QString& path) emit parsingFinished(); }; - if (path.endsWith(QLatin1String(".perfparser"))) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - emit parsingFailed(tr("Failed to open file %1: %2").arg(path, file.errorString())); - return; - } + // note: file is always readable and in supported format here, + // already validated in initParserArgs() + QFile file(path); + file.open(QIODevice::ReadOnly); + if (file.peek(11) == "QPERFSTREAM") { d.setInput(&file); while (!file.atEnd() && !d.stopRequested) { if (!d.tryParse()) { - emit parsingFailed(tr("Failed to parse file")); + // TODO: provide reason + emit parsingFailed(tr("Failed to parse file %1: %2").arg(path, QStringLiteral("Unknown reason"))); return; } } finalize(); return; } + file.close(); QProcess process; process.setProcessEnvironment(perfparserEnvironment(debuginfodUrls)); diff --git a/tests/integrationtests/file_content/perf.data.true.v1 b/tests/integrationtests/file_content/perf.data.true.v1 new file mode 100644 index 000000000..1c673fc48 Binary files /dev/null and b/tests/integrationtests/file_content/perf.data.true.v1 differ diff --git a/tests/integrationtests/file_content/perf.data.true.v2 b/tests/integrationtests/file_content/perf.data.true.v2 new file mode 100644 index 000000000..12fa8ac87 Binary files /dev/null and b/tests/integrationtests/file_content/perf.data.true.v2 differ diff --git a/tests/integrationtests/file_content/perf.data.true.v2.gz b/tests/integrationtests/file_content/perf.data.true.v2.gz new file mode 100644 index 000000000..5f953ee5a Binary files /dev/null and b/tests/integrationtests/file_content/perf.data.true.v2.gz differ diff --git a/tests/integrationtests/file_content/true.perfparser b/tests/integrationtests/file_content/true.perfparser new file mode 100644 index 000000000..ae951816d Binary files /dev/null and b/tests/integrationtests/file_content/true.perfparser differ diff --git a/tests/integrationtests/tst_perfparser.cpp b/tests/integrationtests/tst_perfparser.cpp index 184bff7ed..052165428 100644 --- a/tests/integrationtests/tst_perfparser.cpp +++ b/tests/integrationtests/tst_perfparser.cpp @@ -172,6 +172,109 @@ private slots: m_capabilities = host.perfCapabilities(); } + void testFileErrorHandling() + { + PerfParser parser(this); + QSignalSpy parsingFailedSpy(&parser, &PerfParser::parsingFailed); + QString message; + + const auto notThereFile = QStringLiteral("not_here"); + parser.initParserArgs(notThereFile); + + QCOMPARE(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(notThereFile)); + QVERIFY(message.contains(QLatin1String("does not exist"))); + + const auto parentDirs = QStringLiteral("../.."); + parser.initParserArgs(parentDirs); + // note: initializing parser args reset the attached spy counter + QCOMPARE(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(parentDirs)); + QVERIFY(message.contains(QLatin1String("is not a file"))); + + const auto noRead = QStringLiteral("no_r_possible"); + const auto shell = QStandardPaths::findExecutable(QStringLiteral("bash")); + if (!shell.isEmpty()) { + QProcess::execute( + shell, {QLatin1String("-c"), QStringLiteral("rm -rf %1 && touch %1 && chmod a-r %1").arg(noRead)}); + parser.initParserArgs(noRead); + + QCOMPARE(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(noRead)); + QVERIFY(message.contains(QLatin1String("not readable"))); + } + } + + void testFileContent() + { + Settings::instance()->setAppPath(QStringLiteral("/usr/bin")); // should be the output of `which true` + Settings::instance()->setExtraLibPaths(QFINDTESTDATA("file_content")); + PerfParser parser(this); + QSignalSpy parsingFailedSpy(&parser, &PerfParser::parsingFailed); + QSignalSpy parsingFinishedSpy(&parser, &PerfParser::parsingFinished); + QString message; + + // check pre-exported perfparser + const auto perfData = QFINDTESTDATA("file_content/true.perfparser"); + QVERIFY(!perfData.isEmpty() && QFile::exists(perfData)); + parser.startParseFile(perfData); + QVERIFY(parsingFinishedSpy.wait(2000)); + QCOMPARE(parsingFailedSpy.count(), 0); + QCOMPARE(parsingFinishedSpy.count(), 1); + + // check pre-exported perfparser with "bad extension" + const auto shell = QStandardPaths::findExecutable(QStringLiteral("bash")); + if (!shell.isEmpty()) { + const auto perfDataSomeName = QStringLiteral("fruitper"); + QProcess::execute(shell, + {QStringLiteral("-c"), QLatin1String("cp -f %1 %2").arg(perfData, perfDataSomeName)}); + + parser.startParseFile(perfDataSomeName); + QVERIFY(parsingFinishedSpy.wait(3000)); + QCOMPARE(parsingFailedSpy.count(), 0); + QCOMPARE(parsingFinishedSpy.count(), 2); + } + + // check for invalid data -> expected error + const auto noPerf = QFINDTESTDATA("tst_perfparser.cpp"); + QVERIFY(!noPerf.isEmpty() && QFile::exists(noPerf)); + parser.startParseFile(noPerf); + QCOMPARE(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(noPerf)); + QVERIFY(message.contains(QLatin1String("File format unknown"))); + + // check for PERFv1 -> expected error + const auto perf1 = QFINDTESTDATA("file_content/perf.data.true.v1"); + QVERIFY(!perf1.isEmpty() && QFile::exists(perf1)); + parser.startParseFile(perf1); + QCOMPARE(parsingFailedSpy.count(), 1); + message = qvariant_cast(parsingFailedSpy.takeFirst().at(0)); + QVERIFY(message.contains(perf1)); + QVERIFY(message.contains(QLatin1String("V1 perf data"))); + + // check for PERFv2 + const auto perf2 = QFINDTESTDATA("file_content/perf.data.true.v2"); + QVERIFY(!perf2.isEmpty() && QFile::exists(perf2)); + parser.startParseFile(perf2); + QVERIFY(parsingFinishedSpy.wait(2000)); + QCOMPARE(parsingFailedSpy.count(), 0); + QCOMPARE(parsingFinishedSpy.count(), 3); + +#if KFArchive_FOUND + // check for PERFv2, gzipped + const auto perf2gz = QFINDTESTDATA("file_content/perf.data.true.v2.gz"); + QVERIFY(!perf2gz.isEmpty() && QFile::exists(perf2gz)); + parser.startParseFile(perf2gz); + QVERIFY(parsingFinishedSpy.wait(2000)); + QCOMPARE(parsingFailedSpy.count(), 0); + QCOMPARE(parsingFinishedSpy.count(), 4); +#endif + } + void init() { m_bottomUpData = {}; @@ -743,8 +846,8 @@ private slots: VERIFY_OR_THROW(recordingFinishedSpy.wait(10000)); - COMPARE_OR_THROW(recordingFailedSpy.count(), 0); - COMPARE_OR_THROW(recordingFinishedSpy.count(), 1); + QCOMPARE(recordingFailedSpy.count(), 0); + QCOMPARE(recordingFinishedSpy.count(), 1); COMPARE_OR_THROW(QFileInfo::exists(fileName), true); m_perfCommand = perf.perfCommand(); @@ -790,25 +893,25 @@ private slots: parser.startParseFile(fileName); - VERIFY_OR_THROW(parsingFinishedSpy.wait(6000)); + QVERIFY(parsingFinishedSpy.wait(6000)); // Verify that the test passed - COMPARE_OR_THROW(parsingFailedSpy.count(), 0); - COMPARE_OR_THROW(parsingFinishedSpy.count(), 1); + QCOMPARE(parsingFailedSpy.count(), 0); + QCOMPARE(parsingFinishedSpy.count(), 1); // Verify the summary data isn't empty - COMPARE_OR_THROW(summaryDataSpy.count(), 1); + QCOMPARE(summaryDataSpy.count(), 1); QList summaryDataArgs = summaryDataSpy.takeFirst(); m_summaryData = qvariant_cast(summaryDataArgs.at(0)); - COMPARE_OR_THROW(m_perfCommand, m_summaryData.command); + QCOMPARE(m_perfCommand, m_summaryData.command); VERIFY_OR_THROW(m_summaryData.sampleCount > 0); VERIFY_OR_THROW(m_summaryData.applicationTime.delta() > 0); VERIFY_OR_THROW(m_summaryData.cpusAvailable > 0); - COMPARE_OR_THROW(m_summaryData.processCount, quint32(1)); // for now we always have a single process + QCOMPARE(m_summaryData.processCount, quint32(1)); // for now we always have a single process VERIFY_OR_THROW(m_summaryData.threadCount > 0); // and at least one thread - COMPARE_OR_THROW(m_summaryData.cpuArchitecture, m_cpuArchitecture); - COMPARE_OR_THROW(m_summaryData.linuxKernelVersion, m_linuxKernelVersion); - COMPARE_OR_THROW(m_summaryData.hostName, m_machineHostName); + QCOMPARE(m_summaryData.cpuArchitecture, m_cpuArchitecture); + QCOMPARE(m_summaryData.linuxKernelVersion, m_linuxKernelVersion); + QCOMPARE(m_summaryData.hostName, m_machineHostName); if (checkFrequency) { // Verify the sample frequency is acceptable, greater than 500Hz