Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

dwarves-gui/tests/robot_explorer_links.rs

//! **🧭 Explorer links carry their selection** (owner bug on loki, 0aa9c50: following
//! the Build Thing `tunnr` opened ⚒ Build Thing with nothing selected).
//!
//! The robot drives the SHIPPED shell (`TabbedView`) holding the Explorer and the tabs
//! it links to, over a real forge (Explorer service + code-room proxy) in front of a
//! mock nornir, and a staged constellation the ⚒ Build Thing scene reads. Each link is
//! clicked and the TARGET tab's own state is asserted:
//!
//! * Build Thing `tunnr` (variant `tunnr-cli` picked) → ⚒ Build Thing resolved on
//!   tunnr, row `tunnr-cli`;
//! * workspace → 🌲 Tree on that workspace;
//! * pipeline → Pipeline on that file;
//! * a run of the history → Forge with that run pinned.

use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use dwarves::explorer::lane::{spawn_forge, FakeRunner};
use dwarves_gui::build_forge::DeclaredForge;
use dwarves_gui::build_scene::BuildScene;
use dwarves_gui::explorer::ExplorerTab;
use dwarves_gui::front_tree::FrontTree;
use dwarves_gui::tabbed::{LeafPanel, SplitPanel, TabPanel, TabbedView};
use egui_kittest::kittest::Queryable;
use facett_core::{Facet, Panel};

fn st(shell: &Arc<Mutex<TabbedView>>) -> Value {
    Facet::state_json(&*shell.lock().unwrap())
}

fn tell(shell: &Arc<Mutex<TabbedView>>, msg: &str) {
    Facet::update_json(&mut *shell.lock().unwrap(), msg);
}
use nornir_warehouse_embed::testkit::{spawn_router, MockCodeRoom, WorkspacesLog};
use serde_json::{json, Value};

const PROJECT: &str = r#"
[project]
name = "shop"

[mimir]
name = "shop"

[[workspaces]]
name = "shop"

[[build_things]]
name = "tunnr"
repo = "tunnr"

[[pipelines]]
name = "tunnr-ci"
repo = "tunnr"
file = "tunnr.runa.ron"
"#;

/// The forge's warehouse (the registered project) and a constellation of checkouts:
/// `tunnr` declares two variants and a pipeline; `dwarves` holds the Pipeline tab's
/// own file (its repo root).
fn lay_out(dir: &Path) {
    let projects = dir.join("warehouse/projects");
    std::fs::create_dir_all(&projects).unwrap();
    std::fs::write(projects.join("shop.dwarves.toml"), PROJECT).unwrap();
    let tunnr = dir.join("checkouts/tunnr");
    std::fs::create_dir_all(tunnr.join(".nornir")).unwrap();
    std::fs::create_dir_all(tunnr.join("src")).unwrap();
    std::fs::write(tunnr.join("Cargo.toml"), "[package]\nname = \"tunnr\"\nversion = \"0.2.1\"\nedition = \"2021\"\n\n[[bin]]\nname = \"tunnr-vm\"\npath = \"src/main.rs\"\n").unwrap();
    std::fs::write(tunnr.join("src/main.rs"), "fn main() {}\n").unwrap();
    std::fs::write(
        tunnr.join("Cargo.lock"),
        "version = 4\n\n[[package]]\nname = \"tunnr\"\nversion = \"0.2.1\"\n",
    )
    .unwrap();
    std::fs::write(
        tunnr.join(".nornir/build-variants.toml"),
        r#"
[build]
base_suite = "ORIGINAL"

[[build.variant]]
name = "tunnr-vm"
package = "tunnr"
bin = "tunnr-vm"
role = "server"
default = true
suites = ["ORIGINAL"]
kind = "exe"

[[build.variant]]
name = "tunnr-cli"
package = "tunnr"
bin = "tunnr-vm"
role = "tool"
default = false
suites = ["ORIGINAL"]
kind = "exe"
"#,
    )
    .unwrap();
    std::fs::write(
        tunnr.join("tunnr.runa.ron"),
        r#"(name: "tunnr-ci", stages: [(name: "lint", steps: [CargoTest]), (name: "build", needs: ["lint"], steps: [Build(variant: "tunnr-vm")])])"#,
    )
    .unwrap();
    let dwarves = dir.join("checkouts/dwarves");
    std::fs::create_dir_all(&dwarves).unwrap();
    std::fs::write(
        dwarves.join("dwarves.runa.ron"),
        r#"(name: "dwarves-forge", stages: [(name: "lint", steps: [CargoTest])])"#,
    )
    .unwrap();
}

