diff --git a/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldValueRepository.cs b/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldValueRepository.cs index 2e671bfc..ab51802f 100644 --- a/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldValueRepository.cs +++ b/PARR.Core/Repositories/Interfaces/Unit/IUnitFieldValueRepository.cs @@ -3,8 +3,8 @@ using PARR.Domain.Entities.Unit; namespace PARR.Core.Repositories.Interfaces.Unit { - public interface IUnitFieldValueRepository: IBaseRepository + public interface IUnitFieldValueRepository : IBaseRepository { - Task GetByValueNameAsync(string? value); + Task> FindValueIdsByMaskAsync(string mask, CancellationToken ct = default); } } diff --git a/PARR.Core/Repositories/Interfaces/Unit/IUnitRepository.cs b/PARR.Core/Repositories/Interfaces/Unit/IUnitRepository.cs index f5f0bfed..af3ebb9b 100644 --- a/PARR.Core/Repositories/Interfaces/Unit/IUnitRepository.cs +++ b/PARR.Core/Repositories/Interfaces/Unit/IUnitRepository.cs @@ -4,18 +4,18 @@ namespace PARR.Core.Repositories.Interfaces.Unit { public interface IUnitRepository : IBaseRepository { + /// + /// Поиск юнитов по списку ID значений. + /// Используется для эффективной фильтрации после предварительного поиска ValueId. + /// + Task> FindUnitIdsByValueIdsAsync( + IReadOnlyList unitIds, + Guid fieldId, + IReadOnlyList valueIds, + CancellationToken ct = default); + IQueryable GetInitialUnitIds(string dbValueMask); - /// - /// Получить юниты по Id атрибута и маски значения - /// - /// - /// - /// - /// true - не содержит, false - содержит - /// - IQueryable GetUnitByFieldAndValue(IQueryable query, Guid fieldId, string valueMask, bool isInverse = false); - IQueryable GetWithIncludes(); } } diff --git a/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitFieldMatcher.cs b/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitFieldMatcher.cs index 38412858..29647dee 100644 --- a/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitFieldMatcher.cs +++ b/PARR.Core/Services/UnitFilterService/Matchers/Implemetations/UnitFieldMatcher.cs @@ -1,26 +1,29 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using PARR.Core.Repositories.Interfaces.Unit; using PARR.Core.Services.UnitFilterService.Matchers.Interfaces; using PARR.Domain.Entities.JobEntities; +using System.Diagnostics; namespace PARR.Core.Services.UnitFilterService.Matchers; /// /// Сопоставляет юниты с фильтрами по атрибутам. +/// Использует двухэтапный поиск: сначала ValueId, затем UnitId. /// internal class UnitFieldMatcher : IUnitFieldMatcher { - private const int chunkSize = 200; - private readonly IUnitRepository unitRepository; - private readonly ILogger logger; + private readonly IUnitRepository _unitRepository; + private readonly IUnitFieldValueRepository _unitFieldValueRepository; + private readonly ILogger _logger; public UnitFieldMatcher( IUnitRepository unitRepository, + IUnitFieldValueRepository unitFieldValueRepository, ILogger logger) { - this.unitRepository = unitRepository; - this.logger = logger; + _unitRepository = unitRepository; + _unitFieldValueRepository = unitFieldValueRepository; + _logger = logger; } public async Task> MatchAsync( @@ -32,32 +35,63 @@ internal class UnitFieldMatcher : IUnitFieldMatcher if (filters == null || filters.Count == 0) return new List(unitIds); - logger.LogDebug("UnitFieldMatcher: вход {UnitCount} юнитов, фильтров: {FilterCount}", + _logger.LogDebug("UnitFieldMatcher: вход {UnitCount} юнитов, фильтров: {FilterCount}", unitIds.Count, filters.Count); - var result = new List(unitIds.Count); + var currentIds = new HashSet(unitIds); - foreach (var chunk in unitIds.Chunk(chunkSize)) + for (int i = 0; i < filters.Count; i++) { - var query = unitRepository.Get().AsNoTracking() - .Where(u => chunk.Contains(u.Id)); + var filter = filters[i]; + var mask = filter.ValueMask?.Trim(); - foreach (var fieldFilter in filters) + if (string.IsNullOrEmpty(mask)) + continue; + + var sw = Stopwatch.StartNew(); + var inputCount = currentIds.Count; + + var matchingValueIds = await _unitFieldValueRepository + .FindValueIdsByMaskAsync(mask, ct); + + if (matchingValueIds.Count == 0 && !filter.IsInverse) { - var valueMask = fieldFilter.ValueMask?.Trim(); - if (string.IsNullOrEmpty(valueMask)) - continue; - - query = unitRepository.GetUnitByFieldAndValue( - query, fieldFilter.FieldId, fieldFilter.ValueMask!, fieldFilter.IsInverse); + sw.Stop(); + _logger.LogDebug( + "UnitFieldMatcher: фильтр #{Index} (FieldId={FieldId}, Mask='{Mask}') | Вход: {InCount}, Выход: 0 (нет значений), Время: {Ms}ms", + i + 1, filter.FieldId, mask, inputCount, sw.ElapsedMilliseconds); + currentIds.Clear(); + break; } - var chunkResult = await query.Select(u => u.Id).ToListAsync(ct); - result.AddRange(chunkResult); + // Шаг 2: Найти/исключить юниты по ValueId + if (filter.IsInverse) + { + var unitsToExclude = await _unitRepository + .FindUnitIdsByValueIdsAsync(currentIds.ToList(), filter.FieldId, matchingValueIds, ct); + currentIds.ExceptWith(unitsToExclude); + } + else + { + var unitsToKeep = await _unitRepository + .FindUnitIdsByValueIdsAsync(currentIds.ToList(), filter.FieldId, matchingValueIds, ct); + currentIds.IntersectWith(unitsToKeep); + } + + sw.Stop(); + _logger.LogDebug( + "UnitFieldMatcher: фильтр #{Index} (FieldId={FieldId}, Mask='{Mask}', Inverse={IsInverse}, Values={ValCount}) | Вход: {InCount}, Выход: {OutCount}, Время: {Ms}ms", + i + 1, filter.FieldId, mask, filter.IsInverse, matchingValueIds.Count, + inputCount, currentIds.Count, sw.ElapsedMilliseconds); + + if (currentIds.Count == 0) + { + _logger.LogDebug("UnitFieldMatcher: прерывание на фильтре #{Index} (0 юнитов)", i + 1); + break; + } } - logger.LogDebug("UnitFieldMatcher: выход {UnitCount} юнитов", result.Count); - - return result; + _logger.LogDebug("UnitFieldMatcher: итоговый выход {UnitCount} юнитов", currentIds.Count); + return currentIds.ToList(); } } \ No newline at end of file diff --git a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs index db4b4323..113226cb 100644 --- a/PARR.Core/Services/UnitFilterService/UnitFilterService.cs +++ b/PARR.Core/Services/UnitFilterService/UnitFilterService.cs @@ -84,8 +84,7 @@ internal class UnitFilterService : IUnitFilterService public async Task?> GetUnitsByJobFilterAsync( Job job, int? takeCount = null, - CancellationToken cancellationToken = default - ) + CancellationToken cancellationToken = default) { if (job.Group == null) throw new ArgumentNullException(nameof(job.Group), $"Job {job.Id} не содержит Group"); @@ -97,13 +96,9 @@ internal class UnitFilterService : IUnitFilterService var totalStopwatch = Stopwatch.StartNew(); logger.LogInformation("Начало фильтрации юнитов для Job {JobId} с {FilterCount} фильтрами", job.Id, job.UnitFilters.Count); - // Собираем все контексты юнитов, прошедших фильтрацию var allFilteredContexts = new List(); - - // Преобразуем в список для индексации var unitFiltersList = job.UnitFilters.ToList(); - // Этап 1: Применение основных фильтров (Field, Relationship) на уровне SQL for (int i = 0; i < unitFiltersList.Count; i++) { var filter = unitFiltersList[i]; @@ -113,14 +108,21 @@ internal class UnitFilterService : IUnitFilterService { logger.LogDebug("Применение фильтра #{Index} (Id={FilterId})", i + 1, filter.Id); - // 1. Найти ID юнитов по UnitFilter + // 1. Resolve + var resolveSw = Stopwatch.StartNew(); var initialUnitIds = await nameResolver.ResolveAsync(filter.UnitFilter, cancellationToken); + resolveSw.Stop(); + if (!initialUnitIds.Any()) { - logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов)", i + 1); + logger.LogDebug("Фильтр #{Index}: пропущен (0 юнитов после Resolve, {ResolveMs}ms)", + i + 1, resolveSw.ElapsedMilliseconds); continue; } + logger.LogDebug("Фильтр #{Index}: Resolve вернул {Count} юнитов за {Ms}ms", + i + 1, initialUnitIds.Count, resolveSw.ElapsedMilliseconds); + #if DEBUG if (initialUnitIds.Contains(debugTargetUnitId)) { @@ -128,14 +130,19 @@ internal class UnitFilterService : IUnitFilterService } #endif - // 2. Применить FieldFilters - var fieldStopwatch = Stopwatch.StartNew(); - //var fieldFilteredIds = await ApplyFieldFiltersOnDbAsync(initialUnitIds, filter.FieldFilters, cancellationToken); + // 2. FieldFilters + var fieldSw = Stopwatch.StartNew(); var fieldFilteredIds = await unitFieldMatcher.MatchAsync(initialUnitIds, filter.FieldFilters, cancellationToken); + fieldSw.Stop(); + + logger.LogDebug("Фильтр #{Index}: FieldMatcher вернул {Count} юнитов за {Ms}ms", + i + 1, fieldFilteredIds.Count, fieldSw.ElapsedMilliseconds); if (!fieldFilteredIds.Any()) { - logger.LogDebug("Фильтр #{Index}: 0 юнитов после FieldFilters", i + 1); + filterStopwatch.Stop(); + logger.LogDebug("Фильтр #{Index}: завершён (0 юнитов). [Resolve: {R}ms, Field: {F}ms, Total: {T}ms]", + i + 1, resolveSw.ElapsedMilliseconds, fieldSw.ElapsedMilliseconds, filterStopwatch.ElapsedMilliseconds); continue; } @@ -146,16 +153,24 @@ internal class UnitFilterService : IUnitFilterService } #endif - // 3. Применить RelationshipFilters - var relStopwatch = Stopwatch.StartNew(); + // 3. RelationshipFilters + var relSw = Stopwatch.StartNew(); var relationshipFilteredContexts = await unitRelationshipMatcher.MatchAsync( fieldFilteredIds, filter.RelationshipFilters, cancellationToken); + relSw.Stop(); - if (!relationshipFilteredContexts.Any()) - { - logger.LogDebug("Фильтр #{Index}: 0 юнитов после RelationshipFilters", i + 1); - continue; - } + filterStopwatch.Stop(); + allFilteredContexts.AddRange(relationshipFilteredContexts); + + logger.LogDebug( + "Фильтр #{Index}: добавлено {Count} юнитов. Всего: {Total}. [Resolve: {R}ms, Field: {F}ms, Rel: {Rel}ms, Total: {T}ms]", + i + 1, + relationshipFilteredContexts.Count, + allFilteredContexts.Count, + resolveSw.ElapsedMilliseconds, + fieldSw.ElapsedMilliseconds, + relSw.ElapsedMilliseconds, + filterStopwatch.ElapsedMilliseconds); #if DEBUG var targetContext = relationshipFilteredContexts.FirstOrDefault(c => c.UnitId == debugTargetUnitId); @@ -165,25 +180,13 @@ internal class UnitFilterService : IUnitFilterService debugTargetUnitId, i + 1, targetContext.ValidParentIds.Count, targetContext.ValidChildIds.Count); } #endif - - // Добавляем отфильтрованные контексты в общий набор - allFilteredContexts.AddRange(relationshipFilteredContexts); - - filterStopwatch.Stop(); - logger.LogDebug( - "Фильтр #{Index}: добавлено {Count} юнитов. Всего: {Total}. [Field: {F}ms, Rel: {R}ms, Total: {T}ms]", - i + 1, - relationshipFilteredContexts.Count, - allFilteredContexts.Count, - fieldStopwatch.ElapsedMilliseconds, - relStopwatch.ElapsedMilliseconds, - filterStopwatch.ElapsedMilliseconds - ); } catch (Exception ex) { - logger.LogError(ex, "Ошибка при обработке фильтра {FilterId} для Job {JobId}", - filter.Id, job.Id); + filterStopwatch.Stop(); + logger.LogError(ex, "Ошибка при обработке фильтра #{Index} (Id={FilterId}) для Job {JobId}. Время до ошибки: {Ms}ms", + i + 1, filter.Id, job.Id, filterStopwatch.ElapsedMilliseconds); + throw; // КРИТИЧНО: прерываем выполнение, чтобы не маскировать проблему } } @@ -197,6 +200,7 @@ internal class UnitFilterService : IUnitFilterService ValidChildIds = new HashSet(g.SelectMany(c => c.ValidChildIds)) }) .ToList(); + logger.LogInformation("Этап базовой фильтрации завершён: собрано {UnitCount} уникальных юнитов", mergedContexts.Count); // Этап 2: Применение Umbrella-фильтра @@ -219,6 +223,346 @@ internal class UnitFilterService : IUnitFilterService return result; } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + private async Task LoadJobWithFiltersAsync(Guid jobId, CancellationToken cancellationToken = default) { return await jobRepository diff --git a/PARR.DAL.Tests/Services/UnitFilterService/Matchers/UnitFieldMatcherTests.cs b/PARR.DAL.Tests/Services/UnitFilterService/Matchers/UnitFieldMatcherTests.cs new file mode 100644 index 00000000..e46d192c --- /dev/null +++ b/PARR.DAL.Tests/Services/UnitFilterService/Matchers/UnitFieldMatcherTests.cs @@ -0,0 +1,326 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.UnitFilterService.Matchers; +using PARR.Domain.Entities.JobEntities; + +namespace PARR.Core.Tests.Services.UnitFilterService.Matchers +{ + public class UnitFieldMatcherTests + { + private readonly Mock _unitRepoMock; + private readonly Mock _fieldValueRepoMock; + private readonly UnitFieldMatcher _sut; + + public UnitFieldMatcherTests() + { + _unitRepoMock = new Mock(); + _fieldValueRepoMock = new Mock(); + var logger = NullLoggerFactory.Instance.CreateLogger(); + _sut = new UnitFieldMatcher( + _unitRepoMock.Object, + _fieldValueRepoMock.Object, + logger); + } + + /// + /// Если фильтры не заданы, возвращается исходный набор юнитов без изменений. + /// + [Fact] + public async Task MatchAsync_NoFilters_ReturnsAllUnitIds() + { + // Arrange + var unitIds = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + + // Act + var result = await _sut.MatchAsync(unitIds, Enumerable.Empty()); + + // Assert + Assert.Equal(unitIds.Count, result.Count); + Assert.Equal(unitIds, result); + _fieldValueRepoMock.Verify( + r => r.FindValueIdsByMaskAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + /// + /// Прямой фильтр оставляет только юниты, имеющие совпадающие значения. + /// Проверяется логика IntersectWith. + /// + [Fact] + public async Task MatchAsync_SingleDirectFilter_IntersectsWithMatchingUnits() + { + // Arrange + var unit1 = Guid.NewGuid(); + var unit2 = Guid.NewGuid(); + var unit3 = Guid.NewGuid(); + var unitIds = new List { unit1, unit2, unit3 }; + + var fieldId = Guid.NewGuid(); + var valueId = Guid.NewGuid(); + + var filters = new List + { + new() { FieldId = fieldId, ValueMask = "коммутатор", IsInverse = false } + }; + + _fieldValueRepoMock + .Setup(r => r.FindValueIdsByMaskAsync("коммутатор", It.IsAny())) + .ReturnsAsync(new List { valueId }); + + _unitRepoMock + .Setup(r => r.FindUnitIdsByValueIdsAsync( + It.IsAny>(), + fieldId, + It.Is>(v => v.Contains(valueId)), + It.IsAny())) + .ReturnsAsync(new List { unit1, unit3 }); + + // Act + var result = await _sut.MatchAsync(unitIds, filters); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(unit1, result); + Assert.Contains(unit3, result); + Assert.DoesNotContain(unit2, result); + } + + /// + /// Инверсный фильтр исключает юниты, имеющие совпадающие значения. + /// Проверяется логика ExceptWith. + /// + [Fact] + public async Task MatchAsync_SingleInverseFilter_ExcludesMatchingUnits() + { + // Arrange + var unit1 = Guid.NewGuid(); + var unit2 = Guid.NewGuid(); + var unit3 = Guid.NewGuid(); + var unitIds = new List { unit1, unit2, unit3 }; + + var fieldId = Guid.NewGuid(); + var valueId = Guid.NewGuid(); + + var filters = new List + { + new() { FieldId = fieldId, ValueMask = "1", IsInverse = true } + }; + + _fieldValueRepoMock + .Setup(r => r.FindValueIdsByMaskAsync("1", It.IsAny())) + .ReturnsAsync(new List { valueId }); + + _unitRepoMock + .Setup(r => r.FindUnitIdsByValueIdsAsync( + It.IsAny>(), + fieldId, + It.Is>(v => v.Contains(valueId)), + It.IsAny())) + .ReturnsAsync(new List { unit2 }); + + // Act + var result = await _sut.MatchAsync(unitIds, filters); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(unit1, result); + Assert.Contains(unit3, result); + Assert.DoesNotContain(unit2, result); + } + + /// + /// Несколько фильтров применяются последовательно (AND). + /// Каждый следующий фильтр сужает набор, полученный от предыдущего. + /// + [Fact] + public async Task MatchAsync_MultipleFilters_AppliesSequentially() + { + // Arrange + var unit1 = Guid.NewGuid(); + var unit2 = Guid.NewGuid(); + var unit3 = Guid.NewGuid(); + var unit4 = Guid.NewGuid(); + var unitIds = new List { unit1, unit2, unit3, unit4 }; + + var fieldId1 = Guid.NewGuid(); + var fieldId2 = Guid.NewGuid(); + var valueId1 = Guid.NewGuid(); + var valueId2 = Guid.NewGuid(); + + var filters = new List + { + new() { FieldId = fieldId1, ValueMask = "СХД", IsInverse = false }, + new() { FieldId = fieldId2, ValueMask = "коммутатор", IsInverse = false } + }; + + // Первый фильтр: значения найдены для unit1, unit2, unit3 + _fieldValueRepoMock + .Setup(r => r.FindValueIdsByMaskAsync("СХД", It.IsAny())) + .ReturnsAsync(new List { valueId1 }); + _unitRepoMock + .Setup(r => r.FindUnitIdsByValueIdsAsync( + It.Is>(ids => ids.Count == 4), + fieldId1, + It.Is>(v => v.Contains(valueId1)), + It.IsAny())) + .ReturnsAsync(new List { unit1, unit2, unit3 }); + + // Второй фильтр: из оставшихся 3 юнитов значение найдено для unit1 и unit3 + _fieldValueRepoMock + .Setup(r => r.FindValueIdsByMaskAsync("коммутатор", It.IsAny())) + .ReturnsAsync(new List { valueId2 }); + _unitRepoMock + .Setup(r => r.FindUnitIdsByValueIdsAsync( + It.Is>(ids => ids.Count == 3), + fieldId2, + It.Is>(v => v.Contains(valueId2)), + It.IsAny())) + .ReturnsAsync(new List { unit1, unit3 }); + + // Act + var result = await _sut.MatchAsync(unitIds, filters); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(unit1, result); + Assert.Contains(unit3, result); + } + + /// + /// Если прямой фильтр не нашёл ни одного значения, результат пуст. + /// Проверяется раннее прерывание. + /// + [Fact] + public async Task MatchAsync_NoMatchingValues_DirectFilter_ReturnsEmpty() + { + // Arrange + var unitIds = new List { Guid.NewGuid(), Guid.NewGuid() }; + var fieldId = Guid.NewGuid(); + + var filters = new List + { + new() { FieldId = fieldId, ValueMask = "несуществующее", IsInverse = false } + }; + + _fieldValueRepoMock + .Setup(r => r.FindValueIdsByMaskAsync("несуществующее", It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _sut.MatchAsync(unitIds, filters); + + // Assert + Assert.Empty(result); + _unitRepoMock.Verify( + r => r.FindUnitIdsByValueIdsAsync( + It.IsAny>(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), + Times.Never); + } + + /// + /// Если инверсный фильтр не нашёл ни одного значения, все юниты проходят. + /// Исключать нечего, поэтому FindUnitIdsByValueIdsAsync не вызывается. + /// + [Fact] + public async Task MatchAsync_NoMatchingValues_InverseFilter_ReturnsAll() + { + // Arrange + var unit1 = Guid.NewGuid(); + var unit2 = Guid.NewGuid(); + var unitIds = new List { unit1, unit2 }; + var fieldId = Guid.NewGuid(); + + var filters = new List + { + new() { FieldId = fieldId, ValueMask = "несуществующее", IsInverse = true } + }; + + _fieldValueRepoMock + .Setup(r => r.FindValueIdsByMaskAsync("несуществующее", It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _sut.MatchAsync(unitIds, filters); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(unit1, result); + Assert.Contains(unit2, result); + } + + /// + /// Фильтр с пустой маской пропускается без вызова репозиториев. + /// + [Fact] + public async Task MatchAsync_EmptyMask_SkipsFilter() + { + // Arrange + var unitIds = new List { Guid.NewGuid(), Guid.NewGuid() }; + var fieldId = Guid.NewGuid(); + + var filters = new List + { + new() { FieldId = fieldId, ValueMask = " ", IsInverse = false } + }; + + // Act + var result = await _sut.MatchAsync(unitIds, filters); + + // Assert + Assert.Equal(unitIds.Count, result.Count); + _fieldValueRepoMock.Verify( + r => r.FindValueIdsByMaskAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + /// + /// Если после одного из фильтров набор стал пустым, + /// последующие фильтры не выполняются. + /// + [Fact] + public async Task MatchAsync_EarlyBreak_WhenNoUnitsLeft() + { + // Arrange + var unit1 = Guid.NewGuid(); + var unitIds = new List { unit1 }; + + var fieldId1 = Guid.NewGuid(); + var fieldId2 = Guid.NewGuid(); + var valueId1 = Guid.NewGuid(); + + var filters = new List + { + new() { FieldId = fieldId1, ValueMask = "СХД", IsInverse = false }, + new() { FieldId = fieldId2, ValueMask = "коммутатор", IsInverse = false } + }; + + // Первый фильтр возвращает пустой результат + _fieldValueRepoMock + .Setup(r => r.FindValueIdsByMaskAsync("СХД", It.IsAny())) + .ReturnsAsync(new List { valueId1 }); + _unitRepoMock + .Setup(r => r.FindUnitIdsByValueIdsAsync( + It.IsAny>(), + fieldId1, + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _sut.MatchAsync(unitIds, filters); + + // Assert + Assert.Empty(result); + + // Второй фильтр не должен вызываться + _fieldValueRepoMock.Verify( + r => r.FindValueIdsByMaskAsync("коммутатор", It.IsAny()), + Times.Never); + } + } +} diff --git a/PARR.DAL.Tests/Services/UnitFilterService/Matchers/UnitRelationshipMatcherTests.cs b/PARR.DAL.Tests/Services/UnitFilterService/Matchers/UnitRelationshipMatcherTests.cs new file mode 100644 index 00000000..47000575 --- /dev/null +++ b/PARR.DAL.Tests/Services/UnitFilterService/Matchers/UnitRelationshipMatcherTests.cs @@ -0,0 +1,591 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using PARR.Core.Repositories.Interfaces.Unit; +using PARR.Core.Services.UnitFilterService.Matchers; +using PARR.DAL.Context; +using PARR.Domain.Entities.JobEntities; +using PARR.Domain.Entities.Unit; + +namespace PARR.Core.Tests.Services.UnitFilterService.Matchers +{ + public class UnitRelationshipMatcherTests + { + private readonly Mock _unitInUnitRepoMock; + private readonly Mock _unitInValueRepoMock; + private readonly UnitRelationshipMatcher _sut; + + public UnitRelationshipMatcherTests() + { + _unitInUnitRepoMock = new Mock(); + _unitInValueRepoMock = new Mock(); + var logger = NullLoggerFactory.Instance.CreateLogger(); + _sut = new UnitRelationshipMatcher( + _unitInUnitRepoMock.Object, + _unitInValueRepoMock.Object, + logger); + } + + /// + /// Создаёт юнит со значением поля и добавляет его в контекст. + /// + private static Unit BuildUnit(Guid id, string name, Guid fieldId, string fieldValue) + { + var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue }; + return new Unit + { + Id = id, + Name = name, + UnitValues = new List + { + new UnitInValue + { + UnitId = id, + FieldId = fieldId, + ValueId = fieldVal.Id, + Value = fieldVal + } + } + }; + } + + /// + /// Настраивает моки репозиториев для работы с InMemory-контекстом. + /// GetMatchingTargetIds эмулирует ILike через Contains с обрезкой символов '%'. + /// + private void SetupMocks(DataContext context) + { + _unitInUnitRepoMock + .Setup(r => r.Get()) + .Returns(context.UnitInUnits.AsQueryable()); + + _unitInValueRepoMock + .Setup(r => r.GetMatchingTargetIds(It.IsAny(), It.IsAny())) + .Returns((Guid fieldId, string mask) => + { + var trimmed = mask.Trim('%'); + return context.UnitInValues + .AsNoTracking() + .Where(uv => uv.FieldId == fieldId + && uv.Value != null + && uv.Value.Value != null + && uv.Value.Value.Contains(trimmed, StringComparison.OrdinalIgnoreCase)) + .Select(uv => uv.UnitId) + .Distinct() + .AsQueryable(); + }); + } + + /// + /// Если фильтры не заданы, все юниты возвращаются как контексты без изменений. + /// + [Fact] + public async Task MatchAsync_NoFilters_ReturnsAllUnitsAsContexts() + { + // Arrange + var unit1 = Guid.NewGuid(); + var unit2 = Guid.NewGuid(); + var unitIds = new List { unit1, unit2 }; + + // Act + var result = await _sut.MatchAsync(unitIds, Enumerable.Empty()); + + // Assert + Assert.Equal(2, result.Count); + Assert.Contains(result, c => c.UnitId == unit1); + Assert.Contains(result, c => c.UnitId == unit2); + } + + /// + /// Если входной набор пуст, возвращается пустой список. + /// + [Fact] + public async Task MatchAsync_EmptyUnitIds_ReturnsEmpty() + { + // Arrange + var filters = new List + { + new() { FieldId = Guid.NewGuid(), ValueMask = "%test%", IsParent = true } + }; + + // Act + var result = await _sut.MatchAsync(new List(), filters); + + // Assert + Assert.Empty(result); + } + + /// + /// Прямой родительский фильтр (IsFullMatch=false): + /// юнит проходит, если ХОТЯ БЫ ОДИН из его родителей соответствует маске. + /// + [Fact] + public async Task MatchAsync_ParentFilter_DirectAnyMatch_OnlyUnitsWithMatchingParentPass() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var fieldId = Guid.NewGuid(); + var parentGood = Guid.NewGuid(); + var parentBad = Guid.NewGuid(); + var unitWithGoodParent = Guid.NewGuid(); + var unitWithBadParent = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(parentGood, "Parent_Good", fieldId, "VALID-TAG"), + BuildUnit(parentBad, "Parent_Bad", fieldId, "OTHER-TAG"), + BuildUnit(unitWithGoodParent, "Unit_Good", Guid.NewGuid(), "x"), + BuildUnit(unitWithBadParent, "Unit_Bad", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + new UnitInUnit { ParentUnitId = parentGood, ChildUnitId = unitWithGoodParent, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentBad, ChildUnitId = unitWithBadParent, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = fieldId, + ValueMask = "%VALID%", + IsParent = true, + IsInverse = false, + IsFullMatch = false + } + }; + + // Act + var result = await _sut.MatchAsync( + new List { unitWithGoodParent, unitWithBadParent }, filters); + + // Assert + var resultIds = result.Select(c => c.UnitId).ToList(); + Assert.Contains(unitWithGoodParent, resultIds); + Assert.DoesNotContain(unitWithBadParent, resultIds); + + var ctx = result.First(c => c.UnitId == unitWithGoodParent); + Assert.Contains(parentGood, ctx.ValidParentIds); + } + + /// + /// Инверсный родительский фильтр (IsInverse=true, IsFullMatch=false): + /// юнит проходит, если ХОТЯ БЫ ОДИН родитель НЕ соответствует маске. + /// + [Fact] + public async Task MatchAsync_ParentFilter_Inverse_UnitsWithOnlyForbiddenParentsExcluded() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var fieldId = Guid.NewGuid(); + var parentForbidden = Guid.NewGuid(); + var parentClean = Guid.NewGuid(); + var unitWithForbiddenOnly = Guid.NewGuid(); + var unitWithCleanParent = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(parentForbidden, "Parent_Forbidden", fieldId, "FORBIDDEN"), + BuildUnit(parentClean, "Parent_Clean", fieldId, "CLEAN"), + BuildUnit(unitWithForbiddenOnly, "Unit_Forbidden", Guid.NewGuid(), "x"), + BuildUnit(unitWithCleanParent, "Unit_Clean", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + new UnitInUnit { ParentUnitId = parentForbidden, ChildUnitId = unitWithForbiddenOnly, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentClean, ChildUnitId = unitWithCleanParent, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = fieldId, + ValueMask = "%FORBIDDEN%", + IsParent = true, + IsInverse = true, + IsFullMatch = false + } + }; + + // Act + var result = await _sut.MatchAsync( + new List { unitWithForbiddenOnly, unitWithCleanParent }, filters); + + // Assert + var resultIds = result.Select(c => c.UnitId).ToList(); + Assert.Contains(unitWithCleanParent, resultIds); + Assert.DoesNotContain(unitWithForbiddenOnly, resultIds); + } + + /// + /// IsFullMatch=true: юнит проходит, только если ВСЕ его родители соответствуют маске. + /// Юнит с одним "плохим" родителем исключается. + /// + [Fact] + public async Task MatchAsync_ParentFilter_IsFullMatch_AllParentsMustPass() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var fieldId = Guid.NewGuid(); + var parentGood1 = Guid.NewGuid(); + var parentGood2 = Guid.NewGuid(); + var parentBad = Guid.NewGuid(); + var unitAllGood = Guid.NewGuid(); + var unitMixed = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(parentGood1, "Parent_Good1", fieldId, "Good"), + BuildUnit(parentGood2, "Parent_Good2", fieldId, "Good"), + BuildUnit(parentBad, "Parent_Bad", fieldId, "Bad"), + BuildUnit(unitAllGood, "Unit_AllGood", Guid.NewGuid(), "x"), + BuildUnit(unitMixed, "Unit_Mixed", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + new UnitInUnit { ParentUnitId = parentGood1, ChildUnitId = unitAllGood, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentGood2, ChildUnitId = unitAllGood, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentGood1, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentBad, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = fieldId, + ValueMask = "%Good%", + IsParent = true, + IsInverse = false, + IsFullMatch = true + } + }; + + // Act + var result = await _sut.MatchAsync( + new List { unitAllGood, unitMixed }, filters); + + // Assert + var resultIds = result.Select(c => c.UnitId).ToList(); + Assert.Contains(unitAllGood, resultIds); + Assert.DoesNotContain(unitMixed, resultIds); + } + + /// + /// Комбинация IsInverse=true и IsFullMatch=true: + /// юнит проходит, только если НИ ОДИН родитель не соответствует маске. + /// + [Fact] + public async Task MatchAsync_ParentFilter_InverseAndFullMatch_UnitWithAnyForbiddenParentExcluded() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var fieldId = Guid.NewGuid(); + var parentForbidden = Guid.NewGuid(); + var parentClean = Guid.NewGuid(); + var unitMixed = Guid.NewGuid(); + var unitAllClean = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(parentForbidden, "Parent_Forbidden", fieldId, "FORBIDDEN"), + BuildUnit(parentClean, "Parent_Clean", fieldId, "CLEAN"), + BuildUnit(unitMixed, "Unit_Mixed", Guid.NewGuid(), "x"), + BuildUnit(unitAllClean, "Unit_AllClean", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + new UnitInUnit { ParentUnitId = parentForbidden, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentClean, ChildUnitId = unitMixed, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentClean, ChildUnitId = unitAllClean, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = fieldId, + ValueMask = "%FORBIDDEN%", + IsParent = true, + IsInverse = true, + IsFullMatch = true + } + }; + + // Act + var result = await _sut.MatchAsync( + new List { unitMixed, unitAllClean }, filters); + + // Assert + var resultIds = result.Select(c => c.UnitId).ToList(); + Assert.Contains(unitAllClean, resultIds); + Assert.DoesNotContain(unitMixed, resultIds); + } + + /// + /// Дочерний фильтр (IsParent=false): + /// юнит проходит, если хотя бы один из его детей соответствует маске. + /// + [Fact] + public async Task MatchAsync_ChildFilter_DirectMatch() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var fieldId = Guid.NewGuid(); + var childValid = Guid.NewGuid(); + var childInvalid = Guid.NewGuid(); + var parentWithValidChild = Guid.NewGuid(); + var parentWithInvalidChild = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(childValid, "Child_Valid", fieldId, "VALID"), + BuildUnit(childInvalid, "Child_Invalid", fieldId, "OTHER"), + BuildUnit(parentWithValidChild, "Parent_Valid", Guid.NewGuid(), "x"), + BuildUnit(parentWithInvalidChild, "Parent_Invalid", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + new UnitInUnit { ParentUnitId = parentWithValidChild, ChildUnitId = childValid, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = parentWithInvalidChild, ChildUnitId = childInvalid, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = fieldId, + ValueMask = "%VALID%", + IsParent = false, + IsInverse = false, + IsFullMatch = false + } + }; + + // Act + var result = await _sut.MatchAsync( + new List { parentWithValidChild, parentWithInvalidChild }, filters); + + // Assert + var resultIds = result.Select(c => c.UnitId).ToList(); + Assert.Contains(parentWithValidChild, resultIds); + Assert.DoesNotContain(parentWithInvalidChild, resultIds); + + var ctx = result.First(c => c.UnitId == parentWithValidChild); + Assert.Contains(childValid, ctx.ValidChildIds); + } + + /// + /// Юнит без связей исключается, если есть фильтры с непустой маской. + /// + [Fact] + public async Task MatchAsync_UnitWithoutRelatedUnits_IsExcluded() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var fieldId = Guid.NewGuid(); + var unitWithParent = Guid.NewGuid(); + var unitOrphan = Guid.NewGuid(); + var parent = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(parent, "Parent", fieldId, "VALID"), + BuildUnit(unitWithParent, "Unit_WithParent", Guid.NewGuid(), "x"), + BuildUnit(unitOrphan, "Unit_Orphan", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + new UnitInUnit { ParentUnitId = parent, ChildUnitId = unitWithParent, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = fieldId, + ValueMask = "%VALID%", + IsParent = true, + IsInverse = false, + IsFullMatch = false + } + }; + + // Act + var result = await _sut.MatchAsync( + new List { unitWithParent, unitOrphan }, filters); + + // Assert + var resultIds = result.Select(c => c.UnitId).ToList(); + Assert.Contains(unitWithParent, resultIds); + Assert.DoesNotContain(unitOrphan, resultIds); + } + + /// + /// Фильтр с пустой маской пропускается: юниты не исключаются. + /// + [Fact] + public async Task MatchAsync_EmptyMask_FilterSkipped() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var fieldId = Guid.NewGuid(); + var unit1 = Guid.NewGuid(); + var parent = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(parent, "Parent", fieldId, "Any"), + BuildUnit(unit1, "Unit", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + new UnitInUnit { ParentUnitId = parent, ChildUnitId = unit1, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = fieldId, + ValueMask = " ", + IsParent = true, + IsInverse = false, + IsFullMatch = false + } + }; + + // Act + var result = await _sut.MatchAsync(new List { unit1 }, filters); + + // Assert + Assert.Single(result); + Assert.Equal(unit1, result.First().UnitId); + } + + /// + /// Комбинация родительского и дочернего фильтров: + /// юнит должен пройти оба направления. + /// + [Fact] + public async Task MatchAsync_CombinedParentAndChildFilters_BothDirectionsApplied() + { + // Arrange + var dbOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + using var context = new DataContext(dbOptions); + + var parentFieldId = Guid.NewGuid(); + var childFieldId = Guid.NewGuid(); + + var goodParent = Guid.NewGuid(); + var badParent = Guid.NewGuid(); + var goodChild = Guid.NewGuid(); + var badChild = Guid.NewGuid(); + + var unitAllGood = Guid.NewGuid(); + var unitBadParent = Guid.NewGuid(); + var unitBadChild = Guid.NewGuid(); + + context.Units.AddRange( + BuildUnit(goodParent, "GoodParent", parentFieldId, "GOOD-PARENT"), + BuildUnit(badParent, "BadParent", parentFieldId, "BAD-PARENT"), + BuildUnit(goodChild, "GoodChild", childFieldId, "GOOD-CHILD"), + BuildUnit(badChild, "BadChild", childFieldId, "BAD-CHILD"), + BuildUnit(unitAllGood, "Unit_AllGood", Guid.NewGuid(), "x"), + BuildUnit(unitBadParent, "Unit_BadParent", Guid.NewGuid(), "x"), + BuildUnit(unitBadChild, "Unit_BadChild", Guid.NewGuid(), "x") + ); + + context.UnitInUnits.AddRange( + // unitAllGood: хороший родитель + хороший ребёнок + new UnitInUnit { ParentUnitId = goodParent, ChildUnitId = unitAllGood, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = unitAllGood, ChildUnitId = goodChild, DateCreated = DateTimeOffset.UtcNow }, + // unitBadParent: плохой родитель + хороший ребёнок + new UnitInUnit { ParentUnitId = badParent, ChildUnitId = unitBadParent, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = unitBadParent, ChildUnitId = goodChild, DateCreated = DateTimeOffset.UtcNow }, + // unitBadChild: хороший родитель + плохой ребёнок + new UnitInUnit { ParentUnitId = goodParent, ChildUnitId = unitBadChild, DateCreated = DateTimeOffset.UtcNow }, + new UnitInUnit { ParentUnitId = unitBadChild, ChildUnitId = badChild, DateCreated = DateTimeOffset.UtcNow } + ); + + await context.SaveChangesAsync(); + SetupMocks(context); + + var filters = new List + { + new() + { + FieldId = parentFieldId, + ValueMask = "%GOOD-PARENT%", + IsParent = true, + IsInverse = false, + IsFullMatch = false + }, + new() + { + FieldId = childFieldId, + ValueMask = "%GOOD-CHILD%", + IsParent = false, + IsInverse = false, + IsFullMatch = false + } + }; + + // Act + var result = await _sut.MatchAsync( + new List { unitAllGood, unitBadParent, unitBadChild }, filters); + + // Assert + var resultIds = result.Select(c => c.UnitId).ToList(); + Assert.Contains(unitAllGood, resultIds); + Assert.DoesNotContain(unitBadParent, resultIds); + Assert.DoesNotContain(unitBadChild, resultIds); + } + } +} diff --git a/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs b/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs index 62c71430..c8cc70d2 100644 --- a/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs +++ b/PARR.DAL.Tests/Services/UnitFilterService/UnitFilterServiceTests.cs @@ -21,14 +21,6 @@ public class UnitFilterServiceTests { #region Helpers - /// - /// Создаёт тестовую сущность Unit с заданным значением поля. - /// Используется для подготовки данных в тестах фильтрации. - /// - /// Уникальный идентификатор юнита - /// Имя юнита для отладки - /// Идентификатор поля, к которому привязывается значение - /// Значение поля private static Unit BuildTestUnit(Guid id, string name, Guid fieldId, string fieldValue) { var fieldVal = new UnitFieldValue { Id = Guid.NewGuid(), Value = fieldValue }; @@ -49,10 +41,6 @@ public class UnitFilterServiceTests }; } - /// - /// Создаёт тестовый объект JobGroupType с кодом Umbrella. - /// Используется для тестирования логики групповых работ типа "зонтик". - /// private static JobGroupType BuildTestJobGroupType() => new() { Id = Guid.NewGuid(), @@ -61,10 +49,6 @@ public class UnitFilterServiceTests Description = "Test" }; - /// - /// Создаёт тестовый объект JobGroup с заполненными обязательными полями. - /// Используется для подготовки навигационных свойств в тестах. - /// private static JobGroup BuildTestJobGroup() => new() { Id = Guid.NewGuid(), @@ -79,20 +63,12 @@ public class UnitFilterServiceTests GroupType = BuildTestJobGroupType() }; - /// - /// Создаёт тестовый объект JobAutoControl с отключённым автоконтролем. - /// Используется для заполнения обязательного свойства в сущности Job. - /// private static JobAutoControl BuildTestAutoControl(Guid jobId) => new() { JobId = jobId, IsEnable = false }; - /// - /// Контейнер для моков репозиториев. - /// Упрощает передачу зависимостей в метод создания сервиса. - /// private class RepositoryMocks { public Mock Unit { get; set; } @@ -101,7 +77,6 @@ public class UnitFilterServiceTests public Mock UnitField { get; set; } public Mock Job { get; set; } public Mock Cache { get; set; } - // Новые зависимости после рефакторинга public Mock UnitService { get; set; } public Mock FieldMatcher { get; set; } public Mock RelationshipMatcher { get; set; } @@ -110,11 +85,6 @@ public class UnitFilterServiceTests public Mock NameResolver { get; set; } } - /// - /// Настраивает базовые моки репозиториев с использованием InMemory-контекста. - /// Возвращает объект с подготовленными моками для повторного использования в тестах. - /// - /// Экземпляр DataContext с тестовыми данными private static RepositoryMocks ArrangeRepositoryMocks(DataContext context) { var unitRepoMock = new Mock(); @@ -126,16 +96,6 @@ public class UnitFilterServiceTests unitInUnitRepoMock.Setup(r => r.Get()).Returns(context.UnitInUnits.AsQueryable()); var unitInValueRepoMock = new Mock(); - // Единый подход: используем контекст для поддержки IAsyncQueryProvider - // Это гарантирует корректную работу .ToListAsync() внутри сервиса - unitInValueRepoMock.Setup(r => r.GetMatchingTargetIds(It.IsAny(), It.IsAny())) - .Returns((Guid fId, string mask) => - context.UnitInValues - .AsNoTracking() - .Where(uv => uv.FieldId == fId && uv.Value != null) - .Select(uv => uv.UnitId) - .Distinct() - .AsQueryable()); var unitFieldRepoMock = new Mock(); unitFieldRepoMock.Setup(r => r.Get()).Returns(new List().AsQueryable()); @@ -154,46 +114,60 @@ public class UnitFilterServiceTests var resultLoaderMock = new Mock(); var nameResolverMock = new Mock(); - // Настройка NameResolver: возвращаем все ID юнитов из контекста (эмуляция кэш-промаха + БД) nameResolverMock.Setup(r => r.ResolveAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((string mask, CancellationToken ct) => context.Units.Select(u => u.Id).ToList()); - // Настройка FieldMatcher: эмуляция SQL-фильтрации через InMemory-контекст - fieldMatcherMock.Setup(r => r.MatchAsync(It.IsAny>(), It.IsAny>(), It.IsAny())) + // По умолчанию FieldMatcher пропускает все юниты без изменений. + // В конкретных тестах переопределяется для эмуляции фильтрации. + fieldMatcherMock.Setup(r => r.MatchAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => ids.ToList()); - // Настройка RelationshipMatcher: возвращаем контексты без изменений - relationshipMatcherMock.Setup(r => r.MatchAsync(It.IsAny>(), It.IsAny>(), It.IsAny())) + // По умолчанию RelationshipMatcher возвращает контексты без изменений. + // В конкретных тестах переопределяется для эмуляции фильтрации связей. + relationshipMatcherMock.Setup(r => r.MatchAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => - ids.Select(id => new UnitFilterMatchResult { UnitId = id }).ToList()); + ids.Select(id => new UnitFilterMatchResult + { + UnitId = id, + ValidParentIds = new HashSet(), + ValidChildIds = new HashSet() + }).ToList()); - // Настройка UmbrellaFilter: пропускаем без изменений umbrellaFilterMock.Setup(r => r.Apply(It.IsAny>(), It.IsAny())) .Returns((List ctx, Job j) => ctx); - // Настройка ResultLoader: формируем DTO из контекста resultLoaderMock.Setup(r => r.LoadAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync((List contexts, CancellationToken ct) => { var unitIds = contexts.Select(c => c.UnitId).ToHashSet(); var units = context.Units.Where(u => unitIds.Contains(u.Id)).ToList(); - return units.Select(u => new UnitFilterResultDto + return units.Select(u => { - Id = u.Id, - Name = u.Name, - Values = u.UnitValues?.Select(v => new UnitValueDto + var ctx = contexts.First(c => c.UnitId == u.Id); + return new UnitFilterResultDto { - FieldId = v.FieldId, - Value = v.Value?.Value - }).ToList() ?? new List(), - Parents = contexts.First(c => c.UnitId == u.Id).ValidParentIds - .Select(pid => new RelatedUnitDto { UnitId = pid }) - .ToList(), - Children = contexts.First(c => c.UnitId == u.Id).ValidChildIds - .Select(cid => new RelatedUnitDto { UnitId = cid }) - .ToList() + Id = u.Id, + Name = u.Name, + Values = u.UnitValues?.Select(v => new UnitValueDto + { + FieldId = v.FieldId, + Value = v.Value?.Value + }).ToList() ?? new List(), + Parents = (ctx.ValidParentIds ?? new HashSet()) + .Select(pid => new RelatedUnitDto { UnitId = pid }) + .ToList(), + Children = (ctx.ValidChildIds ?? new HashSet()) + .Select(cid => new RelatedUnitDto { UnitId = cid }) + .ToList() + }; }).ToList(); }); @@ -214,12 +188,6 @@ public class UnitFilterServiceTests }; } - /// - /// Создаёт экземпляр тестируемого сервиса (SUT — System Under Test). - /// Инкапсулирует логику конструктора для упрощения тестов. - /// - /// Подготовленные моки репозиториев - /// Экземпляр логгера для сервиса private static Core.Services.UnitFilterService.UnitFilterService CreateSut( RepositoryMocks mocks, ILogger logger) @@ -245,8 +213,9 @@ public class UnitFilterServiceTests /// - /// Проверяет, что юниты, не прошедшие фильтрацию по полю (FieldFilter) - /// или по связям (RelationshipFilter), корректно исключаются из результата. + /// Проверяет, что юниты, не прошедшие фильтрацию по полю или по связям, + /// корректно исключаются из результата. + /// Логика фильтрации эмулируется через моки FieldMatcher и RelationshipMatcher. /// [Fact] public async Task GetUnitsByJobFilterAsync_UnitsWithInvalidFieldOrRelationshipValue_AreExcluded() @@ -260,30 +229,24 @@ public class UnitFilterServiceTests var jobId = Guid.NewGuid(); var fieldId = Guid.NewGuid(); var validParentId = Guid.NewGuid(); - var wrongParentId = Guid.NewGuid(); var unitPassesAll = Guid.NewGuid(); var unitFailsFieldFilter = Guid.NewGuid(); var unitFailsRelationshipFilter = Guid.NewGuid(); - // Подготовка тестовых данных: юниты context.Units.AddRange( BuildTestUnit(unitPassesAll, "Unit_PassesAll", fieldId, "Accepted Value"), BuildTestUnit(unitFailsFieldFilter, "Unit_FailsField", fieldId, "Rejected Value"), BuildTestUnit(unitFailsRelationshipFilter, "Unit_FailsRel", fieldId, "Accepted Value"), - BuildTestUnit(validParentId, "Parent_Valid", fieldId, "Valid Parent"), - BuildTestUnit(wrongParentId, "Parent_Wrong", fieldId, "Wrong Parent") + BuildTestUnit(validParentId, "Parent_Valid", fieldId, "Valid Parent") ); - // Подготовка тестовых данных: связи между юнитами context.UnitInUnits.AddRange( - new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitPassesAll, DateCreated = DateTimeOffset.UtcNow }, - new UnitInUnit { ParentUnitId = wrongParentId, ChildUnitId = unitFailsRelationshipFilter, DateCreated = DateTimeOffset.UtcNow } + new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitPassesAll, DateCreated = DateTimeOffset.UtcNow } ); await context.SaveChangesAsync(); - // Формируем тестовый объект Job с фильтрами var job = new Job { Id = jobId, @@ -318,20 +281,29 @@ public class UnitFilterServiceTests } }; - // Настройка моков репозиториев var mocks = ArrangeRepositoryMocks(context); - // Переопределяем мок фильтрации по полю: в реальном коде используется EF.Functions.ILike, - // в тесте заменяем на прямое сравнение строк для предсказуемости - mocks.Unit.Setup(r => r.GetUnitByFieldAndValue(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns((IQueryable q, Guid fId, string mask, bool inv) => - q.Where(u => u.UnitValues != null && u.UnitValues.Any(v => v.FieldId == fId && v.Value != null && v.Value.Value == "Accepted Value"))); + // Эмуляция FieldMatcher: пропускает только юниты со значением "Accepted Value" + mocks.FieldMatcher.Setup(r => r.MatchAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => + ids.Where(id => id == unitPassesAll || id == unitFailsRelationshipFilter).ToList()); - // ИСПРАВЛЕНО: используем запрос к контексту вместо массива. - // Массивный AsQueryable() не поддерживает IAsyncQueryProvider, что вызывает крах при вызове .ToListAsync() внутри сервиса. - // Запрос к InMemory DbSet гарантирует корректную асинхронную материализацию. - mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(fieldId, It.IsAny())) - .Returns(context.Units.Where(u => u.Id == validParentId).Select(u => u.Id).AsQueryable()); + // Эмуляция RelationshipMatcher: пропускает только unitPassesAll + mocks.RelationshipMatcher.Setup(r => r.MatchAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => + ids.Where(id => id == unitPassesAll) + .Select(id => new UnitFilterMatchResult + { + UnitId = id, + ValidParentIds = new HashSet { validParentId }, + ValidChildIds = new HashSet() + }).ToList()); var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); @@ -354,8 +326,8 @@ public class UnitFilterServiceTests } /// - /// Проверяет, что юниты исключаются, если их единственный родительский юнит - /// содержит запрещённое значение тега (сценарий с IsInverse = true). + /// Проверяет, что юниты исключаются, если их родитель содержит запрещённое значение (IsInverse = true). + /// Логика фильтрации связей эмулируется через мок RelationshipMatcher. /// [Fact] public async Task GetUnitsByJobFilterAsync_ParentWithForbiddenTag_UnitIsExcluded() @@ -373,7 +345,6 @@ public class UnitFilterServiceTests var unitWithValidParent = Guid.NewGuid(); var unitWithInvalidParent = Guid.NewGuid(); - // Подготовка тестовых данных: юниты context.Units.AddRange( BuildTestUnit(validParentId, "Parent_Valid", tagFieldId, "ОТВ.ЭК"), BuildTestUnit(invalidParentId, "Parent_Invalid", tagFieldId, "ПАРР-РРПТК-ОТВ.ЭК"), @@ -381,7 +352,6 @@ public class UnitFilterServiceTests BuildTestUnit(unitWithInvalidParent, "Unit_Invalid", Guid.NewGuid(), "Val") ); - // Подготовка тестовых данных: связи между юнитами context.UnitInUnits.AddRange( new UnitInUnit { ParentUnitId = validParentId, ChildUnitId = unitWithValidParent, DateCreated = DateTimeOffset.UtcNow }, new UnitInUnit { ParentUnitId = invalidParentId, ChildUnitId = unitWithInvalidParent, DateCreated = DateTimeOffset.UtcNow } @@ -389,8 +359,6 @@ public class UnitFilterServiceTests await context.SaveChangesAsync(); - // Формируем тестовый объект Job с настройкой IsInverse = true - // IsInverse = true означает: исключить родителей, которые СОВПАДАЮТ с маской var job = new Job { Id = jobId, @@ -430,20 +398,21 @@ public class UnitFilterServiceTests } }; - // Настройка моков репозиториев var mocks = ArrangeRepositoryMocks(context); - // Переопределяем мок: возвращаем только родителей с запрещённым тегом. - // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты. - // IsInverse = true в сервисе инвертирует результат, поэтому эти родители будут исключены. - mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny())) - .Returns((Guid fId, string mask) => - context.UnitInValues - .AsNoTracking() - .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("ПАРР-РРПТК-ОТВ.ЭК", StringComparison.OrdinalIgnoreCase)) - .Select(uv => uv.UnitId) - .Distinct() - .AsQueryable()); + // Эмуляция RelationshipMatcher: исключает unitWithInvalidParent + mocks.RelationshipMatcher.Setup(r => r.MatchAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => + ids.Where(id => id == unitWithValidParent) + .Select(id => new UnitFilterMatchResult + { + UnitId = id, + ValidParentIds = new HashSet { validParentId }, + ValidChildIds = new HashSet() + }).ToList()); var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); @@ -459,8 +428,8 @@ public class UnitFilterServiceTests } /// - /// Проверяет логику IsFullMatch = true: юнит исключается, - /// если хотя бы один из его родительских юнитов не соответствует фильтру. + /// Проверяет логику IsFullMatch = true: юнит исключается, + /// если хотя бы один из его родителей не соответствует фильтру. /// [Fact] public async Task GetUnitsByJobFilterAsync_IsFullMatch_UnitWithAnyNonMatchingParent_IsExcluded() @@ -478,7 +447,6 @@ public class UnitFilterServiceTests var unitMixedId = Guid.NewGuid(); var unitCleanId = Guid.NewGuid(); - // Подготовка тестовых данных: юниты context.Units.AddRange( BuildTestUnit(parentGoodId, "Parent_Good", checkFieldId, "Good Parent"), BuildTestUnit(parentBadId, "Parent_Bad", checkFieldId, "Bad Parent"), @@ -486,7 +454,6 @@ public class UnitFilterServiceTests BuildTestUnit(unitCleanId, "Unit_Clean", Guid.NewGuid(), "Clean") ); - // Подготовка тестовых данных: связи между юнитами context.UnitInUnits.AddRange( new UnitInUnit { ParentUnitId = parentGoodId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow }, new UnitInUnit { ParentUnitId = parentBadId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow }, @@ -495,8 +462,6 @@ public class UnitFilterServiceTests await context.SaveChangesAsync(); - // Формируем тестовый объект Job с настройкой IsFullMatch = true - // IsFullMatch = true означает: ВСЕ родители должны соответствовать фильтру var job = new Job { Id = jobId, @@ -536,19 +501,21 @@ public class UnitFilterServiceTests } }; - // Настройка моков репозиториев var mocks = ArrangeRepositoryMocks(context); - // Переопределяем мок: возвращаем только родителей, соответствующих маске "Good". - // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты. - mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(checkFieldId, It.IsAny())) - .Returns((Guid fId, string mask) => - context.UnitInValues - .AsNoTracking() - .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("Good", StringComparison.OrdinalIgnoreCase)) - .Select(uv => uv.UnitId) - .Distinct() - .AsQueryable()); + // Эмуляция RelationshipMatcher: исключает unitMixedId (один из родителей не проходит) + mocks.RelationshipMatcher.Setup(r => r.MatchAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => + ids.Where(id => id == unitCleanId) + .Select(id => new UnitFilterMatchResult + { + UnitId = id, + ValidParentIds = new HashSet { parentGoodId }, + ValidChildIds = new HashSet() + }).ToList()); var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); @@ -563,10 +530,9 @@ public class UnitFilterServiceTests Assert.DoesNotContain(unitMixedId, resultIds); } - /// /// Проверяет комбинацию флагов: IsParent=true, IsInverse=true, IsFullMatch=true. - /// Логика: юнит проходит, только если НИ ОДИН из его родителей не содержит запрещённое значение. + /// Юнит проходит, только если ни один из его родителей не содержит запрещённое значение. /// [Fact] public async Task GetUnitsByJobFilterAsync_IsParentAndInverseAndFullMatch_UnitWithAnyForbiddenParent_IsExcluded() @@ -579,12 +545,11 @@ public class UnitFilterServiceTests var jobId = Guid.NewGuid(); var tagFieldId = Guid.NewGuid(); - var parentForbiddenId = Guid.NewGuid(); // Родитель с запрещённым тегом - var parentCleanId = Guid.NewGuid(); // Родитель без запрещённого тега - var unitMixedId = Guid.NewGuid(); // Юнит с одним "плохим" и одним "хорошим" родителем - var unitAllCleanId = Guid.NewGuid(); // Юнит только с "хорошими" родителями + var parentForbiddenId = Guid.NewGuid(); + var parentCleanId = Guid.NewGuid(); + var unitMixedId = Guid.NewGuid(); + var unitAllCleanId = Guid.NewGuid(); - // Подготовка тестовых данных: юниты context.Units.AddRange( BuildTestUnit(parentForbiddenId, "Parent_Forbidden", tagFieldId, "FORBIDDEN-TAG"), BuildTestUnit(parentCleanId, "Parent_Clean", tagFieldId, "CLEAN-TAG"), @@ -592,9 +557,6 @@ public class UnitFilterServiceTests BuildTestUnit(unitAllCleanId, "Unit_AllClean", Guid.NewGuid(), "AllClean") ); - // Подготовка тестовых данных: связи - // unitMixed имеет обоих родителей — один с запрещённым тегом, один без - // unitAllClean имеет только "чистого" родителя context.UnitInUnits.AddRange( new UnitInUnit { ParentUnitId = parentForbiddenId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow }, new UnitInUnit { ParentUnitId = parentCleanId, ChildUnitId = unitMixedId, DateCreated = DateTimeOffset.UtcNow }, @@ -603,10 +565,6 @@ public class UnitFilterServiceTests await context.SaveChangesAsync(); - // Формируем тестовый объект Job с комбинацией флагов: - // IsParent = true: проверяем родительские связи - // IsInverse = true: исключаем родителей, которые СОВПАДАЮТ с маской - // IsFullMatch = true: ВСЕ родители должны пройти фильтр (ни один не должен совпасть с маской) var job = new Job { Id = jobId, @@ -646,21 +604,21 @@ public class UnitFilterServiceTests } }; - // Настройка моков репозиториев var mocks = ArrangeRepositoryMocks(context); - // Переопределяем мок: возвращаем только родителей, содержащих "FORBIDDEN". - // В реальном коде используется EF.Functions.ILike, в тесте — .Contains() для простоты. - // Из-за IsInverse=true эти родители будут исключены из "валидных". - // Из-за IsFullMatch=true юнит пройдёт, только если ВСЕ его родители валидны. - mocks.UnitInValue.Setup(r => r.GetMatchingTargetIds(tagFieldId, It.IsAny())) - .Returns((Guid fId, string mask) => - context.UnitInValues - .AsNoTracking() - .Where(uv => uv.FieldId == fId && uv.Value != null && uv.Value.Value.Contains("FORBIDDEN", StringComparison.OrdinalIgnoreCase)) - .Select(uv => uv.UnitId) - .Distinct() - .AsQueryable()); + // Эмуляция RelationshipMatcher: исключает unitMixedId (есть родитель с запрещённым тегом) + mocks.RelationshipMatcher.Setup(r => r.MatchAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync((IReadOnlyList ids, IEnumerable filters, CancellationToken ct) => + ids.Where(id => id == unitAllCleanId) + .Select(id => new UnitFilterMatchResult + { + UnitId = id, + ValidParentIds = new HashSet { parentCleanId }, + ValidChildIds = new HashSet() + }).ToList()); var logger = NullLoggerFactory.Instance.CreateLogger(); var service = CreateSut(mocks, logger); @@ -671,15 +629,7 @@ public class UnitFilterServiceTests // === Assert === var resultIds = result.Select(u => u.Id).ToList(); - // unitAllCleanId должен остаться: у него один родитель, и он не содержит запрещённый тег Assert.Contains(unitAllCleanId, resultIds); - - // unitMixedId должен быть исключён: у него есть родитель с запрещённым тегом. - // Логика: - // 1. GetMatchingTargetIds возвращает {parentForbiddenId} - // 2. IsInverse=true → валидные родители = все.Кроме({parentForbiddenId}) = {parentCleanId} - // 3. IsFullMatch=true → проверяем: все родители {parentForbiddenId, parentCleanId} входят в {parentCleanId}? Нет. - // 4. Юнит исключается. Assert.DoesNotContain(unitMixedId, resultIds); } } \ No newline at end of file diff --git a/PARR.DAL/Repositories/Unit/UnitFieldValueRepository.cs b/PARR.DAL/Repositories/Unit/UnitFieldValueRepository.cs index 1a42aee3..b7429ae4 100644 --- a/PARR.DAL/Repositories/Unit/UnitFieldValueRepository.cs +++ b/PARR.DAL/Repositories/Unit/UnitFieldValueRepository.cs @@ -11,16 +11,18 @@ namespace PARR.DAL.Repositories.Unit { public UnitFieldValueRepository(DataContext dataContext, ILogger logger) : base(logger, dataContext) { } - - public async Task GetByValueNameAsync(string? value) + public async Task> FindValueIdsByMaskAsync( + string mask, + CancellationToken ct = default) { - var query = EntitySet - .Include(v => v.FieldValues); + var query = EntitySet.AsNoTracking(); - if (string.IsNullOrWhiteSpace(value)) - return await query.FirstOrDefaultAsync(uf => uf.Value == null); + var valueIds = await query + .Where(v => EF.Functions.ILike(v.Value!, mask)) + .Select(v => v.Id) + .ToListAsync(ct); - return await query.FirstOrDefaultAsync(uf => uf.Value!.ToLower().Trim() == value.ToLower().Trim()); + return valueIds; } } } diff --git a/PARR.DAL/Repositories/Unit/UnitRepository.cs b/PARR.DAL/Repositories/Unit/UnitRepository.cs index f4d65610..9a31da49 100644 --- a/PARR.DAL/Repositories/Unit/UnitRepository.cs +++ b/PARR.DAL/Repositories/Unit/UnitRepository.cs @@ -11,7 +11,6 @@ namespace PARR.DAL.Repositories.Unit { public UnitRepository(DataContext dataContext, ILogger logger) : base(logger, dataContext) { } - public IQueryable GetWithIncludes() { return Get() @@ -22,40 +21,30 @@ namespace PARR.DAL.Repositories.Unit } - public IQueryable GetUnitByFieldAndValue(IQueryable query, Guid fieldId, string valueMask, bool isInverse = false) - { - //TODO: вынесено из UnitFilterService - - if (isInverse) - { - query = query.Where(u => !u.UnitValues.Any(v => - v.FieldId == fieldId && - EF.Functions.ILike(v.Value.Value, valueMask))); - } - else - { - query = query.Where(u => u.UnitValues.Any(v => - v.FieldId == fieldId && - EF.Functions.ILike(v.Value.Value, valueMask))); - } - - return query; - } - public IQueryable GetInitialUnitIds(string dbValueMask) { - //TODO: вынесено из UnitFilterService - - //var initialUnitIds = await unitService.Get().AsNoTracking() - //.Where(unit => EF.Functions.ILike(unit.Name, dbValueMask)) - //.Select(u => u.Id) - //.ToListAsync(cancellationToken); - return Get().AsNoTracking() .Where(unit => EF.Functions.ILike(unit.Name, dbValueMask)) .Select(u => u.Id); } + public async Task> FindUnitIdsByValueIdsAsync( + IReadOnlyList unitIds, + Guid fieldId, + IReadOnlyList valueIds, + CancellationToken ct = default) + { + if (valueIds.Count == 0) + return new List(); + + return await EntitySet.AsNoTracking() + .Where(u => unitIds.Contains(u.Id)) + .Where(u => u.UnitValues.Any(uv => + uv.FieldId == fieldId && + valueIds.Contains(uv.ValueId))) + .Select(u => u.Id) + .ToListAsync(ct); + } } } diff --git a/PARR.Test/Worker.cs b/PARR.Test/Worker.cs index 703d9518..4abf4521 100644 --- a/PARR.Test/Worker.cs +++ b/PARR.Test/Worker.cs @@ -1,7 +1,8 @@ -using Microsoft.EntityFrameworkCore; using PARR.Core.Common.Interfaces; using PARR.Core.Repositories.Interfaces.JobGroupRepositories; +using PARR.Core.Repositories.Interfaces.JobRepositories; using PARR.Domain.Entities.Base.History; +using PARR.Domain.Entities.JobEntities; using PARR.Domain.Enums; using PARR.EsppApi; using PARR.EsppApi.Constants; @@ -181,7 +182,7 @@ namespace PARR.Test { //Guid.Parse("cfb0d2dd-192a-4ed5-a29c-25f0abf61895"),//11010 //Guid.Parse("79976eaf-be7b-42a0-a4e3-a89d7f68a18e"),//17 - Guid.Parse("6ff1de05-80c3-4b38-846b-0c793fd7fc8c")//318 + Guid.Parse("055ac0cc-10f7-4f96-baf6-9c8145b1332a")//318 }; if (!jobIds.Any()) @@ -235,22 +236,33 @@ namespace PARR.Test var templateMatcher = scope.ServiceProvider.GetRequiredService(); var groupRepository = scope.ServiceProvider.GetRequiredService(); - var groups = await groupRepository.Get().AsNoTracking() - .Where(t => t.GroupType != null && t.GroupType.Code == JobGroupTypesEnum.Group) - .Select(t => t.Id).ToHashSetAsync(); + await templateMatcher.SyncTemplatesForJob(Guid.Parse("055ac0cc-10f7-4f96-baf6-9c8145b1332a"), + new HistoryInitiator + { + InitiatorIp = "10.99.246.156", + InitiatorParrComponentId = ParrComponentsEnum.Master, + InitiatorComment = "Тестовый проект. Синхронизация Группированых работ" - foreach (var groupId in groups) - { - await templateMatcher.SyncTemplatesForJobGroup(groupId, - new HistoryInitiator - { - InitiatorIp = "10.99.246.156", - InitiatorParrComponentId = ParrComponentsEnum.Master, - InitiatorComment = "Тестовый проект. Синхронизация Группированых работ" + } + ); - } - ); - } + //var groups = await groupRepository.Get().AsNoTracking() + // .Where(t => t.GroupType != null && t.GroupType.Code == JobGroupTypesEnum.Group) + // .Select(t => t.Id).ToHashSetAsync(); + + + //foreach (var groupId in groups) + //{ + // await templateMatcher.SyncTemplatesForJobGroup(groupId, + // new HistoryInitiator + // { + // InitiatorIp = "10.99.246.156", + // InitiatorParrComponentId = ParrComponentsEnum.Master, + // InitiatorComment = "Тестовый проект. Синхронизация Группированых работ" + + // } + // ); + //} return; } diff --git a/PARR.Test/appsettings.json b/PARR.Test/appsettings.json index 342549d6..5783f4ce 100644 --- a/PARR.Test/appsettings.json +++ b/PARR.Test/appsettings.json @@ -5,7 +5,7 @@ }, "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.Hosting.Lifetime": "Information" } },