/* ======================================================================== * Copyright (c) 2005-2026 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.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging.Abstractions; using NUnit.Framework; using Opc.Ua.Vision.OpenUsd; using Opc.Ua.Vision.OpenUsd.Rendering; namespace Opc.Ua.Vision.Tests { /// /// Coverage tests for /// paths that are not exercised by /// and for the internal guard clauses. /// /// /// Three families of paths in this provider are host-topology-dependent /// and therefore cannot be honestly covered by a unit test on this host: /// /// The else branch of the constructor at /// OpenUsdSceneCameraCaptureProvider.cs lines 96-106 (device /// probe failed → m_device = null) is only reachable on hosts /// where every backend probe fails; on this Windows host D3D12 always /// succeeds. The complementary /// method NoBackend at lines 366-378 is only reached from that /// branch, so it inherits the same restriction. See /// /// for the CI-side invariants. /// The full success path of CaptureCore /// (rendering RGBA, PNG-encoding, returning Succeeded) requires /// a well-formed USD stage and OpenUSD plugin tree, and belongs in /// integration tests. /// Every reachable path inside CaptureCore /// (StageOpenFailed, CameraResolveFailed, RenderFailed, /// BlankFrame, EncodingFailed) is unreachable from managed /// test code today: UsdStage.Open tears the test host down inside /// native code for both an existing malformed .usda file and a /// syntactically valid minimal .usda stage. This is a second /// instance of the same "answers instead of refusing" pattern the /// ValidateRequest comment already calls out for missing files — /// filed as a defect below rather than covered here. /// Similarly, DeviceSelector.FormatException /// is only invoked when a backend probe throws; on this host every probe /// succeeds, so the helper is unreachable from a unit test. /// /// [TestFixture] [Category("OpenUsd")] public sealed class VisionOpenUsdCoverageTests { [Test] public void ConstructorReturnsNullPluginPathWhenConfiguredPluginPathDoesNotExist() { string nonExistent = Path.Combine( TestContext.CurrentContext.TestDirectory, "not-a-real-plugin-dir-" + Guid.NewGuid().ToString("N")); var options = new OpenUsdSceneCaptureOptions { PluginPath = nonExistent }; using var provider = new OpenUsdSceneCameraCaptureProvider(options, telemetry: null); Assert.That(provider.PluginPath, Is.Null, "A configured PluginPath that does not exist must resolve to null so " + "the provider falls back to the auto-discovery path."); } [Test] public void ConstructorResolvesConfiguredPluginPathWhenDirectoryExists() { string tempPluginDir = Path.Combine( TestContext.CurrentContext.TestDirectory, "temp-plugin-dir-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempPluginDir); try { var options = new OpenUsdSceneCaptureOptions { PluginPath = tempPluginDir }; using var provider = new OpenUsdSceneCameraCaptureProvider(options, telemetry: null); Assert.That(provider.PluginPath, Is.EqualTo(tempPluginDir), "A configured PluginPath that exists on disk must be returned verbatim."); } finally { try { Directory.Delete(tempPluginDir, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } } } [Test] public async Task CaptureAsyncRefusesLocalStagePathThatNamesNoReadableFile() { using var provider = new OpenUsdSceneCameraCaptureProvider(); string missingPath = Path.Combine( TestContext.CurrentContext.TestDirectory, "does-not-exist-" + Guid.NewGuid().ToString("N") + ".usda"); var request = new SceneCameraCaptureRequest { StageIdentifier = missingPath, Width = 64, Height = 64, Format = SceneCameraImageFormat.Png }; SceneCameraCaptureResult result = await provider.CaptureAsync(request, CancellationToken.None) .ConfigureAwait(false); Assert.Multiple(() => { Assert.That(result.Status, Is.EqualTo(SceneCameraCaptureStatus.InvalidRequest), "A missing local file must be refused in managed code so the native " + "resolver never sees it; otherwise UsdStage.Open can tear the process down."); Assert.That(result.Reason, Is.Not.Null); Assert.That(result.Reason, Does.Contain("does not name a readable file")); Assert.That(result.Image.IsNull, Is.True); }); } [Test] public async Task CaptureAsyncRefusesUnknownImageFormat() { using var provider = new OpenUsdSceneCameraCaptureProvider(); var request = new SceneCameraCaptureRequest { StageIdentifier = "urn:test:stage", Width = 64, Height = 64, Format = (SceneCameraImageFormat)999 }; SceneCameraCaptureResult result = await provider.CaptureAsync(request, CancellationToken.None) .ConfigureAwait(false); Assert.Multiple(() => { Assert.That(result.Status, Is.EqualTo(SceneCameraCaptureStatus.InvalidRequest)); Assert.That(result.Reason, Is.Not.Null); Assert.That(result.Reason, Does.Contain("not supported")); Assert.That(result.Image.IsNull, Is.True); }); } [Test] public async Task CaptureResultEchoesRequestSuppliedTimestampOnFailurePaths() { using var provider = new OpenUsdSceneCameraCaptureProvider(); var ts = new DateTime(2024, 6, 15, 12, 34, 56, DateTimeKind.Utc); var request = new SceneCameraCaptureRequest { StageIdentifier = string.Empty, Width = 32, Height = 32, Format = SceneCameraImageFormat.Png, TimestampUtc = ts }; SceneCameraCaptureResult result = await provider.CaptureAsync(request, CancellationToken.None) .ConfigureAwait(false); Assert.That(result.TimestampUtc, Is.EqualTo(ts), "A caller-supplied TimestampUtc must be echoed back on failure results " + "so the caller can correlate the failure with the request it made."); } [Test] public async Task CaptureResultCarriesBackendDescriptorOnEveryOutcome() { using var provider = new OpenUsdSceneCameraCaptureProvider(); var request = new SceneCameraCaptureRequest { StageIdentifier = string.Empty, Width = 32, Height = 32, Format = SceneCameraImageFormat.Png }; SceneCameraCaptureResult result = await provider.CaptureAsync(request, CancellationToken.None) .ConfigureAwait(false); Assert.Multiple(() => { Assert.That(result.Backend, Is.Not.Null); Assert.That(result.Backend, Is.SameAs(provider.Backend), "Every result must carry the provider's Backend descriptor unchanged so " + "the caller can correlate a failure with the graphics backend it used."); }); } [Test] public void SceneCameraCaptureRequestExposesEveryInitOnlyPropertyForCallers() { var ts = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); var request = new SceneCameraCaptureRequest { StageIdentifier = "urn:test:cam", PrimPath = "/World/Camera", Width = 640, Height = 480, TimeCode = 42.5, Format = SceneCameraImageFormat.Png, TimestampUtc = ts }; Assert.Multiple(() => { Assert.That(request.StageIdentifier, Is.EqualTo("urn:test:cam")); Assert.That(request.PrimPath, Is.EqualTo("/World/Camera")); Assert.That(request.Width, Is.EqualTo(640)); Assert.That(request.Height, Is.EqualTo(480)); Assert.That(request.TimeCode, Is.EqualTo(42.5)); Assert.That(request.Format, Is.EqualTo(SceneCameraImageFormat.Png)); Assert.That(request.TimestampUtc, Is.EqualTo(ts)); }); } [Test] public void ConstructorSucceedsWithPreferSoftwareOptionAndProducesBackendDescriptor() { using var provider = new OpenUsdSceneCameraCaptureProvider( new OpenUsdSceneCaptureOptions { PreferSoftware = true }, telemetry: null); Assert.Multiple(() => { Assert.That(provider.Backend, Is.Not.Null); Assert.That(provider.Backend.Name, Is.Not.Empty, "PreferSoftware=true changes the D3D12 probe order on Windows but must " + "still resolve to a named backend descriptor."); }); } [Test] public void ConstructorSucceedsWithAllowSoftwareFallbackFalseAndProducesBackendDescriptor() { using var provider = new OpenUsdSceneCameraCaptureProvider( new OpenUsdSceneCaptureOptions { AllowSoftwareFallback = false }, telemetry: null); Assert.Multiple(() => { Assert.That(provider.Backend, Is.Not.Null); Assert.That(provider.Backend.Name, Is.Not.Empty, "AllowSoftwareFallback=false drops the WARP probe on Windows but must " + "still resolve to a named backend descriptor when hardware D3D12 is available."); }); } [Test] public void DeviceSelectorTrySelectDeviceThrowsArgumentNullExceptionForNullOptions() { Assert.That(() => DeviceSelector.TrySelectDevice( null!, NullLogger.Instance, out _, out _), Throws.TypeOf()); } [Test] public void DeviceSelectorTrySelectDeviceThrowsArgumentNullExceptionForNullLogger() { Assert.That(() => DeviceSelector.TrySelectDevice( new OpenUsdSceneCaptureOptions(), null!, out _, out _), Throws.TypeOf()); } [Test] public void DeviceSelectorTrySelectDevicePopulatesBackendDescriptorWhenAnyProbeSucceeds() { bool selected = DeviceSelector.TrySelectDevice( new OpenUsdSceneCaptureOptions(), NullLogger.Instance, out SelectedSilkDevice device, out string reason); if (!selected) { Assert.That(reason, Is.Not.Empty, "TrySelectDevice must fill the aggregate reason when every probe fails, " + "so the caller can surface it in a diagnostic."); return; } try { Assert.Multiple(() => { Assert.That(device.Device, Is.Not.Null); Assert.That(device.Backend, Is.Not.Null); Assert.That(device.Backend.IsAvailable, Is.True); Assert.That(device.Backend.Name, Is.Not.Empty); Assert.That(reason, Is.Empty, "aggregateReason must be empty on the success path so the caller " + "does not log a stale unavailable message."); }); } finally { device.Device.Dispose(); } } [Test] public async Task CaptureAsyncPropagatesCancellationRequestedBeforeCall() { using var provider = new OpenUsdSceneCameraCaptureProvider(); using var cts = new CancellationTokenSource(); cts.Cancel(); var request = new SceneCameraCaptureRequest { StageIdentifier = "urn:test:stage", Width = 32, Height = 32, Format = SceneCameraImageFormat.Png }; OperationCanceledException? thrown = null; try { await provider.CaptureAsync(request, cts.Token).ConfigureAwait(false); } catch (OperationCanceledException ex) { thrown = ex; } Assert.That(thrown, Is.Not.Null, "A pre-cancelled token must cancel the capture before touching the render pipeline; " + "the check runs before ValidateRequest so the stage identifier value is irrelevant."); } [Test] public async Task DisposedProviderDisposeIsIdempotentAndFurtherCapturesRejected() { var provider = new OpenUsdSceneCameraCaptureProvider(); provider.Dispose(); Assert.DoesNotThrow(provider.Dispose, "Dispose must be idempotent - a defensive host may call it twice."); var request = new SceneCameraCaptureRequest { StageIdentifier = "urn:test:stage", Width = 32, Height = 32, Format = SceneCameraImageFormat.Png }; ObjectDisposedException? thrown = null; try { await provider.CaptureAsync(request, CancellationToken.None).ConfigureAwait(false); } catch (ObjectDisposedException ex) { thrown = ex; } Assert.That(thrown, Is.Not.Null, "A capture request after Dispose must be refused with ObjectDisposedException, " + "not swallowed and answered as a failure."); } } }