Skip to content

Commit f0011ca

Browse files
authored
Add tests for SDK extras and fix execute (#933)
* Don't cancel uploads run without control Currently `execute` will drop the import handle immediately, inadvertently canceling the request. Leak the handle, which will never be used, so that we allow the request to run to completion. Closes #930 * Add tests for SDK extras and fix `execute` We've been relying on the CLI disk import tests to cover the `extras` disk importer in the SDK. However, the CLI does not use the `execute` method, and we missed that this was failing immediately due to the cancellation sender being dropped before the request could awaited. Add tests for `extras` to validate that both options work. Also, add a method to DiskImport that does not accept cancellation and use that in `execute`. * Add copyright info
1 parent e0f213f commit f0011ca

File tree

4 files changed

+168
-3
lines changed

4 files changed

+168
-3
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ toml_edit = { workspace = true }
2727
uuid = { workspace = true }
2828

2929
[dev-dependencies]
30+
httpmock = { workspace = true }
31+
oxide-httpmock = { workspace = true }
32+
tempfile = { workspace = true }
33+
test-common = { workspace = true }
3034
tokio = { workspace = true }
3135

3236
[features]

sdk/src/extras/disk.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,17 @@ pub mod builder {
9999
self
100100
}
101101

102-
/// Return a `Future` for the disk creation and a `DiskImportHandle` which
103-
/// can be used to track upload progress and cancel the job.
102+
/// Return a `Future` for the disk creation.
104103
pub fn execute(
105104
self,
106105
) -> Result<
107106
impl Future<Output = Result<(), DiskImportError>> + 'a,
108107
Error<crate::types::Error>,
109108
> {
110-
Ok(self.execute_with_control()?.0)
109+
let (progress_tx, _progress_rx) = watch::channel(0);
110+
let importer = super::types::DiskImport::try_from((self, progress_tx))?;
111+
112+
Ok(importer.run())
111113
}
112114

113115
/// Return a `Future` for the disk creation and a `DiskImportHandle` which
@@ -282,6 +284,20 @@ pub mod types {
282284
Ok(())
283285
}
284286

287+
pub async fn run(self) -> Result<(), DiskImportError> {
288+
if let Err(e) = self.do_disk_import().await {
289+
if let Err(cleanup_err) = self.cleanup().await {
290+
return Err(DiskImportError::Wrapped {
291+
err: cleanup_err.into(),
292+
source: e.into(),
293+
});
294+
}
295+
return Err(e);
296+
}
297+
298+
Ok(())
299+
}
300+
285301
async fn do_disk_import(&self) -> Result<(), DiskImportError> {
286302
self.check_for_existing_disk().await?;
287303

sdk/tests/test_extra.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
// Copyright 2024 Oxide Computer Company
6+
7+
use httpmock::MockServer;
8+
use oxide::extras::disk::types::DiskInfo;
9+
use oxide::extras::ClientExtraDiskExt;
10+
use oxide::types::Disk;
11+
use oxide::{Client, ClientConfig};
12+
use oxide_httpmock::MockServerExt;
13+
use rand::{thread_rng, Rng, SeedableRng};
14+
use std::fs;
15+
use tempfile::TempDir;
16+
use test_common::JsonMock;
17+
use uuid::Uuid;
18+
19+
// A disk import where everything succeeds
20+
#[tokio::test]
21+
async fn test_disk_import() {
22+
let mut src = rand::rngs::SmallRng::seed_from_u64(42);
23+
let server = MockServer::start();
24+
25+
let disk_view_mock = server.disk_view(|when, then| {
26+
when.into_inner().any_request();
27+
then.client_error(
28+
404,
29+
&oxide::types::Error {
30+
error_code: None,
31+
message: "disk not found".into(),
32+
request_id: Uuid::mock_value(&mut src).unwrap().to_string(),
33+
},
34+
);
35+
});
36+
37+
let disk_create_mock = server.disk_create(|when, then| {
38+
when.into_inner().any_request();
39+
then.created(&Disk {
40+
name: "test-import".parse().unwrap(),
41+
..Disk::mock_value(&mut src).unwrap()
42+
});
43+
});
44+
45+
let start_bulk_write_mock = server.disk_bulk_write_import_start(|when, then| {
46+
when.into_inner().any_request();
47+
then.no_content();
48+
});
49+
50+
let disk_bulk_write_mock = server.disk_bulk_write_import(|when, then| {
51+
when.into_inner().any_request();
52+
then.no_content();
53+
});
54+
55+
let stop_bulk_write_mock = server.disk_bulk_write_import_stop(|when, then| {
56+
when.into_inner().any_request();
57+
then.no_content();
58+
});
59+
60+
let finalize_mock = server.disk_finalize_import(|when, then| {
61+
when.into_inner().any_request();
62+
then.no_content();
63+
});
64+
65+
let dir = TempDir::new().unwrap();
66+
let image_path = dir.path().join("image.raw");
67+
68+
let mut rng = thread_rng();
69+
let mut contents = vec![0u8; 8192];
70+
rng.fill(&mut contents[..]);
71+
fs::write(&image_path, contents).unwrap();
72+
73+
let disk_info = DiskInfo::calculate(image_path.clone(), None, None).unwrap();
74+
75+
let cfg = ClientConfig::default().with_host_and_token(server.url(""), "test_extra");
76+
let client = Client::new_authenticated_config(&cfg).unwrap();
77+
78+
// Execute without control.
79+
client
80+
.disk_import()
81+
.project("hi")
82+
.description("test extra")
83+
.upload_thread_ct(1)
84+
.disk("test-import")
85+
.disk_info(disk_info.clone())
86+
.execute()
87+
.unwrap()
88+
.await
89+
.unwrap();
90+
91+
// Execute with control.
92+
let (fut, handle) = client
93+
.disk_import()
94+
.project("hi")
95+
.description("test extra")
96+
.upload_thread_ct(8)
97+
.disk("test-import")
98+
.disk_info(disk_info.clone())
99+
.execute_with_control()
100+
.unwrap();
101+
102+
// Confirm progress channel is updated.
103+
let mut progress = handle.progress();
104+
let progress_updated = tokio::spawn(async move { progress.changed().await.is_ok() });
105+
fut.await.unwrap();
106+
assert!(progress_updated.await.unwrap());
107+
108+
// The initial call to `disk_view` races with the cancellation. In general,
109+
// the call is sent, but this is likely to vary with available parallelism
110+
// and system load. In theory we could see multiple endpoints hit before
111+
// the cancel is received, but this seems less likely.
112+
//
113+
// Assert this endpoint before the next request to avoid defining an
114+
// expectation for whether this call should be made upon immediate
115+
// cancellation.
116+
disk_view_mock.assert_hits(2);
117+
118+
let (fut_to_cancel, handle) = client
119+
.disk_import()
120+
.project("hi")
121+
.description("test extra")
122+
.upload_thread_ct(8)
123+
.disk("test-import")
124+
.disk_info(disk_info.clone())
125+
.execute_with_control()
126+
.unwrap();
127+
128+
// Cancel request immediately.
129+
handle.cancel();
130+
assert_eq!(
131+
fut_to_cancel.await.unwrap_err().to_string(),
132+
"Disk import canceled",
133+
);
134+
135+
// No calls received for the cancelled request.
136+
disk_create_mock.assert_hits(2);
137+
start_bulk_write_mock.assert_hits(2);
138+
disk_bulk_write_mock.assert_hits(2);
139+
stop_bulk_write_mock.assert_hits(2);
140+
finalize_mock.assert_hits(2);
141+
}

0 commit comments

Comments
 (0)