Przeglądaj źródła

Added support for reporting results and updating output parameter values.

Sascha Zelzer 13 lat temu
rodzic
commit
79795e377f

+ 7 - 0
Libs/CommandLineModules/Backend/LocalProcess/ctkCmdLineModuleProcessWatcher.cpp

@@ -39,6 +39,7 @@ ctkCmdLineModuleProcessWatcher::ctkCmdLineModuleProcessWatcher(QProcess& process
 
   connect(&processXmlWatcher, SIGNAL(filterStarted(QString,QString)), SLOT(filterStarted(QString,QString)));
   connect(&processXmlWatcher, SIGNAL(filterProgress(float)), SLOT(filterProgress(float)));
+  connect(&processXmlWatcher, SIGNAL(filterResult(QString,QString)), SLOT(filterResult(QString,QString)));
   connect(&processXmlWatcher, SIGNAL(filterFinished(QString)), SLOT(filterFinished(QString)));
   connect(&processXmlWatcher, SIGNAL(filterXmlError(QString)), SLOT(filterXmlError(QString)));
 
@@ -72,6 +73,12 @@ void ctkCmdLineModuleProcessWatcher::filterProgress(float progress)
 }
 
 //----------------------------------------------------------------------------
+void ctkCmdLineModuleProcessWatcher::filterResult(const QString &parameter, const QString &value)
+{
+  futureInterface.reportResult(ctkCmdLineModuleResult(parameter, value));
+}
+
+//----------------------------------------------------------------------------
 void ctkCmdLineModuleProcessWatcher::filterFinished(const QString& name)
 {
   futureInterface.setProgressValueAndText(incrementProgress(), "Finished: " + name);

+ 1 - 0
Libs/CommandLineModules/Backend/LocalProcess/ctkCmdLineModuleProcessWatcher_p.h

@@ -51,6 +51,7 @@ protected Q_SLOTS:
 
   void filterStarted(const QString& name, const QString& comment);
   void filterProgress(float progress);
+  void filterResult(const QString& parameter, const QString& value);
   void filterFinished(const QString& name);
 
   void filterXmlError(const QString& error);

+ 35 - 3
Libs/CommandLineModules/Core/ctkCmdLineModuleFrontend.cpp

@@ -26,35 +26,48 @@
 #include "ctkCmdLineModuleParameterGroup.h"
 #include "ctkCmdLineModuleReference.h"
 #include "ctkCmdLineModuleFuture.h"
+#include "ctkException.h"
 
 #include <QUrl>
+#include <QFutureWatcher>
 
 //----------------------------------------------------------------------------
 struct ctkCmdLineModuleFrontendPrivate
 {
-  ctkCmdLineModuleFrontendPrivate(const ctkCmdLineModuleReference& moduleRef)
-    : ModuleReference(moduleRef)
+  ctkCmdLineModuleFrontendPrivate(const ctkCmdLineModuleReference& moduleRef, ctkCmdLineModuleFrontend* q)
+    : q(q)
+    , ModuleReference(moduleRef)
   {
   }
 
+  void _q_resultReadyAt(int index)
+  {
+    q->resultReady(Future.resultAt(index));
+  }
+
+  ctkCmdLineModuleFrontend* q;
+
   ctkCmdLineModuleReference ModuleReference;
 
   QList<QString> ParameterNames;
 
   ctkCmdLineModuleFuture Future;
+  QFutureWatcher<ctkCmdLineModuleResult> FutureWatcher;
 };
 
 
 //----------------------------------------------------------------------------
 ctkCmdLineModuleFrontend::ctkCmdLineModuleFrontend(const ctkCmdLineModuleReference& moduleRef)
-  : d(new ctkCmdLineModuleFrontendPrivate(moduleRef))
+  : d(new ctkCmdLineModuleFrontendPrivate(moduleRef, this))
 {
+  connect(&d->FutureWatcher, SIGNAL(resultReadyAt(int)), SLOT(_q_resultReadyAt(int)));
 }
 
 //----------------------------------------------------------------------------
 void ctkCmdLineModuleFrontend::setFuture(const ctkCmdLineModuleFuture &future)
 {
   d->Future = future;
+  d->FutureWatcher.setFuture(d->Future);
 }
 
 //----------------------------------------------------------------------------
@@ -166,3 +179,22 @@ void ctkCmdLineModuleFrontend::resetValues()
     this->setValue(param.name(), param.defaultValue());
   }
 }
