feat(dotnet): add galaxy-browse CLI (§4.6); chore: verify version subcommand (§4.4)
This commit is contained in:
@@ -244,6 +244,19 @@ foreach (LazyBrowseNode root in roots)
|
|||||||
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
|
||||||
`BrowseAsync` again from the root.
|
`BrowseAsync` again from the root.
|
||||||
|
|
||||||
|
The CLI counterpart is `galaxy-browse`. Without `--parent` it walks the root
|
||||||
|
objects and eagerly expands `--depth` further levels into an indented tree; with
|
||||||
|
`--parent <gobject-id>` it fetches exactly one level of children for that object
|
||||||
|
(`--depth` is ignored there). Filter flags map onto `BrowseChildrenOptions`:
|
||||||
|
`--category-ids` and `--template-contains` are comma-separated lists,
|
||||||
|
`--tag-name-glob` / `--alarm-bearing-only` / `--historized-only` are scalar, and
|
||||||
|
`--include-attributes` overrides the server default for attribute population.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-browse --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --depth 1
|
||||||
|
dotnet run --project clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli -- galaxy-browse --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --parent 42 --json
|
||||||
|
```
|
||||||
|
|
||||||
### Watching deploy events
|
### Watching deploy events
|
||||||
|
|
||||||
`WatchDeployEventsAsync` opens the `WatchDeployEvents` server-streaming RPC. The
|
`WatchDeployEventsAsync` opens the `WatchDeployEvents` server-streaming RPC. The
|
||||||
|
|||||||
@@ -105,4 +105,16 @@ public interface IMxGatewayCliClient : IAsyncDisposable
|
|||||||
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches one page of direct children of a Galaxy parent (or the root
|
||||||
|
/// objects when the parent selector is unset), the primitive that backs the
|
||||||
|
/// lazy-browse helper.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The browse-children request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The browse-children reply.</returns>
|
||||||
|
Task<BrowseChildrenReply> GalaxyBrowseChildrenAsync(
|
||||||
|
BrowseChildrenRequest request,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<BrowseChildrenReply> GalaxyBrowseChildrenAsync(
|
||||||
|
BrowseChildrenRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return _galaxyClient.Value.BrowseChildrenRawAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ public static class MxGatewayClientCli
|
|||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
"galaxy-discover" => await GalaxyDiscoverAsync(arguments, client, standardOutput, cancellation.Token)
|
"galaxy-discover" => await GalaxyDiscoverAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
|
"galaxy-browse" => await GalaxyBrowseAsync(arguments, client, standardOutput, standardError, cancellation.Token)
|
||||||
|
.ConfigureAwait(false),
|
||||||
"galaxy-watch" => await GalaxyWatchAsync(arguments, client, standardOutput, cancellation.Token)
|
"galaxy-watch" => await GalaxyWatchAsync(arguments, client, standardOutput, cancellation.Token)
|
||||||
.ConfigureAwait(false),
|
.ConfigureAwait(false),
|
||||||
_ => WriteUnknownCommand(command, standardError),
|
_ => WriteUnknownCommand(command, standardError),
|
||||||
@@ -1607,6 +1609,270 @@ public static class MxGatewayClientCli
|
|||||||
return aggregate;
|
return aggregate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-request page size for the galaxy-browse single-level walks. Mirrors
|
||||||
|
/// the library's <c>BrowseChildrenPageSize</c> so the CLI and the
|
||||||
|
/// lazy-browse helper page identically.
|
||||||
|
/// </summary>
|
||||||
|
private const int BrowseChildrenCliPageSize = 500;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drives the lazy-browse Galaxy surface from the CLI. Without
|
||||||
|
/// <c>--parent</c> it walks the root objects and eagerly expands
|
||||||
|
/// <c>--depth</c> further levels (each level reuses the same
|
||||||
|
/// <see cref="BrowseChildrenOptions"/>, like the library helper). With
|
||||||
|
/// <c>--parent</c> it fetches exactly one level of children for that
|
||||||
|
/// gobject id via a parent-scoped BrowseChildren request; <c>--depth</c>
|
||||||
|
/// is not meaningful there and a warning is emitted if combined, mirroring
|
||||||
|
/// the Go/Rust CLIs.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<int> GalaxyBrowseAsync(
|
||||||
|
CliArguments arguments,
|
||||||
|
IMxGatewayCliClient client,
|
||||||
|
TextWriter output,
|
||||||
|
TextWriter standardError,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
BrowseChildrenOptions options = ParseBrowseChildrenOptions(arguments);
|
||||||
|
bool json = arguments.HasFlag("json");
|
||||||
|
int parent = arguments.GetInt32("parent", -1);
|
||||||
|
int depth = arguments.GetInt32("depth", 0);
|
||||||
|
|
||||||
|
// A specific parent → one level of children via the parent-scoped RPC.
|
||||||
|
if (parent >= 0)
|
||||||
|
{
|
||||||
|
if (depth > 0)
|
||||||
|
{
|
||||||
|
standardError.WriteLine("warning: --depth is ignored when --parent is specified.");
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<GalaxyObject> children = await BrowseOneLevelAsync(
|
||||||
|
client,
|
||||||
|
options,
|
||||||
|
parent,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (json)
|
||||||
|
{
|
||||||
|
output.WriteLine(JsonSerializer.Serialize(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
command = "galaxy-browse",
|
||||||
|
parentId = parent,
|
||||||
|
children = children.Select(GalaxyObjectToJsonElement).ToArray(),
|
||||||
|
},
|
||||||
|
JsonOptions));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteLine(children.Count.ToString(CultureInfo.InvariantCulture));
|
||||||
|
foreach (GalaxyObject child in children)
|
||||||
|
{
|
||||||
|
output.WriteLine(FormatGalaxyObject(child, level: 0, hasChildrenHint: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No parent → walk the root objects, eagerly expanding --depth levels.
|
||||||
|
IReadOnlyList<BrowseTreeNode> roots = await BrowseTreeAsync(
|
||||||
|
client,
|
||||||
|
options,
|
||||||
|
parentGobjectId: 0,
|
||||||
|
remainingDepth: depth,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (json)
|
||||||
|
{
|
||||||
|
output.WriteLine(JsonSerializer.Serialize(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
command = "galaxy-browse",
|
||||||
|
nodes = roots.Select(BrowseTreeNodeToJson).ToArray(),
|
||||||
|
},
|
||||||
|
JsonOptions));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteLine(roots.Count.ToString(CultureInfo.InvariantCulture));
|
||||||
|
foreach (BrowseTreeNode node in roots)
|
||||||
|
{
|
||||||
|
WriteBrowseTreeNode(output, node, level: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One node in the eagerly-expanded galaxy-browse tree: the Galaxy object,
|
||||||
|
/// the server's has-children hint, and any children fetched up to the
|
||||||
|
/// requested depth.
|
||||||
|
/// </summary>
|
||||||
|
private sealed record BrowseTreeNode(
|
||||||
|
GalaxyObject Object,
|
||||||
|
bool HasChildrenHint,
|
||||||
|
IReadOnlyList<BrowseTreeNode> Children);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the direct children of <paramref name="parentGobjectId"/>
|
||||||
|
/// (0 = root) and recursively expands <paramref name="remainingDepth"/>
|
||||||
|
/// further levels. Paging is followed to completion at each level.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<IReadOnlyList<BrowseTreeNode>> BrowseTreeAsync(
|
||||||
|
IMxGatewayCliClient client,
|
||||||
|
BrowseChildrenOptions options,
|
||||||
|
int parentGobjectId,
|
||||||
|
int remainingDepth,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<BrowseTreeNode> nodes = [];
|
||||||
|
string pageToken = string.Empty;
|
||||||
|
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||||
|
do
|
||||||
|
{
|
||||||
|
BrowseChildrenRequest request = BuildBrowseChildrenRequest(options, parentGobjectId, pageToken);
|
||||||
|
BrowseChildrenReply reply = await client.GalaxyBrowseChildrenAsync(request, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
for (int i = 0; i < reply.Children.Count; i++)
|
||||||
|
{
|
||||||
|
GalaxyObject child = reply.Children[i];
|
||||||
|
bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i];
|
||||||
|
IReadOnlyList<BrowseTreeNode> grandChildren = remainingDepth > 0
|
||||||
|
? await BrowseTreeAsync(client, options, child.GobjectId, remainingDepth - 1, cancellationToken)
|
||||||
|
.ConfigureAwait(false)
|
||||||
|
: [];
|
||||||
|
nodes.Add(new BrowseTreeNode(child, hint, grandChildren));
|
||||||
|
}
|
||||||
|
|
||||||
|
pageToken = reply.NextPageToken;
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
|
||||||
|
{
|
||||||
|
throw new MxGatewayException(
|
||||||
|
$"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Fetches exactly one level of children for a parent gobject id, paging to completion.</summary>
|
||||||
|
private static async Task<IReadOnlyList<GalaxyObject>> BrowseOneLevelAsync(
|
||||||
|
IMxGatewayCliClient client,
|
||||||
|
BrowseChildrenOptions options,
|
||||||
|
int parentGobjectId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<GalaxyObject> children = [];
|
||||||
|
string pageToken = string.Empty;
|
||||||
|
HashSet<string> seenPageTokens = new(StringComparer.Ordinal);
|
||||||
|
do
|
||||||
|
{
|
||||||
|
BrowseChildrenRequest request = BuildBrowseChildrenRequest(options, parentGobjectId, pageToken);
|
||||||
|
BrowseChildrenReply reply = await client.GalaxyBrowseChildrenAsync(request, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
children.AddRange(reply.Children);
|
||||||
|
pageToken = reply.NextPageToken;
|
||||||
|
if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken))
|
||||||
|
{
|
||||||
|
throw new MxGatewayException(
|
||||||
|
$"Galaxy BrowseChildren returned a repeated page token '{pageToken}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!string.IsNullOrWhiteSpace(pageToken));
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BrowseChildrenOptions ParseBrowseChildrenOptions(CliArguments arguments)
|
||||||
|
{
|
||||||
|
return new BrowseChildrenOptions
|
||||||
|
{
|
||||||
|
CategoryIds = ParseOptionalInt32List(arguments.GetOptional("category-ids")),
|
||||||
|
TemplateChainContains = ParseOptionalStringList(arguments.GetOptional("template-contains")),
|
||||||
|
TagNameGlob = arguments.GetOptional("tag-name-glob"),
|
||||||
|
AlarmBearingOnly = arguments.HasFlag("alarm-bearing-only"),
|
||||||
|
HistorizedOnly = arguments.HasFlag("historized-only"),
|
||||||
|
// Tri-state: only override the server default when the flag is present.
|
||||||
|
IncludeAttributes = arguments.HasFlag("include-attributes") ? true : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BrowseChildrenRequest BuildBrowseChildrenRequest(
|
||||||
|
BrowseChildrenOptions options,
|
||||||
|
int parentGobjectId,
|
||||||
|
string pageToken)
|
||||||
|
{
|
||||||
|
BrowseChildrenRequest request = new()
|
||||||
|
{
|
||||||
|
PageSize = BrowseChildrenCliPageSize,
|
||||||
|
PageToken = pageToken,
|
||||||
|
ParentGobjectId = parentGobjectId,
|
||||||
|
AlarmBearingOnly = options.AlarmBearingOnly,
|
||||||
|
HistorizedOnly = options.HistorizedOnly,
|
||||||
|
};
|
||||||
|
request.CategoryIds.Add(options.CategoryIds);
|
||||||
|
request.TemplateChainContains.Add(options.TemplateChainContains);
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.TagNameGlob))
|
||||||
|
{
|
||||||
|
request.TagNameGlob = options.TagNameGlob;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.IncludeAttributes.HasValue)
|
||||||
|
{
|
||||||
|
request.IncludeAttributes = options.IncludeAttributes.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteBrowseTreeNode(TextWriter output, BrowseTreeNode node, int level)
|
||||||
|
{
|
||||||
|
output.WriteLine(FormatGalaxyObject(node.Object, level, node.HasChildrenHint));
|
||||||
|
foreach (BrowseTreeNode child in node.Children)
|
||||||
|
{
|
||||||
|
WriteBrowseTreeNode(output, child, level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatGalaxyObject(GalaxyObject galaxyObject, int level, bool? hasChildrenHint)
|
||||||
|
{
|
||||||
|
string indent = new(' ', level * 2);
|
||||||
|
string suffix = hasChildrenHint is null
|
||||||
|
? $"(attrs={galaxyObject.Attributes.Count})"
|
||||||
|
: $"(attrs={galaxyObject.Attributes.Count}, hasChildren={hasChildrenHint.Value})";
|
||||||
|
return $"{indent}{galaxyObject.GobjectId}\t{galaxyObject.TagName}\t{galaxyObject.BrowseName}\t{suffix}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object BrowseTreeNodeToJson(BrowseTreeNode node)
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
@object = GalaxyObjectToJsonElement(node.Object),
|
||||||
|
hasChildren = node.HasChildrenHint,
|
||||||
|
children = node.Children.Select(BrowseTreeNodeToJson).ToArray(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement GalaxyObjectToJsonElement(GalaxyObject galaxyObject)
|
||||||
|
{
|
||||||
|
return JsonDocument.Parse(ProtobufJsonFormatter.Format(galaxyObject)).RootElement.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<int> ParseOptionalInt32List(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? [] : ParseInt32List(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ParseOptionalStringList(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? [] : ParseStringList(value);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<int> GalaxyWatchAsync(
|
private static async Task<int> GalaxyWatchAsync(
|
||||||
CliArguments arguments,
|
CliArguments arguments,
|
||||||
IMxGatewayCliClient client,
|
IMxGatewayCliClient client,
|
||||||
@@ -1736,6 +2002,7 @@ public static class MxGatewayClientCli
|
|||||||
or "galaxy-test-connection"
|
or "galaxy-test-connection"
|
||||||
or "galaxy-last-deploy"
|
or "galaxy-last-deploy"
|
||||||
or "galaxy-discover"
|
or "galaxy-discover"
|
||||||
|
or "galaxy-browse"
|
||||||
or "galaxy-watch";
|
or "galaxy-watch";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1797,6 +2064,7 @@ public static class MxGatewayClientCli
|
|||||||
writer.WriteLine("mxgw-dotnet galaxy-test-connection [--json]");
|
writer.WriteLine("mxgw-dotnet galaxy-test-connection [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet galaxy-last-deploy [--json]");
|
writer.WriteLine("mxgw-dotnet galaxy-last-deploy [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet galaxy-discover [--json]");
|
writer.WriteLine("mxgw-dotnet galaxy-discover [--json]");
|
||||||
|
writer.WriteLine("mxgw-dotnet galaxy-browse [--parent <gobject-id>] [--depth <n>] [--category-ids <n,n>] [--template-contains <s,s>] [--tag-name-glob <glob>] [--alarm-bearing-only] [--historized-only] [--include-attributes] [--json]");
|
||||||
writer.WriteLine("mxgw-dotnet galaxy-watch [--last-seen-deploy-time <iso8601>] [--max-events <n>] [--json]");
|
writer.WriteLine("mxgw-dotnet galaxy-watch [--last-seen-deploy-time <iso8601>] [--max-events <n>] [--json]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,6 +360,146 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies galaxy-browse walks root objects and eagerly expands one further
|
||||||
|
/// level when --depth 1 is passed, printing an indented tree.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_GalaxyBrowse_TextTreeExpandsToDepth()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
// Root level (parent 0): one area with a child hint.
|
||||||
|
fakeClient.GalaxyBrowseChildrenReplies[0] = new Queue<BrowseChildrenReply>(
|
||||||
|
[
|
||||||
|
new BrowseChildrenReply
|
||||||
|
{
|
||||||
|
Children = { new GalaxyObject { GobjectId = 10, TagName = "Area_001", BrowseName = "Area" } },
|
||||||
|
ChildHasChildren = { true },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
// Children of gobject 10.
|
||||||
|
fakeClient.GalaxyBrowseChildrenReplies[10] = new Queue<BrowseChildrenReply>(
|
||||||
|
[
|
||||||
|
new BrowseChildrenReply
|
||||||
|
{
|
||||||
|
Children = { new GalaxyObject { GobjectId = 20, TagName = "Tank_001", BrowseName = "Tank" } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"galaxy-browse",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--depth",
|
||||||
|
"1",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
string text = output.ToString();
|
||||||
|
Assert.Contains("Area_001", text);
|
||||||
|
Assert.Contains("Tank_001", text);
|
||||||
|
// Children are indented beneath their parent (two-space indent per level).
|
||||||
|
Assert.Matches(@"\n \d+\tTank_001", text);
|
||||||
|
// Root fetched with the parent oneof unset; child fetch used parent 10.
|
||||||
|
Assert.Contains(
|
||||||
|
fakeClient.GalaxyBrowseChildrenRequests,
|
||||||
|
request => request.ParentCase == BrowseChildrenRequest.ParentOneofCase.ParentGobjectId
|
||||||
|
&& request.ParentGobjectId == 10);
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies galaxy-browse --json emits a nested JSON document and forwards
|
||||||
|
/// the filter flags onto the BrowseChildren request.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_GalaxyBrowse_JsonForwardsFilters()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
fakeClient.GalaxyBrowseChildrenReplies[0] = new Queue<BrowseChildrenReply>(
|
||||||
|
[
|
||||||
|
new BrowseChildrenReply
|
||||||
|
{
|
||||||
|
Children = { new GalaxyObject { GobjectId = 10, TagName = "Area_001", BrowseName = "Area" } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"galaxy-browse",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--tag-name-glob",
|
||||||
|
"Area*",
|
||||||
|
"--alarm-bearing-only",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
using System.Text.Json.JsonDocument document = System.Text.Json.JsonDocument.Parse(output.ToString());
|
||||||
|
Assert.Equal("galaxy-browse", document.RootElement.GetProperty("command").GetString());
|
||||||
|
Assert.True(document.RootElement.GetProperty("nodes").GetArrayLength() >= 1);
|
||||||
|
BrowseChildrenRequest request = Assert.Single(fakeClient.GalaxyBrowseChildrenRequests);
|
||||||
|
Assert.Equal("Area*", request.TagNameGlob);
|
||||||
|
Assert.True(request.AlarmBearingOnly);
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies galaxy-browse --parent fetches exactly one level of children for
|
||||||
|
/// the supplied gobject id via a parent-scoped BrowseChildren request.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task RunAsync_GalaxyBrowse_ParentFetchesSingleLevel()
|
||||||
|
{
|
||||||
|
using var output = new StringWriter();
|
||||||
|
using var error = new StringWriter();
|
||||||
|
FakeCliClient fakeClient = new();
|
||||||
|
fakeClient.GalaxyBrowseChildrenReplies[10] = new Queue<BrowseChildrenReply>(
|
||||||
|
[
|
||||||
|
new BrowseChildrenReply
|
||||||
|
{
|
||||||
|
Children = { new GalaxyObject { GobjectId = 20, TagName = "Tank_001", BrowseName = "Tank" } },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
int exitCode = await MxGatewayClientCli.RunAsync(
|
||||||
|
[
|
||||||
|
"galaxy-browse",
|
||||||
|
"--endpoint",
|
||||||
|
"http://localhost:5000",
|
||||||
|
"--api-key",
|
||||||
|
"test-api-key",
|
||||||
|
"--parent",
|
||||||
|
"10",
|
||||||
|
],
|
||||||
|
output,
|
||||||
|
error,
|
||||||
|
_ => fakeClient);
|
||||||
|
|
||||||
|
Assert.Equal(0, exitCode);
|
||||||
|
Assert.Contains("Tank_001", output.ToString());
|
||||||
|
BrowseChildrenRequest request = Assert.Single(fakeClient.GalaxyBrowseChildrenRequests);
|
||||||
|
Assert.Equal(BrowseChildrenRequest.ParentOneofCase.ParentGobjectId, request.ParentCase);
|
||||||
|
Assert.Equal(10, request.ParentGobjectId);
|
||||||
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary>
|
/// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
||||||
@@ -1051,5 +1191,33 @@ public sealed class MxGatewayClientCliTests
|
|||||||
yield return deployEvent;
|
yield return deployEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy browse-children requests, in call order.</summary>
|
||||||
|
public List<BrowseChildrenRequest> GalaxyBrowseChildrenRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-parent browse-children replies keyed by <c>parent_gobject_id</c>
|
||||||
|
/// (0 = root). Each parent's queue is dequeued in page order; an absent
|
||||||
|
/// or exhausted queue yields an empty reply.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<int, Queue<BrowseChildrenReply>> GalaxyBrowseChildrenReplies { get; } = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<BrowseChildrenReply> GalaxyBrowseChildrenAsync(
|
||||||
|
BrowseChildrenRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
GalaxyBrowseChildrenRequests.Add(request);
|
||||||
|
int parentId = request.ParentCase == BrowseChildrenRequest.ParentOneofCase.ParentGobjectId
|
||||||
|
? request.ParentGobjectId
|
||||||
|
: 0;
|
||||||
|
if (GalaxyBrowseChildrenReplies.TryGetValue(parentId, out Queue<BrowseChildrenReply>? queue)
|
||||||
|
&& queue.TryDequeue(out BrowseChildrenReply? reply))
|
||||||
|
{
|
||||||
|
return Task.FromResult(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(new BrowseChildrenReply());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user