feat(deploymentmanager): DeployAreaAsync bulk-deploys an area subtree through the shared DeploySiteAsync core; PreviewSubtreeDeployAsync returns confirm counts (Gitea #40)
DeploySiteAsync's body moves into a private core taking the site plus an optional area-id filter; site deploy behaviour is unchanged. The subtree is one in-memory ParentAreaId walk (cycle-safe) shared by the deploy and the preview. DeploymentService gains an ITemplateEngineRepository dependency for the area lookups.
This commit is contained in:
@@ -219,6 +219,30 @@ runs for it. An escape from phase 1 or phase 3 unwinds every not-yet-finalised
|
|||||||
entry the same way: `Failed` status plus lock release, so no instance touched by
|
entry the same way: `Failed` status plus lock release, so no instance touched by
|
||||||
a cancelled bulk deploy can be left holding its lock.
|
a cancelled bulk deploy can be left holding its lock.
|
||||||
|
|
||||||
|
### Area-subtree deployment (`DeployAreaAsync`, Gitea #40)
|
||||||
|
|
||||||
|
`DeployAreaAsync(areaId, user, force)` is the same bulk deploy narrowed to one
|
||||||
|
area **subtree** — the area itself plus every area nested beneath it at any
|
||||||
|
depth — surfaced as the `MgmtDeployArea` management command, the CLI
|
||||||
|
`deploy area --id`, and the Central UI Topology page's **Deploy all…** context
|
||||||
|
item on area nodes (the site node's item calls `DeploySiteAsync`). Both entry
|
||||||
|
points run one private core (`DeployInstancesOfSiteAsync`) that loads the site's
|
||||||
|
instances, keeps only those whose `AreaId` is in the subtree (area-less instances
|
||||||
|
belong to the whole-site scope only), and then applies everything above
|
||||||
|
unchanged: the Disabled skip (even under `force`), the up-to-date skip unless
|
||||||
|
`force`, the three phases, not-all-or-nothing, and the same
|
||||||
|
`SiteDeploymentSummary` with `SkippedInstances`. An unknown area is a failure
|
||||||
|
result, not an empty success.
|
||||||
|
|
||||||
|
The subtree is resolved from ONE read of the site's areas
|
||||||
|
(`GetAreasBySiteIdAsync`) walked in memory over `ParentAreaId`; each area is
|
||||||
|
visited at most once, so a corrupt parent chain that loops cannot hang the walk.
|
||||||
|
`PreviewSubtreeDeployAsync(siteId, areaId?)` shares that walk to return the
|
||||||
|
confirm dialog's `SubtreeDeployPreview(Total, Disabled)` counts (`areaId` null =
|
||||||
|
the whole site; an area of another site is a failure) — a plain DB read with no
|
||||||
|
staleness probe, so the up-to-date count is only known once the deploy reports
|
||||||
|
its skipped rows.
|
||||||
|
|
||||||
`DeployInstanceAsync` is composed from the same three phase helpers with a batch of
|
`DeployInstanceAsync` is composed from the same three phase helpers with a batch of
|
||||||
one, so the two entry points cannot drift on deployment identity, idempotency, lock
|
one, so the two entry points cannot drift on deployment identity, idempotency, lock
|
||||||
coverage, or optimistic concurrency.
|
coverage, or optimistic concurrency.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
||||||
@@ -38,6 +39,7 @@ public class DeploymentService
|
|||||||
{
|
{
|
||||||
private readonly IDeploymentManagerRepository _repository;
|
private readonly IDeploymentManagerRepository _repository;
|
||||||
private readonly ISiteRepository _siteRepository;
|
private readonly ISiteRepository _siteRepository;
|
||||||
|
private readonly ITemplateEngineRepository _templateRepository;
|
||||||
private readonly IFlatteningPipeline _flatteningPipeline;
|
private readonly IFlatteningPipeline _flatteningPipeline;
|
||||||
private readonly CommunicationService _communicationService;
|
private readonly CommunicationService _communicationService;
|
||||||
private readonly OperationLockManager _lockManager;
|
private readonly OperationLockManager _lockManager;
|
||||||
@@ -96,6 +98,11 @@ public class DeploymentService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="repository">Repository for deployment manager data access.</param>
|
/// <param name="repository">Repository for deployment manager data access.</param>
|
||||||
/// <param name="siteRepository">Repository for site data access.</param>
|
/// <param name="siteRepository">Repository for site data access.</param>
|
||||||
|
/// <param name="templateRepository">
|
||||||
|
/// Repository for area lookups — the area-subtree bulk deploy
|
||||||
|
/// (<see cref="DeployAreaAsync"/>) and its pre-confirm counts resolve the area
|
||||||
|
/// tree through it.
|
||||||
|
/// </param>
|
||||||
/// <param name="flatteningPipeline">Pipeline for flattening and validating template configurations.</param>
|
/// <param name="flatteningPipeline">Pipeline for flattening and validating template configurations.</param>
|
||||||
/// <param name="communicationService">Service for cross-cluster communication with sites.</param>
|
/// <param name="communicationService">Service for cross-cluster communication with sites.</param>
|
||||||
/// <param name="lockManager">Manager for per-instance operation locks.</param>
|
/// <param name="lockManager">Manager for per-instance operation locks.</param>
|
||||||
@@ -119,6 +126,7 @@ public class DeploymentService
|
|||||||
public DeploymentService(
|
public DeploymentService(
|
||||||
IDeploymentManagerRepository repository,
|
IDeploymentManagerRepository repository,
|
||||||
ISiteRepository siteRepository,
|
ISiteRepository siteRepository,
|
||||||
|
ITemplateEngineRepository templateRepository,
|
||||||
IFlatteningPipeline flatteningPipeline,
|
IFlatteningPipeline flatteningPipeline,
|
||||||
CommunicationService communicationService,
|
CommunicationService communicationService,
|
||||||
OperationLockManager lockManager,
|
OperationLockManager lockManager,
|
||||||
@@ -132,6 +140,7 @@ public class DeploymentService
|
|||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_siteRepository = siteRepository;
|
_siteRepository = siteRepository;
|
||||||
|
_templateRepository = templateRepository;
|
||||||
_flatteningPipeline = flatteningPipeline;
|
_flatteningPipeline = flatteningPipeline;
|
||||||
_communicationService = communicationService;
|
_communicationService = communicationService;
|
||||||
_lockManager = lockManager;
|
_lockManager = lockManager;
|
||||||
@@ -938,7 +947,37 @@ public class DeploymentService
|
|||||||
if (site == null)
|
if (site == null)
|
||||||
return Result<SiteDeploymentSummary>.Failure($"Site with ID {siteId} not found.");
|
return Result<SiteDeploymentSummary>.Failure($"Site with ID {siteId} not found.");
|
||||||
|
|
||||||
var instances = await _siteRepository.GetInstancesBySiteIdAsync(siteId, cancellationToken);
|
return await DeployInstancesOfSiteAsync(site, areaFilter: null, scope: "site", user, force, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The body of every bulk deploy — whole site (<see cref="DeploySiteAsync"/>)
|
||||||
|
/// or one area subtree (<see cref="DeployAreaAsync"/>). Loads the site's
|
||||||
|
/// instances, narrows them to <paramref name="areaFilter"/> when one is given,
|
||||||
|
/// then applies the Disabled skip, the up-to-date pre-filter and the three
|
||||||
|
/// phases exactly as documented on <see cref="DeploySiteAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="site">The resolved site whose instances are deployed.</param>
|
||||||
|
/// <param name="areaFilter">
|
||||||
|
/// Area ids an instance's <see cref="Instance.AreaId"/> must be in to be part of
|
||||||
|
/// the batch, or null for every instance at the site (area-less ones included).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="scope">Human-readable batch scope for the log lines (e.g. <c>site</c>, <c>area 12</c>).</param>
|
||||||
|
/// <param name="user">The username initiating the deployment.</param>
|
||||||
|
/// <param name="force">Suppresses the up-to-date filter; never the Disabled one.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The per-instance result matrix.</returns>
|
||||||
|
private async Task<Result<SiteDeploymentSummary>> DeployInstancesOfSiteAsync(
|
||||||
|
Site site,
|
||||||
|
IReadOnlySet<int>? areaFilter,
|
||||||
|
string scope,
|
||||||
|
string user,
|
||||||
|
bool force,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IReadOnlyList<Instance> instances = await _siteRepository.GetInstancesBySiteIdAsync(site.Id, cancellationToken);
|
||||||
|
if (areaFilter != null)
|
||||||
|
instances = instances.Where(i => i.AreaId is { } areaId && areaFilter.Contains(areaId)).ToList();
|
||||||
if (instances.Count == 0)
|
if (instances.Count == 0)
|
||||||
return Result<SiteDeploymentSummary>.Success(new SiteDeploymentSummary(site.SiteIdentifier, [], 0, 0, []));
|
return Result<SiteDeploymentSummary>.Success(new SiteDeploymentSummary(site.SiteIdentifier, [], 0, 0, []));
|
||||||
|
|
||||||
@@ -1017,8 +1056,8 @@ public class DeploymentService
|
|||||||
// fails only this instance. Recording it and moving on is what
|
// fails only this instance. Recording it and moving on is what
|
||||||
// makes a bulk deploy usable while individual instances are busy.
|
// makes a bulk deploy usable while individual instances are busy.
|
||||||
_logger.LogWarning(ex,
|
_logger.LogWarning(ex,
|
||||||
"Preparing instance {Instance} for bulk deployment of site {SiteId} failed",
|
"Preparing instance {Instance} for bulk deployment of site {SiteId} ({Scope}) failed",
|
||||||
instance.UniqueName, site.SiteIdentifier);
|
instance.UniqueName, site.SiteIdentifier, scope);
|
||||||
results.Add(new InstanceDeploymentResult(
|
results.Add(new InstanceDeploymentResult(
|
||||||
instance.Id, instance.UniqueName, null, false, ex.Message));
|
instance.Id, instance.UniqueName, null, false, ex.Message));
|
||||||
continue;
|
continue;
|
||||||
@@ -1101,12 +1140,12 @@ public class DeploymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Bulk deployment of site {SiteId} complete: {SuccessCount} succeeded, {FailureCount} failed, " +
|
"Bulk deployment of site {SiteId} ({Scope}) complete: {SuccessCount} succeeded, {FailureCount} failed, " +
|
||||||
"{DisabledCount} skipped as Disabled, {UpToDateCount} skipped as up-to-date (force={Force}) " +
|
"{DisabledCount} skipped as Disabled, {UpToDateCount} skipped as up-to-date (force={Force}) " +
|
||||||
"({ChainLoads} template-chain load(s) across {InstanceCount} deployed instance(s); " +
|
"({ChainLoads} template-chain load(s) across {InstanceCount} deployed instance(s); " +
|
||||||
"up-to-date pre-filter probed {ProbedCount} instance(s) in {PreFilterMs} ms with " +
|
"up-to-date pre-filter probed {ProbedCount} instance(s) in {PreFilterMs} ms with " +
|
||||||
"{PreFilterChainLoads} of those chain load(s))",
|
"{PreFilterChainLoads} of those chain load(s))",
|
||||||
site.SiteIdentifier, summary.SuccessCount, summary.FailureCount, disabledCount, upToDateCount,
|
site.SiteIdentifier, scope, summary.SuccessCount, summary.FailureCount, disabledCount, upToDateCount,
|
||||||
force, session.ChainLoads, deployable.Count,
|
force, session.ChainLoads, deployable.Count,
|
||||||
preFilterProbed, preFilterTimer.ElapsedMilliseconds, preFilterChainLoads);
|
preFilterProbed, preFilterTimer.ElapsedMilliseconds, preFilterChainLoads);
|
||||||
|
|
||||||
@@ -1124,12 +1163,25 @@ public class DeploymentService
|
|||||||
/// <param name="force">Deploy up-to-date instances too; never re-enables Disabled ones.</param>
|
/// <param name="force">Deploy up-to-date instances too; never re-enables Disabled ones.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
/// <returns>The same per-instance result matrix as a site deploy, or a failure when the area cannot be resolved.</returns>
|
/// <returns>The same per-instance result matrix as a site deploy, or a failure when the area cannot be resolved.</returns>
|
||||||
public Task<Result<SiteDeploymentSummary>> DeployAreaAsync(
|
public async Task<Result<SiteDeploymentSummary>> DeployAreaAsync(
|
||||||
int areaId,
|
int areaId,
|
||||||
string user,
|
string user,
|
||||||
bool force = false,
|
bool force = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> throw new NotImplementedException();
|
{
|
||||||
|
var area = await _templateRepository.GetAreaByIdAsync(areaId, cancellationToken);
|
||||||
|
if (area == null)
|
||||||
|
return Result<SiteDeploymentSummary>.Failure($"Area with ID {areaId} not found.");
|
||||||
|
|
||||||
|
var site = await _siteRepository.GetSiteByIdAsync(area.SiteId, cancellationToken);
|
||||||
|
if (site == null)
|
||||||
|
return Result<SiteDeploymentSummary>.Failure(
|
||||||
|
$"Site with ID {area.SiteId} (owner of area {areaId}) not found.");
|
||||||
|
|
||||||
|
var subtree = await GetAreaSubtreeIdsAsync(area, cancellationToken);
|
||||||
|
return await DeployInstancesOfSiteAsync(
|
||||||
|
site, subtree, scope: $"area {area.Id} '{area.Name}'", user, force, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cheap pre-confirm counts for a site or area subtree deploy (Gitea #40): how
|
/// Cheap pre-confirm counts for a site or area subtree deploy (Gitea #40): how
|
||||||
@@ -1140,11 +1192,71 @@ public class DeploymentService
|
|||||||
/// <param name="areaId">The subtree's root area, or null for the whole site.</param>
|
/// <param name="areaId">The subtree's root area, or null for the whole site.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
/// <returns>The counts, or a failure when the site or area cannot be resolved.</returns>
|
/// <returns>The counts, or a failure when the site or area cannot be resolved.</returns>
|
||||||
public Task<Result<SubtreeDeployPreview>> PreviewSubtreeDeployAsync(
|
public async Task<Result<SubtreeDeployPreview>> PreviewSubtreeDeployAsync(
|
||||||
int siteId,
|
int siteId,
|
||||||
int? areaId,
|
int? areaId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> throw new NotImplementedException();
|
{
|
||||||
|
var site = await _siteRepository.GetSiteByIdAsync(siteId, cancellationToken);
|
||||||
|
if (site == null)
|
||||||
|
return Result<SubtreeDeployPreview>.Failure($"Site with ID {siteId} not found.");
|
||||||
|
|
||||||
|
IReadOnlySet<int>? subtree = null;
|
||||||
|
if (areaId is { } id)
|
||||||
|
{
|
||||||
|
var area = await _templateRepository.GetAreaByIdAsync(id, cancellationToken);
|
||||||
|
if (area == null)
|
||||||
|
return Result<SubtreeDeployPreview>.Failure($"Area with ID {id} not found.");
|
||||||
|
if (area.SiteId != siteId)
|
||||||
|
return Result<SubtreeDeployPreview>.Failure(
|
||||||
|
$"Area with ID {id} does not belong to site with ID {siteId}.");
|
||||||
|
subtree = await GetAreaSubtreeIdsAsync(area, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var instances = await _siteRepository.GetInstancesBySiteIdAsync(siteId, cancellationToken);
|
||||||
|
var total = 0;
|
||||||
|
var disabled = 0;
|
||||||
|
foreach (var instance in instances)
|
||||||
|
{
|
||||||
|
if (subtree != null && !(instance.AreaId is { } a && subtree.Contains(a)))
|
||||||
|
continue;
|
||||||
|
total++;
|
||||||
|
if (instance.State == InstanceState.Disabled)
|
||||||
|
disabled++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result<SubtreeDeployPreview>.Success(new SubtreeDeployPreview(total, disabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ids of <paramref name="root"/> and every area nested beneath it at any
|
||||||
|
/// depth, from ONE read of the site's areas walked in memory over
|
||||||
|
/// <see cref="Area.ParentAreaId"/>. Shared by <see cref="DeployAreaAsync"/> and
|
||||||
|
/// <see cref="PreviewSubtreeDeployAsync"/> so the confirm dialog's counts and the
|
||||||
|
/// deploy that follows always agree on the subtree. A corrupt parent chain that
|
||||||
|
/// loops is tolerated: each area is visited at most once.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<IReadOnlySet<int>> GetAreaSubtreeIdsAsync(Area root, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var areas = await _templateRepository.GetAreasBySiteIdAsync(root.SiteId, cancellationToken);
|
||||||
|
var childrenByParent = areas
|
||||||
|
.Where(a => a.ParentAreaId.HasValue)
|
||||||
|
.ToLookup(a => a.ParentAreaId!.Value, a => a.Id);
|
||||||
|
|
||||||
|
var subtree = new HashSet<int> { root.Id };
|
||||||
|
var pending = new Queue<int>();
|
||||||
|
pending.Enqueue(root.Id);
|
||||||
|
while (pending.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var childId in childrenByParent[pending.Dequeue()])
|
||||||
|
{
|
||||||
|
if (subtree.Add(childId))
|
||||||
|
pending.Enqueue(childId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtree;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when a bulk site deploy should leave this instance alone because the
|
/// True when a bulk site deploy should leave this instance alone because the
|
||||||
|
|||||||
@@ -0,0 +1,409 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Akka.Actor;
|
||||||
|
using Akka.TestKit.Xunit2;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NSubstitute;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Area-subtree bulk deployment (Gitea #40). <c>DeployAreaAsync</c> is
|
||||||
|
/// <c>DeploySiteAsync</c> narrowed to the instances under one area and all of its
|
||||||
|
/// descendants, so these tests pin the narrowing — descendants in, siblings and
|
||||||
|
/// ancestors out — plus the bulk rules it must inherit unchanged (Disabled always
|
||||||
|
/// skipped, up-to-date skipped unless forced). They also cover the cheap
|
||||||
|
/// pre-confirm counts <c>PreviewSubtreeDeployAsync</c> returns.
|
||||||
|
/// </summary>
|
||||||
|
public class DeployAreaAsyncTests : TestKit
|
||||||
|
{
|
||||||
|
private const int SiteId = 1;
|
||||||
|
private const int OtherSiteId = 2;
|
||||||
|
|
||||||
|
// Site 1: Plant(100) ─┬─ Line1(101) ── Cell(102)
|
||||||
|
// └─ Line2(103)
|
||||||
|
// Warehouse(200)
|
||||||
|
// Site 2: Remote(900)
|
||||||
|
private const int PlantArea = 100;
|
||||||
|
private const int Line1Area = 101;
|
||||||
|
private const int CellArea = 102;
|
||||||
|
private const int Line2Area = 103;
|
||||||
|
private const int WarehouseArea = 200;
|
||||||
|
private const int RemoteArea = 900;
|
||||||
|
|
||||||
|
private readonly IDeploymentManagerRepository _repo = Substitute.For<IDeploymentManagerRepository>();
|
||||||
|
private readonly IFlatteningPipeline _pipeline = Substitute.For<IFlatteningPipeline>();
|
||||||
|
private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
|
||||||
|
private readonly ITemplateEngineRepository _templateRepo = Substitute.For<ITemplateEngineRepository>();
|
||||||
|
private readonly IAuditService _audit = Substitute.For<IAuditService>();
|
||||||
|
|
||||||
|
private readonly Dictionary<int, string> _deployedHashes = new();
|
||||||
|
private readonly Dictionary<int, DeploymentRecord> _currentRecords = new();
|
||||||
|
private readonly OperationLockManager _lockManager = new();
|
||||||
|
private List<Area> _siteAreas = new();
|
||||||
|
|
||||||
|
/// <summary>Initializes the test class with the area tree above and substituted repositories.</summary>
|
||||||
|
public DeployAreaAsyncTests()
|
||||||
|
{
|
||||||
|
_siteRepo.GetSiteByIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => ci.ArgAt<int>(0) is SiteId or OtherSiteId
|
||||||
|
? new Site($"Site {ci.ArgAt<int>(0)}", $"site-{ci.ArgAt<int>(0)}") { Id = ci.ArgAt<int>(0) }
|
||||||
|
: null);
|
||||||
|
|
||||||
|
_pipeline.CreateSession().Returns(_ => new FlattenSession(new TemplateGraphWatermark()));
|
||||||
|
|
||||||
|
_repo.GetDeployedRevisionHashesAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns(_ => (IReadOnlyDictionary<int, string>)new Dictionary<int, string>(_deployedHashes));
|
||||||
|
_repo.GetCurrentDeploymentStatusesAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(ci => (IReadOnlyDictionary<int, DeploymentRecord>)ci.ArgAt<IReadOnlyCollection<int>>(0)
|
||||||
|
.Where(_currentRecords.ContainsKey)
|
||||||
|
.ToDictionary(id => id, id => _currentRecords[id]));
|
||||||
|
|
||||||
|
ArrangeAreas(
|
||||||
|
Area(PlantArea, SiteId, null),
|
||||||
|
Area(Line1Area, SiteId, PlantArea),
|
||||||
|
Area(CellArea, SiteId, Line1Area),
|
||||||
|
Area(Line2Area, SiteId, PlantArea),
|
||||||
|
Area(WarehouseArea, SiteId, null));
|
||||||
|
|
||||||
|
_templateRepo.GetAreaByIdAsync(RemoteArea, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Area(RemoteArea, OtherSiteId, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Area Area(int id, int siteId, int? parentId) =>
|
||||||
|
new($"Area-{id}") { Id = id, SiteId = siteId, ParentAreaId = parentId };
|
||||||
|
|
||||||
|
/// <summary>Replaces site 1's area set (by-id and by-site lookups both answer from it).</summary>
|
||||||
|
private void ArrangeAreas(params Area[] areas)
|
||||||
|
{
|
||||||
|
_siteAreas = areas.ToList();
|
||||||
|
foreach (var area in areas)
|
||||||
|
_templateRepo.GetAreaByIdAsync(area.Id, Arg.Any<CancellationToken>()).Returns(area);
|
||||||
|
_templateRepo.GetAreasBySiteIdAsync(SiteId, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(_ => (IReadOnlyList<Area>)_siteAreas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seeds one deployable instance per entry, in the given area (null = no area).
|
||||||
|
/// Instance ids are 1-based in argument order and named <c>Inst-NN</c>.
|
||||||
|
/// </summary>
|
||||||
|
private List<Instance> ArrangeInstances(params int?[] areaIds)
|
||||||
|
{
|
||||||
|
var instances = new List<Instance>();
|
||||||
|
for (var i = 1; i <= areaIds.Length; i++)
|
||||||
|
{
|
||||||
|
var instance = new Instance($"Inst-{i:00}")
|
||||||
|
{
|
||||||
|
Id = i,
|
||||||
|
SiteId = SiteId,
|
||||||
|
TemplateId = 10,
|
||||||
|
AreaId = areaIds[i - 1],
|
||||||
|
State = InstanceState.NotDeployed
|
||||||
|
};
|
||||||
|
instances.Add(instance);
|
||||||
|
|
||||||
|
_repo.GetInstanceByIdAsync(i, Arg.Any<CancellationToken>()).Returns(instance);
|
||||||
|
_repo.GetCurrentDeploymentStatusAsync(i, Arg.Any<CancellationToken>()).Returns((DeploymentRecord?)null);
|
||||||
|
|
||||||
|
var config = new FlattenedConfiguration { InstanceUniqueName = instance.UniqueName };
|
||||||
|
_pipeline.FlattenAndValidateAsync(
|
||||||
|
i, Arg.Any<CancellationToken>(), Arg.Any<bool>(), Arg.Any<FlattenSession?>())
|
||||||
|
.Returns(Result<FlatteningPipelineResult>.Success(
|
||||||
|
new FlatteningPipelineResult(config, $"sha256:{i}", ValidationResult.Success())));
|
||||||
|
}
|
||||||
|
|
||||||
|
_siteRepo.GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>()).Returns(instances);
|
||||||
|
return instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Makes the instance <c>Enabled</c> with a deployed snapshot whose hash the probe recomputes.</summary>
|
||||||
|
private void ArrangeUpToDate(Instance instance)
|
||||||
|
{
|
||||||
|
var hash = $"sha256:{instance.Id}";
|
||||||
|
instance.State = InstanceState.Enabled;
|
||||||
|
|
||||||
|
_deployedHashes[instance.Id] = hash;
|
||||||
|
var prior = new DeploymentRecord($"prior-{instance.Id}", "admin")
|
||||||
|
{
|
||||||
|
InstanceId = instance.Id,
|
||||||
|
Status = DeploymentStatus.Success,
|
||||||
|
RevisionHash = hash,
|
||||||
|
};
|
||||||
|
_currentRecords[instance.Id] = prior;
|
||||||
|
_repo.GetCurrentDeploymentStatusAsync(instance.Id, Arg.Any<CancellationToken>()).Returns(prior);
|
||||||
|
_pipeline.ComputeRevisionHashAsync(instance.Id, Arg.Any<FlattenSession?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Result<string>.Success(hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
private (DeploymentService Service, ConcurrentBag<string> Applied) CreateService()
|
||||||
|
{
|
||||||
|
var applied = new ConcurrentBag<string>();
|
||||||
|
var commActor = Sys.ActorOf(Props.Create(() => new RecordingSiteActor(applied)));
|
||||||
|
|
||||||
|
var comms = new CommunicationService(
|
||||||
|
Options.Create(new CommunicationOptions { DeploymentTimeout = TimeSpan.FromSeconds(30) }),
|
||||||
|
NullLogger<CommunicationService>.Instance);
|
||||||
|
comms.SetCommunicationActor(commActor);
|
||||||
|
|
||||||
|
var options = Options.Create(new DeploymentManagerOptions
|
||||||
|
{
|
||||||
|
OperationLockTimeout = TimeSpan.FromSeconds(5),
|
||||||
|
SiteDeploymentMaxParallelism = 3,
|
||||||
|
SiteDeploymentTimeoutPerInstance = TimeSpan.FromSeconds(30),
|
||||||
|
SiteDeploymentReconcileTimeout = TimeSpan.FromSeconds(5)
|
||||||
|
});
|
||||||
|
|
||||||
|
var service = new DeploymentService(
|
||||||
|
_repo, _siteRepo, _templateRepo, _pipeline, comms, _lockManager, _audit,
|
||||||
|
new DiffService(),
|
||||||
|
new RevisionHashService(),
|
||||||
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
|
options,
|
||||||
|
Options.Create(new CommunicationOptions
|
||||||
|
{
|
||||||
|
CentralFetchBaseUrl = "https://central.test:9000",
|
||||||
|
PendingDeploymentTtl = TimeSpan.FromMinutes(5)
|
||||||
|
}),
|
||||||
|
NullLogger<DeploymentService>.Instance);
|
||||||
|
return (service, applied);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The subtree is the area plus every descendant at any depth; an instance
|
||||||
|
/// sitting directly in the area counts, and siblings, ancestors, other root
|
||||||
|
/// areas and area-less instances do not.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeployAreaAsync_DeploysAreaAndDescendants_Only()
|
||||||
|
{
|
||||||
|
// 1:Plant 2:Line1 3:Cell 4:Line2 5:Warehouse 6:no area
|
||||||
|
ArrangeInstances(PlantArea, Line1Area, CellArea, Line2Area, WarehouseArea, null);
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeployAreaAsync(Line1Area, "admin");
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess, result.IsFailure ? result.Error : null);
|
||||||
|
Assert.Equal("site-1", result.Value.SiteIdentifier);
|
||||||
|
Assert.Equal(new[] { "Inst-02", "Inst-03" }, applied.Order().ToArray());
|
||||||
|
Assert.Equal(2, result.Value.SuccessCount);
|
||||||
|
Assert.Equal(0, result.Value.FailureCount);
|
||||||
|
Assert.Empty(result.Value.SkippedInstances!);
|
||||||
|
|
||||||
|
// Nothing outside the subtree was even flattened.
|
||||||
|
foreach (var id in new[] { 1, 4, 5, 6 })
|
||||||
|
await _pipeline.DidNotReceive().FlattenAndValidateAsync(
|
||||||
|
id, Arg.Any<CancellationToken>(), Arg.Any<bool>(), Arg.Any<FlattenSession?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A root area's subtree spans every nested level beneath it.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeployAreaAsync_RootArea_IncludesEveryNestedLevel()
|
||||||
|
{
|
||||||
|
ArrangeInstances(PlantArea, Line1Area, CellArea, Line2Area, WarehouseArea, null);
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeployAreaAsync(PlantArea, "admin");
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(new[] { "Inst-01", "Inst-02", "Inst-03", "Inst-04" }, applied.Order().ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Disabled instances inside the subtree are skipped, forced or not.</summary>
|
||||||
|
/// <param name="force">Whether the deploy is forced.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public async Task DeployAreaAsync_DisabledInstancesAreSkipped_RegardlessOfForce(bool force)
|
||||||
|
{
|
||||||
|
var instances = ArrangeInstances(Line1Area, CellArea);
|
||||||
|
instances[1].State = InstanceState.Disabled;
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeployAreaAsync(Line1Area, "admin", force);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(new[] { "Inst-01" }, applied.ToArray());
|
||||||
|
var skipped = Assert.Single(result.Value.SkippedInstances!);
|
||||||
|
Assert.Equal("Inst-02", skipped.UniqueName);
|
||||||
|
Assert.Contains("deploy individually to re-enable", skipped.Reason);
|
||||||
|
Assert.Equal(InstanceState.Disabled, instances[1].State);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Up-to-date instances inside the subtree are skipped unless the deploy is forced.</summary>
|
||||||
|
/// <param name="force">Whether the deploy is forced.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public async Task DeployAreaAsync_UpToDateInstancesAreSkipped_UnlessForced(bool force)
|
||||||
|
{
|
||||||
|
var instances = ArrangeInstances(Line1Area, CellArea);
|
||||||
|
ArrangeUpToDate(instances[1]);
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeployAreaAsync(Line1Area, "admin", force);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
if (force)
|
||||||
|
{
|
||||||
|
Assert.Equal(new[] { "Inst-01", "Inst-02" }, applied.Order().ToArray());
|
||||||
|
Assert.Empty(result.Value.SkippedInstances!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal(new[] { "Inst-01" }, applied.ToArray());
|
||||||
|
var skipped = Assert.Single(result.Value.SkippedInstances!);
|
||||||
|
Assert.Equal("Inst-02", skipped.UniqueName);
|
||||||
|
Assert.Contains("up-to-date", skipped.Reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>An unknown area is a failure result, not an empty success.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeployAreaAsync_UnknownArea_ReturnsFailure()
|
||||||
|
{
|
||||||
|
ArrangeInstances(Line1Area);
|
||||||
|
_templateRepo.GetAreaByIdAsync(4242, Arg.Any<CancellationToken>()).Returns((Area?)null);
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeployAreaAsync(4242, "admin");
|
||||||
|
|
||||||
|
Assert.True(result.IsFailure);
|
||||||
|
Assert.Contains("not found", result.Error);
|
||||||
|
Assert.Empty(applied);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>An area with no instances anywhere in its subtree is an empty success.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeployAreaAsync_EmptySubtree_SucceedsWithEmptySummary()
|
||||||
|
{
|
||||||
|
ArrangeInstances(WarehouseArea);
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeployAreaAsync(Line2Area, "admin");
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Empty(result.Value.InstanceResults);
|
||||||
|
Assert.Empty(applied);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A corrupt parent chain (a cycle) must not hang the walk: the subtree still
|
||||||
|
/// resolves and the deploy completes.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeployAreaAsync_ParentCycle_TerminatesAndDeploysTheReachableSet()
|
||||||
|
{
|
||||||
|
ArrangeAreas(
|
||||||
|
Area(PlantArea, SiteId, CellArea), // Plant -> Cell -> Line1 -> Plant
|
||||||
|
Area(Line1Area, SiteId, PlantArea),
|
||||||
|
Area(CellArea, SiteId, Line1Area),
|
||||||
|
Area(WarehouseArea, SiteId, null));
|
||||||
|
ArrangeInstances(PlantArea, Line1Area, CellArea, WarehouseArea);
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeployAreaAsync(Line1Area, "admin")
|
||||||
|
.WaitAsync(TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(new[] { "Inst-01", "Inst-02", "Inst-03" }, applied.Order().ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The whole-site deploy is unaffected by areas: area-less and every-area instances all go.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeploySiteAsync_StillDeploysEveryInstance_RegardlessOfArea()
|
||||||
|
{
|
||||||
|
ArrangeInstances(PlantArea, CellArea, WarehouseArea, null);
|
||||||
|
var (service, applied) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.DeploySiteAsync(SiteId, "admin");
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(new[] { "Inst-01", "Inst-02", "Inst-03", "Inst-04" }, applied.Order().ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The whole-site preview counts every instance and the Disabled ones among them.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task PreviewSubtreeDeployAsync_WholeSite_CountsAllAndDisabled()
|
||||||
|
{
|
||||||
|
var instances = ArrangeInstances(PlantArea, CellArea, WarehouseArea, null);
|
||||||
|
instances[2].State = InstanceState.Disabled;
|
||||||
|
var (service, _) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.PreviewSubtreeDeployAsync(SiteId, null);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(new SubtreeDeployPreview(4, 1), result.Value);
|
||||||
|
// Cheap: no staleness probe, no flatten.
|
||||||
|
_pipeline.DidNotReceive().CreateSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The area preview counts exactly the subtree's instances.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task PreviewSubtreeDeployAsync_Area_CountsSubtreeOnly()
|
||||||
|
{
|
||||||
|
var instances = ArrangeInstances(PlantArea, Line1Area, CellArea, Line2Area, WarehouseArea);
|
||||||
|
instances[2].State = InstanceState.Disabled;
|
||||||
|
var (service, _) = CreateService();
|
||||||
|
|
||||||
|
var result = await service.PreviewSubtreeDeployAsync(SiteId, Line1Area);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(new SubtreeDeployPreview(2, 1), result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>An area belonging to a different site, an unknown area, or an unknown site is a failure.</summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task PreviewSubtreeDeployAsync_RejectsForeignOrUnknownAreaAndUnknownSite()
|
||||||
|
{
|
||||||
|
ArrangeInstances(Line1Area);
|
||||||
|
_templateRepo.GetAreaByIdAsync(4242, Arg.Any<CancellationToken>()).Returns((Area?)null);
|
||||||
|
var (service, _) = CreateService();
|
||||||
|
|
||||||
|
Assert.True((await service.PreviewSubtreeDeployAsync(SiteId, RemoteArea)).IsFailure);
|
||||||
|
Assert.True((await service.PreviewSubtreeDeployAsync(SiteId, 4242)).IsFailure);
|
||||||
|
Assert.True((await service.PreviewSubtreeDeployAsync(77, null)).IsFailure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Answers every deploy with Success, recording which instances the site was asked to apply.</summary>
|
||||||
|
private sealed class RecordingSiteActor : ReceiveActor
|
||||||
|
{
|
||||||
|
/// <summary>Initializes the actor with the collection that records applied instance names.</summary>
|
||||||
|
/// <param name="applied">The collection that receives each applied instance's unique name.</param>
|
||||||
|
public RecordingSiteActor(ConcurrentBag<string> applied)
|
||||||
|
{
|
||||||
|
Receive<SiteEnvelope>(env =>
|
||||||
|
{
|
||||||
|
if (env.Message is not RefreshDeploymentCommand cmd)
|
||||||
|
return;
|
||||||
|
|
||||||
|
applied.Add(cmd.InstanceUniqueName);
|
||||||
|
Sender.Tell(new DeploymentStatusResponse(
|
||||||
|
cmd.DeploymentId, cmd.InstanceUniqueName,
|
||||||
|
DeploymentStatus.Success, null, DateTimeOffset.UtcNow));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ public class DeploySiteAsyncTests : TestKit
|
|||||||
});
|
});
|
||||||
|
|
||||||
return new DeploymentService(
|
return new DeploymentService(
|
||||||
_repo, _siteRepo, _pipeline, comms, _lockManager, _audit,
|
_repo, _siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public class DeploymentServiceStalenessTests
|
|||||||
_service = new DeploymentService(
|
_service = new DeploymentService(
|
||||||
_repo,
|
_repo,
|
||||||
siteRepo,
|
siteRepo,
|
||||||
|
Substitute.For<ITemplateEngineRepository>(),
|
||||||
_pipeline,
|
_pipeline,
|
||||||
new CommunicationService(
|
new CommunicationService(
|
||||||
Options.Create(new CommunicationOptions()),
|
Options.Create(new CommunicationOptions()),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
|
|
||||||
var siteRepo = CreateSiteRepoStub();
|
var siteRepo = CreateSiteRepoStub();
|
||||||
_service = new DeploymentService(
|
_service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, _comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, _comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
@@ -196,7 +196,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
siteRepo.GetSiteByIdAsync(42, Arg.Any<CancellationToken>()).Returns((Site?)null);
|
siteRepo.GetSiteByIdAsync(42, Arg.Any<CancellationToken>()).Returns((Site?)null);
|
||||||
|
|
||||||
var service = new DeploymentService(
|
var service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, _comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, _comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
@@ -428,7 +428,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
|
|
||||||
var siteRepo = CreateSiteRepoStub();
|
var siteRepo = CreateSiteRepoStub();
|
||||||
var service = new DeploymentService(
|
var service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
@@ -1243,7 +1243,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
|
|
||||||
var siteRepo = CreateSiteRepoStub();
|
var siteRepo = CreateSiteRepoStub();
|
||||||
return new DeploymentService(
|
return new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
@@ -1862,7 +1862,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
|
|
||||||
var siteRepo = CreateSiteRepoStub();
|
var siteRepo = CreateSiteRepoStub();
|
||||||
var service = new DeploymentService(
|
var service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
@@ -1922,7 +1922,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
var siteRepo = CreateSiteRepoStub();
|
var siteRepo = CreateSiteRepoStub();
|
||||||
var deadline = TimeSpan.FromMilliseconds(300);
|
var deadline = TimeSpan.FromMilliseconds(300);
|
||||||
var service = new DeploymentService(
|
var service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
@@ -1973,7 +1973,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
|
|
||||||
var siteRepo = CreateSiteRepoStub();
|
var siteRepo = CreateSiteRepoStub();
|
||||||
var service = new DeploymentService(
|
var service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
@@ -2021,7 +2021,7 @@ public class DeploymentServiceTests : TestKit
|
|||||||
|
|
||||||
var siteRepo = CreateSiteRepoStub();
|
var siteRepo = CreateSiteRepoStub();
|
||||||
var service = new DeploymentService(
|
var service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, comms, _lockManager, _audit,
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ public class DeploymentStatusNotifierTests : TestKit
|
|||||||
});
|
});
|
||||||
|
|
||||||
_service = new DeploymentService(
|
_service = new DeploymentService(
|
||||||
_repo, siteRepo, _pipeline, _comms, _lockManager, _audit,
|
_repo, siteRepo, Substitute.For<ITemplateEngineRepository>(), _pipeline, _comms, _lockManager, _audit,
|
||||||
new DiffService(), new RevisionHashService(), _notifier,
|
new DiffService(), new RevisionHashService(), _notifier,
|
||||||
options,
|
options,
|
||||||
// Notify-and-fetch: a non-empty CentralFetchBaseUrl lets the
|
// Notify-and-fetch: a non-empty CentralFetchBaseUrl lets the
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public class PendingUpdatesEvaluatorTests
|
|||||||
_deploymentService = new DeploymentService(
|
_deploymentService = new DeploymentService(
|
||||||
_deploymentRepo,
|
_deploymentRepo,
|
||||||
_siteRepo,
|
_siteRepo,
|
||||||
|
Substitute.For<ITemplateEngineRepository>(),
|
||||||
_pipeline,
|
_pipeline,
|
||||||
comms,
|
comms,
|
||||||
new OperationLockManager(),
|
new OperationLockManager(),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
|||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Deployment;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||||
@@ -206,7 +207,7 @@ public sealed class BulkDeployPoisonedContextTests : TestKit, IDisposable
|
|||||||
.Returns(Result<string>.Failure("indeterminate"));
|
.Returns(Result<string>.Failure("indeterminate"));
|
||||||
|
|
||||||
return new DeploymentService(
|
return new DeploymentService(
|
||||||
repository, siteRepository, pipeline, comms, new OperationLockManager(),
|
repository, siteRepository, Substitute.For<ITemplateEngineRepository>(), pipeline, comms, new OperationLockManager(),
|
||||||
Substitute.For<IAuditService>(),
|
Substitute.For<IAuditService>(),
|
||||||
new DiffService(),
|
new DiffService(),
|
||||||
new RevisionHashService(),
|
new RevisionHashService(),
|
||||||
|
|||||||
Reference in New Issue
Block a user