changeset 4060:149172a06b4d framework

splitting MultiThreadingTests.cpp into JobsTests.cpp and ServerJobsTests.cpp
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 11 Jun 2020 14:29:14 +0200
parents e241e5f3f088
children 496e07123e37
files CMakeLists.txt OrthancFramework/UnitTestsSources/JobsTests.cpp OrthancServer/UnitTestsSources/MultiThreadingTests.cpp OrthancServer/UnitTestsSources/ServerJobsTests.cpp
diffstat 4 files changed, 2535 insertions(+), 2229 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Jun 11 14:12:07 2020 +0200
+++ b/CMakeLists.txt	Thu Jun 11 14:29:14 2020 +0200
@@ -120,6 +120,7 @@
   OrthancFramework/UnitTestsSources/FromDcmtkTests.cpp
   OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp
   OrthancFramework/UnitTestsSources/ImageTests.cpp
+  OrthancFramework/UnitTestsSources/JobsTests.cpp
   OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp
   OrthancFramework/UnitTestsSources/LoggingTests.cpp
   OrthancFramework/UnitTestsSources/LuaTests.cpp
@@ -135,9 +136,9 @@
 set(ORTHANC_SERVER_UNIT_TESTS
   OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp
   OrthancServer/UnitTestsSources/LuaServerTests.cpp
-  OrthancServer/UnitTestsSources/MultiThreadingTests.cpp
   OrthancServer/UnitTestsSources/PluginsTests.cpp
   OrthancServer/UnitTestsSources/ServerIndexTests.cpp
+  OrthancServer/UnitTestsSources/ServerJobsTests.cpp
   OrthancServer/UnitTestsSources/UnitTestsMain.cpp
   OrthancServer/UnitTestsSources/VersionsTests.cpp
   )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Jun 11 14:29:14 2020 +0200
