mirror of
https://github.com/zed-industries/zed.git
synced 2026-08-27 01:52:31 +00:00
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #53486 Note that this builds off of #55117 because both touched similar code areas; I thought it best to in order to avoid conflicts. Release Notes: - Added support for local features in dev containers
207 lines
6.4 KiB
Rust
207 lines
6.4 KiB
Rust
use std::process::Output;
|
|
|
|
use async_trait::async_trait;
|
|
use serde::Deserialize;
|
|
use util::command::Command;
|
|
|
|
use crate::devcontainer_api::DevContainerError;
|
|
|
|
pub(crate) struct DefaultCommandRunner;
|
|
|
|
impl DefaultCommandRunner {
|
|
pub(crate) fn new() -> Self {
|
|
Self
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl CommandRunner for DefaultCommandRunner {
|
|
async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error> {
|
|
command.output().await
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
pub(crate) trait CommandRunner: Send + Sync {
|
|
async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error>;
|
|
}
|
|
|
|
pub(crate) async fn evaluate_json_command<T>(
|
|
mut command: Command,
|
|
) -> Result<Option<T>, DevContainerError>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
let output = command.output().await.map_err(|e| {
|
|
log::error!("Error running command {:?}: {e}", command);
|
|
DevContainerError::CommandFailed(command.get_program().display().to_string())
|
|
})?;
|
|
|
|
deserialize_json_output(output).map_err(|e| {
|
|
log::error!("Error running command {:?}: {e}", command);
|
|
DevContainerError::CommandFailed(command.get_program().display().to_string())
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn evaluate_yaml_command<T>(
|
|
mut command: Command,
|
|
) -> Result<Option<T>, DevContainerError>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
let output = command.output().await.map_err(|e| {
|
|
log::error!("Error running command {:?}: {e}", command);
|
|
DevContainerError::CommandFailed(command.get_program().display().to_string())
|
|
})?;
|
|
|
|
deserialize_yaml_output(output).map_err(|e| {
|
|
log::error!("Error running command {:?}: {e}", command);
|
|
DevContainerError::CommandFailed(command.get_program().display().to_string())
|
|
})
|
|
}
|
|
|
|
pub(crate) fn deserialize_yaml_output<T>(output: Output) -> Result<Option<T>, String>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
if output.status.success() {
|
|
let raw = String::from_utf8_lossy(&output.stdout);
|
|
if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
|
|
return Ok(None);
|
|
}
|
|
serde_yaml::from_str(&raw)
|
|
.map(Some)
|
|
.map_err(|e| format!("Error deserializing from raw yaml: {e}"))
|
|
} else {
|
|
let std_err = String::from_utf8_lossy(&output.stderr);
|
|
Err(format!(
|
|
"Sent non-successful output; cannot deserialize. StdErr: {std_err}"
|
|
))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn deserialize_json_output<T>(output: Output) -> Result<Option<T>, String>
|
|
where
|
|
T: for<'de> Deserialize<'de>,
|
|
{
|
|
if output.status.success() {
|
|
let raw = String::from_utf8_lossy(&output.stdout);
|
|
if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
|
|
return Ok(None);
|
|
}
|
|
serde_json_lenient::from_str(&raw)
|
|
.map_err(|e| format!("Error deserializing from raw json: {e}"))
|
|
} else {
|
|
let std_err = String::from_utf8_lossy(&output.stderr);
|
|
Err(format!(
|
|
"Sent non-successful output; cannot deserialize. StdErr: {std_err}"
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::process::ExitStatus;
|
|
|
|
use crate::docker::{DockerComposeConfig, DockerComposeServiceBuild};
|
|
|
|
use super::*;
|
|
|
|
fn success_output(stdout: &str) -> Output {
|
|
Output {
|
|
status: ExitStatus::default(),
|
|
stdout: stdout.as_bytes().to_vec(),
|
|
stderr: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, PartialEq)]
|
|
struct TestItem {
|
|
id: String,
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_newline_delimited_json_rejected() {
|
|
// Strict single-value contract: NDJSON must be rejected. Commands that
|
|
// may legitimately return multiple rows (e.g. `docker ps`) parse their
|
|
// output themselves rather than routing through this helper.
|
|
let output = success_output("{\"id\":\"first\"}\n{\"id\":\"second\"}\n");
|
|
let result: Result<Option<TestItem>, String> = deserialize_json_output(output);
|
|
assert!(result.is_err(), "expected parse error, got {result:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_empty_output() {
|
|
let output = success_output("");
|
|
let result: Option<TestItem> = deserialize_json_output(output).unwrap();
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_empty_object() {
|
|
let output = success_output("{}");
|
|
let result: Option<TestItem> = deserialize_json_output(output).unwrap();
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_yaml_docker_compose_config() {
|
|
let yaml = indoc::indoc! {"
|
|
name: my-project
|
|
services:
|
|
app:
|
|
image: node:18
|
|
command:
|
|
- sleep
|
|
- infinity
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
db:
|
|
image: postgres:15
|
|
volumes: {}
|
|
"};
|
|
let output = success_output(yaml);
|
|
let result: DockerComposeConfig = deserialize_yaml_output(output)
|
|
.expect("deserialization should succeed")
|
|
.expect("result should not be None");
|
|
|
|
assert_eq!(result.name, Some("my-project".to_string()));
|
|
assert_eq!(result.services.len(), 2);
|
|
|
|
let app = result
|
|
.services
|
|
.get("app")
|
|
.expect("app service should exist");
|
|
assert_eq!(app.image, Some("node:18".to_string()));
|
|
assert_eq!(
|
|
app.command,
|
|
vec!["sleep".to_string(), "infinity".to_string()]
|
|
);
|
|
assert_eq!(
|
|
app.build,
|
|
Some(DockerComposeServiceBuild {
|
|
context: Some(".".to_string()),
|
|
dockerfile: Some("Dockerfile".to_string()),
|
|
..Default::default()
|
|
})
|
|
);
|
|
|
|
let db = result.services.get("db").expect("db service should exist");
|
|
assert_eq!(db.image, Some("postgres:15".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_yaml_empty_output() {
|
|
let output = success_output("");
|
|
let result: Option<DockerComposeConfig> = deserialize_yaml_output(output).unwrap();
|
|
assert_eq!(result, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_yaml_empty_object() {
|
|
let output = success_output("{}");
|
|
let result: Option<DockerComposeConfig> = deserialize_yaml_output(output).unwrap();
|
|
assert_eq!(result, None);
|
|
}
|
|
}
|