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
37 changes: 37 additions & 0 deletions crates/turborepo-engine/src/builder/test/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1467,3 +1467,40 @@ fn test_nonconflicting_env_exclusions_keep_resolved_outputs() {
assert_eq!(task.outputs.inclusions, ["automatic-output"]);
}
}

#[test_case(
json!({ "dev": { "interruptible": true } }),
"dev",
"Interruptible tasks must be persistent."
; "interruptible task must be persistent"
)]
#[test_case(
json!({ "build": { "inputs": [{ "mode": "dependencyOutputs" }] } }),
"build",
"dependencyOutputs mode was used for task \"build\", but this task has no dependency tasks to select."
; "dependency outputs require a dependency task to select"
)]
fn test_task_definition_validation_errors(tasks: serde_json::Value, task: &str, expected: &str) {
let repo_root_dir = TempDir::new().unwrap();
let repo_root = AbsoluteSystemPathBuf::try_from(repo_root_dir.path()).unwrap();
let package_graph = mock_package_graph(
&repo_root,
package_jsons! {
repo_root,
"a" => []
},
);
let loader = TestTurboJsonLoader::new(HashMap::from([(
PackageName::Root,
turbo_json(json!({ "tasks": tasks })),
)]));
let err = EngineBuilder::new(&repo_root, &package_graph, &loader, false)
.with_workspaces(vec![PackageName::from("a")])
.with_tasks(vec![Spanned::new(TaskName::from(task).into_owned())])
.build()
.unwrap_err();
assert!(
err.to_string().contains(expected),
"expected {expected:?}, got {err}"
);
}
25 changes: 25 additions & 0 deletions crates/turborepo-turbo-json/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,31 @@ mod tests {
}
}

#[test]
fn test_single_package_rejects_package_tasks() {
let root_dir = tempdir().unwrap();
let repo_root = AbsoluteSystemPath::from_std_path(root_dir.path()).unwrap();
let root_turbo_json = repo_root.join_component(CONFIG_FILE);
root_turbo_json
.create_with_contents(r#"{"tasks": {"build": {}, "my-app#build": {}}}"#)
.unwrap();

let loader = TurboJsonLoader::<NoOpUpdater>::single_package(
TurboJsonReader::new(repo_root.to_owned()),
root_turbo_json,
vec!["build".to_string()],
);
let err = loader.load(&PackageName::Root).unwrap_err();
assert!(
matches!(
&err,
LoaderError::TurboJson(Error::PackageTaskInSinglePackageMode { task_id, .. })
if task_id == "my-app#build"
),
"expected PackageTaskInSinglePackageMode, got {err:?}"
);
}

#[test]
fn test_workspace_turbo_json_loading() {
let root_dir = tempdir().unwrap();
Expand Down
98 changes: 98 additions & 0 deletions crates/turborepo-turbo-json/src/processed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1225,4 +1225,102 @@ mod tests {
assert!(depends_on.extends);
assert_eq!(depends_on.deps.len(), 2);
}

fn process_build_task(inputs: &str) -> Result<ProcessedTaskDefinition, Error> {
let json = format!(r#"{{"tasks": {{"build": {{"inputs": {inputs}}}}}}}"#);
let raw = crate::RawRootTurboJson::parse(&json, "turbo.json").expect("turbo.json parses");
let task = raw
.tasks
.expect("tasks are present")
.0
.remove(&crate::TaskName::from("build"))
.expect("build task is present");
ProcessedTaskDefinition::from_raw(task.into_inner(), &FutureFlags::default())
}

#[test_case(
r#"["$TURBO_DEFAULT$", {"mode": "startup", "globs": ["src/**"]}]"#,
&["Legacy input strings normalize to mode \"startup\"", "Use either legacy startup inputs", "Or one structured startup input"]
; "legacy and structured startup inputs cannot mix"
)]
#[test_case(
r#"[{"mode": "jit", "globs": ["src/generated/**"]}, {"mode": "jit", "globs": ["other/**"]}]"#,
&["duplicate structured \"jit\" input mode"]
; "duplicate structured modes"
)]
#[test_case(
r#"[{"globs": ["src/**"]}]"#,
&["Structured input entries must specify mode"]
; "mode is required"
)]
#[test_case(
r#"[{"mode": "runtime", "globs": ["src/**"]}]"#,
&["Unknown input mode \"runtime\""]
; "unknown modes"
)]
#[test_case(
r#"[{"mode": "jit", "from": ["codegen"], "globs": ["src/generated/**"]}]"#,
&["from is only valid for dependencyOutputs inputs"]
; "from outside dependency outputs"
)]
#[test_case(
r#"[{"mode": "dependencyOutputs", "withDefaults": true}]"#,
&["withDefaults is only valid for startup or jit inputs"]
; "with defaults on dependency outputs"
)]
#[test_case(
r#"[{"mode": "startup", "globs": ["$TURBO_DEFAULT$"]}]"#,
&["Sentinel string \"$TURBO_DEFAULT$\" is not valid inside structured globs"]
; "default sentinel inside structured globs"
)]
#[test_case(
r#"[{"mode": "jit", "globs": ["$TURBO_EXTENDS$"]}]"#,
&["Sentinel string \"$TURBO_EXTENDS$\" is not valid inside structured globs"]
; "extends sentinel inside structured globs"
)]
#[test_case(
r#"[{"mode": "startup", "globs": ["!src/generated/**"]}]"#,
&["negative-only startup globs require withDefaults: true"]
; "negative only startup globs"
)]
#[test_case(
r#"[{"mode": "jit", "globs": ["!src/generated/**"]}]"#,
&["negative-only jit globs require withDefaults: true"]
; "negative only jit globs"
)]
fn test_structured_inputs_reject_invalid_configuration(inputs: &str, expected: &[&str]) {
let err = process_build_task(inputs).unwrap_err();
assert_matches!(err, Error::StructuredInput { .. });
let message = err.to_string();
for fragment in expected {
assert!(
message.contains(fragment),
"expected {fragment:?} in {message:?}"
);
}
}

#[test_case(r#"[{"mode": "startup", "withDefaults": true, "globs": ["!src/generated/**"]}]"# ; "negative only startup globs with defaults")]
#[test_case(r#"[{"mode": "jit", "withDefaults": true, "globs": ["!src/generated/**"]}]"# ; "negative only jit globs with defaults")]
#[test_case(r#"[{"mode": "dependencyOutputs", "from": ["codegen"], "globs": ["dist/**"]}]"# ; "from on dependency outputs")]
fn test_structured_inputs_accept_valid_counterparts(inputs: &str) {
process_build_task(inputs).unwrap();
}

#[test]
fn test_env_rejects_pipeline_delimiter_prefix() {
let err = ProcessedEnv::new(
vec![
Spanned::new(UnescapedString::from("NODE_ENV".to_string())),
Spanned::new(UnescapedString::from("$FOOBAR".to_string())),
],
&FutureFlags::default(),
)
.unwrap_err();
let Error::InvalidEnvPrefix(err) = err else {
panic!("expected InvalidEnvPrefix, got {err:?}");
};
assert_eq!(err.value, "$FOOBAR");
assert_eq!(err.key, "env");
}
}
Loading
Loading