Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 158 additions & 117 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ toml = "0.9"
directories = "6.0"

# Terminal and output
serde_yaml = "0.9"
serde_norway = "0.9"
csv = "1.3"
jpx-core = "0.2.1"

Expand Down
4 changes: 2 additions & 2 deletions crates/redisctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ flate2 = { workspace = true }

# Shared utility dependencies
thiserror = { workspace = true }
serde_yaml = { workspace = true }
serde_norway = { workspace = true }
jpx-core = { workspace = true }
config = { workspace = true }

Expand All @@ -62,7 +62,7 @@ keyring = { workspace = true, optional = true }
tower = { workspace = true }
tower-resilience = { version = "0.1", features = ["circuitbreaker", "retry", "ratelimiter"] }
regex = "1.12.2"
zip = "6.0.0"
zip = "9"
which = "7"
redis = { workspace = true }

Expand Down
96 changes: 95 additions & 1 deletion crates/redisctl/src/commands/enterprise/module_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,12 @@ async fn handle_inspect(file: &Path, verbose: bool, output_format: OutputFormat)
let mut zip_entry = archive
.by_index(i)
.with_context(|| format!("Failed to read zip entry {}", i))?;
let name = zip_entry.name().to_string();
// zip 9 made `name()` fallible; fall back to the raw bytes so inspection
// still lists entries whose names are not valid UTF-8.
let name = match zip_entry.name() {
Ok(name) => name.into_owned(),
Err(_) => String::from_utf8_lossy(zip_entry.name_raw()).into_owned(),
};
let size = zip_entry.size();

files_info.push((name.clone(), size));
Expand Down Expand Up @@ -1097,3 +1102,92 @@ fn format_size(bytes: u64) -> String {
format!("{} bytes", bytes)
}
}

