feat: Add Allowed flag for QP upgrades

pull/201/head
Robert Dailey 10 months ago
parent 0652cfd800
commit 8596168757

@ -116,11 +116,14 @@
"description": "If set to true, enables setting scores to 0 in quality profiles where either a CF was not mentioned in the trash_ids array or it was in that list but did not get a score (e.g. no score in guide).",
"default": false
},
"upgrades_allowed": {
"upgrade": {
"type": "object",
"additionalProperties": false,
"required": ["until_quality"],
"required": ["allowed"],
"properties": {
"allowed": {
"type": "boolean"
},
"until_quality": {
"type": "string"
},

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sync" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr" />
<option name="PROGRAM_PARAMETERS" value="sync" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />

@ -2,7 +2,6 @@ using System.IO.Abstractions;
using System.Reflection;
using Autofac;
using Autofac.Extras.Ordering;
using FluentValidation;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Console.Setup;
@ -16,7 +15,6 @@ using Recyclarr.Cli.Pipelines.ReleaseProfile;
using Recyclarr.Cli.Pipelines.Tags;
using Recyclarr.Cli.Processors;
using Recyclarr.Common;
using Recyclarr.Common.FluentValidation;
using Recyclarr.TrashLib;
using Recyclarr.TrashLib.Interfaces;
using Recyclarr.TrashLib.Startup;
@ -44,11 +42,6 @@ public static class CompositionRoot
CommandRegistrations(builder);
PipelineRegistrations(builder);
builder.RegisterAssemblyTypes(thisAssembly)
.AsClosedTypesOf(typeof(IValidator<>))
.Where(x => !typeof(IManualValidator).IsAssignableFrom(x))
.As<IValidator>();
}
private static void PipelineRegistrations(ContainerBuilder builder)

@ -74,16 +74,16 @@ public class QualityProfileTransactionPhase
continue;
}
var organizer = new QualityItemOrganizer();
var newDto = dto ?? serviceData.Schema;
var updatedProfile = new UpdatedQualityProfile
transactions.UpdatedProfiles.Add(new UpdatedQualityProfile
{
ProfileConfig = config,
ProfileDto = newDto,
UpdateReason = dto is null ? QualityProfileUpdateReason.New : QualityProfileUpdateReason.Changed,
UpdatedQualities = newDto.BuildUpdatedQualityItems(config.Profile)
};
transactions.UpdatedProfiles.Add(updatedProfile);
UpdatedQualities = organizer.OrganizeItems(newDto, config.Profile)
});
}
}

@ -1,6 +1,5 @@
using Recyclarr.Cli.Pipelines.QualityProfile.Api;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.Cli.Pipelines.QualityProfile;
@ -93,6 +92,11 @@ public static class QualityProfileExtensions
return result.Name;
}
public static int? FirstCutoffId(this IEnumerable<ProfileItemDto> items)
{
return GetEligibleCutoffs(items).FirstOrDefault().Id;
}
public static int NewItemId(this IEnumerable<ProfileItemDto> items)
{
// This implementation is based on how the Radarr frontend calculates IDs.
@ -108,14 +112,6 @@ public static class QualityProfileExtensions
return Math.Max(1000, maxExisting) + 1;
}
public static UpdatedQualities BuildUpdatedQualityItems(
this QualityProfileDto dto,
QualityProfileConfig configProfile)
{
var organizer = new QualityItemOrganizer();
return organizer.OrganizeItems(dto, configProfile);
}
public static QualityProfileDto ReverseItems(this QualityProfileDto dto)
{
static ICollection<ProfileItemDto> ReverseItemsImpl(IEnumerable<ProfileItemDto> items)

@ -54,9 +54,7 @@ public record UpdatedQualityProfile
//
// Also: It's important that we assign the cutoff *after* we set Items. Because we pull from a different list of
// items depending on if the `qualities` property is set in config.
newDto.Cutoff = config.UpgradeAllowed
? newDto.Items.FindCutoff(config.UpgradeUntilQuality)
: newDto.Items.FirstOrDefault()?.Id;
newDto.Cutoff = newDto.Items.FindCutoff(config.UpgradeUntilQuality) ?? newDto.Items.FirstCutoffId();
return newDto;
}

