Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ public async Task ExportAlias_WritesNamedReferences()
}
}

[Fact]
public async Task StructureOption_RendersLeftBranchWithIndexes()
{
var tempDirectory = CreateTempDirectory();

try
{
var dbPath = Path.Combine(tempDirectory, "structure.links");

AssertClinkSucceeded(await RunClinkAsync("--db", dbPath, "() ((1: 1 1))"));
AssertClinkSucceeded(await RunClinkAsync("--db", dbPath, "() ((2: 1 2))"));
AssertClinkSucceeded(await RunClinkAsync("--db", dbPath, "() ((3: 2 1))"));
AssertClinkSucceeded(await RunClinkAsync("--db", dbPath, "() ((4: 3 2))"));

var result = await RunClinkAsync("--db", dbPath, "--structure", "4");

AssertClinkSucceeded(result);
Assert.Equal("(4: (3: (2: (1: 1 1) 2) 1) 2)\n", NormalizeNewlines(result.Stdout));
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}

private static async Task<CommandResult> RunClinkAsync(params string[] clinkArguments)
{
var csharpDirectory = FindCsharpDirectory();
Expand Down Expand Up @@ -104,6 +129,11 @@ private static void AssertClinkSucceeded(CommandResult result)
$"clink exited with {result.ExitCode}\nstdout:\n{result.Stdout}\nstderr:\n{result.Stderr}");
}

private static string NormalizeNewlines(string text)
{
return text.Replace("\r\n", "\n");
}

private static string FindCsharpDirectory()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,38 @@ public void WriteToFile_WritesCompleteDatabaseAsLinoLines()
});
}

[Fact]
public void FormatStructure_RendersLeftBranchWithLinkIndexes()
{
WithNamedLinks(links =>
{
var first = links.GetOrCreate(0u, 0u);
var second = links.GetOrCreate(first, first);
var third = links.GetOrCreate(second, first);
var fourth = links.GetOrCreate(third, second);

var formatted = LinoDatabaseOutput.FormatStructure(links, fourth);

Assert.Equal("(4: (3: (2: (1: 0 0) 1) 1) 2)", formatted);
});
}

[Fact]
public void FormatStructure_RendersRepeatedSourceAndTargetAsReferenceOnRight()
{
WithNamedLinks(links =>
{
var first = links.GetOrCreate(0u, 0u);
var second = links.GetOrCreate(first, first);
links.GetOrCreate(second, first);
var fourth = links.GetOrCreate(second, second);

var formatted = LinoDatabaseOutput.FormatStructure(links, fourth);

Assert.Equal("(4: (2: (1: 0 0) 1) 2)", formatted);
});
}

