From 4b4e591ff6722eb381bb7defdac7b4b4586b8e5a Mon Sep 17 00:00:00 2001
From: Christian Kandeler <christian.kandeler@nokia.com>
Date: Thu, 28 Oct 2010 15:03:03 +0200
Subject: [PATCH] SSH: Add test for remote process execution.

Task-number: QTCREATORBUG-2706
---
 .../ssh/remoteprocess/argumentscollector.cpp  | 129 ++++++++++++
 .../ssh/remoteprocess/argumentscollector.h    |  31 +++
 tests/manual/ssh/remoteprocess/main.cpp       |  26 +++
 .../ssh/remoteprocess/remoteprocess.pro       |   6 +
 .../ssh/remoteprocess/remoteprocesstest.cpp   | 184 ++++++++++++++++++
 .../ssh/remoteprocess/remoteprocesstest.h     |  37 ++++
 tests/manual/ssh/ssh.pro                      |   2 +-
 7 files changed, 414 insertions(+), 1 deletion(-)
 create mode 100644 tests/manual/ssh/remoteprocess/argumentscollector.cpp
 create mode 100644 tests/manual/ssh/remoteprocess/argumentscollector.h
 create mode 100644 tests/manual/ssh/remoteprocess/main.cpp
 create mode 100644 tests/manual/ssh/remoteprocess/remoteprocess.pro
 create mode 100644 tests/manual/ssh/remoteprocess/remoteprocesstest.cpp
 create mode 100644 tests/manual/ssh/remoteprocess/remoteprocesstest.h