@ -1,5 +1,6 @@
using FluentValidation;
using Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
using Recyclarr.Common.Extensions;
namespace Recyclarr.Cli.Pipelines.QualityProfile;
@ -7,31 +8,48 @@ public class UpdatedQualityProfileValidator : AbstractValidator<UpdatedQualityPr
{
public UpdatedQualityProfileValidator()
{
RuleFor(x => x.ProfileConfig.Profile.MinFormatScore).Custom((minScore, context) =>
{
var scores = context.InstanceToValidate.UpdatedScores;
var totalScores = scores.Select(x => x.NewScore).Where(x => x > 0).Sum();
if (totalScores < minScore)
{
context.AddFailure(
$"Minimum Custom Format Score of {minScore} can never be satisfied because the total of all " +
$"positive scores is {totalScores}");
}
});
RuleFor(x => x.ProfileConfig.Profile.UpgradeUntilQuality)
.Must((o, x) => o.ProfileDto.Items.FindCutoff(x) is not null)
.When(x => x.ProfileConfig.Profile is {UpgradeUntilQuality: not null, Qualities.Count: 0})
.WithMessage("'until_quality' must refer to an existing and enabled quality or group");
RuleFor(x => x.ProfileConfig.Profile.MinFormatScore)
.Custom(ValidateMinScoreSatisfied);
RuleFor(x => x.ProfileConfig.Profile.UpgradeUntilQuality)
.Must((o, x)
=> !o.UpdatedQualities.InvalidQualityNames.Contains(x, StringComparer.InvariantCultureIgnoreCase))
.WithMessage((_, x) => $"`until_quality` references invalid quality '{x}'");
.Custom(ValidateCutoff!)
.When(x => x.ProfileConfig.Profile.UpgradeUntilQuality is not null);
RuleFor(x => x.ProfileConfig.Profile.Qualities)
.NotEmpty()
.When(x => x.UpdateReason == QualityProfileUpdateReason.New)
.WithMessage("`qualities` is required when creating profiles for the first time");
}
private static void ValidateMinScoreSatisfied(int? minScore, ValidationContext<UpdatedQualityProfile> context)
{
var scores = context.InstanceToValidate.UpdatedScores;
var totalScores = scores.Select(x => x.NewScore).Where(x => x > 0).Sum();
if (totalScores < minScore)
{
context.AddFailure(
$"Minimum Custom Format Score of {minScore} can never be satisfied because the total of all " +
$"positive scores is {totalScores}");
}
}
private static void ValidateCutoff(string untilQuality, ValidationContext<UpdatedQualityProfile> context)
{
var profile = context.InstanceToValidate;
if (profile.UpdatedQualities.InvalidQualityNames.Any(x => x.EqualsIgnoreCase(untilQuality)))
{
context.AddFailure($"`until_quality` references invalid quality '{untilQuality}'");
return;
}
var items = profile.UpdatedQualities.NumWantedItems > 0
? profile.UpdatedQualities.Items
: profile.ProfileDto.Items;
if (items.FindCutoff(untilQuality) is null)
{
context.AddFailure("'until_quality' must refer to an existing and enabled quality or group");
}
}
}

@ -1,9 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace Recyclarr.Common.FluentValidation;
[SuppressMessage("Design", "CA1040:Avoid empty interfaces", Justification =
"Used by AutoFac to exclude IValidator implementations from DI registration")]
public interface IManualValidator
{
}

@ -47,7 +47,7 @@ public class SonarrCapabilityEnforcer
if (config.QualityProfiles.Any(x => x.UpgradeUntilScore is not null))
{
throw new ServiceIncompatibilityException(
"`until_score` under `upgrades_allowed` is not supported by Sonarr v3. " +
"`until_score` under `upgrade` is not supported by Sonarr v3. " +
"Remove the until_score property or use Sonarr v4.");
}

@ -16,10 +16,6 @@ public class ConfigAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(ThisAssembly)
.AsClosedTypesOf(typeof(IValidator<>))
.As<IValidator>();
builder.RegisterAssemblyTypes(ThisAssembly)
.AssignableTo<IYamlBehavior>()
.As<IYamlBehavior>();
@ -42,5 +38,8 @@ public class ConfigAutofacModule : Module
// Config Post Processors
builder.RegisterType<ImplicitUrlAndKeyPostProcessor>().As<IConfigPostProcessor>();
// Validators
builder.RegisterType<RootConfigYamlValidator>().As<IValidator>();
}
}

