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
Expand Up @@ -15,7 +15,6 @@
<PackageReference Include="Skyline.DataMiner.CICD.Tools.WinEncryptedKeys.Lib" />
<PackageReference Include="Skyline.DataMiner.Core.DataMinerSystem.Common" />
<PackageReference Include="Skyline.DataMiner.Dev.Common" />
<PackageReference Include="Skyline.DataMiner.Files.SLNetTypes" />
<PackageReference Include="Skyline.DataMiner.Utils.SecureCoding.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
215 changes: 215 additions & 0 deletions DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
namespace RT_MediaOps.Plan.Workflow.Jobs
{
using System;
using System.Collections.Generic;
using System.Linq;

using Skyline.DataMiner.Net.Messages;
using Skyline.DataMiner.Net.ResourceManager.Helpers;
using Skyline.DataMiner.Net.ResponseErrorData;
using Skyline.DataMiner.Net.SRM.Capabilities;
using Skyline.DataMiner.Net.SRM.Capacities;
using Skyline.DataMiner.Net.SRM.Quarantine;
using Skyline.DataMiner.Solutions.MediaOps.Plan.API;
using Skyline.DataMiner.Solutions.MediaOps.Plan.Exceptions;
using Skyline.DataMiner.Solutions.MediaOps.Plan.UnitTesting.Simulation;

using ResourcePool = Skyline.DataMiner.Solutions.MediaOps.Plan.API.ResourcePool;
using PlanResource = Skyline.DataMiner.Solutions.MediaOps.Plan.API.Resource;

/// <summary>
/// Deterministic, simulation-backed tests for translating core ResourceManager errors into DevPack job resource errors.
/// A real Resource Studio resource is created so the handler can resolve the core resource id to its DOM counterpart.
/// </summary>
[TestClass]
public sealed class ResourceManagerTraceDataHandlerTests
{
private static (IMediaOpsPlanApi Api, PlanResource Resource) CreateContextWithResource()
{
var dms = MediaOpsPlanSimulation.Create();
var connection = dms.CreateConnection();
var api = connection.GetMediaOpsPlanApi();

var prefix = Guid.NewGuid();

var pool = api.ResourcePools.Create(new ResourcePool { Name = $"{prefix}_Pool" });
pool = api.ResourcePools.Complete(pool);

var resource = new UnmanagedResource { Name = $"{prefix}_Resource" }.AssignToPool(pool);
resource = api.Resources.Create(resource);
resource = api.Resources.Complete(resource);

return (api, resource);
}

[TestMethod]
public void Translate_QuarantineError_EmitsResourceNotAvailableWithDomResourceId()
{
var (api, resource) = CreateContextWithResource();
var reservationId = Guid.NewGuid();

var error = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.ReservationUpdateCausedReservationsToGoToQuarantine,
reservationId,
(Guid?)null,
new List<Guid>())
{
MustBeMovedToQuarantine = new List<QuarantinedUsagesOnSingleReservation>
{
new QuarantinedUsagesOnSingleReservation
{
QuarantinedUsages = new List<QuarantinedResourceUsageDefinition>
{
new QuarantinedResourceUsageDefinition
{
QuarantinedResourceUsage = new ResourceUsageDefinition(resource.CoreResourceId),
},
},
},
},
};

var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);

var result = handler.Translate(new[] { error });

Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
var notAvailable = result[reservationId].ErrorData.OfType<JobResourceNotAvailableError>().Single();
Assert.AreEqual(resource.Id, notAvailable.ResourceId, "Expected the DOM resource id to be reported.");
}

[TestMethod]
public void Translate_ResourceCapacityInvalid_EmitsInvalidCapacityWithIds()
{
var (api, resource) = CreateContextWithResource();
var reservationId = Guid.NewGuid();
var capacityId = Guid.NewGuid();

var error = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.ResourceCapacityInvalid,
reservationId,
resource.CoreResourceId,
new MultiResourceCapacityUsage { CapacityProfileID = capacityId });

var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);