#[cfg(test)]
mod tests {
use super::*;

const MODULE_JSON: &str =
r#"{"module_name":"synthetic","semantic_version":"1.0.0","version":10000}"#;

fn write_synthetic_inputs(dir: &Path) -> (std::path::PathBuf, std::path::PathBuf) {
let module_path = dir.join("synthetic.so");
let metadata_path = dir.join("module.json");
fs::write(&module_path, vec![0x7f_u8; 4096]).unwrap();
fs::write(&metadata_path, MODULE_JSON).unwrap();
(module_path, metadata_path)
}

#[tokio::test]
async fn package_writes_deflated_module_json_and_binary_entries() {
let dir = tempfile::tempdir().unwrap();
let (module_path, metadata_path) = write_synthetic_inputs(dir.path());
let output_path = dir.path().join("out").join("synthetic.zip");

handle_package(
&module_path,
&metadata_path,
&output_path,
false,
OutputFormat::Json,
)
.await
.unwrap();

let mut archive = zip::ZipArchive::new(fs::File::open(&output_path).unwrap()).unwrap();
assert_eq!(archive.len(), 2);

let mut names = Vec::new();
for i in 0..archive.len() {
let mut entry = archive.by_index(i).unwrap();
let name = entry.name().unwrap().into_owned();
assert_eq!(entry.compression(), zip::CompressionMethod::Deflated);
let mut content = Vec::new();
entry.read_to_end(&mut content).unwrap();
match name.as_str() {
"module.json" => assert_eq!(content, MODULE_JSON.as_bytes()),
"synthetic.so" => assert_eq!(content, vec![0x7f_u8; 4096]),
other => panic!("unexpected entry {other}"),
}
names.push(name);
}
assert_eq!(names, ["module.json", "synthetic.so"]);
}

#[tokio::test]
async fn inspect_reads_a_package_created_by_package() {
let dir = tempfile::tempdir().unwrap();
let (module_path, metadata_path) = write_synthetic_inputs(dir.path());
let output_path = dir.path().join("synthetic.zip");

handle_package(
&module_path,
&metadata_path,
&output_path,
false,
OutputFormat::Table,
)
.await
.unwrap();

handle_inspect(&output_path, true, OutputFormat::Json)
.await
.unwrap();
handle_inspect(&output_path, false, OutputFormat::Table)
.await
.unwrap();
}

#[tokio::test]
async fn inspect_rejects_non_zip_input() {
let dir = tempfile::tempdir().unwrap();
let bogus = dir.path().join("not-a-zip.zip");
fs::write(&bogus, b"definitely not a zip archive").unwrap();

assert!(
handle_inspect(&bogus, false, OutputFormat::Table)
.await
.is_err()
);
}
}
2 changes: 1 addition & 1 deletion crates/redisctl/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ impl RedisCtlError {
.unwrap_or_else(|_| envelope.to_string())
);
}
OutputFormat::Yaml => match serde_yaml::to_string(&self.envelope()) {
OutputFormat::Yaml => match serde_norway::to_string(&self.envelope()) {
Ok(s) => eprint!("{}", s),
Err(_) => self.print_human_diagnostic(),
},
Expand Down
2 changes: 1 addition & 1 deletion crates/redisctl/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ pub fn print_output<T: Serialize>(
println!("{}", serde_json::to_string_pretty(&json_value)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&json_value)?);
println!("{}", serde_norway::to_string(&json_value)?);
}
OutputFormat::Table => {
print_as_table(&json_value)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/redisctl/tests/cli_confirmation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fn cloud_user_delete_yaml_cancellation_preserves_error_contract() {
.assert()
.code(12)
.stdout(predicate::str::is_empty());
let error: Value = serde_yaml::from_slice(&assertion.get_output().stderr).unwrap();
let error: Value = serde_norway::from_slice(&assertion.get_output().stderr).unwrap();
assert_eq!(error["error"]["code"], "cancelled");
assert_eq!(error["error"]["exit_code"], 12);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/redisctl/tests/profile_path_source_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fn assert_path(mut command: Command, path: &Path, format: &str) {
let report: Value = if format == "json" {
serde_json::from_slice(bytes).unwrap()
} else {
serde_yaml::from_slice(bytes).unwrap()
serde_norway::from_slice(bytes).unwrap()
};
assert_eq!(report, serde_json::json!({"config_path": path.to_str()}));
}
Expand Down
2 changes: 1 addition & 1 deletion crates/redisctl/tests/profile_validation_source_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fn report(path: &Path, format: &str) -> Value {
if format == "json" {
serde_json::from_slice(&assertion.get_output().stdout).unwrap()
} else {
serde_yaml::from_slice(&assertion.get_output().stdout).unwrap()
serde_norway::from_slice(&assertion.get_output().stdout).unwrap()
}
}

Expand Down
10 changes: 5 additions & 5 deletions crates/redisctl/tests/support_package_cancellation_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fn overwrite_cancellation_preserves_structured_errors_and_existing_file() {
let error: Value = if format == "json" {
serde_json::from_slice(stderr).unwrap()
} else {
serde_yaml::from_slice(stderr).unwrap()
serde_norway::from_slice(stderr).unwrap()
};
assert_eq!(error["error"]["code"], "cancelled");
assert_eq!(error["error"]["exit_code"], 12);
Expand Down Expand Up @@ -141,7 +141,7 @@ fn explicit_force_skips_only_confirmation_and_preserves_structured_errors() {
let error: Value = if format == "json" {
serde_json::from_slice(stderr).unwrap()
} else {
serde_yaml::from_slice(stderr).unwrap()
serde_norway::from_slice(stderr).unwrap()
};
assert_eq!(error["error"]["code"], "no_profile_configured");
assert!(!String::from_utf8_lossy(stderr).contains("Overwrite?"));
Expand Down Expand Up @@ -170,7 +170,7 @@ fn force_keeps_missing_output_parent_check_before_profile_resolution() {
let error: Value = if format == "json" {
serde_json::from_slice(stderr).unwrap()
} else {
serde_yaml::from_slice(stderr).unwrap()
serde_norway::from_slice(stderr).unwrap()
};
assert!(
error["error"]["message"]
Expand Down Expand Up @@ -210,7 +210,7 @@ fn skip_checks_retains_broader_bypass_with_or_without_force() {
let error: Value = if format == "json" {
serde_json::from_slice(stderr).unwrap()
} else {
serde_yaml::from_slice(stderr).unwrap()
serde_norway::from_slice(stderr).unwrap()
};
assert_eq!(error["error"]["code"], "no_profile_configured");
assert!(!output.exists());
Expand All @@ -234,7 +234,7 @@ fn new_output_path_does_not_prefix_structured_error_with_human_warning() {
let error: Value = if format == "json" {
serde_json::from_slice(stderr).unwrap()
} else {
serde_yaml::from_slice(stderr).unwrap()
serde_norway::from_slice(stderr).unwrap()
};
assert_eq!(error["error"]["code"], "no_profile_configured");
assert!(!output.exists());
Expand Down
Loading