Skip to content

Commit 731b058

Browse files
committed
gh-41 Add DICOM Export test feature
Signed-off-by: Victor Chang <[email protected]>
1 parent 234cf3f commit 731b058

24 files changed

+866
-194
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ jobs:
276276
tests/Integration.Test/LivingDoc.html
277277
tests/Integration.Test/metrics.log
278278
tests/Integration.Test/services.log
279+
tests/Integration.Test/run.log
279280
retention-days: 30
280281

281282

.vscode/launch.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"request": "launch",
2222
"preLaunchTask": "build",
2323
// If you have changed target frameworks, make sure to update the program path.
24-
"program": "${workspaceFolder}/src/InformaticsGateway/bin/Debug/net5.0/Monai.Deploy.InformaticsGateway",
24+
"program": "${workspaceFolder}/src/InformaticsGateway/bin/Debug/net6.0/Monai.Deploy.InformaticsGateway",
2525
"args": [],
2626
"cwd": "${workspaceFolder}/src/InformaticsGateway",
2727
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
@@ -37,4 +37,4 @@
3737
"request": "attach"
3838
}
3939
]
40-
}
40+
}

src/DicomWebClient/Monai.Deploy.InformaticsGateway.DicomWeb.Client.csproj

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
<!--
2-
// Copyright 2021 MONAI Consortium
3-
// Licensed under the Apache License, Version 2.0 (the "License");
4-
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License at
6-
// http://www.apache.org/licenses/LICENSE-2.0
7-
// Unless required by applicable law or agreed to in writing, software
8-
// distributed under the License is distributed on an "AS IS" BASIS,
9-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10-
// See the License for the specific language governing permissions and
11-
// limitations under the License. -->
12-
1+
<!--
2+
Copyright 2021 MONAI Consortium
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
-->
1313

1414
<Project Sdk="Microsoft.NET.Sdk">
1515

@@ -35,9 +35,9 @@
3535
<ItemGroup>
3636
<PackageReference Include="Ardalis.GuardClauses" Version="3.3.0" />
3737
<PackageReference Include="fo-dicom" Version="5.0.1" />
38-
<PackageReference Include="GitVersion.MsBuild" Version="5.8.1">
39-
<PrivateAssets>All</PrivateAssets>
40-
</PackageReference>
38+
<PackageReference Include="GitVersion.MsBuild" Version="5.8.1">
39+
<PrivateAssets>All</PrivateAssets>
40+
</PackageReference>
4141
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
4242
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
4343
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
@@ -48,4 +48,4 @@
4848
<ProjectReference Include="..\Client.Common\Monai.Deploy.InformaticsGateway.Client.Common.csproj" />
4949
</ItemGroup>
5050

51-
</Project>
51+
</Project>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2022 MONAI Consortium
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
// Unless required by applicable law or agreed to in writing, software
7+
// distributed under the License is distributed on an "AS IS" BASIS,
8+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
// See the License for the specific language governing permissions and
10+
// limitations under the License.
11+
// Licensed to the .NET Foundation under one or more agreements.
12+
// The .NET Foundation licenses this file to you under the MIT license.
13+
14+
using System.Security.Cryptography;
15+
using System.Text;
16+
using FellowOakDicom;
17+
using FellowOakDicom.Imaging;
18+
19+
namespace Monai.Deploy.InformaticsGateway.Integration.Test.Common
20+
{
21+
internal static class DicomExtensions
22+
{
23+
public static string GenerateFileName(this DicomFile dicomFile) => $"{dicomFile.Dataset.GetString(DicomTag.PatientID)}-{dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID)}.dcm";
24+
25+
public static string CalculateHash(this DicomFile dicomFile)
26+
{
27+
var bytes = new List<byte>();
28+
var data = dicomFile.Dataset.GetSingleStringValueAsBytes(DicomTag.PatientID);
29+
bytes.AddRange(data);
30+
data = dicomFile.Dataset.GetSingleStringValueAsBytes(DicomTag.StudyInstanceUID);
31+
bytes.AddRange(data);
32+
data = dicomFile.Dataset.GetSingleStringValueAsBytes(DicomTag.SeriesInstanceUID);
33+
bytes.AddRange(data);
34+
data = dicomFile.Dataset.GetSingleStringValueAsBytes(DicomTag.SOPInstanceUID);
35+
bytes.AddRange(data);
36+
37+
var pixelData = DicomPixelData.Create(dicomFile.Dataset);
38+
for (int frame = 0; frame < pixelData.NumberOfFrames; frame++)
39+
{
40+
var buffer = pixelData.GetFrame(frame);
41+
bytes.AddRange(buffer.Data);
42+
}
43+
44+
using var sha256Hash = SHA256.Create();
45+
var hash = sha256Hash.ComputeHash(bytes.ToArray());
46+
var sb = new StringBuilder();
47+
foreach(var b in hash)
48+
{
49+
sb.Append(b.ToString("X2"));
50+
}
51+
return sb.ToString();
52+
}
53+
54+
public static byte[] GetSingleStringValueAsBytes(this DicomDataset dicomDataset, DicomTag dicomTag)
55+
{
56+
return Encoding.UTF8.GetBytes(dicomDataset.GetString(dicomTag));
57+
}
58+
}
59+
}

