main
Drew Owens 0.9.20 source 5e44698 16d ago
//! `swx_render_image` (0.9.18) in the simulator. The simulated viewport keeps
//! its DISPLAY STATE in the data root (`mock-display-state.json`) so every test
//! can compare it before and after: whatever the render applies is restored,
//! on success and on error. The COM side (com.rs) is UNMEASURED.

mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};

fn s(v: &Value) -> String {
    serde_json::to_string_pretty(v).unwrap_or_default()
}

fn session(scratch: &TempDir) {
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({"running": true, "untitled": 0, "active": "C:\\Users\\Public\\adom-hero\\Moveo.SLDASM",
               "docs": [{"docType": "assembly", "path": "C:\\Users\\Public\\adom-hero\\Moveo.SLDASM",
                         "title": "Moveo.SLDASM", "saved": true, "dirty": false}],
               "files": {}}).to_string(),
    )
    .unwrap();
}

fn display(scratch: &TempDir) -> Value {
    std::fs::read_to_string(scratch.join("mock-display-state.json"))
        .ok()
        .and_then(|t| serde_json::from_str(&t).ok())
        .unwrap_or(Value::Null)
}

const USERS_VIEW: &str = r#"{"displayMode":5,"perspective":false,"realView":false,"shadows":false,"ambientOcclusion":false,"backgroundAppearance":3,"backgroundColor":13421772,"orientation":"user-view","scale":1.0}"#;

fn boot(tag: &str) -> (Bridge, TempDir) {
    let scratch = TempDir::new(tag);
    session(&scratch);
    std::fs::write(scratch.join("mock-display-state.json"), USERS_VIEW).unwrap();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({}));
    (b, scratch)
}

fn hero(extra: Value) -> Value {
    let mut a = json!({"width": 64, "height": 48, "view": "isometric", "displayStyle": "shaded",
                       "perspective": true, "realView": true, "ambientOcclusion": true, "shadows": true,
                       "background": "white", "zoomToFit": true, "reason": "a hero shot of the Moveo assembly"});
    if let (Value::Object(m), Value::Object(e)) = (&mut a, extra) {
        m.extend(e);
    }
    a
}

#[test]
fn a_render_writes_a_png_of_the_asked_size_and_restores_every_setting() {
    let (b, scratch) = boot("render-ok");
    let before = display(&scratch);
    let r = b.call("swx_render_image", hero(json!({})));
    check!(b, "the render succeeds", r["success"] == true, s(&r));
    let path = r["path"].as_str().unwrap_or("").to_string();
    let bytes = std::fs::read(&path).unwrap_or_default();
    check!(
        b,
        "the default path is in the data root's exports folder, named for the document, view and size",
        path.contains("exports") && path.contains("Moveo-isometric-64x48-") && path.ends_with(".png"),
        path.clone()
    );
    check!(b, "the file is a PNG (signature)", bytes.starts_with(b"\x89PNG\r\n\x1a\n"), format!("{} bytes", bytes.len()));
    let (w, h) = (u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]));
    check!(
        b,
        "width/height in the reply are the PNG header's, and they are what was asked",
        (w, h) == (64, 48) && r["width"] == 64 && r["height"] == 48 && r["capped"] == false && r["bytes"] == bytes.len(),
        s(&r)
    );
    let restored = r["restoredSettings"].as_object().cloned().unwrap_or_default();
    check!(
        b,
        "every setting the render changed is listed as restored, with its read-back",
        ["displayMode", "perspective", "realView", "shadows", "ambientOcclusion", "backgroundColor", "orientation", "scale"]
            .iter()
            .all(|k| restored.get(*k).map(|v| v["restored"] == true).unwrap_or(false))
            && r["restoreIncomplete"] == false,
        s(&r["restoredSettings"])
    );
    check!(b, "THE USER'S VIEW IS EXACTLY AS IT WAS", display(&scratch) == before, format!("{} vs {}", display(&scratch), before));
    check!(b, "the temporary BMP is gone", !std::fs::read_dir(scratch.join("exports")).unwrap().flatten().any(|e| e.file_name().to_string_lossy().ends_with(".bmp")), String::new());
}

#[test]
fn a_failed_render_restores_every_setting_too() {
    let (b, scratch) = boot("render-fails");
    let before = display(&scratch);
    std::fs::write(scratch.join("mock-render-fails"), "").unwrap();
    let r = b.call("swx_render_image", hero(json!({})));
    check!(
        b,
        "SaveBMP failed: render_failed, with what was applied and what was restored",
        r["errorCode"] == "render_failed" && r["appliedSettings"]["realView"].is_object()
            && r["restoredSettings"]["realView"]["restored"] == true,
        s(&r)
    );
    check!(b, "...and the user's view is exactly as it was", display(&scratch) == before, display(&scratch).to_string());
}

