Skip to content

Commit

Permalink
Evaluate all requirements at once
Browse files Browse the repository at this point in the history
  • Loading branch information
tyb-dev authored and tyberis committed May 4, 2023
1 parent 9cc2c2d commit 6c13ffd
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 25 deletions.
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

0 comments on commit 6c13ffd

Please sign in to comment.