tests/Integration.Test/Common/Extensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,18 @@ public static long NextLong(this Random random, long minValue, long maxValue)
2121

2222
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
2323
}
24+
25+
public static async Task<bool> WaitUntil(Func<bool> condition, TimeSpan timeout, int frequency = 250)
26+
{
27+
var waitTask = Task.Run(async () =>
28+
{
29+
while (!condition())
30+
{
31+
await Task.Delay(frequency);
32+
}
33+
});
34+
35+
return waitTask == await Task.WhenAny(waitTask, Task.Delay(timeout));
36+
}
2437
}
2538
}

tests/Integration.Test/Drivers/Configurations.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public class TestRunnerSettings
112112
/// </summary>
113113
/// <value></value>
114114
public string HostIp { get; set; }
115+
116+
/// <summary>
117+
/// Gets or sets the name of the bucket test files are uploaded to.
118+
/// </summary>
119+
public string Bucket { get; set; }
115120
}
116121

117122
public class InformaticsGatewaySettings

tests/Integration.Test/Drivers/DicomInstanceGenerator.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public StudyGenerationSpecs Generate(string patientId, int studiesPerPatient, in
119119
if (studySpec is null) throw new ArgumentNullException(nameof(studySpec));
120120

121121
var instancesPerSeries = _random.Next(studySpec.InstanceMin, studySpec.InstanceMax);
122-
122+
var uniqueInstances = new HashSet<string>();
123123
var files = new List<DicomFile>();
124124
DicomFile dicomFile = null;
125125

@@ -136,6 +136,10 @@ public StudyGenerationSpecs Generate(string patientId, int studiesPerPatient, in
136136
var size = _random.NextLong(studySpec.SizeMinBytes, studySpec.SizeMaxBytes);
137137
dicomFile = generator.GenerateNewInstance(size);
138138
files.Add(dicomFile);
139+
if(!uniqueInstances.Add(dicomFile.Dataset.GetString(DicomTag.SOPInstanceUID)))
140+
{
141+
throw new Exception("Instance UID already exists, something's wrong here.");
142+
}
139143
}
140144
}
141145
_outputHelper.WriteLine("DICOM Instance: PID={0}, STUDY={1}",
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2022 MONAI Consortium
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
// Unless required by applicable law or agreed to in writing, software
7+
// distributed under the License is distributed on an "AS IS" BASIS,
8+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
// See the License for the specific language governing permissions and
10+
// limitations under the License.
11+
12+
using System.Diagnostics;
13+
using System.Linq;
14+
using System.Text;
15+
using FellowOakDicom;
16+
using FellowOakDicom.Imaging.Codec;
17+
using FellowOakDicom.Log;
18+
using FellowOakDicom.Network;
19+
using FellowOakDicom.Network.Client;
20+
using Monai.Deploy.InformaticsGateway.Integration.Test.Common;
21+
using TechTalk.SpecFlow.Infrastructure;
22+
23+
namespace Monai.Deploy.InformaticsGateway.Integration.Test.Drivers
24+
{
25+
public class ServerData
26+
{
27+
public FeatureContext Context { get; set; }
28+
public Dictionary<string, string> Instances { get; set; } = new Dictionary<string, string>();
29+
public ISpecFlowOutputHelper OutputHelper {get;set;}
30+
}
31+
32+
internal class CStoreScp : DicomService, IDicomServiceProvider, IDicomCStoreProvider
33+
{
34+
private static readonly Object SyncLock = new Object();
35+
internal static readonly string PayloadsRoot = "./payloads";
36+
37+
public CStoreScp(INetworkStream stream, Encoding fallbackEncoding, ILogger log, ILogManager logManager, INetworkManager network, ITranscoderManager transcoder)
38+
: base(stream, fallbackEncoding, log, logManager, network, transcoder)
39+
{
40+
}
41+
42+
public void OnConnectionClosed(Exception exception)
43+
{
44+
if (exception is not null)
45+
{
46+
Console.WriteLine("Connection closed with error {0}.", exception);
47+
}
48+
else
49+
{
50+
Console.WriteLine("Connection closed.");
51+
}
52+
}
53+
54+
public Task<DicomCStoreResponse> OnCStoreRequestAsync(DicomCStoreRequest request)
55+
{
56+
var data = UserState as ServerData;
57+
58+
if (data == null)
59+
{
60+
throw new Exception("UserState is not instance of ServerData.");
61+
}
62+
63+
try
64+
{
65+
var key = request.File.GenerateFileName();
66+
lock(SyncLock)
67+
{
68+
data.Instances.Add(key, request.File.CalculateHash());
69+
}
70+
data.OutputHelper.WriteLine("Instance received {0}", key);
71+
72+
return Task.FromResult(new DicomCStoreResponse(request, DicomStatus.Success));
73+
}
74+
catch (Exception ex)
75+
{
76+
data.OutputHelper.WriteLine("Exception 'OnCStoreRequestAsync': {0}", ex);
77+
return Task.FromResult(new DicomCStoreResponse(request, DicomStatus.ProcessingFailure));
78+
}
79+
}
80+
81+
public Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e)
82+
{
83+
Console.WriteLine("Exception 'OnCStoreRequestExceptionAsync': {0}", e);
84+
return Task.CompletedTask;
85+
}
86+
87+
public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason)
88+
{
89+
Console.WriteLine("Exception 'OnReceiveAbort': source {0}, reason {1]", source, reason);
90+
}
91+
92+
public Task OnReceiveAssociationReleaseRequestAsync()
93+
{
94+
return SendAssociationReleaseResponseAsync();
95+
}
96+
97+
public Task OnReceiveAssociationRequestAsync(DicomAssociation association)
98+
{
99+
foreach (var pc in association.PresentationContexts)
100+
{
101+
if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None)
102+
{
103+
pc.AcceptTransferSyntaxes(pc.GetTransferSyntaxes().ToArray());
104+
}
105+
}
106+
107+
return SendAssociationAcceptAsync(association);
108+
}
109+
110+
}
111+
}

tests/Integration.Test/Drivers/DicomScu.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public async Task<DicomStatus> CEcho(string host, int port, string callingAeTitl
5252
}
5353
catch (DicomAssociationRejectedException ex)
5454
{
55-
Console.WriteLine("Association Rejected: {0}", ex.Message);
55+
_outputHelper.WriteLine("Association Rejected: {0}", ex.Message);
5656
return DicomStatus.Cancel;
5757
}
5858
}
@@ -67,11 +67,6 @@ public async Task<DicomStatus> CStore(string host, int port, string callingAeTit
6767
var failureStatus = new List<DicomStatus>();
6868
foreach (var file in dicomFiles)
6969
{
70-
// _outputHelper.WriteLine("Sending DICOM Instance: PID={0}, STUDY={1}, SOP={2}",
71-
// file.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, "N/A"),
72-
// file.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, "N/A"),
73-
// file.Dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, "N/A"));
74-
7570
var cStoreRequest = new DicomCStoreRequest(file);
7671
cStoreRequest.OnResponseReceived += (DicomCStoreRequest request, DicomCStoreResponse response) =>
7772
{

0 commit comments

Comments
 (0)