use anyhow::bail;use collections::HashMap;use serde::Deserialize;use util::ResultExt; use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName}; #[derive(Clone, Debug, Deserialize, PartialEq)]#[serde(rename_all = "camelCase")]struct TaskOptions { cwd: Option<String>, #[serde(default)] env: HashMap<String, String>,} #[derive(Clone, Debug, PartialEq)]struct VsCodeTaskDefinition { label: String, command: Option<Command>, other_attributes: HashMap<String, serde_json_lenient::Value>, options: Option<TaskOptions>,} impl<'de> serde::Deserialize<'de> for VsCodeTaskDefinition { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct TaskHelper { #[serde(default)] label: Option<String>, #[serde(flatten)] command: Option<Command>, #[serde(flatten)] other_attributes: HashMap<String, serde_json_lenient::Value>, options: Option<TaskOptions>, } let helper = TaskHelper::deserialize(deserializer)?; let label = helper .label .unwrap_or_else(|| generate_label(&helper.command)); Ok(VsCodeTaskDefinition { label, command: helper.command, other_attributes: helper.other_attributes, options: helper.options, }) }} #[derive(Clone, Deserialize, PartialEq, Debug)]#[serde(tag = "type")]#[serde(rename_all = "camelCase")]enum Command { Npm { script: String, }, Shell { command: String, #[serde(default)] args: Vec<String>, }, Gulp { task: String, },} fn generate_label(command: &Option<Command>) -> String { match command { Some(Command::Npm { script }) => format!("npm: {}", script), Some(Command::Gulp { task }) => format!("gulp: {}", task), Some(Command::Shell { command, .. }) => { if command.trim().is_empty() { "shell".to_string() } else { command.clone() } } None => "Untitled Task".to_string(), }} impl VsCodeTaskDefinition { fn into_zed_format( self, replacer: &EnvVariableReplacer, ) -> anyhow::Result<Option<TaskTemplate>> { if self.other_attributes.contains_key("dependsOn") { log::warn!( "Skipping deserializing of a task `{}` with the unsupported `dependsOn` key", self.label ); return Ok(None); } // `type` might not be set in e.g. tasks that use `dependsOn`; we still want to deserialize the whole object though (hence command is an Option), // as that way we can provide more specific description of why deserialization failed. // E.g. if the command is missing due to `dependsOn` presence, we can check other_attributes first before doing this (and provide nice error message) // catch-all if on value.command presence. let Some(command) = self.command else { bail!("Missing `type` field in task"); }; let (command, args) = match command { Command::Npm { script } => ("npm".to_owned(), vec!["run".to_string(), script]), Command::Shell { command, args } => (command, args), Command::Gulp { task } => ("gulp".to_owned(), vec![task]), }; // Per VSC docs, only `command`, `args` and `options` support variable substitution. let command = replacer.replace(&command); let args = args.into_iter().map(|arg| replacer.replace(&arg)).collect(); let mut template = TaskTemplate { label: self.label, command, args, ..TaskTemplate::default() }; if let Some(options) = self.options { template.cwd = options.cwd.map(|cwd| replacer.replace(&cwd)); template.env = options.env; } Ok(Some(template)) }} /// [`VsCodeTaskFile`] is a superset of Code's task definition format.#[derive(Debug, Deserialize, PartialEq)]pub struct VsCodeTaskFile { tasks: Vec<VsCodeTaskDefinition>,} impl TryFrom<VsCodeTaskFile> for TaskTemplates { type Error = anyhow::Error; fn try_from(value: VsCodeTaskFile) -> Result<Self, Self::Error> { let replacer = EnvVariableReplacer::new(HashMap::from_iter([ ( "workspaceFolder".to_owned(), VariableName::WorktreeRoot.to_string(), ), ("file".to_owned(), VariableName::File.to_string()), ("lineNumber".to_owned(), VariableName::Row.to_string()), ( "selectedText".to_owned(), VariableName::SelectedText.to_string(), ), ])); let templates = value .tasks .into_iter() .filter_map(|vscode_definition| { vscode_definition .into_zed_format(&replacer) .log_err() .flatten() }) .collect(); Ok(Self(templates)) }} #[cfg(test)]mod tests { use std::collections::HashMap; use crate::{ TaskTemplate, TaskTemplates, VsCodeTaskFile, vscode_format::{Command, VsCodeTaskDefinition}, }; use super::{EnvVariableReplacer, generate_label}; fn compare_without_other_attributes(lhs: VsCodeTaskDefinition, rhs: VsCodeTaskDefinition) { assert_eq!( VsCodeTaskDefinition { other_attributes: Default::default(), ..lhs }, VsCodeTaskDefinition { other_attributes: Default::default(), ..rhs }, ); } #[test] fn test_variable_substitution() { let replacer = EnvVariableReplacer::new(Default::default()); assert_eq!(replacer.replace("Food"), "Food"); // Unknown variables are left in tact. assert_eq!( replacer.replace("$PATH is an environment variable"), "$PATH is an environment variable" ); assert_eq!(replacer.replace("${PATH}"), "${PATH}"); assert_eq!(replacer.replace("${PATH:food}"), "${PATH:food}"); // And now, the actual replacing let replacer = EnvVariableReplacer::new(HashMap::from_iter([( "PATH".to_owned(), "ZED_PATH".to_owned(), )])); assert_eq!(replacer.replace("Food"), "Food"); assert_eq!( replacer.replace("$PATH is an environment variable"), "${ZED_PATH} is an environment variable" ); assert_eq!(replacer.replace("${PATH}"), "${ZED_PATH}"); assert_eq!(replacer.replace("${PATH:food}"), "${ZED_PATH:food}"); } #[test] fn can_deserialize_ts_tasks() { const TYPESCRIPT_TASKS: &str = include_str!("../test_data/typescript.json"); let vscode_definitions: VsCodeTaskFile = serde_json_lenient::from_str(TYPESCRIPT_TASKS).unwrap(); let expected = vec![ VsCodeTaskDefinition { label: "gulp: tests".to_string(), command: Some(Command::Npm { script: "build:tests:notypecheck".to_string(), }), other_attributes: Default::default(), options: None, }, VsCodeTaskDefinition { label: "tsc: watch ./src".to_string(), command: Some(Command::Shell { command: "node".to_string(), args: vec![ "${workspaceFolder}/node_modules/typescript/lib/tsc.js".to_string(), "--build".to_string(), "${workspaceFolder}/src".to_string(), "--watch".to_string(), ], }), other_attributes: Default::default(), options: None, }, VsCodeTaskDefinition { label: "npm: build:compiler".to_string(), command: Some(Command::Npm { script: "build:compiler".to_string(), }), other_attributes: Default::default(), options: None, }, VsCodeTaskDefinition { label: "npm: build:tests".to_string(), command: Some(Command::Npm { script: "build:tests:notypecheck".to_string(), }), other_attributes: Default::default(), options: None, }, ]; assert_eq!(vscode_definitions.tasks.len(), expected.len()); vscode_definitions .tasks .iter() .zip(expected) .for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs)); let expected = vec![ TaskTemplate { label: "gulp: tests".to_string(), command: "npm".to_string(), args: vec!["run".to_string(), "build:tests:notypecheck".to_string()], ..Default::default() }, TaskTemplate { label: "tsc: watch ./src".to_string(), command: "node".to_string(), args: vec![ "${ZED_WORKTREE_ROOT}/node_modules/typescript/lib/tsc.js".to_string(), "--build".to_string(), "${ZED_WORKTREE_ROOT}/src".to_string(), "--watch".to_string(), ], ..Default::default() }, TaskTemplate { label: "npm: build:compiler".to_string(), command: "npm".to_string(), args: vec!["run".to_string(), "build:compiler".to_string()], ..Default::default() }, TaskTemplate { label: "npm: build:tests".to_string(), command: "npm".to_string(), args: vec!["run".to_string(), "build:tests:notypecheck".to_string()], ..Default::default() }, ]; let tasks: TaskTemplates = vscode_definitions.try_into().unwrap(); assert_eq!(tasks.0, expected); } #[test] fn can_deserialize_rust_analyzer_tasks() { const RUST_ANALYZER_TASKS: &str = include_str!("../test_data/rust-analyzer.json"); let vscode_definitions: VsCodeTaskFile = serde_json_lenient::from_str(RUST_ANALYZER_TASKS).unwrap(); let expected = vec![ VsCodeTaskDefinition { label: "Build Extension in Background".to_string(), command: Some(Command::Npm { script: "watch".to_string(), }), options: None, other_attributes: Default::default(), }, VsCodeTaskDefinition { label: "Build Extension".to_string(), command: Some(Command::Npm { script: "build".to_string(), }), options: None, other_attributes: Default::default(), }, VsCodeTaskDefinition { label: "Build Server".to_string(), command: Some(Command::Shell { command: "cargo build --package rust-analyzer".to_string(), args: Default::default(), }), options: None, other_attributes: Default::default(), }, VsCodeTaskDefinition { label: "Build Server (Release)".to_string(), command: Some(Command::Shell { command: "cargo build --release --package rust-analyzer".to_string(), args: Default::default(), }), options: None, other_attributes: Default::default(), }, VsCodeTaskDefinition { label: "Pretest".to_string(), command: Some(Command::Npm { script: "pretest".to_string(), }), options: None, other_attributes: Default::default(), }, VsCodeTaskDefinition { label: "Build Server and Extension".to_string(), command: None, options: None, other_attributes: Default::default(), }, VsCodeTaskDefinition { label: "Build Server (Release) and Extension".to_string(), command: None, options: None, other_attributes: Default::default(), }, ]; assert_eq!(vscode_definitions.tasks.len(), expected.len()); vscode_definitions .tasks .iter() .zip(expected) .for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs)); let expected = vec![ TaskTemplate { label: "Build Extension in Background".to_string(), command: "npm".to_string(), args: vec!["run".to_string(), "watch".to_string()], ..Default::default() }, TaskTemplate { label: "Build Extension".to_string(), command: "npm".to_string(), args: vec!["run".to_string(), "build".to_string()], ..Default::default() }, TaskTemplate { label: "Build Server".to_string(), command: "cargo build --package rust-analyzer".to_string(), ..Default::default() }, TaskTemplate { label: "Build Server (Release)".to_string(), command: "cargo build --release --package rust-analyzer".to_string(), ..Default::default() }, TaskTemplate { label: "Pretest".to_string(), command: "npm".to_string(), args: vec!["run".to_string(), "pretest".to_string()], ..Default::default() }, ]; let tasks: TaskTemplates = vscode_definitions.try_into().unwrap(); assert_eq!(tasks.0, expected); } #[test] fn can_deserialize_tasks_without_labels() { const TASKS_WITHOUT_LABELS: &str = include_str!("../test_data/tasks-without-labels.json"); let vscode_definitions: VsCodeTaskFile = serde_json_lenient::from_str(TASKS_WITHOUT_LABELS).unwrap(); assert_eq!(vscode_definitions.tasks.len(), 4); assert_eq!(vscode_definitions.tasks[0].label, "npm: start"); assert_eq!(vscode_definitions.tasks[1].label, "Explicit Label"); assert_eq!(vscode_definitions.tasks[2].label, "gulp: build"); assert_eq!(vscode_definitions.tasks[3].label, "echo hello"); } #[test] fn test_generate_label() { assert_eq!( generate_label(&Some(Command::Npm { script: "start".to_string() })), "npm: start" ); assert_eq!( generate_label(&Some(Command::Gulp { task: "build".to_string() })), "gulp: build" ); assert_eq!( generate_label(&Some(Command::Shell { command: "echo hello".to_string(), args: vec![] })), "echo hello" ); assert_eq!( generate_label(&Some(Command::Shell { command: "cargo build --release".to_string(), args: vec![] })), "cargo build --release" ); assert_eq!( generate_label(&Some(Command::Shell { command: " ".to_string(), args: vec![] })), "shell" ); assert_eq!( generate_label(&Some(Command::Shell { command: "".to_string(), args: vec![] })), "shell" ); assert_eq!(generate_label(&None), "Untitled Task"); }}
use anyhow::bail;
use collections::HashMap;
use serde::Deserialize;
use util::ResultExt;
use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName};
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct TaskOptions {
cwd: Option<String>,
#[serde(default)]
env: HashMap<String, String>,
}
#[derive(Clone, Debug, PartialEq)]
struct VsCodeTaskDefinition {
label: String,
command: Option<Command>,
other_attributes: HashMap<String, serde_json_lenient::Value>,
options: Option<TaskOptions>,
}
impl<'de> serde::Deserialize<'de> for VsCodeTaskDefinition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TaskHelper {
#[serde(default)]
label: Option<String>,
#[serde(flatten)]
command: Option<Command>,
#[serde(flatten)]
other_attributes: HashMap<String, serde_json_lenient::Value>,
options: Option<TaskOptions>,
}
let helper = TaskHelper::deserialize(deserializer)?;
let label = helper
.label
.unwrap_or_else(|| generate_label(&helper.command));
Ok(VsCodeTaskDefinition {
label,
command: helper.command,
other_attributes: helper.other_attributes,
options: helper.options,
})
}
}
#[derive(Clone, Deserialize, PartialEq, Debug)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
enum Command {
Npm {
script: String,
},
Shell {
command: String,
#[serde(default)]
args: Vec<String>,
},
Gulp {
task: String,
},
}
fn generate_label(command: &Option<Command>) -> String {
match command {
Some(Command::Npm { script }) => format!("npm: {}", script),
Some(Command::Gulp { task }) => format!("gulp: {}", task),
Some(Command::Shell { command, .. }) => {
if command.trim().is_empty() {
"shell".to_string()
} else {
command.clone()
}
}
None => "Untitled Task".to_string(),
}
}
impl VsCodeTaskDefinition {
fn into_zed_format(
self,
replacer: &EnvVariableReplacer,
) -> anyhow::Result<Option<TaskTemplate>> {
if self.other_attributes.contains_key("dependsOn") {
log::warn!(
"Skipping deserializing of a task `{}` with the unsupported `dependsOn` key",
self.label
);
return Ok(None);
}
// `type` might not be set in e.g. tasks that use `dependsOn`; we still want to deserialize the whole object though (hence command is an Option),
// as that way we can provide more specific description of why deserialization failed.
// E.g. if the command is missing due to `dependsOn` presence, we can check other_attributes first before doing this (and provide nice error message)
// catch-all if on value.command presence.
let Some(command) = self.command else {
bail!("Missing `type` field in task");
};
let (command, args) = match command {
Command::Npm { script } => ("npm".to_owned(), vec!["run".to_string(), script]),
Command::Shell { command, args } => (command, args),
Command::Gulp { task } => ("gulp".to_owned(), vec![task]),
};
// Per VSC docs, only `command`, `args` and `options` support variable substitution.
let command = replacer.replace(&command);
let args = args.into_iter().map(|arg| replacer.replace(&arg)).collect();
let mut template = TaskTemplate {
label: self.label,
command,
args,
..TaskTemplate::default()
};
if let Some(options) = self.options {
template.cwd = options.cwd.map(|cwd| replacer.replace(&cwd));
template.env = options.env;
}
Ok(Some(template))
}
}
/// [`VsCodeTaskFile`] is a superset of Code's task definition format.
#[derive(Debug, Deserialize, PartialEq)]
pub struct VsCodeTaskFile {
tasks: Vec<VsCodeTaskDefinition>,
}
impl TryFrom<VsCodeTaskFile> for TaskTemplates {
type Error = anyhow::Error;
fn try_from(value: VsCodeTaskFile) -> Result<Self, Self::Error> {
let replacer = EnvVariableReplacer::new(HashMap::from_iter([
(
"workspaceFolder".to_owned(),
VariableName::WorktreeRoot.to_string(),
),
("file".to_owned(), VariableName::File.to_string()),
("lineNumber".to_owned(), VariableName::Row.to_string()),
(
"selectedText".to_owned(),
VariableName::SelectedText.to_string(),
),
]));
let templates = value
.tasks
.into_iter()
.filter_map(|vscode_definition| {
vscode_definition
.into_zed_format(&replacer)
.log_err()
.flatten()
})
.collect();
Ok(Self(templates))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::{
TaskTemplate, TaskTemplates, VsCodeTaskFile,
vscode_format::{Command, VsCodeTaskDefinition},
};
use super::{EnvVariableReplacer, generate_label};
fn compare_without_other_attributes(lhs: VsCodeTaskDefinition, rhs: VsCodeTaskDefinition) {
assert_eq!(
VsCodeTaskDefinition {
other_attributes: Default::default(),
..lhs
},
VsCodeTaskDefinition {
other_attributes: Default::default(),
..rhs
},
);
}
#[test]
fn test_variable_substitution() {
let replacer = EnvVariableReplacer::new(Default::default());
assert_eq!(replacer.replace("Food"), "Food");
// Unknown variables are left in tact.
assert_eq!(
replacer.replace("$PATH is an environment variable"),
"$PATH is an environment variable"
);
assert_eq!(replacer.replace("${PATH}"), "${PATH}");
assert_eq!(replacer.replace("${PATH:food}"), "${PATH:food}");
// And now, the actual replacing
let replacer = EnvVariableReplacer::new(HashMap::from_iter([(
"PATH".to_owned(),
"ZED_PATH".to_owned(),
)]));
assert_eq!(replacer.replace("Food"), "Food");
assert_eq!(
replacer.replace("$PATH is an environment variable"),
"${ZED_PATH} is an environment variable"
);
assert_eq!(replacer.replace("${PATH}"), "${ZED_PATH}");
assert_eq!(replacer.replace("${PATH:food}"), "${ZED_PATH:food}");
}
#[test]
fn can_deserialize_ts_tasks() {
const TYPESCRIPT_TASKS: &str = include_str!("../test_data/typescript.json");
let vscode_definitions: VsCodeTaskFile =
serde_json_lenient::from_str(TYPESCRIPT_TASKS).unwrap();
let expected = vec![
VsCodeTaskDefinition {
label: "gulp: tests".to_string(),
command: Some(Command::Npm {
script: "build:tests:notypecheck".to_string(),
}),
other_attributes: Default::default(),
options: None,
},
VsCodeTaskDefinition {
label: "tsc: watch ./src".to_string(),
command: Some(Command::Shell {
command: "node".to_string(),
args: vec![
"${workspaceFolder}/node_modules/typescript/lib/tsc.js".to_string(),
"--build".to_string(),
"${workspaceFolder}/src".to_string(),
"--watch".to_string(),
],
}),
other_attributes: Default::default(),
options: None,
},
VsCodeTaskDefinition {
label: "npm: build:compiler".to_string(),
command: Some(Command::Npm {
script: "build:compiler".to_string(),
}),
other_attributes: Default::default(),
options: None,
},
VsCodeTaskDefinition {
label: "npm: build:tests".to_string(),
command: Some(Command::Npm {
script: "build:tests:notypecheck".to_string(),
}),
other_attributes: Default::default(),
options: None,
},
];
assert_eq!(vscode_definitions.tasks.len(), expected.len());
vscode_definitions
.tasks
.iter()
.zip(expected)
.for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs));
let expected = vec![
TaskTemplate {
label: "gulp: tests".to_string(),
command: "npm".to_string(),
args: vec!["run".to_string(), "build:tests:notypecheck".to_string()],
..Default::default()
},
TaskTemplate {
label: "tsc: watch ./src".to_string(),
command: "node".to_string(),
args: vec![
"${ZED_WORKTREE_ROOT}/node_modules/typescript/lib/tsc.js".to_string(),
"--build".to_string(),
"${ZED_WORKTREE_ROOT}/src".to_string(),
"--watch".to_string(),
],
..Default::default()
},
TaskTemplate {
label: "npm: build:compiler".to_string(),
command: "npm".to_string(),
args: vec!["run".to_string(), "build:compiler".to_string()],
..Default::default()
},
TaskTemplate {
label: "npm: build:tests".to_string(),
command: "npm".to_string(),
args: vec!["run".to_string(), "build:tests:notypecheck".to_string()],
..Default::default()
},
];
let tasks: TaskTemplates = vscode_definitions.try_into().unwrap();
assert_eq!(tasks.0, expected);
}
#[test]
fn can_deserialize_rust_analyzer_tasks() {
const RUST_ANALYZER_TASKS: &str = include_str!("../test_data/rust-analyzer.json");
let vscode_definitions: VsCodeTaskFile =
serde_json_lenient::from_str(RUST_ANALYZER_TASKS).unwrap();
let expected = vec![
VsCodeTaskDefinition {
label: "Build Extension in Background".to_string(),
command: Some(Command::Npm {
script: "watch".to_string(),
}),
options: None,
other_attributes: Default::default(),
},
VsCodeTaskDefinition {
label: "Build Extension".to_string(),
command: Some(Command::Npm {
script: "build".to_string(),
}),
options: None,
other_attributes: Default::default(),
},
VsCodeTaskDefinition {
label: "Build Server".to_string(),
command: Some(Command::Shell {
command: "cargo build --package rust-analyzer".to_string(),
args: Default::default(),
}),
options: None,
other_attributes: Default::default(),
},
VsCodeTaskDefinition {
label: "Build Server (Release)".to_string(),
command: Some(Command::Shell {
command: "cargo build --release --package rust-analyzer".to_string(),
args: Default::default(),
}),
options: None,
other_attributes: Default::default(),
},
VsCodeTaskDefinition {
label: "Pretest".to_string(),
command: Some(Command::Npm {
script: "pretest".to_string(),
}),
options: None,
other_attributes: Default::default(),
},
VsCodeTaskDefinition {
label: "Build Server and Extension".to_string(),
command: None,
options: None,
other_attributes: Default::default(),
},
VsCodeTaskDefinition {
label: "Build Server (Release) and Extension".to_string(),
command: None,
options: None,
other_attributes: Default::default(),
},
];
assert_eq!(vscode_definitions.tasks.len(), expected.len());
vscode_definitions
.tasks
.iter()
.zip(expected)
.for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs));
let expected = vec![
TaskTemplate {
label: "Build Extension in Background".to_string(),
command: "npm".to_string(),
args: vec!["run".to_string(), "watch".to_string()],
..Default::default()
},
TaskTemplate {
label: "Build Extension".to_string(),
command: "npm".to_string(),
args: vec!["run".to_string(), "build".to_string()],
..Default::default()
},
TaskTemplate {
label: "Build Server".to_string(),
command: "cargo build --package rust-analyzer".to_string(),
..Default::default()
},
TaskTemplate {
label: "Build Server (Release)".to_string(),
command: "cargo build --release --package rust-analyzer".to_string(),
..Default::default()
},
TaskTemplate {
label: "Pretest".to_string(),
command: "npm".to_string(),
args: vec!["run".to_string(), "pretest".to_string()],
..Default::default()
},
];
let tasks: TaskTemplates = vscode_definitions.try_into().unwrap();
assert_eq!(tasks.0, expected);
}
#[test]
fn can_deserialize_tasks_without_labels() {
const TASKS_WITHOUT_LABELS: &str = include_str!("../test_data/tasks-without-labels.json");
let vscode_definitions: VsCodeTaskFile =
serde_json_lenient::from_str(TASKS_WITHOUT_LABELS).unwrap();
assert_eq!(vscode_definitions.tasks.len(), 4);
assert_eq!(vscode_definitions.tasks[0].label, "npm: start");
assert_eq!(vscode_definitions.tasks[1].label, "Explicit Label");
assert_eq!(vscode_definitions.tasks[2].label, "gulp: build");
assert_eq!(vscode_definitions.tasks[3].label, "echo hello");
}
#[test]
fn test_generate_label() {
assert_eq!(
generate_label(&Some(Command::Npm {
script: "start".to_string()
})),
"npm: start"
);
assert_eq!(
generate_label(&Some(Command::Gulp {
task: "build".to_string()
})),
"gulp: build"
);
assert_eq!(
generate_label(&Some(Command::Shell {
command: "echo hello".to_string(),
args: vec![]
})),
"echo hello"
);
assert_eq!(
generate_label(&Some(Command::Shell {
command: "cargo build --release".to_string(),
args: vec![]
})),
"cargo build --release"
);
assert_eq!(
generate_label(&Some(Command::Shell {
command: " ".to_string(),
args: vec![]
})),
"shell"
);
assert_eq!(
generate_label(&Some(Command::Shell {
command: "".to_string(),
args: vec![]
})),
"shell"
);
assert_eq!(generate_label(&None), "Untitled Task");
}
}