// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "squishrunnerprocess.h" #include "squishtr.h" #include #include #include Q_LOGGING_CATEGORY(runnerLOG, "qtc.squish.squishrunner", QtWarningMsg) namespace Squish::Internal { static QString maskedArgument(const QString &originalArg) { QString masked = originalArg; masked.replace('\\', "\\\\"); masked.replace(' ', "\\x20"); return masked; } SquishRunnerProcess::SquishRunnerProcess(QObject *parent) : SquishProcessBase{parent} { } void SquishRunnerProcess::setupProcess(RunnerMode mode) { QTC_ASSERT(!m_mode || m_mode == mode, return); if (m_mode == mode) return; m_mode.emplace(mode); switch (m_mode.value()) { case Run: case StartAut: m_process.setProcessMode(Utils::ProcessMode::Writer); m_process.setStdOutLineCallback([this](const QString &line){ onStdOutput(line); }); break; case QueryServer: break; case Record: m_process.setProcessMode(Utils::ProcessMode::Writer); break; case Inspect: m_process.setProcessMode(Utils::ProcessMode::Writer); m_process.setStdOutLineCallback([this](const QString &line) { onInspectorOutput(line); }); break; } } void SquishRunnerProcess::start(const Utils::CommandLine &cmdline, const Utils::Environment &env) { QTC_ASSERT(m_process.state() == QProcess::NotRunning, return); m_licenseIssues = false; m_autId = 0; m_outputMode = SingleLine; m_multiLineContent.clear(); SquishProcessBase::start(cmdline, env); } void SquishRunnerProcess::onDone() { qCDebug(runnerLOG) << "Runner finished"; if (m_mode == QueryServer) { const QString error = m_licenseIssues ? Tr::tr("Could not get Squish license from server.") : QString(); emit queryDone(m_process.stdOut(), error); } if (m_mode == Record) { emit recorderDone(); } else { emit runnerFinished(); // handle output file stuff - FIXME move over to runner? } setState(Stopped); } void SquishRunnerProcess::onErrorOutput() { // output that must be sent to the Runner/Server Log const QByteArray output = m_process.readAllRawStandardError(); const QList lines = output.split('\n'); for (const QByteArray &line : lines) { const QByteArray trimmed = line.trimmed(); if (trimmed.isEmpty()) continue; emit logOutputReceived("Runner: " + QLatin1String(trimmed)); if (trimmed.startsWith("QSocketNotifier: Invalid socket")) { emit runnerError(InvalidSocket); } else if (trimmed.contains("could not be started.") && trimmed.contains("Mapped AUT")) { emit runnerError(MappedAutMissing); } else if (trimmed.startsWith("Couldn't get license") || trimmed.contains("UNLICENSED version of Squish")) { m_licenseIssues = true; } } } void SquishRunnerProcess::onStdOutput(const QString &lineIn) { int fileLine = -1; int fileColumn = -1; QString fileName; // we might enter this function by invoking it directly instead of getting signaled bool isPrompt = false; QString line = lineIn; line.chop(1); // line has a newline if (line.startsWith("SDBG:")) line = line.mid(5); if (line.isEmpty()) // we have a prompt isPrompt = true; else if (line.startsWith("symb")) { // symbols information (locals) isPrompt = true; // paranoia if (!line.endsWith("}")) return; if (line.at(4) == '.') { // single symbol information line = line.mid(5); emit localsUpdated(line); } else { // line.at(4) == ':' // all locals line = line.mid(6); line.chop(1); emit localsUpdated(line); } } else if (line.startsWith("@line")) { // location information (interrupted) isPrompt = true; // paranoia if (!line.endsWith(":")) return; const QStringList locationParts = line.split(','); QTC_ASSERT(locationParts.size() == 3, return); fileLine = locationParts[0].mid(6).toInt(); fileColumn = locationParts[1].mid(7).toInt(); fileName = locationParts[2].trimmed(); fileName.chop(1); // remove the colon const Utils::FilePath fp = Utils::FilePath::fromUserInput(fileName); if (fp.isRelativePath()) fileName = m_currentTestCasePath.resolvePath(fileName).toUrlishString(); } else if (m_autId == 0 && line.startsWith("AUTID: ")) { isPrompt = true; m_autId = line.mid(7).toInt(); qCInfo(runnerLOG) << "AUT ID set" << m_autId << "(" << line << ")"; emit autIdRetrieved(); } if (isPrompt) emit interrupted(fileName, fileLine, fileColumn); } void SquishRunnerProcess::handleMultiLineOutput(OutputMode mode) { const QScopeGuard cleanup([this] { m_multiLineContent.clear(); m_context.clear(); }); if (mode == MultiLineProperties) { emit propertiesFetched(m_multiLineContent); } else if (mode == MultiLineChildren) { emit updateChildren(m_context, m_multiLineContent); } } void SquishRunnerProcess::onInspectorOutput(const QString &lineIn) { QString line = lineIn; line.chop(1); // line has a newline if (line.startsWith("SSPY:")) line = line.mid(5); if (line.isEmpty()) // we have a prompt, that's fine return; if (m_outputMode != SingleLine) { const OutputMode originalMode = m_outputMode; if (line.startsWith("@end")) { m_outputMode = SingleLine; if (!QTC_GUARD(line.mid(6).chopped(1) == m_context)) { // messed up output m_multiLineContent.clear(); m_context.clear(); return; } } else { m_multiLineContent.append(line); } if (m_outputMode == SingleLine) // we reached the @end handleMultiLineOutput(originalMode); return; } if (line == "@ready") return; if (line.startsWith("@picked: ")) { const QString value = line.mid(9); emit objectPicked(value); return; } if (line.startsWith("@startprop")) { m_outputMode = MultiLineProperties; m_context = line.mid(12).chopped(1); return; } if (line.startsWith("@startobj")) { m_outputMode = MultiLineChildren; m_context = line.mid(11).chopped(1); return; } if (line.contains("license acquisition")) { emit logOutputReceived("Inspect: " + line); return; } // qDebug() << "unhandled" << line; } static QString cmdToString(SquishRunnerProcess::RunnerCommand cmd) { switch (cmd) { case SquishRunnerProcess::Continue: return "continue\n"; case SquishRunnerProcess::EndRecord: return "endrecord\n"; case SquishRunnerProcess::Exit: return "exit\n"; case SquishRunnerProcess::Next: return "next\n"; case SquishRunnerProcess::Pick: return "pick\n"; case SquishRunnerProcess::PrintVariables: return "print variables\n"; case SquishRunnerProcess::Return: return "return\n"; case SquishRunnerProcess::Step: return "step\n"; } return {}; } void SquishRunnerProcess::writeCommand(RunnerCommand cmd) { const QString command = cmdToString(cmd); if (!command.isEmpty()) m_process.write(command); } void SquishRunnerProcess::requestExpanded(const QString &variableName) { m_process.write("print variables +" + variableName + "\n"); } void SquishRunnerProcess::requestListObject(const QString &value) { m_process.write("list objects " + maskedArgument(value) + "\n"); } void SquishRunnerProcess::requestListProperties(const QString &value) { m_process.write("list properties " + maskedArgument(value) + "\n"); } // FIXME: add/removal of breakpoints while debugging not handled yet // FIXME: enabled state of breakpoints Utils::Links SquishRunnerProcess::setBreakpoints(const QString &scriptExtension) { Utils::Links setBPs; using namespace Debugger::Internal; const GlobalBreakpoints globalBPs = BreakpointManager::globalBreakpoints(); for (const GlobalBreakpoint &gb : globalBPs) { if (!gb->isEnabled()) continue; const Utils::FilePath filePath = Utils::FilePath::fromUserInput( gb->data(BreakpointFileColumn, Qt::DisplayRole).toString()); auto fileName = filePath.canonicalPath().toUserOutput(); if (fileName.isEmpty()) continue; if (!fileName.endsWith(scriptExtension)) continue; // mask backslashes and spaces fileName = maskedArgument(fileName); auto line = gb->data(BreakpointLineColumn, Qt::DisplayRole).toInt(); QString cmd = "break "; cmd.append(fileName); cmd.append(':'); cmd.append(QString::number(line)); cmd.append('\n'); qCInfo(runnerLOG).noquote().nospace() << "Setting breakpoint: '" << cmd << "'"; m_process.write(cmd); setBPs.append({filePath, line}); } return setBPs; } } // namespace Squish::Internal