Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Evaluate all delegate requirements at once #1159

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
123 changes: 123 additions & 0 deletions source/Nuke.Build.Tests/DelegateRequirementServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2021 Maintainers of NUKE.
// Distributed under the MIT License.
// https://github.com/nuke-build/nuke/blob/master/LICENSE

using FluentAssertions;
using Nuke.Common.Execution;
using System;
using Xunit;

namespace Nuke.Common.Tests;

public class DelegateRequirementServiceTests
{
public DelegateRequirementServiceTests()
{
// Arguments cannot change across tests due to ValueInjectionUtility.s_valueCache. Use parameters with preset values.
EnvironmentInfo.SetVariable("NullParameter1", "");
EnvironmentInfo.SetVariable("NullParameter2", "");
EnvironmentInfo.SetVariable("StringParameter", "hello");
}

[Fact]
public void TestPassingValidation()
{
var build = new StringParameterBuild();
var targets = ExecutableTargetFactory.CreateAll(build, x => ((IStringParameterInterface)x).PassingRequirement);

// must not throw
DelegateRequirementService.ValidateRequirements(build, targets);
}

[Fact]
public void TestRequiredMember()
{
var build = new RequiredParameterBuild();
var targets = ExecutableTargetFactory.CreateAll(build, x => ((IRequiredParameterInterface)x).FailingRequirement);

var act = () => DelegateRequirementService.ValidateRequirements(build, targets);

act.Should().Throw<Exception>()
.WithMessage("Member 'NullParameter1' is required to be not null");
}

[Fact]
public void TestBooleanExpressionRequirement()
{
var build = new BooleanExpressionRequirementParameterBuild();
var targets = ExecutableTargetFactory.CreateAll(build, x => ((IBooleanExpressionRequirementParameterInterface)x).FailingExpressionRequirement);

var act = () => DelegateRequirementService.ValidateRequirements(build, targets);

act.Should().Throw<Exception>()
.WithMessage("Target 'FailingExpressionRequirement' requires '(value(*).NullParameter1 != null)'");
}

[Fact]
public void TestMultipleFailingRequirements()
{
var build = new MultipleParameters();
var targets = ExecutableTargetFactory.CreateAll(build, x => ((IMultipleParametersInterface)x).MultipleFailingRequirements);

var act = () => DelegateRequirementService.ValidateRequirements(build, targets);

act.Should().Throw<Exception>()
.WithMessage("Target 'MultipleFailingRequirements' requires '(value(*).NullParameter1 != null)'*" +
"Member 'NullParameter2' is required to be not null");
}

private interface IRequiredParameterInterface : INukeBuild
{
[Parameter] string NullParameter1 => TryGetValue(() => NullParameter1);

public Target FailingRequirement => _ => _
.Requires(() => NullParameter1)
.Executes(() => { });
}

private class RequiredParameterBuild : NukeBuild, IRequiredParameterInterface
{
}

private interface IBooleanExpressionRequirementParameterInterface : INukeBuild
{
[Parameter] string NullParameter1 => TryGetValue(() => NullParameter1);

public Target FailingExpressionRequirement => _ => _
.Requires(() => NullParameter1 != null)
.Executes(() => { });
}

private class BooleanExpressionRequirementParameterBuild : NukeBuild, IBooleanExpressionRequirementParameterInterface
{
}

private interface IMultipleParametersInterface : INukeBuild
{
[Parameter] string NullParameter1 => TryGetValue(() => NullParameter1);
[Parameter] string NullParameter2 => TryGetValue(() => NullParameter2);

public Target MultipleFailingRequirements => _ => _
.Requires(() => NullParameter1 != null)
.Requires(() => NullParameter2)
.Executes(() => { });
}

private class MultipleParameters : NukeBuild, IMultipleParametersInterface
{
}

private interface IStringParameterInterface : INukeBuild
{
[Parameter] string StringParameter => TryGetValue(() => StringParameter);

public Target PassingRequirement => _ => _
.Requires(() => StringParameter)
.Executes(() => { });

}

private class StringParameterBuild : NukeBuild, IStringParameterInterface
{
}
}
83 changes: 58 additions & 25 deletions source/Nuke.Build/Execution/DelegateRequirementService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Maintainers of NUKE.
// Copyright 2023 Maintainers of NUKE.
// Distributed under the MIT License.
// https://github.com/nuke-build/nuke/blob/master/LICENSE

