-
Notifications
You must be signed in to change notification settings - Fork 1
Reservation error handling #265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
e54506e
Refactor ResourceManagerTraceDataHandler implementation
JensVandewalle abafb7f
Apply suggestions from code review
JensVandewalle 70511c2
Add file property support to MediaOps Plan API
JensVandewalle ea3e98b
Merge branch '45799_reservation-backend' into 43980_Implement-file-ty…
JensVandewalle 4e1be16
Stricter file name validation and improved attachment cleanup
JensVandewalle b32d166
Build DOM resource ID map from error lists for quarantine
JensVandewalle b282495
Improve file attachment handling in property settings
JensVandewalle 3054119
Improve file name validation and add unit tests
JensVandewalle 94b2fcd
Initial plan
Copilot 357fbd7
Fix orphaned attachments by reconciling against GetNames instead of t…
Copilot e25cc79
Merge pull request #269 from SkylineCommunications/copilot/fix-code-r…
Copilot 3acaec0
Revert merge of PR #269 (fix-code-review-suggestion)
Copilot c4e4044
Merge branch 'main' into 45799_reservation-backend
ThomasRemmery 3df31e4
Add GetResourceStudioFields for partial resource retrieval
JensVandewalle 527918c
Add DomInstanceSelectStore for select read in unit tests
JensVandewalle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
215 changes: 215 additions & 0 deletions
215
DevPack.Tests/Workflow/Jobs/ResourceManagerTraceDataHandlerTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.