cadmpeg 0.6.2

Convert and inspect native CAD files.
// SPDX-License-Identifier: Apache-2.0
//! `cadmpeg query graph` integration tests.

#![allow(clippy::unwrap_used)]

use predicates::prelude::{predicate, PredicateBooleanExt};
use tempfile::tempdir;

mod query_support;
mod support;

use crate::query_support::write;
use crate::support::cadmpeg;

const CHECK_REPORT: &str = r#"{
  "ir_version": "6",
  "command": "check",
  "status": "ok",
  "refusal": null
}"#;

const SIDECAR: &str = r#"{
  "ir_version": "6",
  "ir_sha256": "abc123"
}"#;

const BREP_DOC: &str = r#"{
  "ir_version": "6",
  "model": {
    "bodies": [{"id": "body#1", "regions": ["region#1"]}],
    "regions": [{"id": "region#1", "shells": ["shell#1"]}],
    "shells": [{"id": "shell#1", "faces": ["face#1", "face#2"]}],
    "faces": [{"id": "face#1"}, {"id": "face#2"}]
  }
}"#;

const GRAPH_DOC: &str = r#"{
  "ir_version": "6",
  "model": {
    "features": [
      {
        "id": "f1",
        "links": ["n1"],
        "native_ref": "n2",
        "definition": {"parameters": {"segment_0_object": "n1"}}
      }
    ]
  },
  "native": {
    "rhino": {
      "unknowns": [
        {"id": "n1", "kind": "curve"},
        {"id": "n2", "kind": "other"}
      ]
    }
  }
}"#;

#[test]
fn graph_input_over_256_mib_is_refused_before_json_parse() {
    let dir = tempdir().unwrap();
    let path = dir.path().join("oversized.json");
    std::fs::File::create(&path)
        .unwrap()
        .set_len(256 * 1024 * 1024 + 1)
        .unwrap();
    cadmpeg()
        .args(["query", "graph", path.to_str().unwrap(), "model.faces"])
        .assert()
        .code(2)
        .stderr(predicate::str::contains(
            "exceeds the query input limit of 256 MiB",
        ));
}

#[test]
fn graph_help_mentions_hops_follow_and_reverse() {
    cadmpeg()
        .args(["query", "graph", "--help"])
        .assert()
        .success()
        .stdout(
            predicate::str::contains("--hops")
                .and(predicate::str::contains("--follow"))
                .and(predicate::str::contains("--reverse"))
                .and(predicate::str::contains("--max-paths"))
                .and(predicate::str::contains("start,path,record.id")),
        );
}

#[test]
fn graph_unknown_arena_teaches() {
    let dir = tempdir().unwrap();
    let doc = write(dir.path(), "doc.json", GRAPH_DOC);
    let missing = cadmpeg()
        .args(["query", "graph", doc.to_str().unwrap(), "model.bogus", "f1"])
        .output()
        .unwrap();
    assert_eq!(missing.status.code(), Some(2));
    let stderr = String::from_utf8_lossy(&missing.stderr);
    assert!(stderr.contains("unknown arena model.bogus"), "{stderr}");
    assert!(stderr.contains("addressable arenas"), "{stderr}");
    assert!(stderr.contains("query counts"), "{stderr}");
}

#[test]
fn graph_rejects_report_and_sidecar() {
    let dir = tempdir().unwrap();
    let report = write(dir.path(), "report.json", CHECK_REPORT);
    cadmpeg()
        .args([
            "query",
            "graph",
            report.to_str().unwrap(),
            "model.faces",
            "f1",
        ])
        .assert()
        .code(2)
        .stderr(
            predicate::str::contains("command report")
                .and(predicate::str::contains("query findings"))
                .and(predicate::str::contains("dump SOURCE")),
        );

    let sidecar = write(dir.path(), "model.fidelity.json", SIDECAR);
    cadmpeg()
        .args([
            "query",
            "graph",
            sidecar.to_str().unwrap(),
            "model.faces",
            "f1",
        ])
        .assert()
        .code(2)
        .stderr(
            predicate::str::contains("fidelity.json").and(predicate::str::contains("dump SOURCE")),
        );
}

#[test]
fn graph_json_envelope() {
    let dir = tempdir().unwrap();
    let doc = write(dir.path(), "doc.json", GRAPH_DOC);
    let output = cadmpeg()
        .args([
            "query",
            "graph",
            "--json",
            doc.to_str().unwrap(),
            "model.features",
            "f1",
            "--hops",
            "1",
            "--follow",
            "links",
        ])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(value["command"], "query");
    let graph = value["payload"].as_array().unwrap();
    assert_eq!(graph.len(), 2);
    assert_eq!(graph[0]["start"], "model.features#f1");
    assert!(graph[0]["path"].as_array().unwrap().is_empty());
    assert_eq!(graph[1]["record"]["id"], "n1");
    assert_eq!(graph[1]["path"][0]["field"], "links");
}

#[test]
fn graph_brep_hops_3_reaches_faces() {
    let dir = tempdir().unwrap();
    let doc = write(dir.path(), "brep.json", BREP_DOC);
    let output = cadmpeg()
        .args([
            "query",
            "graph",
            "--json",
            doc.to_str().unwrap(),
            "model.bodies",
            "body#1",
            "--hops",
            "3",
        ])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    let ids: Vec<&str> = value["payload"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|row| row["record"]["id"].as_str())
        .collect();
    assert!(ids.contains(&"body#1"), "{ids:?}");
    assert!(ids.contains(&"region#1"), "{ids:?}");
    assert!(ids.contains(&"shell#1"), "{ids:?}");
    assert!(ids.contains(&"face#1"), "{ids:?}");
    assert!(ids.contains(&"face#2"), "{ids:?}");
}

#[test]
fn graph_missing_id_teaches() {
    let dir = tempdir().unwrap();
    let doc = write(dir.path(), "doc.json", GRAPH_DOC);
    let missing = cadmpeg()
        .args(["query", "graph", doc.to_str().unwrap(), "features", "nope"])
        .output()
        .unwrap();
    assert_eq!(missing.status.code(), Some(2));
    let stderr = String::from_utf8_lossy(&missing.stderr);
    assert!(stderr.contains("no record in model.features"), "{stderr}");
    assert!(stderr.contains("f1"), "{stderr}");
}