Skip to content

Commit

Permalink
More ext tests (#53)
Browse files Browse the repository at this point in the history
Some tests for a few core services against the .NET server, and a better system for running tests.

Also fixes a pretty subtle bug in the chunker that would produce too large chunks. This code really could use some work, since I believe the data chunk size is constant for symmetric messages in a given channel, but it's fine.
  • Loading branch information
einarmo committed Dec 15, 2024
1 parent 22cc920 commit 45ebd2d
Show file tree
Hide file tree
Showing 18 changed files with 699 additions and 142 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

This is a list of things that are known to be missing, or ideas that could be implemented. Feel free to pick up any of these if you wish to contribute.

- Optimize the `Chunker`. It currently allocates too much, and it has some other inefficiencies.
- Flesh out the server and client SDK with tooling for ease if use.
- Make it even easier to implement custom node managers.
- Write a generic `Browser` for the client, to make it easier to recursively browse node hierarchies. This should be made super flexible, perhaps a trait based approach where the browser is generic over something that handles the response from a browse request and returns what nodes to browse next...
Expand Down
10 changes: 10 additions & 0 deletions dotnet-tests/Common/In.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ public class ShutdownMessage : IInMessage
public InMessageType Type { get; set; } = InMessageType.Shutdown;
}

public class ChangeValueMessage : IInMessage
{
public InMessageType Type { get; set; } = InMessageType.ChangeValue;

public string? NodeId { get; set; }
public string? Value { get; set; }
}

public enum InMessageType
{
Shutdown,
ChangeValue,
}

class InMessageConverter : TaggedUnionConverter<IInMessage, InMessageType>
Expand All @@ -26,6 +35,7 @@ class InMessageConverter : TaggedUnionConverter<IInMessage, InMessageType>
return type switch
{
InMessageType.Shutdown => document.Deserialize<ShutdownMessage>(options),
InMessageType.ChangeValue => document.Deserialize<ChangeValueMessage>(options),
_ => throw new JsonException("Unknown type variant")
};
}
Expand Down
2 changes: 1 addition & 1 deletion dotnet-tests/Common/TaggedUnionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public abstract class TaggedUnionConverter<TInterface, TEnum> : JsonConverter<TI
}
if (!Enum.TryParse<TEnum>(prop, true, out var type))
{
throw new JsonException($"Invalid tag \"{TagName}\"");
throw new JsonException($"Invalid tag \"{prop}\"");
}
return FromEnum(doc, options, type);
}
Expand Down
36 changes: 36 additions & 0 deletions dotnet-tests/TestServer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
using Common;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Configuration;

namespace TestServer;

class CommsLogger : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return null;
}

public bool IsEnabled(LogLevel logLevel)
{
return true;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var toLog = formatter(state, exception);
Comms.LogToRust(toLog);
}
}


