diff --git a/README.md b/README.md index 7a6dacd..a5e7346 100644 --- a/README.md +++ b/README.md @@ -138,10 +138,10 @@ clink '((($i:)) (($i:)))' --changes ## Export database as LiNo -Use `--out` to write the complete database to a `.lino` file after the query is processed. The older `--lino-output` option is also accepted. +Use `--out` or `--export` to write the complete database to a `.lino` file after the query is processed. The older `--lino-output` option is also accepted. ```bash -clink '() ((child: father mother))' --out database.lino +clink '() ((child: father mother))' --export database.lino ``` `database.lino`: @@ -319,7 +319,7 @@ clink '((1: 2 1) (2: 1 2)) ()' --changes --after | `--before` | bool | `false` | `-b` | Print the state of the database before applying changes | | `--changes` | bool | `false` | `-c` | Print the changes applied by the query | | `--after` | bool | `false` | `--links`, `-a` | Print the state of the database after applying changes | -| `--out` | string | _None_ | `--lino-output` | Write the complete database as a LiNo file | +| `--out` | string | _None_ | `--export`, `--lino-output` | Write the complete database as a LiNo file | ## For developers and debugging diff --git a/csharp/.changeset/add-export-alias.md b/csharp/.changeset/add-export-alias.md new file mode 100644 index 0000000..0a014d0 --- /dev/null +++ b/csharp/.changeset/add-export-alias.md @@ -0,0 +1,5 @@ +--- +'Foundation.Data.Doublets.Cli': minor +--- + +Added `--export` as an alias for `--out` database export. diff --git a/csharp/Foundation.Data.Doublets.Cli.Tests/CliExportIntegrationTests.cs b/csharp/Foundation.Data.Doublets.Cli.Tests/CliExportIntegrationTests.cs new file mode 100644 index 0000000..080c6f1 --- /dev/null +++ b/csharp/Foundation.Data.Doublets.Cli.Tests/CliExportIntegrationTests.cs @@ -0,0 +1,131 @@ +using System.Diagnostics; + +namespace Foundation.Data.Doublets.Cli.Tests; + +public class CliExportIntegrationTests +{ + [Fact] + public async Task ExportAlias_WritesNumberedReferences() + { + var tempDirectory = CreateTempDirectory(); + + try + { + var dbPath = Path.Combine(tempDirectory, "numbered.links"); + var outputPath = Path.Combine(tempDirectory, "numbered.lino"); + + var result = await RunClinkAsync("--db", dbPath, "() ((1 1) (2 2))", "--export", outputPath); + + AssertClinkSucceeded(result); + Assert.Equal(new[] { "(1: 1 1)", "(2: 2 2)" }, File.ReadAllLines(outputPath)); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + [Fact] + public async Task ExportAlias_WritesNamedReferences() + { + var tempDirectory = CreateTempDirectory(); + + try + { + var dbPath = Path.Combine(tempDirectory, "named.links"); + var outputPath = Path.Combine(tempDirectory, "named.lino"); + + var result = await RunClinkAsync( + "--db", + dbPath, + "--auto-create-missing-references", + "() ((child: father mother))", + "--export", + outputPath); + + AssertClinkSucceeded(result); + Assert.Equal( + new[] { "(father: father father)", "(mother: mother mother)", "(child: father mother)" }, + File.ReadAllLines(outputPath)); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + private static async Task RunClinkAsync(params string[] clinkArguments) + { + var csharpDirectory = FindCsharpDirectory(); + var projectPath = Path.Combine(csharpDirectory, "Foundation.Data.Doublets.Cli", "Foundation.Data.Doublets.Cli.csproj"); + + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "dotnet", + WorkingDirectory = csharpDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true + } + }; + + process.StartInfo.ArgumentList.Add("run"); + process.StartInfo.ArgumentList.Add("--project"); + process.StartInfo.ArgumentList.Add(projectPath); + process.StartInfo.ArgumentList.Add("--"); + foreach (var argument in clinkArguments) + { + process.StartInfo.ArgumentList.Add(argument); + } + + process.Start(); + var stdout = process.StandardOutput.ReadToEndAsync(); + var stderr = process.StandardError.ReadToEndAsync(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + try + { + await process.WaitForExitAsync(cts.Token); + } + catch (OperationCanceledException) + { + process.Kill(entireProcessTree: true); + throw new TimeoutException("Timed out while running the C# clink integration test."); + } + + return new CommandResult(process.ExitCode, await stdout, await stderr); + } + + private static void AssertClinkSucceeded(CommandResult result) + { + Assert.True( + result.ExitCode == 0, + $"clink exited with {result.ExitCode}\nstdout:\n{result.Stdout}\nstderr:\n{result.Stderr}"); + } + + private static string FindCsharpDirectory() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "Foundation.Data.Doublets.Cli.sln"))) + { + return directory.FullName; + } + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("Could not find the csharp directory containing Foundation.Data.Doublets.Cli.sln."); + } + + private static string CreateTempDirectory() + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"clink-export-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDirectory); + return tempDirectory; + } + + private sealed record CommandResult(int ExitCode, string Stdout, string Stderr); +} diff --git a/csharp/Foundation.Data.Doublets.Cli/Program.cs b/csharp/Foundation.Data.Doublets.Cli/Program.cs index 5a6a351..eb40d4c 100644 --- a/csharp/Foundation.Data.Doublets.Cli/Program.cs +++ b/csharp/Foundation.Data.Doublets.Cli/Program.cs @@ -61,7 +61,7 @@ DefaultValueFactory = _ => false }; -var outputOption = new Option("--out", "--lino-output") +var outputOption = new Option("--out", "--lino-output", "--export") { Description = "Path to write the complete database as a LiNo file" }; diff --git a/rust/changelog.d/20260502_061000_export_alias.md b/rust/changelog.d/20260502_061000_export_alias.md new file mode 100644 index 0000000..1e4d95d --- /dev/null +++ b/rust/changelog.d/20260502_061000_export_alias.md @@ -0,0 +1,5 @@ +--- +bump: minor +--- + +Added `--export` as an alias for `--out` database export. diff --git a/rust/src/cli.rs b/rust/src/cli.rs index be27e3e..1dad89e 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -97,7 +97,7 @@ impl Cli { cli.after = parse_bool("--after", value)?; continue; } - if let Some(value) = inline_value(&arg, &["--out", "--lino-output"]) { + if let Some(value) = inline_value(&arg, &["--out", "--lino-output", "--export"]) { cli.lino_output = Some(value.to_string()); continue; } @@ -130,7 +130,7 @@ impl Cli { "-a" | "--after" | "--links" => { cli.after = next_bool_value(&mut args, true)?; } - "--out" | "--lino-output" => { + "--out" | "--lino-output" | "--export" => { cli.lino_output = Some(next_value(&mut args, &arg)?); } "--" => { @@ -178,7 +178,7 @@ impl Cli { " Print the changes applied by the query\n", " -a, --after, --links\n", " Print the state of the database after applying changes\n", - " --out , --lino-output \n", + " --out , --lino-output , --export \n", " Write the complete database as a LiNo file\n", " -h, --help\n", " Print help\n", diff --git a/rust/tests/cli_arguments_tests.rs b/rust/tests/cli_arguments_tests.rs index 2b392cd..030008c 100644 --- a/rust/tests/cli_arguments_tests.rs +++ b/rust/tests/cli_arguments_tests.rs @@ -71,6 +71,20 @@ fn parses_inline_alias_values_and_boolean_values() { assert_eq!(cli.lino_output.as_deref(), Some("links.lino")); } +#[test] +fn parses_export_alias_as_lino_output_path() { + let cli = parse_run(&["clink", "--export", "database.lino"]); + + assert_eq!(cli.lino_output.as_deref(), Some("database.lino")); +} + +#[test] +fn parses_inline_export_alias_as_lino_output_path() { + let cli = parse_run(&["clink", "--export=database.lino"]); + + assert_eq!(cli.lino_output.as_deref(), Some("database.lino")); +} + #[test] fn returns_help_and_version_commands() { assert_eq!( diff --git a/rust/tests/cli_export_tests.rs b/rust/tests/cli_export_tests.rs new file mode 100644 index 0000000..94b74df --- /dev/null +++ b/rust/tests/cli_export_tests.rs @@ -0,0 +1,68 @@ +use anyhow::{ensure, Result}; +use std::path::Path; +use std::process::{Command, Output}; +use tempfile::tempdir; + +#[test] +fn export_alias_writes_numbered_references() -> Result<()> { + let temp_dir = tempdir()?; + let db_path = temp_dir.path().join("numbered.links"); + let output_path = temp_dir.path().join("numbered.lino"); + + let output = run_clink(&db_path, "() ((1 1) (2 2))", false, &output_path)?; + + ensure_success(&output)?; + assert_eq!( + std::fs::read_to_string(&output_path)?, + "(1: 1 1)\n(2: 2 2)\n" + ); + + Ok(()) +} + +#[test] +fn export_alias_writes_named_references() -> Result<()> { + let temp_dir = tempdir()?; + let db_path = temp_dir.path().join("named.links"); + let output_path = temp_dir.path().join("named.lino"); + + let output = run_clink(&db_path, "() ((child: father mother))", true, &output_path)?; + + ensure_success(&output)?; + assert_eq!( + std::fs::read_to_string(&output_path)?, + "(father: father father)\n(mother: mother mother)\n(child: father mother)\n" + ); + + Ok(()) +} + +fn run_clink( + db_path: &Path, + query: &str, + auto_create_missing_references: bool, + output_path: &Path, +) -> Result { + let mut command = Command::new(env!("CARGO_BIN_EXE_clink")); + command.arg("--db").arg(db_path); + if auto_create_missing_references { + command.arg("--auto-create-missing-references"); + } + + Ok(command + .arg(query) + .arg("--export") + .arg(output_path) + .output()?) +} + +fn ensure_success(output: &Output) -> Result<()> { + ensure!( + output.status.success(), + "clink failed with status {:?}\nstdout:\n{}\nstderr:\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + Ok(()) +}