Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Microsoft.DotNet.DockerTools.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<Project Path="eng/src/file-pusher/file-pusher.csproj" />
<Project Path="eng/src/yaml-updater/yaml-updater.csproj" />
<Project Path="src/ImageBuilder/Microsoft.DotNet.ImageBuilder.csproj" />
<Project Path="src/GitAutomation/Microsoft.DotNet.GitAutomation.csproj" />
<Project Path="src/GitAutomation.Tests/Microsoft.DotNet.GitAutomation.Tests.csproj" />
<Project Path="src/ImageBuilder.Models/Microsoft.DotNet.ImageBuilder.Models.csproj" />
<Project Path="src/ImageBuilder.Tests/Microsoft.DotNet.ImageBuilder.Tests.csproj" />
</Solution>
1 change: 1 addition & 0 deletions eng/pipelines/docker-tools-packages-official.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ trigger:
include:
- eng/common/*
- eng/pipelines/*
- src/GitAutomation/*
- src/ImageBuilder/*
- src/ImageBuilder.Models/*
- Directory.Build.props
Expand Down
1 change: 1 addition & 0 deletions eng/pipelines/docker-tools-packages-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pr:
include:
- eng/common/*
- eng/pipelines/*
- src/GitAutomation/*
- src/ImageBuilder/*
- src/ImageBuilder.Models/*
- Directory.Build.props
Expand Down
77 changes: 77 additions & 0 deletions src/GitAutomation.Tests/DependencyInjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Microsoft.DotNet.GitAutomation.Tests;

[TestClass]
public sealed class DependencyInjectionTests
{
[TestMethod]
public void PullRequestManager_ResolvesWithDefaultServices()
{
ServiceCollection services = new();
services.AddPullRequestAutomation(
new AutomationIdentity("bot", "bot@example.com"),
"token");

using ServiceProvider serviceProvider = services.BuildServiceProvider();

PullRequestManager manager = serviceProvider.GetRequiredService<PullRequestManager>();

Assert.IsNotNull(manager);
}

[TestMethod]
public void PullRequestManager_ResolvesWithCustomServices()
{
ServiceCollection services = new();
bool processRunnerResolved = false;
bool accessTokenProviderResolved = false;
services.AddSingleton<IProcessRunner>(_ =>
{
processRunnerResolved = true;
return new StubProcessRunner();
});
services.AddSingleton<IGitAccessTokenProvider>(_ =>
{
accessTokenProviderResolved = true;
return new StaticGitAccessTokenProvider("token");
});
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
services.AddSingleton(new AutomationIdentity("bot", "bot@example.com"));
services.AddSingleton<PullRequestManager>();

using ServiceProvider serviceProvider = services.BuildServiceProvider();

PullRequestManager manager = serviceProvider.GetRequiredService<PullRequestManager>();

Assert.IsNotNull(manager);
Assert.IsTrue(processRunnerResolved);
Assert.IsTrue(accessTokenProviderResolved);
}

[TestMethod]
public void PullRequestManager_CanBeCreatedWithoutDependencyInjection()
{
PullRequestManager manager = new(
"token",
new AutomationIdentity("bot", "bot@example.com"));

Assert.IsNotNull(manager);
}

private sealed class StubProcessRunner : IProcessRunner
{
public Task<ProcessResult> RunAsync(
string? workingDirectory,
string fileName,
IEnumerable<string> arguments,
CancellationToken cancellationToken) =>
Task.FromResult(new ProcessResult(0, string.Empty, string.Empty));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<PropertyGroup>
<IsTestProject>true</IsTestProject>
<SignAssembly>false</SignAssembly>
<TestRunnerName>MSTest</TestRunnerName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CsCheck" Version="4.6.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.17" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="../GitAutomation/Microsoft.DotNet.GitAutomation.csproj" />
</ItemGroup>

</Project>
217 changes: 217 additions & 0 deletions src/GitAutomation.Tests/PullRequestPropertyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CsCheck;

namespace Microsoft.DotNet.GitAutomation.Tests;

[TestClass]
public sealed class PullRequestPlannerTests
{
private const string Workspace = "test-workspace";

private static readonly AutomationIdentity AutomationIdentity = new("bot", "bot@example.com");

private static readonly Uri Url = new("https://github.com/dotnet/example/pull/1");

// A git object SHA-1: a 40-character lowercase hex hash (tree hash, commit SHA, etc.).
private static readonly Gen<string> GenSha1 = Gen.String[Gen.Char["0123456789abcdef"], 40, 40];

private static readonly Gen<int> GenPullRequestNumber = Gen.Int[1, 99999];

private static readonly Gen<PullRequestUpdateStrategy> GenUpdateStrategy =
Gen.OneOfConst(
PullRequestUpdateStrategy.Append,
PullRequestUpdateStrategy.Replace);

private static readonly Gen<ForeignCommitPolicy> GenForeignCommitPolicy =
Gen.OneOfConst(
ForeignCommitPolicy.Proceed,
ForeignCommitPolicy.Stop);

private static readonly Gen<PullRequestState> GenPullRequestState =
from key in Gen.OneOfConst("product-a", "product-b")
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from title in Gen.OneOfConst("Title A", "Title B")
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from body in Gen.OneOfConst("", "Body A", "Body B")
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from targetBranch in Gen.OneOfConst("main", "nightly", "release", "release/1.0")
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from treeHash in GenSha1
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
select new PullRequestState(key, title, body, targetBranch, treeHash);

private static readonly Gen<TargetBranchState> GenTargetBranchState =
GenSha1.Select(treeHash => new TargetBranchState(treeHash));

private static readonly Gen<CommitInfo> GenAutomationCommit =
GenSha1.Select(sha => new CommitInfo(sha, AutomationIdentity.AuthorName, AutomationIdentity.AuthorEmail));

private static readonly Gen<CommitInfo> GenForeignCommit =
GenSha1.Select(sha => new CommitInfo(sha, "Person", "person@example.com"));

private static readonly Gen<CommitInfo> GenRandomCommit = Gen.Frequency((3, GenAutomationCommit), (1, GenForeignCommit));

private static readonly Gen<CommitInfo[]> GenRandomCommits = GenRandomCommit.Array[1, 3];

private static Gen<CommitInfo[]> GenCommitsGuaranteedForeign =
from foreign in GenForeignCommit
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from rest in GenRandomCommit.Array[0, 2]
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from commits in Gen.Shuffle((CommitInfo[])[foreign, .. rest])
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
select commits;

// An arbitrary existing pull request, whose branch may include foreign commits.
private static readonly Gen<ExistingPullRequest> GenExistingPullRequest =
from content in GenPullRequestState
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from number in GenPullRequestNumber
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
lbussell marked this conversation as resolved.
Dismissed
from commits in GenRandomCommits
select new ExistingPullRequest(content, number, Url, commits);

// Bundles the planner inputs for a single test case.
private sealed record PullRequestScenario(
PullRequestState DesiredState,
TargetBranchState TargetBranch,
ExistingPullRequest? ExistingPullRequest,
PullRequestUpdateStrategy UpdateStrategy,
ForeignCommitPolicy OnForeignCommits)
{
public IEnumerable<IOperation> Plan() =>
Planner.Plan(
workspaceDirectory: Workspace,
identity: AutomationIdentity,
desiredState: DesiredState,
targetBranch: TargetBranch,
existingPullRequest: ExistingPullRequest,
updateStrategy: UpdateStrategy,
onForeignCommits: OnForeignCommits);
};

// If no PR exists, and there are changes to be made, then a new PR is
// always created.
[TestMethod]
public void ChangesWithNoPR_CreatesNewPR()
{
var scenario =
from desiredState in GenPullRequestState
from targetBranch in GenTargetBranchState
from updateStrategy in GenUpdateStrategy
from onForeignCommits in GenForeignCommitPolicy
// Collision should be very unlikely, but filter it out anyways
where desiredState.TreeHash != targetBranch.TreeHash
select new PullRequestScenario(
DesiredState: desiredState,
TargetBranch: targetBranch,
ExistingPullRequest: null,
UpdateStrategy: updateStrategy,
OnForeignCommits: onForeignCommits);

scenario.Sample(s => s.Plan().OfType<CreatePullRequestOperation>().Count() == 1);
}

// If no PR exists, and there are changes to be made, the source branch is
// always force pushed to reset it to the state of the target branch.
[TestMethod]
public void ChangesWithNoPR_ResetsExistingBranch()
{
var scenario =
from desiredState in GenPullRequestState
from targetBranch in GenTargetBranchState
from updateStrategy in GenUpdateStrategy
from onForeignCommits in GenForeignCommitPolicy
// Collision should be very unlikely, but filter it out anyways
where desiredState.TreeHash != targetBranch.TreeHash
select new PullRequestScenario(
DesiredState: desiredState,
TargetBranch: targetBranch,
ExistingPullRequest: null,
UpdateStrategy: updateStrategy,
OnForeignCommits: onForeignCommits);

scenario.Sample(s => s.Plan().OfType<PushCommitsOperation>().Single().ForcePush);
}

// For all scenarios where the desired tree is already present in an
// existing pull request, no actions are taken.
[TestMethod]
public void NoChanges_NoOp()
{
var scenario =
from desiredState in GenPullRequestState
from prNumber in GenPullRequestNumber
from existingCommits in GenRandomCommits
from targetBranch in GenTargetBranchState
from updateStrategy in GenUpdateStrategy
select new PullRequestScenario(
desiredState,
targetBranch,
new ExistingPullRequest(desiredState, prNumber, Url, existingCommits),
updateStrategy,
// Don't block on foreign commits
ForeignCommitPolicy.Proceed);

scenario.Sample(s => !s.Plan().Any());
}

// For all scenarios where the desired tree already equals the target tree,
// nothing is pushed.
[TestMethod]
public void NoChanges_DoesNotPush()
{
var scenario =
from desiredState in GenPullRequestState
// 20% of scenarios will have no existing pull request
from existingPullRequest in Gen.Null(GenExistingPullRequest, 0.2)
from targetTree in GenSha1
from updateStrategy in GenUpdateStrategy
// The base tree is the existing PR's head, or the target branch when none exists.
// Pin the desired tree to it so there is no content diff to push.
select new PullRequestScenario(
desiredState with { TreeHash = existingPullRequest?.Content.TreeHash ?? targetTree },
new TargetBranchState(targetTree),
existingPullRequest,
updateStrategy,
// Don't block on foreign commits
ForeignCommitPolicy.Proceed);

scenario.Sample(s => !s.Plan().OfType<PushCommitsOperation>().Any());
}

// If there is already an open PR, never decide to create a second one.
[TestMethod]
public void ExistingPR_DoesNotCreateNewPR()
{
var scenario =
from desiredState in GenPullRequestState
from existingPullRequest in GenExistingPullRequest
from targetBranch in GenTargetBranchState
from updateStrategy in GenUpdateStrategy
from onForeignCommits in GenForeignCommitPolicy
select new PullRequestScenario(
desiredState,
targetBranch,
existingPullRequest,
updateStrategy,
onForeignCommits);

scenario.Sample(s => !s.Plan().OfType<CreatePullRequestOperation>().Any());
}

// For all scenarios where ForeignCommitPolicy.Stop is set, and there is a foreign commit on
// the PR branch, no action should be taken.
[TestMethod]
public void ExistingPR_StopsOnForeignCommits()
{
var scenario =
from desiredState in GenPullRequestState
from existingState in GenPullRequestState
from prNumber in GenPullRequestNumber
from commits in GenCommitsGuaranteedForeign
from targetBranch in GenTargetBranchState
from updateStrategy in GenUpdateStrategy
select new PullRequestScenario(
desiredState,
targetBranch,
new ExistingPullRequest(existingState, prNumber, Url, commits),
updateStrategy,
OnForeignCommits: ForeignCommitPolicy.Stop);

scenario.Sample(s => !s.Plan().Any());
}
}
Loading
Loading