Orthanc/OrthancServer/UnitTestsSources/ServerJobsTests.cpp
2025-06-23 19:07:37 +05:30

1310 lines
39 KiB
C++

/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2023 Osimis S.A., Belgium
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, 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.
*
* 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/FileStorage/PluginStorageAreaAdapter.h"
#include "../../OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h"
#include "../../OrthancFramework/Sources/Logging.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 "../Sources/ServerJobs/Operations/DeleteResourceOperation.h"
#include "../Sources/ServerJobs/Operations/DicomInstanceOperationValue.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() const ORTHANC_OVERRIDE
{
return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
}
virtual void GetJobType(std::string& type) const ORTHANC_OVERRIDE
{
type = "DummyJob";
}
virtual bool Serialize(Json::Value& value) const ORTHANC_OVERRIDE
{
value = Json::objectValue;
value["Type"] = "DummyJob";
return true;
}
virtual void GetPublicContent(Json::Value& value) const ORTHANC_OVERRIDE
{
value["hello"] = "world";
}
virtual bool GetOutput(std::string& output,
MimeType& mime,
std::string& filename,
const std::string& key) ORTHANC_OVERRIDE
{
return false;
}
virtual bool DeleteOutput(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) const 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 CheckIdempotentSetOfInstances(IJobUnserializer& unserializer,
ThreadedSetOfInstancesJob& job)
{
Json::Value a = 42;
if (!job.Serialize(a))
{
return false;
}
else
{
std::unique_ptr<ThreadedSetOfInstancesJob> unserialized
(dynamic_cast<ThreadedSetOfInstancesJob*>(unserializer.UnserializeJob(a)));
Json::Value b = 43;
if (unserialized->Serialize(b))
{
return (CheckSameJson(a, b) &&
job.GetCurrentStep() == unserialized->GetCurrentStep() &&
job.GetInstancesCount() == unserialized->GetInstancesCount() );
}
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,
IJobOperationValue& value)
{
Json::Value a = 42;
value.Serialize(a);
std::unique_ptr<IJobOperationValue> 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));
}
{
DicomInstanceOrigin origin(DicomInstanceOrigin::FromWebDav());
s = 42;
origin.Serialize(s);
}
{
DicomInstanceOrigin origin(s);
ASSERT_EQ(RequestOrigin_WebDav, 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));
}
}
namespace
{
class OrthancJobsSerialization : public testing::Test
{
private:
PluginStorageAreaAdapter storage_;
SQLiteDatabaseWrapper db_; // The SQLite DB is in memory
std::unique_ptr<ServerContext> context_;
public:
OrthancJobsSerialization() :
storage_(new MemoryStorageArea)
{
db_.Open();
context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10, false /* readonly */, 1 /* DCMTK concurrent transcoders */));
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, "");
std::unique_ptr<DicomInstanceToStore> toStore(DicomInstanceToStore::CreateFromParsedDicomFile(dicom));
ServerContext::StoreResult result = context_->Store(id, *toStore, StoreInstanceMode_Default);
return (result.GetStatus() == 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<IJobOperationValue> value;
value.reset(unserializer.UnserializeValue(s));
ASSERT_EQ(IJobOperationValue::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_GE);
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_GE, 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));
}
{
std::unique_ptr<DicomModification> modification(new DicomModification);
modification->SetupAnonymization(DicomVersion_2023b);
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, ResourceType_Patient);
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_GE);
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_GE, 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);
modification->SetLevel(ResourceType_Series);
ResourceModificationJob job(GetContext(), 1);
ASSERT_THROW(job.IsSingleResourceModification(), OrthancException);
job.SetSingleResourceModification(modification.release(), ResourceType_Patient, true);
job.SetOrigin(DicomInstanceOrigin::FromLua());
ASSERT_TRUE(job.IsAnonymization());
ASSERT_TRUE(job.IsSingleResourceModification());
ASSERT_EQ(ResourceType_Patient, job.GetOutputLevel());
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));
ASSERT_TRUE(tmp.IsSingleResourceModification());
ASSERT_EQ(ResourceType_Patient, tmp.GetOutputLevel());
ASSERT_EQ(ResourceType_Series, tmp.GetModification().GetLevel());
}
{
// Backward compatibility with Orthanc 1.9.3
ASSERT_TRUE(s.isMember("OutputLevel"));
ASSERT_TRUE(s.isMember("IsSingleResource"));
s.removeMember("OutputLevel");
s.removeMember("IsSingleResource");
std::unique_ptr<IJob> job;
job.reset(unserializer.UnserializeJob(s));
ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
ASSERT_TRUE(tmp.IsSingleResourceModification());
ASSERT_EQ(ResourceType_Series, tmp.GetOutputLevel()); // old, incorrect behavior
ASSERT_EQ(ResourceType_Series, tmp.GetModification().GetLevel());
}
{
ResourceModificationJob job(GetContext(), 2);
ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
job.SetSingleResourceModification(new DicomModification, ResourceType_Study, false);
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());
ASSERT_TRUE(tmp.IsSingleResourceModification());
ASSERT_EQ(ResourceType_Study, tmp.GetOutputLevel());
ASSERT_EQ(ResourceType_Instance, tmp.GetModification().GetLevel());
}
{
ResourceModificationJob job(GetContext(), 2);
job.SetMultipleResourcesModification(new DicomModification, true);
std::list<std::string> instances;
instances.push_back("toto");
instances.push_back("tutu");
job.AddInstances(instances);
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);
std::set<std::string> instances;
tmp.GetInstances(instances);
ASSERT_EQ(2u, instances.size());
ASSERT_TRUE(instances.find("toto") != instances.end());
ASSERT_TRUE(instances.find("tutu") != instances.end());
ASSERT_TRUE(tmp.IsAnonymization());
ASSERT_FALSE(tmp.IsSingleResourceModification());
ASSERT_THROW(tmp.GetOutputLevel(), OrthancException);
ASSERT_EQ(ResourceType_Instance, tmp.GetModification().GetLevel());
}
{
// Test behavior on broken serialization
ASSERT_FALSE(s.isMember("OutputLevel"));
ASSERT_TRUE(s.isMember("IsSingleResource"));
s.removeMember("IsSingleResource");
{
std::unique_ptr<IJob> job;
job.reset(unserializer.UnserializeJob(s));
ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
ASSERT_TRUE(tmp.IsAnonymization());
ASSERT_TRUE(tmp.IsSingleResourceModification());
ASSERT_EQ(ResourceType_Patient, tmp.GetOutputLevel());
ASSERT_EQ(ResourceType_Instance, tmp.GetModification().GetLevel());
}
s["Modification"]["Level"] = "Series";
{
std::unique_ptr<IJob> job;
job.reset(unserializer.UnserializeJob(s));
ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
ASSERT_TRUE(tmp.IsAnonymization());
ASSERT_TRUE(tmp.IsSingleResourceModification());
ASSERT_EQ(ResourceType_Series, tmp.GetOutputLevel());
ASSERT_EQ(ResourceType_Series, tmp.GetModification().GetLevel());
}
}
// 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());
ASSERT_EQ(DicomToJsonFormat_Short, job->GetQueryFormat());
}
{
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.SetQueryFormat(DicomToJsonFormat_Human);
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());
ASSERT_EQ(DicomToJsonFormat_Human, job->GetQueryFormat());
}
}
TEST_F(OrthancJobsSerialization, DicomMoveScuJob)
{
Json::Value command = Json::objectValue;
command["0008,0005"]["Type"] = "String";
command["0008,0005"]["Content"] = "ISO_IR 100";
command["0010,0020"]["Type"] = "String";
command["0010,0020"]["Content"] = "1234";
Json::Value query = Json::objectValue;
query["0010,0020"] = "456";
query["0008,0052"] = "STUDY";
Json::Value remote = Json::objectValue;
remote["AET"] = "REMOTE";
remote["Host"] = "192.168.1.1";
remote["Port"] = 4242;
Json::Value s = Json::objectValue;
s["Permissive"] = true;
s["Position"] = 1;
s["Description"] = "test";
s["Remote"] = remote;
s["LocalAet"] = "LOCAL";
s["TargetAet"] = "TARGET";
s["QueryFormat"] = "Full";
s["Query"] = Json::arrayValue;
s["Query"].append(query);
s["Commands"] = Json::arrayValue;
s["Commands"].append(command);
Json::Value s2;
{
DicomMoveScuJob job(GetContext(), s);
job.Serialize(s2);
}
{
DicomMoveScuJob job(GetContext(), s2);
ASSERT_EQ("TARGET", job.GetTargetAet());
ASSERT_EQ("LOCAL", job.GetParameters().GetLocalApplicationEntityTitle());
ASSERT_EQ("REMOTE", job.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
ASSERT_EQ("192.168.1.1", job.GetParameters().GetRemoteModality().GetHost());
ASSERT_EQ(4242u, job.GetParameters().GetRemoteModality().GetPortNumber());
ASSERT_EQ("test", job.GetDescription());
ASSERT_TRUE(job.IsPermissive());
ASSERT_EQ(1u, job.GetPosition());
ASSERT_EQ(1u, job.GetCommandsCount());
ASSERT_EQ(DicomToJsonFormat_Full, job.GetQueryFormat());
ASSERT_EQ(1u, s2["Commands"].size());
ASSERT_EQ(command.toStyledString(), s2["Commands"][0].toStyledString());
ASSERT_EQ(1u, s2["Query"].size());
ASSERT_EQ(query.toStyledString(), s2["Query"][0].toStyledString());
}
}