diff --git a/src/Cake.Transifex.Tests/Aliases/TxAliasesTests.cs b/src/Cake.Transifex.Tests/Aliases/TxAliasesTests.cs new file mode 100644 index 0000000..15d83c1 --- /dev/null +++ b/src/Cake.Transifex.Tests/Aliases/TxAliasesTests.cs @@ -0,0 +1,296 @@ +namespace Cake.Transifex.Tests.Aliases; + +using System; +using Cake.Core; +using Cake.Core.IO; +using Cake.Testing; +using Cake.Testing.Fixtures; +using Cake.Transifex.Aliases; +using Cake.Transifex.Settings; +using FluentAssertions; +using NSubstitute; +using Xunit; + +public class TxAliasesTests +{ + [Fact] + public void Init_ShouldRunFixtureNoSettings() + { + var fixture = new TxAliasesFixture(); + + var result = fixture.TxInit(null); + + _ = result.Args.Should().Be("init"); + } + + [Fact] + public void Init_ShouldRunFixtureWithAllSettings() + { + var fixture = new TxAliasesFixture(); + var settings = new TxInitSettings + { + CertificatePath = "/certs/main.cer", + HostName = "www.transifex.com", + RootConfiguration = "/certs/root.cer", + Token = "some-kind-of-token", + }; + + var result = fixture.TxInit(settings); + + _ = result.Args.Should().Be("--cacert /certs/main.cer --hostname www.transifex.com --root-config /certs/root.cer --token some-kind-of-token init"); + } + + [Fact] + public void Pull_ShouldRunFixtureWithAllSettings() + { + var fixture = new TxAliasesFixture(); + + var result = fixture.TxPull(new TxPullSettings + { + Branch = "develop", + CertificatePath = "./certs/main.cer", + ContentEncoding = Enums.ContentEncodings.Base64, + DisableOverwrite = true, + DownloadAllFiles = true, + DownloadJsonFiles = true, + DownloadSourceFile = true, + DownloadTranslationFiles = true, + DownloadXliffFiles = true, + ForceDownloads = true, + HostName = "www.transifex.com", + KeepNewFiles = true, + Languages = new[] { "nb*", "en*" }, + MinimumPercentage = 60, + Mode = Enums.PullMode.OnlyReviewed, + ParallelWorkers = 10, + Pseudo = true, + Resources = new[] { "some-resource", "some-other-resource" }, + RootConfiguration = "./certs/root.cer", + Silent = true, + SkipErrors = true, + Token = "some-token", + UseGitTimestamps = true, + }); + + _ = result.Args.Should().Be("--cacert certs/main.cer --hostname www.transifex.com --root-config certs/root.cer --token some-token pull --branch develop --content_encoding base64 --disable-overwrite --all --json --source --translations --xliff --force --keep-new-files --languages \"nb*,en*\" --minimum-perc 60 --mode onlyreviewed --workers 10 --pseudo --resources \"some-resource,some-other-resource\" --silent --skip --use-git-timestamps"); + } + + [Fact] + public void Pull_ShouldRunFixtureWithToken() + { + var fixture = new TxAliasesFixture(); + + var result = fixture.TxPull("test-token"); + + _ = result.Args.Should().Be("--token test-token pull"); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Pull_ThrowsExceptionOnEmptyTokens(string token) + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxPull(context, token); + }; + + _ = action.Should().ThrowExactly(); + } + + [Fact] + public void Pull_ThrowsExceptionOnNullTokens() + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxPull(context, (string)null); + }; + + _ = action.Should().ThrowExactly(); + } + + [Fact] + public void Pull_ThrowsNullExceptionOnNullSettings() + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxPull(context, (TxPullSettings)null); + }; + + _ = action.Should().ThrowExactly(); + } + + [Fact] + public void Push_ShouldRunFixtureWithAllSettings() + { + var fixture = new TxAliasesFixture(); + + var result = fixture.TxPush(new TxPushSettings + { + Base = "develop", + Branch = "feature/new-aliases", + CertificatePath = "./certs/main.cer", + CreateMissingLanguages = true, + ForceUploads = true, + HostName = "www.transifex.com", + KeepTranslations = true, + Languages = new[] { "nb*", "en*" }, + ParallelWorkers = 15, + ReplaceEditedStrings = true, + Resources = new[] { "resource-6", "another-resource" }, + RootConfiguration = "/certs/root.cer", + Silent = true, + SkipErrors = true, + Token = "some-token", + UploadSourceFile = true, + UploadTranslationFiles = true, + UploadXliffFiles = true, + UseGitTimestamps = true, + }); + + _ = result.Args.Should().Be("--cacert certs/main.cer --hostname www.transifex.com --root-config /certs/root.cer --token some-token push --base develop --branch feature/new-aliases --all --force --keep-translations --languages \"nb*,en*\" --workers 15 --replace-edited-strings --resources \"resource-6,another-resource\" --silent --skip --source --translation --xliff --use-git-timestamps"); + } + + [Fact] + public void Push_ShouldRunFixtureWithToken() + { + { + var fixture = new TxAliasesFixture(); + + var result = fixture.TxPush("testie"); + + _ = result.Args.Should().Be("--token testie push"); + } + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Push_ThrowsExceptionOnEmptyToken(string token) + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxPush(context, token); + }; + + _ = action.Should().ThrowExactly(); + } + + [Fact] + public void Push_ThrowsExceptionOnNullSettings() + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxPush(context, (TxPushSettings)null); + }; + + _ = action.Should().ThrowExactly(); + } + + [Fact] + public void Push_ThrowsExceptionOnNullToken() + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxPush(context, (string)null); + }; + + _ = action.Should().ThrowExactly(); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Status_ThrowsExceptionOnEmptyToken(string token) + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxStatus(context, token); + }; + + _ = action.Should().ThrowExactly(); + } + + [Fact] + public void Status_ThrowsExceptionOnNullSettings() + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxStatus(context, (TxStatusSettings)null); + }; + + _ = action.Should().ThrowExactly(); + } + + [Fact] + public void Status_ThrowsExceptionOnNullToken() + { + Action action = () => + { + var context = Substitute.For(); + TxAliases.TxPush(context, (string)null); + }; + + _ = action.Should().ThrowExactly(); + } + + private class TxAliasesFixture : ToolFixture + where TSettings : TxGlobalSettings, new() + { + public TxAliasesFixture() + : base("tx") + { + var arguments = Substitute.For(); + var registry = Substitute.For(); + var dataService = Substitute.For(); + Context = new CakeContext(FileSystem, Environment, Globber, new FakeLog(), arguments, ProcessRunner, registry, Tools, dataService, Configuration); + } + + public ICakeContext Context { get; } + + public ToolFixtureResult TxInit(TxInitSettings settings) + { + TxAliases.TxInit(Context, settings); + + return ProcessRunner.Results.Count > 0 ? ProcessRunner.Results[0] : null; + } + + public ToolFixtureResult TxPull(string token) + { + TxAliases.TxPull(Context, token); + + return ProcessRunner.Results.Count > 0 ? ProcessRunner.Results[0] : null; + } + + public ToolFixtureResult TxPull(TxPullSettings settings) + { + TxAliases.TxPull(Context, settings); + + return ProcessRunner.Results.Count > 0 ? ProcessRunner.Results[0] : null; + } + + public ToolFixtureResult TxPush(string token) + { + TxAliases.TxPush(Context, token); + + return ProcessRunner.Results.Count > 0 ? ProcessRunner.Results[0] : null; + } + + public ToolFixtureResult TxPush(TxPushSettings settings) + { + TxAliases.TxPush(Context, settings); + + return ProcessRunner.Results.Count > 0 ? ProcessRunner.Results[0] : null; + } + + protected override void RunTool() => throw new NotImplementedException(); + } +} diff --git a/src/Cake.Transifex.Tests/Cake.Transifex.Tests.csproj b/src/Cake.Transifex.Tests/Cake.Transifex.Tests.csproj index b5a6694..39939de 100644 --- a/src/Cake.Transifex.Tests/Cake.Transifex.Tests.csproj +++ b/src/Cake.Transifex.Tests/Cake.Transifex.Tests.csproj @@ -1,6 +1,7 @@ net7.0;net6.0 + 10 diff --git a/src/Cake.Transifex.Tests/Runners/TxGlobalRunnerTests.cs b/src/Cake.Transifex.Tests/Runners/TxGlobalRunnerTests.cs new file mode 100644 index 0000000..ddc020a --- /dev/null +++ b/src/Cake.Transifex.Tests/Runners/TxGlobalRunnerTests.cs @@ -0,0 +1,140 @@ +namespace Cake.Transifex.Tests.Runners; + +using Cake.Core.IO; +using Cake.Transifex.Settings; +using FluentAssertions; +using Xunit; + +public abstract class TxGlobalRunnerTests + where TSettings : TxGlobalSettings, new() +{ + private readonly string _commandName; + + protected TxGlobalRunnerTests(string commandName) + { + _commandName = commandName; + } + + protected TxRunnerFixture Fixture { get; } = new() + { + Settings = new() + }; + + protected TSettings Settings => Fixture.Settings; + + [Fact] + public void Run_ShouldOnlySetCommandNameWithoutArguments() + { + var result = Fixture.Run(); + + _ = result.Args.Should().Be(_commandName); + } + + [Fact] + public void Run_ShouldSetCertificatePath() + { + var certificatePath = new FilePath("./certs/certificate.cer"); + + Settings.CertificatePath = certificatePath; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--cacert {certificatePath.FullPath} {_commandName}"); + } + + [Fact] + public void Run_ShouldSetCertificatePathInQuotes() + { + var certificatePath = new FilePath("./certs with space/certif icate.cer"); + + Settings.CertificatePath = certificatePath; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--cacert \"{certificatePath.FullPath}\" {_commandName}"); + } + + [Fact] + public void Run_ShouldSetConfigurationPath() + { + var configPath = new FilePath("./tx/config"); + + Settings.Configuration = configPath.FullPath; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--config {configPath.FullPath} {_commandName}"); + } + + [Fact] + public void Run_ShouldSetConfigurationPathInQuotes() + { + var configPath = new FilePath("./tx/config with space"); + + Settings.Configuration = configPath.FullPath; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--config \"{configPath.FullPath}\" {_commandName}"); + } + + [Fact] + public void Run_ShouldSetHostName() + { + const string expectedValue = "www.transifex.com"; + + Settings.HostName = expectedValue; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--hostname {expectedValue} {_commandName}"); + } + + [Fact] + public void Run_ShouldSetRootConfigurationPath() + { + var configPath = new FilePath("./configuration/root"); + + Settings.RootConfiguration = configPath.FullPath; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--root-config {configPath.FullPath} {_commandName}"); + } + + [Fact] + public void Run_ShouldSetRootConfigurationPathInQuotes() + { + var configPath = new FilePath("./root configuration"); + + Settings.RootConfiguration = configPath.FullPath; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--root-config \"{configPath.FullPath}\" {_commandName}"); + } + + [Fact] + public void Run_ShouldSetToken() + { + const string token = "MY-AWESOME-TOKEN"; + + Settings.Token = token; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--token {token} {_commandName}"); + } + + [Fact] + public void Run_ShouldSetTokenInQuotes() + { + const string token = "MY AWESOME TOKEN"; + + Settings.Token = token; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"--token \"{token}\" {_commandName}"); + } +} diff --git a/src/Cake.Transifex.Tests/Runners/TxInitRunnerTests.cs b/src/Cake.Transifex.Tests/Runners/TxInitRunnerTests.cs new file mode 100644 index 0000000..ea70861 --- /dev/null +++ b/src/Cake.Transifex.Tests/Runners/TxInitRunnerTests.cs @@ -0,0 +1,11 @@ +namespace Cake.Transifex.Tests.Runners; + +using Cake.Transifex.Settings; + +public class TxInitRunnerTests : TxGlobalRunnerTests +{ + public TxInitRunnerTests() + : base("init") + { + } +} diff --git a/src/Cake.Transifex.Tests/Runners/TxPullRunnerTests.cs b/src/Cake.Transifex.Tests/Runners/TxPullRunnerTests.cs new file mode 100644 index 0000000..22aeb6b --- /dev/null +++ b/src/Cake.Transifex.Tests/Runners/TxPullRunnerTests.cs @@ -0,0 +1,398 @@ +namespace Cake.Transifex.Tests.Runners; + +using System; +using Cake.Transifex.Enums; +using Cake.Transifex.Settings; +using FluentAssertions; +using Xunit; + +public class TxPullRunnerTests : TxGlobalRunnerTests +{ + public TxPullRunnerTests() + : base("pull") + { + } + + public static TheoryData Encodings => new(Enum.GetValues()); + + public static TheoryData Modes => new(Enum.GetValues()); + + [Fact] + public void Run_ShouldNotIncludeBranchWhenNull() + { + Settings.Branch = null; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetAll() + { + Settings.DownloadAllFiles = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetDisableOverwrite() + { + Settings.DisableOverwrite = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetForce() + { + Settings.ForceDownloads = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetJson() + { + Settings.DownloadJsonFiles = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetKeepNewFiles() + { + Settings.KeepNewFiles = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetPseudo() + { + Settings.Pseudo = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetSilent() + { + Settings.Silent = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetSkip() + { + Settings.SkipErrors = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetSource() + { + Settings.DownloadSourceFile = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetTranslations() + { + Settings.DownloadTranslationFiles = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetUseGitTimestamps() + { + Settings.UseGitTimestamps = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldNotSetXliff() + { + Settings.DownloadXliffFiles = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull"); + } + + [Fact] + public void Run_ShouldSetAll() + { + Settings.DownloadAllFiles = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --all"); + } + + [Theory] + [InlineData("master")] + [InlineData("develop")] + [InlineData("feature/branch-argument")] + public void Run_ShouldSetBranch(string name) + { + Settings.Branch = name; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"pull --branch {name}"); + } + + [Fact] + public void Run_ShouldSetBranchOnEmptyValue() + { + Settings.Branch = string.Empty; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --branch \"\""); + } + + [Theory] + [MemberData(nameof(Encodings))] + public void Run_ShouldSetContentEncoding(ContentEncodings encoding) + { + Settings.ContentEncoding = encoding; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"pull --content_encoding {encoding.ToString().ToLowerInvariant()}"); + } + + [Fact] + public void Run_ShouldSetDisableOverwrite() + { + Settings.DisableOverwrite = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --disable-overwrite"); + } + + [Fact] + public void Run_ShouldSetForce() + { + Settings.ForceDownloads = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --force"); + } + + [Fact] + public void Run_ShouldSetJson() + { + Settings.DownloadJsonFiles = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --json"); + } + + [Fact] + public void Run_ShouldSetKeepNewFiles() + { + Settings.KeepNewFiles = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --keep-new-files"); + } + + [Fact] + public void Run_ShouldSetLanguageMultiple() + { + Settings.Languages.Add("en"); + Settings.Languages.Add("nb"); + Settings.Languages.Add("fr"); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --languages \"en,nb,fr\""); + } + + [Fact] + public void Run_ShouldSetLanguageSingle() + { + Settings.Languages.Add("en"); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --languages en"); + } + + [Fact] + public void Run_ShouldSetLanguageSingleWithAsterisk() + { + Settings.Languages.Add("en*"); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --languages \"en*\""); + } + + [Theory] + [InlineData(1)] + [InlineData(50)] + [InlineData(100)] + public void Run_ShouldSetMinimumPercentage(short percentage) + { + Settings.MinimumPercentage = percentage; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"pull --minimum-perc {percentage}"); + } + + [Theory] + [MemberData(nameof(Modes))] + public void Run_ShouldSetMode(PullMode mode) + { + Settings.Mode = mode; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"pull --mode {mode.ToString().ToLowerInvariant()}"); + } + + [Theory] + [InlineData(1)] + [InlineData(4)] + [InlineData(19)] + public void Run_ShouldSetParallelWorkers(byte workers) + { + Settings.ParallelWorkers = workers; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"pull --workers {workers}"); + } + + [Fact] + public void Run_ShouldSetPseudo() + { + Settings.Pseudo = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --pseudo"); + } + + [Fact] + public void Run_ShouldSetResourcesMultiple() + { + Settings.Resources = new[] + { + "caketransifex.commonresx", + "caketransifex.exceptionresx" + }; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --resources \"caketransifex.commonresx,caketransifex.exceptionresx\""); + } + + [Theory] + [InlineData("caketransifex.commonresx")] + [InlineData("caketransifex.exceptionsresx")] + public void Run_ShouldSetResourcesSingle(string resourceName) + { + Settings.Resources.Add(resourceName); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"pull --resources {resourceName}"); + } + + [Fact] + public void Run_ShouldSetSilent() + { + Settings.Silent = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --silent"); + } + + [Fact] + public void Run_ShouldSetSkip() + { + Settings.SkipErrors = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --skip"); + } + + [Fact] + public void Run_ShouldSetSource() + { + Settings.DownloadSourceFile = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --source"); + } + + [Fact] + public void Run_ShouldSetTranslations() + { + Settings.DownloadTranslationFiles = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --translations"); + } + + [Fact] + public void Run_ShouldSetUseGitTimestamps() + { + Settings.UseGitTimestamps = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --use-git-timestamps"); + } + + [Fact] + public void Run_ShouldSetXliff() + { + Settings.DownloadXliffFiles = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("pull --xliff"); + } +} diff --git a/src/Cake.Transifex.Tests/Runners/TxPushRunnerFixture.cs b/src/Cake.Transifex.Tests/Runners/TxPushRunnerFixture.cs new file mode 100644 index 0000000..3dff9fb --- /dev/null +++ b/src/Cake.Transifex.Tests/Runners/TxPushRunnerFixture.cs @@ -0,0 +1,348 @@ +namespace Cake.Transifex.Tests.Runners; + +using Cake.Transifex.Settings; +using FluentAssertions; +using Xunit; + +public class TxPushRunnerFixture : TxGlobalRunnerTests +{ + public TxPushRunnerFixture() + : base("push") + { + } + + public static TheoryData NullOrEmptyString => new( + null, + string.Empty, + " "); + + public static TheoryData TestBranches => new( + "master", + "develop", + "support/2.x", + "feature/test-feature"); + + [Fact] + public void Run_ShouldNotIncludeBranchWhenNull() + { + Settings.Branch = null; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetAll() + { + Settings.CreateMissingLanguages = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Theory] + [MemberData(nameof(NullOrEmptyString))] + public void Run_ShouldNotSetBase(string name) + { + Settings.Base = name; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetForce() + { + Settings.ForceUploads = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetKeepTranslations() + { + Settings.KeepTranslations = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetReplaceEditedStrings() + { + Settings.ReplaceEditedStrings = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetSilent() + { + Settings.Silent = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetSkip() + { + Settings.SkipErrors = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetSource() + { + Settings.UploadSourceFile = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetTranslation() + { + Settings.UploadTranslationFiles = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetUseGitTimestamps() + { + Settings.UseGitTimestamps = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldNotSetXliff() + { + Settings.UploadXliffFiles = false; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push"); + } + + [Fact] + public void Run_ShouldSetAll() + { + Settings.CreateMissingLanguages = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --all"); + } + + [Theory] + [MemberData(nameof(TestBranches))] + public void Run_ShouldSetBase(string name) + { + Settings.Base = name; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"push --base {name}"); + } + + [Theory] + [MemberData(nameof(TestBranches))] + public void Run_ShouldSetBranch(string name) + { + Settings.Branch = name; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"push --branch {name}"); + } + + [Fact] + public void Run_ShouldSetBranchOnEmptyValue() + { + Settings.Branch = string.Empty; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --branch \"\""); + } + + [Fact] + public void Run_ShouldSetForce() + { + Settings.ForceUploads = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --force"); + } + + [Fact] + public void Run_ShouldSetKeepTranslations() + { + Settings.KeepTranslations = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --keep-translations"); + } + + [Fact] + public void Run_ShouldSetLanguageMultiple() + { + Settings.Languages.Add("en"); + Settings.Languages.Add("nb"); + Settings.Languages.Add("fr"); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --languages \"en,nb,fr\""); + } + + [Fact] + public void Run_ShouldSetLanguageSingle() + { + Settings.Languages.Add("en"); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --languages en"); + } + + [Fact] + public void Run_ShouldSetLanguageSingleWithAsterisk() + { + Settings.Languages.Add("en*"); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --languages \"en*\""); + } + + [Theory] + [InlineData(1)] + [InlineData(4)] + [InlineData(19)] + public void Run_ShouldSetParallelWorkers(byte workers) + { + Settings.ParallelWorkers = workers; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"push --workers {workers}"); + } + + [Fact] + public void Run_ShouldSetReplaceEditedStrings() + { + Settings.ReplaceEditedStrings = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --replace-edited-strings"); + } + + [Fact] + public void Run_ShouldSetResourcesMultiple() + { + Settings.Resources = new[] + { + "caketransifex.commonresx", + "caketransifex.exceptionresx" + }; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --resources \"caketransifex.commonresx,caketransifex.exceptionresx\""); + } + + [Theory] + [InlineData("caketransifex.commonresx")] + [InlineData("caketransifex.exceptionsresx")] + public void Run_ShouldSetResourcesSingle(string resourceName) + { + Settings.Resources.Add(resourceName); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be($"push --resources {resourceName}"); + } + + [Fact] + public void Run_ShouldSetSilent() + { + Settings.Silent = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --silent"); + } + + [Fact] + public void Run_ShouldSetSkip() + { + Settings.SkipErrors = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --skip"); + } + + [Fact] + public void Run_ShouldSetSource() + { + Settings.UploadSourceFile = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --source"); + } + + [Fact] + public void Run_ShouldSetTranslation() + { + Settings.UploadTranslationFiles = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --translation"); + } + + [Fact] + public void Run_ShouldSetUseGitTimestamps() + { + Settings.UseGitTimestamps = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --use-git-timestamps"); + } + + [Fact] + public void Run_ShouldSetXliff() + { + Settings.UploadXliffFiles = true; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("push --xliff"); + } +} diff --git a/src/Cake.Transifex.Tests/Runners/TxRunnerFixture.cs b/src/Cake.Transifex.Tests/Runners/TxRunnerFixture.cs new file mode 100644 index 0000000..aa920bf --- /dev/null +++ b/src/Cake.Transifex.Tests/Runners/TxRunnerFixture.cs @@ -0,0 +1,20 @@ +namespace Cake.Transifex.Tests.Runners; + +using Cake.Testing.Fixtures; +using Cake.Transifex.Runners; +using Cake.Transifex.Settings; + +public class TxRunnerFixture : ToolFixture + where TSettings : TxGlobalSettings, new() +{ + public TxRunnerFixture() + : base("tx") + { + } + + protected override void RunTool() + { + var tool = new TxRunner(FileSystem, Environment, ProcessRunner, Tools); + tool.Run(Settings); + } +} diff --git a/src/Cake.Transifex.Tests/Runners/TxStatusRunnerTests.cs b/src/Cake.Transifex.Tests/Runners/TxStatusRunnerTests.cs new file mode 100644 index 0000000..df866fb --- /dev/null +++ b/src/Cake.Transifex.Tests/Runners/TxStatusRunnerTests.cs @@ -0,0 +1,44 @@ +namespace Cake.Transifex.Tests.Runners; + +using System; +using Cake.Transifex.Settings; +using FluentAssertions; +using Xunit; + +public class TxStatusRunnerTests : TxGlobalRunnerTests +{ + public TxStatusRunnerTests() + : base("status") + { + } + + [Fact] + public void Run_SetsResourcesWithSingleValue() + { + Settings.Resources = new[] { "test-resource" }; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("status --resources test-resource"); + } + + [Fact] + public void Run_SetsResourcesWithMultipleValues() + { + Settings.Resources = new[] { "test-resource-1", "test-resource-2", "something-else" }; + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("status --resources \"test-resource-1,test-resource-2,something-else\""); + } + + [Fact] + public void Run_IgnoresResourcesWhenEmpty() + { + Settings.Resources = Array.Empty(); + + var result = Fixture.Run(); + + _ = result.Args.Should().Be("status"); + } +} diff --git a/src/Cake.Transifex.Tests/Settings/TxGlobalSettingsTests.cs b/src/Cake.Transifex.Tests/Settings/TxGlobalSettingsTests.cs new file mode 100644 index 0000000..706ef3a --- /dev/null +++ b/src/Cake.Transifex.Tests/Settings/TxGlobalSettingsTests.cs @@ -0,0 +1,221 @@ +namespace Cake.Transifex.Tests.Settings; + +using System.Collections.Generic; +using Cake.Core.IO; +using Cake.Transifex.Settings; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +public abstract class TxGlobalSettingsTests + where TSettings : TxGlobalSettings, new() +{ + public static TheoryData EmptyValues => new(null, "", " "); + + protected TSettings Settings { get; } = new(); + + [Fact] + public void CertificatePath_ShouldRemoveVariableWhenNull() + { + CertificatePath_ShouldSetExpectedVariable(); + + Settings.CertificatePath = null; + + using (new AssertionScope()) + { + _ = Settings.CertificatePath.Should().BeNull(); + + AssertNoAvailableKey("--cacert"); + } + } + + [Fact] + public void CertificatePath_ShouldSetExpectedVariable() + { + var expectedPath = new FilePath("./config/test-certificate.cer"); + Settings.CertificatePath = expectedPath; + + using (new AssertionScope()) + { + _ = Settings.CertificatePath.Should().Be(expectedPath); + + AssertPrependedValue("--cacert", expectedPath); + } + } + + [Fact] + public void Configuration_ShouldRemoveVariableWhenNull() + { + Configuration_ShouldSetExpectedVariable(); + + Settings.Configuration = null; + + using (new AssertionScope()) + { + _ = Settings.Configuration.Should().BeNull(); + + AssertNoAvailableKey("--config"); + } + } + + [Fact] + public void Configuration_ShouldSetExpectedVariable() + { + var expectedPath = new FilePath("./.transifex/config"); + Settings.Configuration = expectedPath; + + using (new AssertionScope()) + { + _ = Settings.Configuration.Should().Be(expectedPath); + + AssertPrependedValue("--config", expectedPath); + } + } + + [Theory] + [MemberData(nameof(EmptyValues))] + public void HostName_ShouldRemoveVariableWhenEmptyOrNull(string hostName) + { + HostName_ShouldSetExpectedVariable(); + Settings.HostName = hostName; + + using (new AssertionScope()) + { + _ = Settings.HostName.Should().BeNull(); + + AssertNoAvailableKey("--hostname"); + } + } + + [Fact] + public void HostName_ShouldSetExpectedVariable() + { + const string expectedHostName = "www.transifex.com"; + Settings.HostName = expectedHostName; + + using (new AssertionScope()) + { + _ = Settings.HostName.Should().Be(expectedHostName); + + AssertPrependedValue("--hostname", expectedHostName); + } + } + + [Fact] + public void RootConfiguration_ShouldRemoveVariableWhenNull() + { + RootConfiguration_ShouldSetExpectedVariable(); + Settings.RootConfiguration = null; + + using (new AssertionScope()) + { + _ = Settings.RootConfiguration.Should().BeNull(); + + AssertNoAvailableKey("--root-config"); + } + } + + [Fact] + public void RootConfiguration_ShouldSetExpectedVariable() + { + var expectedPath = new FilePath("./.transifex/root-config"); + Settings.RootConfiguration = expectedPath; + + using (new AssertionScope()) + { + _ = Settings.RootConfiguration.Should().Be(expectedPath); + + AssertPrependedValue("--root-config", expectedPath); + } + } + + [Theory] + [MemberData(nameof(EmptyValues))] + public void Token_ShouldRemoveVariableWhenEmptyOrNull(string token) + { + Token_ShouldSetExpectedVariable(); + Settings.Token = token; + + using (new AssertionScope()) + { + _ = Settings.Token.Should().BeNull(); + + AssertNoAvailableKey("--token"); + } + } + + [Fact] + public void Token_ShouldSetExpectedVariable() + { + const string expectedToken = "MY-AWESOME-TOKEN"; + Settings.Token = expectedToken; + + using (new AssertionScope()) + { + _ = Settings.Token.Should().Be(expectedToken); + + AssertPrependedSecretValue("--token", expectedToken); + } + } + + protected void AssertAppendedCollectionValue(string key, ICollection expectedValues) + { + var arguments = Settings.GetAllAppendedArguments(); + _ = arguments.Should().ContainKey(key).WhoseValue.Should().BeAssignableTo>(); + var values = (ICollection)arguments[key]; + + if (expectedValues.Count == 0) + { + _ = values.Should().BeEmpty(); + } + else + { + _ = values.Should().BeEquivalentTo(expectedValues); + } + + _ = arguments.Should().NotContainKey("!" + key); + _ = Settings.GetAllPrependendArguments().Should().NotContainKeys("!" + key, key); + } + + protected void AssertAppendedSecretValue(string key, TValue expectedValue) + { + var arguments = Settings.GetAllAppendedArguments(); + _ = arguments.Should().ContainKey("!" + key).WhoseValue.Should().BeOfType(typeof(TValue)).And.Be(expectedValue); + _ = arguments.Should().NotContainKey(key); + + _ = Settings.GetAllPrependendArguments().Should().NotContainKeys("!" + key, key); + } + + protected void AssertAppendedValue(string key, TValue expectedValue) + { + var arguments = Settings.GetAllAppendedArguments(); + _ = arguments.Should().ContainKey(key).WhoseValue.Should().BeOfType().And.Be(expectedValue); + _ = arguments.Should().NotContainKey("!" + key); + + _ = Settings.GetAllPrependendArguments().Should().NotContainKeys("!" + key, key); + } + + protected void AssertNoAvailableKey(string key) + { + _ = Settings.GetAllPrependendArguments().Should().NotContainKeys("!" + key, key); + _ = Settings.GetAllAppendedArguments().Should().NotContainKeys("!" + key, key); + } + + protected void AssertPrependedSecretValue(string key, TValue expectedValue) + { + var arguments = Settings.GetAllPrependendArguments(); + _ = arguments.Should().ContainKey("!" + key).WhoseValue.Should().BeOfType(typeof(TValue)).And.Be(expectedValue); + _ = arguments.Should().NotContainKey(key); + + _ = Settings.GetAllAppendedArguments().Should().NotContainKeys("!" + key, key); + } + + protected void AssertPrependedValue(string key, TValue expectedValue) + { + var arguments = Settings.GetAllPrependendArguments(); + _ = arguments.Should().NotContainKey("!" + key); + _ = arguments.Should().ContainKey(key).WhoseValue.Should().BeOfType(typeof(TValue)).And.Be(expectedValue); + + _ = Settings.GetAllAppendedArguments().Should().NotContainKeys("!" + key, key); + } +} diff --git a/src/Cake.Transifex.Tests/Settings/TxInitSettingsTests.cs b/src/Cake.Transifex.Tests/Settings/TxInitSettingsTests.cs new file mode 100644 index 0000000..25a5257 --- /dev/null +++ b/src/Cake.Transifex.Tests/Settings/TxInitSettingsTests.cs @@ -0,0 +1,7 @@ +namespace Cake.Transifex.Tests.Settings; + +using Cake.Transifex.Settings; + +public class TxInitSettingsTests : TxGlobalSettingsTests +{ +} diff --git a/src/Cake.Transifex.Tests/Settings/TxPullSettingsTests.cs b/src/Cake.Transifex.Tests/Settings/TxPullSettingsTests.cs new file mode 100644 index 0000000..90ccd3d --- /dev/null +++ b/src/Cake.Transifex.Tests/Settings/TxPullSettingsTests.cs @@ -0,0 +1,454 @@ +namespace Cake.Transifex.Tests.Settings; + +using System; +using System.Collections.Generic; +using Cake.Transifex.Settings; +using FluentAssertions.Execution; +using FluentAssertions; +using Xunit; +using Cake.Transifex.Enums; + +public class TxPullSettingsTests : TxGlobalSettingsTests +{ + public static TheoryData TransifexModes => new(Enum.GetValues()); + + [Fact] + public void Branch_ShouldRemoveVariableWhenNull() + { + Branch_ShouldSetExpectedVariable("something"); + + Settings.Branch = null; + + using (new AssertionScope()) + { + _ = Settings.Branch.Should().BeNull(); + + AssertNoAvailableKey("--branch"); + } + } + + [Theory] + [InlineData("develop")] + [InlineData("support/1.x")] + [InlineData("")] + public void Branch_ShouldSetExpectedVariable(string value) + { + Settings.Branch = value; + + using (new AssertionScope()) + { + _ = Settings.Branch.Should().Be(value); + + AssertAppendedValue("--branch", value); + } + } + + [Fact] + public void Constructor_ShouldNotSetAnyVariables() + { + using (new AssertionScope()) + { + _ = Settings.GetAllAppendedArguments().Should().BeEmpty(); + _ = Settings.GetAllPrependendArguments().Should().BeEmpty(); + } + } + + [Theory] + [InlineData(ContentEncodings.Text)] + [InlineData(ContentEncodings.Base64)] + public void ContentEncoding_ShouldSetExpectedVariable(ContentEncodings encoding) + { + Settings.ContentEncoding = encoding; + + using (new AssertionScope()) + { + _ = Settings.ContentEncoding.Should().Be(encoding); + + AssertAppendedValue("--content_encoding", encoding); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DisableOverwrite_ShouldSetExpectedVariable(bool value) + { + Settings.DisableOverwrite = value; + + using (new AssertionScope()) + { + _ = Settings.DisableOverwrite.Should().Be(value); + + AssertAppendedValue("--disable-overwrite", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DownloadAllFiles_ShouldSetExpectedVariable(bool value) + { + Settings.DownloadAllFiles = value; + + using (new AssertionScope()) + { + _ = Settings.DownloadAllFiles.Should().Be(value); + + AssertAppendedValue("--all", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DownloadJsonFiles_ShouldSetExpectedVariable(bool value) + { + Settings.DownloadJsonFiles = value; + + using (new AssertionScope()) + { + _ = Settings.DownloadJsonFiles.Should().Be(value); + + AssertAppendedValue("--json", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DownloadSourceFile_ShouldSetExpectedVariable(bool value) + { + Settings.DownloadSourceFile = value; + + using (new AssertionScope()) + { + _ = Settings.DownloadSourceFile.Should().Be(value); + + AssertAppendedValue("--source", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DownloadTranslationFiles_ShouldSetExpectedVariable(bool value) + { + Settings.DownloadTranslationFiles = value; + + using (new AssertionScope()) + { + _ = Settings.DownloadTranslationFiles.Should().Be(value); + + AssertAppendedValue("--translations", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DownloadXliffFiles_ShouldSetExpectedVariable(bool value) + { + Settings.DownloadXliffFiles = value; + + using (new AssertionScope()) + { + _ = Settings.DownloadXliffFiles.Should().Be(value); + + AssertAppendedValue("--xliff", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ForceDownloads_ShouldSetExpectedVariable(bool value) + { + Settings.ForceDownloads = value; + + using (new AssertionScope()) + { + _ = Settings.ForceDownloads.Should().Be(value); + + AssertAppendedValue("--force", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void KeepNewFiles_ShouldSetExpectedVariable(bool value) + { + Settings.KeepNewFiles = value; + + using (new AssertionScope()) + { + _ = Settings.KeepNewFiles.Should().Be(value); + + AssertAppendedValue("--keep-new-files", value); + } + } + + [Fact] + public void Languages_ShouldAppendExpectedValueToVariable() + { + Settings.Languages.Add("en*"); + + using (new AssertionScope()) + { + _ = Settings.Languages.Should().ContainSingle().And.Contain("en*"); + + AssertAppendedCollectionValue("--languages", new[] { "en*" }); + } + } + + [Fact] + public void Languages_ShouldDefaultToEmptyArray() + { + using (new AssertionScope()) + { + _ = Settings.Languages.Should().BeEmpty(); + + AssertAppendedCollectionValue("--languages", Array.Empty()); + } + } + + [Fact] + public void Languages_ShouldRemoveExpectedVariableWhenNull() + { + Languages_ShouldSetExpectedVariable(); + + Settings.Languages = null; + + using (new AssertionScope()) + { + AssertNoAvailableKey("--languages"); + } + } + + [Fact] + public void Languages_ShouldSetExpectedVariable() + { + var testValue = new List { "nb-NO*", "en*", "fr" }; + + Settings.Languages = testValue; + + using (new AssertionScope()) + { + _ = Settings.Languages.Should().BeSameAs(testValue); + + AssertAppendedCollectionValue("--languages", testValue); + } + } + + [Fact] + public void MinimumPercent_ShouldHaveMinusOneAsDefault() + { + using (new AssertionScope()) + { + { + _ = Settings.MinimumPercentage.Should().Be(-1); + + AssertNoAvailableKey("--minimum-perc"); + } + } + } + + [Fact] + public void MinimumPercent_ShouldSetExpectedVariable() + { + Settings.MinimumPercentage = 60; + + using (new AssertionScope()) + { + _ = Settings.MinimumPercentage.Should().Be(60); + + AssertAppendedValue("--minimum-perc", 60); + } + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(101)] + public void MinimumPercentage_ShouldThrowExceptionWhenOutOfRange(short value) + { + Action action = () => Settings.MinimumPercentage = value; + + _ = action.Should().Throw() + .WithMessage($"Value must be between 1 and 100. (Parameter '{nameof(Settings.MinimumPercentage)}')") + .And.ParamName.Should().Be(nameof(Settings.MinimumPercentage)); + } + + [Theory] + [MemberData(nameof(TransifexModes))] + public void Mode_ShouldSetExpectedVariable(PullMode mode) + { + Settings.Mode = mode; + + using (new AssertionScope()) + { + _ = Settings.Mode.Should().Be(mode); + + AssertAppendedValue("--mode", mode); + } + } + + [Fact] + public void Mode_ShouldSpecifDefaultByDefault() + => Settings.Mode.Should().Be(PullMode.Default); + + [Fact] + public void ParallelWorkers_ShouldDefaultToFive() + => Settings.ParallelWorkers.Should().Be(5); + + [Fact] + public void ParallelWorkers_ShouldNotSetFiveAsArgumentValueByDefault() + { + _ = Settings.ParallelWorkers; + + using (new AssertionScope()) + { + AssertNoAvailableKey("--workers"); + } + } + + [Theory] + [InlineData(0)] + [InlineData(21)] + public void ParallelWorkers_ShouldThrowExceptionWhenOutOfRange(byte workers) + { + Action action = () => Settings.ParallelWorkers = workers; + + _ = action.Should().Throw() + .WithMessage($"Value must be between 1 and 20. (Parameter '{nameof(Settings.ParallelWorkers)}')") + .And.ParamName.Should().Be(nameof(Settings.ParallelWorkers)); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(20)] + public void ParallelWorkes_ShouldSetExpectedVariable(byte workers) + { + Settings.ParallelWorkers = workers; + + using (new AssertionScope()) + { + _ = Settings.ParallelWorkers.Should().Be(workers); + + AssertAppendedValue("--workers", workers); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Pseudo_ShouldSetExpectedVariable(bool value) + { + Settings.Pseudo = value; + + using (new AssertionScope()) + { + _ = Settings.Pseudo.Should().Be(value); + + AssertAppendedValue("--pseudo", value); + } + } + + [Fact] + public void Resources_ShouldAppendExpectedValueToVariable() + { + Settings.Resources.Add("o:cake-contrib:p:caketransifex:r:exceptionsresx"); + + using (new AssertionScope()) + { + _ = Settings.Resources.Should().ContainSingle().And.Contain("o:cake-contrib:p:caketransifex:r:exceptionsresx"); + + AssertAppendedCollectionValue("--resources", new[] { "o:cake-contrib:p:caketransifex:r:exceptionsresx" }); + } + } + + [Fact] + public void Resources_ShouldDefaultToEmptyArray() + { + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeEmpty(); + + AssertAppendedCollectionValue("--resources", Array.Empty()); + } + } + + [Fact] + public void Resources_ShouldRemoveExpectedVariableWhenNull() + { + Resources_ShouldSetExpectedVariable(); + + Settings.Resources = null; + + using (new AssertionScope()) + { + AssertNoAvailableKey("--resources"); + } + } + + [Fact] + public void Resources_ShouldSetExpectedVariable() + { + var testValue = new List { "o:cake-contrib:p:caketransifex:r:exceptionsresx", "o:cake-contrib:p:caketransifex:r:commonresx" }; + + Settings.Resources = testValue; + + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeSameAs(testValue); + + AssertAppendedCollectionValue("--resources", testValue); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Silent_ShouldSetExpectedVariable(bool value) + { + Settings.Silent = value; + + using (new AssertionScope()) + { + _ = Settings.Silent.Should().Be(value); + + AssertAppendedValue("--silent", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SkipErrors_ShouldSetExpectedVariable(bool value) + { + Settings.SkipErrors = value; + + using (new AssertionScope()) + { + _ = Settings.SkipErrors.Should().Be(value); + + AssertAppendedValue("--skip", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UseGitTimestamps_ShouldSetExpectedVariable(bool value) + { + Settings.UseGitTimestamps = value; + + using (new AssertionScope()) + { + _ = Settings.UseGitTimestamps.Should().Be(value); + + AssertAppendedValue("--use-git-timestamps", value); + } + } +} diff --git a/src/Cake.Transifex.Tests/Settings/TxPushSettingsTests.cs b/src/Cake.Transifex.Tests/Settings/TxPushSettingsTests.cs new file mode 100644 index 0000000..0a9061a --- /dev/null +++ b/src/Cake.Transifex.Tests/Settings/TxPushSettingsTests.cs @@ -0,0 +1,349 @@ +namespace Cake.Transifex.Tests.Settings; + +using System; +using System.Collections.Generic; +using Cake.Transifex.Settings; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +public class TxPushSettingsTests : TxGlobalSettingsTests +{ + [Fact] + public void BranchCurrent_ShouldRemoveVariableWhenNull() + { + BranchCurrent_ShouldSetExpectedVariable("something"); + + Settings.Branch = null; + + using (new AssertionScope()) + { + _ = Settings.Branch.Should().BeNull(); + + AssertNoAvailableKey("--branch"); + } + } + + [Theory] + [InlineData("develop")] + [InlineData("support/1.x")] + [InlineData("")] + public void BranchCurrent_ShouldSetExpectedVariable(string value) + { + Settings.Branch = value; + + using (new AssertionScope()) + { + _ = Settings.Branch.Should().Be(value); + + AssertAppendedValue("--branch", value); + } + } + + [Fact] + public void Constructor_ShouldNotSetAnyVariables() + { + using (new AssertionScope()) + { + _ = Settings.GetAllAppendedArguments().Should().BeEmpty(); + _ = Settings.GetAllPrependendArguments().Should().BeEmpty(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateMissingLanguages_ShouldSetExpectedVariable(bool value) + { + Settings.CreateMissingLanguages = value; + + using (new AssertionScope()) + { + _ = Settings.CreateMissingLanguages.Should().Be(value); + + AssertAppendedValue("--all", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ForceUploads_ShouldSetExpectedVariable(bool value) + { + Settings.ForceUploads = value; + + using (new AssertionScope()) + { + _ = Settings.ForceUploads.Should().Be(value); + + AssertAppendedValue("--force", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void KeepTranslations_ShouldSetExpectedVariable(bool value) + { + Settings.KeepTranslations = value; + + using (new AssertionScope()) + { + _ = Settings.KeepTranslations.Should().Be(value); + + AssertAppendedValue("--keep-translations", value); + } + } + + [Fact] + public void Languages_ShouldAppendExpectedValueToVariable() + { + Settings.Languages.Add("en*"); + + using (new AssertionScope()) + { + _ = Settings.Languages.Should().ContainSingle().And.Contain("en*"); + + AssertAppendedCollectionValue("--languages", new[] { "en*" }); + } + } + + [Fact] + public void Languages_ShouldDefaultToEmptyArray() + { + using (new AssertionScope()) + { + _ = Settings.Languages.Should().BeEmpty(); + + AssertAppendedCollectionValue("--languages", Array.Empty()); + } + } + + [Fact] + public void Languages_ShouldRemoveExpectedVariableWhenNull() + { + Languages_ShouldSetExpectedVariable(); + + Settings.Languages = null; + + using (new AssertionScope()) + { + AssertNoAvailableKey("--languages"); + } + } + + [Fact] + public void Languages_ShouldSetExpectedVariable() + { + var testValue = new List { "nb-NO*", "en*", "fr" }; + + Settings.Languages = testValue; + + using (new AssertionScope()) + { + _ = Settings.Languages.Should().BeSameAs(testValue); + + AssertAppendedCollectionValue("--languages", testValue); + } + } + + [Fact] + public void ParallelWorkers_ShouldDefaultToFive() + => Settings.ParallelWorkers.Should().Be(5); + + [Fact] + public void ParallelWorkers_ShouldNotSetFiveAsArgumentValueByDefault() + { + _ = Settings.ParallelWorkers; + + using (new AssertionScope()) + { + AssertNoAvailableKey("--workers"); + } + } + + [Theory] + [InlineData(0)] + [InlineData(21)] + public void ParallelWorkers_ShouldThrowExceptionWhenOutOfRange(byte workers) + { + Action action = () => Settings.ParallelWorkers = workers; + + _ = action.Should().Throw() + .WithMessage($"Value must be between 1 and 20. (Parameter '{nameof(Settings.ParallelWorkers)}')") + .And.ParamName.Should().Be(nameof(Settings.ParallelWorkers)); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(20)] + public void ParallelWorkes_ShouldSetExpectedVariable(byte workers) + { + Settings.ParallelWorkers = workers; + + using (new AssertionScope()) + { + _ = Settings.ParallelWorkers.Should().Be(workers); + + AssertAppendedValue("--workers", workers); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReplaceEditedStrings_ShouldSetExpectedVariable(bool value) + { + Settings.ReplaceEditedStrings = value; + + using (new AssertionScope()) + { + _ = Settings.ReplaceEditedStrings.Should().Be(value); + + AssertAppendedValue("--replace-edited-strings", value); + } + } + + [Fact] + public void Resources_ShouldAppendExpectedValueToVariable() + { + Settings.Resources.Add("o:cake-contrib:p:caketransifex:r:exceptionsresx"); + + using (new AssertionScope()) + { + _ = Settings.Resources.Should().ContainSingle().And.Contain("o:cake-contrib:p:caketransifex:r:exceptionsresx"); + + AssertAppendedCollectionValue("--resources", new[] { "o:cake-contrib:p:caketransifex:r:exceptionsresx" }); + } + } + + [Fact] + public void Resources_ShouldDefaultToEmptyArray() + { + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeEmpty(); + + AssertAppendedCollectionValue("--resources", Array.Empty()); + } + } + + [Fact] + public void Resources_ShouldRemoveExpectedVariableWhenNull() + { + Resources_ShouldSetExpectedVariable(); + + Settings.Resources = null; + + using (new AssertionScope()) + { + AssertNoAvailableKey("--resources"); + } + } + + [Fact] + public void Resources_ShouldSetExpectedVariable() + { + var testValue = new List { "o:cake-contrib:p:caketransifex:r:exceptionsresx", "o:cake-contrib:p:caketransifex:r:commonresx" }; + + Settings.Resources = testValue; + + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeSameAs(testValue); + + AssertAppendedCollectionValue("--resources", testValue); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Silent_ShouldSetExpectedVariable(bool value) + { + Settings.Silent = value; + + using (new AssertionScope()) + { + _ = Settings.Silent.Should().Be(value); + + AssertAppendedValue("--silent", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SkipErrors_ShouldSetExpectedVariable(bool value) + { + Settings.SkipErrors = value; + + using (new AssertionScope()) + { + _ = Settings.SkipErrors.Should().Be(value); + + AssertAppendedValue("--skip", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UploadSourceFile_ShouldSetExpectedVariable(bool value) + { + Settings.UploadSourceFile = value; + + using (new AssertionScope()) + { + _ = Settings.UploadSourceFile.Should().Be(value); + + AssertAppendedValue("--source", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UploadTranslationFiles_ShouldSetExpectedVariable(bool value) + { + Settings.UploadTranslationFiles = value; + + using (new AssertionScope()) + { + _ = Settings.UploadTranslationFiles.Should().Be(value); + + AssertAppendedValue("--translation", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UploadXliffFiles_ShouldSetExpectedVariable(bool value) + { + Settings.UploadXliffFiles = value; + + using (new AssertionScope()) + { + _ = Settings.UploadXliffFiles.Should().Be(value); + + AssertAppendedValue("--xliff", value); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void UseGitTimestamps_ShouldSetExpectedVariable(bool value) + { + Settings.UseGitTimestamps = value; + + using (new AssertionScope()) + { + _ = Settings.UseGitTimestamps.Should().Be(value); + + AssertAppendedValue("--use-git-timestamps", value); + } + } +} diff --git a/src/Cake.Transifex.Tests/Settings/TxStatusSettingsTests.cs b/src/Cake.Transifex.Tests/Settings/TxStatusSettingsTests.cs new file mode 100644 index 0000000..4a80376 --- /dev/null +++ b/src/Cake.Transifex.Tests/Settings/TxStatusSettingsTests.cs @@ -0,0 +1,70 @@ +namespace Cake.Transifex.Tests.Settings; + +using System; +using System.Collections.Generic; +using Cake.Transifex.Settings; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +public class TxStatusSettingsTests : TxGlobalSettingsTests +{ + [Fact] + public void Resources_ShouldDefaultToAnEmptyArray() + { + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeEmpty(); + + var arguments = Settings.GetAllAppendedArguments(); + + _ = arguments.Should().ContainKey("--resources").WhoseValue.Should().BeOfType>(); + var resources = (ICollection)arguments["--resources"]; + _ = resources.Should().BeEmpty(); + + _ = arguments.Should().NotContainKey("!--resources"); + _ = Settings.GetAllPrependendArguments().Should().NotContainKeys("!--resources", "--resources"); + } + } + + [Fact] + public void Resources_ShouldKeepEmptyArrayWhenSet() + { + Settings.Resources = Array.Empty(); + + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeEmpty(); + + AssertAppendedValue("--resources", Array.Empty()); + } + } + + [Fact] + public void Resources_ShouldSetExpectedVariable() + { + var expected = new[] { "nb" }; + Settings.Resources = expected; + + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeEquivalentTo(expected); + + AssertAppendedValue("--resources", expected); + } + } + + [Fact] + public void Resources_ShouldSetMultipleValuesToExpectedVariable() + { + var expected = new[] { "nb*", "en*", "fr" }; + Settings.Resources = expected; + + using (new AssertionScope()) + { + _ = Settings.Resources.Should().BeEquivalentTo(expected); + + AssertAppendedValue("--resources", expected); + } + } +} diff --git a/src/Cake.Transifex/Aliases/TxAliases.cs b/src/Cake.Transifex/Aliases/TxAliases.cs new file mode 100644 index 0000000..8b14e2a --- /dev/null +++ b/src/Cake.Transifex/Aliases/TxAliases.cs @@ -0,0 +1,223 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Aliases; + +using Cake.Core; +using Cake.Core.Annotations; +using Cake.Transifex.Runners; +using Cake.Transifex.Settings; + +/// +/// Contains aliases to communicate with the latest tx executable. +/// +[CakeAliasCategory("Localization")] +[CakeNamespaceImport("Cake.Transifex.Enums")] +[CakeNamespaceImport("Cake.Transifex.Aliases")] +[CakeNamespaceImport("Cake.Transifex.Settings")] +public static class TxAliases +{ + /// + /// Initializes the current git repository with a default configuration file that can be used with Transifex. + /// + /// The context. + /// The settings to use when initializing the repository. + /// + /// + /// + /// { + /// TxInit(); + /// }); + /// ]]> + /// + /// + /// + /// { + /// var settings = new TxInitSettings + /// { + /// Hostname = "www.transifex.com" + /// }; + /// TxInit(settings); + /// }); + /// ]]> + /// + /// + [CakeMethodAlias] + public static void TxInit(this ICakeContext context, TxInitSettings settings = null) + => Run(context, settings ?? new TxInitSettings()); + + /// + /// Downloads the translated files from Transifex using a previously initialized configuration file. + /// + /// The context. + /// The token to use when authorizing with Transifex. + /// + /// + /// + /// { + /// TxPull("transifex-authorization-token"); + /// }); + /// ]]> + /// + /// + [CakeMethodAlias] + public static void TxPull(this ICakeContext context, string token) + { + Expects.NotNullOrWhitespace(token); + + TxPull(context, new TxPullSettings + { + Token = token, + }); + } + + /// + /// Downloads the translated files from Transifex using a previously initialized configuration file. + /// + /// The context. + /// The settings for the arguments to pass to the tx executable. + /// + /// + /// + /// { + /// var settings = new TxPullSettings + /// { + /// DownloadAllFiles = true, + /// MinimumPercentage = 75, + /// Mode = PullMode.Reviewed, + /// Token = "transifex-authorization-token", + /// UseGitTimestamps = true, + /// }; + /// TxPull(settings); + /// }); + /// ]]> + /// + /// + [CakeMethodAlias] + public static void TxPull(this ICakeContext context, TxPullSettings settings) + => Run(context, settings); + + /// + /// Uploads all local source files to Transifex using the previously initalized configuration file. + /// + /// The context. + /// The token to use when authorizing with Transifex. + /// + /// + /// + /// { + /// TxPush("transifex-authorization-token"); + /// }); + /// ]]> + /// + /// + [CakeMethodAlias] + public static void TxPush(this ICakeContext context, string token) + { + Expects.NotNullOrWhitespace(token); + + TxPush(context, new TxPushSettings + { + Token = token, + }); + } + + /// + /// Uploads all local source files to Transifex using the previously initalized configuration file. + /// + /// The context. + /// The settings for the arguments to pass to the tx executable. + /// + /// + /// + /// { + /// var settings = new TxPushSettings + /// { + /// Token = "transifex-authorization-token", + /// UploadSourceFile = true, + /// UseGitTimestamps = true, + /// }; + /// TxPush(settings); + /// }); + /// ]]> + /// + /// + [CakeMethodAlias] + public static void TxPush(this ICakeContext context, TxPushSettings settings) + => Run(context, settings); + + /// + /// Prints the status of the current project by reading the data in the configuration file. + /// + /// The context. + /// The token to use when authorizing with Transifex. + /// + /// + /// + /// { + /// TxStatus("transifex-authorization-token"); + /// }); + /// ]]> + /// + /// + [CakeMethodAlias] + public static void TxStatus(this ICakeContext context, string token) + { + Expects.NotNullOrWhitespace(token); + + TxStatus(context, new TxStatusSettings + { + Token = token, + }); + } + + /// + /// Prints the status of the current project by reading the data in the configuration file. + /// + /// The context. + /// The settings for the arguments to pass to the tx executable. + /// + /// + /// + /// { + /// var settings = new TxStatusSettings + /// { + /// Resources = new[] { "resource-name }, + /// Token = "transifex-authorization-token" + /// }; + /// TxStatus(settings); + /// });]]> + /// + /// + [CakeMethodAlias] + public static void TxStatus(this ICakeContext context, TxStatusSettings settings) + => Run(context, settings); + + private static void Run(ICakeContext context, TxGlobalSettings settings) + { + Expects.NotNull(context); + Expects.NotNull(settings); + + var runner = new TxRunner(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools); + + runner.Run(settings); + } +} diff --git a/src/Cake.Transifex/AssemblyInfo.cs b/src/Cake.Transifex/AssemblyInfo.cs index 36d82a6..be958ca 100644 --- a/src/Cake.Transifex/AssemblyInfo.cs +++ b/src/Cake.Transifex/AssemblyInfo.cs @@ -1,5 +1,5 @@ // -// Copyright (c) 2017-2021 Kim J. Nordmo and Cake Contrib. +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. // Licensed under the MIT license. See LICENSE in the project. // diff --git a/src/Cake.Transifex/Cake.Transifex.csproj b/src/Cake.Transifex/Cake.Transifex.csproj index 06830a9..d0c2744 100644 --- a/src/Cake.Transifex/Cake.Transifex.csproj +++ b/src/Cake.Transifex/Cake.Transifex.csproj @@ -16,8 +16,9 @@ Supports Cake $(CakeVersion)+ bin\$(Configuration)\$(TargetFramework)\Cake.Transifex.xml en-GB net7.0;net6.0 - 7 + 10 ..\Cake.Transifex.ruleset + disable diff --git a/src/Cake.Transifex/Common.Designer.cs b/src/Cake.Transifex/Common.Designer.cs index 7ca6220..f193179 100644 --- a/src/Cake.Transifex/Common.Designer.cs +++ b/src/Cake.Transifex/Common.Designer.cs @@ -10,7 +10,6 @@ namespace Cake.Transifex { using System; - using System.Reflection; /// @@ -20,7 +19,7 @@ namespace Cake.Transifex { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Common { @@ -40,7 +39,7 @@ internal Common() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Cake.Transifex.Common", typeof(Common).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Cake.Transifex.Common", typeof(Common).Assembly); resourceMan = temp; } return resourceMan; @@ -69,5 +68,14 @@ internal static string TransifexRunner { return ResourceManager.GetString("TransifexRunner", resourceCulture); } } + + /// + /// Looks up a localized string similar to TX (Transifex) Runner. + /// + internal static string TxRunner { + get { + return ResourceManager.GetString("TxRunner", resourceCulture); + } + } } } diff --git a/src/Cake.Transifex/Common.resx b/src/Cake.Transifex/Common.resx index bffb545..e2a2056 100644 --- a/src/Cake.Transifex/Common.resx +++ b/src/Cake.Transifex/Common.resx @@ -120,4 +120,7 @@ Transifex Runner + + TX (Transifex) Runner + \ No newline at end of file diff --git a/src/Cake.Transifex/Enums/ContentEncodings.cs b/src/Cake.Transifex/Enums/ContentEncodings.cs new file mode 100644 index 0000000..ecbf926 --- /dev/null +++ b/src/Cake.Transifex/Enums/ContentEncodings.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Enums; + +/// +/// The available encodings to use for contents. +/// +public enum ContentEncodings +{ + /// + /// Use the text encoding. + /// + Text, + + /// + /// Use the Base 64 encoding. + /// + Base64, +} diff --git a/src/Cake.Transifex/Enums/PullMode.cs b/src/Cake.Transifex/Enums/PullMode.cs new file mode 100644 index 0000000..fe79f07 --- /dev/null +++ b/src/Cake.Transifex/Enums/PullMode.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Enums; + +/// +/// Specifies the mode of the translation file to pull. +/// +public enum PullMode +{ + /// + /// Use the default mode for translations. + /// + Default, + + /// + /// Only pull down reviewed translations. + /// + /// + /// All translations that have not been reviewed will either be + /// empty, or be in the source language (depending on the file + /// format used) + /// + Reviewed, + + /// + /// Pull down all translations that have been proof read. + /// + /// + /// All translations that have not been proof read will either be + /// empty, or be in the source language (depending on the file + /// format used). + /// + Proofread, + + /// + /// Pull down all completed translations, whether they have been + /// reviewed or not. + /// + /// + /// These are files suitable for offline translation of the + /// resource(s). + /// + Translator, + + /// + /// Pull down translation strings that has not been translated + /// yet. + /// + Untranslated, + + /// + /// Pull down only strings that have been translated. + /// + OnlyTranslated, + + /// + /// Pull down only strings that have been translated and reviewed. + /// + OnlyReviewed, + + /// + /// Pull down only strings that have been proofread. + /// + OnlyProofread, + + /// + /// Pull down both translated and untranslated strings + /// (untranslated strings will be set to the value of the source). + /// + SourceAsTranslation, +} diff --git a/src/Cake.Transifex/Expects.cs b/src/Cake.Transifex/Expects.cs new file mode 100644 index 0000000..ea6939f --- /dev/null +++ b/src/Cake.Transifex/Expects.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +/// +/// Contains a collection of helpers to use when ensuring certain +/// conditions. +/// +/// +/// New conditions should be added to this class when needed. +/// +internal static class Expects +{ + /// + /// Asserts that the specified is not + /// null. + /// + /// The type of the value. + /// The value to assert. + /// Name of the parameter. + /// + /// If the passed in is null. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static void NotNull([NotNull] TValue value, [CallerArgumentExpression(nameof(value))] string parameterName = null) + => ArgumentNullException.ThrowIfNull(value, parameterName); + + /// + /// Asserts that the specified is not + /// null or white space. + /// + /// The value to assert. + /// Name of the parameter. + /// + /// If the passed in is null. + /// + /// + /// If the passed in is empty or only + /// contain white space. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static void NotNullOrWhitespace([NotNull] string value, [CallerArgumentExpression(nameof(value))] string parameterName = null) + { +#if NET7_0_OR_GREATER + ArgumentNullException.ThrowIfNullOrEmpty(value?.Trim(), parameterName); +#else + NotNull(value, parameterName); + + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException(null, nameof(value)); + } +#endif + } + + /// + /// Asserts that the specified is between + /// the specified and + /// values (Inclusive). + /// + /// The inclusive minimum value expected. + /// The inclusive maximum value expected. + /// The value to assert. + /// Name of the parameter. + /// + /// Value must be between and + /// . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static void Range(int minimum, int maximum, int value, [CallerArgumentExpression(nameof(value))] string parameterName = null) + { + if (value < minimum || value > maximum) + { + throw new ArgumentOutOfRangeException(parameterName, $"Value must be between {minimum} and {maximum}."); + } + } +} diff --git a/src/Cake.Transifex/Runners/TxRunner.cs b/src/Cake.Transifex/Runners/TxRunner.cs new file mode 100644 index 0000000..657d945 --- /dev/null +++ b/src/Cake.Transifex/Runners/TxRunner.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Runners; + +using System; +using System.Collections.Generic; +using System.Linq; + +using Cake.Core; +using Cake.Core.IO; +using Cake.Core.Tooling; +using Cake.Transifex.Settings; + +/// +/// The wrapper around the tx client. This class cannot be inherited. +/// +/// +internal sealed class TxRunner : Tool +{ + /// + /// Initializes a new instance of the + /// class. + /// + /// The file system. + /// The environment. + /// The process runner. + /// The tool locator. + internal TxRunner(IFileSystem fileSystem, ICakeEnvironment environment, IProcessRunner processRunner, IToolLocator tools) + : base(fileSystem, environment, processRunner, tools) + { + } + + /// + /// Runs the tx client using the specified + /// . + /// + /// The settings. + /// + /// If the passed in is null. + /// + public void Run(TxGlobalSettings settings) + { + Expects.NotNull(settings); + + var args = GetTxRunnerArguments(settings); + Run(settings, args); + } + + /// + protected override IEnumerable GetToolExecutableNames() + => new[] { "tx.exe", "tx" }; + + /// + protected override string GetToolName() + => Common.TxRunner; + + private static void AddValue(ProcessArgumentBuilder args, string key, object value) + { + if (value.GetType().IsEnum) + { + AddValue(args, key, value.ToString().ToLowerInvariant()); + } + else + { + AddValue(args, key, value.ToString()); + } + } + + private static void AddValue(ProcessArgumentBuilder args, string key, string value) + { + if (string.IsNullOrEmpty(value)) + { + AddValue(args, key, "\"\""); + return; + } + + var quote = value.IndexOfAny(new[] { ' ', '*', ',' }) >= 0; + + if (key.StartsWith("!", StringComparison.OrdinalIgnoreCase)) + { + if (quote) + { + _ = args.AppendSwitchQuotedSecret(key.TrimStart('!'), value); + } + else + { + _ = args.AppendSwitchSecret(key.TrimStart('!'), value); + } + } + else + { + if (quote) + { + _ = args.AppendSwitchQuoted(key, value); + } + else + { + _ = args.AppendSwitch(key, value); + } + } + } + + private static void AddValue(ProcessArgumentBuilder args, string key, bool value) + { + if (value) + { + _ = args.Append(key); + } + } + + private static ProcessArgumentBuilder GetTxRunnerArguments(TxGlobalSettings settings) + { + var args = new ProcessArgumentBuilder(); + + SetArguments(args, settings.GetAllPrependendArguments()); + _ = args.Append(settings.Command); + SetArguments(args, settings.GetAllAppendedArguments()); + + return args; + } + + private static void SetArguments(ProcessArgumentBuilder args, IDictionary arguments) + { + foreach (var argument in arguments.Where(a => a.Value != null)) + { + switch (argument.Value) + { + case bool value: + AddValue(args, argument.Key, value); + break; + + case ICollection stringValue: + if (stringValue.Count > 0) + { + AddValue(args, argument.Key, string.Join(',', stringValue)); + } + + break; + + default: + AddValue(args, argument.Key, argument.Value); + break; + } + } + } +} diff --git a/src/Cake.Transifex/Settings/TxGlobalSettings.cs b/src/Cake.Transifex/Settings/TxGlobalSettings.cs new file mode 100644 index 0000000..1e28017 --- /dev/null +++ b/src/Cake.Transifex/Settings/TxGlobalSettings.cs @@ -0,0 +1,262 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Settings; + +using System.Collections.Generic; + +using Cake.Core.IO; +using Cake.Core.Tooling; + +/// +/// Defines the arguments that are available for all commands, which +/// are usually prepended to the command. +/// +/// +public abstract class TxGlobalSettings : ToolSettings +{ + private readonly IDictionary appendArguments = new Dictionary(); + private readonly IDictionary prependArguments = new Dictionary(); + + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The name of the command to execute. + /// + protected TxGlobalSettings(string command) + { + Command = command; + } + + /// + /// Gets or sets the certificate to use when connecting to the + /// host. + /// + /// The path to the CA Certificate. + public FilePath CertificatePath + { + get => GetPrependValue("--cacert"); + set => PrependValue("--cacert", value); + } + + /// + /// Gets or sets the path to the transifex resource configuration. + /// + /// + /// The path to the transifex resource configuration. + /// + public FilePath Configuration + { + get => GetPrependValue("--config"); + set => PrependValue("--config", value); + } + + /// + /// Gets or sets the name of the API host. + /// + /// The name of the API host. + public string HostName + { + get => GetPrependValue("--hostname"); + set => PrependValue("--hostname", value); + } + + /// + /// Gets or sets the path to the root configuration. + /// + /// The path to the root configuration. + public FilePath RootConfiguration + { + get => GetPrependValue("--root-config"); + set => PrependValue("--root-config", value); + } + + /// + /// Gets or sets the authorization token to use when cummunicating + /// with transifex. + /// + /// The authorization token. + public string Token + { + get => GetPrependValue("!--token"); + set => PrependValue("!--token", value); + } + + /// + /// Gets or sets the name of the command to execute. + /// + internal string Command { get; set; } + + /// + /// Gets all the arguments that should be added after the command + /// name. + /// + /// The stored arguments. + internal virtual IDictionary GetAllAppendedArguments() + => appendArguments; + + /// + /// Gets all the arguments that should be added before the command + /// name. + /// + /// The stored arguments. + internal virtual IDictionary GetAllPrependendArguments() + => prependArguments; + + /// + /// Stores the specified with the + /// specified as an argument that should + /// be appended after the command name. + /// + /// The key/name to use for the argument. + /// + /// The value that should be part of the key. + /// + /// + /// if set to true allow the argument to be used without a + /// value. + /// + /// + /// Marking allow empty value stores empty and whitespace values, + /// but remove any passed null values. + /// + protected void AppendValue(string key, object value, bool allowEmptyValue = false) + => SetValue(appendArguments, key, value, allowEmptyValue); + + /// + /// Gets the stored value with the specified + /// . + /// + /// The key/id of the value. + /// + /// The expected type the value should be in. + /// + /// + /// The stored value, or the default value of a collection. + /// + protected ICollection GetAppendedCollectionValue(string key) + { + var collection = GetAppendedValue>(key); + + if (collection is null) + { + collection = new List(); + AppendValue(key, collection); + } + + return collection; + } + + /// + /// Gets the stored value with the specified + /// . + /// + /// The key/id of the value. + /// + /// The value to return if no value have been stored. + /// + /// + /// The expected type the value should be in. + /// + /// + /// The stored value, or the specified + /// . + /// + protected TValue GetAppendedValue(string key, TValue defaultValue = default) + { + if (appendArguments.TryGetValue(key, out var objValue) && objValue is TValue value) + { + return value; + } + + return defaultValue; + } + + /// + /// Gets the stored value with the specified + /// . + /// + /// The key/id of the value. + /// + /// The expected type the value should be in. + /// + /// + /// The stored value, or the default value for a collection. + /// + protected ICollection GetPrependCollectionValue(string key) + { + var collection = GetPrependValue>(key); + + if (collection is null) + { + collection = new List(); + PrependValue(key, collection); + } + + return collection; + } + + /// + /// Gets the stored value with the specified + /// . + /// + /// The key/id of the value. + /// + /// The value to return if no value have been stored. + /// + /// + /// The expected type the value should be in. + /// + /// + /// The stored value, or the specified + /// . + /// + protected TValue GetPrependValue(string key, TValue defaultValue = default) + { + if (prependArguments.TryGetValue(key, out var objValue) && objValue is TValue value) + { + return value; + } + + return defaultValue; + } + + /// + /// Stores the specified with the + /// specified as an argument that should + /// be prepended before the command name. + /// + /// The key/name to use for the argument. + /// + /// The value that should be part of the key. + /// + /// + /// if set to true allow the argument to be used without a + /// value. + /// + /// + /// Marking allow empty value stores empty and whitespace values, + /// but remove any passed null values. + /// + protected void PrependValue(string key, object value, bool allowEmptyValue = false) + => SetValue(prependArguments, key, value, allowEmptyValue); + + private static void SetValue(IDictionary storage, string key, object value, bool allowEmptyValue) + { + if (value is null || (!allowEmptyValue && value is string sValue && string.IsNullOrWhiteSpace(sValue))) + { + if (storage.ContainsKey(key)) + { + _ = storage.Remove(key); + } + } + else + { + storage[key] = value; + } + } +} diff --git a/src/Cake.Transifex/Settings/TxInitSettings.cs b/src/Cake.Transifex/Settings/TxInitSettings.cs new file mode 100644 index 0000000..12654ee --- /dev/null +++ b/src/Cake.Transifex/Settings/TxInitSettings.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Settings; + +/// +/// Defines the arguments that are available when initializing +/// transifex configuration. +/// +/// +public sealed class TxInitSettings : TxGlobalSettings +{ + /// + /// Initializes a new instance of the + /// class. + /// + public TxInitSettings() + : base("init") + { + } +} diff --git a/src/Cake.Transifex/Settings/TxPullSettings.cs b/src/Cake.Transifex/Settings/TxPullSettings.cs new file mode 100644 index 0000000..5e5f2cc --- /dev/null +++ b/src/Cake.Transifex/Settings/TxPullSettings.cs @@ -0,0 +1,286 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Settings; + +using System; +using System.Collections.Generic; + +using Cake.Transifex.Enums; + +/// +/// Defines the arguments available when pulling down translations +/// from transifex. +/// +/// +public sealed class TxPullSettings : TxGlobalSettings +{ + /// + /// Initializes a new instance of the + /// class. + /// + public TxPullSettings() + : base("pull") + { + } + + /// + /// Gets or sets the name of the current branch to use when + /// pulling files. + /// + /// The name of the branch to pull files from. + /// + /// Set the property as an empty string to have the tx utility try + /// resolving the current branch name. + /// + public string Branch + { + get => GetAppendedValue("--branch"); + set => AppendValue("--branch", value, allowEmptyValue: true); + } + + /// + /// Gets or sets the content encoding to use. + /// + /// The content encoding. + public ContentEncodings ContentEncoding + { + get => GetAppendedValue("--content_encoding", ContentEncodings.Text); + set => AppendValue("--content_encoding", value); + } + + /// + /// Gets or sets a value indicating whether existing files should + /// be overwritten. + /// + /// + /// true to overwrite files; otherwise, false. + /// + public bool DisableOverwrite + { + get => GetAppendedValue("--disable-overwrite", false); + set => AppendValue("--disable-overwrite", value); + } + + /// + /// Gets or sets a value indicating whether all files should be + /// downloaded no matter what the status is. + /// + /// + /// true to download all files; otherwise, false. + /// + public bool DownloadAllFiles + { + get => GetAppendedValue("--all", false); + set => AppendValue("--all", value); + } + + /// + /// Gets or sets a value indicating whether to download + /// translations as JSON files. + /// + /// + /// true if to download JSON files; otherwise, + /// false. + /// + public bool DownloadJsonFiles + { + get => GetAppendedValue("--json", false); + set => AppendValue("--json", value); + } + + /// + /// Gets or sets a value indicating whether the source files + /// should be downloaded. + /// + /// + /// true to download the source files; otherwise, + /// false. + /// + public bool DownloadSourceFile + { + get => GetAppendedValue("--source", false); + set => AppendValue("--source", value); + } + + /// + /// Gets or sets a value indicating whether translation files + /// should be downloaded. + /// + /// + /// true to download translation files; otherwise, + /// false. + /// + public bool DownloadTranslationFiles + { + get => GetAppendedValue("--translations", false); + set => AppendValue("--translations", value); + } + + /// + /// Gets or sets a value indicating whether to download + /// translations as xliff files. + /// + /// + /// true to download xliff files; otherwise, false. + /// + public bool DownloadXliffFiles + { + get => GetAppendedValue("--xliff", false); + set => AppendValue("--xliff", value); + } + + /// + /// Gets or sets a value indicating whether files that are still + /// up to date should be downloaded or not. + /// + /// + /// true to download files that are up to date; otherwise, + /// false. + /// + public bool ForceDownloads + { + get => GetAppendedValue("--force", false); + set => AppendValue("--force", value); + } + + /// + /// Gets or sets a value indicating whether to create new files + /// with the .new file extension. + /// + /// + /// true to keep new files; otherwise, false. + /// + /// + /// Used together when the is + /// true. + /// + public bool KeepNewFiles + { + get => GetAppendedValue("--keep-new-files", false); + set => AppendValue("--keep-new-files", value); + } + + /// + /// Gets or sets the languages that should be downloaded. + /// + /// The languages to download. + public ICollection Languages + { + get => GetAppendedCollectionValue("--languages"); + set => AppendValue("--languages", value); + } + + /// + /// Gets or sets the minimum acceptable percentage of a + /// translation mode in order to download it. + /// + /// + /// The minimum acceptable percentage for the mode for it to be + /// downloaded. + /// + public short MinimumPercentage + { + get => GetAppendedValue("--minimum-perc", -1); + set + { + Expects.Range(1, 100, value, nameof(MinimumPercentage)); + + AppendValue("--minimum-perc", value); + } + } + + /// + /// Gets or sets the mode to use when downloading translations. + /// + /// The mode to use when downloading translations. + public PullMode Mode + { + get => GetAppendedValue("--mode", PullMode.Default); + set => AppendValue("--mode", value); + } + + /// + /// Gets or sets the amount of parallel workers to use when + /// downloading files. + /// + /// The amount of parallel workers. + /// + /// The tx client defaults to 5 workers, with a maximum value of + /// 20 workers. + /// + public byte ParallelWorkers + { + get => GetAppendedValue("--workers", 5); + set + { + Expects.Range(1, 20, value, nameof(ParallelWorkers)); + + AppendValue("--workers", value); + } + } + + /// + /// Gets or sets a value indicating whether pulling translations + /// should generate mock strings. + /// + /// + /// true to generate mock strings; otherwise, false. + /// + public bool Pseudo + { + get => GetAppendedValue("--pseudo", false); + set => AppendValue("--pseudo", value); + } + + /// + /// Gets or sets the resources to download. + /// + /// The resources to download. + public ICollection Resources + { + get => GetAppendedCollectionValue("--resources"); + set => AppendValue("--resources", value); + } + + /// + /// Gets or sets a value indicating whether the tx client should + /// reduce its verbosity or not. + /// + /// true if silent; otherwise, false. + public bool Silent + { + get => GetAppendedValue("--silent", false); + set => AppendValue("--silent", value); + } + + /// + /// Gets or sets a value indicating whether errors should be + /// ignored. + /// + /// + /// true to ignore errors; otherwise, false. + /// + public bool SkipErrors + { + get => GetAppendedValue("--skip", false); + set => AppendValue("--skip", value); + } + + /// + /// Gets or sets a value indicating whether local files should be + /// compared with their transifex equivalent based on the git + /// timestamp. + /// + /// + /// true if files should be compared using git timestamps; + /// otherwise, false. + /// + public bool UseGitTimestamps + { + get => GetAppendedValue("--use-git-timestamps", false); + set => AppendValue("--use-git-timestamps", value); + } +} diff --git a/src/Cake.Transifex/Settings/TxPushSettings.cs b/src/Cake.Transifex/Settings/TxPushSettings.cs new file mode 100644 index 0000000..fe8ef04 --- /dev/null +++ b/src/Cake.Transifex/Settings/TxPushSettings.cs @@ -0,0 +1,234 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Settings; + +using System; +using System.Collections.Generic; + +/// +/// Defines the arguments that are available when pushing source or +/// translation files. +/// +/// +public sealed class TxPushSettings : TxGlobalSettings +{ + /// + /// Initializes a new instance of the + /// class. + /// + public TxPushSettings() + : base("push") + { + } + + /// + /// Gets or sets name of the base branch to use when pushing + /// sources. + /// + /// The name to use as a base branch. + /// + /// If a name is not set, the main resource will be used as the + /// base. + /// + public string Base + { + get => GetAppendedValue("--base"); + set => AppendValue("--base", value); + } + + /// + /// Gets or sets the name of the current branch to use when + /// pushing sources. + /// + /// The name to use as the current branch. + /// + /// Set the property as an empty string to have the tx utility try + /// resolving the current branch name. + /// + public string Branch + { + get => GetAppendedValue("--branch"); + set => AppendValue("--branch", value, allowEmptyValue: true); + } + + /// + /// Gets or sets a value indicating whether to create missing + /// languages on the remote server when possible. + /// + /// + /// true if the server should try creating missing + /// languages; otherwise, false. + /// + public bool CreateMissingLanguages + { + get => GetAppendedValue("--all", false); + set => AppendValue("--all", value); + } + + /// + /// Gets or sets a value indicating whether all files sholud be + /// pushed without checking the modification time first. + /// + /// + /// true to push all files no matter when it was modified; + /// otherwise, false. + /// + public bool ForceUploads + { + get => GetAppendedValue("--force", false); + set => AppendValue("--force", value); + } + + /// + /// Gets or sets a value indicating whether to keep translations + /// even when a source key changes. + /// + /// + /// true to keep translations on source key changes; + /// otherwise, false. + /// + public bool KeepTranslations + { + get => GetAppendedValue("--keep-translations", false); + set => AppendValue("--keep-translations", value); + } + + /// + /// Gets or sets the languages to upload. + /// + /// The languages to upload. + public ICollection Languages + { + get => GetAppendedCollectionValue("--languages"); + set => AppendValue("--languages", value); + } + + /// + /// Gets or sets the amount of parallel workers to use when + /// uploading files. + /// + /// The amount of parallel workers. + /// + /// The tx client defaults to 5 workers, with a maximum value of + /// 20 workers. + /// + public byte ParallelWorkers + { + get => GetAppendedValue("--workers", 5); + set + { + Expects.Range(1, 20, value, nameof(ParallelWorkers)); + + AppendValue("--workers", value); + } + } + + /// + /// Gets or sets a value indicating whether to replace source + /// strings that have been edited. + /// + /// + /// true if replacing edited source strings; otherwise, + /// false. + /// + /// + /// Free usage of Transifex does not allow editing source strings. + /// + public bool ReplaceEditedStrings + { + get => GetAppendedValue("--replace-edited-strings", false); + set => AppendValue("--replace-edited-strings", value); + } + + /// + /// Gets or sets the resources to upload. + /// + /// The resources to upload. + public ICollection Resources + { + get => GetAppendedCollectionValue("--resources"); + set => AppendValue("--resources", value); + } + + /// + /// Gets or sets a value indicating whether the tx client should + /// reduce its verbosity or not. + /// + /// true if silent; otherwise, false. + public bool Silent + { + get => GetAppendedValue("--silent", false); + set => AppendValue("--silent", value); + } + + /// + /// Gets or sets a value indicating whether errors should be + /// ignored. + /// + /// + /// true to ignore errors; otherwise, false. + /// + public bool SkipErrors + { + get => GetAppendedValue("--skip", false); + set => AppendValue("--skip", value); + } + + /// + /// Gets or sets a value indicating whether the source files + /// should be uploaded. + /// + /// + /// true to upload the source files; otherwise, + /// false. + /// + public bool UploadSourceFile + { + get => GetAppendedValue("--source", false); + set => AppendValue("--source", value); + } + + /// + /// Gets or sets a value indicating whether translation files + /// should be uploaded. + /// + /// + /// true to upload translation files; otherwise, + /// false. + /// + public bool UploadTranslationFiles + { + get => GetAppendedValue("--translation", false); + set => AppendValue("--translation", value); + } + + /// + /// Gets or sets a value indicating whether to upload xliff files. + /// + /// + /// true to upload xliff files; otherwise, false. + /// + public bool UploadXliffFiles + { + get => GetAppendedValue("--xliff", false); + set => AppendValue("--xliff", value); + } + + /// + /// Gets or sets a value indicating whether local files should be + /// compared with their transifex equivalent based on the git + /// timestamp. + /// + /// + /// true if files should be compared using git timestamps; + /// otherwise, false. + /// + public bool UseGitTimestamps + { + get => GetAppendedValue("--use-git-timestamps", false); + set => AppendValue("--use-git-timestamps", value); + } +} diff --git a/src/Cake.Transifex/Settings/TxStatusSettings.cs b/src/Cake.Transifex/Settings/TxStatusSettings.cs new file mode 100644 index 0000000..4134ce7 --- /dev/null +++ b/src/Cake.Transifex/Settings/TxStatusSettings.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) 2017-2024 Kim J. Nordmo and Cake Contrib. +// Licensed under the MIT license. See LICENSE in the project. +// + +namespace Cake.Transifex.Settings; + +using System.Collections.Generic; + +/// +/// Defines the arguments available when acquiring the Transifex +/// status. +/// +/// +public sealed class TxStatusSettings : TxGlobalSettings +{ + /// + /// Initializes a new instance of the + /// class. + /// + public TxStatusSettings() + : base("status") + { + } + + /// + /// Gets or sets the resources to get the status for. + /// + /// The resources. + public ICollection Resources + { + get => GetAppendedCollectionValue("--resources"); + set => AppendValue("--resources", value); + } +} diff --git a/src/stylecop.json b/src/stylecop.json index c8e2e82..22b70d7 100644 --- a/src/stylecop.json +++ b/src/stylecop.json @@ -12,7 +12,7 @@ "copyrightText": "Copyright (c) {year} {user} and {companyName}.\nLicensed under the {licenseName} license. See {licenseFile} in the project.", "xmlHeader": true, "variables": { - "year": "2017-2021", + "year": "2017-2024", "user": "Kim J. Nordmo", "licenseName": "MIT", "licenseFile": "LICENSE"