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
50 changes: 43 additions & 7 deletions src/Opc.Ua.Client/AliasNames/AliasNameClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.Client;

namespace Opc.Ua.Client.AliasNames
{
Expand Down Expand Up @@ -444,13 +445,7 @@ public async IAsyncEnumerable<AliasNameSubCategoryInfo>
throw new ServiceResultException(br.StatusCode);
}

int refCount = br.References.Count;
var refs = new ReferenceDescription[refCount];
for (int i = 0; i < refCount; i++)
{
refs[i] = br.References[i];
}
foreach (ReferenceDescription r in refs)
foreach (ReferenceDescription r in SnapshotReferences(br.References))
{
if (!r.TypeDefinition.Equals(ObjectTypeIds.AliasNameCategoryType))
{
Expand All @@ -467,12 +462,53 @@ public async IAsyncEnumerable<AliasNameSubCategoryInfo>
r.BrowseName,
r.DisplayName);
}

ByteString continuationPoint = br.ContinuationPoint;
while (!continuationPoint.IsEmpty)
{
(_, continuationPoint, ArrayOf<ReferenceDescription> nextReferences) =
await Session.BrowseNextAsync(
requestHeader: null,
releaseContinuationPoint: false,
continuationPoint,
ct).ConfigureAwait(false);

foreach (ReferenceDescription r in SnapshotReferences(nextReferences))
{
if (!r.TypeDefinition.Equals(ObjectTypeIds.AliasNameCategoryType))
{
continue;
}
var localId = ExpandedNodeId.ToNodeId(
r.NodeId, Session.NamespaceUris);
if (localId.IsNull)
{
continue;
}
yield return new AliasNameSubCategoryInfo(
localId,
r.BrowseName,
r.DisplayName);
}
}
}

// --------------------------------------------------------------
// Internal helpers
// --------------------------------------------------------------

private static ReferenceDescription[] SnapshotReferences(
ArrayOf<ReferenceDescription> references)
{
int refCount = references.Count;
var snapshot = new ReferenceDescription[refCount];
for (int i = 0; i < refCount; i++)
{
snapshot[i] = references[i];
}
return snapshot;
}

private async Task<NodeId> ResolveChildAsync(
string childBrowseName,
CancellationToken ct)
Expand Down
62 changes: 62 additions & 0 deletions tests/Opc.Ua.Client.Tests/AliasNames/AliasNameClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,5 +228,67 @@ await client.AddAliasesToCategoryAsync(
]).ConfigureAwait(false);
}, Throws.TypeOf<System.UnauthorizedAccessException>());
}

[Test]
public async Task EnumerateSubCategoriesAsyncFollowsBrowseContinuationPointsAsync()
{
var harness = AliasNameSessionHarness.Create();
var continuationPoint = new ByteString(new byte[] { 0x01 });

harness.BrowseHandler = description =>
{
Assert.That(description.NodeId, Is.EqualTo(ObjectIds.Aliases));
return new BrowseResult
{
StatusCode = StatusCodes.Good,
ContinuationPoint = continuationPoint,
References = new[]
{
CreateCategoryReference("Page1", "Page1")
}.ToArrayOf()
};
};

harness.BrowseNextHandler = cp =>
{
Assert.That(cp, Is.EqualTo(continuationPoint));
return new BrowseResult
{
StatusCode = StatusCodes.Good,
ContinuationPoint = ByteString.Empty,
References = new[]
{
CreateCategoryReference("Page2", "Page2")
}.ToArrayOf()
};
};

var client = AliasNameClient.OpenStandardAliases(harness.Session);
var names = new List<string>();

await foreach (AliasNameSubCategoryInfo info in
client.EnumerateSubCategoriesAsync().ConfigureAwait(false))
{
names.Add(info.BrowseName.Name);
}

var expectedNames = new[] { "Page1", "Page2" };
Assert.That(names, Is.EqualTo(expectedNames));
Assert.That(harness.BrowseRequests, Has.Count.EqualTo(1));
Assert.That(harness.BrowseNextRequests, Has.Count.EqualTo(1));

static ReferenceDescription CreateCategoryReference(
string browseName,
string nodeId)
{
return new ReferenceDescription
{
NodeId = new ExpandedNodeId(new NodeId(nodeId, 2)),
BrowseName = new QualifiedName(browseName, 2),
DisplayName = new LocalizedText(browseName),
TypeDefinition = ObjectTypeIds.AliasNameCategoryType
};
}
}
}
}
51 changes: 44 additions & 7 deletions tests/Opc.Ua.Client.Tests/AliasNames/AliasNameSessionHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ internal sealed class AliasNameSessionHarness
public IServiceMessageContext MessageContext { get; }
public List<CallMethodRequest> CallRequests { get; } = [];
public List<ReadValueId> ReadRequests { get; } = [];
public List<BrowseDescription> BrowseRequests { get; } = [];
public List<ByteString> BrowseNextRequests { get; } = [];
public Func<CallMethodRequest, CallMethodResult> CallHandler { get; set; }
public Func<ReadValueId, DataValue> ReadHandler { get; set; }
public Func<BrowseDescription, BrowseResult> BrowseHandler { get; set; }
public Func<ByteString, BrowseResult> BrowseNextHandler { get; set; }

public Func<BrowsePath, BrowsePathResult> BrowsePathHandler { get; set; }

Expand Down Expand Up @@ -164,13 +168,11 @@ public static AliasNameSessionHarness Create()
var results = new BrowseResult[descriptions.Count];
for (int i = 0; i < descriptions.Count; i++)
{
results[i] = new BrowseResult
{
StatusCode = StatusCodes.Good,
References = Array
.Empty<ReferenceDescription>()
.ToArrayOf()
};
BrowseDescription description = descriptions[i];
harness.BrowseRequests.Add(description);
results[i] = harness.BrowseHandler != null
? harness.BrowseHandler(description)
: DefaultBrowseResult();
}
return new ValueTask<BrowseResponse>(new BrowseResponse
{
Expand All @@ -180,6 +182,32 @@ public static AliasNameSessionHarness Create()
});
});

sessionMock
.Setup(s => s.BrowseNextAsync(
It.IsAny<RequestHeader>(),
It.IsAny<bool>(),
It.IsAny<ArrayOf<ByteString>>(),
It.IsAny<CancellationToken>()))
.Returns<RequestHeader, bool, ArrayOf<ByteString>, CancellationToken>(
(_, _, continuationPoints, _) =>
{
var results = new BrowseResult[continuationPoints.Count];
for (int i = 0; i < continuationPoints.Count; i++)
{
ByteString continuationPoint = continuationPoints[i];
harness.BrowseNextRequests.Add(continuationPoint);
results[i] = harness.BrowseNextHandler != null
? harness.BrowseNextHandler(continuationPoint)
: DefaultBrowseResult();
}
return new ValueTask<BrowseNextResponse>(new BrowseNextResponse
{
ResponseHeader = new ResponseHeader(),
Results = results.ToArrayOf(),
DiagnosticInfos = default
});
});

return harness;
}

Expand All @@ -192,6 +220,15 @@ private static CallMethodResult DefaultCallResult()
};
}

private static BrowseResult DefaultBrowseResult()
{
return new BrowseResult
{
StatusCode = StatusCodes.Good,
References = Array.Empty<ReferenceDescription>().ToArrayOf()
};
}

/// <summary>
/// Default browse-path resolver — returns a synthetic NodeId
/// (browseName-named string identifier in ns=0) so that
Expand Down
Loading