From 95d3184f425435ad4ed709a6d09fbb3826049bff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 05:51:17 +0000 Subject: [PATCH 1/4] Initial plan From b365ed6ecb8c8af04704f9bd48d398bc1c386443 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 06:04:11 +0000 Subject: [PATCH 2/4] Fix UpdateConfig property types and add comprehensive tests Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../ServiceNodes/Swarm/UpdateConfig.cs | 7 +- .../UpdateConfigTests.cs | 210 ++++++++++++++++++ 2 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs diff --git a/src/Aspire.Hosting.Docker/Resources/ServiceNodes/Swarm/UpdateConfig.cs b/src/Aspire.Hosting.Docker/Resources/ServiceNodes/Swarm/UpdateConfig.cs index fac70abfe6c..1701e7776e6 100644 --- a/src/Aspire.Hosting.Docker/Resources/ServiceNodes/Swarm/UpdateConfig.cs +++ b/src/Aspire.Hosting.Docker/Resources/ServiceNodes/Swarm/UpdateConfig.cs @@ -21,7 +21,7 @@ public sealed class UpdateConfig /// be updated simultaneously. /// [YamlMember(Alias = "parallelism")] - public string? Parallelism { get; set; } + public int? Parallelism { get; set; } /// /// Represents the delay between each update operation for a service node in a swarm configuration. @@ -30,10 +30,11 @@ public sealed class UpdateConfig public string? Delay { get; set; } /// - /// Indicates whether the update process should stop and fail upon encountering an error. + /// Gets or sets the action to take when an update fails. + /// Valid values are "continue", "rollback", or "pause" (default). /// [YamlMember(Alias = "failure_action")] - public bool? FailOnError { get; set; } + public string? FailureAction { get; set; } /// /// Gets or sets the duration or interval for monitoring the progress of an update. diff --git a/tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs b/tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs new file mode 100644 index 00000000000..edba6d8821f --- /dev/null +++ b/tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.Docker.Resources; +using Aspire.Hosting.Docker.Resources.ComposeNodes; +using Aspire.Hosting.Docker.Resources.ServiceNodes.Swarm; + +namespace Aspire.Hosting.Docker.Tests; + +public class UpdateConfigTests +{ + [Fact] + public void UpdateConfig_SerializesParallelismAsInteger() + { + // Arrange + var updateConfig = new UpdateConfig + { + Parallelism = 2 + }; + + var composeFile = new ComposeFile + { + Services = new Dictionary + { + ["test-service"] = new Service + { + Name = "test-service", + Image = "nginx", + Deploy = new Deploy + { + UpdateConfig = updateConfig + } + } + } + }; + + // Act + var yaml = composeFile.ToYaml(); + + // Print for debugging + Console.WriteLine("Generated YAML:"); + Console.WriteLine(yaml); + + // Assert - should serialize as integer (correct) + Assert.Contains("parallelism: 2", yaml); + Assert.DoesNotContain("parallelism: \"2\"", yaml); + } + + [Fact] + public void UpdateConfig_SerializesFailureActionAsString() + { + // Arrange + var updateConfig = new UpdateConfig + { + FailureAction = "rollback" + }; + + var composeFile = new ComposeFile + { + Services = new Dictionary + { + ["test-service"] = new Service + { + Name = "test-service", + Image = "nginx", + Deploy = new Deploy + { + UpdateConfig = updateConfig + } + } + } + }; + + // Act + var yaml = composeFile.ToYaml(); + + // Assert - should serialize as string + Assert.Contains("failure_action: rollback", yaml); + } + + [Theory] + [InlineData("continue")] + [InlineData("rollback")] + [InlineData("pause")] + public void UpdateConfig_AcceptsValidFailureActionValues(string failureAction) + { + // Arrange & Act + var updateConfig = new UpdateConfig + { + FailureAction = failureAction + }; + + // Assert - no exception should be thrown + Assert.Equal(failureAction, updateConfig.FailureAction); + } + + [Fact] + public void UpdateConfig_SerializesCompleteConfiguration() + { + // Arrange + var updateConfig = new UpdateConfig + { + Parallelism = 1, + Delay = "10s", + Monitor = "60s", + Order = "start-first", + FailureAction = "pause", + MaxFailureRatio = "0.1" + }; + + var composeFile = new ComposeFile + { + Services = new Dictionary + { + ["web"] = new Service + { + Name = "web", + Image = "nginx", + Deploy = new Deploy + { + UpdateConfig = updateConfig + } + } + } + }; + + // Act + var yaml = composeFile.ToYaml(); + + // Assert + Assert.Contains("parallelism: 1", yaml); + Assert.Contains("delay: 10s", yaml); + Assert.Contains("monitor: 60s", yaml); + Assert.Contains("order: start-first", yaml); + Assert.Contains("failure_action: pause", yaml); + Assert.Contains("max_failure_ratio: 0.1", yaml); + } + + [Fact] + public void UpdateConfig_ParallelismNullValueOmittedFromYaml() + { + // Arrange + var updateConfig = new UpdateConfig + { + Parallelism = null, // Should be omitted from YAML + Delay = "10s" + }; + + var composeFile = new ComposeFile + { + Services = new Dictionary + { + ["test-service"] = new Service + { + Name = "test-service", + Image = "nginx", + Deploy = new Deploy + { + UpdateConfig = updateConfig + } + } + } + }; + + // Act + var yaml = composeFile.ToYaml(); + + // Print for debugging + Console.WriteLine("Generated YAML:"); + Console.WriteLine(yaml); + + // Assert - null values should be omitted + Assert.DoesNotContain("parallelism:", yaml); + Assert.Contains("delay: 10s", yaml); + } + + [Fact] + public void UpdateConfig_FailureActionNullValueOmittedFromYaml() + { + // Arrange + var updateConfig = new UpdateConfig + { + FailureAction = null, // Should be omitted from YAML + Delay = "10s" + }; + + var composeFile = new ComposeFile + { + Services = new Dictionary + { + ["test-service"] = new Service + { + Name = "test-service", + Image = "nginx", + Deploy = new Deploy + { + UpdateConfig = updateConfig + } + } + } + }; + + // Act + var yaml = composeFile.ToYaml(); + + // Assert - null values should be omitted + Assert.DoesNotContain("failure_action:", yaml); + Assert.Contains("delay: 10s", yaml); + } +} \ No newline at end of file From 36ab85e0af8770f01b153e7d3eb23f9b962caf3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 06:10:28 +0000 Subject: [PATCH 3/4] Complete UpdateConfig serialization fix with API updates and validation Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../api/Aspire.Hosting.Docker.cs | 4 +- .../UpdateConfigTests.cs | 128 +----------------- 2 files changed, 9 insertions(+), 123 deletions(-) diff --git a/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs b/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs index 489b3a399b8..5175d706221 100644 --- a/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs +++ b/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs @@ -584,7 +584,7 @@ public sealed partial class UpdateConfig public string? Delay { get { throw null; } set { } } [YamlDotNet.Serialization.YamlMember(Alias = "failure_action")] - public bool? FailOnError { get { throw null; } set { } } + public string? FailureAction { get { throw null; } set { } } [YamlDotNet.Serialization.YamlMember(Alias = "max_failure_ratio")] public string? MaxFailureRatio { get { throw null; } set { } } @@ -596,6 +596,6 @@ public sealed partial class UpdateConfig public string? Order { get { throw null; } set { } } [YamlDotNet.Serialization.YamlMember(Alias = "parallelism")] - public string? Parallelism { get { throw null; } set { } } + public int? Parallelism { get { throw null; } set { } } } } \ No newline at end of file diff --git a/tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs b/tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs index edba6d8821f..7235ba71628 100644 --- a/tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs +++ b/tests/Aspire.Hosting.Docker.Tests/UpdateConfigTests.cs @@ -37,47 +37,11 @@ public void UpdateConfig_SerializesParallelismAsInteger() // Act var yaml = composeFile.ToYaml(); - // Print for debugging - Console.WriteLine("Generated YAML:"); - Console.WriteLine(yaml); - // Assert - should serialize as integer (correct) Assert.Contains("parallelism: 2", yaml); Assert.DoesNotContain("parallelism: \"2\"", yaml); } - [Fact] - public void UpdateConfig_SerializesFailureActionAsString() - { - // Arrange - var updateConfig = new UpdateConfig - { - FailureAction = "rollback" - }; - - var composeFile = new ComposeFile - { - Services = new Dictionary - { - ["test-service"] = new Service - { - Name = "test-service", - Image = "nginx", - Deploy = new Deploy - { - UpdateConfig = updateConfig - } - } - } - }; - - // Act - var yaml = composeFile.ToYaml(); - - // Assert - should serialize as string - Assert.Contains("failure_action: rollback", yaml); - } - [Theory] [InlineData("continue")] [InlineData("rollback")] @@ -95,93 +59,14 @@ public void UpdateConfig_AcceptsValidFailureActionValues(string failureAction) } [Fact] - public void UpdateConfig_SerializesCompleteConfiguration() + public void UpdateConfig_NullValuesOmittedFromYaml() { - // Arrange - var updateConfig = new UpdateConfig - { - Parallelism = 1, - Delay = "10s", - Monitor = "60s", - Order = "start-first", - FailureAction = "pause", - MaxFailureRatio = "0.1" - }; - - var composeFile = new ComposeFile - { - Services = new Dictionary - { - ["web"] = new Service - { - Name = "web", - Image = "nginx", - Deploy = new Deploy - { - UpdateConfig = updateConfig - } - } - } - }; - - // Act - var yaml = composeFile.ToYaml(); - - // Assert - Assert.Contains("parallelism: 1", yaml); - Assert.Contains("delay: 10s", yaml); - Assert.Contains("monitor: 60s", yaml); - Assert.Contains("order: start-first", yaml); - Assert.Contains("failure_action: pause", yaml); - Assert.Contains("max_failure_ratio: 0.1", yaml); - } - - [Fact] - public void UpdateConfig_ParallelismNullValueOmittedFromYaml() - { - // Arrange - var updateConfig = new UpdateConfig - { - Parallelism = null, // Should be omitted from YAML - Delay = "10s" - }; - - var composeFile = new ComposeFile - { - Services = new Dictionary - { - ["test-service"] = new Service - { - Name = "test-service", - Image = "nginx", - Deploy = new Deploy - { - UpdateConfig = updateConfig - } - } - } - }; - - // Act - var yaml = composeFile.ToYaml(); - - // Print for debugging - Console.WriteLine("Generated YAML:"); - Console.WriteLine(yaml); - - // Assert - null values should be omitted - Assert.DoesNotContain("parallelism:", yaml); - Assert.Contains("delay: 10s", yaml); - } - - [Fact] - public void UpdateConfig_FailureActionNullValueOmittedFromYaml() - { - // Arrange + // Arrange - Only set some properties var updateConfig = new UpdateConfig { - FailureAction = null, // Should be omitted from YAML - Delay = "10s" + Parallelism = 3, // Set to verify not all are null + FailureAction = null, // Should be omitted + Delay = null // Should be omitted }; var composeFile = new ComposeFile @@ -204,7 +89,8 @@ public void UpdateConfig_FailureActionNullValueOmittedFromYaml() var yaml = composeFile.ToYaml(); // Assert - null values should be omitted + Assert.Contains("parallelism: 3", yaml); Assert.DoesNotContain("failure_action:", yaml); - Assert.Contains("delay: 10s", yaml); + Assert.DoesNotContain("delay:", yaml); } } \ No newline at end of file From 103b2c31ae53ffe6c73350a76bd83ee67847b688 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Sep 2025 07:37:47 +0000 Subject: [PATCH 4/4] Revert API file changes as requested while keeping implementation fixes Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs b/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs index 5175d706221..489b3a399b8 100644 --- a/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs +++ b/src/Aspire.Hosting.Docker/api/Aspire.Hosting.Docker.cs @@ -584,7 +584,7 @@ public sealed partial class UpdateConfig public string? Delay { get { throw null; } set { } } [YamlDotNet.Serialization.YamlMember(Alias = "failure_action")] - public string? FailureAction { get { throw null; } set { } } + public bool? FailOnError { get { throw null; } set { } } [YamlDotNet.Serialization.YamlMember(Alias = "max_failure_ratio")] public string? MaxFailureRatio { get { throw null; } set { } } @@ -596,6 +596,6 @@ public sealed partial class UpdateConfig public string? Order { get { throw null; } set { } } [YamlDotNet.Serialization.YamlMember(Alias = "parallelism")] - public int? Parallelism { get { throw null; } set { } } + public string? Parallelism { get { throw null; } set { } } } } \ No newline at end of file