Expand All @@ -19,56 +19,89 @@ internal static class DelegateRequirementService
{
public static void ValidateRequirements(INukeBuild build, IReadOnlyCollection<ExecutableTarget> scheduledTargets)
{
var failedAssertions = new List<string>();
var requiredMembers = new List<(MemberInfo, ExecutableTarget)>();

foreach (var target in scheduledTargets)
foreach (var requirement in target.DelegateRequirements)
{
if (requirement is Expression<Func<bool>> boolExpression)
{
// TODO: same as HasSkippingCondition.GetSkipReason
Assert.True(boolExpression.Compile().Invoke(), $"Target '{target.Name}' requires '{requirement.Body}'");
else if (IsMemberNull(requirement.GetMemberInfo(), build, target))
Assert.Fail($"Target '{target.Name}' requires member '{GetMemberName(requirement.GetMemberInfo())}' to be not null");
if (!boolExpression.Compile().Invoke())
failedAssertions.Add($"Target '{target.Name}' requires '{requirement.Body}'");
}
else
requiredMembers.Add((requirement.GetMemberInfo(), target));
}

var requiredMembers = ValueInjectionUtility.GetInjectionMembers(build.GetType())
.Select(x => x.Member)
.Where(x => x.HasCustomAttribute<RequiredAttribute>());
foreach (var member in requiredMembers)
requiredMembers.AddRange(
ValueInjectionUtility.GetInjectionMembers(build.GetType())
.Select(x => x.Member)
.Where(x => x.HasCustomAttribute<RequiredAttribute>())
.Select(x => (x, (ExecutableTarget)null)));

foreach (var (member, target) in requiredMembers)
{
if (IsMemberNull(member, build))
Assert.Fail($"Member '{GetMemberName(member)}' is required to be not null");
var buildMember = GetMemberInBuildType(member, build);

if (!buildMember.HasCustomAttribute<ValueInjectionAttributeBase>())
{
var from = target != null ? $"from target '{target.Name}' " : string.Empty;
failedAssertions.Add($"Member '{GetMemberName(buildMember)}' is required {from}but not marked with an injection attribute.");
}

if (CanInjectValueInteractive(buildMember, build) &&
IsMemberNull(buildMember, build))
{
// If we already have errors, there is no point asking the user for input. Print the errors so far and exit.
if (failedAssertions.Any())
break;
InjectValueInteractive(buildMember, build);
}

if (IsMemberNull(buildMember, build))
failedAssertions.Add($"Member '{GetMemberName(member)}' is required to be not null");
}

if (failedAssertions.Any())
Assert.Fail(string.Join(Environment.NewLine, failedAssertions));
}

private static bool IsMemberNull(MemberInfo member, INukeBuild build)
{
return member.GetValue(build) == null;
}

private static bool IsMemberNull(MemberInfo member, INukeBuild build, ExecutableTarget target = null)
private static MemberInfo GetMemberInBuildType(MemberInfo member, INukeBuild build)
{
member = member.DeclaringType != build.GetType()
return member.DeclaringType != build.GetType()
? build.GetType().GetMember(member.Name).SingleOrDefault() ?? member
: member;

var from = target != null ? $"from target '{target.Name}' " : string.Empty;
Assert.True(member.HasCustomAttribute<ValueInjectionAttributeBase>(),
$"Member '{GetMemberName(member)}' is required {from}but not marked with an injection attribute.");

if (build.Host is Terminal)
TryInjectValueInteractive(member, build);

return member.GetValue(build) == null;
}

private static void TryInjectValueInteractive(MemberInfo member, INukeBuild build)
private static bool CanInjectValueInteractive(MemberInfo member, INukeBuild build)
{
if (build.Host is not Terminal)
return false;

if (!member.HasCustomAttribute<ParameterAttribute>())
return;
return false;

if (member is PropertyInfo property && !property.CanWrite)
return;
return false;

return true;
}

private static void InjectValueInteractive(MemberInfo member, INukeBuild build)
{
var memberType = member.GetMemberType();
var nameOrDescription = ParameterService.GetParameterDescription(member) ??
ParameterService.GetParameterMemberName(member);
var text = $"{nameOrDescription.TrimEnd('.')}:";

while (member.GetValue(build) == null)
while (IsMemberNull(member, build))
{
var valueSet = ParameterService.GetParameterValueSet(member, build);
var value = valueSet == null
Expand Down