Skip to content

Commit 05dccdf

Browse files
committed
tests: Use random ports, allowing test to run in parallel
1 parent 68e2988 commit 05dccdf

3 files changed

Lines changed: 62 additions & 40 deletions

File tree

src/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ fn init_logger(level: tracing::Level) {
4545
.with(
4646
tracing_subscriber::fmt::layer()
4747
.compact()
48-
.with_ansi(true)
4948
.with_file(false)
5049
.with_line_number(false)
5150
.with_target(false),

tests/common/mod.rs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@ use testcontainers::runners::AsyncRunner;
88
use testcontainers::{ContainerAsync, GenericImage};
99
use tokio::io::{AsyncBufReadExt, BufReader, Lines};
1010
use tokio::process::{Child, ChildStdout, Command};
11-
use tokio::time::{timeout_at, Instant};
1211

13-
const STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
14-
const STARTED_MESSAGE: &str = "Started DICOMweb server";
15-
16-
/// Spawns a container running the latest version of Orthanc.
1712
pub async fn spawn_orthanc() -> anyhow::Result<ContainerAsync<GenericImage>> {
1813
GenericImage::new("jodogne/orthanc", "latest")
1914
.with_exposed_port(4242.tcp())
@@ -24,17 +19,17 @@ pub async fn spawn_orthanc() -> anyhow::Result<ContainerAsync<GenericImage>> {
2419
.context("failed to start Orthanc container")
2520
}
2621

27-
/// Spawns the DICOM-RST binary with the given config and waits until it is ready.
2822
pub async fn spawn_dicomrst(config: &str) -> anyhow::Result<ServerProcess> {
2923
let mut server = ServerProcess::spawn(config)?;
30-
server.wait_until_started().await?;
24+
server.http_port = server.wait_until_started().await?;
3125
Ok(server)
3226
}
3327

3428
pub struct ServerProcess {
3529
child: Child,
3630
stdout: Lines<BufReader<ChildStdout>>,
3731
workdir: PathBuf,
32+
http_port: u16,
3833
}
3934

4035
impl ServerProcess {
@@ -46,46 +41,52 @@ impl ServerProcess {
4641

4742
let mut child = Command::new(env!("CARGO_BIN_EXE_dicom-rst"))
4843
.stdout(Stdio::piped())
49-
.stderr(Stdio::piped())
44+
.stderr(Stdio::null())
45+
.env("NO_COLOR", "true") // disables colored ANSI output
5046
.current_dir(&workdir)
5147
.spawn()
5248
.context("failed to spawn DICOM-RST server binary")?;
5349

5450
let stdout = BufReader::new(child.stdout.take().unwrap()).lines();
55-
let mut stderr = BufReader::new(child.stderr.take().unwrap()).lines();
56-
57-
tokio::spawn(async move {
58-
while let Ok(Some(line)) = stderr.next_line().await {
59-
eprintln!("[dicom-rst] {line}");
60-
}
61-
});
6251

6352
Ok(Self {
6453
child,
6554
stdout,
6655
workdir,
56+
http_port: 0,
6757
})
6858
}
6959

70-
async fn wait_until_started(&mut self) -> anyhow::Result<()> {
71-
let deadline = Instant::now() + STARTUP_TIMEOUT;
72-
73-
loop {
74-
match timeout_at(deadline, self.stdout.next_line()).await {
75-
Ok(Ok(Some(line))) => {
76-
eprintln!("[dicom-rst] {line}");
77-
if line.contains(STARTED_MESSAGE) {
78-
return Ok(());
79-
}
80-
}
81-
Ok(Ok(None)) => {
82-
let status = self.child.wait().await?;
83-
bail!("DICOM-RST exited before becoming ready: {status}");
60+
async fn wait_until_started(&mut self) -> anyhow::Result<u16> {
61+
tokio::time::timeout(Duration::from_secs(15), async {
62+
while let Some(line) = self
63+
.stdout
64+
.next_line()
65+
.await
66+
.context("Failed to read DICOM-RST stdout")?
67+
{
68+
if !line.contains("Started DICOMweb server") {
69+
continue;
8470
}
85-
Ok(Err(e)) => return Err(e).context("failed to read DICOM-RST stdout"),
86-
Err(_) => bail!("timed out waiting for `{STARTED_MESSAGE}`"),
71+
72+
let port = line
73+
.split_whitespace()
74+
.find_map(|part| part.strip_prefix("server.port="))
75+
.ok_or_else(|| {
76+
anyhow::Error::msg(
77+
"DICOM-RST started, but stdout did not contain server.port=",
78+
)
79+
})?
80+
.parse::<u16>()
81+
.context("Failed to parse DICOM-RST server.port as u16")?;
82+
83+
return Ok(port);
8784
}
88-
}
85+
86+
bail!("DICOM-RST exited before becoming ready");
87+
})
88+
.await
89+
.context("Timed out waiting for DICOM-RST to start")?
8990
}
9091
}
9192

@@ -96,7 +97,7 @@ impl Drop for ServerProcess {
9697
}
9798
}
9899

99-
pub async fn with_test_deployment(
100+
pub async fn with_test_environment(
100101
config: &str,
101102
test: impl AsyncFnOnce(DicomWebClient) -> anyhow::Result<()>,
102103
) -> anyhow::Result<()> {
@@ -107,9 +108,12 @@ pub async fn with_test_deployment(
107108
.context("failed to get mapped Orthanc DIMSE port")?;
108109

109110
let config = config.replace("${ORTHANC_PORT}", &orthanc_port.to_string());
110-
let _server = spawn_dicomrst(&config).await?;
111+
let server = spawn_dicomrst(&config).await?;
111112

112-
let client = DicomWebClient::with_single_url(&format!("http://localhost:8080/aets/ORTHANC"));
113+
let client = DicomWebClient::with_single_url(&format!(
114+
"http://localhost:{}/aets/ORTHANC",
115+
server.http_port
116+
));
113117
test(client).await?;
114118

115119
Ok(())

tests/stow.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ use std::time::{Duration, Instant};
1111
#[tokio::test]
1212
async fn can_upload_study_instances() -> anyhow::Result<()> {
1313
let config = "
14+
server:
15+
http:
16+
port: 0
17+
dimse:
18+
- aet: DICOM-RST
19+
interface: 0.0.0.0
20+
port: 0
1421
aets:
1522
- aet: ORTHANC
1623
host: 127.0.0.1
1724
port: ${ORTHANC_PORT}
1825
backend: DIMSE
19-
";
26+
";
2027

2128
let instances = [
2229
"pydicom/liver.dcm",
@@ -26,7 +33,7 @@ async fn can_upload_study_instances() -> anyhow::Result<()> {
2633
.map(|path| open_file(dicom_test_files::path(path).unwrap()).unwrap());
2734
let instances = futures::stream::iter(instances);
2835

29-
with_test_deployment(&config, async |client| {
36+
with_test_environment(config, async |client| {
3037
let response = client
3138
.store_instances()
3239
.with_instances(instances.clone())
@@ -67,6 +74,13 @@ async fn can_upload_study_instances() -> anyhow::Result<()> {
6774
#[tokio::test]
6875
async fn does_not_leak_semaphore_permits_if_association_is_rejected() -> anyhow::Result<()> {
6976
let config = "
77+
server:
78+
http:
79+
port: 0
80+
dimse:
81+
- aet: DICOM-RST
82+
interface: 0.0.0.0
83+
port: 0
7084
aets:
7185
- aet: ORTHANC
7286
host: 127.0.0.1
@@ -88,7 +102,7 @@ async fn does_not_leak_semaphore_permits_if_association_is_rejected() -> anyhow:
88102
};
89103
let instances = [create_instance(), create_instance(), create_instance()];
90104

91-
with_test_deployment(config, async |client| {
105+
with_test_environment(config, async |client| {
92106
let start = Instant::now();
93107
let response = client
94108
.store_instances()
@@ -124,15 +138,20 @@ async fn returns_413_if_max_upload_size_is_exceeded() -> anyhow::Result<()> {
124138
let config = "
125139
server:
126140
http:
141+
port: 0
127142
max-upload-size: 1
143+
dimse:
144+
- aet: DICOM-RST
145+
interface: 0.0.0.0
146+
port: 0
128147
aets:
129148
- aet: ORTHANC
130149
host: 127.0.0.1
131150
port: ${ORTHANC_PORT}
132151
backend: DIMSE
133152
";
134153

135-
with_test_deployment(config, async |client| {
154+
with_test_environment(config, async |client| {
136155
let instance = open_file(dicom_test_files::path("pydicom/CT_small.dcm").unwrap()).unwrap();
137156
let result = client
138157
.store_instances()

0 commit comments

Comments
 (0)