@@ -0,0 +1,1434 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "gtest/gtest.h"
+
+#include "../../OrthancFramework/Sources/Compatibility.h"
+#include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h"
+#include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h"
+#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
+#include "../../OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h"
+#include "../../OrthancFramework/Sources/JobsEngine/JobsEngine.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h"
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h"
+#include "../../OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h"
+#include "../../OrthancFramework/Sources/Logging.h"
+#include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h"
+#include "../../OrthancFramework/Sources/OrthancException.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+
+
+using namespace Orthanc;
+
+namespace
+{
+  class DummyJob : public IJob
+  {
+  private:
+    bool         fails_;
+    unsigned int count_;
+    unsigned int steps_;
+
+  public:
+    DummyJob() :
+      fails_(false),
+      count_(0),
+      steps_(4)
+    {
+    }
+
+    explicit DummyJob(bool fails) :
+      fails_(fails),
+      count_(0),
+      steps_(4)
+    {
+    }
+
+    virtual void Start() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void Reset() ORTHANC_OVERRIDE
+    {
+    }
+    
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      if (fails_)
+      {
+        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL);
+      }
+      else if (count_ == steps_ - 1)
+      {
+        return JobStepResult::Success();
+      }
+      else
+      {
+        count_++;
+        return JobStepResult::Continue();
+      }
+    }
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual float GetProgress() ORTHANC_OVERRIDE
+    {
+      return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
+    }
+
+    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    {
+      type = "DummyJob";
+    }
+
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    {
+      value = Json::objectValue;
+      value["Type"] = "DummyJob";
+      return true;
+    }
+
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    {
+      value["hello"] = "world";
+    }
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+  };
+
+
+  class DummyInstancesJob : public SetOfInstancesJob
+  {
+  private:
+    bool   trailingStepDone_;
+    
+  protected:
+    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
+    {
+      return (instance != "nope");
+    }
+
+    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE
+    {
+      if (HasTrailingStep())
+      {
+        if (trailingStepDone_)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+        else
+        {
+          trailingStepDone_ = true;
+          return true;
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+  public:
+    DummyInstancesJob() :
+      trailingStepDone_(false)
+    {
+    }
+    
+    DummyInstancesJob(const Json::Value& value) :
+      SetOfInstancesJob(value)
+    {
+      if (HasTrailingStep())
+      {
+        trailingStepDone_ = (GetPosition() == GetCommandsCount());
+      }
+      else
+      {
+        trailingStepDone_ = false;
+      }
+    }
+
+    bool IsTrailingStepDone() const
+    {
+      return trailingStepDone_;
+    }
+    
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    {
+      s = "DummyInstancesJob";
+    }
+  };
+
+
+  class DummyUnserializer : public GenericJobUnserializer
+  {
+  public:
+    virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE
+    {
+      if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob")
+      {
+        return new DummyInstancesJob(value);
+      }
+      else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob")
+      {
+        return new DummyJob;
+      }
+      else
+      {
+        return GenericJobUnserializer::UnserializeJob(value);
+      }
+    }
+  };
+
+    
+  class DynamicInteger : public IDynamicObject
+  {
+  private:
+    int value_;
+    std::set<int>& target_;
+
+  public:
+    DynamicInteger(int value, std::set<int>& target) : 
+      value_(value), target_(target)
+    {
+    }
+
+    int GetValue() const
+    {
+      return value_;
+    }
+  };
+}
+
+
+TEST(MultiThreading, SharedMessageQueueBasic)
+{
+  std::set<int> s;
+
+  SharedMessageQueue q;
+  ASSERT_TRUE(q.WaitEmpty(0));
+  q.Enqueue(new DynamicInteger(10, s));
+  ASSERT_FALSE(q.WaitEmpty(1));
+  q.Enqueue(new DynamicInteger(20, s));
+  q.Enqueue(new DynamicInteger(30, s));
+  q.Enqueue(new DynamicInteger(40, s));
+
+  std::unique_ptr<DynamicInteger> i;
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue());
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue());
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue());
+  ASSERT_FALSE(q.WaitEmpty(1));
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue());
+  ASSERT_TRUE(q.WaitEmpty(0));
+  ASSERT_EQ(NULL, q.Dequeue(1));
+}
+
+
+TEST(MultiThreading, SharedMessageQueueClean)
+{
+  std::set<int> s;
+
+  try
+  {
+    SharedMessageQueue q;
+    q.Enqueue(new DynamicInteger(10, s));
+    q.Enqueue(new DynamicInteger(20, s));  
+    throw OrthancException(ErrorCode_InternalError);
+  }
+  catch (OrthancException&)
+  {
+  }
+}
+
+
+
+
+static bool CheckState(JobsRegistry& registry,
+                       const std::string& id,
+                       JobState state)
+{
+  JobState s;
+  if (registry.GetState(s, id))
+  {
+    return state == s;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+static bool CheckErrorCode(JobsRegistry& registry,
+                           const std::string& id,
+                           ErrorCode code)
+{
+  JobInfo s;
+  if (registry.GetJobInfo(s, id))
+  {
+    return code == s.GetStatus().GetErrorCode();
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+TEST(JobsRegistry, Priority)
+{
+  JobsRegistry registry(10);
+
+  std::string i1, i2, i3, i4;
+  registry.Submit(i1, new DummyJob(), 10);
+  registry.Submit(i2, new DummyJob(), 30);
+  registry.Submit(i3, new DummyJob(), 20);
+  registry.Submit(i4, new DummyJob(), 5);  
+
+  registry.SetMaxCompletedJobs(2);
+
+  std::set<std::string> id;
+  registry.ListJobs(id);
+
+  ASSERT_EQ(4u, id.size());
+  ASSERT_TRUE(id.find(i1) != id.end());
+  ASSERT_TRUE(id.find(i2) != id.end());
+  ASSERT_TRUE(id.find(i3) != id.end());
+  ASSERT_TRUE(id.find(i4) != id.end());
+
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(30, job.GetPriority());
+    ASSERT_EQ(i2, job.GetId());
+
+    ASSERT_TRUE(CheckState(registry, i2, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i3, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(20, job.GetPriority());
+    ASSERT_EQ(i3, job.GetId());
+
+    job.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, i3, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i3, JobState_Success));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(10, job.GetPriority());
+    ASSERT_EQ(i1, job.GetId());
+  }
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(5, job.GetPriority());
+    ASSERT_EQ(i4, job.GetId());
+  }
+
+  {
+    JobsRegistry::RunningJob job(registry, 1);
+    ASSERT_FALSE(job.IsValid());
+  }
+
+  JobState s;
+  ASSERT_TRUE(registry.GetState(s, i1));
+  ASSERT_FALSE(registry.GetState(s, i2));  // Removed because oldest
+  ASSERT_FALSE(registry.GetState(s, i3));  // Removed because second oldest
+  ASSERT_TRUE(registry.GetState(s, i4));
+
+  registry.SetMaxCompletedJobs(1);  // (*)
+  ASSERT_FALSE(registry.GetState(s, i1));  // Just discarded by (*)
+  ASSERT_TRUE(registry.GetState(s, i4));
+}
+
+
+TEST(JobsRegistry, Simultaneous)
+{
+  JobsRegistry registry(10);
+
+  std::string i1, i2;
+  registry.Submit(i1, new DummyJob(), 20);
+  registry.Submit(i2, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, i1, JobState_Pending));
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job1(registry, 0);
+    JobsRegistry::RunningJob job2(registry, 0);
+
+    ASSERT_TRUE(job1.IsValid());
+    ASSERT_TRUE(job2.IsValid());
+
+    job1.MarkFailure();
+    job2.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, i1, JobState_Running));
+    ASSERT_TRUE(CheckState(registry, i2, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i1, JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Success));
+}
+
+
+TEST(JobsRegistry, Resubmit)
+{
+  JobsRegistry registry(10);
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkFailure();
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+
+    registry.Resubmit(id);
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, Retry)
+{
+  JobsRegistry registry(10);
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkRetry(0);
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+  
+  registry.ScheduleRetries();
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, PausePending)
+{
+  JobsRegistry registry(10);
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+}
+
+
+TEST(JobsRegistry, PauseRunning)
+{
+  JobsRegistry registry(10);
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    registry.Resubmit(id);
+    job.MarkPause();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, PauseRetry)
+{
+  JobsRegistry registry(10);
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkRetry(0);
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, Cancel)
+{
+  JobsRegistry registry(10);
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_FALSE(registry.Cancel("nope"));
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+            
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  registry.Submit(id, new DummyJob(), 10);
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+
+    job.MarkCanceled();
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Pause(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+
+    job.MarkRetry(500);
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+}
+
+
+
+TEST(JobsEngine, SubmitAndWait)
+{
+  JobsEngine engine(10);
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  Json::Value content = Json::nullValue;
+  engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10);
+  ASSERT_EQ(Json::objectValue, content.type());
+  ASSERT_EQ("world", content["hello"].asString());
+
+  content = Json::nullValue;
+  ASSERT_THROW(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10), OrthancException);
+  ASSERT_EQ(Json::nullValue, content.type());
+
+  engine.Stop();
+}
+
+
+TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
+{
+  JobsEngine engine(10);
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  std::string id;
+  SequenceOfOperationsJob* job = NULL;
+
+  {
+    std::unique_ptr<SequenceOfOperationsJob> a(new SequenceOfOperationsJob);
+    job = a.get();
+    engine.GetRegistry().Submit(id, a.release(), 0);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(500));
+
+  {
+    SequenceOfOperationsJob::Lock lock(*job);
+    size_t i = lock.AddOperation(new LogJobOperation);
+    size_t j = lock.AddOperation(new LogJobOperation);
+    size_t k = lock.AddOperation(new LogJobOperation);
+
+    StringOperationValue a("Hello");
+    StringOperationValue b("World");
+    lock.AddInput(i, a);
+    lock.AddInput(i, b);
+    
+    lock.Connect(i, j);
+    lock.Connect(j, k);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
+
+  engine.Stop();
+
+}
+
+
+static bool CheckSameJson(const Json::Value& a,
+                          const Json::Value& b)
+{
+  std::string s = a.toStyledString();
+  std::string t = b.toStyledString();
+
+  if (s == t)
+  {
+    return true;
+  }
+  else
+  {
+    LOG(ERROR) << "Expected serialization: " << s;
+    LOG(ERROR) << "Actual serialization: " << t;
+    return false;
+  }
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         IJob& job)
+{
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::unique_ptr<IJob> unserialized(unserializer.UnserializeJob(a));
+  
+    Json::Value b = 43;
+    if (unserialized->Serialize(b))
+    {
+      return (CheckSameJson(a, b));
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+
+static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
+                                          SetOfInstancesJob& job)
+{
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::unique_ptr<SetOfInstancesJob> unserialized
+      (dynamic_cast<SetOfInstancesJob*>(unserializer.UnserializeJob(a)));
+  
+    Json::Value b = 43;
+    if (unserialized->Serialize(b))
+    {    
+      return (CheckSameJson(a, b) &&
+              job.HasTrailingStep() == unserialized->HasTrailingStep() &&
+              job.GetPosition() == unserialized->GetPosition() &&
+              job.GetInstancesCount() == unserialized->GetInstancesCount() &&
+              job.GetCommandsCount() == unserialized->GetCommandsCount());
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         JobOperationValue& value)
+{
+  Json::Value a = 42;
+  value.Serialize(a);
+  
+  std::unique_ptr<JobOperationValue> unserialized(unserializer.UnserializeValue(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
+
+
+TEST(JobsSerialization, BadFileFormat)
+{
+  GenericJobUnserializer unserializer;
+
+  Json::Value s;
+
+  s = Json::objectValue;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = Json::arrayValue;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = "hello";
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = 42;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+}
+
+
+TEST(JobsSerialization, JobOperationValues)
+{
+  Json::Value s;
+
+  {
+    JobOperationValues values;
+    values.Append(new NullOperationValue);
+    values.Append(new StringOperationValue("hello"));
+    values.Append(new StringOperationValue("world"));
+
+    s = 42;
+    values.Serialize(s);
+  }
+
+  {
+    GenericJobUnserializer unserializer;
+    std::unique_ptr<JobOperationValues> values(JobOperationValues::Unserialize(unserializer, s));
+    ASSERT_EQ(3u, values->GetSize());
+    ASSERT_EQ(JobOperationValue::Type_Null, values->GetValue(0).GetType());
+    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(1).GetType());
+    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(2).GetType());
+
+    ASSERT_EQ("hello", dynamic_cast<const StringOperationValue&>(values->GetValue(1)).GetContent());
+    ASSERT_EQ("world", dynamic_cast<const StringOperationValue&>(values->GetValue(2)).GetContent());
+  }
+}
+
+
+TEST(JobsSerialization, GenericValues)
+{
+  GenericJobUnserializer unserializer;
+  Json::Value s;
+
+  {
+    NullOperationValue null;
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, null));
+    null.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  std::unique_ptr<JobOperationValue> value;
+  value.reset(unserializer.UnserializeValue(s));
+  
+  ASSERT_EQ(JobOperationValue::Type_Null, value->GetType());
+
+  {
+    StringOperationValue str("Hello");
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, str));
+    str.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+  value.reset(unserializer.UnserializeValue(s));
+
+  ASSERT_EQ(JobOperationValue::Type_String, value->GetType());
+  ASSERT_EQ("Hello", dynamic_cast<StringOperationValue&>(*value).GetContent());
+}
+
+
+TEST(JobsSerialization, GenericJobs)
+{   
+  Json::Value s;
+
+  // This tests SetOfInstancesJob
+  
+  {
+    DummyInstancesJob job;
+    job.SetDescription("description");
+    job.AddInstance("hello");
+    job.AddInstance("nope");
+    job.AddInstance("world");
+    job.SetPermissive(true);
+    ASSERT_THROW(job.Step("jobId"), OrthancException);  // Not started yet
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    job.Start();
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    DummyUnserializer unserializer;
+    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    const DummyInstancesJob& tmp = dynamic_cast<const DummyInstancesJob&>(*job);
+    ASSERT_FALSE(tmp.IsStarted());
+    ASSERT_TRUE(tmp.IsPermissive());
+    ASSERT_EQ("description", tmp.GetDescription());
+    ASSERT_EQ(3u, tmp.GetInstancesCount());
+    ASSERT_EQ(2u, tmp.GetPosition());
+    ASSERT_EQ(1u, tmp.GetFailedInstances().size());
+    ASSERT_EQ("hello", tmp.GetInstance(0));
+    ASSERT_EQ("nope", tmp.GetInstance(1));
+    ASSERT_EQ("world", tmp.GetInstance(2));
+    ASSERT_TRUE(tmp.IsFailedInstance("nope"));
+  }
+
+  // SequenceOfOperationsJob
+
+  {
+    SequenceOfOperationsJob job;
+    job.SetDescription("hello");
+
+    {
+      SequenceOfOperationsJob::Lock lock(job);
+      size_t a = lock.AddOperation(new LogJobOperation);
+      size_t b = lock.AddOperation(new LogJobOperation);
+      lock.Connect(a, b);
+
+      StringOperationValue s1("hello");
+      StringOperationValue s2("world");
+      lock.AddInput(a, s1);
+      lock.AddInput(a, s2);
+      lock.SetTrailingOperationTimeout(300);
+    }
+
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+
+    {
+      GenericJobUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
+    }
+    
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    GenericJobUnserializer unserializer;
+    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    std::string tmp;
+    dynamic_cast<SequenceOfOperationsJob&>(*job).GetDescription(tmp);
+    ASSERT_EQ("hello", tmp);
+  }  
+}
+
+
+static bool IsSameTagValue(ParsedDicomFile& dicom1,
+                           ParsedDicomFile& dicom2,
+                           DicomTag tag)
+{
+  std::string a, b;
+  return (dicom1.GetTagValue(a, tag) &&
+          dicom2.GetTagValue(b, tag) &&
+          (a == b));
+}
+                       
+
+
+TEST(JobsSerialization, DicomModification)
+{   
+  Json::Value s;
+
+  ParsedDicomFile source(true);
+  source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false, "");
+  source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false, "");
+  source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false, "");
+
+  std::unique_ptr<ParsedDicomFile> modified(source.Clone(true));
+
+  {
+    DicomModification modification;
+    modification.SetLevel(ResourceType_Series);
+    modification.Clear(DICOM_TAG_STUDY_DESCRIPTION);
+    modification.Remove(DICOM_TAG_SERIES_DESCRIPTION);
+    modification.Replace(DICOM_TAG_PATIENT_NAME, "Test 4", true);
+
+    modification.Apply(*modified);
+
+    s = 42;
+    modification.Serialize(s);
+  }
+
+  {
+    DicomModification modification(s);
+    ASSERT_EQ(ResourceType_Series, modification.GetLevel());
+    
+    std::unique_ptr<ParsedDicomFile> second(source.Clone(true));
+    modification.Apply(*second);
+
+    std::string s;
+    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_STUDY_DESCRIPTION));
+    ASSERT_TRUE(s.empty());
+    ASSERT_FALSE(second->GetTagValue(s, DICOM_TAG_SERIES_DESCRIPTION));
+    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+    ASSERT_EQ("Test 4", s);
+
+    ASSERT_TRUE(IsSameTagValue(source, *modified, DICOM_TAG_STUDY_INSTANCE_UID));
+    ASSERT_TRUE(IsSameTagValue(source, *second, DICOM_TAG_STUDY_INSTANCE_UID));
+
+    ASSERT_FALSE(IsSameTagValue(source, *second, DICOM_TAG_SERIES_INSTANCE_UID));
+    ASSERT_TRUE(IsSameTagValue(*modified, *second, DICOM_TAG_SERIES_INSTANCE_UID));
+  }
+}
+
+
+TEST(JobsSerialization, Registry)
+{   
+  Json::Value s;
+  std::string i1, i2;
+
+  {
+    JobsRegistry registry(10);
+    registry.Submit(i1, new DummyJob(), 10);
+    registry.Submit(i2, new SequenceOfOperationsJob(), 30);
+    registry.Serialize(s);
+  }
+
+  {
+    DummyUnserializer unserializer;
+    JobsRegistry registry(unserializer, s, 10);
+
+    Json::Value t;
+    registry.Serialize(t);
+    ASSERT_TRUE(CheckSameJson(s, t));
+  }
+}
+
+
+TEST(JobsSerialization, TrailingStep)
+{
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    ASSERT_EQ(0u, job.GetCommandsCount());
+    ASSERT_EQ(0u, job.GetInstancesCount());
+
+    job.Start();
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    job.AddInstance("hello");
+    job.AddInstance("world");
+    ASSERT_EQ(2u, job.GetCommandsCount());
+    ASSERT_EQ(2u, job.GetInstancesCount());
+
+    job.Start();
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(2u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    ASSERT_EQ(0u, job.GetInstancesCount());
+    ASSERT_EQ(0u, job.GetCommandsCount());
+    job.AddTrailingStep();
+    ASSERT_EQ(0u, job.GetInstancesCount());
+    ASSERT_EQ(1u, job.GetCommandsCount());
+
+    job.Start(); // This adds the trailing step
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_TRUE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_TRUE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    job.AddInstance("hello");
+    ASSERT_EQ(1u, job.GetInstancesCount());
+    ASSERT_EQ(1u, job.GetCommandsCount());
+    job.AddTrailingStep();
+    ASSERT_EQ(1u, job.GetInstancesCount());
+    ASSERT_EQ(2u, job.GetCommandsCount());
+    
+    job.Start();
+    ASSERT_EQ(2u, job.GetCommandsCount());
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_TRUE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(2u, job.GetPosition());
+    ASSERT_TRUE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
+  }
+}
+
+
+TEST(JobsSerialization, RemoteModalityParameters)
+{
+  Json::Value s;
+
+  {
+    RemoteModalityParameters modality;
+    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
+    modality.Serialize(s, false);
+    ASSERT_EQ(Json::arrayValue, s.type());
+  }
+
+  {
+    RemoteModalityParameters modality(s);
+    ASSERT_EQ("ORTHANC", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("127.0.0.1", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, modality.GetManufacturer());
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+
+  s = Json::nullValue;
+
+  {
+    RemoteModalityParameters modality;
+    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
+    ASSERT_THROW(modality.SetPortNumber(0), OrthancException);
+    ASSERT_THROW(modality.SetPortNumber(65535), OrthancException);
+    modality.SetApplicationEntityTitle("HELLO");
+    modality.SetHost("world");
+    modality.SetPortNumber(45);
+    modality.SetManufacturer(ModalityManufacturer_GenericNoWildcardInDates);
+    modality.Serialize(s, true);
+    ASSERT_EQ(Json::objectValue, s.type());
+  }
+
+  {
+    RemoteModalityParameters modality(s);
+    ASSERT_EQ("HELLO", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("world", modality.GetHost());
+    ASSERT_EQ(45u, modality.GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_GenericNoWildcardInDates, modality.GetManufacturer());
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+
+  s["Port"] = "46";
+  
+  {
+    RemoteModalityParameters modality(s);
+    ASSERT_EQ(46u, modality.GetPortNumber());
+  }
+
+  s["Port"] = -1;     ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
+  s["Port"] = 65535;  ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
+  s["Port"] = "nope"; ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
+
+  std::set<DicomRequestType> operations;
+  operations.insert(DicomRequestType_Echo);
+  operations.insert(DicomRequestType_Find);
+  operations.insert(DicomRequestType_Get);
+  operations.insert(DicomRequestType_Move);
+  operations.insert(DicomRequestType_Store);
+  operations.insert(DicomRequestType_NAction);
+  operations.insert(DicomRequestType_NEventReport);
+
+  ASSERT_EQ(7u, operations.size());
+
+  for (std::set<DicomRequestType>::const_iterator 
+         it = operations.begin(); it != operations.end(); ++it)
+  {
+    {
+      RemoteModalityParameters modality;
+      modality.SetRequestAllowed(*it, false);
+      ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+
+      modality.Serialize(s, false);
+      ASSERT_EQ(Json::objectValue, s.type());
+    }
+
+    {
+      RemoteModalityParameters modality(s);
+
+      ASSERT_FALSE(modality.IsRequestAllowed(*it));
+
+      for (std::set<DicomRequestType>::const_iterator 
+             it2 = operations.begin(); it2 != operations.end(); ++it2)
+      {
+        if (*it2 != *it)
+        {
+          ASSERT_TRUE(modality.IsRequestAllowed(*it2));
+        }
+      }
+    }
+  }
+
+  {
+    Json::Value s;
+    s["AllowStorageCommitment"] = false;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+
+  {
+    Json::Value s;
+    s["AllowNAction"] = false;
+    s["AllowNEventReport"] = true;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    s["AllowTranscoding"] = false;
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_FALSE(modality.IsTranscodingAllowed());
+  }
+
+  {
+    Json::Value s;
+    s["AllowNAction"] = true;
+    s["AllowNEventReport"] = true;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+}
--- a/OrthancServer/UnitTestsSources/MultiThreadingTests.cpp	Thu Jun 11 14:12:07 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2228 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../../OrthancFramework/Sources/Compatibility.h"
-#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
-#include "../../OrthancFramework/Sources/JobsEngine/JobsEngine.h"
-#include "../../OrthancFramework/Sources/Logging.h"
-#include "../../OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h"
-#include "../../OrthancFramework/Sources/OrthancException.h"
-#include "../../OrthancFramework/Sources/SerializationToolbox.h"
-#include "../../OrthancFramework/Sources/SystemToolbox.h"
-#include "../../OrthancFramework/Sources/Toolbox.h"
-#include "../Sources/Database/SQLiteDatabaseWrapper.h"
-#include "../Sources/ServerContext.h"
-#include "../Sources/ServerJobs/LuaJobManager.h"
-#include "../Sources/ServerJobs/OrthancJobUnserializer.h"
-
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h"
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h"
-#include "../Sources/ServerJobs/Operations/DicomInstanceOperationValue.h"
-
-#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
-#include "../Sources/ServerJobs/Operations/DeleteResourceOperation.h"
-#include "../Sources/ServerJobs/Operations/ModifyInstanceOperation.h"
-#include "../Sources/ServerJobs/Operations/StorePeerOperation.h"
-#include "../Sources/ServerJobs/Operations/StoreScuOperation.h"
-#include "../Sources/ServerJobs/Operations/SystemCallOperation.h"
-
-#include "../Sources/ServerJobs/ArchiveJob.h"
-#include "../Sources/ServerJobs/DicomModalityStoreJob.h"
-#include "../Sources/ServerJobs/DicomMoveScuJob.h"
-#include "../Sources/ServerJobs/MergeStudyJob.h"
-#include "../Sources/ServerJobs/OrthancPeerStoreJob.h"
-#include "../Sources/ServerJobs/ResourceModificationJob.h"
-#include "../Sources/ServerJobs/SplitStudyJob.h"
-
-
-using namespace Orthanc;
-
-namespace
-{
-  class DummyJob : public IJob
-  {
-  private:
-    bool         fails_;
-    unsigned int count_;
-    unsigned int steps_;
-
-  public:
-    DummyJob() :
-      fails_(false),
-      count_(0),
-      steps_(4)
-    {
-    }
-
-    explicit DummyJob(bool fails) :
-      fails_(fails),
-      count_(0),
-      steps_(4)
-    {
-    }
-
-    virtual void Start() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void Reset() ORTHANC_OVERRIDE
-    {
-    }
-    
-    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      if (fails_)
-      {
-        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL);
-      }
-      else if (count_ == steps_ - 1)
-      {
-        return JobStepResult::Success();
-      }
-      else
-      {
-        count_++;
-        return JobStepResult::Continue();
-      }
-    }
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual float GetProgress() ORTHANC_OVERRIDE
-    {
-      return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
-    }
-
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
-    {
-      type = "DummyJob";
-    }
-
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
-    {
-      value = Json::objectValue;
-      value["Type"] = "DummyJob";
-      return true;
-    }
-
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
-    {
-      value["hello"] = "world";
-    }
-
-    virtual bool GetOutput(std::string& output,
-                           MimeType& mime,
-                           const std::string& key) ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-  };
-
-
-  class DummyInstancesJob : public SetOfInstancesJob
-  {
-  private:
-    bool   trailingStepDone_;
-    
-  protected:
-    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
-    {
-      return (instance != "nope");
-    }
-
-    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE
-    {
-      if (HasTrailingStep())
-      {
-        if (trailingStepDone_)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-        else
-        {
-          trailingStepDone_ = true;
-          return true;
-        }
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-  public:
-    DummyInstancesJob() :
-      trailingStepDone_(false)
-    {
-    }
-    
-    DummyInstancesJob(const Json::Value& value) :
-      SetOfInstancesJob(value)
-    {
-      if (HasTrailingStep())
-      {
-        trailingStepDone_ = (GetPosition() == GetCommandsCount());
-      }
-      else
-      {
-        trailingStepDone_ = false;
-      }
-    }
-
-    bool IsTrailingStepDone() const
-    {
-      return trailingStepDone_;
-    }
-    
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
-    {
-      s = "DummyInstancesJob";
-    }
-  };
-
-
-  class DummyUnserializer : public GenericJobUnserializer
-  {
-  public:
-    virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE
-    {
-      if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob")
-      {
-        return new DummyInstancesJob(value);
-      }
-      else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob")
-      {
-        return new DummyJob;
-      }
-      else
-      {
-        return GenericJobUnserializer::UnserializeJob(value);
-      }
-    }
-  };
-
-    
-  class DynamicInteger : public IDynamicObject
-  {
-  private:
-    int value_;
-    std::set<int>& target_;
-
-  public:
-    DynamicInteger(int value, std::set<int>& target) : 
-      value_(value), target_(target)
-    {
-    }
-
-    int GetValue() const
-    {
-      return value_;
-    }
-  };
-}
-
-
-TEST(MultiThreading, SharedMessageQueueBasic)
-{
-  std::set<int> s;
-
-  SharedMessageQueue q;
-  ASSERT_TRUE(q.WaitEmpty(0));
-  q.Enqueue(new DynamicInteger(10, s));
-  ASSERT_FALSE(q.WaitEmpty(1));
-  q.Enqueue(new DynamicInteger(20, s));
-  q.Enqueue(new DynamicInteger(30, s));
-  q.Enqueue(new DynamicInteger(40, s));
-
-  std::unique_ptr<DynamicInteger> i;
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue());
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue());
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue());
-  ASSERT_FALSE(q.WaitEmpty(1));
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue());
-  ASSERT_TRUE(q.WaitEmpty(0));
-  ASSERT_EQ(NULL, q.Dequeue(1));
-}
-
-
-TEST(MultiThreading, SharedMessageQueueClean)
-{
-  std::set<int> s;
-
-  try
-  {
-    SharedMessageQueue q;
-    q.Enqueue(new DynamicInteger(10, s));
-    q.Enqueue(new DynamicInteger(20, s));  
-    throw OrthancException(ErrorCode_InternalError);
-  }
-  catch (OrthancException&)
-  {
-  }
-}
-
-
-
-
-static bool CheckState(JobsRegistry& registry,
-                       const std::string& id,
-                       JobState state)
-{
-  JobState s;
-  if (registry.GetState(s, id))
-  {
-    return state == s;
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-static bool CheckErrorCode(JobsRegistry& registry,
-                           const std::string& id,
-                           ErrorCode code)
-{
-  JobInfo s;
-  if (registry.GetJobInfo(s, id))
-  {
-    return code == s.GetStatus().GetErrorCode();
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-TEST(JobsRegistry, Priority)
-{
-  JobsRegistry registry(10);
-
-  std::string i1, i2, i3, i4;
-  registry.Submit(i1, new DummyJob(), 10);
-  registry.Submit(i2, new DummyJob(), 30);
-  registry.Submit(i3, new DummyJob(), 20);
-  registry.Submit(i4, new DummyJob(), 5);  
-
-  registry.SetMaxCompletedJobs(2);
-
-  std::set<std::string> id;
-  registry.ListJobs(id);
-
-  ASSERT_EQ(4u, id.size());
-  ASSERT_TRUE(id.find(i1) != id.end());
-  ASSERT_TRUE(id.find(i2) != id.end());
-  ASSERT_TRUE(id.find(i3) != id.end());
-  ASSERT_TRUE(id.find(i4) != id.end());
-
-  ASSERT_TRUE(CheckState(registry, i2, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    ASSERT_EQ(30, job.GetPriority());
-    ASSERT_EQ(i2, job.GetId());
-
-    ASSERT_TRUE(CheckState(registry, i2, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, i2, JobState_Failure));
-  ASSERT_TRUE(CheckState(registry, i3, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    ASSERT_EQ(20, job.GetPriority());
-    ASSERT_EQ(i3, job.GetId());
-
-    job.MarkSuccess();
-
-    ASSERT_TRUE(CheckState(registry, i3, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, i3, JobState_Success));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    ASSERT_EQ(10, job.GetPriority());
-    ASSERT_EQ(i1, job.GetId());
-  }
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    ASSERT_EQ(5, job.GetPriority());
-    ASSERT_EQ(i4, job.GetId());
-  }
-
-  {
-    JobsRegistry::RunningJob job(registry, 1);
-    ASSERT_FALSE(job.IsValid());
-  }
-
-  JobState s;
-  ASSERT_TRUE(registry.GetState(s, i1));
-  ASSERT_FALSE(registry.GetState(s, i2));  // Removed because oldest
-  ASSERT_FALSE(registry.GetState(s, i3));  // Removed because second oldest
-  ASSERT_TRUE(registry.GetState(s, i4));
-
-  registry.SetMaxCompletedJobs(1);  // (*)
-  ASSERT_FALSE(registry.GetState(s, i1));  // Just discarded by (*)
-  ASSERT_TRUE(registry.GetState(s, i4));
-}
-
-
-TEST(JobsRegistry, Simultaneous)
-{
-  JobsRegistry registry(10);
-
-  std::string i1, i2;
-  registry.Submit(i1, new DummyJob(), 20);
-  registry.Submit(i2, new DummyJob(), 10);
-
-  ASSERT_TRUE(CheckState(registry, i1, JobState_Pending));
-  ASSERT_TRUE(CheckState(registry, i2, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job1(registry, 0);
-    JobsRegistry::RunningJob job2(registry, 0);
-
-    ASSERT_TRUE(job1.IsValid());
-    ASSERT_TRUE(job2.IsValid());
-
-    job1.MarkFailure();
-    job2.MarkSuccess();
-
-    ASSERT_TRUE(CheckState(registry, i1, JobState_Running));
-    ASSERT_TRUE(CheckState(registry, i2, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, i1, JobState_Failure));
-  ASSERT_TRUE(CheckState(registry, i2, JobState_Success));
-}
-
-
-TEST(JobsRegistry, Resubmit)
-{
-  JobsRegistry registry(10);
-
-  std::string id;
-  registry.Submit(id, new DummyJob(), 10);
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  registry.Resubmit(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    job.MarkFailure();
-
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-
-    registry.Resubmit(id);
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
-
-  registry.Resubmit(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    ASSERT_EQ(id, job.GetId());
-
-    job.MarkSuccess();
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
-
-  registry.Resubmit(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
-}
-
-
-TEST(JobsRegistry, Retry)
-{
-  JobsRegistry registry(10);
-
-  std::string id;
-  registry.Submit(id, new DummyJob(), 10);
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    job.MarkRetry(0);
-
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
-
-  registry.Resubmit(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
-  
-  registry.ScheduleRetries();
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    job.MarkSuccess();
-
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
-}
-
-
-TEST(JobsRegistry, PausePending)
-{
-  JobsRegistry registry(10);
-
-  std::string id;
-  registry.Submit(id, new DummyJob(), 10);
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  registry.Pause(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
-
-  registry.Pause(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
-
-  registry.Resubmit(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
-
-  registry.Resume(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-}
-
-
-TEST(JobsRegistry, PauseRunning)
-{
-  JobsRegistry registry(10);
-
-  std::string id;
-  registry.Submit(id, new DummyJob(), 10);
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-
-    registry.Resubmit(id);
-    job.MarkPause();
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
-
-  registry.Resubmit(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
-
-  registry.Resume(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-
-    job.MarkSuccess();
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
-}
-
-
-TEST(JobsRegistry, PauseRetry)
-{
-  JobsRegistry registry(10);
-
-  std::string id;
-  registry.Submit(id, new DummyJob(), 10);
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-
-    job.MarkRetry(0);
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
-
-  registry.Pause(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
-
-  registry.Resume(id);
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-
-    job.MarkSuccess();
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
-}
-
-
-TEST(JobsRegistry, Cancel)
-{
-  JobsRegistry registry(10);
-
-  std::string id;
-  registry.Submit(id, new DummyJob(), 10);
-
-  ASSERT_FALSE(registry.Cancel("nope"));
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
-            
-  ASSERT_TRUE(registry.Cancel(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-  
-  ASSERT_TRUE(registry.Cancel(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-  
-  ASSERT_TRUE(registry.Resubmit(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-  
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-
-    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
-
-    job.MarkSuccess();
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
-
-  ASSERT_TRUE(registry.Cancel(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
-
-  registry.Submit(id, new DummyJob(), 10);
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    ASSERT_EQ(id, job.GetId());
-
-    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-
-    job.MarkCanceled();
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-
-  ASSERT_TRUE(registry.Resubmit(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-
-  ASSERT_TRUE(registry.Pause(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-
-  ASSERT_TRUE(registry.Cancel(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-
-  ASSERT_TRUE(registry.Resubmit(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-
-  {
-    JobsRegistry::RunningJob job(registry, 0);
-    ASSERT_TRUE(job.IsValid());
-    ASSERT_EQ(id, job.GetId());
-
-    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
-    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
-
-    job.MarkRetry(500);
-  }
-
-  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
-
-  ASSERT_TRUE(registry.Cancel(id));
-  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
-  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
-}
-
-
-
-TEST(JobsEngine, SubmitAndWait)
-{
-  JobsEngine engine(10);
-  engine.SetThreadSleep(10);
-  engine.SetWorkersCount(3);
-  engine.Start();
-
-  Json::Value content = Json::nullValue;
-  engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10);
-  ASSERT_EQ(Json::objectValue, content.type());
-  ASSERT_EQ("world", content["hello"].asString());
-
-  content = Json::nullValue;
-  ASSERT_THROW(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10), OrthancException);
-  ASSERT_EQ(Json::nullValue, content.type());
-
-  engine.Stop();
-}
-
-
-TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
-{
-  JobsEngine engine(10);
-  engine.SetThreadSleep(10);
-  engine.SetWorkersCount(3);
-  engine.Start();
-
-  std::string id;
-  SequenceOfOperationsJob* job = NULL;
-
-  {
-    std::unique_ptr<SequenceOfOperationsJob> a(new SequenceOfOperationsJob);
-    job = a.get();
-    engine.GetRegistry().Submit(id, a.release(), 0);
-  }
-
-  boost::this_thread::sleep(boost::posix_time::milliseconds(500));
-
-  {
-    SequenceOfOperationsJob::Lock lock(*job);
-    size_t i = lock.AddOperation(new LogJobOperation);
-    size_t j = lock.AddOperation(new LogJobOperation);
-    size_t k = lock.AddOperation(new LogJobOperation);
-
-    StringOperationValue a("Hello");
-    StringOperationValue b("World");
-    lock.AddInput(i, a);
-    lock.AddInput(i, b);
-    
-    lock.Connect(i, j);
-    lock.Connect(j, k);
-  }
-
-  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
-
-  engine.Stop();
-
-}
-
-
-TEST(JobsEngine, DISABLED_Lua)
-{
-  JobsEngine engine(10);
-  engine.SetThreadSleep(10);
-  engine.SetWorkersCount(2);
-  engine.Start();
-
-  LuaJobManager lua;
-  lua.SetMaxOperationsPerJob(5);
-  lua.SetTrailingOperationTimeout(200);
-
-  for (size_t i = 0; i < 30; i++)
-  {
-    boost::this_thread::sleep(boost::posix_time::milliseconds(150));
-
-    LuaJobManager::Lock lock(lua, engine);
-    size_t a = lock.AddLogOperation();
-    size_t b = lock.AddLogOperation();
-    size_t c = lock.AddSystemCallOperation("echo");
-    lock.AddStringInput(a, boost::lexical_cast<std::string>(i));
-    lock.AddNullInput(a);
-    lock.Connect(a, b);
-    lock.Connect(a, c);
-  }
-
-  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
-
-  engine.Stop();
-}
-
-
-static bool CheckSameJson(const Json::Value& a,
-                          const Json::Value& b)
-{
-  std::string s = a.toStyledString();
-  std::string t = b.toStyledString();
-
-  if (s == t)
-  {
-    return true;
-  }
-  else
-  {
-    LOG(ERROR) << "Expected serialization: " << s;
-    LOG(ERROR) << "Actual serialization: " << t;
-    return false;
-  }
-}
-
-
-static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
-                                         IJob& job)
-{
-  Json::Value a = 42;
-  
-  if (!job.Serialize(a))
-  {
-    return false;
-  }
-  else
-  {
-    std::unique_ptr<IJob> unserialized(unserializer.UnserializeJob(a));
-  
-    Json::Value b = 43;
-    if (unserialized->Serialize(b))
-    {
-      return (CheckSameJson(a, b));
-    }
-    else
-    {
-      return false;
-    }
-  }
-}
-
-
-static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
-                                          SetOfInstancesJob& job)
-{
-  Json::Value a = 42;
-  
-  if (!job.Serialize(a))
-  {
-    return false;
-  }
-  else
-  {
-    std::unique_ptr<SetOfInstancesJob> unserialized
-      (dynamic_cast<SetOfInstancesJob*>(unserializer.UnserializeJob(a)));
-  
-    Json::Value b = 43;
-    if (unserialized->Serialize(b))
-    {    
-      return (CheckSameJson(a, b) &&
-              job.HasTrailingStep() == unserialized->HasTrailingStep() &&
-              job.GetPosition() == unserialized->GetPosition() &&
-              job.GetInstancesCount() == unserialized->GetInstancesCount() &&
-              job.GetCommandsCount() == unserialized->GetCommandsCount());
-    }
-    else
-    {
-      return false;
-    }
-  }
-}
-
-
-static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
-                                         IJobOperation& operation)
-{
-  Json::Value a = 42;
-  operation.Serialize(a);
-  
-  std::unique_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a));
-  
-  Json::Value b = 43;
-  unserialized->Serialize(b);
-
-  return CheckSameJson(a, b);
-}
-
-
-static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
-                                         JobOperationValue& value)
-{
-  Json::Value a = 42;
-  value.Serialize(a);
-  
-  std::unique_ptr<JobOperationValue> unserialized(unserializer.UnserializeValue(a));
-  
-  Json::Value b = 43;
-  unserialized->Serialize(b);
-
-  return CheckSameJson(a, b);
-}
-
-
-TEST(JobsSerialization, BadFileFormat)
-{
-  GenericJobUnserializer unserializer;
-
-  Json::Value s;
-
-  s = Json::objectValue;
-  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-
-  s = Json::arrayValue;
-  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-
-  s = "hello";
-  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-
-  s = 42;
-  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-}
-
-
-TEST(JobsSerialization, JobOperationValues)
-{
-  Json::Value s;
-
-  {
-    JobOperationValues values;
-    values.Append(new NullOperationValue);
-    values.Append(new StringOperationValue("hello"));
-    values.Append(new StringOperationValue("world"));
-
-    s = 42;
-    values.Serialize(s);
-  }
-
-  {
-    GenericJobUnserializer unserializer;
-    std::unique_ptr<JobOperationValues> values(JobOperationValues::Unserialize(unserializer, s));
-    ASSERT_EQ(3u, values->GetSize());
-    ASSERT_EQ(JobOperationValue::Type_Null, values->GetValue(0).GetType());
-    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(1).GetType());
-    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(2).GetType());
-
-    ASSERT_EQ("hello", dynamic_cast<const StringOperationValue&>(values->GetValue(1)).GetContent());
-    ASSERT_EQ("world", dynamic_cast<const StringOperationValue&>(values->GetValue(2)).GetContent());
-  }
-}
-
-
-TEST(JobsSerialization, GenericValues)
-{
-  GenericJobUnserializer unserializer;
-  Json::Value s;
-
-  {
-    NullOperationValue null;
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, null));
-    null.Serialize(s);
-  }
-
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-
-  std::unique_ptr<JobOperationValue> value;
-  value.reset(unserializer.UnserializeValue(s));
-  
-  ASSERT_EQ(JobOperationValue::Type_Null, value->GetType());
-
-  {
-    StringOperationValue str("Hello");
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, str));
-    str.Serialize(s);
-  }
-
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-  value.reset(unserializer.UnserializeValue(s));
-
-  ASSERT_EQ(JobOperationValue::Type_String, value->GetType());
-  ASSERT_EQ("Hello", dynamic_cast<StringOperationValue&>(*value).GetContent());
-}
-
-
-TEST(JobsSerialization, GenericOperations)
-{   
-  DummyUnserializer unserializer;
-  Json::Value s;
-
-  {
-    LogJobOperation operation;
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-    operation.Serialize(s);
-  }
-
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-
-  {
-    std::unique_ptr<IJobOperation> operation;
-    operation.reset(unserializer.UnserializeOperation(s));
-
-    // Make sure that we have indeed unserialized a log operation
-    Json::Value dummy;
-    ASSERT_THROW(dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy), std::bad_cast);
-    dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy);
-  }
-}
-
-
-TEST(JobsSerialization, GenericJobs)
-{   
-  Json::Value s;
-
-  // This tests SetOfInstancesJob
-  
-  {
-    DummyInstancesJob job;
-    job.SetDescription("description");
-    job.AddInstance("hello");
-    job.AddInstance("nope");
-    job.AddInstance("world");
-    job.SetPermissive(true);
-    ASSERT_THROW(job.Step("jobId"), OrthancException);  // Not started yet
-    ASSERT_FALSE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    job.Start();
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    DummyUnserializer unserializer;
-    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    const DummyInstancesJob& tmp = dynamic_cast<const DummyInstancesJob&>(*job);
-    ASSERT_FALSE(tmp.IsStarted());
-    ASSERT_TRUE(tmp.IsPermissive());
-    ASSERT_EQ("description", tmp.GetDescription());
-    ASSERT_EQ(3u, tmp.GetInstancesCount());
-    ASSERT_EQ(2u, tmp.GetPosition());
-    ASSERT_EQ(1u, tmp.GetFailedInstances().size());
-    ASSERT_EQ("hello", tmp.GetInstance(0));
-    ASSERT_EQ("nope", tmp.GetInstance(1));
-    ASSERT_EQ("world", tmp.GetInstance(2));
-    ASSERT_TRUE(tmp.IsFailedInstance("nope"));
-  }
-
-  // SequenceOfOperationsJob
-
-  {
-    SequenceOfOperationsJob job;
-    job.SetDescription("hello");
-
-    {
-      SequenceOfOperationsJob::Lock lock(job);
-      size_t a = lock.AddOperation(new LogJobOperation);
-      size_t b = lock.AddOperation(new LogJobOperation);
-      lock.Connect(a, b);
-
-      StringOperationValue s1("hello");
-      StringOperationValue s2("world");
-      lock.AddInput(a, s1);
-      lock.AddInput(a, s2);
-      lock.SetTrailingOperationTimeout(300);
-    }
-
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-
-    {
-      GenericJobUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
-    }
-    
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    GenericJobUnserializer unserializer;
-    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    std::string tmp;
-    dynamic_cast<SequenceOfOperationsJob&>(*job).GetDescription(tmp);
-    ASSERT_EQ("hello", tmp);
-  }  
-}
-
-
-static bool IsSameTagValue(ParsedDicomFile& dicom1,
-                           ParsedDicomFile& dicom2,
-                           DicomTag tag)
-{
-  std::string a, b;
-  return (dicom1.GetTagValue(a, tag) &&
-          dicom2.GetTagValue(b, tag) &&
-          (a == b));
-}
-                       
-
-
-TEST(JobsSerialization, DicomModification)
-{   
-  Json::Value s;
-
-  ParsedDicomFile source(true);
-  source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false, "");
-  source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false, "");
-  source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false, "");
-
-  std::unique_ptr<ParsedDicomFile> modified(source.Clone(true));
-
-  {
-    DicomModification modification;
-    modification.SetLevel(ResourceType_Series);
-    modification.Clear(DICOM_TAG_STUDY_DESCRIPTION);
-    modification.Remove(DICOM_TAG_SERIES_DESCRIPTION);
-    modification.Replace(DICOM_TAG_PATIENT_NAME, "Test 4", true);
-
-    modification.Apply(*modified);
-
-    s = 42;
-    modification.Serialize(s);
-  }
-
-  {
-    DicomModification modification(s);
-    ASSERT_EQ(ResourceType_Series, modification.GetLevel());
-    
-    std::unique_ptr<ParsedDicomFile> second(source.Clone(true));
-    modification.Apply(*second);
-
-    std::string s;
-    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_STUDY_DESCRIPTION));
-    ASSERT_TRUE(s.empty());
-    ASSERT_FALSE(second->GetTagValue(s, DICOM_TAG_SERIES_DESCRIPTION));
-    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_PATIENT_NAME));
-    ASSERT_EQ("Test 4", s);
-
-    ASSERT_TRUE(IsSameTagValue(source, *modified, DICOM_TAG_STUDY_INSTANCE_UID));
-    ASSERT_TRUE(IsSameTagValue(source, *second, DICOM_TAG_STUDY_INSTANCE_UID));
-
-    ASSERT_FALSE(IsSameTagValue(source, *second, DICOM_TAG_SERIES_INSTANCE_UID));
-    ASSERT_TRUE(IsSameTagValue(*modified, *second, DICOM_TAG_SERIES_INSTANCE_UID));
-  }
-}
-
-
-TEST(JobsSerialization, DicomInstanceOrigin)
-{   
-  Json::Value s;
-  std::string t;
-
-  {
-    DicomInstanceOrigin origin;
-
-    s = 42;
-    origin.Serialize(s);
-  }
-
-  {
-    DicomInstanceOrigin origin(s);
-    ASSERT_EQ(RequestOrigin_Unknown, origin.GetRequestOrigin());
-    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
-    ASSERT_FALSE(origin.LookupRemoteIp(t));
-    ASSERT_FALSE(origin.LookupRemoteAet(t));
-    ASSERT_FALSE(origin.LookupCalledAet(t));
-    ASSERT_FALSE(origin.LookupHttpUsername(t));
-  }
-
-  {
-    DicomInstanceOrigin origin(DicomInstanceOrigin::FromDicomProtocol("host", "aet", "called"));
-
-    s = 42;
-    origin.Serialize(s);
-  }
-
-  {
-    DicomInstanceOrigin origin(s);
-    ASSERT_EQ(RequestOrigin_DicomProtocol, origin.GetRequestOrigin());
-    ASSERT_EQ("aet", std::string(origin.GetRemoteAetC()));
-    ASSERT_TRUE(origin.LookupRemoteIp(t));   ASSERT_EQ("host", t);
-    ASSERT_TRUE(origin.LookupRemoteAet(t));  ASSERT_EQ("aet", t);
-    ASSERT_TRUE(origin.LookupCalledAet(t));  ASSERT_EQ("called", t);
-    ASSERT_FALSE(origin.LookupHttpUsername(t));
-  }
-
-  {
-    DicomInstanceOrigin origin(DicomInstanceOrigin::FromHttp("host", "username"));
-
-    s = 42;
-    origin.Serialize(s);
-  }
-
-  {
-    DicomInstanceOrigin origin(s);
-    ASSERT_EQ(RequestOrigin_RestApi, origin.GetRequestOrigin());
-    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
-    ASSERT_TRUE(origin.LookupRemoteIp(t));     ASSERT_EQ("host", t);
-    ASSERT_FALSE(origin.LookupRemoteAet(t));
-    ASSERT_FALSE(origin.LookupCalledAet(t));
-    ASSERT_TRUE(origin.LookupHttpUsername(t)); ASSERT_EQ("username", t);
-  }
-
-  {
-    DicomInstanceOrigin origin(DicomInstanceOrigin::FromLua());
-
-    s = 42;
-    origin.Serialize(s);
-  }
-
-  {
-    DicomInstanceOrigin origin(s);
-    ASSERT_EQ(RequestOrigin_Lua, origin.GetRequestOrigin());
-    ASSERT_FALSE(origin.LookupRemoteIp(t));
-    ASSERT_FALSE(origin.LookupRemoteAet(t));
-    ASSERT_FALSE(origin.LookupCalledAet(t));
-    ASSERT_FALSE(origin.LookupHttpUsername(t));
-  }
-
-  {
-    DicomInstanceOrigin origin(DicomInstanceOrigin::FromPlugins());
-
-    s = 42;
-    origin.Serialize(s);
-  }
-
-  {
-    DicomInstanceOrigin origin(s);
-    ASSERT_EQ(RequestOrigin_Plugins, origin.GetRequestOrigin());
-    ASSERT_FALSE(origin.LookupRemoteIp(t));
-    ASSERT_FALSE(origin.LookupRemoteAet(t));
-    ASSERT_FALSE(origin.LookupCalledAet(t));
-    ASSERT_FALSE(origin.LookupHttpUsername(t));
-  }
-}
-
-
-namespace
-{
-  class OrthancJobsSerialization : public testing::Test
-  {
-  private:
-    MemoryStorageArea              storage_;
-    SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
-    std::unique_ptr<ServerContext>   context_;
-
-  public:
-    OrthancJobsSerialization()
-    {
-      db_.Open();
-      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
-      context_->SetupJobsEngine(true, false);
-    }
-
-    virtual ~OrthancJobsSerialization() ORTHANC_OVERRIDE
-    {
-      context_->Stop();
-      context_.reset(NULL);
-      db_.Close();
-    }
-
-    ServerContext& GetContext() 
-    {
-      return *context_;
-    }
-
-    bool CreateInstance(std::string& id)
-    {
-      // Create a sample DICOM file
-      ParsedDicomFile dicom(true);
-      dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"),
-                    false, DicomReplaceMode_InsertIfAbsent, "");
-
-      DicomInstanceToStore toStore;
-      toStore.SetParsedDicomFile(dicom);
-
-      return (context_->Store(id, toStore, StoreInstanceMode_Default) == StoreStatus_Success);
-    }
-  };
-}
-
-
-TEST_F(OrthancJobsSerialization, Values)
-{
-  std::string id;
-  ASSERT_TRUE(CreateInstance(id));
-
-  Json::Value s;
-  OrthancJobUnserializer unserializer(GetContext());
-    
-  {
-    DicomInstanceOperationValue instance(GetContext(), id);
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, instance));
-    instance.Serialize(s);
-  }
-
-  std::unique_ptr<JobOperationValue> value;
-  value.reset(unserializer.UnserializeValue(s));
-  ASSERT_EQ(JobOperationValue::Type_DicomInstance, value->GetType());
-  ASSERT_EQ(id, dynamic_cast<DicomInstanceOperationValue&>(*value).GetId());
-
-  {
-    std::string content;
-    dynamic_cast<DicomInstanceOperationValue&>(*value).ReadDicom(content);
-
-    ParsedDicomFile dicom(content);
-    ASSERT_TRUE(dicom.GetTagValue(content, DICOM_TAG_PATIENT_NAME));
-    ASSERT_EQ("JODOGNE", content);
-  }
-}
-
-
-TEST_F(OrthancJobsSerialization, Operations)
-{
-  std::string id;
-  ASSERT_TRUE(CreateInstance(id));
-
-  Json::Value s;
-  OrthancJobUnserializer unserializer(GetContext()); 
-
-  // DeleteResourceOperation
-  
-  {
-    DeleteResourceOperation operation(GetContext());
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-    operation.Serialize(s);
-  }
-
-  std::unique_ptr<IJobOperation> operation;
-
-  {
-    operation.reset(unserializer.UnserializeOperation(s));
-
-    Json::Value dummy;
-    ASSERT_THROW(dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy), std::bad_cast);
-    dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy);
-  }
-
-  // StorePeerOperation
-
-  {
-    WebServiceParameters peer;
-    peer.SetUrl("http://localhost/");
-    peer.SetCredentials("username", "password");
-    peer.SetPkcs11Enabled(true);
-
-    StorePeerOperation operation(peer);
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-    operation.Serialize(s);
-  }
-
-  {
-    operation.reset(unserializer.UnserializeOperation(s));
-
-    const StorePeerOperation& tmp = dynamic_cast<StorePeerOperation&>(*operation);
-    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
-    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
-    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
-    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
-  }
-
-  // StoreScuOperation
-
-  {
-    TimeoutDicomConnectionManager luaManager;
-    
-    {
-      RemoteModalityParameters modality;
-      modality.SetApplicationEntityTitle("REMOTE");
-      modality.SetHost("192.168.1.1");
-      modality.SetPortNumber(1000);
-      modality.SetManufacturer(ModalityManufacturer_StoreScp);
-
-      StoreScuOperation operation(GetContext(), luaManager, "TEST", modality);
-
-      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-      operation.Serialize(s);
-    }
-
-    {
-      operation.reset(unserializer.UnserializeOperation(s));
-
-      const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation);
-      ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
-      ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
-      ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
-      ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
-      ASSERT_EQ("TEST", tmp.GetLocalAet());
-    }
-  }
-
-  // SystemCallOperation
-
-  {
-    SystemCallOperation operation(std::string("echo"));
-    operation.AddPreArgument("a");
-    operation.AddPreArgument("b");
-    operation.AddPostArgument("c");
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-    operation.Serialize(s);
-  }
-
-  {
-    operation.reset(unserializer.UnserializeOperation(s));
-
-    const SystemCallOperation& tmp = dynamic_cast<SystemCallOperation&>(*operation);
-    ASSERT_EQ("echo", tmp.GetCommand());
-    ASSERT_EQ(2u, tmp.GetPreArgumentsCount());
-    ASSERT_EQ(1u, tmp.GetPostArgumentsCount());
-    ASSERT_EQ("a", tmp.GetPreArgument(0));
-    ASSERT_EQ("b", tmp.GetPreArgument(1));
-    ASSERT_EQ("c", tmp.GetPostArgument(0));
-  }
-
-  // ModifyInstanceOperation
-
-  {
-    std::unique_ptr<DicomModification> modification(new DicomModification);
-    modification->SetupAnonymization(DicomVersion_2008);
-    
-    ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release());
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-    operation.Serialize(s);
-  }
-
-  {
-    operation.reset(unserializer.UnserializeOperation(s));
-
-    const ModifyInstanceOperation& tmp = dynamic_cast<ModifyInstanceOperation&>(*operation);
-    ASSERT_EQ(RequestOrigin_Lua, tmp.GetRequestOrigin());
-    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
-  }
-}
-
-
-TEST_F(OrthancJobsSerialization, Jobs)
-{
-  Json::Value s;
-
-  // ArchiveJob
-
-  {
-    ArchiveJob job(GetContext(), false, false);
-    ASSERT_FALSE(job.Serialize(s));  // Cannot serialize this
-  }
-
-  // DicomModalityStoreJob
-
-  OrthancJobUnserializer unserializer(GetContext()); 
-
-  {
-    RemoteModalityParameters modality;
-    modality.SetApplicationEntityTitle("REMOTE");
-    modality.SetHost("192.168.1.1");
-    modality.SetPortNumber(1000);
-    modality.SetManufacturer(ModalityManufacturer_StoreScp);
-
-    DicomModalityStoreJob job(GetContext());
-    job.SetLocalAet("LOCAL");
-    job.SetRemoteModality(modality);
-    job.SetMoveOriginator("MOVESCU", 42);
-
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
-    ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_TRUE(tmp.HasMoveOriginator());
-    ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet());
-    ASSERT_EQ(42, tmp.GetMoveOriginatorId());
-  }
-
-  // OrthancPeerStoreJob
-
-  {
-    WebServiceParameters peer;
-    peer.SetUrl("http://localhost/");
-    peer.SetCredentials("username", "password");
-    peer.SetPkcs11Enabled(true);
-
-    OrthancPeerStoreJob job(GetContext());
-    job.SetPeer(peer);
-    
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
-    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
-    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
-    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
-    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
-    ASSERT_FALSE(tmp.IsTranscode());
-    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
-  }
-
-  {
-    OrthancPeerStoreJob job(GetContext());
-    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
-    job.SetTranscode("1.2.840.10008.1.2.4.50");
-    
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
-    ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl());
-    ASSERT_EQ("", tmp.GetPeer().GetUsername());
-    ASSERT_EQ("", tmp.GetPeer().GetPassword());
-    ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled());
-    ASSERT_TRUE(tmp.IsTranscode());
-    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
-  }
-
-  // ResourceModificationJob
-
-  {
-    std::unique_ptr<DicomModification> modification(new DicomModification);
-    modification->SetupAnonymization(DicomVersion_2008);    
-
-    ResourceModificationJob job(GetContext());
-    job.SetModification(modification.release(), ResourceType_Patient, true);
-    job.SetOrigin(DicomInstanceOrigin::FromLua());
-
-    job.AddTrailingStep();  // Necessary since 1.7.0
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
-    ASSERT_TRUE(tmp.IsAnonymization());
-    ASSERT_FALSE(tmp.IsTranscode());
-    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
-    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
-    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
-  }
-
-  {
-    ResourceModificationJob job(GetContext());
-    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
-    job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
-
-    job.AddTrailingStep();  // Necessary since 1.7.0
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
-    ASSERT_FALSE(tmp.IsAnonymization());
-    ASSERT_TRUE(tmp.IsTranscode());
-    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
-    ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin());
-  }
-
-  // SplitStudyJob
-
-  std::string instance;
-  ASSERT_TRUE(CreateInstance(instance));
-
-  std::string study, series;
-
-  {
-    ServerContext::DicomCacheLocker lock(GetContext(), instance);
-    study = lock.GetDicom().GetHasher().HashStudy();
-    series = lock.GetDicom().GetHasher().HashSeries();
-  }
-
-  {
-    std::list<std::string> tmp;
-    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
-    ASSERT_EQ(1u, tmp.size());
-    ASSERT_EQ(study, tmp.front());
-    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
-    ASSERT_EQ(1u, tmp.size());
-    ASSERT_EQ(series, tmp.front());
-  }
-
-  std::string study2;
-
-  {
-    std::string a, b;
-
-    {
-      ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
-
-      SplitStudyJob job(GetContext(), study);
-      job.SetKeepSource(true);
-      job.AddSourceSeries(series);
-      ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
-      job.SetOrigin(DicomInstanceOrigin::FromLua());
-      job.Replace(DICOM_TAG_PATIENT_NAME, "hello");
-      job.Remove(DICOM_TAG_PATIENT_BIRTH_DATE);
-      ASSERT_THROW(job.Replace(DICOM_TAG_SERIES_DESCRIPTION, "nope"), OrthancException);
-      ASSERT_THROW(job.Remove(DICOM_TAG_SERIES_DESCRIPTION), OrthancException);
-    
-      ASSERT_TRUE(job.GetTargetStudy().empty());
-      a = job.GetTargetStudyUid();
-      ASSERT_TRUE(job.LookupTargetSeriesUid(b, series));
-
-      job.AddTrailingStep();
-      job.Start();
-      ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-      ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-
-      study2 = job.GetTargetStudy();
-      ASSERT_FALSE(study2.empty());
-
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-      ASSERT_TRUE(job.Serialize(s));
-    }
-
-    {
-      std::unique_ptr<IJob> job;
-      job.reset(unserializer.UnserializeJob(s));
-
-      SplitStudyJob& tmp = dynamic_cast<SplitStudyJob&>(*job);
-      ASSERT_TRUE(tmp.IsKeepSource());
-      ASSERT_EQ(study, tmp.GetSourceStudy());
-      ASSERT_EQ(a, tmp.GetTargetStudyUid());
-      ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
-
-      std::string s;
-      ASSERT_EQ(study2, tmp.GetTargetStudy());
-      ASSERT_FALSE(tmp.LookupTargetSeriesUid(s, "nope"));
-      ASSERT_TRUE(tmp.LookupTargetSeriesUid(s, series));
-      ASSERT_EQ(b, s);
-
-      ASSERT_FALSE(tmp.LookupReplacement(s, DICOM_TAG_STUDY_DESCRIPTION));
-      ASSERT_TRUE(tmp.LookupReplacement(s, DICOM_TAG_PATIENT_NAME));
-      ASSERT_EQ("hello", s);
-      ASSERT_FALSE(tmp.IsRemoved(DICOM_TAG_PATIENT_NAME));
-      ASSERT_TRUE(tmp.IsRemoved(DICOM_TAG_PATIENT_BIRTH_DATE));
-    }
-  }
-
-  {
-    std::list<std::string> tmp;
-    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
-    ASSERT_EQ(2u, tmp.size());
-    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
-    ASSERT_EQ(2u, tmp.size());
-  }
-
-  // MergeStudyJob
-
-  {
-    ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
-
-    MergeStudyJob job(GetContext(), study);
-    job.SetKeepSource(true);
-    job.AddSource(study2);
-    ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
-    ASSERT_THROW(job.AddSourceStudy("nope"), OrthancException);
-    ASSERT_THROW(job.AddSource("nope"), OrthancException);
-    job.SetOrigin(DicomInstanceOrigin::FromLua());
-    
-    ASSERT_EQ(job.GetTargetStudy(), study);
-
-    job.AddTrailingStep();
-    job.Start();
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::list<std::string> tmp;
-    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
-    ASSERT_EQ(2u, tmp.size());
-    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
-    ASSERT_EQ(3u, tmp.size());
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    MergeStudyJob& tmp = dynamic_cast<MergeStudyJob&>(*job);
-    ASSERT_TRUE(tmp.IsKeepSource());
-    ASSERT_EQ(study, tmp.GetTargetStudy());
-    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
-  }
-}
-
-
-TEST(JobsSerialization, Registry)
-{   
-  Json::Value s;
-  std::string i1, i2;
-
-  {
-    JobsRegistry registry(10);
-    registry.Submit(i1, new DummyJob(), 10);
-    registry.Submit(i2, new SequenceOfOperationsJob(), 30);
-    registry.Serialize(s);
-  }
-
-  {
-    DummyUnserializer unserializer;
-    JobsRegistry registry(unserializer, s, 10);
-
-    Json::Value t;
-    registry.Serialize(t);
-    ASSERT_TRUE(CheckSameJson(s, t));
-  }
-}
-
-
-TEST(JobsSerialization, TrailingStep)
-{
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    ASSERT_EQ(0u, job.GetCommandsCount());
-    ASSERT_EQ(0u, job.GetInstancesCount());
-
-    job.Start();
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_FALSE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), OrthancException);
-  }
-
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    job.AddInstance("hello");
-    job.AddInstance("world");
-    ASSERT_EQ(2u, job.GetCommandsCount());
-    ASSERT_EQ(2u, job.GetInstancesCount());
-
-    job.Start();
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_FALSE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(2u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), OrthancException);
-  }
-
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    ASSERT_EQ(0u, job.GetInstancesCount());
-    ASSERT_EQ(0u, job.GetCommandsCount());
-    job.AddTrailingStep();
-    ASSERT_EQ(0u, job.GetInstancesCount());
-    ASSERT_EQ(1u, job.GetCommandsCount());
-
-    job.Start(); // This adds the trailing step
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_TRUE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_TRUE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), OrthancException);
-  }
-
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    job.AddInstance("hello");
-    ASSERT_EQ(1u, job.GetInstancesCount());
-    ASSERT_EQ(1u, job.GetCommandsCount());
-    job.AddTrailingStep();
-    ASSERT_EQ(1u, job.GetInstancesCount());
-    ASSERT_EQ(2u, job.GetCommandsCount());
-    
-    job.Start();
-    ASSERT_EQ(2u, job.GetCommandsCount());
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_TRUE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(2u, job.GetPosition());
-    ASSERT_TRUE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), OrthancException);
-  }
-}
-
-
-TEST(JobsSerialization, RemoteModalityParameters)
-{
-  Json::Value s;
-
-  {
-    RemoteModalityParameters modality;
-    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
-    modality.Serialize(s, false);
-    ASSERT_EQ(Json::arrayValue, s.type());
-  }
-
-  {
-    RemoteModalityParameters modality(s);
-    ASSERT_EQ("ORTHANC", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("127.0.0.1", modality.GetHost());
-    ASSERT_EQ(104u, modality.GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, modality.GetManufacturer());
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-
-  s = Json::nullValue;
-
-  {
-    RemoteModalityParameters modality;
-    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
-    ASSERT_THROW(modality.SetPortNumber(0), OrthancException);
-    ASSERT_THROW(modality.SetPortNumber(65535), OrthancException);
-    modality.SetApplicationEntityTitle("HELLO");
-    modality.SetHost("world");
-    modality.SetPortNumber(45);
-    modality.SetManufacturer(ModalityManufacturer_GenericNoWildcardInDates);
-    modality.Serialize(s, true);
-    ASSERT_EQ(Json::objectValue, s.type());
-  }
-
-  {
-    RemoteModalityParameters modality(s);
-    ASSERT_EQ("HELLO", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("world", modality.GetHost());
-    ASSERT_EQ(45u, modality.GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_GenericNoWildcardInDates, modality.GetManufacturer());
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Echo));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Find));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Get));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Store));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_Move));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-
-  s["Port"] = "46";
-  
-  {
-    RemoteModalityParameters modality(s);
-    ASSERT_EQ(46u, modality.GetPortNumber());
-  }
-
-  s["Port"] = -1;     ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
-  s["Port"] = 65535;  ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
-  s["Port"] = "nope"; ASSERT_THROW(RemoteModalityParameters m(s), OrthancException);
-
-  std::set<DicomRequestType> operations;
-  operations.insert(DicomRequestType_Echo);
-  operations.insert(DicomRequestType_Find);
-  operations.insert(DicomRequestType_Get);
-  operations.insert(DicomRequestType_Move);
-  operations.insert(DicomRequestType_Store);
-  operations.insert(DicomRequestType_NAction);
-  operations.insert(DicomRequestType_NEventReport);
-
-  ASSERT_EQ(7u, operations.size());
-
-  for (std::set<DicomRequestType>::const_iterator 
-         it = operations.begin(); it != operations.end(); ++it)
-  {
-    {
-      RemoteModalityParameters modality;
-      modality.SetRequestAllowed(*it, false);
-      ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
-
-      modality.Serialize(s, false);
-      ASSERT_EQ(Json::objectValue, s.type());
-    }
-
-    {
-      RemoteModalityParameters modality(s);
-
-      ASSERT_FALSE(modality.IsRequestAllowed(*it));
-
-      for (std::set<DicomRequestType>::const_iterator 
-             it2 = operations.begin(); it2 != operations.end(); ++it2)
-      {
-        if (*it2 != *it)
-        {
-          ASSERT_TRUE(modality.IsRequestAllowed(*it2));
-        }
-      }
-    }
-  }
-
-  {
-    Json::Value s;
-    s["AllowStorageCommitment"] = false;
-    s["AET"] = "AET";
-    s["Host"] = "host";
-    s["Port"] = "104";
-    
-    RemoteModalityParameters modality(s);
-    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
-    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("host", modality.GetHost());
-    ASSERT_EQ(104u, modality.GetPortNumber());
-    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-
-  {
-    Json::Value s;
-    s["AllowNAction"] = false;
-    s["AllowNEventReport"] = true;
-    s["AET"] = "AET";
-    s["Host"] = "host";
-    s["Port"] = "104";
-    s["AllowTranscoding"] = false;
-    
-    RemoteModalityParameters modality(s);
-    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
-    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("host", modality.GetHost());
-    ASSERT_EQ(104u, modality.GetPortNumber());
-    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_FALSE(modality.IsTranscodingAllowed());
-  }
-
-  {
-    Json::Value s;
-    s["AllowNAction"] = true;
-    s["AllowNEventReport"] = true;
-    s["AET"] = "AET";
-    s["Host"] = "host";
-    s["Port"] = "104";
-    
-    RemoteModalityParameters modality(s);
-    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
-    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("host", modality.GetHost());
-    ASSERT_EQ(104u, modality.GetPortNumber());
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-}
-
-
-TEST_F(OrthancJobsSerialization, DicomAssociationParameters)
-{
-  Json::Value v;
-
-  {
-    v = Json::objectValue;
-    DicomAssociationParameters p;
-    p.SerializeJob(v);
-  }
-
-  {
-    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
-    ASSERT_EQ("ORTHANC", p.GetLocalApplicationEntityTitle());
-    ASSERT_EQ("ANY-SCP", p.GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ(104u, p.GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
-    ASSERT_EQ("127.0.0.1", p.GetRemoteModality().GetHost());
-    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), p.GetTimeout());
-  }
-
-  {
-    v = Json::objectValue;
-    DicomAssociationParameters p;
-    p.SetLocalApplicationEntityTitle("HELLO");
-    p.SetRemoteApplicationEntityTitle("WORLD");
-    p.SetRemotePort(42);
-    p.SetRemoteHost("MY_HOST");
-    p.SetTimeout(43);
-    p.SerializeJob(v);
-  }
-
-  {
-    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
-    ASSERT_EQ("HELLO", p.GetLocalApplicationEntityTitle());
-    ASSERT_EQ("WORLD", p.GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ(42u, p.GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
-    ASSERT_EQ("MY_HOST", p.GetRemoteModality().GetHost());
-    ASSERT_EQ(43u, p.GetTimeout());
-  }
-  
-  {
-    DicomModalityStoreJob job(GetContext());
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomModalityStoreJob> job(
-      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
-    ASSERT_FALSE(job->HasMoveOriginator());
-    ASSERT_THROW(job->GetMoveOriginatorAet(), OrthancException);
-    ASSERT_THROW(job->GetMoveOriginatorId(), OrthancException);
-    ASSERT_FALSE(job->HasStorageCommitment());
-  }
-  
-  {
-    RemoteModalityParameters r;
-    r.SetApplicationEntityTitle("HELLO");
-    r.SetPortNumber(42);
-    r.SetHost("MY_HOST");
-
-    DicomModalityStoreJob job(GetContext());
-    job.SetLocalAet("WORLD");
-    job.SetRemoteModality(r);
-    job.SetTimeout(43);
-    job.SetMoveOriginator("ORIGINATOR", 100);
-    job.EnableStorageCommitment(true);
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomModalityStoreJob> job(
-      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
-    ASSERT_TRUE(job->HasMoveOriginator());
-    ASSERT_EQ("ORIGINATOR", job->GetMoveOriginatorAet());
-    ASSERT_EQ(100, job->GetMoveOriginatorId());
-    ASSERT_TRUE(job->HasStorageCommitment());
-  }
-    
-  {
-    DicomMoveScuJob job(GetContext());
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomMoveScuJob> job(
-      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
-  }
-  
-  {
-    RemoteModalityParameters r;
-    r.SetApplicationEntityTitle("HELLO");
-    r.SetPortNumber(42);
-    r.SetHost("MY_HOST");
-
-    DicomMoveScuJob job(GetContext());
-    job.SetLocalAet("WORLD");
-    job.SetRemoteModality(r);
-    job.SetTimeout(43);
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomMoveScuJob> job(
-      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Jun 11 14:29:14 2020 +0200
@@ -0,0 +1,1099 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../../OrthancFramework/Sources/Compatibility.h"
+#include "../../OrthancFramework/Sources/FileStorage/MemoryStorageArea.h"
+#include "../../OrthancFramework/Sources/SerializationToolbox.h"
+
+#include "../Sources/Database/SQLiteDatabaseWrapper.h"
+#include "../Sources/ServerContext.h"
+#include "../Sources/ServerJobs/LuaJobManager.h"
+#include "../Sources/ServerJobs/OrthancJobUnserializer.h"
+
+#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
+#include "../Sources/ServerJobs/Operations/DicomInstanceOperationValue.h"
+#include "../Sources/ServerJobs/Operations/DeleteResourceOperation.h"
+#include "../Sources/ServerJobs/Operations/ModifyInstanceOperation.h"
+#include "../Sources/ServerJobs/Operations/StorePeerOperation.h"
+#include "../Sources/ServerJobs/Operations/StoreScuOperation.h"
+#include "../Sources/ServerJobs/Operations/SystemCallOperation.h"
+
+#include "../Sources/ServerJobs/ArchiveJob.h"
+#include "../Sources/ServerJobs/DicomModalityStoreJob.h"
+#include "../Sources/ServerJobs/DicomMoveScuJob.h"
+#include "../Sources/ServerJobs/MergeStudyJob.h"
+#include "../Sources/ServerJobs/OrthancPeerStoreJob.h"
+#include "../Sources/ServerJobs/ResourceModificationJob.h"
+#include "../Sources/ServerJobs/SplitStudyJob.h"
+
+
+using namespace Orthanc;
+
+namespace
+{
+  class DummyJob : public IJob
+  {
+  private:
+    bool         fails_;
+    unsigned int count_;
+    unsigned int steps_;
+
+  public:
+    DummyJob() :
+      fails_(false),
+      count_(0),
+      steps_(4)
+    {
+    }
+
+    explicit DummyJob(bool fails) :
+      fails_(fails),
+      count_(0),
+      steps_(4)
+    {
+    }
+
+    virtual void Start() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void Reset() ORTHANC_OVERRIDE
+    {
+    }
+    
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      if (fails_)
+      {
+        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL);
+      }
+      else if (count_ == steps_ - 1)
+      {
+        return JobStepResult::Success();
+      }
+      else
+      {
+        count_++;
+        return JobStepResult::Continue();
+      }
+    }
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual float GetProgress() ORTHANC_OVERRIDE
+    {
+      return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
+    }
+
+    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    {
+      type = "DummyJob";
+    }
+
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    {
+      value = Json::objectValue;
+      value["Type"] = "DummyJob";
+      return true;
+    }
+
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    {
+      value["hello"] = "world";
+    }
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+  };
+
+
+  class DummyInstancesJob : public SetOfInstancesJob
+  {
+  private:
+    bool   trailingStepDone_;
+    
+  protected:
+    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
+    {
+      return (instance != "nope");
+    }
+
+    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE
+    {
+      if (HasTrailingStep())
+      {
+        if (trailingStepDone_)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+        else
+        {
+          trailingStepDone_ = true;
+          return true;
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+  public:
+    DummyInstancesJob() :
+      trailingStepDone_(false)
+    {
+    }
+    
+    DummyInstancesJob(const Json::Value& value) :
+      SetOfInstancesJob(value)
+    {
+      if (HasTrailingStep())
+      {
+        trailingStepDone_ = (GetPosition() == GetCommandsCount());
+      }
+      else
+      {
+        trailingStepDone_ = false;
+      }
+    }
+
+    bool IsTrailingStepDone() const
+    {
+      return trailingStepDone_;
+    }
+    
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    {
+      s = "DummyInstancesJob";
+    }
+  };
+
+
+  class DummyUnserializer : public GenericJobUnserializer
+  {
+  public:
+    virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE
+    {
+      if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob")
+      {
+        return new DummyInstancesJob(value);
+      }
+      else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob")
+      {
+        return new DummyJob;
+      }
+      else
+      {
+        return GenericJobUnserializer::UnserializeJob(value);
+      }
+    }
+  };
+
+    
+  class DynamicInteger : public IDynamicObject
+  {
+  private:
+    int value_;
+    std::set<int>& target_;
+
+  public:
+    DynamicInteger(int value, std::set<int>& target) : 
+      value_(value), target_(target)
+    {
+    }
+
+    int GetValue() const
+    {
+      return value_;
+    }
+  };
+}
+
+
+TEST(JobsEngine, DISABLED_Lua)
+{
+  JobsEngine engine(10);
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(2);
+  engine.Start();
+
+  LuaJobManager lua;
+  lua.SetMaxOperationsPerJob(5);
+  lua.SetTrailingOperationTimeout(200);
+
+  for (size_t i = 0; i < 30; i++)
+  {
+    boost::this_thread::sleep(boost::posix_time::milliseconds(150));
+
+    LuaJobManager::Lock lock(lua, engine);
+    size_t a = lock.AddLogOperation();
+    size_t b = lock.AddLogOperation();
+    size_t c = lock.AddSystemCallOperation("echo");
+    lock.AddStringInput(a, boost::lexical_cast<std::string>(i));
+    lock.AddNullInput(a);
+    lock.Connect(a, b);
+    lock.Connect(a, c);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
+
+  engine.Stop();
+}
+
+
+static bool CheckSameJson(const Json::Value& a,
+                          const Json::Value& b)
+{
+  std::string s = a.toStyledString();
+  std::string t = b.toStyledString();
+
+  if (s == t)
+  {
+    return true;
+  }
+  else
+  {
+    LOG(ERROR) << "Expected serialization: " << s;
+    LOG(ERROR) << "Actual serialization: " << t;
+    return false;
+  }
+}
+
+
+static bool CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
+                                          SetOfInstancesJob& job)
+{
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::unique_ptr<SetOfInstancesJob> unserialized
+      (dynamic_cast<SetOfInstancesJob*>(unserializer.UnserializeJob(a)));
+  
+    Json::Value b = 43;
+    if (unserialized->Serialize(b))
+    {    
+      return (CheckSameJson(a, b) &&
+              job.HasTrailingStep() == unserialized->HasTrailingStep() &&
+              job.GetPosition() == unserialized->GetPosition() &&
+              job.GetInstancesCount() == unserialized->GetInstancesCount() &&
+              job.GetCommandsCount() == unserialized->GetCommandsCount());
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         IJobOperation& operation)
+{
+  Json::Value a = 42;
+  operation.Serialize(a);
+  
+  std::unique_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         JobOperationValue& value)
+{
+  Json::Value a = 42;
+  value.Serialize(a);
+  
+  std::unique_ptr<JobOperationValue> unserialized(unserializer.UnserializeValue(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
+
+
+TEST(JobsSerialization, GenericOperations)
+{   
+  DummyUnserializer unserializer;
+  Json::Value s;
+
+  {
+    LogJobOperation operation;
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+
+  {
+    std::unique_ptr<IJobOperation> operation;
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    // Make sure that we have indeed unserialized a log operation
+    Json::Value dummy;
+    ASSERT_THROW(dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy), std::bad_cast);
+    dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy);
+  }
+}
+
+
+TEST(JobsSerialization, DicomInstanceOrigin)
+{   
+  Json::Value s;
+  std::string t;
+
+  {
+    DicomInstanceOrigin origin;
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Unknown, origin.GetRequestOrigin());
+    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
+    ASSERT_FALSE(origin.LookupRemoteIp(t));
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromDicomProtocol("host", "aet", "called"));
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_DicomProtocol, origin.GetRequestOrigin());
+    ASSERT_EQ("aet", std::string(origin.GetRemoteAetC()));
+    ASSERT_TRUE(origin.LookupRemoteIp(t));   ASSERT_EQ("host", t);
+    ASSERT_TRUE(origin.LookupRemoteAet(t));  ASSERT_EQ("aet", t);
+    ASSERT_TRUE(origin.LookupCalledAet(t));  ASSERT_EQ("called", t);
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromHttp("host", "username"));
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_RestApi, origin.GetRequestOrigin());
+    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
+    ASSERT_TRUE(origin.LookupRemoteIp(t));     ASSERT_EQ("host", t);
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_TRUE(origin.LookupHttpUsername(t)); ASSERT_EQ("username", t);
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromLua());
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Lua, origin.GetRequestOrigin());
+    ASSERT_FALSE(origin.LookupRemoteIp(t));
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromPlugins());
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Plugins, origin.GetRequestOrigin());
+    ASSERT_FALSE(origin.LookupRemoteIp(t));
+    ASSERT_FALSE(origin.LookupRemoteAet(t));
+    ASSERT_FALSE(origin.LookupCalledAet(t));
+    ASSERT_FALSE(origin.LookupHttpUsername(t));
+  }
+}
+
+
+namespace
+{
+  class OrthancJobsSerialization : public testing::Test
+  {
+  private:
+    MemoryStorageArea              storage_;
+    SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
+    std::unique_ptr<ServerContext>   context_;
+
+  public:
+    OrthancJobsSerialization()
+    {
+      db_.Open();
+      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
+      context_->SetupJobsEngine(true, false);
+    }
+
+    virtual ~OrthancJobsSerialization() ORTHANC_OVERRIDE
+    {
+      context_->Stop();
+      context_.reset(NULL);
+      db_.Close();
+    }
+
+    ServerContext& GetContext() 
+    {
+      return *context_;
+    }
+
+    bool CreateInstance(std::string& id)
+    {
+      // Create a sample DICOM file
+      ParsedDicomFile dicom(true);
+      dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"),
+                    false, DicomReplaceMode_InsertIfAbsent, "");
+
+      DicomInstanceToStore toStore;
+      toStore.SetParsedDicomFile(dicom);
+
+      return (context_->Store(id, toStore, StoreInstanceMode_Default) == StoreStatus_Success);
+    }
+  };
+}
+
+
+TEST_F(OrthancJobsSerialization, Values)
+{
+  std::string id;
+  ASSERT_TRUE(CreateInstance(id));
+
+  Json::Value s;
+  OrthancJobUnserializer unserializer(GetContext());
+    
+  {
+    DicomInstanceOperationValue instance(GetContext(), id);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, instance));
+    instance.Serialize(s);
+  }
+
+  std::unique_ptr<JobOperationValue> value;
+  value.reset(unserializer.UnserializeValue(s));
+  ASSERT_EQ(JobOperationValue::Type_DicomInstance, value->GetType());
+  ASSERT_EQ(id, dynamic_cast<DicomInstanceOperationValue&>(*value).GetId());
+
+  {
+    std::string content;
+    dynamic_cast<DicomInstanceOperationValue&>(*value).ReadDicom(content);
+
+    ParsedDicomFile dicom(content);
+    ASSERT_TRUE(dicom.GetTagValue(content, DICOM_TAG_PATIENT_NAME));
+    ASSERT_EQ("JODOGNE", content);
+  }
+}
+
+
+TEST_F(OrthancJobsSerialization, Operations)
+{
+  std::string id;
+  ASSERT_TRUE(CreateInstance(id));
+
+  Json::Value s;
+  OrthancJobUnserializer unserializer(GetContext()); 
+
+  // DeleteResourceOperation
+  
+  {
+    DeleteResourceOperation operation(GetContext());
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  std::unique_ptr<IJobOperation> operation;
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    Json::Value dummy;
+    ASSERT_THROW(dynamic_cast<LogJobOperation&>(*operation).Serialize(dummy), std::bad_cast);
+    dynamic_cast<DeleteResourceOperation&>(*operation).Serialize(dummy);
+  }
+
+  // StorePeerOperation
+
+  {
+    WebServiceParameters peer;
+    peer.SetUrl("http://localhost/");
+    peer.SetCredentials("username", "password");
+    peer.SetPkcs11Enabled(true);
+
+    StorePeerOperation operation(peer);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const StorePeerOperation& tmp = dynamic_cast<StorePeerOperation&>(*operation);
+    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
+    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
+  }
+
+  // StoreScuOperation
+
+  {
+    TimeoutDicomConnectionManager luaManager;
+    
+    {
+      RemoteModalityParameters modality;
+      modality.SetApplicationEntityTitle("REMOTE");
+      modality.SetHost("192.168.1.1");
+      modality.SetPortNumber(1000);
+      modality.SetManufacturer(ModalityManufacturer_StoreScp);
+
+      StoreScuOperation operation(GetContext(), luaManager, "TEST", modality);
+
+      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+      operation.Serialize(s);
+    }
+
+    {
+      operation.reset(unserializer.UnserializeOperation(s));
+
+      const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation);
+      ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
+      ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
+      ASSERT_EQ(1000, tmp.GetRemoteModality().GetPortNumber());
+      ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
+      ASSERT_EQ("TEST", tmp.GetLocalAet());
+    }
+  }
+
+  // SystemCallOperation
+
+  {
+    SystemCallOperation operation(std::string("echo"));
+    operation.AddPreArgument("a");
+    operation.AddPreArgument("b");
+    operation.AddPostArgument("c");
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const SystemCallOperation& tmp = dynamic_cast<SystemCallOperation&>(*operation);
+    ASSERT_EQ("echo", tmp.GetCommand());
+    ASSERT_EQ(2u, tmp.GetPreArgumentsCount());
+    ASSERT_EQ(1u, tmp.GetPostArgumentsCount());
+    ASSERT_EQ("a", tmp.GetPreArgument(0));
+    ASSERT_EQ("b", tmp.GetPreArgument(1));
+    ASSERT_EQ("c", tmp.GetPostArgument(0));
+  }
+
+  // ModifyInstanceOperation
+
+  {
+    std::unique_ptr<DicomModification> modification(new DicomModification);
+    modification->SetupAnonymization(DicomVersion_2008);
+    
+    ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release());
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const ModifyInstanceOperation& tmp = dynamic_cast<ModifyInstanceOperation&>(*operation);
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetRequestOrigin());
+    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
+  }
+}
+
+
+TEST_F(OrthancJobsSerialization, Jobs)
+{
+  Json::Value s;
+
+  // ArchiveJob
+
+  {
+    ArchiveJob job(GetContext(), false, false);
+    ASSERT_FALSE(job.Serialize(s));  // Cannot serialize this
+  }
+
+  // DicomModalityStoreJob
+
+  OrthancJobUnserializer unserializer(GetContext()); 
+
+  {
+    RemoteModalityParameters modality;
+    modality.SetApplicationEntityTitle("REMOTE");
+    modality.SetHost("192.168.1.1");
+    modality.SetPortNumber(1000);
+    modality.SetManufacturer(ModalityManufacturer_StoreScp);
+
+    DicomModalityStoreJob job(GetContext());
+    job.SetLocalAet("LOCAL");
+    job.SetRemoteModality(modality);
+    job.SetMoveOriginator("MOVESCU", 42);
+
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
+    ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_TRUE(tmp.HasMoveOriginator());
+    ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet());
+    ASSERT_EQ(42, tmp.GetMoveOriginatorId());
+  }
+
+  // OrthancPeerStoreJob
+
+  {
+    WebServiceParameters peer;
+    peer.SetUrl("http://localhost/");
+    peer.SetCredentials("username", "password");
+    peer.SetPkcs11Enabled(true);
+
+    OrthancPeerStoreJob job(GetContext());
+    job.SetPeer(peer);
+    
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
+    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
+    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
+    ASSERT_FALSE(tmp.IsTranscode());
+    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
+  }
+
+  {
+    OrthancPeerStoreJob job(GetContext());
+    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
+    job.SetTranscode("1.2.840.10008.1.2.4.50");
+    
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
+    ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("", tmp.GetPeer().GetPassword());
+    ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled());
+    ASSERT_TRUE(tmp.IsTranscode());
+    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
+  }
+
+  // ResourceModificationJob
+
+  {
+    std::unique_ptr<DicomModification> modification(new DicomModification);
+    modification->SetupAnonymization(DicomVersion_2008);    
+
+    ResourceModificationJob job(GetContext());
+    job.SetModification(modification.release(), ResourceType_Patient, true);
+    job.SetOrigin(DicomInstanceOrigin::FromLua());
+
+    job.AddTrailingStep();  // Necessary since 1.7.0
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
+    ASSERT_TRUE(tmp.IsAnonymization());
+    ASSERT_FALSE(tmp.IsTranscode());
+    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
+  }
+
+  {
+    ResourceModificationJob job(GetContext());
+    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
+    job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
+
+    job.AddTrailingStep();  // Necessary since 1.7.0
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
+    ASSERT_FALSE(tmp.IsAnonymization());
+    ASSERT_TRUE(tmp.IsTranscode());
+    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
+    ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin());
+  }
+
+  // SplitStudyJob
+
+  std::string instance;
+  ASSERT_TRUE(CreateInstance(instance));
+
+  std::string study, series;
+
+  {
+    ServerContext::DicomCacheLocker lock(GetContext(), instance);
+    study = lock.GetDicom().GetHasher().HashStudy();
+    series = lock.GetDicom().GetHasher().HashSeries();
+  }
+
+  {
+    std::list<std::string> tmp;
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
+    ASSERT_EQ(1u, tmp.size());
+    ASSERT_EQ(study, tmp.front());
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
+    ASSERT_EQ(1u, tmp.size());
+    ASSERT_EQ(series, tmp.front());
+  }
+
+  std::string study2;
+
+  {
+    std::string a, b;
+
+    {
+      ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
+
+      SplitStudyJob job(GetContext(), study);
+      job.SetKeepSource(true);
+      job.AddSourceSeries(series);
+      ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
+      job.SetOrigin(DicomInstanceOrigin::FromLua());
+      job.Replace(DICOM_TAG_PATIENT_NAME, "hello");
+      job.Remove(DICOM_TAG_PATIENT_BIRTH_DATE);
+      ASSERT_THROW(job.Replace(DICOM_TAG_SERIES_DESCRIPTION, "nope"), OrthancException);
+      ASSERT_THROW(job.Remove(DICOM_TAG_SERIES_DESCRIPTION), OrthancException);
+    
+      ASSERT_TRUE(job.GetTargetStudy().empty());
+      a = job.GetTargetStudyUid();
+      ASSERT_TRUE(job.LookupTargetSeriesUid(b, series));
+
+      job.AddTrailingStep();
+      job.Start();
+      ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+      ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+
+      study2 = job.GetTargetStudy();
+      ASSERT_FALSE(study2.empty());
+
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+      ASSERT_TRUE(job.Serialize(s));
+    }
+
+    {
+      std::unique_ptr<IJob> job;
+      job.reset(unserializer.UnserializeJob(s));
+
+      SplitStudyJob& tmp = dynamic_cast<SplitStudyJob&>(*job);
+      ASSERT_TRUE(tmp.IsKeepSource());
+      ASSERT_EQ(study, tmp.GetSourceStudy());
+      ASSERT_EQ(a, tmp.GetTargetStudyUid());
+      ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+
+      std::string s;
+      ASSERT_EQ(study2, tmp.GetTargetStudy());
+      ASSERT_FALSE(tmp.LookupTargetSeriesUid(s, "nope"));
+      ASSERT_TRUE(tmp.LookupTargetSeriesUid(s, series));
+      ASSERT_EQ(b, s);
+
+      ASSERT_FALSE(tmp.LookupReplacement(s, DICOM_TAG_STUDY_DESCRIPTION));
+      ASSERT_TRUE(tmp.LookupReplacement(s, DICOM_TAG_PATIENT_NAME));
+      ASSERT_EQ("hello", s);
+      ASSERT_FALSE(tmp.IsRemoved(DICOM_TAG_PATIENT_NAME));
+      ASSERT_TRUE(tmp.IsRemoved(DICOM_TAG_PATIENT_BIRTH_DATE));
+    }
+  }
+
+  {
+    std::list<std::string> tmp;
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
+    ASSERT_EQ(2u, tmp.size());
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
+    ASSERT_EQ(2u, tmp.size());
+  }
+
+  // MergeStudyJob
+
+  {
+    ASSERT_THROW(SplitStudyJob(GetContext(), std::string("nope")), OrthancException);
+
+    MergeStudyJob job(GetContext(), study);
+    job.SetKeepSource(true);
+    job.AddSource(study2);
+    ASSERT_THROW(job.AddSourceSeries("nope"), OrthancException);
+    ASSERT_THROW(job.AddSourceStudy("nope"), OrthancException);
+    ASSERT_THROW(job.AddSource("nope"), OrthancException);
+    job.SetOrigin(DicomInstanceOrigin::FromLua());
+    
+    ASSERT_EQ(job.GetTargetStudy(), study);
+
+    job.AddTrailingStep();
+    job.Start();
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::list<std::string> tmp;
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Study);
+    ASSERT_EQ(2u, tmp.size());
+    GetContext().GetIndex().GetAllUuids(tmp, ResourceType_Series);
+    ASSERT_EQ(3u, tmp.size());
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    MergeStudyJob& tmp = dynamic_cast<MergeStudyJob&>(*job);
+    ASSERT_TRUE(tmp.IsKeepSource());
+    ASSERT_EQ(study, tmp.GetTargetStudy());
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+  }
+}
+
+
+
+TEST_F(OrthancJobsSerialization, DicomAssociationParameters)
+{
+  Json::Value v;
+
+  {
+    v = Json::objectValue;
+    DicomAssociationParameters p;
+    p.SerializeJob(v);
+  }
+
+  {
+    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
+    ASSERT_EQ("ORTHANC", p.GetLocalApplicationEntityTitle());
+    ASSERT_EQ("ANY-SCP", p.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ(104u, p.GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
+    ASSERT_EQ("127.0.0.1", p.GetRemoteModality().GetHost());
+    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), p.GetTimeout());
+  }
+
+  {
+    v = Json::objectValue;
+    DicomAssociationParameters p;
+    p.SetLocalApplicationEntityTitle("HELLO");
+    p.SetRemoteApplicationEntityTitle("WORLD");
+    p.SetRemotePort(42);
+    p.SetRemoteHost("MY_HOST");
+    p.SetTimeout(43);
+    p.SerializeJob(v);
+  }
+
+  {
+    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
+    ASSERT_EQ("HELLO", p.GetLocalApplicationEntityTitle());
+    ASSERT_EQ("WORLD", p.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ(42u, p.GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
+    ASSERT_EQ("MY_HOST", p.GetRemoteModality().GetHost());
+    ASSERT_EQ(43u, p.GetTimeout());
+  }
+  
+  {
+    DicomModalityStoreJob job(GetContext());
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomModalityStoreJob> job(
+      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
+    ASSERT_FALSE(job->HasMoveOriginator());
+    ASSERT_THROW(job->GetMoveOriginatorAet(), OrthancException);
+    ASSERT_THROW(job->GetMoveOriginatorId(), OrthancException);
+    ASSERT_FALSE(job->HasStorageCommitment());
+  }
+  
+  {
+    RemoteModalityParameters r;
+    r.SetApplicationEntityTitle("HELLO");
+    r.SetPortNumber(42);
+    r.SetHost("MY_HOST");
+
+    DicomModalityStoreJob job(GetContext());
+    job.SetLocalAet("WORLD");
+    job.SetRemoteModality(r);
+    job.SetTimeout(43);
+    job.SetMoveOriginator("ORIGINATOR", 100);
+    job.EnableStorageCommitment(true);
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomModalityStoreJob> job(
+      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
+    ASSERT_TRUE(job->HasMoveOriginator());
+    ASSERT_EQ("ORIGINATOR", job->GetMoveOriginatorAet());
+    ASSERT_EQ(100, job->GetMoveOriginatorId());
+    ASSERT_TRUE(job->HasStorageCommitment());
+  }
+    
+  {
+    DicomMoveScuJob job(GetContext());
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomMoveScuJob> job(
+      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
+  }
+  
+  {
+    RemoteModalityParameters r;
+    r.SetApplicationEntityTitle("HELLO");
+    r.SetPortNumber(42);
+    r.SetHost("MY_HOST");
+
+    DicomMoveScuJob job(GetContext());
+    job.SetLocalAet("WORLD");
+    job.SetRemoteModality(r);
+    job.SetTimeout(43);
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomMoveScuJob> job(
+      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
+  }
+}