/**************************************************************************** ** ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). ** Contact: Qt Software Information (qt-info@nokia.com) ** ** This file is part of the BM project on Qt Labs. ** ** This file may be used under the terms of the GNU General Public ** License version 2.0 or 3.0 as published by the Free Software Foundation ** and appearing in the file LICENSE.GPL included in the packaging of ** this file. Please review the following information to ensure GNU ** General Public Licensing requirements will be met: ** http://www.fsf.org/licensing/licenses/info/GPLv2.html and ** http://www.gnu.org/copyleft/gpl.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at qt-sales@nokia.com. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ****************************************************************************/ #include #include #include #include "bm.h" #include "bmrequest.h" #include "bmmisc.h" class Connection { public: Connection(const QString &serverHost, quint16 serverPort) : serverHost(serverHost), serverPort(serverPort), clientConn(0) {} ~Connection() { if (clientConn) delete clientConn; } bool sendRequest( BMRequest *request, BMRequest::OutputFormat outputFormat, const QStringList &args) { if (!clientConn) { clientConn = new BMClientConnection(serverHost, serverPort, outputFormat, args); if (!clientConn->connect()) { lastError_ = "failed to connect to server"; return false; } qApp->connect(clientConn, SIGNAL(replyDone()), SLOT(quit())); } return clientConn->sendRequest(request); } QString lastError() const { return clientConn ? clientConn->lastError() : lastError_; } private: QString serverHost; quint16 serverPort; BMClientConnection *clientConn; QString lastError_; }; static bool timeRangeIsValid(const QString ×tamp1, const QString ×tamp2) { bool ok1; timestamp1.toInt(&ok1); bool ok2; timestamp2.toInt(&ok2); return (ok1 || (timestamp1 == "first")) && (ok2 || (timestamp2 == "last")); } static bool getRanking(const QStringList &args, int pos, QString *ranking, QString *error) { *ranking = args.at(pos); if ((*ranking != "worst") && (*ranking != "best") && (*ranking != "absolute")) { *error = QString("invalid ranking: >%1<").arg(*ranking); return false; } return true; } static bool getScope(const QStringList &args, int pos, QString *scope, QString *error) { *scope = args.at(pos); if ((*scope != "global") && (*scope != "testFunction")) { *error = QString("invalid scope: >%1<").arg(*scope); return false; } return true; } class Executor { public: Executor() : useExplicitOutputFormat(false) {} virtual ~Executor() {} int exec() const; private: bool ensureServerOption(QStringList *args, QString *error) const; bool getServerLocation( const QStringList &args, QString *host, quint16 *port, QString *error) const; BMRequest * createRequest(const QStringList &args, QString *error) const; BMRequest * createGetHistoryRequest(const QStringList &args, QString *error) const; BMRequest * createGetHistory2Request(const QStringList &args, QString *error) const; BMRequest * createIndexGetValuesRequest( const QStringList &args, QString *error, const QString &command = "index get values") const; BMRequest * createIndexPutConfigRequest(const QStringList &args, QString *error) const; BMRequest * createGetHistoriesRequest(const QStringList &args, QString *error) const; BMRequest * createGetIXHistoriesRequest(const QStringList &args, QString *error) const; BMRequest * createASFStatsGetValuesRequest( const QStringList &args, QString *error, const QString &command = "asfstats get values") const; BMRequest * createASFStatsGetValues2Request( const QStringList &args, QString *error, const QString &command = "asfstats get values2") const; mutable BMRequest::OutputFormat explicitOutputFormat; mutable bool useExplicitOutputFormat; BMRequest::OutputFormat outputFormat() const; void setOutputFormat(BMRequest::OutputFormat format) const { explicitOutputFormat = format; useExplicitOutputFormat = true; } protected: // Prints a help message if requested. Returns true iff a help message was printed. virtual bool printHelp() const { return false; } // Extracts the overall argument list for the program. // The first argument (pos 0) is assumed to contain the name of the executable, // and any -server option is assumed to occupy the the next two arguments (pos 1-2) // Returns true iff the argument extraction succeeds. virtual bool getArgs(QStringList *args, QString *error) const = 0; // Executes a local command (not involving sending a request to the server) if requested. // The first argument (pos 0) is assumed to contain the name of the executable. // Returns true iff a request for a local command was detected (whether it was executed // successfully or not). virtual bool execLocal(const QStringList &args, int *exitCode) const { Q_UNUSED(args); Q_UNUSED(exitCode); return false; } virtual void reportError(const QString &error) const = 0; virtual BMRequest::OutputFormat defaultOutputFormat() const = 0; }; int Executor::exec() const { if (printHelp()) return 0; QString error; QStringList args; if (!getArgs(&args, &error)) { reportError(QString("failed to extract arguments: %1").arg(error)); return 1; } int exitCode; if (execLocal(args, &exitCode)) return exitCode; if (!ensureServerOption(&args, &error)) { reportError(QString("failed to establish server arguments: %1").arg(error)); return 1; } QString serverHost; quint16 serverPort; if (!getServerLocation(args, &serverHost, &serverPort, &error)) { reportError(QString("failed to extract server location: %1").arg(error)); return 1; } BMRequest *request = createRequest(args.mid(3), &error); if (!request) { reportError(QString("failed to create request: %1").arg(error)); return 1; } Connection connection(serverHost, serverPort); if (!connection.sendRequest(request, outputFormat(), args)) { reportError(QString("failed to send request: %1").arg(connection.lastError())); return 1; } return qApp->exec(); } BMRequest::OutputFormat Executor::outputFormat() const { if (useExplicitOutputFormat) return explicitOutputFormat; return defaultOutputFormat(); } static bool getDefaultServer(QString *server, QString *error = 0) { QStringList sysenv = QProcess::systemEnvironment(); QRegExp rx = QRegExp("^BMSERVER=(\\S+)$"); int pos; if ((pos = sysenv.indexOf(rx)) != -1) { rx.indexIn(sysenv.at(pos)); *server = rx.cap(1); return true; } if (error) *error = QString("BMSERVER environment variable not set"); return false; } static bool extractResultsFromStdin(QString *results, QString *error) { QFile file; if (!file.open(stdin, QIODevice::ReadOnly)) { *error = QString("failed to open stdin for reading results"); return false; } int line; int col; QString errorMsg; QDomDocument doc; if (doc.setContent(&file, &errorMsg, &line, &col) == false) { *error = QString("failed to parse results structure at line %1, column %2: %3") .arg(line).arg(col).arg(errorMsg).toLatin1().data(); return false; } QDomNodeList testCaseNodes = doc.elementsByTagName("TestCase"); if (testCaseNodes.size() != 1) { *error = QString("results structure doesn't contain exactly one element"); return false; } QDomElement testCaseElem = testCaseNodes.at(0).toElement(); const QString testCase = testCaseElem.attributeNode("name").value(); if (testCase.isEmpty()) { *error = QString("results structure doesn't contain a test case name"); return false; } *results = QString("{\"testCase\": \"%1\", \"testFunctions\": [").arg(testCase); QDomNodeList testFunctionNodes = testCaseElem.elementsByTagName("TestFunction"); bool firstResults = true; for (int i = 0; i < testFunctionNodes.size(); ++i) { QDomElement testFunctionElem = testFunctionNodes.at(i).toElement(); const QString testFunction = testFunctionElem.attributeNode("name").value(); if (testFunction.isEmpty()) { *error = QString("results structure contains an unnamed test function"); return false; } QDomNodeList dataTagNodes = testFunctionElem.elementsByTagName("BenchmarkResult"); if (dataTagNodes.isEmpty()) continue; // note: benchmark results are optional for a test function if (!firstResults) results->append(", "); firstResults = false; results->append(QString("{\"testFunction\": \"%1\", \"results\": [").arg(testFunction)); for (int j = 0; j < dataTagNodes.size(); ++j) { QDomElement dataTagElem = dataTagNodes.at(j).toElement(); const QString dataTag = dataTagElem.attributeNode("tag").value(); const QString metric = dataTagElem.attributeNode("metric").value(); if (metric.isEmpty()) { *error = QString( "results structure contains a result with no 'metric' attribute"); return false; } bool ok; qreal value = dataTagElem.attributeNode("value").value().toDouble(&ok); if (!ok) { *error = QString( "results structure contains a result with an invalid 'value' attribute"); return false; } const qint64 iterations = dataTagElem.attributeNode("iterations").value().toLongLong(&ok); if (ok && (iterations > 0)) value /= iterations; if (j > 0) results->append(", "); results->append( QString("{\"dataTag\": \"%1\", \"metric\": \"%2\", \"value\": \"%3\"}") .arg(dataTag).arg(metric).arg(value)); } results->append("]}"); } results->append("]}"); return true; } // Ensures that \a args contains the -server option at index 1-2. If not already present, // an attempt is made to insert the option from the BMSERVER environment variable. // Returns true iff the -server option is successfully established. bool Executor::ensureServerOption(QStringList *args, QString *error) const { if ((args->size() > 1) && (args->at(1) == "-server")) { if (args->size() > 2) return true; *error = QString("-server option missing argument"); } else { QString serverArg; QString specificError; if (getDefaultServer(&serverArg, &specificError)) { Q_ASSERT(!serverArg.isEmpty()); args->insert(1, serverArg); args->insert(1, "-server"); return true; } *error = QString("no default server found:").arg(specificError); } return false; } bool Executor::getServerLocation( const QStringList &args, QString *host, quint16 *port, QString *error) const { const int pos = args.indexOf("-server"); Q_ASSERT(pos != -1); Q_ASSERT(pos < (args.size() - 1)); QRegExp rx("^(\\S+):(\\d+)$"); if (rx.indexIn(args.at(pos + 1)) == -1) { *error = QString("invalid syntax for -server option"); return false; } *host = rx.cap(1); bool ok; *port = rx.cap(2).toShort(&ok); Q_ASSERT(ok); // should already be verified by regexp return true; } BMRequest * Executor::createRequest(const QStringList &args, QString *error) const { if (args.isEmpty()) { *error = QString("empty command!"); return 0; } if ((args.size() >= 2) && (args.at(0) == "put") && (args.at(1) == "results")) { // --- 'put results' command --- if (args.size() < 6) { *error = QString("too few arguments for 'put results' command"); return 0; } const QString gitDir = (args.size() == 7) ? args.at(6) : QDir::currentPath(); return new BMRequest_PutResults( args.at(2), args.at(3), args.at(4), args.at(5), gitDir); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "metrics")) { // --- 'get metrics' command --- return new BMRequest_GetMetrics( BMMisc::getOption(args, "-platform"), BMMisc::getOption(args, "-host"), BMMisc::getOption(args, "-branch"), BMMisc::getOption(args, "-branch", 1)); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "platforms")) { // --- 'get platforms' command --- return new BMRequest_GetPlatforms( BMMisc::getOption(args, "-metric"), BMMisc::getOption(args, "-host"), BMMisc::getOption(args, "-branch"), BMMisc::getOption(args, "-branch", 1)); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "hosts")) { // --- 'get hosts' command --- return new BMRequest_GetHosts( BMMisc::getOption(args, "-metric"), BMMisc::getOption(args, "-platform"), BMMisc::getOption(args, "-branch"), BMMisc::getOption(args, "-branch", 1)); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "branches")) { // --- 'get branches' command --- return new BMRequest_GetBranches( BMMisc::getOption(args, "-metric"), BMMisc::getOption(args, "-platform"), BMMisc::getOption(args, "-host")); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "history")) { // --- 'get history' command --- return createGetHistoryRequest(args, error); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "rankedbenchmarks")) { // --- 'get rankedbenchmarks' command --- if (args.size() < 18) { *error = QString("too few arguments for 'get rankedbenchmarks' command"); return 0; } // Get time range ... QStringList values; if (!BMMisc::getOption(args, "-timerange", &values, 2, 0, error)) { if (error->isEmpty()) *error = QString("-timerange option not found"); return 0; } if (!timeRangeIsValid(values.at(0), values.at(1))) { *error = QString("invalid time range"); return 0; } const QPair timeRange = qMakePair(values.at(0), values.at(1)); // Extract difference tolerance ... bool ok; qreal diffTolerance = args.at(10).toDouble(&ok); if (!ok) { *error = QString( "failed to extract difference tolerance as a double for " "'get rankedbenchmarks' command"); return 0; } else if (diffTolerance < 0) { diffTolerance = 0.0; } // Extract stability tolerance ... int stabTolerance = args.at(11).toInt(&ok); if (!ok) { *error = QString( "failed to extract stability tolerance as an integer for " "'get rankedbenchmarks' command"); return 0; } else if (stabTolerance < 0) { stabTolerance = 0; } // Extract ranking ... QString ranking; QString specificError; if (!getRanking(args, 12, &ranking, &specificError)) { *error = QString("failed to extract ranking for 'get rankedbenchmarks' command: %1") .arg(specificError); return 0; } // Extract scope ... QString scope; if (!getScope(args, 13, &scope, &specificError)) { *error = QString("failed to extract scope for 'get rankedbenchmarks' command: %1") .arg(specificError); return 0; } // Extract max size ... int maxSize = args.at(14).toInt(&ok); if (!ok) { *error = QString( "failed to extract max size as an integer for " "'get rankedbenchmarks' command"); return 0; } else if (maxSize < 0) { maxSize = -1; } // Extract benchmark filters ... const QString testCaseFilter = args.at(15); const QString testFunctionFilter = args.at(16); const QString dataTagFilter = args.at(17); return new BMRequest_GetRankedBenchmarks( args.at(2), args.at(3), args.at(4), args.at(5), args.at(6), timeRange.first, timeRange.second, diffTolerance, stabTolerance, ranking, scope, maxSize, testCaseFilter, testFunctionFilter, dataTagFilter); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "rankedbenchmarks2")) { // --- 'get rankedbenchmarks2' command --- if (args.size() < 17) { *error = QString("too few arguments for 'get rankedbenchmarks2' command"); return 0; } // Extract difference tolerance ... bool ok; qreal diffTolerance = args.at(9).toDouble(&ok); if (!ok) { *error = QString( "failed to extract difference tolerance as a double for " "'get rankedbenchmarks2' command"); return 0; } else if (diffTolerance < 0) { diffTolerance = 0.0; } // Extract stability tolerance ... int stabTolerance = args.at(10).toInt(&ok); if (!ok) { *error = QString( "failed to extract stability tolerance as an integer for " "'get rankedbenchmarks2' command"); return 0; } else if (stabTolerance < 0) { stabTolerance = 0; } // Extract ranking ... QString ranking; QString specificError; if (!getRanking(args, 11, &ranking, &specificError)) { *error = QString("failed to extract ranking for 'get rankedbenchmarks2' command: %1") .arg(specificError); return 0; } // Extract scope ... QString scope; if (!getScope(args, 12, &scope, &specificError)) { *error = QString("failed to extract scope for 'get rankedbenchmarks2' command: %1") .arg(specificError); return 0; } // Extract max size ... int maxSize = args.at(13).toInt(&ok); if (!ok) { *error = QString( "failed to extract max size as an integer for " "'get rankedbenchmarks2' command"); return 0; } else if (maxSize < 0) { maxSize = -1; } // Extract benchmark filters ... const QString testCaseFilter = args.at(14); const QString testFunctionFilter = args.at(15); const QString dataTagFilter = args.at(16); return new BMRequest_GetRankedBenchmarks2( args.at(2), args.at(3), args.at(4), args.at(5), args.at(6), args.at(7), args.at(8), diffTolerance, stabTolerance, ranking, scope, maxSize, testCaseFilter, testFunctionFilter, dataTagFilter); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "stats")) { // --- 'get stats' command --- if (args.size() < 15) { *error = QString("too few arguments for 'get stats' command"); return 0; } // Get time range ... QStringList values; if (!BMMisc::getOption(args, "-timerange", &values, 2, 0, error)) { if (error->isEmpty()) *error = QString("-timerange option not found"); return 0; } if (!timeRangeIsValid(values.at(0), values.at(1))) { *error = QString("invalid timerange"); return 0; } const QPair timeRange = qMakePair(values.at(0), values.at(1)); // Extract difference tolerance ... bool ok; qreal diffTolerance = args.at(10).toDouble(&ok); if (!ok) { *error = QString( "failed to extract difference tolerance as a double for " "'get stats' command"); return 0; } else if (diffTolerance < 0) { diffTolerance = 0.0; } // Extract stability tolerance ... int stabTolerance = args.at(11).toInt(&ok); if (!ok) { *error = QString( "failed to extract stability tolerance as an integer for " "'get stats' command"); return 0; } else if (stabTolerance < 0) { stabTolerance = 0; } // Extract benchmark filters ... const QString testCaseFilter = args.at(12); const QString testFunctionFilter = args.at(13); const QString dataTagFilter = args.at(14); return new BMRequest_GetStats( args.at(2), args.at(3), args.at(4), args.at(5), args.at(6), timeRange.first, timeRange.second, diffTolerance, stabTolerance, testCaseFilter, testFunctionFilter, dataTagFilter); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "contexts")) { // --- 'get contexts' command --- return new BMRequest_GetContexts(); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "tccontexts")) { // --- 'get tccontexts' command --- return new BMRequest_GetTCContexts(); } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "result")) { // --- 'get result' command --- if (args.size() < 4) { *error = QString("too few arguments for 'get result' command"); return 0; } QStringList values; // Get ID ... if (!BMMisc::getOption(args, "-id", &values, 1, 0, error)) { if (error->isEmpty()) *error = QString("-id option not found"); return 0; } bool ok; const int resultId = values.first().toInt(&ok); if (!ok) { *error = QString("result ID not an integer"); return 0; } return new BMRequest_GetResult(resultId); } else if ( (args.size() >= 3) && (args.at(0) == "index") && (args.at(1) == "get") && (args.at(2) == "values")) { // --- 'index get values' command --- return createIndexGetValuesRequest(args, error); } else if ( (args.size() >= 3) && (args.at(0) == "index") && (args.at(1) == "get") && (args.at(2) == "plot")) { // --- 'index get plot' command --- BMRequest *request = createIndexGetValuesRequest(args, error, "index get plot"); setOutputFormat(BMRequest::Image); return request; } else if ( (args.size() >= 3) && (args.at(0) == "index") && (args.at(1) == "get") && (args.at(2) == "detailspage")) { // --- 'index get detailspage' command --- BMRequest *request = createIndexGetValuesRequest(args, error, "index get detailspage"); setOutputFormat(BMRequest::HTML); return request; } else if( (args.size() >= 3) && (args.at(0) == "index") && (args.at(1) == "get") && (args.at(2) == "configs")) { // --- 'index get configs' command --- return new BMRequest_IndexGetConfigs(); } else if( (args.size() >= 3) && (args.at(0) == "index") && (args.at(1) == "get") && (args.at(2) == "config")) { // --- 'index get config' command --- QStringList values; // Get name ... if (!BMMisc::getOption(args, "-name", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-name option not found"; return 0; } const QString configName = values.first().trimmed(); if (configName.isEmpty()) { *error = "empty config name"; return 0; } return new BMRequest_IndexGetConfig(configName); } else if( (args.size() >= 3) && (args.at(0) == "index") && (args.at(1) == "put") && (args.at(2) == "config")) { // --- 'index put config' command --- return createIndexPutConfigRequest(args, error); } else if( (args.size() >= 3) && (args.at(0) == "index") && (args.at(1) == "delete") && (args.at(2) == "config")) { // --- 'index delete config' command --- QStringList values; // Get name ... if (!BMMisc::getOption(args, "-name", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-name option not found"; return 0; } const QString configName = values.first().trimmed(); if (configName.isEmpty()) { *error = "empty config name"; return 0; } return new BMRequest_IndexDeleteConfig(configName); } else if ( (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "histories") && (args.at(2) != "plot") && (args.at(2) != "detailspage")) { // --- 'get histories' command --- return createGetHistoriesRequest(args, error); } else if ( (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "histories") && (args.at(2) == "plot")) { // --- 'get histories plot' command --- BMRequest *request = createGetHistoriesRequest(args, error); setOutputFormat(BMRequest::Image); return request; } else if ( (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "histories") && (args.at(2) == "detailspage")) { // --- 'get histories detailspage' command --- BMRequest *request = createGetHistoriesRequest(args, error); setOutputFormat(BMRequest::HTML); return request; } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "bmtree")) { // --- 'get bmtree' command --- return new BMRequest_GetBMTree(); } else if ( (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "ixhistories") && (args.at(2) != "plot") && (args.at(2) != "detailspage")) { // --- 'get ixhistories' command --- return createGetIXHistoriesRequest(args, error); } else if ( (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "ixhistories") && (args.at(2) == "plot")) { // --- 'get ixhistories plot' command --- BMRequest *request = createGetIXHistoriesRequest(args, error); setOutputFormat(BMRequest::Image); return request; } else if ( (args.size() >= 3) && (args.at(0) == "get") && (args.at(1) == "ixhistories") && (args.at(2) == "detailspage")) { // --- 'get ixhistories detailspage' command --- BMRequest *request = createGetIXHistoriesRequest(args, error); setOutputFormat(BMRequest::HTML); return request; } else if ( (args.size() >= 3) && (args.at(0) == "asfstats") && (args.at(1) == "get") && (args.at(2) == "values")) { // --- 'asfstats get values' command --- return createASFStatsGetValuesRequest(args, error); } else if ( (args.size() >= 3) && (args.at(0) == "asfstats") && (args.at(1) == "get") && (args.at(2) == "detailspage")) { // --- 'asfstats get detailspage' command --- BMRequest *request = createASFStatsGetValuesRequest(args, error, "asfstats get detailspage"); setOutputFormat(BMRequest::HTML); return request; } else if ( (args.size() >= 3) && (args.at(0) == "asfstats") && (args.at(1) == "get") && (args.at(2) == "values2")) { // --- 'asfstats get values2' command --- return createASFStatsGetValues2Request(args, error); } else if ( (args.size() >= 3) && (args.at(0) == "asfstats") && (args.at(1) == "get") && (args.at(2) == "detailspage2")) { // --- 'asfstats get detailspage2' command --- BMRequest *request = createASFStatsGetValues2Request(args, error, "asfstats get detailspage2"); setOutputFormat(BMRequest::HTML); return request; } else if ( (args.size() >= 3) && (args.at(0) == "asfstats") && (args.at(1) == "get") && (args.at(2) == "plot2")) { // --- 'asfstats get plot2' command --- BMRequest *request = createASFStatsGetValues2Request(args, error, "asfstats get plot2"); setOutputFormat(BMRequest::Image); return request; } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "detailspage")) { // --- 'get detailspage' command --- BMRequest *request = createGetHistoryRequest(args, error); setOutputFormat(BMRequest::HTML); return request; } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "historyplot")) { // --- 'get historyplot' command --- // ### NOTE: 'get historyplot' is normally invoked by the browser to load an tag // in the HTML returned by the 'get detailspage' command. // The data retrieved by the latter should somehow be kept/cached instead of being // retrieved over again like we currently do. // (see also 'get historyplot2') // ====> Implement this in the same fashion as for 'index get plot2' ... 2 B DONE! BMRequest *request = createGetHistoryRequest(args, error); setOutputFormat(BMRequest::HTML); return request; } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "detailspage2")) { // --- 'get detailspage2' command --- BMRequest *request = createGetHistory2Request(args, error); setOutputFormat(BMRequest::HTML); return request; } else if ((args.size() >= 2) && (args.at(0) == "get") && (args.at(1) == "historyplot2")) { // --- 'get historyplot2' command --- // ### NOTE: (see 'get historyplot') BMRequest *request = createGetHistory2Request(args, error); setOutputFormat(BMRequest::HTML); return request; } *error = QString("invalid command: %1").arg(args.join(" ")); return 0; } BMRequest * Executor::createGetHistoryRequest(const QStringList &args, QString *error) const { if (args.size() < 18) { *error = QString("too few arguments for 'get history' command"); return 0; } // Get time range ... QStringList values; if (!BMMisc::getOption(args, "-timerange", &values, 2, 0, error)) { if (error->isEmpty()) *error = QString("-timerange option not found"); return 0; } if (!timeRangeIsValid(values.at(0), values.at(1))) { *error = QString("invalid time range"); return 0; } const QPair timeRange = qMakePair(values.at(0), values.at(1)); // Extract difference tolerance ... bool ok; qreal diffTolerance = args.at(13).toDouble(&ok); if (!ok) { *error = QString( "failed to extract difference tolerance as a double for 'get history' command"); return 0; } else if (diffTolerance < 0) { diffTolerance = 0.0; } // Extract stability tolerance ... int stabTolerance = args.at(14).toInt(&ok); if (!ok) { *error = QString( "failed to extract stability tolerance as an integer for 'get history' command"); return 0; } else if (stabTolerance < 0) { stabTolerance = 0; } // Extract max size ... int maxSize = args.at(15).toInt(&ok); if (!ok) { *error = QString( "failed to extract max size as an integer for 'get history' command"); return 0; } else if (maxSize < 0) { maxSize = -1; } // Extract current timestamp ... int currTimestamp = args.at(17).toInt(&ok); if ((!ok) || (currTimestamp < 0)) { currTimestamp = -1; } return new BMRequest_GetHistory( args.at(2), args.at(3), args.at(4), args.at(5), args.at(6), args.at(7), args.at(8), args.at(9), timeRange.first, timeRange.second, diffTolerance, stabTolerance, maxSize, args.at(16), currTimestamp); } BMRequest * Executor::createGetHistory2Request(const QStringList &args, QString *error) const { if (args.size() < 21) { *error = QString("too few arguments for 'get history2' command"); return 0; } // Get time range ... QStringList values; if (!BMMisc::getOption(args, "-timerange", &values, 2, 0, error)) { if (error->isEmpty()) *error = QString("-timerange option not found"); return 0; } if (!timeRangeIsValid(values.at(0), values.at(1))) { *error = QString("invalid time range"); return 0; } const QPair timeRange = qMakePair(values.at(0), values.at(1)); // Extract difference tolerance ... bool ok; qreal diffTolerance = args.at(15).toDouble(&ok); if (!ok) { *error = QString( "failed to extract difference tolerance as a double for 'get history2' command"); return 0; } else if (diffTolerance < 0) { diffTolerance = 0.0; } // Extract stability tolerance ... int stabTolerance = args.at(16).toInt(&ok); if (!ok) { *error = QString( "failed to extract stability tolerance as an integer for 'get history2' command"); return 0; } else if (stabTolerance < 0) { stabTolerance = 0; } // Extract max size ... int maxSize = args.at(17).toInt(&ok); if (!ok) { *error = QString( "failed to extract max size as an integer for 'get history2' command"); return 0; } else if (maxSize < 0) { maxSize = -1; } // Extract shared time scale ... bool sharedTimeScale = bool(args.at(18).toInt(&ok)); if (!ok) { *error = QString( "failed to extract sharedTimeScale as an integer for 'get history2' command"); return 0; } // Extract current timestamp ... int currTimestamp = args.at(20).toInt(&ok); if (!ok) { *error = QString( "failed to extract current timestamp as an integer for 'get history2' command"); return 0; } else if (currTimestamp < 0) { currTimestamp = -1; } return new BMRequest_GetHistory2( args.at(2), args.at(3), args.at(4), args.at(5), args.at(6), args.at(7), args.at(8), args.at(9), args.at(10), args.at(11), timeRange.first, timeRange.second, diffTolerance, stabTolerance, maxSize, sharedTimeScale, args.at(19), currTimestamp); } BMRequest * Executor::createIndexGetValuesRequest( const QStringList &args, QString *error, const QString &command) const { Q_UNUSED(command); QStringList values; // Get base timestamp ... int baseTimestamp = -1; // default is current time if (BMMisc::getOption(args, "-basetimestamp", &values, 1, 0, error)) { bool ok; baseTimestamp = values.at(0).toInt(&ok); if (!ok) { *error = "failed to extract base timestamp as an integer"; return 0; } baseTimestamp = qMax(baseTimestamp, -1); } else if (!error->isEmpty()) { return 0; } // Get median window size ... int medianWinSize = 8; // default if (BMMisc::getOption(args, "-medianwinsize", &values, 1, 0, error)) { bool ok; medianWinSize = values.at(0).toInt(&ok); if ((!ok) || (medianWinSize < 1)) { *error = "failed to extract median window size as a positive integer"; return 0; } } else if (!error->isEmpty()) { return 0; } // Get cache key ... QString cacheKey; if (BMMisc::getOption(args, "-cachekey", &values, 1, 0, error)) { cacheKey = values.at(0).trimmed(); bool ok; cacheKey.toInt(&ok); if (!ok) { *error = "failed to extract cache key as an integer"; return 0; } } else if (!error->isEmpty()) { return 0; } // Get filters ... QStringList testCaseFilter; if (!BMMisc::getMultiOption(args, "-testcase", &testCaseFilter, error)) return 0; QStringList metricFilter; if (!BMMisc::getMultiOption(args, "-metric", &metricFilter, error)) return 0; QStringList platformFilter; if (!BMMisc::getMultiOption(args, "-platform", &platformFilter, error)) return 0; QStringList hostFilter; if (!BMMisc::getMultiOption(args, "-host", &hostFilter, error)) return 0; QStringList branchFilter; if (!BMMisc::getMultiOption(args, "-branch", &branchFilter, error)) return 0; // Get data quality stats params ... bool dataQualityStats = false; qreal dqStatsDiffTol = -1; int dqStatsStabTol = -1; if (BMMisc::hasOption(args, "-dataqualitystats")) { dataQualityStats = true; // Difference tolerance ... if (BMMisc::getOption(args, "-dqstatsdifftol", &values, 1, 0, error)) { bool ok; dqStatsDiffTol = values.at(0).toDouble(&ok); if (!ok) { *error = "failed to extract difference tolerance for data quality stats as a double"; return 0; } dqStatsDiffTol = qMax(dqStatsDiffTol, 0.0); } else { if (error->isEmpty()) *error = QString("-dqstatsdifftol option not found"); return 0; } // Stability tolerance ... if (BMMisc::getOption(args, "-dqstatsstabtol", &values, 1, 0, error)) { bool ok; dqStatsStabTol = values.at(0).toInt(&ok); if ((!ok) || (dqStatsStabTol < 2)) { *error = "failed to extract stability tolerance for data quality stats as an " "integer >= 2"; return 0; } } else { if (error->isEmpty()) *error = QString("-dqstatsstabtol option not found"); return 0; } } return new BMRequest_IndexGetValues( baseTimestamp, medianWinSize, cacheKey, testCaseFilter, metricFilter, platformFilter, hostFilter, branchFilter, dataQualityStats, dqStatsDiffTol, dqStatsStabTol); } BMRequest * Executor::createIndexPutConfigRequest(const QStringList &args, QString *error) const { QStringList values; // Get name ... if (!BMMisc::getOption(args, "-name", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-name option not found"; return 0; } const QString configName = values.first().trimmed(); if (configName.isEmpty()) { *error = "empty config name"; return 0; } // Get base timestamp ... if (!BMMisc::getOption(args, "-basetimestamp", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-basetimestamp option not found"; return 0; } bool ok; int baseTimestamp = values.at(0).toInt(&ok); if (!ok) { *error = "failed to extract base timestamp as an integer"; return 0; } baseTimestamp = qMax(baseTimestamp, -1); // Get median window size ... if (!BMMisc::getOption(args, "-medianwinsize", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-medianwinsize option not found"; return 0; } const int medianWinSize = values.at(0).toInt(&ok); if ((!ok) || (medianWinSize < 1)) { *error = "failed to extract median window size as a positive integer"; return 0; } // Get filters ... QStringList testCaseFilter; if (!BMMisc::getMultiOption(args, "-testcase", &testCaseFilter, error)) return 0; QStringList metricFilter; if (!BMMisc::getMultiOption(args, "-metric", &metricFilter, error)) return 0; QStringList platformFilter; if (!BMMisc::getMultiOption(args, "-platform", &platformFilter, error)) return 0; QStringList hostFilter; if (!BMMisc::getMultiOption(args, "-host", &hostFilter, error)) return 0; QStringList branchFilter; if (!BMMisc::getMultiOption(args, "-branch", &branchFilter, error)) return 0; return new BMRequest_IndexPutConfig( configName, baseTimestamp, medianWinSize, testCaseFilter, metricFilter, platformFilter, hostFilter, branchFilter); } BMRequest * Executor::createGetHistoriesRequest(const QStringList &args, QString *error) const { QStringList values; // Get test case ... if (!BMMisc::getOption(args, "-testcase", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-testcase option not found"; return 0; } const QString testCase = values.first().trimmed(); if (testCase.isEmpty()) { *error = "empty test case"; return 0; } // Get test function ... if (!BMMisc::getOption(args, "-testfunction", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-testfunction option not found"; return 0; } const QString testFunction = values.first().trimmed(); if (testFunction.isEmpty()) { *error = "empty test function"; return 0; } // Get data tag ... if (!BMMisc::getOption(args, "-datatag", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-datatag option not found"; return 0; } const QString dataTag = values.first().trimmed(); // note that empty data tags are allowed // Get cache key ... QString cacheKey; if (BMMisc::getOption(args, "-cachekey", &values, 1, 0, error)) { cacheKey = values.at(0).trimmed(); bool ok; cacheKey.toInt(&ok); if (!ok) { *error = "failed to extract cache key as an integer"; return 0; } } else if (!error->isEmpty()) { return 0; } return new BMRequest_GetHistories(testCase, testFunction, dataTag, cacheKey); } BMRequest * Executor::createGetIXHistoriesRequest(const QStringList &args, QString *error) const { QStringList values; bool ok; // Get evaluation timestamp ... if (!BMMisc::getOption(args, "-evaltimestamp", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-evaltimestamp option not found"; return 0; } const int evalTimestamp = values.first().toInt(&ok); if ((!ok) || (evalTimestamp < 0)) { *error = "failed to extract eval timestamp as a non-negative integer"; return 0; } // Get median window size ... if (!BMMisc::getOption(args, "-medianwinsize", &values, 1, 0, error)) { if (error->isEmpty()) *error = "-medianwinsize option not found"; return 0; } const int medianWinSize = values.first().toInt(&ok); if ((!ok) || (medianWinSize < 1)) { *error = "failed to extract median window size as a positive integer"; return 0; } // Get ranked infos ... QList rankedStrings; if (!BMMisc::getMultiOption2(args, "-rankedinfo", &rankedStrings, 5, error)) return 0; if (rankedStrings.isEmpty()) { *error = "no ranked infos specified"; return 0; } QList rankedInfos; for (int i = 0; i < rankedStrings.size(); ++i) { QStringList strings = rankedStrings.at(i); Q_ASSERT(strings.size() == 5); const int bmcontextId = strings.at(0).toInt(&ok); Q_ASSERT(ok); const int basePos = strings.at(1).toInt(&ok); Q_ASSERT(ok); const int diffPos1 = strings.at(2).toInt(&ok); Q_ASSERT(ok); const int diffPos2 = strings.at(3).toInt(&ok); Q_ASSERT(ok); const QString descr = strings.at(4); rankedInfos.append(Index::RankedInfo(bmcontextId, basePos, diffPos1, diffPos2, descr)); } // Get cache key ... QString cacheKey; if (BMMisc::getOption(args, "-cachekey", &values, 1, 0, error)) { cacheKey = values.at(0).trimmed(); bool ok; cacheKey.toInt(&ok); if (!ok) { *error = "failed to extract cache key as an integer"; return 0; } } else if (!error->isEmpty()) { return 0; } return new BMRequest_GetIXHistories(evalTimestamp, medianWinSize, rankedInfos, cacheKey); } BMRequest * Executor::createASFStatsGetValuesRequest( const QStringList &args, QString *error, const QString &command) const { Q_UNUSED(command); QStringList values; // Get median window size ... int medianWinSize = -1; if (BMMisc::getOption(args, "-medianwinsize", &values, 1, 0, error)) { bool ok; medianWinSize = values.at(0).toInt(&ok); if ((!ok) || (medianWinSize < 1)) { *error = "failed to extract median window size as a positive integer"; return 0; } } else { if (error->isEmpty()) *error = "-medianwinsize option not found"; return 0; } // Get cache key ... QString cacheKey; if (BMMisc::getOption(args, "-cachekey", &values, 1, 0, error)) { cacheKey = values.at(0).trimmed(); bool ok; cacheKey.toInt(&ok); if (!ok) { *error = "failed to extract cache key as an integer"; return 0; } } else if (!error->isEmpty()) { return 0; } // Get filters ... QStringList testCaseFilter; if (!BMMisc::getMultiOption(args, "-testcase", &testCaseFilter, error)) return 0; QStringList metricFilter; if (!BMMisc::getMultiOption(args, "-metric", &metricFilter, error)) return 0; QStringList platformFilter; if (!BMMisc::getMultiOption(args, "-platform", &platformFilter, error)) return 0; QStringList hostFilter; if (!BMMisc::getMultiOption(args, "-host", &hostFilter, error)) return 0; QStringList branchFilter; if (!BMMisc::getMultiOption(args, "-branch", &branchFilter, error)) return 0; // Get ASF-specific params ... // ... timestamps ... int fromTimestamp = -1; if (BMMisc::getOption(args, "-fromtime", &values, 1, 0, error)) { bool ok; fromTimestamp = values.at(0).toInt(&ok); if ((!ok) || (fromTimestamp < 0)) { *error = "failed to extract 'from' time as a non-negative integer"; return 0; } } else { if (error->isEmpty()) *error = QString("-fromtime option not found"); return 0; } int toTimestamp = -1; if (BMMisc::getOption(args, "-totime", &values, 1, 0, error)) { bool ok; toTimestamp = values.at(0).toInt(&ok); if ((!ok) || (toTimestamp < fromTimestamp)) { *error = QString("failed to extract 'to' time as an integer >= %1").arg(fromTimestamp); return 0; } } else { if (error->isEmpty()) *error = QString("-totime option not found"); return 0; } // ... tolerance values ... qreal diffTol = -1; if (!BMMisc::getClampedPercentageOption(args, "-difftol", &diffTol, error)) return 0; int stabTol = -1; if (BMMisc::getOption(args, "-stabtol", &values, 1, 0, error)) { bool ok; stabTol = values.at(0).toInt(&ok); if ((!ok) || (stabTol < 1)) { *error = "failed to extract stability tolerance as an integer >= 1"; return 0; } } else { if (error->isEmpty()) *error = QString("-stabtol option not found"); return 0; } qreal sfTol = -1; if (!BMMisc::getClampedPercentageOption(args, "-sftol", &sfTol, error)) return 0; qreal lfTol = -1; if (!BMMisc::getClampedPercentageOption(args, "-lftol", &lfTol, error)) return 0; qreal maxLDTol = -1; if (!BMMisc::getDoubleOption(args, "-maxldtol", &maxLDTol, error)) return 0; return new BMRequest_ASFStatsGetValues( medianWinSize, cacheKey, testCaseFilter, metricFilter, platformFilter, hostFilter, branchFilter, fromTimestamp, toTimestamp, diffTol, stabTol, sfTol, lfTol, maxLDTol); } BMRequest * Executor::createASFStatsGetValues2Request( const QStringList &args, QString *error, const QString &command) const { Q_UNUSED(command); QStringList values; // Extract BM context ID ... int bmcontextId = -1; if (BMMisc::getOption(args, "-bmcontextid", &values, 1, 0, error)) { bool ok; bmcontextId = values.at(0).toInt(&ok); if ((!ok) || (bmcontextId < 0)) { *error = "failed to extract BM context ID as a non-negative integer"; return 0; } } else { if (error->isEmpty()) *error = "-bmcontextid option not found"; return 0; } // Get cache key ... QString cacheKey; if (BMMisc::getOption(args, "-cachekey", &values, 1, 0, error)) { cacheKey = values.at(0).trimmed(); bool ok; cacheKey.toInt(&ok); if (!ok) { *error = "failed to extract cache key as an integer"; return 0; } } else if (!error->isEmpty()) { return 0; } return new BMRequest_ASFStatsGetValues2(bmcontextId, cacheKey); } // ### 2 B DOCUMENTED! static void splitQuotedArgs(const QString &arg_s, QStringList *args) { QRegExp rx1("^([^'\\s]+)\\s*"); // non-quoted word QRegExp rx2("^'([^']*)'\\s*"); // quoted word (allowing for internal white space) const QString arg_st = arg_s.trimmed(); int pos = 0; while (true) { if (rx1.indexIn(arg_st.mid(pos).trimmed()) != -1) { args->append(rx1.cap(1)); pos += rx1.matchedLength(); } else if (rx2.indexIn(arg_st.mid(pos).trimmed()) != -1) { args->append(rx2.cap(1)); pos += rx2.matchedLength(); } else { break; } } } class CGIExecutor : public Executor { bool getArgs(QStringList *args, QString *error) const { *args = QStringList() << qApp->arguments().first(); int index; QRegExp rx; QStringList sysenv = QProcess::systemEnvironment(); rx = QRegExp("^QUERY_STRING=(.+)$"); if ((index = sysenv.indexOf(rx)) == -1) { // At this point, we assume the HTTP request method is POST. return true; } // At this point, we assume the HTTP request method is GET. const QUrl url = QUrl::fromEncoded(QString("?%1").arg(rx.cap(1)).toLatin1().data()); if (!url.hasQueryItem("command")) { *error = QString("'command' not found in QUERY_STRING"); return false; } QString command = url.queryItemValue("command"); if (command.isEmpty()) { *error = QString("empty command"); return false; } splitQuotedArgs(command, args); return true; } bool execLocal(const QStringList &args, int *exitCode) const { Q_ASSERT(args.size() >= 1); *exitCode = 0; if (args.size() == 1) { // --- File contents passed via HTTP POST method --- // QFile file; // file.open(stdin, QIODevice::ReadOnly); // printf("Content-type: text/plain\n\n%s\n", file.readAll().constData()); QString results; QString reply; QString error; if (extractResultsFromStdin(&results, &error)) reply = QString("%1").arg(results.toLatin1().data()); else reply = QString("{\"error\": \"failed to extract results: %1\"}").arg(error); BMMisc::printJSONOutput(reply); return true; } if ((args.at(1) == "get") && ((args.size() == 3) && args.at(2) == "server")) { // --- 'get server' command --- QString serverArg; QString error; QString reply; if (getDefaultServer(&serverArg, &error)) reply = QString("{\"server\": \"%1\"}").arg(serverArg.toLatin1().data()); else reply = QString("{\"error\": \"default server not found: %1\"}").arg(error); BMMisc::printJSONOutput(reply); return true; } *exitCode = 1; return false; } void reportError(const QString &error) const { QString s = QString("{\n\"error\": \"%1\"\n}").arg(error); // appendEnvironment(&s); BMMisc::printJSONOutput(s); } BMRequest::OutputFormat defaultOutputFormat() const { return BMRequest::JSON; } }; static bool extractValue( qreal *value, QString *error, const QString &testCase, const QString &testFunction, const QString &dataTag, const QString &metric, const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { *error = QString("failed to open results file"); return false; } int line; int col; QString errorMsg; QDomDocument doc; if (doc.setContent(&file, &errorMsg, &line, &col) == false) { *error = QString("failed to parse results file at line %1, column %2: %3") .arg(line).arg(col).arg(errorMsg).toLatin1().data(); return false; } QDomNodeList testCaseNodes = doc.elementsByTagName("TestCase"); if (testCaseNodes.size() != 1) { *error = QString("results file doesn't contain exactly one element"); return false; } QDomElement testCaseElem = testCaseNodes.at(0).toElement(); const QString testCase_ = testCaseElem.attributeNode("name").value(); if (testCase_.isEmpty()) { *error = QString("results file doesn't contain a test case name"); return false; } if (testCase_ != testCase) { *error = QString("test case mismatch in results file: '%1' != '%2'") .arg(testCase_).arg(testCase); return false; } QDomNodeList testFunctionNodes = testCaseElem.elementsByTagName("TestFunction"); if (testFunctionNodes.isEmpty()) { *error = QString("results file doesn't contain any test functions"); return false; } bool matchFound = false; for (int i = 0; i < testFunctionNodes.size() && !matchFound; ++i) { QDomElement testFunctionElem = testFunctionNodes.at(i).toElement(); const QString testFunction_ = testFunctionElem.attributeNode("name").value(); if (testFunction_.isEmpty()) { *error = QString("results file contains an unnamed test function"); return false; } QDomNodeList dataTagNodes = testFunctionElem.elementsByTagName("BenchmarkResult"); if (dataTagNodes.isEmpty()) continue; // note: benchmark results are optional for a test function for (int j = 0; j < dataTagNodes.size() && !matchFound; ++j) { QDomElement dataTagElem = dataTagNodes.at(j).toElement(); const QString dataTag_ = dataTagElem.attributeNode("tag").value(); const QString metric_ = dataTagElem.attributeNode("metric").value(); if (metric_.isEmpty()) { *error = QString("results file contains a result with no 'metric' attribute"); return false; } bool ok; qreal value_ = dataTagElem.attributeNode("value").value().toDouble(&ok); if (!ok) { *error = QString( "results file contains a result with an invalid 'value' attribute"); return false; } const qint64 iterations = dataTagElem.attributeNode("iterations").value().toLongLong(&ok); if (ok && (iterations > 0)) value_ /= iterations; // Check for match ... if ((testFunction_ == testFunction) && (dataTag_ == dataTag) && (metric_ == metric)) { *value = value_; matchFound = true; } } } if (!matchFound) { *error = QString( "results file contains no result matching the given testCase/testFunction/" "dataTag/metric combination"); return false; } return true; } class DirectExecutor : public Executor { bool getArgs(QStringList *args, QString *error) const { Q_UNUSED(error); *args = qApp->arguments(); return true; } bool printHelp() const { const QStringList args = qApp->arguments(); Q_ASSERT(args.size() > 1); if (args.at(1).endsWith("help")) { printUsage(args.first()); return true; } return false; } bool execLocal(const QStringList &args, int *exitCode) const { Q_ASSERT(args.size() > 1); if ((args.at(1) == "get") && ((args.size() > 2) && args.at(2) == "value")) { // --- 'get value' command --- if (args.size() < 8) { qDebug() << "too few arguments for 'get value' command"; *exitCode = 1; } else { qreal value = 0; QString error; if (!extractValue( &value, &error, args.at(3), args.at(4), args.at(5), args.at(6), args.at(7))) { qDebug() << QString("failed to extract value: %1") .arg(error).toLatin1().data(); *exitCode = 1; } else { printf("%g\n", value); *exitCode = 0; } } return true; } else if ((args.at(1) == "get") && ((args.size() == 3) && args.at(2) == "server")) { // --- 'get server' command --- QString serverArg; if (getDefaultServer(&serverArg)) { Q_ASSERT(!serverArg.isEmpty()); printf("%s\n", serverArg.toLatin1().data()); } *exitCode = 0; return true; } return false; } void reportError(const QString &error) const { qDebug() << "error:" << error.toLatin1().data(); } BMRequest::OutputFormat defaultOutputFormat() const { return BMRequest::Raw; } void printUsage(const QString &execName) const { qDebug().nospace() << "usage: " << execName.toLatin1().data() << " {[-server :] COMMAND} | help\n\n" << "where COMMAND is one of the following:\n\n" << "put results []\n" << "get metrics [-platform ] [-host ] \\\n" " [-branch ]\n" << "get platforms [-metric ] [-host ] \\\n" " [-branch ]\n" << "get hosts [-metric ] [-platform ] \\\n" " [-branch ]\n" << "get branches [-metric ] [-platform ] [-host ]\n" << "get history " " \\\n" " -timerange \\\n" " \\\n" "