+
+//----------------------------------------------------------------------------
+void ctkCmdLineModuleFrontend::resultReady(const ctkCmdLineModuleResult &result)
+{
+  try
+  {
+    if (this->moduleReference().description().parameter(result.parameter()).channel() != "output")
+    {
+      qWarning() << "Module" << this->moduleReference().location() << "is reporting results for non-output parameter"
+                 << result.parameter() << ". Report ignored.";
+      return;
+    }
+    this->setValue(result.parameter(), result.value());
+  }
+  catch (const ctkInvalidArgumentException&)
+  {}
+}
+
+#include "moc_ctkCmdLineModuleFrontend.h"

+ 16 - 0
Libs/CommandLineModules/Core/ctkCmdLineModuleFrontend.h

@@ -32,6 +32,7 @@ class QUrl;
 class ctkCmdLineModuleFuture;
 class ctkCmdLineModuleReference;
 class ctkCmdLineModuleParameter;
+class ctkCmdLineModuleResult;
 struct ctkCmdLineModuleFrontendPrivate;
 
 /**
@@ -229,13 +230,28 @@ protected:
    */
   void setFuture(const ctkCmdLineModuleFuture& future);
 
+private Q_SLOTS:
+
+  /**
+   * @brief Provides results as reported by the running module.
+   * @param result
+   *
+   * This method is called when a running module reports a new
+   * result. The default implementation updates the current value
+   * of the output parameter in the GUI with the reported value.
+   */
+  virtual void resultReady(const ctkCmdLineModuleResult& result);
+
 private:
 
   Q_DISABLE_COPY(ctkCmdLineModuleFrontend)
 
+  friend class ctkCmdLineModuleFrontendPrivate;
   friend class ctkCmdLineModuleManager;
   friend class ctkCmdLineModulePrivate;
 
+  Q_PRIVATE_SLOT(d, void _q_resultReadyAt(int))
+
   QScopedPointer<ctkCmdLineModuleFrontendPrivate> d;
 
 };

+ 5 - 0
Libs/CommandLineModules/Core/ctkCmdLineModuleResult.h

@@ -40,6 +40,11 @@ public:
     : Parameter(parameter), Value(value)
   {}
 
+  bool operator==(const ctkCmdLineModuleResult& other) const
+  {
+    return Parameter == other.Parameter && Value == other.Value;
+  }
+
   inline QString parameter() const { return Parameter; }
   inline QVariant value() const { return Value; }
 

+ 18 - 1
Libs/CommandLineModules/Core/ctkCmdLineModuleXmlProgressWatcher.cpp

@@ -33,6 +33,7 @@ static QString FILTER_START = "filter-start";
 static QString FILTER_NAME = "filter-name";
 static QString FILTER_COMMENT = "filter-comment";
 static QString FILTER_PROGRESS = "filter-progress";
+static QString FILTER_RESULT = "filter-result";
 static QString FILTER_END = "filter-end";
 
 }
@@ -104,6 +105,10 @@ public:
         {
           currentProgress = reader.text().toString().toFloat();
         }
+        else if (stack.size() == 1 && stack.back() == FILTER_RESULT)
+        {
+          currentResultValue = reader.text().toString();
+        }
         break;
       }
       case QXmlStreamReader::StartElement:
@@ -119,6 +124,7 @@ public:
 
         if (name.compare(FILTER_START, Qt::CaseInsensitive) == 0 ||
             name.compare(FILTER_PROGRESS, Qt::CaseInsensitive) == 0 ||
+            name.compare(FILTER_RESULT, Qt::CaseInsensitive) == 0 ||
             name.compare(FILTER_END, Qt::CaseInsensitive) == 0)
         {
           if (!parent.isEmpty())
@@ -133,6 +139,11 @@ public:
             currentComment.clear();
             currentProgress = 0;
           }
+          else if (name.compare(FILTER_RESULT, Qt::CaseInsensitive) == 0)
+          {
+            currentResultParameter = reader.attributes().value("name").toString();
+            currentResultValue.clear();
+          }
         }
         break;
       }
@@ -164,6 +175,10 @@ public:
           {
             emit q->filterProgress(currentProgress);
           }
+          else if (name.compare(FILTER_RESULT, Qt::CaseInsensitive) == 0)
+          {
+            emit q->filterResult(currentResultParameter, currentResultValue);
+          }
           else if (name.compare(FILTER_END, Qt::CaseInsensitive) == 0)
           {
             emit q->filterFinished(currentName);
@@ -208,6 +223,8 @@ public:
   QString currentName;
   QString currentComment;
   float currentProgress;
+  QString currentResultParameter;
+  QString currentResultValue;
 };
 
 
