This repository has been archived by the owner on Mar 16, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add OwnerSetComparer which detects ownership changes
Progress on NuGet/NuGetGallery#6475
- Loading branch information
1 parent
dedbbcc
commit e506ec6
Showing
6 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
src/NuGet.Services.AzureSearch/Owners2AzureSearch/IOwnerSetComparer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
|
||
namespace NuGet.Services.AzureSearch.Owners2AzureSearch | ||
{ | ||
public interface IOwnerSetComparer | ||
{ | ||
SortedDictionary<string, SortedSet<string>> Compare( | ||
SortedDictionary<string, SortedSet<string>> oldData, | ||
SortedDictionary<string, SortedSet<string>> newData); | ||
} | ||
} |
97 changes: 97 additions & 0 deletions
97
src/NuGet.Services.AzureSearch/Owners2AzureSearch/OwnerSetComparer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace NuGet.Services.AzureSearch.Owners2AzureSearch | ||
{ | ||
public class OwnerSetComparer : IOwnerSetComparer | ||
{ | ||
private readonly ILogger _logger; | ||
|
||
public OwnerSetComparer(ILogger logger) | ||
{ | ||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
} | ||
|
||
public SortedDictionary<string, SortedSet<string>> Compare( | ||
SortedDictionary<string, SortedSet<string>> oldData, | ||
SortedDictionary<string, SortedSet<string>> newData) | ||
{ | ||
// If there is no old data, the new data is the same as the set of changes. | ||
if (oldData.Count == 0) | ||
{ | ||
return newData; | ||
} | ||
|
||
// We use a very simplistic algorithm here. Perform one pass on the new data to find the added or changed | ||
// package IDs. Then perform a second pass on the old data to find removed package IDs. We can optimize | ||
// this later if necessary. | ||
// | ||
// We emit all of the usernames when a package ID's owners have changed instead of the delta. This is | ||
// because Azure Search does not have a way to append or remove a specific item from a field that is an | ||
// array. The entire new array needs to be provided. | ||
var builder = new PackageIdToOwnersBuilder(_logger); | ||
|
||
// First pass: find added or changed sets. | ||
foreach (var pair in newData) | ||
{ | ||
var id = pair.Key; | ||
var newOwners = pair.Value; | ||
if (!oldData.TryGetValue(id, out var oldOwners)) | ||
{ | ||
// ADDED: The package ID does not exist in the old data, which means the package ID was added. | ||
builder.Add(id, newOwners); | ||
_logger.LogInformation( | ||
"The package ID {ID} has been added, with {AddedCount} owners.", | ||
id, | ||
newOwners.Count); | ||
} | ||
else | ||
{ | ||
// The package ID exists in the old data. We need to check if the owner set has changed. Perform | ||
// an ordinal comparison to allow username case changes to flow through. | ||
var removedUsernames = oldOwners.Except(newOwners, StringComparer.Ordinal).ToList(); | ||
var addedUsernames = newOwners.Except(oldOwners, StringComparer.Ordinal).ToList(); | ||
|
||
if (removedUsernames.Any() || addedUsernames.Any()) | ||
{ | ||
// CHANGED: The username set has changed. | ||
builder.Add(id, newOwners); | ||
_logger.LogInformation( | ||
"The package ID {ID} has an ownership change, with {RemovedCount} owners removed and " + | ||
"{AddedCount} owners added.", | ||
id, | ||
removedUsernames.Count, | ||
addedUsernames.Count); | ||
} | ||
} | ||
} | ||
|
||
var result = builder.GetResult(); | ||
|
||
// Second pass: find removed sets. | ||
foreach (var pair in oldData) | ||
{ | ||
var id = pair.Key; | ||
var oldOwners = pair.Value; | ||
|
||
if (!newData.TryGetValue(id, out var newOwners)) | ||
{ | ||
// REMOVED: The package ID does not exist in the new data, which means the package ID was removed. | ||
result.Add(id, new SortedSet<string>()); | ||
_logger.LogInformation( | ||
"The package ID {ID} has been removed, with {RemovedCount} owners", | ||
id, | ||
oldOwners.Count); | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
tests/NuGet.Services.AzureSearch.Tests/Owners2AzureSearch/OwnerSetComparerFacts.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq; | ||
using NuGet.Services.AzureSearch.Support; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace NuGet.Services.AzureSearch.Owners2AzureSearch | ||
{ | ||
public class OwnerSetComparerFacts | ||
{ | ||
public class Compare : Facts | ||
{ | ||
public Compare(ITestOutputHelper output) : base(output) | ||
{ | ||
} | ||
|
||
[Fact] | ||
public void FindsAddedPackageIds() | ||
{ | ||
var oldData = Data("NuGet.Core: NuGet, Microsoft"); | ||
var newData = Data("NuGet.Core: NuGet, Microsoft", | ||
"NuGet.Versioning: NuGet, Microsoft"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
var pair = Assert.Single(changes); | ||
Assert.Equal("NuGet.Versioning", pair.Key); | ||
Assert.Equal(new[] { "Microsoft", "NuGet" }, pair.Value.ToArray()); | ||
} | ||
|
||
|
||
[Fact] | ||
public void FindsRemovedPackageIds() | ||
{ | ||
var oldData = Data("NuGet.Core: NuGet, Microsoft", | ||
"NuGet.Versioning: NuGet, Microsoft"); | ||
var newData = Data("NuGet.Core: NuGet, Microsoft"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
var pair = Assert.Single(changes); | ||
Assert.Equal("NuGet.Versioning", pair.Key); | ||
Assert.Empty(pair.Value); | ||
} | ||
|
||
[Fact] | ||
public void FindsAddedOwner() | ||
{ | ||
var oldData = Data("NuGet.Core: NuGet"); | ||
var newData = Data("NuGet.Core: NuGet, Microsoft"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
var pair = Assert.Single(changes); | ||
Assert.Equal("NuGet.Core", pair.Key); | ||
Assert.Equal(new[] { "Microsoft", "NuGet" }, pair.Value.ToArray()); | ||
} | ||
|
||
[Fact] | ||
public void FindsRemovedOwner() | ||
{ | ||
var oldData = Data("NuGet.Core: NuGet, Microsoft"); | ||
var newData = Data("NuGet.Core: NuGet"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
var pair = Assert.Single(changes); | ||
Assert.Equal("NuGet.Core", pair.Key); | ||
Assert.Equal(new[] { "NuGet" }, pair.Value.ToArray()); | ||
} | ||
|
||
[Fact] | ||
public void FindsOwnerWithChangedCase() | ||
{ | ||
var oldData = Data("NuGet.Core: NuGet, Microsoft"); | ||
var newData = Data("NuGet.Core: NuGet, microsoft"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
var pair = Assert.Single(changes); | ||
Assert.Equal("NuGet.Core", pair.Key); | ||
Assert.Equal(new[] { "microsoft", "NuGet" }, pair.Value.ToArray()); | ||
} | ||
|
||
[Fact] | ||
public void FindsManyChangesAtOnce() | ||
{ | ||
var oldData = Data("NuGet.Core: NuGet, Microsoft", | ||
"NuGet.Frameworks: NuGet", | ||
"NuGet.Protocol: NuGet"); | ||
var newData = Data("NuGet.Core: NuGet, microsoft", | ||
"NuGet.Versioning: NuGet", | ||
"NuGet.Protocol: NuGet"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
Assert.Equal(3, changes.Count); | ||
Assert.Equal(new[] { "NuGet.Core", "NuGet.Frameworks", "NuGet.Versioning" }, changes.Keys.ToArray()); | ||
Assert.Equal(new[] { "microsoft", "NuGet" }, changes["NuGet.Core"].ToArray()); | ||
Assert.Empty(changes["NuGet.Frameworks"]); | ||
Assert.Equal(new[] { "NuGet" }, changes["NuGet.Versioning"].ToArray()); | ||
} | ||
|
||
[Fact] | ||
public void FindsNoChanges() | ||
{ | ||
var oldData = Data("NuGet.Core: NuGet, Microsoft", | ||
"NuGet.Versioning: NuGet, Microsoft"); | ||
var newData = Data("NuGet.Core: NuGet, Microsoft", | ||
"NuGet.Versioning: NuGet, Microsoft"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
Assert.Empty(changes); | ||
} | ||
|
||
[Fact] | ||
public void UsesNewDataAsChangesWhenOldDataIsEmpty() | ||
{ | ||
var oldData = Data(); | ||
var newData = Data("NuGet.Core: NuGet, Microsoft", | ||
"NuGet.Versioning: NuGet, Microsoft"); | ||
|
||
var changes = Target.Compare(oldData, newData); | ||
|
||
Assert.Same(newData, changes); | ||
} | ||
} | ||
|
||
public abstract class Facts | ||
{ | ||
public Facts(ITestOutputHelper output) | ||
{ | ||
Logger = output.GetLogger<OwnerSetComparer>(); | ||
|
||
Target = new OwnerSetComparer(Logger); | ||
} | ||
|
||
public RecordingLogger<OwnerSetComparer> Logger { get; } | ||
public OwnerSetComparer Target { get; } | ||
|
||
/// <summary> | ||
/// A helper to turn lines formatted like this "PackageId: OwnerA, OwnerB" into package ID to owners | ||
/// dictionary. | ||
/// </summary> | ||
public SortedDictionary<string, SortedSet<string>> Data(params string[] lines) | ||
{ | ||
var builder = new PackageIdToOwnersBuilder(Logger); | ||
foreach (var line in lines) | ||
{ | ||
var pieces = line.Split(new[] { ':' }, 2); | ||
var id = pieces[0].Trim(); | ||
var usernames = pieces[1] | ||
.Split(',') | ||
.Select(x => x.Trim()) | ||
.Where(x => x.Length > 0); | ||
|
||
builder.Add(id, usernames); | ||
} | ||
|
||
return builder.GetResult(); | ||
} | ||
} | ||
} | ||
} |