/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using NUnit.Framework;
namespace Opc.Ua.Interop.Tests
{
///
/// Known defects of a peer's stack, read from "expected-differences.json"
/// next to the peer: { "<check or test name>": { "reason": "...",
/// "issue": "<upstream link>" } }. A listed failure is reported as
/// inconclusive with its reason instead of failing the 2.0 tests; a
/// listed check that passes is reported with a warning so the list is
/// pruned once the stack is fixed.
///
public static class ExpectedDifferences
{
public const string FileName = "expected-differences.json";
private static readonly Lazy> s_differences = new(Load);
///
/// The reason a check or test is expected to fail with this peer, or
/// null when it is expected to pass.
///
public static string Reason(string name)
{
if (s_differences.Value.TryGetValue(name, out string? reason))
{
return reason;
}
// A key ending with '*' lists every test whose name starts with
// the rest, e.g. all checks of one security configuration.
foreach (KeyValuePair entry in s_differences.Value)
{
if (entry.Key.EndsWith('*') &&
name.StartsWith(entry.Key.Substring(0, entry.Key.Length - 1), StringComparison.Ordinal))
{
return entry.Value;
}
}
return null!;
}
///
/// Ends a test whose check failed as expected, or warns when a listed
/// check passed.
///
public static void Apply(string name, bool passed, string failure)
{
string reason = Reason(name);
if (reason == null)
{
return;
}
if (passed)
{
Assert.Warn($"'{name}' is listed in {FileName} ({reason}) but passed; remove the entry.");
return;
}
Assert.Inconclusive($"Expected difference of this peer: {reason}{Environment.NewLine}{failure}");
}
///
/// The reason a test of this stack against the peer is expected to
/// fail, looked up by "Fixture.Test" (e.g.
/// "LegacyServerInteropTests.ConnectWithWrongPasswordIsRejected").
///
public static string ReasonForTest(string fixture, string testName)
{
return Reason(fixture + "." + testName);
}
private static Dictionary Load()
{
var differences = new Dictionary(StringComparer.Ordinal);
// Next to the peer, or up to two levels up for peers run from a
// build output folder (milo/target/milo-peer.jar,
// async-opcua/target/release/async-opcua-peer).
string folder = LegacyPeerProcess.PeerDirectory;
string file = Path.Combine(folder, FileName);
for (int level = 0; level < 2 && !File.Exists(file) && Path.GetDirectoryName(folder) != null; level++)
{
folder = Path.GetDirectoryName(folder)!;
file = Path.Combine(folder!, FileName);
}
if (!File.Exists(file))
{
return differences;
}
using var document = JsonDocument.Parse(File.ReadAllText(file));
foreach (JsonProperty entry in document.RootElement.EnumerateObject())
{
string reason = (entry.Value.TryGetProperty("reason", out JsonElement r) ? r.GetString() : string.Empty)!;
if (entry.Value.TryGetProperty("issue", out JsonElement issue))
{
reason += " (" + issue.GetString() + ")";
}
differences[entry.Name] = reason!;
}
return differences;
}
}
}