#[test]
fn a_restore_that_did_not_take_is_said_loudly() {
    let (b, scratch) = boot("render-norestore");
    std::fs::write(scratch.join("mock-render-skips-restore"), "").unwrap();
    let r = b.call("swx_render_image", hero(json!({})));
    check!(
        b,
        "restoreIncomplete:true, the settings named, the hint says so",
        r["restoreIncomplete"] == true && r["restoreFailures"].as_array().map(|a| !a.is_empty()).unwrap_or(false)
            && r["_hint"].as_str().unwrap_or("").contains("NOT EVERY DISPLAY SETTING"),
        s(&r)
    );
}

#[test]
fn a_window_capped_export_says_so() {
    let (b, scratch) = boot("render-capped");
    std::fs::write(scratch.join("mock-render-capped"), "").unwrap();
    let r = b.call("swx_render_image", hero(json!({"width": 2000, "height": 1000})));
    check!(
        b,
        "capped:true, with the size actually in the file (read back), not the size asked",
        r["success"] == true && r["capped"] == true && r["width"] == 1568 && r["height"] == 882
            && r["requested"]["width"] == 2000,
        s(&r)
    );
}

#[test]
fn bad_destinations_and_options_are_refused_before_anything_changes() {
    let (b, scratch) = boot("render-refusals");
    let before = display(&scratch);
    let out = TempDir::new("render-out");
    let missing = format!("{}/no-such-folder/hero.png", out.path());
    let r = b.call("swx_render_image", hero(json!({"path": missing})));
    check!(b, "a folder that does not exist: output_dir_missing (never created)", r["errorCode"] == "output_dir_missing" && !out.join("no-such-folder").exists(), s(&r));
    let existing = out.join("taken.png");
    std::fs::write(&existing, "x").unwrap();
    let r = b.call("swx_render_image", hero(json!({"path": existing.to_string_lossy()})));
    check!(b, "an existing file: output_exists (never written over)", r["errorCode"] == "output_exists" && std::fs::read(&existing).unwrap() == b"x", s(&r));
    let r = b.call("swx_render_image", hero(json!({"background": "transparent"})));
    check!(b, "transparent: render_option_unsupported, said plainly", r["errorCode"] == "render_option_unsupported", s(&r));
    let r = b.call("swx_render_image", hero(json!({"path": "relative.png"})));
    check!(b, "a relative path: unsafe_path", r["errorCode"] == "unsafe_path", s(&r));
    let r = b.call("swx_render_image", hero(json!({"width": 100000})));
    check!(b, "an absurd size: bad_size", r["errorCode"] == "bad_size", s(&r));
    let r = b.call("swx_render_image", json!({"width": 64, "height": 48}));
    check!(b, "no reason: reason_required", r["errorCode"] == "reason_required", s(&r));
    check!(b, "...and NONE of those changed the user's view", display(&scratch) == before, display(&scratch).to_string());
    // A path that IS valid: written exactly there.
    let good = out.join("hero.png");
    let r = b.call("swx_render_image", hero(json!({"path": good.to_string_lossy()})));
    check!(b, "a valid path: written there", r["success"] == true && good.is_file(), s(&r));
}

#[test]
fn an_unreadable_enum_id_refuses_before_changing_anything() {
    let (b, scratch) = boot("render-enum");
    let before = display(&scratch);
    std::fs::write(scratch.join("mock-render-enum-unresolved"), "").unwrap();
    let r = b.call("swx_render_image", hero(json!({})));
    check!(b, "render_enum_unresolved — the bridge never guesses a swconst id", r["errorCode"] == "render_enum_unresolved", s(&r));
    check!(b, "...and nothing changed", display(&scratch) == before, display(&scratch).to_string());
}

#[test]
fn a_render_that_changes_the_screen_respects_the_user_at_the_keyboard() {
    let scratch = TempDir::new("render-presence");
    session(&scratch);
    std::fs::write(scratch.join("mock-display-state.json"), USERS_VIEW).unwrap();
    let machine = TempDir::new("render-presence-m");
    std::fs::write(machine.join("windows.json"), "[]").unwrap();
    std::fs::write(machine.join("last-input-secs"), "2").unwrap();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);
    b.call("swx_start", json!({}));
    let pid = b.call("swx_get_app_state", json!({}))["processId"].as_u64().unwrap();
    std::fs::write(machine.join("foreground.json"), json!({"hwnd": 4242, "pid": pid, "title": "SOLIDWORKS", "className": "Afx"}).to_string()).unwrap();
    let r = b.call("swx_render_image", hero(json!({})));
    check!(b, "a render that changes the view, the user at the keyboard: user_is_active", r["errorCode"] == "user_is_active", s(&r));
    let r = b.call("swx_render_image", json!({"width": 64, "height": 48, "reason": "the current view, as it is"}));
    check!(
        b,
        "an export of the CURRENT view changes nothing on screen: allowed, and nothing was applied",
        r["success"] == true && r["appliedSettings"].as_object().map(|m| m.is_empty()).unwrap_or(false),
        s(&r)
    );
}

