Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ against every tmux from **3.2a to 3.7b** on **net8.0** and **net10.0**.
> settled, and any release may change or remove exported identifiers without a
> deprecation period. Pin an exact version. Not recommended for production.

<!-- snippet: ConnectAndBuild usings: LibTmux -->
<!-- snippet: ConnectAndBuild+BuildHierarchy usings: LibTmux -->
```csharp
using LibTmux;

Expand Down
26 changes: 15 additions & 11 deletions eng/docs/sync_snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
READMEs, and nuget.org renders the markdown it is given without resolving
anything.

Anchors name the region, and optionally namespaces the document adds above the
block that the snippet file does not need::
Anchors name one region or a ``+``-joined sequence of regions, and optionally
namespaces the document adds above the block that the snippet file does not
need::

<!-- snippet: ConnectAndBuild usings: LibTmux -->
```csharp
Expand Down Expand Up @@ -70,14 +71,18 @@ def read_regions() -> dict[str, str]:
return regions


def render(name: str, options: str, regions: dict[str, str]) -> str:
"""Return the fenced block a document should carry for one region."""
if name not in regions:
known = ", ".join(sorted(regions)) or "none"
msg = f"no #region named {name}. Published regions: {known}"
raise SystemExit(msg)
def render(name: str, options: str, regions: dict[str, str], used: set[str]) -> str:
"""Return the fenced block a document should carry for named regions."""
bodies: list[str] = []
for component in name.split("+"):
if component not in regions:
known = ", ".join(sorted(regions)) or "none"
msg = f"no #region named {component}. Published regions: {known}"
raise SystemExit(msg)
used.add(component)
bodies.append(regions[component])

body = regions[name]
body = "\n".join(bodies)
using = USINGS.search(options)
if using:
namespaces = [part.strip() for part in re.split(r"[ ,]+", using.group("names")) if part.strip()]
Expand All @@ -91,8 +96,7 @@ def apply(text: str, regions: dict[str, str], used: set[str]) -> str:

def replace(match: re.Match[str]) -> str:
name = match.group("name")
used.add(name)
block = render(name, match.group("options"), regions)
block = render(name, match.group("options"), regions, used)
return f"{match.group('open')}{block}{match.group('close')}"

