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: #477
  • Loading branch information
GitMensch committed Nov 1, 2023
1 parent 38b7650 commit f41fe8e
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 7 deletions.
31 changes: 24 additions & 7 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,21 @@ bool PerfParser::initParserArgs(const QString& path)
return false;
}

// peek into file header
QFile file(path);
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 Down Expand Up @@ -1583,22 +1599,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
101 changes: 101 additions & 0 deletions tests/integrationtests/tst_perfparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,107 @@ private slots:
m_capabilities = host.perfCapabilities();
}

void testFileErrorHandling()
{
PerfParser parser(this);
QSignalSpy parsingFailedSpy(&parser, &PerfParser::parsingFailed);
QString message;

const auto notThereFile = QLatin1String("not_here");
parser.initParserArgs(notThereFile);

COMPARE_OR_THROW(parsingFailedSpy.count(), 1);
message = qvariant_cast<QString>(parsingFailedSpy.takeFirst().at(0));
// qDebug("Error message is '%s'", qPrintable(message));
QVERIFY(message.contains(notThereFile));
QVERIFY(message.contains(QLatin1String("does not exist")));

const auto parentDirs = QLatin1String("../..");
parser.initParserArgs(parentDirs);
// note: initializing parser args reset the attached spy counter
COMPARE_OR_THROW(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 = QLatin1String("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);

COMPARE_OR_THROW(parsingFailedSpy.count(), 1);
message = qvariant_cast<QString>(parsingFailedSpy.takeFirst().at(0));
QVERIFY(message.contains(noRead));
QVERIFY(message.contains(QLatin1String("not readable")));
}
}

void testFileContent()
{
PerfParser parser(this);
QSignalSpy parsingFailedSpy(&parser, &PerfParser::parsingFailed);
QSignalSpy parsingFinishedSpy(&parser, &PerfParser::parsingFinished);
QString message;

const auto perfData = QFINDTESTDATA("custom_cost_aggregation_testfiles/custom_cost_aggregation.perfparser");
QVERIFY(!perfData.isEmpty() && QFile::exists(perfData));

parser.startParseFile(perfData);

VERIFY_OR_THROW(parsingFinishedSpy.wait(6000));

// Verify that the test passed
COMPARE_OR_THROW(parsingFailedSpy.count(), 0);
COMPARE_OR_THROW(parsingFinishedSpy.count(), 1);

const auto shell = QStandardPaths::findExecutable(QStringLiteral("bash"));
if (!shell.isEmpty()) {
const auto perfDataSomeName = QLatin1String("fruitper");
QProcess::execute(shell,
{QLatin1String("-c"), QStringLiteral("cp -f %1 %2").arg(perfData, perfDataSomeName)});

parser.startParseFile(perfDataSomeName);

VERIFY_OR_THROW(parsingFinishedSpy.wait(6000));

// Verify that the test passed
COMPARE_OR_THROW(parsingFailedSpy.count(), 0);
COMPARE_OR_THROW(parsingFinishedSpy.count(), 2);
}

// check for invalid data
const auto noPerf = QFINDTESTDATA("custom_cost_aggregation_testfiles/by_cpu.txt");
QVERIFY(!noPerf.isEmpty() && QFile::exists(noPerf));
parser.startParseFile(noPerf);
COMPARE_OR_THROW(parsingFailedSpy.count(), 1);
message = qvariant_cast<QString>(parsingFailedSpy.takeFirst().at(0));
QVERIFY(message.contains(noPerf));
QVERIFY(message.contains(QLatin1String("File format unknown")));

// TODO: check for PERFv1 (missing: data file)
// const auto perf1 = QFINDTESTDATA("custom_cost_aggregation_testfiles/perf.data.custom_cost_aggregation");
// QVERIFY(!perf1.isEmpty() && QFile::exists(perf1));
// parser.startParseFile(perf1);
// COMPARE_OR_THROW(parsingFailedSpy.count(), 1);
// message = qvariant_cast<QString>(parsingFailedSpy.takeFirst().at(0));
// QVERIFY(message.contains(perf1));
// QVERIFY(message.contains(QLatin1String("V1 perf data")));

// TODO: check for PERFv2 (missing: data file)
// const auto perf2 = QFINDTESTDATA("custom_cost_aggregation_testfiles/perf.data.custom_cost_aggregation");
// QVERIFY(!perf2.isEmpty() && QFile::exists(perf2));

// parser.startParseFile(perf2);

// VERIFY_OR_THROW(parsingFinishedSpy.wait(6000));

// // Verify that the test passed
// COMPARE_OR_THROW(parsingFailedSpy.count(), 0);
// COMPARE_OR_THROW(parsingFinishedSpy.count(), 1);
}

void init()
{
m_bottomUpData = {};
Expand Down

0 comments on commit f41fe8e

Please sign in to comment.