link-cli 0.2.6

A CLI tool and reusable library for links manipulation backed by a LiNo-notation doublet storage engine.
Documentation
//! C# AdvancedMixedQueryProcessor parity tests.

use anyhow::Result;
use link_cli::{Link, NamedTypes, NamedTypesDecorator, QueryProcessor};
use tempfile::NamedTempFile;

fn with_storage(
    test: impl FnOnce(&mut NamedTypesDecorator, &QueryProcessor) -> Result<()>,
) -> Result<()> {
    let temp_file = NamedTempFile::new()?;
    let names_file = NamedTempFile::new()?;
    let db_path = temp_file.path().to_str().unwrap();
    let names_path = names_file.path().to_str().unwrap();
    let mut storage = NamedTypesDecorator::with_names_database_path(db_path, names_path, false)?;
    let processor = QueryProcessor::new(false).with_auto_create_missing_references(true);
    test(&mut storage, &processor)
}

fn sorted_links(storage: &NamedTypesDecorator) -> Vec<Link> {
    let mut links: Vec<Link> = storage.all().into_iter().copied().collect();
    links.sort_by_key(|link| link.index);
    links
}

fn assert_link_exists(storage: &NamedTypesDecorator, index: u32, source: u32, target: u32) {
    let link = storage
        .get(index)
        .unwrap_or_else(|| panic!("missing link {index}: {source} {target}"));
    assert_eq!(*link, Link::new(index, source, target));
}

fn name_id(storage: &mut NamedTypesDecorator, name: &str) -> Result<u32> {
    Ok(storage
        .get_by_name(name)?
        .unwrap_or_else(|| panic!("{name} should exist")))
}

#[test]
fn test_unwrapped_create_query_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "() ((1: 1 1))")?;

        assert_eq!(storage.all().len(), 1);
        assert_link_exists(storage, 1, 1, 1);
        Ok(())
    })
}

#[test]
fn test_create_explicit_index_after_gap_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((3: 3 3)))")?;

        assert_eq!(storage.all().len(), 1);
        assert_link_exists(storage, 3, 3, 3);
        Ok(())
    })
}

#[test]
fn test_create_deep_nested_numeric_links_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() (((1 1) ((2 2) ((3 3) ((4 4) (5 5)))))))")?;

        assert_eq!(storage.all().len(), 9);
        assert_link_exists(storage, 1, 1, 1);
        assert_link_exists(storage, 2, 2, 2);
        assert_link_exists(storage, 3, 3, 3);
        assert_link_exists(storage, 4, 4, 4);
        assert_link_exists(storage, 5, 5, 5);
        assert_link_exists(storage, 6, 4, 5);
        assert_link_exists(storage, 7, 3, 6);
        assert_link_exists(storage, 8, 2, 7);
        assert_link_exists(storage, 9, 1, 8);
        Ok(())
    })
}

#[test]
fn test_delete_by_source_target_pattern_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((1: 1 1) (2: 2 2) (3: 1 2)))")?;

        processor.process_query(storage, "(((1 2)) ())")?;

        assert_eq!(storage.all().len(), 2);
        assert_link_exists(storage, 1, 1, 1);
        assert_link_exists(storage, 2, 2, 2);
        Ok(())
    })
}

#[test]
fn test_delete_by_wildcard_target_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((1 2) (2 2)))")?;

        processor.process_query(storage, "(((1 *)) ())")?;

        assert_eq!(storage.all().len(), 1);
        assert_link_exists(storage, 2, 2, 2);
        Ok(())
    })
}

#[test]
fn test_delete_all_by_index_wildcard_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((1 2) (2 2)))")?;

        processor.process_query(storage, "(((*:)) ())")?;

        assert!(storage.all().is_empty());
        Ok(())
    })
}

#[test]
fn test_swap_all_links_using_variables_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((1 2) (2 1)))")?;

        processor.process_query(
            storage,
            "((($index: $source $target)) (($index: $target $source)))",
        )?;

        assert_eq!(storage.all().len(), 2);
        assert_link_exists(storage, 1, 2, 1);
        assert_link_exists(storage, 2, 1, 2);
        Ok(())
    })
}

#[test]
fn test_no_op_variable_query_returns_matched_changes() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((1 1)))")?;
        processor.process_query(storage, "(() ((2 2)))")?;

        let changes = processor.process_query(
            storage,
            "((($index: $source $target)) (($index: $source $target)))",
        )?;

        assert_eq!(
            sorted_links(storage),
            vec![Link::new(1, 1, 1), Link::new(2, 2, 2)]
        );
        assert_eq!(changes.len(), 2);
        assert!(changes.contains(&(Some(Link::new(1, 1, 1)), Some(Link::new(1, 1, 1)))));
        assert!(changes.contains(&(Some(Link::new(2, 2, 2)), Some(Link::new(2, 2, 2)))));
        Ok(())
    })
}