internal sealed class Program
{
private static async Task<int> Main(string[] args)
Expand Down Expand Up @@ -50,6 +71,21 @@ private static async Task<int> Main(string[] args)
{
source.Cancel();
}
else if (message is ChangeValueMessage ch)
{
try
{
server.NodeManager.UpdateValue(ch);
}
catch (Exception ex)
{
Comms.Send(new ErrorMessage
{
Message = $"Fatal error setting value: {ex}"
});
return 1;
}
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions dotnet-tests/TestServer/Server.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Common;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Server;

Expand All @@ -8,10 +9,19 @@ public class TestServer : StandardServer
{
TestNodeManager custom = null!;

public TestNodeManager NodeManager => custom;

protected override void OnServerStarting(ApplicationConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);

if (Environment.GetEnvironmentVariable("OPCUA_NET_TEST_SERVER_TRACE") == "true")
{
Utils.SetTraceMask(Utils.TraceMasks.All);
Utils.SetLogLevel(LogLevel.Trace);
Utils.SetLogger(new CommsLogger());
}

base.OnServerStarting(configuration);
}

Expand Down
19 changes: 19 additions & 0 deletions dotnet-tests/TestServer/TestNodeManager.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Buffers.Text;
using Common;
using Opc.Ua;
using Opc.Ua.Server;

Expand All @@ -20,6 +22,21 @@ public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> e
base.CreateAddressSpace(externalReferences);
}

public void UpdateValue(ChangeValueMessage message)
{
var nodeId = NodeId.Parse(message.NodeId);
var bytes = Convert.FromBase64String(message.Value!);

var variant = new BinaryDecoder(bytes, Server.MessageContext).ReadVariant("");

var vb = PredefinedNodes[nodeId];
if (vb is not BaseDataVariableState state) throw new Exception("Not a variable");
state.Value = variant.Value;
state.Timestamp = DateTime.UtcNow;
state.StatusCode = StatusCodes.Good;
state.ClearChangeMasks(SystemContext, false);
}

private void PopulateCore(IDictionary<NodeId, IList<IReference>> externalReferences)
{
var root = CreateObject("CoreBase");
Expand Down Expand Up @@ -71,6 +88,8 @@ private void PopulateCore(IDictionary<NodeId, IList<IReference>> externalReferen

return ServiceResult.Good;
};
AddNodeRelation(mHello, root, ReferenceTypeIds.HasComponent);

AddPredefinedNode(SystemContext, mHello);
}

Expand Down
17 changes: 15 additions & 2 deletions dotnet-tests/external-tests/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ use std::{
time::Duration,
};

use opcua::client::ClientBuilder;
use opcua::{
client::{ClientBuilder, Session},
types::{NodeId, Variant},
};

use crate::common::{spawn_proc, ProcessWrapper};
use crate::common::{spawn_proc, InMessage, ProcessWrapper, UpdateValueMessage};

pub struct ClientTestState {
pub server: ProcessWrapper,
Expand All @@ -22,6 +25,16 @@ impl ClientTestState {

Self { server, handle }
}

pub async fn send_change_message(&self, session: &Session, node_id: NodeId, value: Variant) {
let msg = {
let context = session.context();
let ctx_r = context.read();
InMessage::ChangeValue(UpdateValueMessage::new(node_id, value, &ctx_r.context()))
};

self.server.send_message(msg).await;
}
}

pub static TEST_COUNTER: AtomicU16 = AtomicU16::new(0);
Expand Down
20 changes: 19 additions & 1 deletion dotnet-tests/external-tests/src/common/comms.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::process::Stdio;

use opcua::types::{BinaryEncodable, ByteString, Context, NodeId, Variant};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::{
Expand Down Expand Up @@ -78,9 +79,26 @@ pub struct GeneralMessage {
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case", tag = "type")]
#[serde(rename_all = "snake_case")]
pub struct UpdateValueMessage {
pub node_id: String,
pub value: String,
}

impl UpdateValueMessage {
pub fn new(node_id: NodeId, value: Variant, ctx: &Context) -> Self {
Self {
node_id: node_id.to_string(),
value: ByteString::from(value.encode_to_vec(ctx)).as_base64(),
}
}
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase", tag = "type")]
pub enum InMessage {
Shutdown {},
ChangeValue(UpdateValueMessage),
}

impl ProcessLoop {
Expand Down
2 changes: 2 additions & 0 deletions dotnet-tests/external-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ mod tests;

#[tokio::main]
pub async fn main() {
opcua::console_logging::init();

let runner = Runner::new();
run_client_tests(&runner).await
}
Expand Down
91 changes: 91 additions & 0 deletions dotnet-tests/external-tests/src/tests/client/connect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::sync::Arc;

use opcua::{
client::{IdentityToken, Session},
crypto::SecurityPolicy,
types::{
AttributeId, MessageSecurityMode, ReadValueId, ServerState, TimestampsToReturn, VariableId,
},
};

use crate::{client::ClientTestState, tests::client::with_session, Runner};

async fn test_connect_inner(session: Arc<Session>, _ctx: &mut ClientTestState) {
let read = session
.read(
&[ReadValueId {
node_id: VariableId::Server_ServerStatus_State.into(),
attribute_id: AttributeId::Value as u32,
..Default::default()
}],
TimestampsToReturn::Both,
0.0,
)
.await
.unwrap();
assert_eq!(
read[0].value.clone().unwrap().try_cast_to::<i32>().unwrap(),
ServerState::Running as i32
);
}

async fn test_connect(
policy: SecurityPolicy,
mode: MessageSecurityMode,
ctx: &mut ClientTestState,
) {
with_session(
test_connect_inner,
policy,
mode,
IdentityToken::UserName("test".to_owned(), "pass".to_owned()),
ctx,
)
.await;
}

pub async fn run_connect_tests(runner: &Runner, ctx: &mut ClientTestState) {
for (policy, mode) in [
(SecurityPolicy::None, MessageSecurityMode::None),
(SecurityPolicy::Basic256Sha256, MessageSecurityMode::Sign),
(
SecurityPolicy::Basic256Sha256,
MessageSecurityMode::SignAndEncrypt,
),
(
SecurityPolicy::Aes128Sha256RsaOaep,
MessageSecurityMode::Sign,
),
(
SecurityPolicy::Aes128Sha256RsaOaep,
MessageSecurityMode::SignAndEncrypt,
),
(
SecurityPolicy::Aes256Sha256RsaPss,
MessageSecurityMode::Sign,
),
(
SecurityPolicy::Aes256Sha256RsaPss,
MessageSecurityMode::SignAndEncrypt,
),
// The .NET SDK is hard to use with these, since its configuration around minimum
// required nonce length is really weird.
/*(SecurityPolicy::Basic128Rsa15, MessageSecurityMode::Sign),
(
SecurityPolicy::Basic128Rsa15,
MessageSecurityMode::SignAndEncrypt,
), */
(SecurityPolicy::Basic256, MessageSecurityMode::Sign),
(
SecurityPolicy::Basic256,
MessageSecurityMode::SignAndEncrypt,
),
] {
runner
.run_test(
&format!("Connect {policy}:{mode}"),
test_connect(policy, mode, ctx),
)
.await;
}
}
Loading

0 comments on commit 45ebd2d

Please sign in to comment.