/// EVERY REQUESTED SETTING IS ACCOUNTED FOR (0.9.19). Measured 2026-10-05:
/// `shadows:true` appeared in neither applied nor restored — already on, or not
/// applied? `settings` says which, for every setting asked for.
#[test]
fn every_requested_setting_says_what_happened_to_it() {
    let scratch = TempDir::new("render-settings");
    session(&scratch);
    // Shadows ALREADY on in the user's view.
    std::fs::write(scratch.join("mock-display-state.json"), USERS_VIEW.replace(r#""shadows":false"#, r#""shadows":true"#)).unwrap();
    // ...and ambient occlusion unreadable on this machine.
    std::fs::write(scratch.join("mock-render-unreadable-ambientOcclusion"), "").unwrap();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_render_image", hero(json!({"zoomFactor": 2.5})));
    let st = &r["settings"];
    let names = ["view", "displayStyle", "perspective", "realView", "ambientOcclusion", "shadows", "background", "zoomToFit", "zoomFactor"];
    check!(
        b,
        "every requested setting has a row {requested, before, after, changed, restored, why}",
        names.iter().all(|n| ["requested", "before", "after", "changed", "restored", "why"].iter().all(|k| st[*n].get(*k).is_some())),
        s(st)
    );
    check!(
        b,
        "shadows: already on — changed false, restored null, why alreadyAsRequested",
        st["shadows"]["changed"] == false && st["shadows"]["restored"].is_null() && st["shadows"]["why"] == "alreadyAsRequested"
            && st["shadows"]["before"] == true,
        s(&st["shadows"])
    );
    check!(
        b,
        "ambient occlusion: unreadable — said so, not silently absent",
        st["ambientOcclusion"]["why"] == "unreadable" && st["ambientOcclusion"]["changed"] == false,
        s(&st["ambientOcclusion"])
    );
    check!(
        b,
        "realView: applied and restored",
        st["realView"]["changed"] == true && st["realView"]["restored"] == true && st["realView"]["before"] == false && st["realView"]["after"] == true,
        s(&st["realView"])
    );
    check!(
        b,
        "zoomFactor: applied after the fit (scale 0.42 x 2.5) and restored",
        st["zoomFactor"]["changed"] == true && st["zoomFactor"]["restored"] == true && st["zoomFactor"]["after"] == json!(1.05),
        s(&st["zoomFactor"])
    );
    check!(b, "the swconst ids are reported for the record", r["enumIds"].is_object(), s(&r["enumIds"]));
    let bad = b.call("swx_render_image", hero(json!({"zoomFactor": 0})));
    check!(b, "a zoomFactor out of range: bad_zoom_factor", bad["errorCode"] == "bad_zoom_factor", s(&bad));
}

/// MEASURED 2026-10-05: RealView's put returned OK and the read-back was still
/// FALSE (uncertified GPU). A set SOLIDWORKS ignored is NOT "applied": why
/// setIgnored, changed false, restored null — and a `notes` line says so.
#[test]
fn a_setting_solidworks_ignores_is_not_reported_as_applied() {
    let scratch = TempDir::new("render-ignored");
    session(&scratch);
    std::fs::write(scratch.join("mock-render-ignores-realView"), "").unwrap();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_render_image", hero(json!({})));
    let rv = &r["settings"]["realView"];
    check!(b, "the render itself still succeeded", r["success"] == true, s(&r));
    check!(
        b,
        "realView: why setIgnored, changed false, restored null, before=after=false",
        rv["why"] == "setIgnored" && rv["changed"] == false && rv["restored"].is_null()
            && rv["before"] == false && rv["after"] == false,
        s(rv)
    );
    check!(
        b,
        "not in appliedSettings, not in restoredSettings",
        r["appliedSettings"].get("realView").is_none() && r["restoredSettings"].get("realView").is_none(),
        s(&r)
    );
    let notes = r["notes"].as_array().cloned().unwrap_or_default();
    check!(
        b,
        "a notes line says RealView was requested but left off, and why",
        notes.iter().any(|n| {
            let n = n.as_str().unwrap_or("");
            n.starts_with("RealView was requested but SOLIDWORKS left it off") && n.contains("certified graphics card")
                && n.contains("minimized")
        }),
        s(&r["notes"])
    );
    check!(
        b,
        "the other settings were applied as usual",
        r["settings"]["shadows"]["why"] == "applied" && r["settings"]["ambientOcclusion"]["why"] == "applied",
        s(&r["settings"])
    );
    std::fs::remove_file(scratch.join("mock-render-ignores-realView")).unwrap();
    let clean = b.call("swx_render_image", hero(json!({})));
    check!(b, "with nothing ignored, notes is empty", clean["notes"] == json!([]), s(&clean["notes"]));
}