181 lines
7.8 KiB
Rust
181 lines
7.8 KiB
Rust
//! `app.scheduleTask` / `app.cancelTask` (#773): repeating tasks run whenever they are due (frontends
|
|
//! tick [`crate::run_tasks`] every frame) until they are cancelled, throw, or their owner goes
|
|
//! away; one-shot tasks scheduled later run when due too.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use effectcraft_engine::{ScriptRequest, Session};
|
|
use serde_json::{Value, json};
|
|
use web_time::{Duration, Instant};
|
|
|
|
use crate::run_code;
|
|
use crate::tasks::run_tasks_at;
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session { expr: Some(Arc::new(effectcraft_expr::Expressions)), ..Default::default() };
|
|
crate::install(&mut s);
|
|
s.config = Some(Arc::new(effectcraft_engine::config::MemoryConfig::default()));
|
|
s
|
|
}
|
|
|
|
fn ok(s: &mut Session, code: &str) {
|
|
let o = run_code(s, code, "tasks.jsx");
|
|
assert!(o.error.is_none(), "script failed: {:?}\noutput: {:?}", o.error, o.output);
|
|
}
|
|
|
|
fn console(s: &mut Session, code: &str) -> crate::Outcome {
|
|
let o = crate::run(s, &ScriptRequest { code, name: "console", console: true });
|
|
assert!(o.error.is_none(), "console failed: {:?}", o.error);
|
|
o
|
|
}
|
|
|
|
fn ms(n: u64) -> Duration {
|
|
Duration::from_millis(n)
|
|
}
|
|
|
|
fn output(r: &Value) -> Vec<String> {
|
|
r["output"].as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect()
|
|
}
|
|
|
|
fn comps(s: &Session) -> usize {
|
|
s.project.items.values().filter(|i| i.as_comp().is_some()).count()
|
|
}
|
|
|
|
#[test]
|
|
fn a_repeating_task_runs_every_interval_until_cancelled() {
|
|
let mut s = session();
|
|
ok(
|
|
&mut s,
|
|
"var n = 0;\nvar id = app.scheduleTask(\"n++; writeLn('tick ' + n); app.project.items.addComp('C' + n, 64, 64, 1, 1, 25); if (n == 3) app.cancelTask(id);\", 1000, true);",
|
|
);
|
|
let t0 = Instant::now();
|
|
// Not due yet: nothing runs, and the frontend learns when to come back.
|
|
let r = run_tasks_at(&mut s, t0);
|
|
assert_eq!(r["ran"], json!(0));
|
|
assert!(r["next"].as_u64().is_some_and(|n| n <= 1000), "{r}");
|
|
let r = run_tasks_at(&mut s, t0 + ms(1500));
|
|
assert_eq!((r["ran"].clone(), output(&r)), (json!(1), vec!["tick 1".to_string()]), "{r}");
|
|
assert_eq!(comps(&s), 1, "the task edits the project through engine commands");
|
|
// Due again an interval after it ran, once per pass (no catch-up burst).
|
|
assert_eq!(run_tasks_at(&mut s, t0 + ms(1500))["ran"], json!(0));
|
|
assert_eq!(run_tasks_at(&mut s, t0 + ms(2600))["ran"], json!(1));
|
|
let r = run_tasks_at(&mut s, t0 + ms(9000));
|
|
assert_eq!(output(&r), vec!["tick 3"]);
|
|
// Cancelled: the script (no windows, no tasks) has ended.
|
|
let r = run_tasks_at(&mut s, t0 + ms(20_000));
|
|
assert_eq!((r["ran"].clone(), r["next"].clone()), (json!(0), Value::Null), "{r}");
|
|
assert_eq!(comps(&s), 3);
|
|
assert!(s.history.undo.len() >= 3, "every run is undoable");
|
|
}
|
|
|
|
#[test]
|
|
fn a_panels_task_refreshes_it_until_the_panel_closes() {
|
|
let mut s = session();
|
|
ok(
|
|
&mut s,
|
|
r#"
|
|
var w = new Window("palette", "Clock");
|
|
var label = w.add("statictext", undefined, "0");
|
|
var n = 0;
|
|
app.scheduleTask("n++; label.text = String(n);", 200, true);
|
|
w.show();
|
|
"#,
|
|
);
|
|
let win = s.script_ui.windows[0].id;
|
|
let text = |s: &Session| s.script_ui.window(win).map(|w| w.root.children[0].text.clone());
|
|
let t0 = Instant::now();
|
|
run_tasks_at(&mut s, t0 + ms(500));
|
|
run_tasks_at(&mut s, t0 + ms(1000));
|
|
assert_eq!(text(&s).as_deref(), Some("2"), "the window shows what the task set");
|
|
// A project change doesn't stop a script whose window is open.
|
|
s.execute("file.closeProject", json!({})).unwrap();
|
|
assert_eq!(run_tasks_at(&mut s, t0 + ms(1500))["ran"], json!(1));
|
|
assert_eq!(text(&s).as_deref(), Some("3"));
|
|
// Closing the window ends the script and its tasks.
|
|
s.execute("scriptui.close", json!({"window": win})).unwrap();
|
|
let r = run_tasks_at(&mut s, t0 + ms(5000));
|
|
assert_eq!((r["ran"].clone(), r["next"].clone()), (json!(0), Value::Null), "{r}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_throwing_task_is_reported_once_and_cancelled() {
|
|
let mut s = session();
|
|
ok(&mut s, "app.scheduleTask('undefinedThing.go()', 100, true); app.scheduleTask('writeLn(\"fine\")', 100, true);");
|
|
let t0 = Instant::now();
|
|
let r = run_tasks_at(&mut s, t0 + ms(500));
|
|
assert_eq!(r["ran"], json!(2));
|
|
let errors = r["errors"].as_array().cloned().unwrap_or_default();
|
|
assert_eq!(errors.len(), 1, "{r}");
|
|
assert!(errors[0]["message"].as_str().is_some_and(|m| m.contains("undefinedThing") && m.contains("cancelled")), "{r}");
|
|
// The other task goes on; the failing one doesn't run again.
|
|
let r = run_tasks_at(&mut s, t0 + ms(1000));
|
|
assert_eq!((r["ran"].clone(), output(&r), r["errors"].clone()), (json!(1), vec!["fine".to_string()], json!([])), "{r}");
|
|
}
|
|
|
|
#[test]
|
|
fn tasks_are_bounded() {
|
|
let mut s = session();
|
|
// Repeating tasks run at most every MIN_REPEAT_MS.
|
|
ok(&mut s, "var hits = 0; app.scheduleTask('hits++', 0, true);");
|
|
let t0 = Instant::now();
|
|
assert_eq!(run_tasks_at(&mut s, t0 + ms(crate::MIN_REPEAT_MS) + ms(20))["ran"], json!(1));
|
|
assert_eq!(run_tasks_at(&mut s, t0 + ms(crate::MIN_REPEAT_MS) + ms(30))["ran"], json!(0));
|
|
// At most MAX_TASKS pending tasks per script.
|
|
let o = run_code(&mut s, "for (var i = 0; i < 1000; i++) app.scheduleTask('1', 1000, true);", "many.jsx");
|
|
assert!(o.error.as_ref().is_some_and(|e| e.message.contains(&format!("at most {}", crate::MAX_TASKS))), "{:?}", o.error);
|
|
// Task code nested too deeply for the parser is refused up front.
|
|
let deep = format!("app.scheduleTask('{}', 10, false);", "(".repeat(10_000));
|
|
let o = run_code(&mut s, &deep, "deep.jsx");
|
|
assert!(o.error.as_ref().is_some_and(|e| e.message.contains("nested too deeply")), "{:?}", o.error);
|
|
// Odd delays and ids are harmless.
|
|
ok(
|
|
&mut s,
|
|
"app.cancelTask(-1); app.cancelTask(NaN); app.cancelTask(1e20); app.scheduleTask('1', NaN, false); app.scheduleTask('1', -5, false); app.scheduleTask('1', 1e300, true);",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn closing_the_project_stops_tasks_of_scripts_without_windows() {
|
|
let mut s = session();
|
|
ok(&mut s, "app.scheduleTask('writeLn(1)', 100, true);");
|
|
console(&mut s, "var cid = app.scheduleTask('writeLn(2)', 100, true);");
|
|
let t0 = Instant::now();
|
|
assert_eq!(run_tasks_at(&mut s, t0 + ms(500))["ran"], json!(2));
|
|
s.execute("file.closeProject", json!({})).unwrap();
|
|
let r = run_tasks_at(&mut s, t0 + ms(5000));
|
|
assert_eq!((r["ran"].clone(), r["next"].clone()), (json!(0), Value::Null), "{r}");
|
|
}
|
|
|
|
#[test]
|
|
fn console_tasks_run_in_the_console_and_cancel_there() {
|
|
let mut s = session();
|
|
console(&mut s, "var hits = 0; var tid = app.scheduleTask('hits++', 100, true); app.scheduleTask('writeLn(\"once \" + hits)', 300, false);");
|
|
let t0 = Instant::now();
|
|
assert_eq!(run_tasks_at(&mut s, t0 + ms(200))["ran"], json!(1));
|
|
let r = run_tasks_at(&mut s, t0 + ms(400));
|
|
assert_eq!((r["ran"].clone(), output(&r)), (json!(2), vec!["once 2".to_string()]), "{r}");
|
|
assert_eq!(console(&mut s, "app.cancelTask(tid); hits").result, json!(2));
|
|
let r = run_tasks_at(&mut s, t0 + ms(5000));
|
|
assert_eq!((r["ran"].clone(), r["next"].clone()), (json!(0), Value::Null), "{r}");
|
|
}
|
|
|
|
#[test]
|
|
fn handlers_schedule_one_shot_tasks_that_run_when_due() {
|
|
let mut s = session();
|
|
ok(
|
|
&mut s,
|
|
r#"
|
|
var w = new Window("palette", "Later");
|
|
var b = w.add("button", undefined, "Go", { name: "go" });
|
|
b.onClick = function () { app.scheduleTask("writeLn('later')", 100, false); };
|
|
w.show();
|
|
"#,
|
|
);
|
|
s.execute("scriptui.click", json!({"widget": "go"})).unwrap();
|
|
let r = run_tasks_at(&mut s, Instant::now() + ms(500));
|
|
assert_eq!(output(&r), vec!["later"], "{r}");
|
|
// The engine command runs the same pass (headless hosts, agents).
|
|
assert_eq!(s.execute("script.runTasks", json!({})).unwrap()["ran"], json!(0));
|
|
s.execute("scriptui.close", json!({})).unwrap();
|
|
}
|