fn board() -> Value {
    json!({
        "state": "complete",
        "members": [{ "member": "tunnr", "phase": "landed", "sha": "t1", "units": 1, "done": 1, "own_units": 1 }],
        "counts": { "done": 1 },
        "distinct_units": 1,
        "units": [{ "key": "t", "package": "path+ws:tunnr#tunnr@0.2.1", "target": "bin:tunnr-vm", "mode": "check", "test": false, "features": [], "members": ["tunnr"], "shared_by": 1, "state": "done", "deps": [] }],
    })
}

fn step_until(
    h: &mut egui_kittest::Harness<'_>,
    shell: &Arc<Mutex<TabbedView>>,
    secs: u64,
    what: &str,
    cond: impl Fn(&Value) -> bool,
) -> Value {
    let t0 = Instant::now();
    loop {
        h.step();
        let s = st(shell);
        if cond(&s) {
            return s;
        }
        assert!(
            t0.elapsed() < Duration::from_secs(secs),
            "{what}: never happened — active {} · explorer {} · build-thing {}",
            s["active_tab"],
            s["explorer"],
            s["build-thing"]
        );
        std::thread::sleep(Duration::from_millis(15));
    }
}

#[test]
fn every_explorer_link_opens_its_tab_on_the_selection() {
    let dir = tempfile::tempdir().unwrap();
    lay_out(dir.path());
    let checkouts = dir.path().join("checkouts");
    // the Pipeline tab's repo root: the dwarves checkout of this constellation
    std::env::set_var("DWARVES_REPO_ROOT", checkouts.join("dwarves"));

    let registry = WorkspacesLog::default();
    registry.set_member_states(&[("tunnr", "ok")]);
    let room = MockCodeRoom::answering(&[
        ("Coderoom.Units", board()),
        (
            "Coderoom.Artifacts",
            json!({ "state": "empty_fact", "count": 0, "artifacts": [] }),
        ),
    ]);
    let nornir = spawn_router(move |mut s| {
        s.add_service(
            nornir_warehouse_embed::pb::workspaces_server::WorkspacesServer::new(registry),
        )
        .add_service(nornir_warehouse_embed::pb::code_room_server::CodeRoomServer::new(room))
    });
    nornir_warehouse_embed::register_workspace_descriptor(
        &nornir.endpoint,
        "",
        "shop",
        "[workspace]\nname = \"shop\"\n",
        "monitored",
    )
    .unwrap();
    let forge = spawn_forge(
        nornir.endpoint.clone(),
        String::new(),
        dir.path().join("warehouse"),
        Some(checkouts.clone()),
        Arc::new(FakeRunner::ok(Duration::from_millis(200))),
    );

    let explorer: Box<dyn Panel> =
        Box::new(ExplorerTab::new(forge.endpoint.clone(), String::new()));
    let build_thing: Box<dyn LeafPanel> =
        Box::new(BuildScene::new(Box::new(DeclaredForge::at(checkouts.clone()))).hosted());
    let tree: Box<dyn Panel> = Box::new(FrontTree::new(
        forge.endpoint.clone(),
        String::new(),
        String::new(),
    ));
    let pipeline: Box<dyn SplitPanel> = Box::new(dwarves_gui::pipeline::pipeline_view());
    let forge_tab: Box<dyn Panel> = dwarves_embed::panel(forge.endpoint.clone());
    let tabs: Vec<(String, TabPanel)> = vec![
        ("Explorer".into(), explorer.into()),
        ("Build Thing".into(), build_thing.into()),
        ("Tree".into(), tree.into()),
        ("Pipeline".into(), pipeline.into()),
        ("Forge".into(), forge_tab.into()),
    ];
    let shell = Arc::new(Mutex::new(TabbedView::new("links", tabs)));
    let s2 = shell.clone();
    let mut h = egui_kittest::Harness::builder()
        .with_size(egui::vec2(1600.0, 1100.0))
        .build_ui(move |ui| Facet::ui(&mut *s2.lock().unwrap(), ui));

    // ── Build Thing `tunnr`, variant `tunnr-cli` → ⚒ Build Thing on tunnr ──
    step_until(&mut h, &shell, 20, "the project tree", |s| {
        s["explorer"]["tree"]
            .as_array()
            .is_some_and(|t| t.iter().any(|n| n == "build_thing:tunnr"))
    });
    let s = st(&shell);
    assert_eq!(
        s["build-thing"]["recipe"],
        Value::Null,
        "RED before the link: nothing is resolved in ⚒ Build Thing"
    );
    h.get_by_label("🔨 tunnr").click();
    step_until(&mut h, &shell, 20, "tunnr's variants", |s| {
        s["explorer"]["variants"] == json!(["tunnr-vm", "tunnr-cli"])
    });
    h.get_by_label("tunnr-cli").click();
    h.step();
    h.get_by_label("open ⚒ Build Thing").click();
    let s = step_until(&mut h, &shell, 20, "⚒ Build Thing open on tunnr", |s| {
        s["active_tab"] == "Build Thing" && !s["build-thing"]["recipe"].is_null()
    });
    assert_eq!(
        s["build-thing"]["artifact"], "tunnr",
        "{}",
        s["build-thing"]
    );
    assert_eq!(
        s["build-thing"]["row"], "tunnr-cli",
        "the picked variant is the selected row: {}",
        s["build-thing"]
    );
    assert_eq!(s["explorer"]["last_link"]["tab"], "build-thing");

    // ── workspace → 🌲 Tree on that workspace ──
    tell(&shell, r#"{"set_tab":"Explorer"}"#);
    h.step();
    h.get_by_label("📚 shop").click();
    step_until(&mut h, &shell, 20, "the workspace", |s| {
        s["explorer"]["detail_kind"] == "workspace"
    });
    h.get_by_label("open 🌲 Tree").click();
    let s = step_until(&mut h, &shell, 20, "🌲 Tree open on shop", |s| {
        s["active_tab"] == "Tree"
    });
    assert_eq!(s["tree"]["workspace"], "shop", "{}", s["tree"]);

    // ── a run → Forge, that run pinned ──
    tell(&shell, r#"{"set_tab":"Explorer"}"#);
    h.step();
    h.get_by_label("▶ Build workspace").click();
    let s = step_until(&mut h, &shell, 60, "a run in the history", |s| {
        s["explorer"]["builds"]
            .as_array()
            .is_some_and(|b| !b.is_empty())
    });
    let run = s["explorer"]["builds"][0].as_str().unwrap().to_string();
    h.get_by_label(run.as_str()).click();
    let s = step_until(&mut h, &shell, 20, "Forge open on the run", |s| {
        s["active_tab"] == "Forge"
    });
    assert_eq!(s["forge"]["selected_run_id"], json!(run), "{}", s["forge"]);

    // ── pipeline → Pipeline on that file (a sibling checkout's) ──
    tell(&shell, r#"{"set_tab":"Explorer"}"#);
    h.step();
    h.get_by_label("🔀 tunnr-ci").click();
    step_until(&mut h, &shell, 20, "the pipeline", |s| {
        s["explorer"]["detail_kind"] == "pipeline"
    });
    h.get_by_label("open Pipeline").click();
    let s = step_until(&mut h, &shell, 20, "Pipeline open on tunnr.runa.ron", |s| {
        s["active_tab"] == "Pipeline"
    });
    assert_eq!(
        (
            s["pipeline"]["pipeline_file"].as_str(),
            s["pipeline"]["pipeline_name"].as_str()
        ),
        (Some("tunnr.runa.ron"), Some("tunnr-ci")),
        "{}",
        s["pipeline"]
    );
}