Mercurial > hg > orthanc
changeset 2853:52b017d22a4f
New URI: "/studies/.../merge" to merge a study
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 02 Oct 2018 17:05:07 +0200 |
parents | 859e880ac9a8 |
children | ebb3f2aa5f23 |
files | CMakeLists.txt Core/Images/Font.cpp NEWS OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/ServerJobs/MergeStudyJob.cpp OrthancServer/ServerJobs/MergeStudyJob.h OrthancServer/ServerJobs/OrthancJobUnserializer.cpp UnitTestsSources/MultiThreadingTests.cpp |
diffstat | 8 files changed, 652 insertions(+), 9 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Mon Oct 01 14:19:45 2018 +0200 +++ b/CMakeLists.txt Tue Oct 02 17:05:07 2018 +0200 @@ -85,6 +85,7 @@ OrthancServer/ServerJobs/ArchiveJob.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/ServerJobs/LuaJobManager.cpp + OrthancServer/ServerJobs/MergeStudyJob.cpp OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp
--- a/Core/Images/Font.cpp Mon Oct 01 14:19:45 2018 +0200 +++ b/Core/Images/Font.cpp Tue Oct 02 17:05:07 2018 +0200 @@ -219,6 +219,7 @@ } case PixelFormat_RGBA32: + case PixelFormat_BGRA32: { assert(bpp == 4); @@ -256,7 +257,8 @@ { if (target.GetFormat() != PixelFormat_Grayscale8 && target.GetFormat() != PixelFormat_RGB24 && - target.GetFormat() != PixelFormat_RGBA32) + target.GetFormat() != PixelFormat_RGBA32 && + target.GetFormat() != PixelFormat_BGRA32) { throw OrthancException(ErrorCode_NotImplemented); } @@ -311,7 +313,25 @@ uint8_t g, uint8_t b) const { - uint8_t color[4] = { r, g, b, 255 }; + uint8_t color[4]; + + switch (target.GetFormat()) + { + case PixelFormat_BGRA32: + color[0] = b; + color[1] = g; + color[2] = r; + color[3] = 255; + break; + + default: + color[0] = r; + color[1] = g; + color[2] = b; + color[3] = 255; + break; + } + DrawInternal(target, utf8, x, y, color); }
--- a/NEWS Mon Oct 01 14:19:45 2018 +0200 +++ b/NEWS Tue Oct 02 17:05:07 2018 +0200 @@ -5,6 +5,7 @@ REST API -------- +* New URI: "/studies/.../merge" to merge a study * New URI: "/studies/.../split" to split a study Maintenance
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Mon Oct 01 14:19:45 2018 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue Oct 02 17:05:07 2018 +0200 @@ -38,6 +38,7 @@ #include "../../Core/Logging.h" #include "../../Core/SerializationToolbox.h" #include "../ServerContext.h" +#include "../ServerJobs/MergeStudyJob.h" #include "../ServerJobs/ResourceModificationJob.h" #include "../ServerJobs/SplitStudyJob.h" @@ -741,6 +742,47 @@ } + static void MergeStudy(RestApiPostCall& call) + { + ServerContext& context = OrthancRestApi::GetContext(call); + + Json::Value request; + if (!call.ParseJsonRequest(request)) + { + // Bad JSON request + throw OrthancException(ErrorCode_BadFileFormat); + } + + const std::string study = call.GetUriComponent("id", ""); + int priority = Toolbox::GetJsonIntegerField(request, "Priority", 0); + + std::auto_ptr<MergeStudyJob> job(new MergeStudyJob(context, study)); + job->SetOrigin(call); + job->SetDescription("REST API"); + + std::vector<std::string> resources; + SerializationToolbox::ReadArrayOfStrings(resources, request, "Resources"); + + for (size_t i = 0; i < resources.size(); i++) + { + job->AddSource(resources[i]); + } + + static const char* KEEP_SOURCE = "KeepSource"; + if (request.isMember(KEEP_SOURCE)) + { + job->SetKeepSource(SerializationToolbox::ReadBoolean(request, KEEP_SOURCE)); + } + + std::string id; + context.GetJobsEngine().GetRegistry().Submit(id, job.release(), priority); + + Json::Value v; + v["ID"] = id; + call.GetOutput().AnswerJson(v); + } + + void OrthancRestApi::RegisterAnonymizeModify() { Register("/instances/{id}/modify", ModifyInstance); @@ -756,5 +798,6 @@ Register("/tools/create-dicom", CreateDicom); Register("/studies/{id}/split", SplitStudy); + Register("/studies/{id}/merge", MergeStudy); } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp Tue Oct 02 17:05:07 2018 +0200 @@ -0,0 +1,396 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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 "MergeStudyJob.h" + +#include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../../Core/Logging.h" +#include "../../Core/SerializationToolbox.h" + + +namespace Orthanc +{ + void MergeStudyJob::AddSourceSeriesInternal(const std::string& series) + { + // Generate a target SeriesInstanceUID for this series + seriesUidMap_[series] = FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series); + + // Add all the instances of the series as to be processed + std::list<std::string> instances; + context_.GetIndex().GetChildren(instances, series); + + for (std::list<std::string>::const_iterator + it = instances.begin(); it != instances.end(); ++it) + { + AddInstance(*it); + } + } + + + void MergeStudyJob::AddSourceStudyInternal(const std::string& study) + { + if (study == targetStudy_) + { + LOG(ERROR) << "Cannot merge a study into the same study: " << study; + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + std::list<std::string> series; + context_.GetIndex().GetChildren(series, study); + + for (std::list<std::string>::const_iterator + it = series.begin(); it != series.end(); ++it) + { + AddSourceSeriesInternal(*it); + } + } + } + + + bool MergeStudyJob::HandleInstance(const std::string& instance) + { + /** + * Retrieve the DICOM instance to be modified + **/ + + std::auto_ptr<ParsedDicomFile> modified; + + try + { + ServerContext::DicomCacheLocker locker(context_, instance); + modified.reset(locker.GetDicom().Clone(true)); + } + catch (OrthancException&) + { + LOG(WARNING) << "An instance was removed after the job was issued: " << instance; + return false; + } + + + /** + * Chose the target UIDs + **/ + + std::string series = modified->GetHasher().HashSeries(); + + SeriesUidMap::const_iterator targetSeriesUid = seriesUidMap_.find(series); + + if (targetSeriesUid == seriesUidMap_.end()) + { + throw OrthancException(ErrorCode_BadFileFormat); // Should never happen + } + + + /** + * Copy the tags from the "Patient Module Attributes" and "General + * Study Module Attributes" modules of the target study + **/ + + for (std::set<DicomTag>::const_iterator it = removals_.begin(); + it != removals_.end(); ++it) + { + modified->Remove(*it); + } + + for (Replacements::const_iterator it = replacements_.begin(); + it != replacements_.end(); ++it) + { + modified->ReplacePlainString(it->first, it->second); + } + + + /** + * Store the new instance into Orthanc + **/ + + modified->ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, targetSeriesUid->second); + + DicomInstanceToStore toStore; + toStore.SetOrigin(origin_); + toStore.SetParsedDicomFile(*modified); + + std::string modifiedInstance; + if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success) + { + LOG(ERROR) << "Error while storing a modified instance " << instance; + return false; + } + + return true; + } + + + bool MergeStudyJob::HandleTrailingStep() + { + if (!keepSource_) + { + const size_t n = GetInstancesCount(); + + for (size_t i = 0; i < n; i++) + { + Json::Value tmp; + context_.DeleteResource(tmp, GetInstance(i), ResourceType_Instance); + } + } + + return true; + } + + + MergeStudyJob::MergeStudyJob(ServerContext& context, + const std::string& targetStudy) : + SetOfInstancesJob(true /* with trailing step */), + context_(context), + keepSource_(false), + targetStudy_(targetStudy) + { + /** + * Check the validity of the input ID + **/ + + ResourceType type; + + if (!context_.GetIndex().LookupResourceType(type, targetStudy) || + type != ResourceType_Study) + { + LOG(ERROR) << "Cannot merge into an unknown study: " << targetStudy; + throw OrthancException(ErrorCode_UnknownResource); + } + + + /** + * Detect the tags to be removed/replaced by parsing one child + * instance of the study + **/ + + DicomTag::AddTagsForModule(removals_, DicomModule_Patient); + DicomTag::AddTagsForModule(removals_, DicomModule_Study); + + std::list<std::string> instances; + context_.GetIndex().GetChildInstances(instances, targetStudy); + + if (instances.empty()) + { + throw OrthancException(ErrorCode_UnknownResource); + } + + DicomMap dicom; + + { + ServerContext::DicomCacheLocker locker(context_, instances.front()); + locker.GetDicom().ExtractDicomSummary(dicom); + } + + const std::set<DicomTag> moduleTags = removals_; + for (std::set<DicomTag>::const_iterator it = moduleTags.begin(); + it != moduleTags.end(); ++it) + { + const DicomValue* value = dicom.TestAndGetValue(*it); + std::string str; + + if (value != NULL && + value->CopyToString(str, false)) + { + removals_.erase(*it); + replacements_.insert(std::make_pair(*it, str)); + } + } + } + + + void MergeStudyJob::SetOrigin(const DicomInstanceOrigin& origin) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else + { + origin_ = origin; + } + } + + + void MergeStudyJob::SetOrigin(const RestApiCall& call) + { + SetOrigin(DicomInstanceOrigin::FromRest(call)); + } + + + void MergeStudyJob::AddSource(const std::string& studyOrSeries) + { + ResourceType level; + + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (!context_.GetIndex().LookupResourceType(level, studyOrSeries)) + { + LOG(ERROR) << "Cannot find this resource: " << studyOrSeries; + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + switch (level) + { + case ResourceType_Study: + AddSourceStudyInternal(studyOrSeries); + break; + + case ResourceType_Series: + AddSourceSeries(studyOrSeries); + break; + + default: + LOG(ERROR) << "This resource is neither a study, nor a series: " + << studyOrSeries << " is a " << EnumerationToString(level); + throw OrthancException(ErrorCode_UnknownResource); + } + } + } + + + void MergeStudyJob::AddSourceSeries(const std::string& series) + { + std::string parent; + + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (!context_.GetIndex().LookupParent(parent, series, ResourceType_Study)) + { + LOG(ERROR) << "This resource is not a series: " << series; + throw OrthancException(ErrorCode_UnknownResource); + } + else if (parent == targetStudy_) + { + LOG(ERROR) << "Cannot merge series " << series + << " into its parent study " << targetStudy_; + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + AddSourceSeriesInternal(series); + } + } + + + void MergeStudyJob::AddSourceStudy(const std::string& study) + { + ResourceType actualLevel; + + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + else if (!context_.GetIndex().LookupResourceType(actualLevel, study) || + actualLevel != ResourceType_Study) + { + LOG(ERROR) << "This resource is not a study: " << study; + throw OrthancException(ErrorCode_UnknownResource); + } + else + { + AddSourceStudyInternal(study); + } + } + + + void MergeStudyJob::SetKeepSource(bool keep) + { + if (IsStarted()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + keepSource_ = keep; + } + + + void MergeStudyJob::GetPublicContent(Json::Value& value) + { + SetOfInstancesJob::GetPublicContent(value); + value["TargetStudy"] = targetStudy_; + } + + + static const char* KEEP_SOURCE = "KeepSource"; + static const char* TARGET_STUDY = "TargetStudy"; + static const char* REPLACEMENTS = "Replacements"; + static const char* REMOVALS = "Removals"; + static const char* SERIES_UID_MAP = "SeriesUIDMap"; + static const char* ORIGIN = "Origin"; + + + MergeStudyJob::MergeStudyJob(ServerContext& context, + const Json::Value& serialized) : + SetOfInstancesJob(serialized), // (*) + context_(context) + { + if (!HasTrailingStep()) + { + // Should have been set by (*) + throw OrthancException(ErrorCode_InternalError); + } + + keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE); + targetStudy_ = SerializationToolbox::ReadString(serialized, TARGET_STUDY); + SerializationToolbox::ReadMapOfTags(replacements_, serialized, REPLACEMENTS); + SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS); + SerializationToolbox::ReadMapOfStrings(seriesUidMap_, serialized, SERIES_UID_MAP); + origin_ = DicomInstanceOrigin(serialized[ORIGIN]); + } + + + bool MergeStudyJob::Serialize(Json::Value& target) + { + if (!SetOfInstancesJob::Serialize(target)) + { + return false; + } + else + { + target[KEEP_SOURCE] = keepSource_; + target[TARGET_STUDY] = targetStudy_; + SerializationToolbox::WriteMapOfTags(target, replacements_, REPLACEMENTS); + SerializationToolbox::WriteSetOfTags(target, removals_, REMOVALS); + SerializationToolbox::WriteMapOfStrings(target, seriesUidMap_, SERIES_UID_MAP); + origin_.Serialize(target[ORIGIN]); + + return true; + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/OrthancServer/ServerJobs/MergeStudyJob.h Tue Oct 02 17:05:07 2018 +0200 @@ -0,0 +1,115 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2018 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/>. + **/ + + +#pragma once + +#include "../../Core/JobsEngine/SetOfInstancesJob.h" + +#include "../ServerContext.h" + +namespace Orthanc +{ + class MergeStudyJob : public SetOfInstancesJob + { + private: + typedef std::map<std::string, std::string> SeriesUidMap; + typedef std::map<DicomTag, std::string> Replacements; + + + ServerContext& context_; + bool keepSource_; + std::string targetStudy_; + Replacements replacements_; + std::set<DicomTag> removals_; + SeriesUidMap seriesUidMap_; + DicomInstanceOrigin origin_; + + + void AddSourceSeriesInternal(const std::string& series); + + void AddSourceStudyInternal(const std::string& study); + + + protected: + virtual bool HandleInstance(const std::string& instance); + + virtual bool HandleTrailingStep(); + + public: + MergeStudyJob(ServerContext& context, + const std::string& targetStudy); + + MergeStudyJob(ServerContext& context, + const Json::Value& serialized); + + const std::string& GetTargetStudy() const + { + return targetStudy_; + } + + void AddSource(const std::string& studyOrSeries); + + void AddSourceStudy(const std::string& study); + + void AddSourceSeries(const std::string& series); + + bool IsKeepSource() const + { + return keepSource_; + } + + void SetKeepSource(bool keep); + + void SetOrigin(const DicomInstanceOrigin& origin); + + void SetOrigin(const RestApiCall& call); + + const DicomInstanceOrigin& GetOrigin() const + { + return origin_; + } + + virtual void Stop(JobStopReason reason) + { + } + + virtual void GetJobType(std::string& target) + { + target = "MergeStudy"; + } + + virtual void GetPublicContent(Json::Value& value); + + virtual bool Serialize(Json::Value& target); + }; +}
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Mon Oct 01 14:19:45 2018 +0200 +++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp Tue Oct 02 17:05:07 2018 +0200 @@ -48,6 +48,7 @@ #include "DicomModalityStoreJob.h" #include "OrthancPeerStoreJob.h" #include "ResourceModificationJob.h" +#include "MergeStudyJob.h" #include "SplitStudyJob.h" namespace Orthanc @@ -79,6 +80,10 @@ { return new ResourceModificationJob(context_, source); } + else if (type == "MergeStudy") + { + return new MergeStudyJob(context_, source); + } else if (type == "SplitStudy") { return new SplitStudyJob(context_, source);
--- a/UnitTestsSources/MultiThreadingTests.cpp Mon Oct 01 14:19:45 2018 +0200 +++ b/UnitTestsSources/MultiThreadingTests.cpp Tue Oct 02 17:05:07 2018 +0200 @@ -61,6 +61,7 @@ #include "../OrthancServer/ServerJobs/ArchiveJob.h" #include "../OrthancServer/ServerJobs/DicomModalityStoreJob.h" +#include "../OrthancServer/ServerJobs/MergeStudyJob.h" #include "../OrthancServer/ServerJobs/OrthancPeerStoreJob.h" #include "../OrthancServer/ServerJobs/ResourceModificationJob.h" #include "../OrthancServer/ServerJobs/SplitStudyJob.h" @@ -1561,19 +1562,31 @@ // SplitStudyJob - std::string id; - ASSERT_TRUE(CreateInstance(id)); + std::string instance; + ASSERT_TRUE(CreateInstance(instance)); std::string study, series; { - ServerContext::DicomCacheLocker lock(GetContext(), id); + ServerContext::DicomCacheLocker lock(GetContext(), instance); study = lock.GetDicom().GetHasher().HashStudy(); series = lock.GetDicom().GetHasher().HashSeries(); } { - std::string a, b, c; + 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); @@ -1596,8 +1609,8 @@ ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); ASSERT_EQ(JobStepCode_Success, job.Step().GetCode()); - c = job.GetTargetStudy(); - ASSERT_FALSE(c.empty()); + study2 = job.GetTargetStudy(); + ASSERT_FALSE(study2.empty()); ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job)); ASSERT_TRUE(job.Serialize(s)); @@ -1614,7 +1627,7 @@ ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin()); std::string s; - ASSERT_EQ(c, tmp.GetTargetStudy()); + ASSERT_EQ(study2, tmp.GetTargetStudy()); ASSERT_FALSE(tmp.LookupTargetSeriesUid(s, "nope")); ASSERT_TRUE(tmp.LookupTargetSeriesUid(s, series)); ASSERT_EQ(b, s); @@ -1626,6 +1639,55 @@ 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.Start(); + ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode()); + ASSERT_EQ(JobStepCode_Success, job.Step().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::auto_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()); + } }