var result = handler.Translate(new[] { error });

Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
var capacityError = result[reservationId].ErrorData.OfType<JobResourceInvalidCapacityError>().Single();
Assert.AreEqual(resource.Id, capacityError.ResourceId, "Expected the DOM resource id to be reported.");
Assert.AreEqual(capacityId, capacityError.CapacityId, "Expected the capacity profile id to be reported.");
}

[TestMethod]
public void Translate_ResourceCapabilityInvalid_EmitsInvalidCapabilityWithIds()
{
var (api, resource) = CreateContextWithResource();
var reservationId = Guid.NewGuid();
var capabilityId = Guid.NewGuid();

var error = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.ResourceCapabilityInvalid,
reservationId,
resource.CoreResourceId,
new ResourceCapabilityUsage { CapabilityProfileID = capabilityId },
"Capability not available.");

var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);

var result = handler.Translate(new[] { error });

Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
var capabilityError = result[reservationId].ErrorData.OfType<JobResourceInvalidCapabilityError>().Single();
Assert.AreEqual(resource.Id, capabilityError.ResourceId, "Expected the DOM resource id to be reported.");
Assert.AreEqual(capabilityId, capabilityError.CapabilityId, "Expected the capability profile id to be reported.");
}

[TestMethod]
public void Translate_UncategorizedError_FallsBackToRawMessage()
{
var (api, _) = CreateContextWithResource();
var reservationId = Guid.NewGuid();

var error = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.UnknownError,
reservationId,
(Guid?)null,
new List<Guid>());

var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);

var result = handler.Translate(new[] { error });

Assert.IsTrue(result.ContainsKey(reservationId), "Expected the raw fallback to be keyed by the reservation id.");
var errorData = result[reservationId].ErrorData.ToList();
Assert.IsFalse(
errorData.OfType<JobResourceError>().Any(),
"Expected no typed job resource error for an uncategorized reason.");
Assert.IsTrue(
errorData.Any(x => x.GetType() == typeof(MediaOpsErrorData)),
"Expected a raw MediaOpsErrorData fallback for an uncategorized reason.");
}

[TestMethod]
public void Translate_KnownAndUnknownErrors_MapsKnownAndAddsRawForUnknown()
{
var (api, resource) = CreateContextWithResource();
var reservationId = Guid.NewGuid();
var capacityId = Guid.NewGuid();

var capacityError = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.ResourceCapacityInvalid,
reservationId,
resource.CoreResourceId,
new MultiResourceCapacityUsage { CapacityProfileID = capacityId });

var unknownError = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.UnknownError,
reservationId,
(Guid?)null,
new List<Guid>());

var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);

var result = handler.Translate(new[] { capacityError, unknownError });

Assert.IsTrue(result.ContainsKey(reservationId), "Expected the translated errors to be keyed by the reservation id.");
var errorData = result[reservationId].ErrorData.ToList();
Assert.AreEqual(
capacityId,
errorData.OfType<JobResourceInvalidCapacityError>().Single().CapacityId,
"Expected the known capacity error to be translated.");
Assert.IsTrue(
errorData.Any(x => x.GetType() == typeof(MediaOpsErrorData)),
"Expected the unknown error to be added as a raw default alongside the translated one.");
}
[TestMethod]
public void Translate_WhenCalledMultipleTimes_DoesNotReturnPreviousResults()
{
var (api, resource) = CreateContextWithResource();
var handler = new ResourceManagerTraceDataHandler((MediaOpsPlanApi)api);

var reservationId1 = Guid.NewGuid();
var reservationId2 = Guid.NewGuid();

var first = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.ResourceCapacityInvalid,
reservationId1,
resource.CoreResourceId,
new MultiResourceCapacityUsage { CapacityProfileID = Guid.NewGuid() });

var second = new ResourceManagerErrorData(
ResourceManagerErrorData.Reason.UnknownError,
reservationId2,
(Guid?)null,
new List<Guid>());

Assert.IsTrue(handler.Translate(new[] { first }).ContainsKey(reservationId1));

