swh-graph-stdlib 13.2.0

Library of algorithms and data structures for swh-graph
Documentation
// Copyright (C) 2024-2026  The Software Heritage developers
// See the AUTHORS file at the top-level directory of this distribution
// License: GNU General Public License version 3, or any later version
// See top-level LICENSE file for more information

use std::collections::HashMap;

use anyhow::Result;

use swh_graph::graph::*;
use swh_graph::graph_builder::GraphBuilder;
use swh_graph::labels::{LabelNameId, Permission};
use swh_graph::swhid;
use swh_graph::views::GraphSpy;
use swh_graph_stdlib::*;

use swh_graph_stdlib::fs::IdPath;

mod data;

#[test]
fn test_bitfield_id_path() {
    let ids = vec![LabelNameId(5), LabelNameId(3), LabelNameId(7)];

    let path = fs::BitFieldIdPath::from(ids.clone());
    assert_eq!(path.iter().collect::<Vec<_>>(), ids);

    let path = fs::BitFieldIdPath::from(ids.as_slice());
    assert_eq!(path.iter().collect::<Vec<_>>(), ids);

    let as_box: Box<[LabelNameId]> = path.into();
    assert_eq!(&*as_box, ids.as_slice());

    let path = fs::BitFieldIdPath::from(&[][..] as &[LabelNameId]);
    assert_eq!(path.iter().collect::<Vec<_>>(), vec![]);
}

#[test]
fn test_resolve_path() -> Result<()> {
    let graph = data::build_test_fs_tree_1()?;
    let props = graph.properties();
    let root_node = props.node_id(swhid!(swh:1:dir:0000000000000000000000000000000000000000))?;

    assert!(fs::resolve_name(&graph, root_node, "does-not-exist.txt").is_err());
    assert_eq!(
        props.swhid(fs::resolve_name(&graph, root_node, "README.md")?.unwrap()),
        swhid!(swh:1:cnt:0000000000000000000000000000000000000007)
    );
    assert_eq!(
        props.swhid(fs::resolve_name(&graph, root_node, "src")?.unwrap()),
        swhid!(swh:1:dir:0000000000000000000000000000000000000001)
    );

    assert!(fs::resolve_path(&graph, root_node, "does-not-exist.txt").is_err());
    assert!(fs::resolve_path(&graph, root_node, "README.md/is-not-a-dir").is_err());
    assert_eq!(
        props.swhid(fs::resolve_name(&graph, root_node, "doc")?.unwrap()),
        swhid!(swh:1:dir:0000000000000000000000000000000000000002)
    );
    assert_eq!(
        props.swhid(fs::resolve_path(&graph, root_node, "src/main.c")?.unwrap()),
        swhid!(swh:1:cnt:0000000000000000000000000000000000000004)
    );
    assert_eq!(
        props.swhid(fs::resolve_path(&graph, root_node, "doc/ls/ls.1")?.unwrap()),
        swhid!(swh:1:cnt:0000000000000000000000000000000000000006)
    );

    Ok(())
}

#[test]
fn test_resolve_path_by_id_with_bitfield() -> Result<()> {
    let graph = data::build_test_fs_tree_1()?;
    let props = graph.properties();
    let root_node = props.node_id(swhid!(swh:1:dir:0000000000000000000000000000000000000000))?;

    // Found: "src/main.c" → cnt:04
    let path = fs::BitFieldIdPath::from(vec![
        props.label_name_id(b"src")?,
        props.label_name_id(b"main.c")?,
    ]);
    assert_eq!(
        props.swhid(fs::resolve_path_by_id(&graph, root_node, path)?.unwrap()),
        swhid!(swh:1:cnt:0000000000000000000000000000000000000004)
    );

    // Not found: "main.c" is a known label but is not in the root dir
    let path = fs::BitFieldIdPath::from(vec![props.label_name_id(b"main.c")?]);
    assert_eq!(fs::resolve_path_by_id(&graph, root_node, path)?, None);

    Ok(())
}

#[test]
fn test_cached_path_resolver() -> Result<()> {
    let mut builder = GraphBuilder::default();
    let dir_a = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000010))?
        .done();
    let dir_b = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000011))?
        .done();
    let dir_shared = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000012))?
        .done();
    let dir_inner = builder
        .node(swhid!(swh:1:dir:0000000000000000000000000000000000000013))?
        .done();
    let cnt1 = builder
        .node(swhid!(swh:1:cnt:0000000000000000000000000000000000000014))?
        .done();
    let cnt2 = builder
        .node(swhid!(swh:1:cnt:0000000000000000000000000000000000000015))?
        .done();
    builder.dir_arc(dir_a, dir_shared, Permission::Directory, "src");
    builder.dir_arc(dir_b, dir_shared, Permission::Directory, "src");
    builder.dir_arc(dir_shared, cnt1, Permission::Content, "README.txt");
    builder.dir_arc(dir_shared, dir_inner, Permission::Directory, "src");
    builder.dir_arc(dir_inner, cnt2, Permission::Content, "README.txt");
    let graph = builder.done()?;
    let path = vec![
        graph.properties().label_name_id(b"src")?,
        graph.properties().label_name_id(b"README.txt")?,
    ];

    let spy = GraphSpy::new(graph);
    let history = spy.history.clone();
    let walks = || {
        history
            .lock()
            .unwrap()
            .drain(..)
            .filter(|(m, _)| *m == "untyped_labeled_successors")
            .map(|(_, args)| args.clone())
            .collect::<Vec<_>>()
    };
    let mut resolver = fs::CachedPathResolver::new(&spy, path);

    assert_eq!(resolver.resolve_from(dir_a)?, Some(cnt1));
    assert_eq!(walks(), vec!["(0,)".to_string(), "(2,)".to_string()]); // dir_a and dir_shared

    // uses the cache
    assert_eq!(resolver.resolve_from(dir_a)?, Some(cnt1));
    assert_eq!(walks(), Vec::<String>::new());

    assert_eq!(resolver.resolve_from(dir_b)?, Some(cnt1));
    assert_eq!(walks(), vec!["(1,)".to_string()]); // dir_b

    // doesn't reuse dir_shared cache
    assert_eq!(resolver.resolve_from(dir_shared)?, Some(cnt2));
    assert_eq!(walks(), vec!["(2,)".to_string(), "(3,)".to_string()]); // dir_shared and dir_inner
    Ok(())
}

#[test]
fn test_fs_ls_tree() -> Result<()> {
    let graph = data::build_test_fs_tree_1()?;
    let props = graph.properties();
    let doc_dir = props.node_id("swh:1:dir:0000000000000000000000000000000000000002")?;

    use swh_graph::labels::Permission;
    use swh_graph_stdlib::fs::FsTree::*;
    assert_eq!(
        fs::ls_tree(&graph, doc_dir)?,
        Directory(HashMap::from([(
            Vec::from("ls"),
            (
                Directory(HashMap::from([(
                    Vec::from("ls.1"),
                    (Content, Some(Permission::Content))
                )])),
                Some(Permission::Directory)
            )
        )]))
    );
    Ok(())
}