@ -29,6 +29,7 @@ public record QualitySizeConfigYaml
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public record QualityProfileFormatUpgradeYaml
{
public bool? Allowed { get; init; }
public int? UntilScore { get; init; }
public string? UntilQuality { get; init; }
}
@ -45,7 +46,7 @@ public record QualityProfileQualityConfigYaml
public record QualityProfileConfigYaml
{
public string? Name { get; init; }
public QualityProfileFormatUpgradeYaml? UpgradesAllowed { get; init; }
public QualityProfileFormatUpgradeYaml? Upgrade { get; init; }
public int? MinFormatScore { get; init; }
public bool ResetUnmatchedScores { get; init; }
public QualitySortAlgorithm? QualitySort { get; init; }

@ -1,11 +1,9 @@
using FluentValidation;
using JetBrains.Annotations;
using Recyclarr.Common.Extensions;
using Recyclarr.Common.FluentValidation;
namespace Recyclarr.TrashLib.Config.Parsing;
[UsedImplicitly]
public class ServiceConfigYamlValidator : AbstractValidator<ServiceConfigYaml>
{
public ServiceConfigYamlValidator()
@ -36,7 +34,6 @@ public class ServiceConfigYamlValidator : AbstractValidator<ServiceConfigYaml>
}
}
[UsedImplicitly]
public class CustomFormatConfigYamlValidator : AbstractValidator<CustomFormatConfigYaml>
{
public CustomFormatConfigYamlValidator()
@ -53,7 +50,6 @@ public class CustomFormatConfigYamlValidator : AbstractValidator<CustomFormatCon
}
}
[UsedImplicitly]
public class QualityScoreConfigYamlValidator : AbstractValidator<QualityScoreConfigYaml>
{
public QualityScoreConfigYamlValidator()
@ -63,7 +59,6 @@ public class QualityScoreConfigYamlValidator : AbstractValidator<QualityScoreCon
}
}
[UsedImplicitly]
public class QualitySizeConfigYamlValidator : AbstractValidator<QualitySizeConfigYaml>
{
public QualitySizeConfigYamlValidator()
@ -77,59 +72,67 @@ public class QualitySizeConfigYamlValidator : AbstractValidator<QualitySizeConfi
}
}
[UsedImplicitly]
public class QualityProfileFormatUpgradeYamlValidator : AbstractValidator<QualityProfileFormatUpgradeYaml>
{
public QualityProfileFormatUpgradeYamlValidator()
public QualityProfileFormatUpgradeYamlValidator(QualityProfileConfigYaml config)
{
RuleFor(x => x.Allowed)
.NotNull()
.WithMessage(
$"For profile {config.Name}, 'allowed' under 'upgrade' is required. " +
$"If you don't want Recyclarr to manage upgrades, delete the whole 'upgrade' block.");
RuleFor(x => x.UntilQuality)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithMessage("'until_quality' is required when allowing profile upgrades");
.NotNull()
.When(x => x.Allowed is true && config.Qualities is not null)
.WithMessage(
$"For profile {config.Name}, 'until_quality' is required when 'allowed' is set to 'true' and " +
$"an explicit 'qualities' list is provided.");
}
}
[UsedImplicitly]
public class QualityProfileConfigYamlValidator : AbstractValidator<QualityProfileConfigYaml>
{
public QualityProfileConfigYamlValidator()
{
ClassLevelCascadeMode = CascadeMode.Stop;
RuleLevelCascadeMode = CascadeMode.Stop;
RuleFor(x => x.Name)
.Cascade(CascadeMode.Stop)
.NotEmpty()
.WithMessage(x => $"For profile {x.Name}, 'name' is required for root-level 'quality_profiles' elements");
RuleFor(x => x.UpgradesAllowed)
.SetNonNullableValidator(new QualityProfileFormatUpgradeYamlValidator());
RuleFor(x => x.Upgrade)
.SetNonNullableValidator(x => new QualityProfileFormatUpgradeYamlValidator(x));
RuleFor(x => x.Qualities)
.Custom(ValidateHaveNoDuplicates!)
.Must(x => x!.Any(y => y.Enabled is true or null))
.WithMessage(x =>
$"For profile {x.Name}, at least one explicitly listed quality under 'qualities' must be enabled.")
.When(x => x is {Qualities.Count: > 0});
RuleFor(x => x.Qualities)
.Cascade(CascadeMode.Stop)
.Must((o, x) => !x!
.Where(y => y.Qualities is not null)
.SelectMany(y => y.Qualities!)
.Contains(o.UpgradesAllowed!.UntilQuality, StringComparer.InvariantCultureIgnoreCase))
.Contains(o.Upgrade!.UntilQuality, StringComparer.InvariantCultureIgnoreCase))
.WithMessage(o =>
$"For profile {o.Name}, 'until_quality' must not refer to qualities contained within groups")
.Must((o, x) => !x!
.Where(y => y is {Enabled: false, Name: not null})
.Select(y => y.Name!)
.Contains(o.UpgradesAllowed!.UntilQuality, StringComparer.InvariantCultureIgnoreCase))
.Contains(o.Upgrade!.UntilQuality, StringComparer.InvariantCultureIgnoreCase))
.WithMessage(o =>
$"For profile {o.Name}, 'until_quality' must not refer to explicitly disabled qualities")
.Must((o, x) => x!
.Select(y => y.Name)
.Contains(o.UpgradesAllowed!.UntilQuality, StringComparer.InvariantCultureIgnoreCase))
.Contains(o.Upgrade!.UntilQuality, StringComparer.InvariantCultureIgnoreCase))
.WithMessage(o =>
$"For profile {o.Name}, 'qualities' must contain the quality mentioned in 'until_quality', " +
$"which is '{o.UpgradesAllowed!.UntilQuality}'")
.When(x => x is {UpgradesAllowed: not null, Qualities.Count: > 0});
RuleFor(x => x.Qualities)
.Custom(ValidateHaveNoDuplicates!)
.Must(x => x!.Any(y => y.Enabled is true or null))
.WithMessage(x =>
$"For profile {x.Name}, at least one explicitly listed quality under 'qualities' must be enabled.")
.When(x => x is {Qualities.Count: > 0});
$"which is '{o.Upgrade!.UntilQuality}'")
.When(x => x is {Upgrade.Allowed: not false, Qualities.Count: > 0});
}
private static void ValidateHaveNoDuplicates(
@ -153,7 +156,6 @@ public class QualityProfileConfigYamlValidator : AbstractValidator<QualityProfil
}
}
[UsedImplicitly]
public class RadarrConfigYamlValidator : CustomValidator<RadarrConfigYaml>
{
public RadarrConfigYamlValidator()
@ -162,7 +164,6 @@ public class RadarrConfigYamlValidator : CustomValidator<RadarrConfigYaml>
}
}
[UsedImplicitly]
public class SonarrConfigYamlValidator : CustomValidator<SonarrConfigYaml>
{
public SonarrConfigYamlValidator()
@ -177,7 +178,6 @@ public class SonarrConfigYamlValidator : CustomValidator<SonarrConfigYaml>
}
}
[UsedImplicitly]
public class ReleaseProfileConfigYamlValidator : CustomValidator<ReleaseProfileConfigYaml>
{
public ReleaseProfileConfigYamlValidator()
@ -190,7 +190,6 @@ public class ReleaseProfileConfigYamlValidator : CustomValidator<ReleaseProfileC
}
}
[UsedImplicitly]
public class ReleaseProfileFilterConfigYamlValidator : CustomValidator<ReleaseProfileFilterConfigYaml>
{
public ReleaseProfileFilterConfigYamlValidator()
@ -212,7 +211,6 @@ public class ReleaseProfileFilterConfigYamlValidator : CustomValidator<ReleasePr
}
}
[UsedImplicitly]
public class RootConfigYamlValidator : CustomValidator<RootConfigYaml>
{
public RootConfigYamlValidator()

@ -18,8 +18,9 @@ public class ConfigYamlMapperProfile : Profile
.ForMember(x => x.Enabled, o => o.NullSubstitute(true));
CreateMap<QualityProfileConfigYaml, QualityProfileConfig>()
.ForMember(x => x.UpgradeUntilQuality, o => o.MapFrom(x => x.UpgradesAllowed!.UntilQuality))
.ForMember(x => x.UpgradeUntilScore, o => o.MapFrom(x => x.UpgradesAllowed!.UntilScore))
.ForMember(x => x.UpgradeAllowed, o => o.MapFrom(x => x.Upgrade!.Allowed))
.ForMember(x => x.UpgradeUntilQuality, o => o.MapFrom(x => x.Upgrade!.UntilQuality))
.ForMember(x => x.UpgradeUntilScore, o => o.MapFrom(x => x.Upgrade!.UntilScore))
.ForMember(x => x.QualitySort, o => o.NullSubstitute(QualitySortAlgorithm.Top));
CreateMap<ServiceConfigYaml, ServiceConfiguration>()

@ -62,7 +62,7 @@ public enum QualitySortAlgorithm
public record QualityProfileConfig
{
public string Name { get; init; } = "";
public bool UpgradeAllowed => UpgradeUntilQuality is not null;
public bool? UpgradeAllowed { get; init; }
public string? UpgradeUntilQuality { get; init; }
public int? UpgradeUntilScore { get; init; }
public int? MinFormatScore { get; init; }

@ -63,8 +63,8 @@ public class UpdatedQualityProfileTest
{
Name = "config_name",
MinFormatScore = 110,
UpgradeUntilScore = 220,
UpgradeUntilQuality = "Quality Item 3"
UpgradeAllowed = true,
UpgradeUntilScore = 220
}),
UpdatedQualities = new UpdatedQualities
{
@ -92,9 +92,8 @@ public class UpdatedQualityProfileTest
MinFormatScore = 110,
CutoffFormatScore = 220,
UpgradeAllowed = true,
Cutoff = 3,
Items = profile.UpdatedQualities.Items
});
}, o => o.Excluding(x => x.Cutoff));
}
[Test]
@ -151,4 +150,112 @@ public class UpdatedQualityProfileTest
dto.Name.Should().Be("config_name");
}
[Test]
public void Cutoff_obtained_from_updated_qualities()
{
var profile = new UpdatedQualityProfile
{
ProfileDto = new QualityProfileDto
{
Items = new List<ProfileItemDto>
{
NewQp.QualityDto(8, "Quality Item 8", true),
NewQp.QualityDto(9, "Quality Item 9", true)
}
},
ProfileConfig = new ProcessedQualityProfileData(new QualityProfileConfig
{
UpgradeUntilQuality = "Quality Item 2"
}),
UpdatedQualities = new UpdatedQualities
{
NumWantedItems = 1,
Items = new List<ProfileItemDto>
{
NewQp.QualityDto(1, "Quality Item 1", true),
NewQp.QualityDto(2, "Quality Item 2", true),
NewQp.GroupDto(3, "Quality Item 3", true,
NewQp.QualityDto(4, "Quality Item 4", true))
}
},
UpdateReason = QualityProfileUpdateReason.New
};
var dto = profile.BuildUpdatedDto();
dto.Cutoff.Should().Be(2);
}
[Test]
public void Cutoff_obtained_from_original_qualities()
{
var profile = new UpdatedQualityProfile
{
ProfileDto = new QualityProfileDto
{
Items = new List<ProfileItemDto>
{
NewQp.QualityDto(8, "Quality Item 8", true),
NewQp.QualityDto(9, "Quality Item 9", true)
}
},
ProfileConfig = new ProcessedQualityProfileData(new QualityProfileConfig
{
UpgradeUntilQuality = "Quality Item 9"
}),
UpdatedQualities = new UpdatedQualities
{
NumWantedItems = 0, // zero forces cutoff search to fall back to original DTO items
Items = new List<ProfileItemDto>
{
NewQp.QualityDto(1, "Quality Item 1", true),
NewQp.QualityDto(2, "Quality Item 2", true),
NewQp.GroupDto(3, "Quality Item 3", true,
NewQp.QualityDto(4, "Quality Item 4", true))
}
},
UpdateReason = QualityProfileUpdateReason.New
};
var dto = profile.BuildUpdatedDto();
dto.Cutoff.Should().Be(9);
}
[Test]
public void Cutoff_fall_back_to_first()
{
var profile = new UpdatedQualityProfile
{
ProfileDto = new QualityProfileDto
{
Items = new List<ProfileItemDto>
{
NewQp.QualityDto(8, "Quality Item 8", true),
NewQp.QualityDto(9, "Quality Item 9", true)
}
},
ProfileConfig = new ProcessedQualityProfileData(new QualityProfileConfig
{
// UpgradeUntilQuality = "Quality Item 9"
}),
UpdatedQualities = new UpdatedQualities
{
NumWantedItems = 1,
Items = new List<ProfileItemDto>
{
NewQp.QualityDto(1, "Quality Item 1", true),
NewQp.QualityDto(2, "Quality Item 2", true),
NewQp.GroupDto(3, "Quality Item 3", true,
NewQp.QualityDto(4, "Quality Item 4", true))
}
},
UpdateReason = QualityProfileUpdateReason.New
};
var dto = profile.BuildUpdatedDto();
dto.Cutoff.Should().Be(1);
}
}

