shifty-cli 0.2.5

CLI for the formalism-first SHACL engine: inspect, validate, infer
use std::process::Command;

#[test]
fn version_subcommand_prints_package_version() {
    let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .arg("version")
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(
        String::from_utf8(output.stdout).unwrap().trim(),
        env!("CARGO_PKG_VERSION")
    );
}

#[test]
fn validation_executes_over_data_and_shapes_union() {
    let dir = std::env::temp_dir().join(format!("shifty-cli-union-class-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let shapes = dir.join("shapes.ttl");
    let data = dir.join("data.ttl");

    std::fs::write(
        &shapes,
        r#"
            @prefix ex: <http://ex/> .
            @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
            @prefix sh: <http://www.w3.org/ns/shacl#> .

            ex:Parent a rdfs:Class, sh:NodeShape ;
                sh:property [ sh:path ex:forbidden ; sh:maxCount 0 ] .
            ex:Child rdfs:subClassOf ex:Parent .
            ex:shapeItem a ex:Child ; ex:forbidden ex:value .
        "#,
    )
    .unwrap();
    std::fs::write(
        &data,
        r#"
            @prefix ex: <http://ex/> .
            ex:item a ex:Child ; ex:forbidden ex:value .
        "#,
    )
    .unwrap();

    let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
        ])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("conforms: false"), "stdout: {stdout}");
    assert!(stdout.contains("<http://ex/item>"), "stdout: {stdout}");
    assert!(
        !stdout.contains("<http://ex/shapeItem>"),
        "stdout: {stdout}"
    );

    let data_only = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
            "--graph-mode",
            "data",
        ])
        .output()
        .unwrap();
    let data_stdout = String::from_utf8(data_only.stdout).unwrap();
    assert!(
        data_stdout.contains("conforms: true"),
        "stdout: {data_stdout}"
    );

    let union_all = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
            "--graph-mode",
            "union-all",
        ])
        .output()
        .unwrap();
    let union_all_stdout = String::from_utf8(union_all.stdout).unwrap();
    assert!(
        union_all_stdout.contains("<http://ex/item>"),
        "stdout: {union_all_stdout}"
    );
    assert!(
        union_all_stdout.contains("<http://ex/shapeItem>"),
        "stdout: {union_all_stdout}"
    );

    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn sparql_constraints_see_the_shapes_graph() {
    let dir = std::env::temp_dir().join(format!("shifty-cli-union-sparql-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let shapes = dir.join("shapes.ttl");
    let data = dir.join("data.ttl");

    std::fs::write(
        &shapes,
        r#"
            @prefix ex: <http://ex/> .
            @prefix sh: <http://www.w3.org/ns/shacl#> .

            ex:enabled ex:value true .
            ex:S a sh:NodeShape ;
                sh:targetNode ex:item ;
                sh:sparql [
                    sh:select "SELECT $this WHERE { ex:enabled ex:value true }"
                ] .
        "#,
    )
    .unwrap();
    std::fs::write(&data, "@prefix ex: <http://ex/> . ex:item ex:value 1 .").unwrap();

    let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
        ])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("conforms: false"), "stdout: {stdout}");
    assert!(
        stdout.contains("SPARQL constraint at") && stdout.contains("not satisfied"),
        "stdout: {stdout}"
    );

    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn inference_executes_over_data_and_shapes_union() {
    let dir = std::env::temp_dir().join(format!("shifty-cli-infer-union-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let shapes = dir.join("shapes.ttl");
    let data = dir.join("data.ttl");

    std::fs::write(
        &shapes,
        r#"
            @prefix ex: <http://ex/> .
            @prefix sh: <http://www.w3.org/ns/shacl#> .

            ex:S a sh:NodeShape ;
                sh:targetClass ex:Thing ;
                sh:rule [
                    a sh:SPARQLRule ;
                    sh:construct """
                        CONSTRUCT { ?object ?inverse $this }
                        WHERE {
                            $this ?predicate ?object .
                            ?predicate ex:inverseOf ?inverse .
                        }
                    """
                ] .
            ex:p ex:inverseOf ex:q .
        "#,
    )
    .unwrap();
    std::fs::write(
        &data,
        r#"
            @prefix ex: <http://ex/> .
            ex:a a ex:Thing ; ex:p ex:b .
        "#,
    )
    .unwrap();

    let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "infer",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
        ])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("inferred 1 triple(s)"), "stdout: {stdout}");
    assert!(
        stdout.contains("<http://ex/b> <http://ex/q> <http://ex/a>"),
        "stdout: {stdout}"
    );

    std::fs::remove_dir_all(dir).unwrap();
}