#[test]
fn test_named_link_rename_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((child: father mother)))")?;

        processor.process_query(storage, "(((child: father mother)) ((son: father mother)))")?;

        assert_eq!(storage.get_by_name("child")?, None);
        let son_id = name_id(storage, "son")?;
        let father_id = name_id(storage, "father")?;
        let mother_id = name_id(storage, "mother")?;
        assert_link_exists(storage, son_id, father_id, mother_id);
        assert_eq!(storage.all().len(), 3);
        Ok(())
    })
}

#[test]
fn test_delete_by_names_keeps_leaf_names_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((child: father mother)))")?;

        processor.process_query(storage, "(((child: father mother)) ())")?;

        assert_eq!(storage.get_by_name("child")?, None);
        assert!(storage.get_by_name("father")?.is_some());
        assert!(storage.get_by_name("mother")?.is_some());
        assert_eq!(storage.all().len(), 2);
        Ok(())
    })
}

#[test]
fn test_unknown_named_restriction_fails_without_auto_create() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((known: left right)))")?;

        let strict_processor = QueryProcessor::new(false);
        let error = strict_processor
            .process_query(storage, "(((unknown: left right)) ())")
            .expect_err("unknown named restriction should fail validation");

        assert!(error.to_string().contains("unknown"));
        assert!(error
            .to_string()
            .contains("--auto-create-missing-references"));
        assert!(storage.get_by_name("known")?.is_some());
        assert!(storage.get_by_name("unknown")?.is_none());
        Ok(())
    })
}

#[test]
fn test_string_composite_left_child_does_not_create_extra_leaf() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((type: type type)))")?;
        processor.process_query(storage, "(() ((link: link type)))")?;

        let type_id = name_id(storage, "type")?;
        let link_id = name_id(storage, "link")?;
        assert_eq!(storage.all().len(), 2);
        assert_link_exists(storage, type_id, type_id, type_id);
        assert_link_exists(storage, link_id, link_id, type_id);
        Ok(())
    })
}

#[test]
fn test_string_aliases_in_variable_restriction_constrain_matches_to_named_links_matches_csharp(
) -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((father: father father)))")?;
        processor.process_query(storage, "(() ((mother: mother mother)))")?;
        processor.process_query(storage, "(() ((child: father mother)))")?;

        let father_id = name_id(storage, "father")?;
        let mother_id = name_id(storage, "mother")?;
        let child_id = name_id(storage, "child")?;

        processor.process_query(storage, "((($id: father mother)) (($id: mother father)))")?;

        assert_eq!(storage.all().len(), 3);
        assert_link_exists(storage, father_id, father_id, father_id);
        assert_link_exists(storage, mother_id, mother_id, mother_id);
        assert_link_exists(storage, child_id, mother_id, father_id);
        Ok(())
    })
}

#[test]
fn test_issue_20_substitute_matched_link_and_outgoing_link_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(
            storage,
            "(() ((1: 1 1) (18: 1 21) (19: 1 20) (20: 20 20) (21: 21 21)))",
        )?;

        processor.process_query(storage, "((($i: 1 21)) (($i: $s $t) ($i 20)))")?;

        let links = sorted_links(storage);
        assert_eq!(links.len(), 6);
        assert_link_exists(storage, 1, 1, 1);
        assert_link_exists(storage, 18, 1, 21);
        assert_link_exists(storage, 19, 1, 20);
        assert_link_exists(storage, 20, 20, 20);
        assert_link_exists(storage, 21, 21, 21);

        let outgoing_links = links
            .iter()
            .filter(|link| link.source == 18 && link.target == 20)
            .collect::<Vec<_>>();
        assert_eq!(outgoing_links.len(), 1);
        assert_ne!(outgoing_links[0].index, 0);
        assert_ne!(outgoing_links[0].index, u32::MAX);
        assert!(links.iter().all(|link| {
            link.index != u32::MAX && link.source != u32::MAX && link.target != u32::MAX
        }));
        Ok(())
    })
}

#[test]
fn test_issue_20_substitute_full_point_with_unbound_parts_matches_csharp() -> Result<()> {
    with_storage(|storage, processor| {
        processor.process_query(storage, "(() ((21: 21 21)))")?;

        processor.process_query(storage, "(((21: 21 21)) ((21: $s $t)))")?;

        assert_eq!(storage.all().len(), 1);
        assert_link_exists(storage, 21, 21, 21);
        assert!(storage
            .all()
            .iter()
            .all(|link| link.source != u32::MAX && link.target != u32::MAX));
        Ok(())
    })
}