var secondResult = handler.Translate(new[] { second });
Assert.IsFalse(secondResult.ContainsKey(reservationId1), "Expected previous translation output not to leak into subsequent calls.");
Assert.IsTrue(secondResult.ContainsKey(reservationId2));
}
}
}
Comment thread
JensVandewalle marked this conversation as resolved.
8 changes: 8 additions & 0 deletions DevPack.UnitTesting/Simulation/SimulatedDms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ internal bool TryHandleMessage(DMSMessage message, out IEnumerable<DMSMessage> r
throw new ArgumentNullException(nameof(message));
}

// A DOM select read only asks for specific fields. The DOM message handler does not support it,
// so it is translated into a regular read and then reduced to the requested fields.
if (DomInstanceSelectStore.TryHandleMessage(message, _domSlNetMessageHandler, out var selectResponse))
{
responses = new[] { selectResponse };
return true;
}

if (_domSlNetMessageHandler.TryHandleMessage(message, out var domResponse))
{
responses = new[] { domResponse };
Expand Down
101 changes: 101 additions & 0 deletions DevPack.UnitTesting/Stores/DomInstanceSelectStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
namespace Skyline.DataMiner.Solutions.MediaOps.Plan.UnitTesting.Stores
{
using System;
using System.Collections.Generic;

using Skyline.DataMiner.Net.Apps.DataMinerObjectModel;
using Skyline.DataMiner.Net.Apps.DataMinerObjectModel.Select;
using Skyline.DataMiner.Net.Apps.ManagerStore.Select;
using Skyline.DataMiner.Net.Messages;
using Skyline.DataMiner.Utils.DOM.UnitTesting;

/// <summary>
/// Handles DOM instance select (partial object) read requests, mirroring how a real DataMiner Agent
/// answers a <see cref="ManagerStoreSelectReadRequest{T}"/>.
/// </summary>
/// <remarks>
/// A select read returns only the requested fields instead of full objects. The agent replies with a
/// <see cref="ManagerStoreCrudResponse{T}"/> whose custom response data holds a <see cref="SelectResult"/>.
/// <see cref="DomSLNetMessageHandler"/> does not support these requests and returns <see langword="null"/>,
/// so the request is translated into a regular read whose results are reduced to the requested fields.
/// </remarks>
internal static class DomInstanceSelectStore
{
public static bool TryHandleMessage(DMSMessage message, DomSLNetMessageHandler domHandler, out DMSMessage response)
{
if (domHandler is null)
{
throw new ArgumentNullException(nameof(domHandler));
}

response = null;

if (!(message is ManagerStoreSelectReadRequest<DomInstance> request))
{
return false;
}

var readRequest = new ManagerStoreReadRequest<DomInstance>(request.Query)
{
ModuleId = request.ModuleId,
};

if (!domHandler.TryHandleMessage(readRequest, out var readResponse)
|| !(readResponse is ManagerStoreCrudResponse<DomInstance> crudResponse))
{
return false;
}

response = CreateResponse(request, crudResponse.Objects ?? new List<DomInstance>());
return true;
}

private static DMSMessage CreateResponse(ManagerStoreSelectReadRequest<DomInstance> request, IEnumerable<DomInstance> instances)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}

var selectedFields = request.SelectedFields ?? new List<SelectedFieldReference>();
var objects = new List<PartialObjectData>();

foreach (var instance in instances)
{
if (instance?.ID is null)
{
continue;
}

var factory = new PartialDomInstanceFactory(instance.ID.ModuleId);
var values = new List<IPartialObjectValue>(selectedFields.Count);

foreach (var selectedField in selectedFields)
{
values.Add(new PartialObjectValue<object>
{
FieldReferenceId = selectedField.Id,
Value = Execute(selectedField, instance),
});
}

objects.Add(factory.GetPartialObjectData(instance.ID, values));
}

return new ManagerStoreCrudResponse<DomInstance>((object)new SelectResult { Objects = objects });
}

private static object Execute(SelectedFieldReference selectedField, DomInstance instance)
{
try
{
return selectedField.SerializableExposer?.Exposer?.execute(instance);
}
catch
{
// Mirror the agent's fail-safe behavior: a field that cannot be read is returned as no value.
return null;
}
}
}
}
Loading
Loading