return ANCHOR.sub(replace, text)
Expand Down
52 changes: 52 additions & 0 deletions eng/docs/tests/test_sync_snippets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Exercise source-grounded snippet rendering."""

from __future__ import annotations

import pathlib
import runpy
import typing as t

import pytest


def load_synchronizer() -> dict[str, t.Any]:
"""Load the synchronizer without making its directory a package."""
return runpy.run_path(str(pathlib.Path(__file__).parents[1] / "sync_snippets.py"))


def test_composed_anchor_concatenates_regions_in_declared_order() -> None:
"""A README can publish setup and a shared body without duplicating either."""
synchronizer = load_synchronizer()
used: set[str] = set()

rendered = synchronizer["render"](
"ConnectAndBuild+BuildHierarchy",
"usings: LibTmux",
{
"ConnectAndBuild": "Server server = await Server.ConnectAsync();",
"BuildHierarchy": "await server.CreateSessionAsync(new NewSessionRequest(name: \"build\"));",
},
used,
)

assert rendered == """```csharp
using LibTmux;

Server server = await Server.ConnectAsync();
await server.CreateSessionAsync(new NewSessionRequest(name: \"build\"));
```
"""
assert used == {"ConnectAndBuild", "BuildHierarchy"}


def test_composed_anchor_requires_every_region() -> None:
"""A misspelled component must fail instead of silently publishing half an example."""
synchronizer = load_synchronizer()

with pytest.raises(SystemExit, match="BuildHierarchy"):
synchronizer["render"](
"ConnectAndBuild+BuildHierarchy",
"",
{"ConnectAndBuild": "Server server = await Server.ConnectAsync();"},
set(),
)
114 changes: 113 additions & 1 deletion examples/LibTmux.Examples/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.Versioning;
using System.Text;
using System.Text.Json;

namespace LibTmux.Examples;

Expand All @@ -20,9 +22,20 @@ private static async Task<int> Main(string[] args)
return 0;
}

if (args is ["--arena-one-shot"])
{
if (OperatingSystem.IsWindows())
{
Console.Error.WriteLine("The tmux arena requires Linux or macOS.");
return 1;
}

return await RunArenaOneShotAsync();
}

if (args.Length != 0)
{
Console.Error.WriteLine("usage: LibTmux.Examples [--psmux]");
Console.Error.WriteLine("usage: LibTmux.Examples [--arena-one-shot|--psmux]");
return 2;
}

Expand All @@ -36,6 +49,84 @@ private static async Task<int> Main(string[] args)
return await RunTmuxExamplesAsync();
}

[UnsupportedOSPlatform("windows")]
private static async Task<int> RunArenaOneShotAsync()
{
if (Environment.GetEnvironmentVariable("LIBTMUX_ARENA_DESCRIPTOR") is null or "")
{
ExampleCase example = ExampleCase.Discover().Single(
example => string.Equals(
$"{example.Topic}.{example.Id}",
"OneShot.ConnectAndBuild",
StringComparison.Ordinal));
await example.RunAsync();
return 0;
}

ArenaContract? contract = ArenaContract.Read();
if (contract is null)
{
return 2;
}

try
{
Server server = await Server.ConnectAsync(
new ServerConnectionOptions(
tmuxBinaryPath: contract.TmuxBinaryPath,
socketPath: contract.SocketPath));
await Snippets.OneShot.BuildHierarchy(server);

IReadOnlyList<string>? identity = await server.DisplayMessageAsync(
new DisplayMessageRequest("#{pid}\t#{socket_path}", returnText: true));
if (identity is not [string value])
{
throw new InvalidOperationException("tmux did not report one arena server identity.");
}

string[] parts = value.Split('\t');
if (parts.Length != 2
|| !int.TryParse(
parts[0],
NumberStyles.None,
CultureInfo.InvariantCulture,
out int processId)
|| processId <= 0
|| !string.Equals(parts[1], contract.SocketPath, StringComparison.Ordinal))
{
throw new InvalidOperationException("tmux reported an invalid arena server identity.");
}

IReadOnlyList<TmuxOption> options = await server.Options.GetAsync(
new GetOptionRequest(
"@libtmux_arena_challenge",
OptionScope.Session,
global: true,
quiet: true));
if (options is not [{ Value.Raw: string challenge }] || string.IsNullOrWhiteSpace(challenge))
{
throw new InvalidOperationException("tmux did not report an arena challenge.");
}

string evidence = JsonSerializer.Serialize(
new
{
schema = 1,
server_pid = processId,
socket_path = parts[1],
challenge,
artifact = contract.Artifact,
});
Console.WriteLine($"LIBTMUX_ARENA_EVIDENCE={evidence}");
return 0;
}
catch (Exception failure) when (failure is not OperationCanceledException)
{
Console.Error.WriteLine(failure.Message);
return 1;
}
}

[UnsupportedOSPlatform("windows")]
private static async Task<int> RunTmuxExamplesAsync()
{
Expand All @@ -61,4 +152,25 @@ private static async Task<int> RunTmuxExamplesAsync()
Console.WriteLine();
return failed == 0 ? 0 : 1;
}

private sealed record ArenaContract(string Artifact, string SocketPath, string TmuxBinaryPath)
{
public static ArenaContract? Read()
{
string? artifact = Environment.GetEnvironmentVariable("LIBTMUX_ARENA_ARTIFACT");
string? socketPath = Environment.GetEnvironmentVariable("LIBTMUX_SOCKET_PATH");
string? tmuxBinaryPath = Environment.GetEnvironmentVariable("LIBTMUX_TMUX_BIN");
if (string.IsNullOrWhiteSpace(artifact)
|| string.IsNullOrWhiteSpace(socketPath)
|| string.IsNullOrWhiteSpace(tmuxBinaryPath)
|| !Path.IsPathFullyQualified(tmuxBinaryPath)
|| !string.Equals(artifact, "OneShot.ConnectAndBuild", StringComparison.Ordinal))
{
Console.Error.WriteLine("The arena contract is incomplete or mismatched.");
return null;
}

return new ArenaContract(artifact, socketPath, tmuxBinaryPath);
}
}
}
12 changes: 12 additions & 0 deletions examples/LibTmux.Examples/Snippets/OneShot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ public static async Task ConnectAndBuild()
{
#region ConnectAndBuild
Server server = await Server.ConnectAsync();
#endregion
await BuildHierarchy(server);
}

/// <summary>Builds the hierarchy on a supplied server.</summary>
[Example(
"Build a session and window on a supplied server",
RunsInDefaultSuite = false)]
public static async Task BuildHierarchy(Server server)
{
ArgumentNullException.ThrowIfNull(server);
#region BuildHierarchy
Session session = await server.CreateSessionAsync(new NewSessionRequest(name: "build"));
Window window = await session.CreateWindowAsync(new NewWindowRequest(name: "tests"));
Pane pane = (await window.GetPanesAsync())[0];
Expand Down
Loading
Loading