private static void WithNamedLinks(Action<NamedTypesDecorator<uint>> test)
{
var dbPath = Path.GetTempFileName();
Expand Down
39 changes: 39 additions & 0 deletions csharp/Foundation.Data.Doublets.Cli/LinoDatabaseOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public static string FormatChange(INamedTypesLinks<uint> links, DoubletLink link
return $"({beforeText}) ({afterText})";
}

public static string FormatStructure(INamedTypesLinks<uint> links, uint linkId)
{
var visited = new HashSet<uint>();
return FormatStructure(links, linkId, visited);
}

public static string Namify(INamedTypesLinks<uint> namedLinks, string linksNotation)
{
return NumberTokenRegex.Replace(linksNotation, match =>
Expand All @@ -59,6 +65,39 @@ public static string Namify(INamedTypesLinks<uint> namedLinks, string linksNotat
});
}

private static string FormatStructure(INamedTypesLinks<uint> links, uint linkId, HashSet<uint> visited)
{
if (!links.Exists(linkId))
{
throw new InvalidOperationException($"Link '{linkId}' does not exist.");
}

if (!visited.Add(linkId))
{
return FormatReference(links, linkId);
}

try
{
var link = new DoubletLink(links.GetLink(linkId));
var source = FormatStructureSource(links, link.Source, visited);
var target = FormatReference(links, link.Target);

return $"({FormatReference(links, link.Index)}: {source} {target})";
}
finally
{
visited.Remove(linkId);
}
}

private static string FormatStructureSource(INamedTypesLinks<uint> links, uint linkId, HashSet<uint> visited)
{
return links.Exists(linkId) && !visited.Contains(linkId)
? FormatStructure(links, linkId, visited)
: FormatReference(links, linkId);
}

private static string FormatReference(INamedTypesLinks<uint> links, uint link)
{
var name = links.GetName(link);
Expand Down
4 changes: 2 additions & 2 deletions csharp/Foundation.Data.Doublets.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@
var linkId = structure.Value;
try
{
var structureFormatted = decoratedLinks.FormatStructure(linkId, link => decoratedLinks.IsFullPoint(linkId), true, true);
Console.WriteLine(LinoDatabaseOutput.Namify(decoratedLinks, structureFormatted));
var structureFormatted = LinoDatabaseOutput.FormatStructure(decoratedLinks, linkId);
Console.WriteLine(structureFormatted);
}
catch (Exception ex)
{
Expand Down
48 changes: 14 additions & 34 deletions rust/src/link_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! This module provides the LinkStorage struct for managing link persistence.

use anyhow::{Context, Result};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
Expand Down Expand Up @@ -339,47 +339,27 @@ impl LinkStorage {

/// Formats the structure of a link
pub fn format_structure(&self, id: u32) -> Result<String> {
if let Some(link) = self.get(id) {
Ok(self.format_structure_recursive(link, true))
} else {
Err(LinkError::NotFound(id).into())
}
let mut visited = HashSet::new();
self.format_structure_recursive(id, &mut visited)
}

/// Recursively formats a link structure
fn format_structure_recursive(&self, link: &Link, is_root: bool) -> String {
if link.is_full_point() && !is_root {
// Self-referential point - just show the name/id
return self
.names
.get(&link.index)
.cloned()
.unwrap_or_else(|| link.index.to_string());
fn format_structure_recursive(&self, id: u32, visited: &mut HashSet<u32>) -> Result<String> {
let link = self.get(id).ok_or(LinkError::NotFound(id))?;
if !visited.insert(id) {
return Ok(self.format_lino_reference(id));
}

let source_str = if link.source == link.index {
self.names
.get(&link.index)
.cloned()
.unwrap_or_else(|| link.index.to_string())
} else if let Some(source_link) = self.get(link.source) {
self.format_structure_recursive(source_link, false)
} else {
link.source.to_string()
};

let target_str = if link.target == link.index {
self.names
.get(&link.index)
.cloned()
.unwrap_or_else(|| link.index.to_string())
} else if let Some(target_link) = self.get(link.target) {
self.format_structure_recursive(target_link, false)
let source = if self.exists(link.source) && !visited.contains(&link.source) {
self.format_structure_recursive(link.source, visited)?
} else {
link.target.to_string()
self.format_lino_reference(link.source)
};
let target = self.format_lino_reference(link.target);
let index = self.format_lino_reference(link.index);
visited.remove(&id);

format!("({} {})", source_str, target_str)
Ok(format!("({index}: {source} {target})"))
}

/// Prints all links
Expand Down
37 changes: 18 additions & 19 deletions rust/src/named_type_links.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use std::collections::HashSet;
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::path::Path;
Expand Down Expand Up @@ -99,32 +100,30 @@ pub trait NamedTypeLinks {
}

fn format_structure(&mut self, id: u32) -> Result<String> {
let link = self.get_link(id).ok_or(LinkError::NotFound(id))?;
self.format_structure_recursive(&link, true)
let mut visited = HashSet::new();
self.format_structure_recursive(id, &mut visited)
}

fn format_structure_recursive(&mut self, link: &Link, is_root: bool) -> Result<String> {
if link.is_full_point() && !is_root {
return self.format_reference(link.index);
fn format_structure_recursive(
&mut self,
id: u32,
visited: &mut HashSet<u32>,
) -> Result<String> {
let link = self.get_link(id).ok_or(LinkError::NotFound(id))?;
if !visited.insert(id) {
return self.format_reference(id);
}

let source = if link.source == link.index {
self.format_reference(link.index)?
} else if let Some(source_link) = self.get_link(link.source) {
self.format_structure_recursive(&source_link, false)?
} else {
link.source.to_string()
};

let target = if link.target == link.index {
self.format_reference(link.index)?
} else if let Some(target_link) = self.get_link(link.target) {
self.format_structure_recursive(&target_link, false)?
let source = if self.exists(link.source) && !visited.contains(&link.source) {
self.format_structure_recursive(link.source, visited)?
} else {
link.target.to_string()
self.format_reference(link.source)?
};
let target = self.format_reference(link.target)?;
let index = self.format_reference(link.index)?;
visited.remove(&id);

Ok(format!("({source} {target})"))
Ok(format!("({index}: {source} {target})"))
}
}

Expand Down
38 changes: 38 additions & 0 deletions rust/tests/cli_export_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ fn export_alias_writes_named_references() -> Result<()> {
Ok(())
}

#[test]
fn structure_option_renders_left_branch_with_indexes() -> Result<()> {
let temp_dir = tempdir()?;
let db_path = temp_dir.path().join("structure.links");

ensure_success(&run_query(&db_path, "() ((1: 1 1))")?)?;
ensure_success(&run_query(&db_path, "() ((2: 1 2))")?)?;
ensure_success(&run_query(&db_path, "() ((3: 2 1))")?)?;
ensure_success(&run_query(&db_path, "() ((4: 3 2))")?)?;

let output = run_structure(&db_path, 4)?;

ensure_success(&output)?;
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"(4: (3: (2: (1: 1 1) 2) 1) 2)\n"
);

Ok(())
}

fn run_clink(
db_path: &Path,
query: &str,
Expand All @@ -56,6 +77,23 @@ fn run_clink(
.output()?)
}

fn run_query(db_path: &Path, query: &str) -> Result<Output> {
Ok(Command::new(env!("CARGO_BIN_EXE_clink"))
.arg("--db")
.arg(db_path)
.arg(query)
.output()?)
}

fn run_structure(db_path: &Path, structure: u32) -> Result<Output> {
Ok(Command::new(env!("CARGO_BIN_EXE_clink"))
.arg("--db")
.arg(db_path)
.arg("--structure")
.arg(structure.to_string())
.output()?)
}

fn ensure_success(output: &Output) -> Result<()> {
ensure!(
output.status.success(),
Expand Down
35 changes: 35 additions & 0 deletions rust/tests/link_storage_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,38 @@ fn test_write_lino_output_writes_complete_database() -> Result<()> {

Ok(())
}

#[test]
fn test_format_structure_renders_left_branch_with_link_indexes() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let db_path = temp_file.path().to_str().unwrap();

let mut storage = LinkStorage::new(db_path, false)?;
let first = storage.create(0, 0);
let second = storage.create(first, first);
let third = storage.create(second, first);
let fourth = storage.create(third, second);

assert_eq!(
storage.format_structure(fourth)?,
"(4: (3: (2: (1: 0 0) 1) 1) 2)"
);

Ok(())
}

#[test]
fn test_format_structure_renders_repeated_source_and_target_as_reference_on_right() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let db_path = temp_file.path().to_str().unwrap();

let mut storage = LinkStorage::new(db_path, false)?;
let first = storage.create(0, 0);
let second = storage.create(first, first);
storage.create(second, first);
let fourth = storage.create(second, second);

assert_eq!(storage.format_structure(fourth)?, "(4: (2: (1: 0 0) 1) 2)");

Ok(())
}
Loading