@@ -239,4 +256,4 @@ ctkCmdLineModuleXmlProgressWatcher::~ctkCmdLineModuleXmlProgressWatcher()
 {
 }
 
-#include "moc_ctkCmdLineModuleXmlProgressWatcher.cxx"
+#include "moc_ctkCmdLineModuleXmlProgressWatcher.h"

+ 1 - 0
Libs/CommandLineModules/Core/ctkCmdLineModuleXmlProgressWatcher.h

@@ -50,6 +50,7 @@ Q_SIGNALS:
 
   void filterStarted(const QString& name, const QString& comment);
   void filterProgress(float progress);
+  void filterResult(const QString& parameter, const QString& value);
   void filterFinished(const QString& name);
   void filterXmlError(const QString& error);
 

+ 91 - 3
Libs/CommandLineModules/Testing/Cpp/ctkCmdLineModuleFutureTest.cpp

@@ -168,10 +168,30 @@ void ctkCmdLineModuleFutureTester::testStartFinish()
 {
   QList<QString> expectedSignals;
   expectedSignals << "module.started"
+
+                     // the following signals are send when connecting a QFutureWatcher to
+                     // an already started QFuture
                   << "module.progressRangeChanged(0,0)"
                   << "module.progressValueChanged(0)"
+
                   << "module.progressRangeChanged(0,1000)"
+
+                     // the test module always reports error data when starting
                   << "module.errorReady"
+
+                     // the following two signals are send when the module reports "filter start"
+                  << "module.progressValueChanged(1)"
+                  << "module.progressTextChanged(Test Filter)"
+
+                     // imageOutput result
+                  << "module.resultReadyAt(0,1)"
+                  << "module.resultReadyAt(0)"
+
+                     // exitStatusOutput result
+                  << "module.resultReadyAt(1,2)"
+                  << "module.resultReadyAt(1)"
+
+                     // the following signal is sent at the end to report completion
                   << "module.progressValueChanged(1000)"
                   << "module.finished";
 
@@ -183,6 +203,11 @@ void ctkCmdLineModuleFutureTester::testStartFinish()
 
   QCoreApplication::processEvents();
   QVERIFY(signalTester.checkSignals(expectedSignals));
+
+  QList<ctkCmdLineModuleResult> results;
+  results << ctkCmdLineModuleResult("imageOutput", "/tmp/out.nrrd");
+  results << ctkCmdLineModuleResult("exitStatusOutput", "Normal exit");
+  QCOMPARE(signalTester.results(), results);
 }
 
 //-----------------------------------------------------------------------------
@@ -190,10 +215,12 @@ void ctkCmdLineModuleFutureTester::testProgress()
 {
   QList<QString> expectedSignals;
   expectedSignals << "module.started"
+
                      // the following signals are send when connecting a QFutureWatcher to
                      // an already started QFuture
                   << "module.progressRangeChanged(0,0)"
                   << "module.progressValueChanged(0)"
+
                   << "module.progressRangeChanged(0,1000)"
 
                      // the test module always reports error data when starting
@@ -203,11 +230,23 @@ void ctkCmdLineModuleFutureTester::testProgress()
                   << "module.progressValueChanged(1)"
                   << "module.progressTextChanged(Test Filter)"
 
+                     // the output data on the standard output channel
+                  << "module.outputReady"
+
                      // this signal is send when the module reports progress for "output1"
                   << "module.progressValueChanged(999)"
 
-                     // the output data (the order is not really deterministic here...)
-                  << "module.outputReady"
+                     // first resultNumberOutput result
+                  << "module.resultReadyAt(0,1)"
+                  << "module.resultReadyAt(0)"
+
+                     // imageOutput result
+                  << "module.resultReadyAt(1,2)"
+                  << "module.resultReadyAt(1)"
+
+                     // exitStatusOutput result
+                  << "module.resultReadyAt(2,3)"
+                  << "module.resultReadyAt(2)"
 
                      // the following signal is sent at the end to report completion
                   << "module.progressValueChanged(1000)"
@@ -225,6 +264,12 @@ void ctkCmdLineModuleFutureTester::testProgress()
   QCoreApplication::processEvents();
 
   QVERIFY(signalTester.checkSignals(expectedSignals));
+
+  QList<ctkCmdLineModuleResult> results;
+  results << ctkCmdLineModuleResult("resultNumberOutput", 1);
+  results << ctkCmdLineModuleResult("imageOutput", "/tmp/out.nrrd");
+  results << ctkCmdLineModuleResult("exitStatusOutput", "Normal exit");
+  QCOMPARE(signalTester.results(), results);
 }
 
 //-----------------------------------------------------------------------------
