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
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2026 the Eclipse Milo Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

package org.eclipse.milo.opcua.sdk.client.typetree;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.List;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseNextResponse;
import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
import org.junit.jupiter.api.Test;

class ClientBrowseUtilsTest {

@Test
void releasesContinuationPointWhenBrowseNextLimitIsReached() throws UaException {
var client = mock(OpcUaClient.class);
var response = mock(BrowseNextResponse.class);
var result = mock(BrowseResult.class);
var continuationPoint = ByteString.of(new byte[] {1, 2, 3, 4});

when(client.browseNext(false, List.of(continuationPoint))).thenReturn(response);
when(response.getResults()).thenReturn(new BrowseResult[] {result});
when(result.getContinuationPoint()).thenReturn(continuationPoint);

assertThrows(
UaException.class, () -> ClientBrowseUtils.maybeBrowseNext(client, continuationPoint));

verify(client, times(1000)).browseNext(false, List.of(continuationPoint));
verify(client).browseNext(true, List.of(continuationPoint));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 the Eclipse Milo Authors
* Copyright (c) 2026 the Eclipse Milo Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -17,6 +17,8 @@
import org.eclipse.milo.opcua.sdk.core.typetree.DataTypeTree;
import org.eclipse.milo.opcua.sdk.test.AbstractClientServerTest;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.encoding.EncodingContext;
import org.eclipse.milo.opcua.stack.core.types.DataTypeManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

Expand Down Expand Up @@ -62,6 +64,21 @@ void setDataTypeTreeFactoryResetsCache() throws Exception {
assertInstanceOf(LazyClientDataTypeTree.class, tree2);
}

@Test
void readDataTypeTreeResetsDerivedCaches() throws Exception {
DataTypeTree treeBefore = client.getDataTypeTree();
DataTypeManager managerBefore = client.getDynamicDataTypeManager();
EncodingContext contextBefore = client.getDynamicEncodingContext();

DataTypeTree treeAfter = client.readDataTypeTree();

// The dynamic DataTypeManager and EncodingContext hold references to the tree they were
// created against, so refreshing the tree must rebuild them too.
assertNotSame(treeBefore, treeAfter);
assertNotSame(managerBefore, client.getDynamicDataTypeManager());
assertNotSame(contextBefore, client.getDynamicEncodingContext());
}

@Test
void customFactoryIsUsed() throws Exception {
var customTree = new LazyClientDataTypeTree(client);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 the Eclipse Milo Authors
* Copyright (c) 2026 the Eclipse Milo Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -14,6 +14,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.atomic.AtomicInteger;
Expand All @@ -36,13 +37,17 @@ public class LazyClientDataTypeTreeTest extends AbstractDataTypeTreeTest {

@Override
protected DataTypeTree getDataTypeTree() {
return newLazyTree();
}

private LazyClientDataTypeTree newLazyTree() {
return new LazyClientDataTypeTree(client);
}

@Test
void initiallyOnlyContainsBaseDataType() {
// Create a fresh tree to test the initial state
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// BaseDataType should be resolved (it's the root)
assertTrue(freshTree.isResolved(NodeIds.BaseDataType));
Expand All @@ -55,7 +60,7 @@ void initiallyOnlyContainsBaseDataType() {

@Test
void resolvesTypeOnDemand() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// Int32 not resolved initially
assertFalse(freshTree.isResolved(NodeIds.Int32));
Expand All @@ -76,7 +81,7 @@ void resolvesTypeOnDemand() {

@Test
void resolvesStructuredTypes() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// Query a structured type
DataType xvType = freshTree.getDataType(NodeIds.XVType);
Expand All @@ -96,7 +101,7 @@ void resolvesStructuredTypes() {

@Test
void isSubtypeOfWorksWithLazyResolution() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// Neither Int32 nor Integer are resolved yet
assertFalse(freshTree.isResolved(NodeIds.Int32));
Expand All @@ -113,7 +118,7 @@ void isSubtypeOfWorksWithLazyResolution() {

@Test
void containsTypeTriggersResolution() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// Double not resolved initially
assertFalse(freshTree.isResolved(NodeIds.Double));
Expand All @@ -127,7 +132,7 @@ void containsTypeTriggersResolution() {

@Test
void cachesResolvedTypes() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// First query - triggers resolution
DataType first = freshTree.getDataType(NodeIds.Int32);
Expand All @@ -144,7 +149,7 @@ void cachesResolvedTypes() {
@Test
void lazyTreeMatchesEagerTreeForResolvedTypes() throws UaException {
DataTypeTree eagerTree = DataTypeTreeBuilder.build(client);
var lazyTestTree = new LazyClientDataTypeTree(client);
var lazyTestTree = newLazyTree();

// Test a variety of types
NodeId[] typesToTest = {
Expand Down Expand Up @@ -188,9 +193,8 @@ void lazyTreeMatchesEagerTreeForResolvedTypes() throws UaException {
}

@Test
void diagnosticTestForStructure() throws Exception {
// Test that Structure can be resolved
var freshTree = new LazyClientDataTypeTree(client);
void getTypeTriggersLazyResolution() {
var freshTree = newLazyTree();

// BaseDataType should be the only resolved type initially
assertTrue(freshTree.isResolved(NodeIds.BaseDataType), "BaseDataType should be pre-loaded");
Expand All @@ -205,26 +209,29 @@ void diagnosticTestForStructure() throws Exception {

@Test
void clearFailedResolutionsAllowsRetry() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// Try to resolve a non-existent type
// Resolving a non-existent type fails and leaves the type unresolved
NodeId fakeTypeId = new NodeId(999, "FakeDataType");
DataType result = freshTree.getDataType(fakeTypeId);
assertNull(freshTree.getDataType(fakeTypeId));
assertFalse(freshTree.isResolved(fakeTypeId));

// Should return null
assertTrue(result == null || !freshTree.isResolved(fakeTypeId));
// Resolve a real type as a control for the clear below
assertNotNull(freshTree.getDataType(NodeIds.Int32));

// Clear failed resolutions
freshTree.clearFailedResolutions();

// Now it can be attempted again (will still fail, but the point is it's retried)
result = freshTree.getDataType(fakeTypeId);
assertTrue(result == null || !freshTree.isResolved(fakeTypeId));
// Clearing failed resolutions retains resolved types
assertTrue(freshTree.isResolved(NodeIds.Int32));

// The failed type can be attempted again (it fails again against this server)
assertNull(freshTree.getDataType(fakeTypeId));
assertFalse(freshTree.isResolved(fakeTypeId));
}

@Test
void getRootReturnsSnapshot() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// Initially only BaseDataType is in the tree
Tree<DataType> snapshot1 = freshTree.getRoot();
Expand Down Expand Up @@ -253,7 +260,7 @@ void getRootReturnsSnapshot() {

@Test
void getRootSnapshotIsTraversable() {
var freshTree = new LazyClientDataTypeTree(client);
var freshTree = newLazyTree();

// Resolve a few types to build up the tree
freshTree.getDataType(NodeIds.Int32);
Expand Down
Loading
Loading