diff --git a/tests/manual/ssh/remoteprocess/argumentscollector.cpp b/tests/manual/ssh/remoteprocess/argumentscollector.cpp
new file mode 100644
index 00000000000..87b765b4723
--- /dev/null
+++ b/tests/manual/ssh/remoteprocess/argumentscollector.cpp
@@ -0,0 +1,129 @@
+#include "argumentscollector.h"
+
+#include <iostream>
+
+using namespace std;
+using namespace Core;
+
+ArgumentsCollector::ArgumentsCollector(const QStringList &args)
+    : m_arguments(args)
+{
+}
+
+Core::SshConnectionParameters ArgumentsCollector::collect(bool &success) const
+{
+    SshConnectionParameters parameters(Core::SshConnectionParameters::NoProxy);
+    try {
+        bool authTypeGiven = false;
+        bool portGiven = false;
+        bool timeoutGiven = false;
+        bool proxySettingGiven = false;
+        int pos;
+        int port;
+        for (pos = 1; pos < m_arguments.count() - 1; ++pos) {
+            if (checkAndSetStringArg(pos, parameters.host, "-h")
+                || checkAndSetStringArg(pos, parameters.uname, "-u"))
+                continue;
+            if (checkAndSetIntArg(pos, port, portGiven, "-p")
+                || checkAndSetIntArg(pos, parameters.timeout, timeoutGiven, "-t"))
+                continue;
+            if (checkAndSetStringArg(pos, parameters.pwd, "-pwd")) {
+                if (!parameters.privateKeyFile.isEmpty())
+                    throw ArgumentErrorException(QLatin1String("-pwd and -k are mutually exclusive."));
+                parameters.authType
+                    = SshConnectionParameters::AuthByPwd;
+                authTypeGiven = true;
+                continue;
+            }
+            if (checkAndSetStringArg(pos, parameters.privateKeyFile, "-k")) {
+                if (!parameters.pwd.isEmpty())
+                    throw ArgumentErrorException(QLatin1String("-pwd and -k are mutually exclusive."));
+                parameters.authType
+                    = SshConnectionParameters::AuthByKey;
+                authTypeGiven = true;
+                continue;
+            }
+            if (!checkForNoProxy(pos, parameters.proxyType, proxySettingGiven))
+                throw ArgumentErrorException(QLatin1String("unknown option ") + m_arguments.at(pos));
+        }
+
+        Q_ASSERT(pos <= m_arguments.count());
+        if (pos == m_arguments.count() - 1) {
+            if (!checkForNoProxy(pos, parameters.proxyType, proxySettingGiven))
+                throw ArgumentErrorException(QLatin1String("unknown option ") + m_arguments.at(pos));
+        }
+
+        if (!authTypeGiven)
+            throw ArgumentErrorException(QLatin1String("No authentication argument given."));
+        if (parameters.host.isEmpty())
+            throw ArgumentErrorException(QLatin1String("No host given."));
+        if (parameters.uname.isEmpty())
+            throw ArgumentErrorException(QLatin1String("No user name given."));
+
+        parameters.port = portGiven ? port : 22;
+        if (!timeoutGiven)
+            parameters.timeout = 30;
+        success = true;
+    } catch (ArgumentErrorException &ex) {
+        cerr << "Error: " << qPrintable(ex.error) << endl;
+        printUsage();
+        success = false;
+    }
+    return parameters;
+}
+
+void ArgumentsCollector::printUsage() const
+{
+    cerr << "Usage: " << qPrintable(m_arguments.first())
+        << " -h <host> -u <user> "
+        << "-pwd <password> | -k <private key file> [ -p <port> ] "
+        << "[ -t <timeout> ] [ -no-proxy ]" << endl;
+}
+
+bool ArgumentsCollector::checkAndSetStringArg(int &pos, QString &arg, const char *opt) const
+{
+    if (m_arguments.at(pos) == QLatin1String(opt)) {
+        if (!arg.isEmpty()) {
+            throw ArgumentErrorException(QLatin1String("option ") + opt
+                + QLatin1String(" was given twice."));
+        }
+        arg = m_arguments.at(++pos);
+        if (arg.isEmpty() && QLatin1String(opt) != QLatin1String("-pwd"))
+            throw ArgumentErrorException(QLatin1String("empty argument not allowed here."));
+        return true;
+    }
+    return false;
+}
+
+bool ArgumentsCollector::checkAndSetIntArg(int &pos, int &val,
+    bool &alreadyGiven, const char *opt) const
+{
+    if (m_arguments.at(pos) == QLatin1String(opt)) {
+        if (alreadyGiven) {
+            throw ArgumentErrorException(QLatin1String("option ") + opt
+                + QLatin1String(" was given twice."));
+        }
+        bool isNumber;
+        val = m_arguments.at(++pos).toInt(&isNumber);
+        if (!isNumber) {
+            throw ArgumentErrorException(QLatin1String("option ") + opt
+                 + QLatin1String(" needs integer argument"));
+        }
+        alreadyGiven = true;
+        return true;
+    }
+    return false;
+}
+
+bool ArgumentsCollector::checkForNoProxy(int &pos,
+    SshConnectionParameters::ProxyType &type, bool &alreadyGiven) const
+{
+    if (m_arguments.at(pos) == QLatin1String("-no-proxy")) {
+        if (alreadyGiven)
+            throw ArgumentErrorException(QLatin1String("proxy setting given twice."));
+        type = SshConnectionParameters::NoProxy;
+        alreadyGiven = true;
+        return true;
+    }
+    return false;
+}
diff --git a/tests/manual/ssh/remoteprocess/argumentscollector.h b/tests/manual/ssh/remoteprocess/argumentscollector.h
new file mode 100644
index 00000000000..f7ba749a4bb
--- /dev/null
+++ b/tests/manual/ssh/remoteprocess/argumentscollector.h
@@ -0,0 +1,31 @@
+#ifndef ARGUMENTSCOLLECTOR_H
+#define ARGUMENTSCOLLECTOR_H
+
+#include <coreplugin/ssh/sshconnection.h>
+
+#include <QtCore/QStringList>
+
+class ArgumentsCollector
+{
+public:
+    ArgumentsCollector(const QStringList &args);
+    Core::SshConnectionParameters collect(bool &success) const;
+private:
+    struct ArgumentErrorException
+    {
+        ArgumentErrorException(const QString &error) : error(error) {}
+        const QString error;
+    };
+
+    void printUsage() const;
+    bool checkAndSetStringArg(int &pos, QString &arg, const char *opt) const;
+    bool checkAndSetIntArg(int &pos, int &val, bool &alreadyGiven,
+        const char *opt) const;
+    bool checkForNoProxy(int &pos,
+        Core::SshConnectionParameters::ProxyType &type,
+        bool &alreadyGiven) const;
+
+    const QStringList m_arguments;
+};
+
+#endif // ARGUMENTSCOLLECTOR_H
diff --git a/tests/manual/ssh/remoteprocess/main.cpp b/tests/manual/ssh/remoteprocess/main.cpp
new file mode 100644
index 00000000000..e2c4afcd2d9
--- /dev/null
+++ b/tests/manual/ssh/remoteprocess/main.cpp
@@ -0,0 +1,26 @@
+#include "argumentscollector.h"
+#include "remoteprocesstest.h"
+
+#include <coreplugin/ssh/sshconnection.h>
+
+#include <QtCore/QCoreApplication>
+#include <QtCore/QObject>
+#include <QtCore/QStringList>
+
+#include <cstdlib>
+#include <iostream>
+
+using namespace Core;
+
+int main(int argc, char *argv[])
+{
+    QCoreApplication app(argc, argv);
+    bool parseSuccess;
+    const Core::SshConnectionParameters &parameters
+        = ArgumentsCollector(app.arguments()).collect(parseSuccess);
+    if (!parseSuccess)
+        return EXIT_FAILURE;
+    RemoteProcessTest remoteProcessTest(parameters);
+    remoteProcessTest.run();
+    return app.exec();
+}
diff --git a/tests/manual/ssh/remoteprocess/remoteprocess.pro b/tests/manual/ssh/remoteprocess/remoteprocess.pro
new file mode 100644
index 00000000000..96c272268a3
--- /dev/null
+++ b/tests/manual/ssh/remoteprocess/remoteprocess.pro
@@ -0,0 +1,6 @@
+include(../ssh.pri)
+
+TARGET=remoteprocess
+SOURCES=main.cpp remoteprocesstest.cpp argumentscollector.cpp
+HEADERS=remoteprocesstest.h argumentscollector.h
+
diff --git a/tests/manual/ssh/remoteprocess/remoteprocesstest.cpp b/tests/manual/ssh/remoteprocess/remoteprocesstest.cpp
new file mode 100644
index 00000000000..bb61b1cb58e
--- /dev/null
+++ b/tests/manual/ssh/remoteprocess/remoteprocesstest.cpp
@@ -0,0 +1,184 @@
+#include "remoteprocesstest.h"
+
+#include <QtCore/QCoreApplication>
+#include <QtCore/QTimer>
+
+#include <iostream>
+
+using namespace Core;
+
+RemoteProcessTest::RemoteProcessTest(const SshConnectionParameters &params)
+    : m_timeoutTimer(new QTimer(this)),
+      m_remoteRunner(SshRemoteProcessRunner::create(params)),
+      m_state(Inactive)
+{
+    m_timeoutTimer->setInterval(5000);
+    connect(m_timeoutTimer, SIGNAL(timeout()), SLOT(handleTimeout()));
+}
+
+RemoteProcessTest::~RemoteProcessTest() { }
+
+void RemoteProcessTest::run()
+{
+    connect(m_remoteRunner.data(), SIGNAL(connectionError(Core::SshError)),
+        SLOT(handleConnectionError()));
+    connect(m_remoteRunner.data(), SIGNAL(processStarted()),
+        SLOT(handleProcessStarted()));
+    connect(m_remoteRunner.data(), SIGNAL(processOutputAvailable(QByteArray)),
+        SLOT(handleProcessStdout(QByteArray)));
+    connect(m_remoteRunner.data(),
+        SIGNAL(processErrorOutputAvailable(QByteArray)),
+        SLOT(handleProcessStderr(QByteArray)));
+    connect(m_remoteRunner.data(), SIGNAL(processClosed(int)),
+        SLOT(handleProcessClosed(int)));
+
+    std::cout << "Testing successful remote process..." << std::endl;
+    m_state = TestingSuccess;
+    m_started = false;
+    m_timeoutTimer->start();
+    m_remoteRunner->run("ls -a /tmp");
+}
+
+void RemoteProcessTest::handleConnectionError()
+{
+    std::cerr << "Error: Connection failure ("
+        << qPrintable(m_remoteRunner->connection()->errorString()) << ")."
+        << std::endl;
+    qApp->quit();
+}
+
+void RemoteProcessTest::handleProcessStarted()
+{
+    if (m_started) {
+        std::cerr << "Error: Received started() signal again." << std::endl;
+        qApp->quit();
+    } else {
+        m_started = true;
+        if (m_state == TestingCrash) {
+            Core::SshRemoteProcess::Ptr killer
+                = m_remoteRunner->connection()->createRemoteProcess("pkill -9 sleep");
+            killer->start();
+        }
+    }
+}
+
+void RemoteProcessTest::handleProcessStdout(const QByteArray &output)
+{
+    if (!m_started) {
+        std::cerr << "Error: Remote output from non-started process."
+            << std::endl;
+        qApp->quit();
+    } else if (m_state != TestingSuccess) {
+        std::cerr << "Error: Got remote standard output in state " << m_state
+            << "." << std::endl;
+        qApp->quit();
+    } else {
+        m_remoteStdout += output;
+    }
+}
+
+void RemoteProcessTest::handleProcessStderr(const QByteArray &output)
+{
+    if (!m_started) {
+        std::cerr << "Error: Remote error output from non-started process."
+            << std::endl;
+        qApp->quit();
+    } else if (m_state == TestingSuccess) {
+        std::cerr << "Error: Unexpected remote standard error output."
+            << std::endl;
+        qApp->quit();
+    } else {
+        m_remoteStderr += output;
+    }
+}
+
+void RemoteProcessTest::handleProcessClosed(int exitStatus)
+{
+    switch (exitStatus) {
+    case SshRemoteProcess::ExitedNormally:
+        if (!m_started) {
+            std::cerr << "Error: Process exited without starting." << std::endl;
+            qApp->quit();
+            return;
+        }
+        switch (m_state) {
+        case TestingSuccess: {
+            const int exitCode = m_remoteRunner->process()->exitCode();
+            if (exitCode != 0) {
+                std::cerr << "Error: exit code is " << exitCode
+                    << ", expected zero." << std::endl;
+                qApp->quit();
+                return;
+            }
+            if (m_remoteStdout.isEmpty()) {
+                std::cerr << "Error: Command did not produce output."
+                    << std::endl;
+                qApp->quit();
+                return;
+            }
+
+            std::cout << "Ok. Testing unsuccessful remote process..."
+                << std::endl;
+            m_state = TestingFailure;
+            m_started = false;
+            m_timeoutTimer->start();
+            m_remoteRunner->run("ls /wedontexepectsuchafiletoexist");
+            break;
+        }
+        case TestingFailure: {
+            const int exitCode = m_remoteRunner->process()->exitCode();
+            if (exitCode == 0) {
+                std::cerr << "Error: exit code is zero, expected non-zero."
+                    << std::endl;
+                qApp->quit();
+                return;
+            }
+            if (m_remoteStderr.isEmpty()) {
+                std::cerr << "Error: Command did not produce error output."
+                    << std::endl;
+                qApp->quit();
+                return;
+            }
+
+            std::cout << "Ok. Testing crashing remote process..."
+                << std::endl;
+            m_state = TestingCrash;
+            m_started = false;
+            m_timeoutTimer->start();
+            m_remoteRunner->run("sleep 100");
+            break;
+        }
+        case TestingCrash:
+            std::cerr << "Error: Successful exit from process that was "
+                "supposed to crash." << std::endl;
+            qApp->quit();
+            return;
+        case Inactive:
+            Q_ASSERT(false);
+        }
+        break;
+    case SshRemoteProcess::FailedToStart:
+        if (m_started) {
+            std::cerr << "Error: Got 'failed to start' signal for process "
+                "that has not started yet." << std::endl;
+        } else {
+            std::cerr << "Error: Process failed to start." << std::endl;
+        }
+        qApp->quit();
+        break;
+    case SshRemoteProcess::KilledBySignal:
+        if (m_state != TestingCrash) {
+            std::cerr << "Error: Unexpected crash." << std::endl;
+            qApp->quit();
+            return;
+        }
+        std::cout << "Ok. All tests succeeded." << std::endl;
+        qApp->quit();
+    }
+}
+
+void RemoteProcessTest::handleTimeout()
+{
+    std::cerr << "Error: Timeout waiting for progress." << std::endl;
+    qApp->quit();
+}
diff --git a/tests/manual/ssh/remoteprocess/remoteprocesstest.h b/tests/manual/ssh/remoteprocess/remoteprocesstest.h
new file mode 100644
index 00000000000..a9a2219c7e5
--- /dev/null
+++ b/tests/manual/ssh/remoteprocess/remoteprocesstest.h
@@ -0,0 +1,37 @@
+#ifndef SFTPTEST_H
+#define SFTPTEST_H
+
+#include <coreplugin/ssh/sshremoteprocessrunner.h>
+
+QT_FORWARD_DECLARE_CLASS(QTimer);
+#include <QtCore/QObject>
+
+class RemoteProcessTest : public QObject
+{
+    Q_OBJECT
+public:
+    RemoteProcessTest(const Core::SshConnectionParameters &params);
+    ~RemoteProcessTest();
+    void run();
+
+private slots:
+    void handleConnectionError();
+    void handleProcessStarted();
+    void handleProcessStdout(const QByteArray &output);
+    void handleProcessStderr(const QByteArray &output);
+    void handleProcessClosed(int exitStatus);
+    void handleTimeout();
+
+private:
+    enum State { Inactive, TestingSuccess, TestingFailure, TestingCrash };
+
+    QTimer * const m_timeoutTimer;
+    const Core::SshRemoteProcessRunner::Ptr m_remoteRunner;
+    QByteArray m_remoteStdout;
+    QByteArray m_remoteStderr;
+    State m_state;
+    bool m_started;
+};
+
+
+#endif // SFTPTEST_H
diff --git a/tests/manual/ssh/ssh.pro b/tests/manual/ssh/ssh.pro
index 432f8f192b5..a722b3d543e 100644
--- a/tests/manual/ssh/ssh.pro
+++ b/tests/manual/ssh/ssh.pro
@@ -5,4 +5,4 @@
 #-------------------------------------------------
 
 TEMPLATE = subdirs
-SUBDIRS = errorhandling sftp
+SUBDIRS = errorhandling sftp remoteprocess
-- 
GitLab