kcl-lib 0.2.186

KittyCAD Language implementation and tools
Documentation
use std::path::Path;
use std::path::PathBuf;

use serde::Deserialize;

use crate::ExecOutcome;
use crate::ExecutorContext;
use crate::ExecutorSettings;
use crate::Program;
use crate::TypedPath;
use crate::execution::MockConfig;

#[tokio::test(flavor = "multi_thread")]
async fn exec_outcome_renders_sketch_png_separately_from_constraint_report() {
    let outcome = execute_visualizer_kcl(&sketch_visualizer_test_root().join("connected_profile/input.kcl")).await;
    let report = outcome.sketch_constraint_report();
    let statuses = report
        .fully_constrained
        .iter()
        .chain(&report.under_constrained)
        .chain(&report.over_constrained)
        .chain(&report.errors)
        .collect::<Vec<_>>();

    assert_eq!(statuses.len(), 1);
    let png = outcome
        .render_sketch_png(&statuses[0].name)
        .expect("the sketch should render from the same execution outcome");
    assert!(png.starts_with(b"\x89PNG\r\n\x1a\n"));
    assert_eq!(
        png,
        outcome.render_sketch_png_instance(&statuses[0].name, Some(0)).unwrap()
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn duplicate_names_require_an_explicit_instance() {
    use crate::tooling::sketch_visualizer::SketchVisualizationError;

    let path = sketch_visualizer_test_root().join("duplicate_names/input.kcl");
    let mut previous_images = None;
    for _ in 0..2 {
        let outcome = execute_visualizer_kcl(&path).await;
        let report = outcome.sketch_constraint_report();
        let indices = report
            .fully_constrained
            .iter()
            .map(|s| s.instance_index)
            .collect::<Vec<_>>();
        assert_eq!(indices, [0, 1]);
        assert!(matches!(
            outcome.render_sketch_png("profile"),
            Err(SketchVisualizationError::AmbiguousSketchName { count: 2, .. })
        ));
        assert!(matches!(
            outcome.render_sketch_png_instance("profile", Some(2)),
            Err(SketchVisualizationError::InstanceNotFound { index: 2, count: 2, .. })
        ));
        assert!(matches!(
            outcome.render_sketch_png_instance("missing", Some(0)),
            Err(SketchVisualizationError::SketchNotFound { .. })
        ));
        let images = [0, 1].map(|index| outcome.render_sketch_png_instance("profile", Some(index)).unwrap());
        assert_ne!(images[0], images[1]);
        if let Some(previous) = previous_images {
            assert_eq!(images, previous, "instance selection must survive a fresh execution");
        }
        previous_images = Some(images);
    }
}

#[tokio::test(flavor = "multi_thread")]
async fn instance_indices_are_assigned_before_status_grouping() {
    let outcome = execute_visualizer_kcl(&sketch_visualizer_test_root().join("duplicate_names/statuses.kcl")).await;
    let report = outcome.sketch_constraint_report();
    assert_eq!(report.under_constrained[0].name, "profile");
    assert_eq!(report.under_constrained[0].instance_index, 0);
    assert_eq!(report.fully_constrained[0].name, "profile");
    assert_eq!(report.fully_constrained[0].instance_index, 1);
}

#[tokio::test(flavor = "multi_thread")]
async fn duplicate_names_from_imported_functions_are_selectable() {
    let path = sketch_visualizer_test_root().join("duplicate_names/imports.kcl");
    let mut previous_images = None;
    for _ in 0..2 {
        let outcome = execute_visualizer_kcl(&path).await;
        let report = outcome.sketch_constraint_report();
        assert_eq!(report.fully_constrained.len(), 2);
        assert_eq!(report.fully_constrained[0].instance_index, 0);
        assert_eq!(report.fully_constrained[1].instance_index, 1);
        let images = [0, 1].map(|index| outcome.render_sketch_png_instance("profile", Some(index)).unwrap());
        assert_ne!(images[0], images[1]);
        if let Some(previous) = previous_images {
            assert_eq!(images, previous);
        }
        previous_images = Some(images);
    }
}

#[tokio::test(flavor = "multi_thread")]
async fn snapshots_kcl_visualizer_pngs() {
    let manifest = sketch_visualizer_snapshot_manifest();

    for case in manifest.cases {
        let input_path = sketch_visualizer_test_root().join(&case.input);
        let outcome = execute_visualizer_kcl(&input_path).await;
        let png = outcome
            .render_sketch_png(&case.sketch)
            .unwrap_or_else(|err| panic!("failed to visualize sketch for case `{}`: {err:?}", case.name));
        assert_png_snapshot(&case.name, &png);
    }
}

fn assert_png_snapshot(case_name: &str, png: &[u8]) {
    let output_dir = sketch_visualizer_test_root().join(case_name);
    let image = image::load_from_memory(png).unwrap();
    twenty_twenty::assert_image(output_dir.join("dof.png"), &image, 1.0);
}

async fn execute_visualizer_kcl(input_path: &Path) -> ExecOutcome {
    let source = std::fs::read_to_string(input_path)
        .unwrap_or_else(|err| panic!("failed to read `{}`: {err}", input_path.display()));
    let program = Program::parse_no_errs(&source)
        .unwrap_or_else(|err| panic!("failed to parse `{}`: {err:?}", input_path.display()));
    let mut settings = ExecutorSettings::default();
    settings.with_current_file(TypedPath(input_path.to_path_buf()));
    settings.project_directory = input_path.parent().map(|path| TypedPath(path.to_path_buf()));
    let ctx = ExecutorContext::new_mock(Some(settings)).await;
    let outcome = ctx
        .run_mock(
            &program,
            &MockConfig {
                use_prev_memory: false,
                ..Default::default()
            },
        )
        .await
        .unwrap_or_else(|err| panic!("failed to execute `{}`: {err:?}", input_path.display()));
    ctx.close().await;
    outcome
}

fn sketch_visualizer_snapshot_manifest() -> SketchVisualizerSnapshotManifest {
    let manifest_path = sketch_visualizer_test_root().join("manifest.toml");
    let contents = std::fs::read_to_string(&manifest_path)
        .unwrap_or_else(|err| panic!("failed to read `{}`: {err}", manifest_path.display()));
    toml::from_str(&contents).unwrap_or_else(|err| panic!("failed to parse `{}`: {err}", manifest_path.display()))
}

fn sketch_visualizer_test_root() -> PathBuf {
    Path::new("tests").join("sketch_visualizer")
}

#[derive(Debug, Deserialize)]
struct SketchVisualizerSnapshotManifest {
    cases: Vec<SketchVisualizerSnapshotCase>,
}

#[derive(Debug, Deserialize)]
struct SketchVisualizerSnapshotCase {
    name: String,
    input: PathBuf,
    sketch: String,
}