@@ -317,17 +362,53 @@ void ctkCmdLineModuleFutureTester::testOutput()
 
   QList<QString> expectedSignals;
   expectedSignals << "module.started"
+
+                     // the following signals are send when connecting a QFutureWatcher to
+                     // an already started QFuture
                   << "module.progressRangeChanged(0,0)"
                   << "module.progressValueChanged(0)"
+
                   << "module.progressRangeChanged(0,1000)"
+
+                     // the test module always reports error data when starting
                   << "module.errorReady"
+
+                     // the following two signals are send when the module reports "filter start"
                   << "module.progressValueChanged(1)"
                   << "module.progressTextChanged(Test Filter)"
+
+                     // the output data on the standard output channel "Output 1"
+                  << "module.outputReady"
+
+                     // this signal is send when the module reports progress for "output1"
                   << "module.progressValueChanged(500)"
+
+                     // first resultNumberOutput result
+                  << "module.resultReadyAt(0,1)"
+                  << "module.resultReadyAt(0)"
+
+                     // the output data on the standard output channel "Output 2"
                   << "module.outputReady"
+
+                     // this signal is send when the module reports progress for "output2"
                   << "module.progressValueChanged(999)"
-                  << "module.outputReady"
+
+                     // second resultNumberOutput result
+                  << "module.resultReadyAt(1,2)"
+                  << "module.resultReadyAt(1)"
+
+                     // imageOutput result
+                  << "module.resultReadyAt(2,3)"
+                  << "module.resultReadyAt(2)"
+
+                     // exitStatusOutput result
+                  << "module.resultReadyAt(3,4)"
+                  << "module.resultReadyAt(3)"
+
+                     // final error message
                   << "module.errorReady"
+
+                     // the following signal is sent at the end to report completion
                   << "module.progressValueChanged(1000)"
                   << "module.finished";
 
@@ -341,6 +422,13 @@ void ctkCmdLineModuleFutureTester::testOutput()
 
   QCOMPARE(future.readAllOutputData().data(), expectedOutput);
   QCOMPARE(future.readAllErrorData().data(), expectedError);
+
+  QList<ctkCmdLineModuleResult> results;
+  results << ctkCmdLineModuleResult("resultNumberOutput", 1);
+  results << ctkCmdLineModuleResult("resultNumberOutput", 2);
+  results << ctkCmdLineModuleResult("errorMsgOutput", "Final error msg.");
+  results << ctkCmdLineModuleResult("exitStatusOutput", "Normal exit");
+  QCOMPARE(signalTester.results(), results);
 }
 
 //-----------------------------------------------------------------------------

+ 26 - 0
Libs/CommandLineModules/Testing/Cpp/ctkCmdLineModuleSignalTester.cpp

@@ -23,6 +23,7 @@
 
 #include <QDebug>
 