@ -8,28 +8,52 @@ namespace Recyclarr.TrashLib.Tests.Config.Parsing;
public class ConfigYamlDataObjectsValidationTest
{
[Test]
public void Quality_profile_name_required()
public void Quality_profile_format_upgrade_allowed_required()
{
var data = new QualityProfileConfigYaml();
var data = new QualityProfileConfigYaml
{
Name = "My QP",
Upgrade = new QualityProfileFormatUpgradeYaml()
};
var validator = new QualityProfileConfigYamlValidator();
var result = validator.TestValidate(data);
var validator = new QualityProfileFormatUpgradeYamlValidator(data);
var result = validator.TestValidate(data.Upgrade);
result.ShouldHaveValidationErrorFor(x => x.Name);
result.ShouldHaveValidationErrorFor(x => x.Allowed).WithErrorMessage(
$"For profile {data.Name}, 'allowed' under 'upgrade' is required. " +
$"If you don't want Recyclarr to manage upgrades, delete the whole 'upgrade' block.");
}
[Test]
public void Quality_profile_until_quality_required()
public void Quality_profile_format_upgrade_until_quality_required()
{
var data = new QualityProfileConfigYaml
{
UpgradesAllowed = new QualityProfileFormatUpgradeYaml()
Name = "My QP",
Upgrade = new QualityProfileFormatUpgradeYaml
{
Allowed = true
},
Qualities = new List<QualityProfileQualityConfigYaml>()
};
var validator = new QualityProfileFormatUpgradeYamlValidator(data);
var result = validator.TestValidate(data.Upgrade);
result.ShouldHaveValidationErrorFor(x => x.UntilQuality).WithErrorMessage(
$"For profile {data.Name}, 'until_quality' is required when 'allowed' is set to 'true' and " +
$"an explicit 'qualities' list is provided.");
}
[Test]
public void Quality_profile_name_required()
{
var data = new QualityProfileConfigYaml();
var validator = new QualityProfileConfigYamlValidator();
var result = validator.TestValidate(data);
result.ShouldHaveValidationErrorFor(x => x.UpgradesAllowed!.UntilQuality);
result.ShouldHaveValidationErrorFor(x => x.Name);
}
[Test]
@ -38,8 +62,9 @@ public class ConfigYamlDataObjectsValidationTest
var data = new QualityProfileConfigYaml
{
Name = "My QP",
UpgradesAllowed = new QualityProfileFormatUpgradeYaml
Upgrade = new QualityProfileFormatUpgradeYaml
{
Allowed = true,
UntilQuality = "Test Quality"
},
Qualities = new[]
@ -55,23 +80,7 @@ public class ConfigYamlDataObjectsValidationTest
result.Errors.Select(x => x.ErrorMessage).Should().BeEquivalentTo(
$"For profile {data.Name}, 'qualities' must contain the quality mentioned in 'until_quality', " +
$"which is '{data.UpgradesAllowed!.UntilQuality}'");
}
[Test]
public void Quality_profile_qualities_cutoff_required()
{
var data = new QualityProfileConfigYaml
{
Name = "My QP",
UpgradesAllowed = new QualityProfileFormatUpgradeYaml()
};
var validator = new QualityProfileConfigYamlValidator();
var result = validator.TestValidate(data);
result.ShouldHaveValidationErrorFor(x => x.UpgradesAllowed!.UntilQuality)
.WithErrorMessage("'until_quality' is required when allowing profile upgrades");
$"which is '{data.Upgrade!.UntilQuality}'");
}
[Test]
@ -80,8 +89,9 @@ public class ConfigYamlDataObjectsValidationTest
var data = new QualityProfileConfigYaml
{
Name = "My QP",
UpgradesAllowed = new QualityProfileFormatUpgradeYaml
Upgrade = new QualityProfileFormatUpgradeYaml
{
Allowed = true,
UntilQuality = "Child Quality"
},
Qualities = new[]
@ -166,8 +176,9 @@ public class ConfigYamlDataObjectsValidationTest
var data = new QualityProfileConfigYaml
{
Name = "My QP",
UpgradesAllowed = new QualityProfileFormatUpgradeYaml
Upgrade = new QualityProfileFormatUpgradeYaml
{
Allowed = true,
UntilQuality = "Disabled Quality"
},
Qualities = new[]

Loading…
Cancel
Save