diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 728c890b..be87ef46 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -15,7 +14,7 @@ namespace Microsoft.Tye { public static class ApplicationFactory { - public static async Task CreateAsync(OutputContext output, FileInfo source) + public static async Task CreateAsync(OutputContext output, FileInfo source, ApplicationFactoryFilter? filter = null) { if (source is null) { @@ -71,7 +70,11 @@ namespace Microsoft.Tye root.Extensions.Add(extension); } - foreach (var configService in config.Services) + var services = filter?.ServicesFilter != null ? + config.Services.Where(filter.ServicesFilter).ToList() : + config.Services; + + foreach (var configService in services) { ServiceBuilder service; if (root.Services.Any(s => s.Name == configService.Name)) @@ -320,7 +323,11 @@ namespace Microsoft.Tye } } - foreach (var configIngress in config.Ingress) + var ingresses = filter?.IngressFilter != null ? + config.Ingress.Where(filter.IngressFilter).ToList() : + config.Ingress; + + foreach (var configIngress in ingresses) { var ingress = new IngressBuilder(configIngress.Name!); ingress.Replicas = configIngress.Replicas ?? 1; diff --git a/src/Microsoft.Tye.Core/ApplicationFactoryFilter.cs b/src/Microsoft.Tye.Core/ApplicationFactoryFilter.cs new file mode 100644 index 00000000..134b5e4e --- /dev/null +++ b/src/Microsoft.Tye.Core/ApplicationFactoryFilter.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using Microsoft.Tye.ConfigModel; + +namespace Microsoft.Tye +{ + public class ApplicationFactoryFilter + { + public Func? ServicesFilter { get; set; } + public Func? IngressFilter { get; set; } + + public static ApplicationFactoryFilter? GetApplicationFactoryFilter(string[] tags) + { + ApplicationFactoryFilter? filter = null; + + if (tags != null && tags.Any()) + { + filter = new ApplicationFactoryFilter + { + ServicesFilter = service => tags.Any(b => service.Tags.Contains(b)), + IngressFilter = service => tags.Any(b => service.Tags.Contains(b)) + }; + } + + return filter; + } + } +} diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs index 23429427..1fdbcf86 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs @@ -14,5 +14,6 @@ namespace Microsoft.Tye.ConfigModel public int? Replicas { get; set; } public List Rules { get; set; } = new List(); public List Bindings { get; set; } = new List(); + public List Tags { get; set; } = new List(); } } diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs index 86aef3d6..5f9a84e0 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Tye; using YamlDotNet.Serialization; namespace Microsoft.Tye.ConfigModel @@ -37,6 +36,7 @@ namespace Microsoft.Tye.ConfigModel [YamlMember(Alias = "env")] public List Configuration { get; set; } = new List(); public List BuildProperties { get; set; } = new List(); + public List Tags { get; set; } = new List(); public ConfigProbe? Liveness { get; set; } public ConfigProbe? Readiness { get; set; } } diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs index da40d87d..1629c3ff 100644 --- a/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/ConfigIngressParser.cs @@ -59,6 +59,14 @@ namespace Tye.Serialization } HandleIngressBindings((child.Value as YamlSequenceNode)!, configIngress.Bindings); break; + case "tags": + if (child.Value.NodeType != YamlNodeType.Sequence) + { + throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key)); + } + + HandleIngressTags((child.Value as YamlSequenceNode)!, configIngress.Tags); + break; default: throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key)); } @@ -137,5 +145,14 @@ namespace Tye.Serialization } } } + + private static void HandleIngressTags(YamlSequenceNode yamlSequenceNode, List tags) + { + foreach (var child in yamlSequenceNode!.Children) + { + var tag = YamlParser.GetScalarValue(child); + tags.Add(tag); + } + } } } diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs index 52b04ef8..7c126188 100644 --- a/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/ConfigServiceParser.cs @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Linq; -using Microsoft.Build.Evaluation; using Microsoft.Tye.ConfigModel; using YamlDotNet.RepresentationModel; @@ -131,6 +129,14 @@ namespace Tye.Serialization service.Readiness = new ConfigProbe(); HandleServiceProbe((YamlMappingNode)child.Value, service.Readiness!); break; + case "tags": + if (child.Value.NodeType != YamlNodeType.Sequence) + { + throw new TyeYamlException(child.Value.Start, CoreStrings.FormatExpectedYamlSequence(key)); + } + + HandleServiceTags((child.Value as YamlSequenceNode)!, service.Tags); + break; default: throw new TyeYamlException(child.Key.Start, CoreStrings.FormatUnrecognizedKey(key)); } @@ -444,5 +450,14 @@ namespace Tye.Serialization } } } + + private static void HandleServiceTags(YamlSequenceNode yamlSequenceNode, List tags) + { + foreach (var child in yamlSequenceNode!.Children) + { + var tag = YamlParser.GetScalarValue(child); + tags.Add(tag); + } + } } } diff --git a/src/Microsoft.Tye.Core/Serialization/YamlParser.cs b/src/Microsoft.Tye.Core/Serialization/YamlParser.cs index 089f8e14..e5754b74 100644 --- a/src/Microsoft.Tye.Core/Serialization/YamlParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/YamlParser.cs @@ -62,12 +62,14 @@ namespace Tye.Serialization service.Bindings ??= new List(); service.Configuration ??= new List(); service.Volumes ??= new List(); + service.Tags ??= new List(); } foreach (var ingress in app.Ingress) { ingress.Bindings ??= new List(); ingress.Rules ??= new List(); + ingress.Tags ??= new List(); } return app; diff --git a/src/Microsoft.Tye.Core/StandardOptions.cs b/src/Microsoft.Tye.Core/StandardOptions.cs index 48dd8eb9..b59a00b3 100644 --- a/src/Microsoft.Tye.Core/StandardOptions.cs +++ b/src/Microsoft.Tye.Core/StandardOptions.cs @@ -30,6 +30,22 @@ namespace Microsoft.Tye } } + public static Option Tags + { + get + { + return new Option("--tags", "--filter") + { + Argument = new Argument>("tags") + { + Arity = ArgumentArity.OneOrMore + }, + Description = "Filter the group of running services by tag.", + Required = false + }; + } + } + public static Option Force { get diff --git a/src/tye/BuildHost.cs b/src/tye/BuildHost.cs index 1d0a67d1..3e46ca00 100644 --- a/src/tye/BuildHost.cs +++ b/src/tye/BuildHost.cs @@ -11,10 +11,11 @@ namespace Microsoft.Tye { public static class BuildHost { - public static async Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive) + public static async Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string[] tags) { var output = new OutputContext(console, verbosity); - var application = await ApplicationFactory.CreateAsync(output, path); + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); + var application = await ApplicationFactory.CreateAsync(output, path, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); diff --git a/src/tye/GenerateHost.cs b/src/tye/GenerateHost.cs index a336e82a..1ff0cec5 100644 --- a/src/tye/GenerateHost.cs +++ b/src/tye/GenerateHost.cs @@ -13,12 +13,13 @@ namespace Microsoft.Tye { public static class GenerateHost { - public static async Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string ns) + public static async Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive, string ns, string[] tags) { var output = new OutputContext(console, verbosity); output.WriteInfoLine("Loading Application Details..."); - var application = await ApplicationFactory.CreateAsync(output, path); + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); + var application = await ApplicationFactory.CreateAsync(output, path, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); diff --git a/src/tye/Program.BuildCommand.cs b/src/tye/Program.BuildCommand.cs index 0af67d4f..eb436aba 100644 --- a/src/tye/Program.BuildCommand.cs +++ b/src/tye/Program.BuildCommand.cs @@ -21,9 +21,10 @@ namespace Microsoft.Tye CommonArguments.Path_Required, StandardOptions.Interactive, StandardOptions.Verbosity, + StandardOptions.Tags }; - command.Handler = CommandHandler.Create((console, path, verbosity, interactive) => + command.Handler = CommandHandler.Create((console, path, verbosity, interactive, tags) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) @@ -31,7 +32,7 @@ namespace Microsoft.Tye throw new CommandException("No project or solution file was found."); } - return BuildHost.BuildAsync(console, path, verbosity, interactive); + return BuildHost.BuildAsync(console, path, verbosity, interactive, tags); }); return command; diff --git a/src/tye/Program.DeployCommand.cs b/src/tye/Program.DeployCommand.cs index a525a123..f2f1fbaf 100644 --- a/src/tye/Program.DeployCommand.cs +++ b/src/tye/Program.DeployCommand.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Tye.ConfigModel; @@ -23,6 +24,7 @@ namespace Microsoft.Tye StandardOptions.Interactive, StandardOptions.Verbosity, StandardOptions.Namespace, + StandardOptions.Tags, }; command.AddOption(new Option(new[] { "-f", "--force" }) @@ -31,7 +33,7 @@ namespace Microsoft.Tye Required = false }); - command.Handler = CommandHandler.Create(async (console, path, verbosity, interactive, force, @namespace) => + command.Handler = CommandHandler.Create(async (console, path, verbosity, interactive, force, @namespace, tags) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) @@ -42,12 +44,15 @@ namespace Microsoft.Tye var output = new OutputContext(console, verbosity); output.WriteInfoLine("Loading Application Details..."); - var application = await ApplicationFactory.CreateAsync(output, path); + + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); + + var application = await ApplicationFactory.CreateAsync(output, path, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } - if (!String.IsNullOrEmpty(@namespace)) + if (!string.IsNullOrEmpty(@namespace)) { application.Namespace = @namespace; } diff --git a/src/tye/Program.GenerateCommand.cs b/src/tye/Program.GenerateCommand.cs index a6764712..7f5e1ef2 100644 --- a/src/tye/Program.GenerateCommand.cs +++ b/src/tye/Program.GenerateCommand.cs @@ -22,13 +22,14 @@ namespace Microsoft.Tye StandardOptions.Interactive, StandardOptions.Verbosity, StandardOptions.Namespace, + StandardOptions.Tags }; // This is a super-secret VIP-only command! It's useful for testing, but we're // not documenting it right now. command.IsHidden = true; - command.Handler = CommandHandler.Create((console, path, verbosity, interactive, @namespace) => + command.Handler = CommandHandler.Create((console, path, verbosity, interactive, @namespace, tags) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) @@ -36,7 +37,7 @@ namespace Microsoft.Tye throw new CommandException("No project or solution file was found."); } - return GenerateHost.GenerateAsync(console, path, verbosity, interactive, @namespace); + return GenerateHost.GenerateAsync(console, path, verbosity, interactive, @namespace, tags); }); return command; diff --git a/src/tye/Program.PushCommand.cs b/src/tye/Program.PushCommand.cs index f17507a8..0209c93d 100644 --- a/src/tye/Program.PushCommand.cs +++ b/src/tye/Program.PushCommand.cs @@ -18,6 +18,7 @@ namespace Microsoft.Tye CommonArguments.Path_Required, StandardOptions.Interactive, StandardOptions.Verbosity, + StandardOptions.Tags }; command.AddOption(new Option(new[] { "-f", "--force" }) @@ -26,7 +27,7 @@ namespace Microsoft.Tye Required = false }); - command.Handler = CommandHandler.Create(async (console, path, verbosity, interactive, force) => + command.Handler = CommandHandler.Create(async (console, path, verbosity, interactive, force, tags) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) @@ -37,7 +38,9 @@ namespace Microsoft.Tye var output = new OutputContext(console, verbosity); output.WriteInfoLine("Loading Application Details..."); - var application = await ApplicationFactory.CreateAsync(output, path); + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); + + var application = await ApplicationFactory.CreateAsync(output, path, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index 1eb75455..917f2903 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -3,15 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; -using System.CommandLine.IO; using System.IO; +using System.Linq; using System.Threading; -using Microsoft.Tye.ConfigModel; -using Microsoft.Tye.Extensions; using Microsoft.Tye.Hosting; -using Microsoft.Tye.Hosting.Diagnostics; namespace Microsoft.Tye { @@ -76,8 +74,7 @@ namespace Microsoft.Tye Description = "Watches for code changes for all dotnet projects.", Required = false }, - - + StandardOptions.Tags, StandardOptions.Verbosity, }; @@ -92,7 +89,10 @@ namespace Microsoft.Tye var output = new OutputContext(args.Console, args.Verbosity); output.WriteInfoLine("Loading Application Details..."); - var application = await ApplicationFactory.CreateAsync(output, args.Path); + + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(args.Tags); + + var application = await ApplicationFactory.CreateAsync(output, args.Path, filter); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); @@ -170,6 +170,8 @@ namespace Microsoft.Tye public Verbosity Verbosity { get; set; } public bool Watch { get; set; } + + public string[] Tags { get; set; } = Array.Empty(); } } } diff --git a/src/tye/Program.UndeployCommand.cs b/src/tye/Program.UndeployCommand.cs index 0f02a6dc..8dc3ccb9 100644 --- a/src/tye/Program.UndeployCommand.cs +++ b/src/tye/Program.UndeployCommand.cs @@ -22,6 +22,7 @@ namespace Microsoft.Tye StandardOptions.Namespace, StandardOptions.Interactive, StandardOptions.Verbosity, + StandardOptions.Tags, new Option(new[]{ "--what-if", }, "print what would be deleted without making changes") { @@ -29,7 +30,7 @@ namespace Microsoft.Tye }, }; - command.Handler = CommandHandler.Create((console, path, verbosity, @namespace, interactive, whatIf) => + command.Handler = CommandHandler.Create((console, path, verbosity, @namespace, interactive, whatIf, tags) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) @@ -37,7 +38,7 @@ namespace Microsoft.Tye throw new CommandException("No project or solution file was found."); } - return UndeployHost.UndeployAsync(console, path, verbosity, @namespace, interactive, whatIf); + return UndeployHost.UndeployAsync(console, path, verbosity, @namespace, interactive, whatIf, tags); }); return command; diff --git a/src/tye/UndeployHost.cs b/src/tye/UndeployHost.cs index bcf32be6..9dc32c7f 100644 --- a/src/tye/UndeployHost.cs +++ b/src/tye/UndeployHost.cs @@ -17,7 +17,7 @@ namespace Microsoft.Tye { public static class UndeployHost { - public static async Task UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, string @namespace, bool interactive, bool whatIf) + public static async Task UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, string @namespace, bool interactive, bool whatIf, string[] tags) { var watch = System.Diagnostics.Stopwatch.StartNew(); @@ -25,8 +25,8 @@ namespace Microsoft.Tye output.WriteInfoLine("Loading Application Details..."); - // We don't need to know anything about the services, just the application name. - var application = ConfigFactory.FromFile(path); + var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); + var application = await ApplicationFactory.CreateAsync(output, path, filter); if (!string.IsNullOrEmpty(@namespace)) { application.Namespace = @namespace; @@ -41,7 +41,7 @@ namespace Microsoft.Tye output.WriteAlwaysLine($"Time Elapsed: {elapsedTime.Hours:00}:{elapsedTime.Minutes:00}:{elapsedTime.Seconds:00}:{elapsedTime.Milliseconds / 10:00}"); } - public static async Task ExecuteUndeployAsync(OutputContext output, ConfigApplication application, string @namespace, bool interactive, bool whatIf) + public static async Task ExecuteUndeployAsync(OutputContext output, ApplicationBuilder application, string @namespace, bool interactive, bool whatIf) { var config = KubernetesClientConfiguration.BuildDefaultConfig(); diff --git a/test/Test.Infrastructure/TyeAssert.cs b/test/Test.Infrastructure/TyeAssert.cs index 36c4644b..00e7f689 100644 --- a/test/Test.Infrastructure/TyeAssert.cs +++ b/test/Test.Infrastructure/TyeAssert.cs @@ -44,6 +44,7 @@ namespace Test.Infrastructure Assert.NotNull(otherBinding); } + Assert.Equal(otherIngress.Tags, ingress.Tags); } foreach (var service in actual.Services) @@ -61,20 +62,7 @@ namespace Test.Infrastructure Assert.Equal(otherService.Project, service.Project); Assert.Equal(otherService.Replicas, service.Replicas); Assert.Equal(otherService.WorkingDirectory, service.WorkingDirectory); - - foreach (var binding in service.Bindings) - { - var otherBinding = otherService.Bindings - .Where(o => o.Name == binding.Name - && o.Port == binding.Port - && o.Protocol == binding.Protocol - && o.ConnectionString == binding.ConnectionString - && o.ContainerPort == binding.ContainerPort - && o.Host == binding.Host) - .Single(); - - Assert.NotNull(otherBinding); - } + Assert.Equal(otherService.Tags, service.Tags); foreach (var binding in service.Bindings) { diff --git a/test/UnitTests/TyeDeserializationTests.cs b/test/UnitTests/TyeDeserializationTests.cs index a1c9dd8c..82c9c346 100644 --- a/test/UnitTests/TyeDeserializationTests.cs +++ b/test/UnitTests/TyeDeserializationTests.cs @@ -46,6 +46,9 @@ ingress: - host: b.example.com service: appB replicas: 2 + tags: + - tagA + - tagC services: - name: appA project: ApplicationA/ApplicationA.csproj @@ -53,6 +56,9 @@ services: - name: Configuration - value: Debug replicas: 2 + tags: + - tagA + - tagC external: false image: abc build: false @@ -66,8 +72,8 @@ services: value: ""test2"" volumes: - name: volume - source: /data - target: /data + source: /data + target: /data bindings: - name: test port: 4444 @@ -77,7 +83,10 @@ services: protocol: http - name: appB project: ApplicationB/ApplicationB.csproj - replicas: 2"; + replicas: 2 + tags: + - tagB + - tagD"; using var parser = new YamlParser(input); var app = parser.ParseConfigApplication(); @@ -115,10 +124,14 @@ ingress: [Fact] public void ServicesSetCorrectly() { - var input = @"services: + var input = @" +services: - name: appA project: ApplicationA/ApplicationA.csproj replicas: 2 + tags: + - A + - B external: false image: abc build: false @@ -132,8 +145,8 @@ ingress: value: ""test2"" volumes: - name: volume - source: /data - target: /data + source: /data + target: /data bindings: - name: test port: 4444 @@ -143,7 +156,10 @@ ingress: protocol: http - name: appB project: ApplicationB/ApplicationB.csproj - replicas: 2"; + replicas: 2 + tags: + - tC + - tD"; using var parser = new YamlParser(input); var actual = parser.ParseConfigApplication(); @@ -436,6 +452,38 @@ services: Assert.Contains(CoreStrings.FormatUnrecognizedKey("abc"), exception.Message); } + + [Fact] + public void Ingress_Tags_MustBeSequence() + { + using var parser = new YamlParser( +@"ingress: + - name: ingress + tags: abc"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatExpectedYamlSequence("tags"), exception.Message); + } + + [Fact] + public void Ingress_Tags_SetCorrectly() + { + var input = @" +ingress: + - name: ingress + tags: + - tagA + - with space + - ""C.X"" +"; + using var parser = new YamlParser(input); + var actual = parser.ParseConfigApplication(); + + var expected = _deserializer.Deserialize(new StringReader(input)); + + TyeAssert.Equal(expected, actual); + } + [Fact] public void Services_External_MustBeBool() { @@ -497,16 +545,47 @@ services: Assert.Contains(CoreStrings.FormatExpectedYamlSequence("env"), exception.Message); } + [Fact] + public void Services_Tags_MustBeSequence() + { + using var parser = new YamlParser( +@"services: + - name: ingress + tags: abc"); + + var exception = Assert.Throws(() => parser.ParseConfigApplication()); + Assert.Contains(CoreStrings.FormatExpectedYamlSequence("tags"), exception.Message); + } + + [Fact] + public void Services_Tags_SetCorrectly() + { + var input = @" +services: + - name: ingress + tags: + - tagA + - with space + - ""C.X"" +"; + using var parser = new YamlParser(input); + var actual = parser.ParseConfigApplication(); + + var expected = _deserializer.Deserialize(new StringReader(input)); + + TyeAssert.Equal(expected, actual); + } + [Fact] public void Services_UnrecognizedKey() { using var parser = new YamlParser( @"services: - name: ingress - env: abc"); + xyz: abc"); var exception = Assert.Throws(() => parser.ParseConfigApplication()); - Assert.Contains(CoreStrings.FormatExpectedYamlSequence("env"), exception.Message); + Assert.Contains(CoreStrings.FormatUnrecognizedKey("xyz"), exception.Message); } [Theory]