Host or attach the asset service, archive generated and terminal outputs, and provide image/video/audio/text/3D viewers with fullscreen and gallery navigation. Load only visible bounded previews, generate video posters and text excerpts, and normalize legacy thumbnails off the UI thread. Expose video frame counts and Sandbox chat providers, make failed queue cleanup idempotent, compact the panels, and correct short-wire direction. Replace App view with a movable, resizable CodeEditor overlay with debounced source saves, live graph updates, invalid-source preservation and close/reopen geometry. Validation: release Flow, Flow UI and flowgraph suites passed 335 tests (two ignored). Updated Flow UI suite passed 107 tests; asset route suites passed nine tests after legacy preview fixes. The rebuilt release app passed remote editing, dragging, resizing, invalid-source, close/reopen and App removal checks; final All assets scrolling measured 16.5 ms median and 18.3 ms maximum over 12 inputs. Test instances were closed through the remote protocol.
152 lines
5.9 KiB
Rust
152 lines
5.9 KiB
Rust
#![cfg(not(target_arch = "wasm32"))]
|
|
|
|
use makepad_asset_client::ApiEndpoints;
|
|
use makepad_flow::engine::executors::publish::{
|
|
AssetListQuery, AssetStoreConfig, AssetWorker, PublishExecutor,
|
|
};
|
|
use makepad_flow::engine::executors::{Executor, Poll};
|
|
use makepad_flow::{Literal, Loc, Node, Port, PortType, Value};
|
|
use std::path::PathBuf;
|
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
|
|
struct Roots(Vec<PathBuf>);
|
|
|
|
impl Roots {
|
|
fn new() -> Self { Self(Vec::new()) }
|
|
|
|
fn make(&mut self, label: &str) -> PathBuf {
|
|
let nonce = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
|
|
let path = std::env::temp_dir().join(format!("flow-publish-{}-{label}-{nonce}", std::process::id()));
|
|
std::fs::create_dir_all(&path).unwrap();
|
|
self.0.push(path.clone());
|
|
path
|
|
}
|
|
}
|
|
|
|
impl Drop for Roots {
|
|
fn drop(&mut self) {
|
|
for path in &self.0 { let _ = std::fs::remove_dir_all(path); }
|
|
}
|
|
}
|
|
|
|
fn start_store(root: PathBuf) -> (makepad_asset_store::AssetServer, String) {
|
|
let mut config = makepad_asset_store::ServerConfig::new(root.clone());
|
|
config.control_addr = "127.0.0.1:0".parse().unwrap();
|
|
config.data_addr = "127.0.0.1:0".parse().unwrap();
|
|
config.bootstrap_admin = true;
|
|
config.discovery = None;
|
|
config.log = false;
|
|
let server = makepad_asset_store::AssetServer::start(config).unwrap();
|
|
let token = std::fs::read_to_string(root.join("admin-token")).unwrap().trim().to_string();
|
|
(server, token)
|
|
}
|
|
|
|
fn node() -> Node {
|
|
Node {
|
|
id: "library".into(),
|
|
kind: "publish".into(),
|
|
type_name: "Publish".into(),
|
|
params: vec![
|
|
("title".into(), Literal::Str("Sunset result".into())),
|
|
("namespace".into(), Literal::Str("flows".into())),
|
|
("tags".into(), Literal::Arr(vec![Literal::Str("flow".into()), Literal::Str("sunset".into())])),
|
|
("description".into(), Literal::Str("painted from a prompt".into())),
|
|
("alias".into(), Literal::Str("flows/sunset-result".into())),
|
|
],
|
|
inputs: Vec::new(),
|
|
outputs: vec![Port { name: "asset".into(), ty: PortType::Json }],
|
|
at: None,
|
|
size: None,
|
|
flip: false,
|
|
loc: Loc::default(),
|
|
fn_src: None,
|
|
face_src: None,
|
|
on_fail: "fail".into(),
|
|
label: None,
|
|
domain: None,
|
|
doc: None,
|
|
}
|
|
}
|
|
|
|
fn picture(fill: u8) -> Value {
|
|
let mut pixels = vec![fill; 256 * 256 * 4];
|
|
for alpha in pixels.iter_mut().skip(3).step_by(4) { *alpha = 255; }
|
|
let png = makepad_ai_hub::testpattern::encode_png_rgba(&pixels, 256, 256).unwrap();
|
|
Value::media(PortType::Image, "image/png", png)
|
|
}
|
|
|
|
fn publish(worker: &AssetWorker, value: Value) -> makepad_strict_json::Value {
|
|
let mut executor = PublishExecutor::new(
|
|
Some(worker.handle()),
|
|
"prompt-to-library".into(),
|
|
"instance-1".into(),
|
|
String::new(),
|
|
);
|
|
executor.start(&node(), &[("value".into(), value)]).unwrap();
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
loop {
|
|
match executor.poll() {
|
|
Poll::Done(outputs) => {
|
|
let value = outputs.into_iter().find(|(port, _)| port == "asset").unwrap().1;
|
|
return makepad_strict_json::parse(&value.bytes).unwrap();
|
|
}
|
|
Poll::Failed(error) => panic!("publish failed: {error}"),
|
|
_ if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(20)),
|
|
other => panic!("publish timed out after {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn picture_publish_and_alias_republish_create_revisions() {
|
|
let mut roots = Roots::new();
|
|
let (mut server, token) = start_store(roots.make("store"));
|
|
let config = AssetStoreConfig {
|
|
archive_outputs: false,
|
|
cache_dir: roots.make("cache"),
|
|
token: Some(token),
|
|
endpoints: Some(ApiEndpoints { control: server.control_addr(), data: server.data_addr() }),
|
|
server_id: Some(server.server_id()),
|
|
discovery_port: 0,
|
|
discovery_wait_ms: 0,
|
|
};
|
|
let mut worker = AssetWorker::start(config).unwrap();
|
|
let first = publish(&worker, picture(30));
|
|
let second = publish(&worker, picture(90));
|
|
assert_eq!(first.get("id").and_then(|v| v.as_str()), second.get("id").and_then(|v| v.as_str()));
|
|
assert_ne!(first.get("revision").and_then(|v| v.as_str()), second.get("revision").and_then(|v| v.as_str()));
|
|
assert_eq!(second.get("alias").and_then(|v| v.as_str()), Some("flows/sunset-result"));
|
|
assert_eq!(second.get("namespace").and_then(|v| v.as_str()), Some("flows"));
|
|
assert_eq!(second.get("title").and_then(|v| v.as_str()), Some("Sunset result"));
|
|
assert_eq!(second.get("kind").and_then(|v| v.as_str()), Some("texture"));
|
|
|
|
let rows = worker.handle().list(AssetListQuery { text: String::new(), namespace: Some("flows".into()), limit: 10, cursor: None }).unwrap();
|
|
assert_eq!(rows.len(), 1);
|
|
assert_eq!(rows[0].title, "Sunset result");
|
|
assert_eq!(rows[0].namespace, "flows");
|
|
assert_eq!(rows[0].tags, vec!["flow", "sunset"]);
|
|
worker.stop();
|
|
server.shutdown();
|
|
}
|
|
|
|
#[test]
|
|
fn missing_store_failure_names_discovery() {
|
|
let mut roots = Roots::new();
|
|
let mut config = AssetStoreConfig::new(roots.make("cache"));
|
|
config.discovery_wait_ms = 0;
|
|
let mut worker = AssetWorker::start(config).unwrap();
|
|
let mut executor = PublishExecutor::new(Some(worker.handle()), "demo".into(), "one".into(), String::new());
|
|
executor.start(&node(), &[("value".into(), picture(10))]).unwrap();
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
loop {
|
|
match executor.poll() {
|
|
Poll::Failed(error) => {
|
|
assert!(error.contains("no asset server discovered on this LAN"), "{error}");
|
|
break;
|
|
}
|
|
_ if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(10)),
|
|
_ => panic!("missing store failure timed out"),
|
|
}
|
|
}
|
|
worker.stop();
|
|
}
|