+//-----------------------------------------------------------------------------
 ctkCmdLineModuleSignalTester::ctkCmdLineModuleSignalTester()
 {
   connect(&Watcher, SIGNAL(started()), SLOT(moduleStarted()));
@@ -43,86 +44,104 @@ ctkCmdLineModuleSignalTester::ctkCmdLineModuleSignalTester()
   connect(&Watcher, SIGNAL(errorDataReady()), SLOT(errorDataReady()));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::setFuture(const ctkCmdLineModuleFuture &future)
 {
   this->Watcher.setFuture(future);
 }
 
+//-----------------------------------------------------------------------------
 ctkCmdLineModuleFutureWatcher *ctkCmdLineModuleSignalTester::watcher()
 {
   return &this->Watcher;
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::moduleStarted()
 {
   Events.push_back("module.started");
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::moduleFinished()
 {
   Events.push_back("module.finished");
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::moduleProgressValueChanged(int progress)
 {
   Events.push_back(QString("module.progressValueChanged(%1)").arg(progress));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::moduleProgressTextChanged(const QString& text)
 {
   Events.push_back(QString("module.progressTextChanged(\"%1\")").arg(text));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::modulePaused()
 {
   Events.push_back("module.paused");
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::moduleResumed()
 {
   Events.push_back("module.resumed");
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::moduleCanceled()
 {
   Events.push_back("module.canceled");
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::resultReadyAt(int resultIndex)
 {
   Events.push_back(QString("module.resultReadyAt(%1)").arg(resultIndex));
+  Results.push_back(Watcher.resultAt(resultIndex));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::resultReadyAt(int beginIndex, int endIndex)
 {
   Events.push_back(QString("module.resultReadyAt(%1,%2)").arg(beginIndex).arg(endIndex));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::progressRangeChanged(int minimum, int maximum)
 {
   Events.push_back(QString("module.progressRangeChanged(%1,%2)").arg(minimum).arg(maximum));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::progressValueChanged(int progressValue)
 {
   Events.push_back(QString("module.progressValueChanged(%1)").arg(progressValue));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::progressTextChanged(const QString &progressText)
 {
   Events.push_back(QString("module.progressTextChanged(%1)").arg(progressText));
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::outputDataReady()
 {
   Events.push_back("module.outputReady");
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::errorDataReady()
 {
   Events.push_back("module.errorReady");
 }
 
+//-----------------------------------------------------------------------------
 bool ctkCmdLineModuleSignalTester::checkSignals(const QList<QString>& expectedSignals)
 {
   if (Events.size() != expectedSignals.size())
@@ -142,6 +161,7 @@ bool ctkCmdLineModuleSignalTester::checkSignals(const QList<QString>& expectedSi
   return true;
 }
 
+//-----------------------------------------------------------------------------
 void ctkCmdLineModuleSignalTester::dumpSignals(const QList<QString>& expectedSignals)
 {
   int max = Events.size() > expectedSignals.size() ? Events.size() : expectedSignals.size();
@@ -159,3 +179,9 @@ void ctkCmdLineModuleSignalTester::dumpSignals(const QList<QString>& expectedSig
     }
   }
 }
+
+//-----------------------------------------------------------------------------
+QList<ctkCmdLineModuleResult> ctkCmdLineModuleSignalTester::results() const
+{
+  return Results;
+}

+ 2 - 0
Libs/CommandLineModules/Testing/Cpp/ctkCmdLineModuleSignalTester.h

@@ -43,6 +43,7 @@ public:
   bool checkSignals(const QList<QString>& expectedSignals);
   void dumpSignals(const QList<QString>& expectedSignals);
 
+  QList<ctkCmdLineModuleResult> results() const;
 
 public Q_SLOTS:
 
@@ -69,6 +70,7 @@ private:
 
   ctkCmdLineModuleFutureWatcher Watcher;
   QList<QString> Events;
+  QList<ctkCmdLineModuleResult> Results;
 };
 
 #endif // CTKCMDLINEMODULESIGNALTESTER_H

+ 37 - 13
Libs/CommandLineModules/Testing/Modules/TestBed/ctkCmdLineModuleTestBed.cpp

@@ -67,6 +67,7 @@ int main(int argc, char* argv[])
   parser.addArgument("exitCrash", "", QVariant::Bool, "Force crash", false);
   parser.addArgument("exitTime", "", QVariant::Int, "Exit time", 0);
   parser.addArgument("errorText", "", QVariant::String, "Error text printed at the end");
+
   QTextStream out(stdout, QIODevice::WriteOnly | QIODevice::Text);
   QTextStream err(stderr, QIODevice::WriteOnly | QIODevice::Text);
 
@@ -75,7 +76,7 @@ int main(int argc, char* argv[])
   QHash<QString, QVariant> parsedArgs = parser.parseArguments(QCoreApplication::arguments(), &ok);
   if (!ok)
   {
-    err << "Error parsing arguments: " << parser.errorString() << "\n";
+    err << "Error parsing arguments:" << parser.errorString() << endl;
     return EXIT_FAILURE;
   }
 
@@ -83,6 +84,9 @@ int main(int argc, char* argv[])
   if (parsedArgs.contains("help") || parsedArgs.contains("h"))
   {
     out << parser.helpText();
+    out.setFieldWidth(parser.fieldWidth());
+    out.setFieldAlignment(QTextStream::AlignLeft);
+    out << "  <output-path>" << "Path to the output image" << endl;
     return EXIT_SUCCESS;
   }
 
@@ -94,6 +98,12 @@ int main(int argc, char* argv[])
     return EXIT_SUCCESS;
   }
 
+  if (parser.unparsedArguments().isEmpty())
+  {
+    err << "Error parsing arguments: <output-path> argument missing" << endl;
+    return EXIT_FAILURE;
+  }
+
   // Do something
 
   float runtime = parsedArgs["runtime"].toFloat();
@@ -104,6 +114,8 @@ int main(int argc, char* argv[])
   bool exitCrash = parsedArgs["exitCrash"].toBool();
   QString errorText = parsedArgs["errorText"].toString();
 
+  QString imageOutput = parser.unparsedArguments().at(0);
+
   err << "A superficial error message." << endl;
 
   // sleep 500ms to give the "errorReady" signal a chance
@@ -120,14 +132,12 @@ int main(int argc, char* argv[])
   QTime time;
   time.start();
 
-  if (!outputs.empty())
-  {
-    out << "<filter-start>\n";
-    out << "<filter-name>Test Filter</filter-name>\n";
-    out << "<filter-comment>Does nothing useful</filter-comment>\n";
-    out << "</filter-start>\n";
-  }
-  else
+  out << "<filter-start>\n";
+  out << "<filter-name>Test Filter</filter-name>\n";
+  out << "<filter-comment>Does nothing useful</filter-comment>\n";
+  out << "</filter-start>" << endl;
+
+  if (outputs.empty())
   {
     outputs.push_back("dummy");
   }
@@ -158,24 +168,38 @@ int main(int argc, char* argv[])
     if (output != "dummy")
     {
       out << output << endl;
+
       // report progress
-      out << "<filter-progress>" << (i+1)*progressStep << "</filter-progress>\n";
+      out << "<filter-progress>" << (i+1)*progressStep << "</filter-progress>" << endl;
+      // report the current output number as a result
+      out << "<filter-result name=\"resultNumberOutput\">" << (i+1) << "</filter-result>" << endl;
     }
   }
 
   // sleep 500ms to avoid squashing the last progress event with the finished event
   sleep_ms(500);
 
-  out.flush();
+  if (!errorText.isEmpty())
+  {
+    err << errorText;
+    out << "<filter-result name=\"errorMsgOutput\">" << errorText << "</filter-result>" << endl;
+  }
+  else
+  {
+    out << "<filter-result name=\"imageOutput\">" << imageOutput << "</filter-result>" << endl;
+  }
 
+  out << "<filter-result name=\"exitStatusOutput\">";
   if (exitCrash)
   {
+    out << "Crashed</filter-result>" << endl;
     int* crash = 0;
     *crash = 5;
   }
-  if (!errorText.isEmpty())
+  else
   {
-    err << errorText;
+    out << "Normal exit</filter-result>" << endl;
   }
+
   return exitCode;
 }

+ 39 - 2
Libs/CommandLineModules/Testing/Modules/TestBed/ctkCmdLineModuleTestBed.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<executable>
+<executable xsi:noNamespaceSchemaLocation="../../../Core/Resources/ctkCmdLineModule.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <category>Testing</category>
   <title>Test Bed</title>
   <description>
@@ -16,7 +16,7 @@ Configurable behaviour for testing purposes.
     <integer>
       <name>runtimeVar</name>
       <longflag>runtime</longflag>
-      <description>An integer without constraints</description>
+      <description>An integer with constraints</description>
       <label>Runtime (seconds)</label>
       <default>1</default>
       <constraints>
@@ -60,6 +60,43 @@ Configurable behaviour for testing purposes.
       <label>Error text</label>
     </string>
   </parameters>
+  
+  <parameters>
+    <label>Output parameter</label>
+    <description>Output parameters for testing purposes.</description>
+    <integer>
+      <name>resultNumberOutput</name>
+      <index>1000</index>
+      <description>The number of results reported by this module.</description>
+      <label>Number of results</label>
+      <default>0</default>
+      <channel>output</channel>
+    </integer>
+    <string>
+      <name>errorMsgOutput</name>
+      <index>1000</index>
+      <description>Exit error message.</description>
+      <label>Error</label>
+      <channel>output</channel>
+    </string>
+    <string-enumeration>
+      <name>exitStatusOutput</name>
+      <index>1000</index>
+      <description>Exit status (crashed or normal exit)</description>
+      <label>Exit status</label>
+      <channel>output</channel>
+      <element>Normal exit</element>
+      <element>Crashed</element>
+    </string-enumeration>
+    <image>
+      <name>imageOutput</name>
+      <index>0</index>
+      <description>Image output path.</description>
+      <label>Output image</label>
+      <default>/tmp/out.nrrd</default>
+      <channel>output</channel>
+    </image>
+  </parameters>
 
 </executable>