diff --git a/Test/DurableTask.Core.Tests/VersionSettingsTests.cs b/Test/DurableTask.Core.Tests/VersionSettingsTests.cs deleted file mode 100644 index 44128164d..000000000 --- a/Test/DurableTask.Core.Tests/VersionSettingsTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ---------------------------------------------------------------------------------- -// Copyright Microsoft Corporation -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ---------------------------------------------------------------------------------- - -using DurableTask.Core.Settings; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace DurableTask.Core.Tests -{ - [TestClass] - public class VersionSettingsTests - { - [TestMethod] - [DataRow("1.0.0", "1.0.0", 0)] - [DataRow("1.1.0", "1.0.0", 1)] - [DataRow("1.0.0", "1.1.0", -1)] - [DataRow("1", "1", 0)] - [DataRow("2", "1", 1)] - [DataRow("1", "2", -1)] - [DataRow("", "1", -1)] - [DataRow("1", "", 1)] - [DataRow("", "", 0)] - public void TestVersionComparison(string orchVersion, string settingVersion, int expectedComparison) - { - int result = VersioningSettings.CompareVersions(orchVersion, settingVersion); - - if (expectedComparison == 0) - { - Assert.AreEqual(0, result, $"Expected {orchVersion} to be equal to {settingVersion}"); - } - else if (expectedComparison < 0) - { - Assert.IsTrue(result < 0, $"Expected {orchVersion} to be less than {settingVersion}"); - } - else - { - Assert.IsTrue(result > 0, $"Expected {orchVersion} to be greater than {settingVersion}"); - } - } - } -} \ No newline at end of file diff --git a/docs/features/versioning.md b/docs/features/versioning.md index 0eb6d21c4..4351cdb81 100644 --- a/docs/features/versioning.md +++ b/docs/features/versioning.md @@ -224,6 +224,14 @@ var instance = await client.CreateOrchestrationInstanceAsync( | `Reject` | Default. Abandon the work item so another worker can pick it up (or retry later). | | `Fail` | Fail the orchestration with a `VersionMismatch` error. | +#### Unversioned Infrastructure Orchestrations + +A small number of explicitly registered, unversioned infrastructure orchestrations are internally exempted from the worker version check so that they can run under the configured version policy without being treated as business orchestrations. This exemption is internal to the Durable Task Framework and its approved integrations; it is not a public, customer-configurable allow list. + +The exemption applies only when both the orchestration name is an exact, exempted match and its execution version is `null` or the empty string. Nonempty versions, including whitespace, still follow the configured version matching and failure strategies for every orchestration, exempted or not. Business orchestration version policies and failure behavior are otherwise unchanged and apply per worker exactly as described above. + +The exclusion skips only the worker version check, which runs before orchestration middleware and execution. It does not register an orchestration, change its execution version or history, or bypass name/version lookup. The same check applies on replay and after `ContinueAsNew`; continuing with a nonempty version re-enables the normal version policy. + #### Blue-Green Deployment Example Run old and new workers simultaneously during deployments: diff --git a/src/DurableTask.Core/AssemblyInfo.cs b/src/DurableTask.Core/AssemblyInfo.cs index d414ce1e6..f764b05ec 100644 --- a/src/DurableTask.Core/AssemblyInfo.cs +++ b/src/DurableTask.Core/AssemblyInfo.cs @@ -13,8 +13,17 @@ using System.Runtime.CompilerServices; -#if !SIGN_ASSEMBLY +#if SIGN_ASSEMBLY +// Grants the signed Azure Functions Durable Task host access to internal members, such as the +// worker-version exclusion used for its own infrastructure orchestrations. +// A strongly-named assembly must specify the full public key, not just the public key token, of +// any strongly-named friend assembly. +[assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.DurableTask, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")] +#else [assembly: InternalsVisibleTo("DurableTask.Core.Tests")] [assembly: InternalsVisibleTo("DurableTask.Framework.Tests")] [assembly: InternalsVisibleTo("DurableTask.ServiceBus.Tests")] +// An unsigned assembly does not enforce a public key on its friends, so the same host assembly +// can be declared by simple name alone for unsigned (non-Release) builds of this assembly. +[assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.DurableTask")] #endif diff --git a/src/DurableTask.Core/Settings/VersioningSettings.cs b/src/DurableTask.Core/Settings/VersioningSettings.cs index 41b00d161..43214e7ef 100644 --- a/src/DurableTask.Core/Settings/VersioningSettings.cs +++ b/src/DurableTask.Core/Settings/VersioningSettings.cs @@ -12,6 +12,7 @@ // ---------------------------------------------------------------------------------- using System; +using System.Collections.Generic; namespace DurableTask.Core.Settings { @@ -72,6 +73,19 @@ public enum VersionFailureStrategy /// public VersionFailureStrategy FailureStrategy { get; set; } = VersionFailureStrategy.Reject; + /// + /// Gets the orchestration names whose unversioned executions are excluded from worker version checks. + /// + /// + /// This set is empty by default and matches names using an exact, case-sensitive ordinal comparison. + /// An exclusion applies only when the execution version is null or empty; nonempty versions still + /// follow and . + /// This is reserved for registering explicitly, unversioned infrastructure orchestrations, not business + /// orchestrations. Exclusions do not change execution versions or orchestration registration and lookup. + /// Configure this set before starting the worker and do not modify it while the worker is running. + /// + internal ISet ExcludedOrchestrationNames { get; } = new HashSet(StringComparer.Ordinal); + /// /// Compare two versions to each other. /// diff --git a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs index 649e7b47a..679d37774 100644 --- a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs +++ b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs @@ -398,7 +398,9 @@ protected async Task OnProcessWorkItemAsync(TaskOrchestrationWorkItem work IReadOnlyList decisions = new List(); bool versioningFailed = false; - if (this.versioningSettings != null) + if (this.versioningSettings != null && + !(string.IsNullOrEmpty(runtimeState.Version) && + this.versioningSettings.ExcludedOrchestrationNames.Contains(runtimeState.Name))) { switch (this.versioningSettings.MatchStrategy) { diff --git a/test/DurableTask.Core.Tests/TaskOrchestrationDispatcherVersioningTests.cs b/test/DurableTask.Core.Tests/TaskOrchestrationDispatcherVersioningTests.cs new file mode 100644 index 000000000..c56ec6418 --- /dev/null +++ b/test/DurableTask.Core.Tests/TaskOrchestrationDispatcherVersioningTests.cs @@ -0,0 +1,373 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace DurableTask.Core.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using DurableTask.Core.Exceptions; + using DurableTask.Core.History; + using DurableTask.Core.Logging; + using DurableTask.Core.Middleware; + using DurableTask.Core.Settings; + using DurableTask.Emulator; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using static DurableTask.Core.Settings.VersioningSettings; + + [TestClass] + public class TaskOrchestrationDispatcherVersioningTests + { + const string InternalName = "Internal.Orchestration"; + + [TestMethod] + [DataRow(VersionMatchStrategy.Strict, null, false)] + [DataRow(VersionMatchStrategy.Strict, "", false)] + [DataRow(VersionMatchStrategy.Strict, "1.0", true)] + [DataRow(VersionMatchStrategy.Strict, "1.0.0", false)] + [DataRow(VersionMatchStrategy.Strict, "2.0", false)] + [DataRow(VersionMatchStrategy.CurrentOrOlder, null, true)] + [DataRow(VersionMatchStrategy.CurrentOrOlder, "", true)] + [DataRow(VersionMatchStrategy.CurrentOrOlder, "0.9", true)] + [DataRow(VersionMatchStrategy.CurrentOrOlder, "1.0", true)] + [DataRow(VersionMatchStrategy.CurrentOrOlder, "2.0", false)] + [DataRow(VersionMatchStrategy.None, "", true)] + [DataRow(VersionMatchStrategy.None, "2.0", true)] + public async Task BusinessOrchestrationsRetainVersionPolicies( + VersionMatchStrategy matchStrategy, string version, bool shouldExecute) + { + foreach (VersionFailureStrategy failureStrategy in Enum.GetValues(typeof(VersionFailureStrategy))) + { + foreach (bool configureExclusion in new[] { false, true }) + { + var settings = new VersioningSettings + { + Version = "1.0", + MatchStrategy = matchStrategy, + FailureStrategy = failureStrategy, + }; + if (configureExclusion) + { + settings.ExcludedOrchestrationNames.Add(InternalName); + } + + await AssertVersionPolicyAsync(settings, "Business.Orchestration", version, shouldExecute); + } + } + } + + [TestMethod] + [DataRow(InternalName, true)] + [DataRow("internal.orchestration", false)] + [DataRow("Internal.Orchestration.Child", false)] + [DataRow("Internal", false)] + [DataRow("Business.Orchestration", false)] + public async Task OnlyExactExcludedUnversionedNamesExecute(string name, bool shouldExecute) + { + foreach (VersionFailureStrategy failureStrategy in Enum.GetValues(typeof(VersionFailureStrategy))) + { + foreach (string version in new[] { null, string.Empty }) + { + var settings = CreateSettings(failureStrategy); + await AssertVersionPolicyAsync(settings, name, version, shouldExecute); + } + } + } + + [TestMethod] + [DataRow(VersionMatchStrategy.Strict, "1.0", true)] + [DataRow(VersionMatchStrategy.Strict, "2.0", false)] + [DataRow(VersionMatchStrategy.Strict, " ", false)] + [DataRow(VersionMatchStrategy.CurrentOrOlder, "0.9", true)] + [DataRow(VersionMatchStrategy.CurrentOrOlder, "2.0", false)] + public async Task ExcludedNamesWithNonemptyVersionsRetainVersionPolicies( + VersionMatchStrategy matchStrategy, string version, bool shouldExecute) + { + foreach (VersionFailureStrategy failureStrategy in Enum.GetValues(typeof(VersionFailureStrategy))) + { + var settings = CreateSettings(failureStrategy); + settings.MatchStrategy = matchStrategy; + await AssertVersionPolicyAsync(settings, InternalName, version, shouldExecute); + } + } + + [TestMethod] + public async Task NullAndDefaultSettingsKeepVersioningDisabled() + { + foreach (VersioningSettings settings in new[] { null, new VersioningSettings() }) + { + await AssertVersionPolicyAsync(settings, InternalName, string.Empty, shouldExecute: true); + await AssertVersionPolicyAsync(settings, "Business.Orchestration", "2.0", shouldExecute: true); + } + } + + [TestMethod] + public async Task ExclusionDoesNotBypassOrchestrationResolution() + { + using var service = new RecordingOrchestrationService(); + var dispatcher = new TestDispatcher( + service, new NameVersionObjectManager(), + new DispatchMiddlewarePipeline(), CreateSettings(VersionFailureStrategy.Fail)); + TaskOrchestrationWorkItem workItem = CreateWorkItem(InternalName, string.Empty); + + await Assert.ThrowsExceptionAsync(() => dispatcher.ProcessAsync(workItem)); + + Assert.AreEqual(0, service.AbandonCount); + Assert.IsNull(service.CompletedState); + Assert.AreEqual(string.Empty, workItem.OrchestrationRuntimeState.Version); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + public async Task ExclusionAppliesToReplayAndContinueAsNewWithoutChangingVersion(string version) + { + using var service = new RecordingOrchestrationService(); + var orchestration = new TimerOrchestration(); + var manager = new NameVersionObjectManager(); + manager.Add(new NameValueObjectCreator(InternalName, version, orchestration)); + var pipeline = new DispatchMiddlewarePipeline(); + int middlewareCalls = 0; + pipeline.Add((context, next) => + { + OrchestrationRuntimeState state = context.GetProperty(); + Assert.AreEqual(InternalName, state.Name); + Assert.AreEqual(version, state.Version); + middlewareCalls++; + return next(); + }); + var dispatcher = new TestDispatcher(service, manager, pipeline, CreateSettings(VersionFailureStrategy.Fail)); + TaskOrchestrationWorkItem workItem = CreateWorkItem(InternalName, version); + + await dispatcher.ProcessAsync(workItem); + for (int generation = 0; generation < 3; generation++) + { + Assert.AreEqual(OrchestrationStatus.Running, service.CompletedState.OrchestrationStatus); + string executionId = service.CompletedState.OrchestrationInstance.ExecutionId; + TaskMessage timerMessage = service.TimerMessages.Single(); + Assert.IsInstanceOfType(timerMessage.Event, typeof(TimerFiredEvent)); + workItem = new TaskOrchestrationWorkItem + { + InstanceId = workItem.InstanceId, + LockedUntilUtc = DateTime.MaxValue, + OrchestrationRuntimeState = new OrchestrationRuntimeState(service.CompletedRuntimeState.Events), + NewMessages = new[] { timerMessage }, + }; + + await dispatcher.ProcessAsync(workItem); + + Assert.AreEqual(0, service.AbandonCount); + Assert.AreEqual(version, service.CompletedState.Version); + if (generation < 2) + { + Assert.AreNotEqual(executionId, service.CompletedState.OrchestrationInstance.ExecutionId); + } + } + + Assert.AreEqual(OrchestrationStatus.Completed, service.CompletedState.OrchestrationStatus); + Assert.AreEqual("2", service.CompletedState.Output); + Assert.AreEqual(6, orchestration.ExecutionCount); + Assert.AreEqual(6, middlewareCalls); + } + + [TestMethod] + [DataRow(VersionFailureStrategy.Reject)] + [DataRow(VersionFailureStrategy.Fail)] + public async Task ContinueAsNewToNonemptyVersionReappliesVersionPolicy(VersionFailureStrategy failureStrategy) + { + using var service = new RecordingOrchestrationService(); + var orchestration = new VersionChangingOrchestration(); + var manager = new NameVersionObjectManager(); + manager.Add(new NameValueObjectCreator(InternalName, string.Empty, orchestration)); + manager.Add(new NameValueObjectCreator(InternalName, "2.0", orchestration)); + var dispatcher = new TestDispatcher(service, manager, new DispatchMiddlewarePipeline(), CreateSettings(failureStrategy)); + TaskOrchestrationWorkItem workItem = CreateWorkItem(InternalName, string.Empty); + + await dispatcher.ProcessAsync(workItem); + + Assert.AreEqual(1, orchestration.ExecutionCount); + Assert.AreEqual("2.0", workItem.OrchestrationRuntimeState.Version); + AssertVersionFailure(service, failureStrategy); + } + + static VersioningSettings CreateSettings(VersionFailureStrategy failureStrategy) + { + var settings = new VersioningSettings + { + Version = "1.0", + MatchStrategy = VersionMatchStrategy.Strict, + FailureStrategy = failureStrategy, + }; + settings.ExcludedOrchestrationNames.Add(InternalName); + return settings; + } + + static async Task AssertVersionPolicyAsync(VersioningSettings settings, string name, string version, bool shouldExecute) + { + using var service = new RecordingOrchestrationService(); + var orchestration = new CompletingOrchestration(); + var manager = new NameVersionObjectManager(); + manager.Add(new NameValueObjectCreator(name, version, orchestration)); + var pipeline = new DispatchMiddlewarePipeline(); + int middlewareCalls = 0; + pipeline.Add((context, next) => + { + Assert.AreSame(orchestration, context.GetProperty()); + Assert.AreEqual(version, context.GetProperty().Version); + middlewareCalls++; + return next(); + }); + var dispatcher = new TestDispatcher(service, manager, pipeline, settings); + TaskOrchestrationWorkItem workItem = CreateWorkItem(name, version); + + await dispatcher.ProcessAsync(workItem); + + Assert.AreEqual(shouldExecute ? 1 : 0, orchestration.ExecutionCount); + Assert.AreEqual(shouldExecute ? 1 : 0, middlewareCalls); + Assert.AreEqual(version, workItem.OrchestrationRuntimeState.Version); + if (shouldExecute) + { + Assert.AreEqual(0, service.AbandonCount); + Assert.AreEqual(OrchestrationStatus.Completed, service.CompletedState.OrchestrationStatus); + Assert.AreEqual("0", service.CompletedState.Output); + } + else + { + AssertVersionFailure(service, settings.FailureStrategy); + } + } + + static void AssertVersionFailure(RecordingOrchestrationService service, VersionFailureStrategy failureStrategy) + { + if (failureStrategy == VersionFailureStrategy.Reject) + { + Assert.AreEqual(1, service.AbandonCount); + Assert.IsNull(service.CompletedState); + } + else + { + Assert.AreEqual(0, service.AbandonCount); + Assert.AreEqual(OrchestrationStatus.Failed, service.CompletedState.OrchestrationStatus); + Assert.AreEqual("VersionMismatch", service.CompletedState.FailureDetails.ErrorType); + } + } + + static TaskOrchestrationWorkItem CreateWorkItem(string name, string version) + { + var instance = new OrchestrationInstance + { + InstanceId = Guid.NewGuid().ToString(), + ExecutionId = Guid.NewGuid().ToString(), + }; + return new TaskOrchestrationWorkItem + { + InstanceId = instance.InstanceId, + LockedUntilUtc = DateTime.MaxValue, + OrchestrationRuntimeState = new OrchestrationRuntimeState(), + NewMessages = new[] + { + new TaskMessage + { + OrchestrationInstance = instance, + Event = new ExecutionStartedEvent(-1, "0") + { + Name = name, + Version = version, + OrchestrationInstance = instance, + }, + }, + }, + }; + } + + sealed class TestDispatcher : TaskOrchestrationDispatcher + { + public TestDispatcher( + IOrchestrationService service, + INameVersionObjectManager manager, + DispatchMiddlewarePipeline pipeline, + VersioningSettings settings) + : base(service, manager, pipeline, new LogHelper(null), ErrorPropagationMode.UseFailureDetails, settings, null) + { + } + + public Task ProcessAsync(TaskOrchestrationWorkItem workItem) => this.OnProcessWorkItemAsync(workItem); + } + + sealed class RecordingOrchestrationService : LocalOrchestrationService, IOrchestrationService + { + public int AbandonCount { get; private set; } + public OrchestrationState CompletedState { get; private set; } + public OrchestrationRuntimeState CompletedRuntimeState { get; private set; } + public IList TimerMessages { get; private set; } + + public new Task AbandonTaskOrchestrationWorkItemAsync(TaskOrchestrationWorkItem workItem) + { + this.AbandonCount++; + return Task.CompletedTask; + } + + public new Task CompleteTaskOrchestrationWorkItemAsync( + TaskOrchestrationWorkItem workItem, OrchestrationRuntimeState newOrchestrationRuntimeState, + IList outboundMessages, IList orchestratorMessages, + IList timerMessages, TaskMessage continuedAsNewMessage, OrchestrationState state) + { + this.CompletedState = state; + this.CompletedRuntimeState = newOrchestrationRuntimeState; + this.TimerMessages = timerMessages; + return Task.CompletedTask; + } + } + + sealed class CompletingOrchestration : TaskOrchestration + { + public int ExecutionCount { get; private set; } + + public override Task RunTask(OrchestrationContext context, int input) + { + this.ExecutionCount++; + return Task.FromResult(input); + } + } + + sealed class TimerOrchestration : TaskOrchestration + { + public int ExecutionCount { get; private set; } + + public override async Task RunTask(OrchestrationContext context, int input) + { + this.ExecutionCount++; + await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(1), input); + if (input < 2) + { + context.ContinueAsNew(input + 1); + } + + return input; + } + } + + sealed class VersionChangingOrchestration : TaskOrchestration + { + public int ExecutionCount { get; private set; } + + public override Task RunTask(OrchestrationContext context, int input) + { + this.ExecutionCount++; + context.ContinueAsNew("2.0", input); + return Task.FromResult(input); + } + } + } +} diff --git a/test/DurableTask.Core.Tests/VersionSettingsTests.cs b/test/DurableTask.Core.Tests/VersionSettingsTests.cs new file mode 100644 index 000000000..42197be3a --- /dev/null +++ b/test/DurableTask.Core.Tests/VersionSettingsTests.cs @@ -0,0 +1,93 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Reflection; +using DurableTask.Core.Settings; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DurableTask.Core.Tests +{ + [TestClass] + public class VersionSettingsTests + { + [TestMethod] + public void ExcludedOrchestrationNamesHasNoPublicSurface() + { + // Only publicly-visible members are returned without BindingFlags.NonPublic. + Assert.IsNull( + typeof(VersioningSettings).GetProperty("ExcludedOrchestrationNames"), + "ExcludedOrchestrationNames must not be a public member of VersioningSettings."); + + PropertyInfo internalProperty = typeof(VersioningSettings).GetProperty( + "ExcludedOrchestrationNames", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull(internalProperty, "ExcludedOrchestrationNames must remain accessible internally."); + Assert.IsTrue(internalProperty.GetMethod.IsAssembly, "The getter must be internal, not public or private."); + Assert.IsNull(internalProperty.SetMethod, "The property must remain getter-only."); + } + + [TestMethod] + public void ExcludedOrchestrationNamesAreEmptyAndInstanceScopedByDefault() + { + var settings = new VersioningSettings(); + var otherSettings = new VersioningSettings(); + + Assert.AreEqual(0, settings.ExcludedOrchestrationNames.Count); + settings.ExcludedOrchestrationNames.Add("Internal.Orchestration"); + + Assert.AreEqual(0, otherSettings.ExcludedOrchestrationNames.Count); + Assert.AreEqual(string.Empty, settings.Version); + Assert.AreEqual(VersioningSettings.VersionMatchStrategy.None, settings.MatchStrategy); + Assert.AreEqual(VersioningSettings.VersionFailureStrategy.Reject, settings.FailureStrategy); + } + + [TestMethod] + public void ExcludedOrchestrationNamesUseExactOrdinalMatching() + { + var settings = new VersioningSettings(); + Assert.IsTrue(settings.ExcludedOrchestrationNames.Add("Internal.Orchestration")); + Assert.IsFalse(settings.ExcludedOrchestrationNames.Add("Internal.Orchestration")); + + Assert.IsTrue(settings.ExcludedOrchestrationNames.Contains("Internal.Orchestration")); + Assert.IsFalse(settings.ExcludedOrchestrationNames.Contains("internal.orchestration")); + Assert.IsFalse(settings.ExcludedOrchestrationNames.Contains("Internal.Orchestration.Child")); + } + + [TestMethod] + [DataRow("1.0.0", "1.0.0", 0)] + [DataRow("1.1.0", "1.0.0", 1)] + [DataRow("1.0.0", "1.1.0", -1)] + [DataRow("1", "1", 0)] + [DataRow("2", "1", 1)] + [DataRow("1", "2", -1)] + [DataRow("", "1", -1)] + [DataRow("1", "", 1)] + [DataRow("", "", 0)] + public void TestVersionComparison(string orchVersion, string settingVersion, int expectedComparison) + { + int result = VersioningSettings.CompareVersions(orchVersion, settingVersion); + + if (expectedComparison == 0) + { + Assert.AreEqual(0, result, $"Expected {orchVersion} to be equal to {settingVersion}"); + } + else if (expectedComparison < 0) + { + Assert.IsTrue(result < 0, $"Expected {orchVersion} to be less than {settingVersion}"); + } + else + { + Assert.IsTrue(result > 0, $"Expected {orchVersion} to be greater than {settingVersion}"); + } + } + } +} \ No newline at end of file