Skip to content

Commit

Permalink
peek into file before open
Browse files Browse the repository at this point in the history
* check file open, providing nice and early error
* explicit check for perfparser (magic number QPERFSTREAM)
  opening directly without the need to fall back to file name
* explicit check for V1 perf data which isn't supported by perfparser
* explicit check for unknown file type

Fixes: KDAB#477
  • Loading branch information
GitMensch committed Nov 3, 2023
1 parent 89c3808 commit 4ffef0b
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 21 deletions.
38 changes: 28 additions & 10 deletions src/parsers/perf/perfparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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."));
Expand All @@ -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};
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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));
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
125 changes: 114 additions & 11 deletions tests/integrationtests/tst_perfparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<QString>(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<QString>(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<QString>(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<QString>(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<QString>(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 = {};
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<QVariant> summaryDataArgs = summaryDataSpy.takeFirst();
m_summaryData = qvariant_cast<Data::Summary>(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
Expand Down

0 comments on commit 4ffef0b

Please sign in to comment.