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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- **Fixed** A mistyped or misplaced setting in a task now gets an error that names it. For example, `cache: { outputs: [...] }` now fails with an `unknown field` error that names `outputs` and lists the accepted settings, instead of `data did not match any variant of untagged enum UserTaskDefinition` ([vite-plus#2635](https://github.com/voidzero-dev/vite-plus/issues/2635), [#815](https://github.com/voidzero-dev/vite-task/pull/815)).
- **Fixed** In cached tasks on Linux and macOS, programs that pass `-1` as a directory descriptor to calls such as `openat` no longer abort. For example, `ps -p <pid>` from procps-ng 4.0.6 or later aborted when the process had already exited ([#862](https://github.com/voidzero-dev/vite-task/pull/862)).
- **Fixed** `vp run` no longer panics before running a script that contains non-ASCII characters, such as `echo こんにちは`, and no longer cuts off such commands where it displays them ([#861](https://github.com/voidzero-dev/vite-task/pull/861)).
- **Fixed** On Linux, concurrent system calls in tracked programs, such as `pnpm dlx`, no longer cause a stack overflow ([#854](https://github.com/voidzero-dev/vite-task/pull/854)).
Expand Down
30 changes: 30 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ rustls = { version = "0.23.44", default-features = false }
# SeccompAction::UserNotif (SECCOMP_RET_USER_NOTIF) was added after the latest published release (v0.5.0)
seccompiler = { git = "https://github.com/rust-vmm/seccompiler", rev = "08587106340b8e3cb361c7561411510039436857" }
serde = "1.0.219"
serde-untagged = "0.1.9"
serde_bytes = "0.11.19"
serde_json = "1.0.140"
serde_norway = "0.9.42"
Expand Down
1 change: 1 addition & 0 deletions crates/vt_graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ wincode = { workspace = true, features = ["derive"] }
petgraph = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, features = ["derive", "rc"] }
serde-untagged = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
Expand Down
66 changes: 59 additions & 7 deletions crates/vt_graph/src/config/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
use std::sync::Arc;

use rustc_hash::FxHashMap;
use serde::Deserialize;
use serde::{Deserialize, Deserializer};
use serde_untagged::UntaggedEnumVisitor;
#[cfg(all(test, not(clippy)))]
use ts_rs::TS;
use vec1::Vec1;
Expand Down Expand Up @@ -154,17 +155,27 @@ pub enum UserOutputEntry {
}

/// The value of a task's `cache` field.
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "TaskCache"))]
#[serde(untagged)]
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "TaskCache", untagged))]
pub enum UserCacheConfig {
/// `true` enables caching with default settings; `false` disables caching.
Bool(bool),
/// Enables caching with the given settings.
Config(EnabledCacheConfig),
}

// Not `#[serde(untagged)]`, which replaces each variant's error with a generic one.
impl<'de> Deserialize<'de> for UserCacheConfig {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
UntaggedEnumVisitor::new()
.expecting("a boolean or an object of cache settings")
.bool(|enabled| Ok(Self::Bool(enabled)))
.map(|map| map.deserialize().map(Self::Config))
.deserialize(deserializer)
}
}

impl Default for UserCacheConfig {
/// Caching enabled with default settings, used when `cache` is omitted.
fn default() -> Self {
Expand Down Expand Up @@ -321,17 +332,30 @@ pub struct UserTaskConfig {
}

/// User-defined task configuration or command-only shorthand in `vite.config.*`.
#[derive(Debug, Deserialize, PartialEq)]
#[derive(Debug, PartialEq)]
// TS derive macro generates code using std types that clippy disallows; skip derive during linting
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "TaskDefinition"))]
#[serde(untagged)]
#[cfg_attr(all(test, not(clippy)), derive(TS), ts(rename = "TaskDefinition", untagged))]
pub enum UserTaskDefinition {
/// Full task object form.
Object(UserTaskConfig),
/// Command-only shorthand form using default task options.
CommandShorthand(Command),
}

// Not `#[serde(untagged)]`, which replaces each variant's error with a generic one.
impl<'de> Deserialize<'de> for UserTaskDefinition {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
UntaggedEnumVisitor::new()
.expecting("a command string, an array of command strings, or a task object")
.string(|command| Ok(Self::CommandShorthand(Command::Single(command.into()))))
.seq(|seq| {
seq.deserialize().map(|commands| Self::CommandShorthand(Command::Array(commands)))
})
.map(|map| map.deserialize().map(Self::Object))
.deserialize(deserializer)
}
}

/// Root-level cache configuration.
///
/// Controls caching behavior for the entire workspace.
Expand Down Expand Up @@ -1068,4 +1092,32 @@ mod tests {
.is_err()
);
}

#[test]
fn test_invalid_task_error_names_the_problem() {
let cases = [
(json!({ "command": "echo", "outputs": [] }), "unknown field `outputs`"),
(
json!({ "command": "echo", "cache": { "outputs": [] } }),
"unknown field `outputs`, expected one of `env`, `untrackedEnv`, `input`, \
`output`, `remote`",
),
(
json!({ "command": "echo", "cache": "yes" }),
"invalid type: string \"yes\", expected a boolean or an object of cache settings",
),
(json!({ "cache": false }), "missing field `command`"),
(json!(["echo one", 2]), "invalid type: integer `2`, expected a string"),
(
json!(123),
"invalid type: integer `123`, expected a command string, an array of command \
strings, or a task object",
),
];
for (task, expected) in cases {
let user_config_json = json!({ "tasks": { "build": task } });
let error = serde_json::from_value::<UserRunConfig>(user_config_json).unwrap_err();
assert_eq!(error.to_string(), expected);
}
}
}
Loading