#[test]
fn validation_runs_inference_first() {
    let dir =
        std::env::temp_dir().join(format!("shifty-cli-validate-infer-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let shapes = dir.join("shapes.ttl");
    let data = dir.join("data.ttl");

    std::fs::write(
        &shapes,
        r#"
            @prefix ex: <http://ex/> .
            @prefix sh: <http://www.w3.org/ns/shacl#> .

            ex:S a sh:NodeShape ;
                sh:targetNode ex:item ;
                sh:property [ sh:path ex:derived ; sh:maxCount 0 ] ;
                sh:rule [
                    a sh:TripleRule ;
                    sh:subject sh:this ;
                    sh:predicate ex:derived ;
                    sh:object ex:value
                ] .
        "#,
    )
    .unwrap();
    std::fs::write(
        &data,
        "@prefix ex: <http://ex/> . ex:item ex:input ex:value .",
    )
    .unwrap();

    let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
        ])
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("conforms: false"), "stdout: {stdout}");
    assert!(stdout.contains("at most 0 value(s)"), "stdout: {stdout}");

    let no_infer = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
            "--no-infer",
        ])
        .output()
        .unwrap();
    assert!(
        no_infer.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&no_infer.stderr)
    );
    let no_infer_stdout = String::from_utf8(no_infer.stdout).unwrap();
    assert!(
        no_infer_stdout.contains("conforms: true"),
        "stdout: {no_infer_stdout}"
    );

    let embedded_no_infer = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--no-infer",
        ])
        .output()
        .unwrap();
    assert!(
        embedded_no_infer.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&embedded_no_infer.stderr)
    );
    let embedded_no_infer_stdout = String::from_utf8(embedded_no_infer.stdout).unwrap();
    assert!(
        embedded_no_infer_stdout.contains("conforms: true"),
        "stdout: {embedded_no_infer_stdout}"
    );

    let embedded_report_no_infer = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--no-infer",
            "--report",
        ])
        .output()
        .unwrap();
    assert!(
        embedded_report_no_infer.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&embedded_report_no_infer.stderr)
    );
    let embedded_report_no_infer_stdout =
        String::from_utf8(embedded_report_no_infer.stdout).unwrap();
    assert!(
        embedded_report_no_infer_stdout.contains("sh:conforms true"),
        "stdout: {embedded_report_no_infer_stdout}"
    );

    let report = Command::new(env!("CARGO_BIN_EXE_shifty"))
        .args([
            "validate",
            "--shapes",
            shapes.to_str().unwrap(),
            "--data",
            data.to_str().unwrap(),
            "--report",
        ])
        .output()
        .unwrap();
    assert!(
        report.status.success(),
        "stderr: {}",
        String::from_utf8_lossy(&report.stderr)
    );
    let report_stdout = String::from_utf8(report.stdout).unwrap();
    assert!(
        report_stdout.contains("sh:conforms false"),
        "stdout: {report_stdout}"
    );
    assert!(
        report_stdout.contains("sh:sourceConstraintComponent sh:MaxCountConstraintComponent"),
        "stdout: {report_stdout}"
    );

    std::fs::remove_dir_all(dir).unwrap();
}