From 62867c6e28548157ab8c60318591d3e26838dda8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:26:24 +0100 Subject: [PATCH 01/12] Wire Nullean.Snapshots into every test project The ApiExplorer pilot (#4654) used Nullean.Snapshots.Html and .TUnit 0.2.0 in one project. This makes the library available to all unit and integration test projects at 0.4.0 and adds the shared pieces the next migration steps build on: - tests-shared/Elastic.Documentation.Testing: DocsHtml (page section without the fixture H1 and htmx attributes), DiagnosticsSnapshot, NavigationDump, MockFileSystemDump. A scrubber for temp and repo paths is compiled into each test project. - The snap CLI as a local tool; CI uploads received snapshots when a test run fails; the build fails on orphaned snapshots after a unit run. - AGENTS.md describes the workflow. The PR review prompt no longer claims the repo uses xUnit. The five TestDiagnosticsCollector copies stay: they differ in lifecycle (Write vs HandleItem), so merging them is a behaviour change. DiagnosticsSnapshot works on any of them. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 9 +++ .github/workflows/pr-review.yml | 2 +- AGENTS.md | 6 ++ Directory.Packages.props | 8 ++- build/Targets.fs | 6 ++ docs-builder.slnx | 4 ++ dotnet-tools.json | 7 ++ tests-integration/Directory.Build.props | 7 ++ tests-shared/Directory.Build.props | 11 +++ .../DiagnosticsSnapshot.cs | 32 +++++++++ .../Elastic.Documentation.Testing/DocsHtml.cs | 34 +++++++++ .../Elastic.Documentation.Testing.csproj | 28 ++++++++ .../Linked/SnapshotScrubbing.cs | 39 +++++++++++ .../MockFileSystemDump.cs | 35 ++++++++++ .../NavigationDump.cs | 51 ++++++++++++++ tests/Directory.Build.props | 7 ++ .../Elastic.ApiExplorer.Tests.csproj | 2 - .../Testing/SnapshotHelpersTests.cs | 70 +++++++++++++++++++ 18 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 tests-shared/Directory.Build.props create mode 100644 tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/DocsHtml.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj create mode 100644 tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs create mode 100644 tests-shared/Elastic.Documentation.Testing/NavigationDump.cs create mode 100644 tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50c34ac2c2..7d4066e86c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -203,6 +203,15 @@ jobs: # --single-target skips the compile dependency; the Compile step above already built everything run: dotnet run --project build -c release -- unit-test --single-target + - name: Upload received snapshots + # A failed snapshot test leaves its received file here. Download it into .artifacts/.snapshots/received/ and run `dotnet snap review`. + if: ${{ failure() && needs.changes.outputs.code == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: received-snapshots-${{ matrix.os }} + path: .artifacts/.snapshots/received/ + if-no-files-found: ignore + - name: Publish AOT if: ${{ needs.changes.outputs.code == 'true' && (github.event_name == 'push' && matrix.os != 'ubuntu-latest') }} # publish containers already validates AOT build run: dotnet run --project build -c release -- publishbinaries diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml index 3c884acdc3..314cf54941 100644 --- a/.github/workflows/pr-review.yml +++ b/.github/workflows/pr-review.yml @@ -27,7 +27,7 @@ jobs: validates cross-repo references, and ships as native AOT binaries for CI. ## Language and frameworks - - C# (.NET) with xUnit v3 + AwesomeAssertions for tests. Do NOT suggest TUnit APIs — the migration hasn't happened yet. + - C# (.NET) with TUnit + AwesomeAssertions for tests. Expected output lives in Nullean.Snapshots snapshots (inline literals and `.snapshots/` files); never suggest accepting snapshots in code or CI. - TypeScript/React frontend under src/Elastic.Documentation.Site/. - Test naming convention: Method_Scenario_Expected. diff --git a/AGENTS.md b/AGENTS.md index 0f31e021e8..feb01f4633 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,6 +65,12 @@ Tests live in `tests/` (unit) and `tests-integration/` (integration). - **C#**: TUnit · AwesomeAssertions · FakeItEasy across all test projects. All test projects (unit and integration) use TUnit (wired centrally via `tests/Directory.Build.props` and `tests-integration/Directory.Build.props`; no per-project flag needed). Write new tests using TUnit APIs: `[Test]`, `[Arguments(...)]`, `[MethodDataSource(nameof(X))]`. - **TypeScript**: Vitest +- **Snapshots** (`Nullean.Snapshots`, wired for every test project in `tests/Directory.Build.props`; helpers in `tests-shared/Elastic.Documentation.Testing`): expected output lives in snapshots, not in chains of `Contain` assertions. + - Small values go inline: `x.Should().MatchInlineSnapshot("""...""")`, `MatchInlineHtmlSnapshot`, `MatchInlineJsonSnapshot`. Leave the argument out to write a new one. Large values go to a verified file: `MatchSnapshot()`, `MatchHtmlSnapshot()`, `MatchJsonSnapshot()`, `MatchYamlSnapshot()`. + - Diagnostics: `collector.Diagnostics.ToSnapshotText()` (`DiagnosticsSnapshot`). A clean run is the empty snapshot. + - Navigation trees and generated files: `NavigationDump.ToText(...)`, `MockFileSystemDump.ToText(fs, dir)`. Rendered docs HTML: `DocsHtml.Section()` (the page content, no fixture H1, no htmx attributes) or `DocsHtml.Body()`. + - Accept with the `snap` tool (`dotnet snap status`, `dotnet snap review`, `dotnet snap accept `), or `SNAPSHOT_ACCEPT=1`. Read the diff of the changed `.verified.*` files and inline literals before committing. Nothing is accepted in CI. When a CI test run fails, download the `received-snapshots-*` artifact into `.artifacts/.snapshots/received/` and run `dotnet snap review`. + - Changing a snapshot test's output is a behaviour change. Mutate the code once and confirm the test fails before you commit. - **HTML snapshots** (`Nullean.Snapshots.Html`, used in `Elastic.ApiExplorer.Tests`): when a test would check one rendered block with many `Contain` assertions, compare it to a verified snapshot instead and keep one assertion that states the intent. - Write `html.Should().MatchHtmlSnapshot()`. Narrow it with `new HtmlSnapshotOptions().Select("#some-id")` or `.RemoveAttributes(...)` so unrelated markup and volatile values (bundle names, CI git info) stay out. - Snapshot a partial or one section, not a whole page. Whole-page snapshots break on unrelated changes. diff --git a/Directory.Packages.props b/Directory.Packages.props index 782c52aab0..b1dcf0d2c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -127,8 +127,12 @@ - - + + + + + + diff --git a/build/Targets.fs b/build/Targets.fs index a73833190f..432a81e056 100644 --- a/build/Targets.fs +++ b/build/Targets.fs @@ -205,6 +205,12 @@ let private runTests (testSuite: TestSuite) (parsed: ParseResults) = |> Array.toList if not (List.isEmpty failures) then failwithf "Test projects failed:\n%s" (String.concat "\n" failures) + + // Every test project has now run, so the run manifests are complete: a verified snapshot nobody used belongs to a deleted or renamed test. + if testSuite <> Integration then + match exec { exit_code_of "dotnet" "snap" "orphans" } with + | 0 -> () + | _ -> failwith "Orphaned snapshots found. Delete them with `dotnet snap orphans --delete` and commit the removal." let private compressibleExtensions = set [".html"; ".css"; ".js"; ".json"; ".svg"; ".xml"; ".txt"] diff --git a/docs-builder.slnx b/docs-builder.slnx index 47468e9ede..31e5d66687 100644 --- a/docs-builder.slnx +++ b/docs-builder.slnx @@ -90,6 +90,10 @@ + + + + diff --git a/dotnet-tools.json b/dotnet-tools.json index 084330e010..d77fdfb621 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -43,6 +43,13 @@ "curb" ], "rollForward": false + }, + "nullean.snapshots.cli": { + "version": "0.4.0", + "commands": [ + "snap" + ], + "rollForward": false } } } diff --git a/tests-integration/Directory.Build.props b/tests-integration/Directory.Build.props index 379438ea3e..1717682543 100644 --- a/tests-integration/Directory.Build.props +++ b/tests-integration/Directory.Build.props @@ -18,6 +18,13 @@ + + + + + + + diff --git a/tests-shared/Directory.Build.props b/tests-shared/Directory.Build.props new file mode 100644 index 0000000000..5b92d69384 --- /dev/null +++ b/tests-shared/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + + CA1822;CA1707;IDE0058 + false + false + + diff --git a/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs b/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs new file mode 100644 index 0000000000..0e8f94f1f2 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs @@ -0,0 +1,32 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text; +using Elastic.Documentation.Diagnostics; + +namespace Elastic.Documentation.Testing; + +public static class DiagnosticsSnapshot +{ + /// + /// One line per diagnostic, error|warning|hint file:line message, in the order they were written. Empty when + /// there are none, so a clean run reads as MatchInlineSnapshot(""). + /// + public static string ToSnapshotText(this IEnumerable diagnostics) + { + var sb = new StringBuilder(); + foreach (var d in diagnostics) + { + var severity = d.Severity switch + { + Severity.Error => "error", + Severity.Warning => "warning", + _ => "hint" + }; + var position = d.Line is { } line ? $"{d.File}:{line}" : d.File; + sb.Append(severity).Append(' ').Append(position).Append(' ').Append(d.Message).Append('\n'); + } + return sb.ToString(); + } +} diff --git a/tests-shared/Elastic.Documentation.Testing/DocsHtml.cs b/tests-shared/Elastic.Documentation.Testing/DocsHtml.cs new file mode 100644 index 0000000000..961b026997 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/DocsHtml.cs @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Nullean.Snapshots.Html; + +namespace Elastic.Documentation.Testing; + +/// +/// How docs-builder's tests normalise rendered HTML before comparing or snapshotting it. One place for what +/// Elastic.Markdown.Tests.PrettyHtmlExtensions and Elastic.Authoring.Tests.Framework.HtmlAssertions +/// used to do separately. +/// +public static class DocsHtml +{ + /// htmx navigation attributes on links are plumbing, not content. + private static readonly string[] HtmxAttributes = ["hx-get", "hx-select-oob", "hx-swap", "hx-indicator", "hx-push-url", "preload"]; + + /// The whole body, with htmx attributes removed from links. + public static HtmlSnapshotOptions Body() => new HtmlSnapshotOptions().RemoveAttributesFrom("a", HtmxAttributes); + + /// + /// The rendered page's content section, without the leading H1 the test fixtures wrap every document in + /// (# Test Document), and with htmx attributes removed from links. + /// + public static HtmlSnapshotOptions Section() => + Body() + .Select("section#elastic-docs-v3") + .Transform(root => + { + if (root.FirstElementChild is { TagName: "H1" } h1) + h1.Remove(); + }); +} diff --git a/tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj b/tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj new file mode 100644 index 0000000000..ab56af73cd --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/Elastic.Documentation.Testing.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + Elastic.Documentation.Testing + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs new file mode 100644 index 0000000000..6a80bb71fc --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs @@ -0,0 +1,39 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Nullean.Snapshots; + +namespace Elastic.Documentation.Testing.Linked; + +/// +/// Scrubbers every snapshot in docs-builder's tests gets: machine-specific paths and git SHAs never belong in a +/// verified file. Runs when any test project referencing this assembly loads it. +/// +internal static class SnapshotScrubbing +{ +#pragma warning disable CA2255 // compiled into each test project, which is application code + [ModuleInitializer] +#pragma warning restore CA2255 + internal static void Register() + { + SnapshotDefaults.AddScrubber( + Scrubbers.Regex( + new Regex(Regex.Escape(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar)) + @"[^\s""'<>]*", RegexOptions.Compiled), + "{temp}" + ) + ); + if (FindSolutionRoot() is { } root) + SnapshotDefaults.AddScrubber(Scrubbers.Regex(new Regex(Regex.Escape(root), RegexOptions.Compiled), "{repo}")); + } + + private static string? FindSolutionRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "docs-builder.slnx"))) + dir = dir.Parent; + return dir?.FullName.TrimEnd(Path.DirectorySeparatorChar); + } +} diff --git a/tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs b/tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs new file mode 100644 index 0000000000..daaf2d8ed6 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/MockFileSystemDump.cs @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions; +using System.Text; + +namespace Elastic.Documentation.Testing; + +public static class MockFileSystemDump +{ + /// + /// Every file under , sorted by path, each under an === relative/path === + /// header. Meant for tests that read generated files back from a mock file system. + /// + public static string ToText(IFileSystem fileSystem, string directory, string searchPattern = "*") + { + var root = fileSystem.Path.GetFullPath(directory); + var sb = new StringBuilder(); + var files = fileSystem + .Directory + .EnumerateFiles(root, searchPattern, SearchOption.AllDirectories) + .Select(f => (Full: f, Relative: fileSystem.Path.GetRelativePath(root, f).Replace('\\', '/'))) + .OrderBy(f => f.Relative, StringComparer.Ordinal); + foreach (var (full, relative) in files) + { + sb.Append("=== ").Append(relative).Append(" ===\n"); + var content = fileSystem.File.ReadAllText(full).Replace("\r\n", "\n"); + sb.Append(content); + if (!content.EndsWith('\n')) + sb.Append('\n'); + } + return sb.ToString(); + } +} diff --git a/tests-shared/Elastic.Documentation.Testing/NavigationDump.cs b/tests-shared/Elastic.Documentation.Testing/NavigationDump.cs new file mode 100644 index 0000000000..a70dfa66f6 --- /dev/null +++ b/tests-shared/Elastic.Documentation.Testing/NavigationDump.cs @@ -0,0 +1,51 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text; +using Elastic.Documentation.Navigation; + +namespace Elastic.Documentation.Testing; + +public static class NavigationDump +{ + /// + /// The navigation tree as indented text, one node per line: Type url "title", with [hidden] and + /// [island] flags. A node lists its index page first (index: Type url "title"), then its children. The + /// index page is where most of the interesting URLs live. A child whose Parent is not the node it is listed + /// under is flagged [wrong parent]; index pages are not checked, because a virtual file's index has a + /// different parent. + /// + public static string ToText(INavigationItem root) + { + var sb = new StringBuilder(); + Write(sb, root, 0, null, null); + return sb.ToString(); + } + + private static void Write(StringBuilder sb, INavigationItem item, int depth, string? label, INavigationItem? container) + { + sb + .Append(' ', depth * 2) + .Append(label) + .Append(item.GetType().Name.Split('`')[0]) + .Append(" ") + .Append(item.Url) + .Append(" \"") + .Append(item.NavigationTitle) + .Append('"'); + if (item.Hidden) + sb.Append(" [hidden]"); + if (item.IsIsland) + sb.Append(" [island]"); + if (container is not null && !ReferenceEquals(item.Parent, container)) + sb.Append(" [wrong parent]"); + sb.Append('\n'); + + if (item is not INodeNavigationItem node) + return; + Write(sb, node.Index, depth + 1, "index: ", null); + foreach (var child in node.NavigationItems) + Write(sb, child, depth + 1, null, item); + } +} diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 6ce509c511..b3667da5e1 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -27,5 +27,12 @@ + + + + + + + diff --git a/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj b/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj index b6138fd950..17b5bb72f8 100644 --- a/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj +++ b/tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj @@ -12,8 +12,6 @@ - - diff --git a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs new file mode 100644 index 0000000000..5897c67b4a --- /dev/null +++ b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions.TestingHelpers; +using AwesomeAssertions; +using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; +using Nullean.Snapshots.Html; + +namespace Elastic.Documentation.Tests.Testing; + +public class SnapshotHelpersTests +{ + [Test] + public void ToSnapshotText_NoDiagnostics_IsEmpty() => new List().ToSnapshotText().Should().MatchInlineSnapshot(""); + + [Test] + public void ToSnapshotText_MixedSeverities_OneLineEachInOrder() + { + var diagnostics = new List + { + new() { Severity = Severity.Error, File = "a.md", Line = 3, Message = "bad link" }, + new() { Severity = Severity.Warning, File = "b.md", Message = "no line" }, + new() { Severity = Severity.Hint, File = "c.md", Line = 1, Message = "consider" } + }; + diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + error a.md:3 bad link + warning b.md no line + hint c.md:1 consider + """ + ); + } + + [Test] + public void MockFileSystemDump_SortsByPathAndNormalisesLineEndings() + { + var fs = new MockFileSystem(); + fs.AddFile("/out/b/two.md", new MockFileData("two\r\nlines")); + fs.AddFile("/out/a.md", new MockFileData("one\n")); + MockFileSystemDump + .ToText(fs, "/out") + .Should() + .MatchInlineSnapshot( + """ + === a.md === + one + === b/two.md === + two + lines + """ + ); + } + + [Test] + public void DocsHtmlSection_DropsFixtureHeadingAndHtmxAttributes() => + """ +

Test Document

x

nav
+ """.Should().MatchInlineHtmlSnapshot( + """ +

x

+ """, + DocsHtml.Section() + ); +} From a9fd42330390df7a7c5f9ced0b4102e1d1f7f9fa Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:29:06 +0100 Subject: [PATCH 02/12] Compare rendered HTML with Nullean.Snapshots.Html ShouldBeHtml and ShouldContainHtml (Elastic.Markdown.Tests) and ConvertsToHtml, ConvertsToContainingHtml and the per-file ToHtml and ContainsHtml (Elastic.Authoring.Tests) keep their names and call sites, and now run on BeHtml and ContainHtml with the shared DocsHtml options. That removes the second copy of the AngleSharp.Diffing diff code and the AngleSharp.Diffing package. DiffPlex stays in Authoring for the plain text assertions. One test passed without testing anything. "marks exactly two options as a fork" gave the bare text `is-fork` to ConvertsToContainingHtml. The old normaliser printed only element children, so a text-only fragment became an empty string and every page contains an empty string. It now uses ConvertsToContainingRawHtml, like the test above it. Checked by corrupting one ShouldContainHtml and one ConvertsToHtml expectation: both fail. Co-Authored-By: Claude Sonnet 5.5 --- Directory.Packages.props | 1 - .../Blocks/Hub/GetStarted.cs | 2 +- .../Elastic.Authoring.Tests.csproj | 1 - .../Framework/HtmlAssertions.cs | 177 ++---------------- .../Elastic.Markdown.Tests.csproj | 2 - .../PrettyHtmlExtensions.cs | 127 +------------ 6 files changed, 22 insertions(+), 288 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b1dcf0d2c0..0fdb1d5ee6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -126,7 +126,6 @@
- diff --git a/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs b/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs index dec67a93fb..d0ccf282e0 100644 --- a/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs +++ b/tests/Elastic.Authoring.Tests/Blocks/Hub/GetStarted.cs @@ -164,7 +164,7 @@ public async Task PutsTheFirstStepInItsOwnBox() => await Docs.ConvertsToContainingRawHtml("""
  • """); [Test, DisplayName("marks exactly two options as a fork")] - public async Task MarksExactlyTwoOptionsAsAFork() => await Docs.ConvertsToContainingHtml("""is-fork"""); + public async Task MarksExactlyTwoOptionsAsAFork() => await Docs.ConvertsToContainingRawHtml("""is-fork"""); [Test, DisplayName("has no errors")] public async Task HasNoErrors() => await Docs.HasNoErrors(); diff --git a/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj b/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj index ee73b53ebd..4e5e05627f 100644 --- a/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj +++ b/tests/Elastic.Authoring.Tests/Elastic.Authoring.Tests.csproj @@ -5,7 +5,6 @@ - diff --git a/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs b/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs index 2bf599f62b..7fbebbdf03 100644 --- a/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs +++ b/tests/Elastic.Authoring.Tests/Framework/HtmlAssertions.cs @@ -3,13 +3,12 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using AngleSharp.Diffing; -using AngleSharp.Diffing.Core; -using AngleSharp.Html; -using AngleSharp.Html.Parser; +using AwesomeAssertions; using DiffPlex.DiffBuilder; using DiffPlex.DiffBuilder.Model; +using Elastic.Documentation.Testing; using JetBrains.Annotations; +using Nullean.Snapshots.Html; namespace Elastic.Authoring.Tests.Framework; @@ -27,8 +26,7 @@ namespace Elastic.Authoring.Tests.Framework; /// would appear verbatim in every assertion snapshot otherwise. /// /// -/// Produces a humanized diff via that names the diff type -/// (NodeDiff / AttrDiff / MissingNodeDiff etc.) and shows expected vs. actual node text. +/// Compares through Nullean.Snapshots.Html, which reports a structural diff and then a line diff. /// /// /// All assertion methods are synchronous — the async wrapper lives on . @@ -73,173 +71,28 @@ internal static string Diff(string expected, string actual) } // ──────────────────────────────────────────────────────────────────────────────── - // AngleSharp deep HTML diff + // HTML comparison on Nullean.Snapshots.Html // ──────────────────────────────────────────────────────────────────────────────── - internal static string HtmlDiffString(IEnumerable diffs) - { - var formatter = new PrettyMarkupFormatter(); - - string NodeText(ComparisonSource source) - { - using var sw = new StringWriter(); - source.Node.ToHtml(sw, formatter); - return sw.ToString(); - } - - string AttrText(AttributeComparisonSource source) - { - using var sw = new StringWriter(); - source.Attribute.ToHtml(sw, formatter); - return sw.ToString(); - } - - // NodeDiffText and AttrDiffText accept non-nullable (callers pass .Control / .Test directly). - // For Missing*/Unexpected* cases we pass a sentinel "missing" string instead of calling these helpers. - string NodeDiffText(ComparisonSource control, ComparisonSource test) => - $"\nexpected: {NodeText(control)}\nactual: {NodeText(test)}\n"; - - string MissingNodeText(ComparisonSource control) => $"\nexpected: {NodeText(control)}\nactual: missing\n"; - - string UnexpectedNodeText(ComparisonSource test) => $"\nexpected: missing\nactual: {NodeText(test)}\n"; - - string AttrDiffText(AttributeComparisonSource control, AttributeComparisonSource test) => - $"\nexpected: {AttrText(control)}\nactual: {AttrText(test)}\n"; - - string MissingAttrText(AttributeComparisonSource control) => $"\nexpected: {AttrText(control)}\nactual: missing\n"; - - string UnexpectedAttrText(AttributeComparisonSource test) => $"\nexpected: missing\nactual: {AttrText(test)}\n"; - - static string NodeName(ComparisonSource source) => source.Node.NodeType.ToString().ToLowerInvariant(); - - return string.Join('\n', diffs.Select(d => - { - var label = d switch - { - NodeDiff nd when nd.Target == DiffTarget.Text && nd.Control.Path.Equals(nd.Test.Path, StringComparison.Ordinal) => - $"The text in {nd.Control.Path} is different.", - NodeDiff nd when nd.Target == DiffTarget.Text => - $"The expected {NodeName(nd.Control)} at {nd.Control.Path} and the actual {NodeName(nd.Test)} at {nd.Test.Path} is different.", - NodeDiff nd when nd.Control.Path.Equals(nd.Test.Path, StringComparison.Ordinal) => - $"The {NodeName(nd.Control)}s at {nd.Control.Path} are different.", - NodeDiff nd => - $"The expected {NodeName(nd.Control)} at {nd.Control.Path} and the actual {NodeName(nd.Test)} at {nd.Test.Path} are different.", - AttrDiff ad when ad.Control.Path.Equals(ad.Test.Path, StringComparison.Ordinal) => - $"The values of the attributes at {ad.Control.Path} are different.", - AttrDiff ad => $"The value of the attribute {ad.Control.Path} and actual attribute {ad.Test.Path} are different.", - MissingNodeDiff md => $"The {NodeName(md.Control)} at {md.Control.Path} is missing.", - MissingAttrDiff ma => $"The attribute at {ma.Control.Path} is missing.", - UnexpectedNodeDiff ud => $"The {NodeName(ud.Test)} at {ud.Test.Path} was not expected.", - UnexpectedAttrDiff ua => $"The attribute at {ua.Test.Path} was not expected.", - _ => throw new InvalidOperationException($"Unknown diff type detected: {d.GetType()}") - }; - - var detail = d switch - { - NodeDiff nd => NodeDiffText(nd.Control, nd.Test), - AttrDiff ad => AttrDiffText(ad.Control, ad.Test), - MissingNodeDiff md => MissingNodeText(md.Control), - MissingAttrDiff ma => MissingAttrText(ma.Control), - UnexpectedNodeDiff ud => UnexpectedNodeText(ud.Test), - UnexpectedAttrDiff ua => UnexpectedAttrText(ua.Test), - _ => throw new InvalidOperationException($"Unknown diff type detected: {d.GetType()}") - }; - - return label + detail; - })); - } - - // ──────────────────────────────────────────────────────────────────────────────── - // Pretty printer — matches the F# prettyHtml behaviour exactly - // ──────────────────────────────────────────────────────────────────────────────── - - private static string PrettyHtml(string html, string? querySelector) - { - var parser = new HtmlParser(); - var document = parser.ParseDocument(html); - var element = querySelector is null ? document.Body! : document.QuerySelector(querySelector)!; - - // Strip HTMX attributes from links — they are navigation artifacts, not content. - foreach (var link in element.QuerySelectorAll("a")) - { - link.RemoveAttribute("hx-get"); - link.RemoveAttribute("hx-select-oob"); - link.RemoveAttribute("hx-swap"); - link.RemoveAttribute("hx-indicator"); - link.RemoveAttribute("hx-push-url"); - link.RemoveAttribute("preload"); - } - - var formatter = new PrettyMarkupFormatter(); - using var sw = new StringWriter(); - - // Skip the first child if it is an H1 — the fixture wraps Setup.Markdown content in one. - var children = element.Children.ToList(); - for (var i = 0; i < children.Count; i++) - { - if (i == 0 && children[i].TagName == "H1") - continue; - children[i].ToHtml(sw, formatter); - } - - return sw.ToString().TrimStart('\n'); - } - - private static void CreateDiff(string expected, string actual) - { - // Identical pretty-printed HTML has no structural differences, so skip the AngleSharp comparison for the usual passing case. - if (string.Equals(expected, actual, StringComparison.Ordinal)) - return; - - var diffs = DiffBuilder.Compare(expected).WithTest(actual).Build(); - - var deepComparison = HtmlDiffString(diffs); - if (string.IsNullOrEmpty(deepComparison)) - return; - - var textDiff = Diff(expected, actual); - throw new AwesomeAssertions.Execution.AssertionFailedException( - $""" -Html was not equal --- DIFF -- -{textDiff} + /// + /// The rendered page's content section, minus the fixture's leading H1 and the htmx attributes on links. + /// The expected fragment is wrapped in the same section so both sides go through identical normalisation. + /// + private static HtmlSnapshotOptions Options => DocsHtml.Section(); --- Comparison -- -{deepComparison} -""" - ); - } + private static string InSection(string fragment) => $"
    {fragment}
    "; // ──────────────────────────────────────────────────────────────────────────────── // Per-file assertions (called from MarkdownResultTask / Scenario) // ──────────────────────────────────────────────────────────────────────────────── [DebuggerStepThrough] - internal static void ToHtml([LanguageInjection("html")] string expected, MarkdownResult actual) - { - var expectedHtml = PrettyHtml(expected, null); - var actualHtml = PrettyHtml(actual.Html, "section#elastic-docs-v3"); - CreateDiff(expectedHtml, actualHtml); - } + internal static void ToHtml([LanguageInjection("html")] string expected, MarkdownResult actual) => + actual.Html.Should().BeHtml(InSection(expected), Options); [DebuggerStepThrough] - internal static void ContainsHtml([LanguageInjection("html")] string expected, MarkdownResult actual) - { - var prettyExpected = PrettyHtml(expected, null); - var prettyActual = PrettyHtml(actual.Html, "section#elastic-docs-v3"); - - if (!prettyActual.Contains(prettyExpected, StringComparison.Ordinal)) - throw new AwesomeAssertions.Execution.AssertionFailedException( - $""" -Expected html to contain: -{prettyExpected} - -But was not found in: - -{prettyActual} -""" - ); - } + internal static void ContainsHtml([LanguageInjection("html")] string expected, MarkdownResult actual) => + actual.Html.Should().ContainHtml(InSection(expected), Options); [DebuggerStepThrough] internal static void ContainsRawHtml(string expected, MarkdownResult actual) diff --git a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj index a128c14af7..e791376c2a 100644 --- a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj +++ b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj @@ -5,8 +5,6 @@ - - diff --git a/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs b/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs index eb552208b7..29cd3b7467 100644 --- a/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs +++ b/tests/Elastic.Markdown.Tests/PrettyHtmlExtensions.cs @@ -2,142 +2,27 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using AngleSharp.Diffing; -using AngleSharp.Html; -using AngleSharp.Html.Parser; using AwesomeAssertions; -using AwesomeAssertions.Execution; -using DiffPlex.DiffBuilder; -using DiffPlex.DiffBuilder.Model; +using Elastic.Documentation.Testing; using JetBrains.Annotations; +using Nullean.Snapshots.Html; namespace Elastic.Markdown.Tests; +/// HTML comparisons on Nullean.Snapshots.Html: structural, so attribute order and whitespace never fail a test. public static class PrettyHtmlExtensions { - public static string PrettyHtml([LanguageInjection("html")] this string html, bool sanitize = true) - { - var parser = new HtmlParser(); - - var document = parser.ParseDocument(html); - var element = document.Body; - if (element is null) - return string.Empty; - - if (sanitize) - { - var links = element.QuerySelectorAll("a"); - foreach (var l in links) - { - l.RemoveAttribute("hx-get"); - l.RemoveAttribute("hx-select-oob"); - l.RemoveAttribute("hx-swap"); - l.RemoveAttribute("hx-indicator"); - l.RemoveAttribute("hx-push-url"); - l.RemoveAttribute("preload"); - } - } - - using var sw = new StringWriter(); - var formatter = new PrettyMarkupFormatter(); - foreach (var c in element.Children) - { - // ReSharper disable once AccessToDisposedClosure - c.ToHtml(sw, formatter); - } - return sw.ToString().TrimStart('\n'); - } + private static HtmlSnapshotOptions Options(bool sanitize) => sanitize ? DocsHtml.Body() : new HtmlSnapshotOptions(); public static void ShouldBeHtml( [LanguageInjection("html")] this string actual, [LanguageInjection("html")] string expected, bool sanitize = true - ) - { - expected = expected.Trim('\n').PrettyHtml(sanitize); - actual = actual.Trim('\n').PrettyHtml(sanitize); + ) => actual.Trim('\n').Should().BeHtml(expected.Trim('\n'), Options(sanitize)); - var diff = DiffBuilder.Compare(actual).WithTest(expected).Build().ToArray(); - - if (diff.Length == 0) - return; - - throw new AssertionFailedException(CreateDiff(actual, expected, sanitize)); - } public static void ShouldContainHtml( [LanguageInjection("html")] this string actual, [LanguageInjection("html")] string expected, bool sanitize = true - ) - { - expected = expected.Trim('\n').PrettyHtml(sanitize); - actual = actual.Trim('\n').PrettyHtml(sanitize); - - var actualCompare = actual.Replace("\t", string.Empty); - var expectedCompare = expected.Replace("\t", string.Empty); - - // we compare over unindented HTML, but if that fails, we rely on the pretty HTML Contain(). - // to throw for improved error messages - if (!actualCompare.Contains(expectedCompare)) - actual.Should().Contain(expected); - } - - public static string CreateDiff(this string actual, string expected, bool sanitize = true) - { - expected = expected.Trim('\n').PrettyHtml(sanitize); - actual = actual.Trim('\n').PrettyHtml(sanitize); - var diffLines = InlineDiffBuilder.Diff(expected, actual).Lines; - - var mutatedCount = diffLines.Count( - l => - l.Type switch - { - ChangeType.Unchanged => false, - ChangeType.Deleted => true, - ChangeType.Inserted => true, - ChangeType.Imaginary => false, - ChangeType.Modified => true, - _ => false - } - ); - if (mutatedCount == 0) - return string.Empty; - - var actualLineLength = actual.Split("\n").Length; - if (mutatedCount >= actualLineLength) - { - return $$""" - Mutations {{mutatedCount}} on all {{actualLineLength}} showing - EXPECTED: - {{expected}} - ACTUAL: - {{actual}} - """; - } - - using var sw = new StringWriter(); - foreach (var l in diffLines) - { - switch (l.Type) - { - case ChangeType.Unchanged: - sw.WriteLine(l.Text); - break; - case ChangeType.Deleted: - sw.WriteLine("- " + l.Text); - break; - case ChangeType.Inserted: - sw.WriteLine("+ " + l.Text); - break; - case ChangeType.Imaginary: - sw.WriteLine("? " + l.Text); - break; - case ChangeType.Modified: - sw.WriteLine("+ " + l.Text); - break; - } - } - - return sw.ToString(); - } + ) => actual.Trim('\n').Should().ContainHtml(expected.Trim('\n'), Options(sanitize)); } From 4819dfe82e10d450a9b333bb853663c9ef35c647 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:38:41 +0100 Subject: [PATCH 03/12] Changelog render tests: snapshot the rendered files The render tests read index.md, highlights.md and the other generated files back from the mock file system and checked a few Contain and NotContain fragments. Each one now snapshots every file the service wrote (MockFileSystemDump), inline when the output is short and as a verified file otherwise, so a change to any generated line shows up as a diff and the absence of a file is part of the expectation. The tests became stricter: before accepting, every old Contain and NotContain fragment was checked against the received output, and the ordering checks in the GFM tests (sections, highlights first) are covered by the snapshot. Warnings and errors the tests looked for with Diagnostics.Should().Contain(...) are inline diagnostics snapshots now, so any other diagnostic would fail them too. Guids in the paths of the mock file system are numbered by a scrubber registered for this project. The invalid-bundle test accepted either of two messages; it now asserts the single error that is actually produced. Co-Authored-By: Claude Sonnet 5.5 --- ...tIncludeCrossFileLinksInIndex.verified.txt | 23 ++ ...gnoresDropdownsFlag.dropdowns.verified.txt | 17 ++ ...gnoresDropdownsFlag.flattened.verified.txt | 17 ++ ...RendersCorrectFormat.dropdown.verified.txt | 24 ++ ...endersCorrectFormat.flattened.verified.txt | 23 ++ ...nsFalse_RendersFlattendFormat.verified.txt | 21 ++ ...wnsTrue_RendersDropdownFormat.verified.txt | 21 ++ ...criptions.hidden-descriptions.verified.txt | 5 + ...akingChange_UsesBlockComments.verified.txt | 25 +++ ...Deprecation_UsesBlockComments.verified.txt | 21 ++ ...Entries_CreatesHighlightsFile.verified.txt | 23 ++ ...ghlightedEntries_GroupsByArea.verified.txt | 32 +++ ...eType_ValidatesAsciidocFormat.verified.txt | 35 +++ ...cLinks_RendersOnlyPublicLinks.verified.txt | 21 ++ .../Changelogs/Render/BasicRenderTests.cs | 84 +++---- .../Render/BundleValidationTests.cs | 104 +++++++-- .../Render/ChecksumValidationTests.cs | 26 ++- .../Render/DescriptionVisibilityTests.cs | 212 +++++++++++------- .../Changelogs/Render/DropdownRenderTests.cs | 120 +++------- .../Render/DuplicateHandlingTests.cs | 28 ++- .../Changelogs/Render/ErrorHandlingTests.cs | 69 +++--- .../Changelogs/Render/GfmRenderTests.cs | 161 +++++++------ .../Changelogs/Render/HideFeaturesTests.cs | 205 +++++++++++------ .../Render/HighlightsRenderTests.cs | 59 ++--- .../Changelogs/Render/OutputFormatTests.cs | 90 ++++---- .../Changelogs/Render/PrivateLinkBugTests.cs | 59 +++-- .../Changelogs/Render/TitleTargetTests.cs | 90 ++++++-- .../Elastic.Changelog.Tests/SnapshotSetup.cs | 16 ++ 28 files changed, 1108 insertions(+), 523 deletions(-) create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/BasicRenderTests/RenderChangelogs_WithMultipleTypes_DoesNotIncludeCrossFileLinksInIndex.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.dropdowns.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.flattened.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.dropdown.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.flattened.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsFalse_RendersFlattendFormat.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsTrue_RendersDropdownFormat.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/GfmRenderTests/RenderChangelogs_WithGfmFileType_HandlesDescriptionsAndHideDescriptions.hidden-descriptions.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_BreakingChange_UsesBlockComments.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_Deprecation_UsesBlockComments.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithHighlightedEntries_CreatesHighlightsFile.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithMultipleHighlightedEntries_GroupsByArea.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/OutputFormatTests/RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/PrivateLinkBugTests/RenderChangelogs_WithMixedPrivateAndPublicLinks_RendersOnlyPublicLinks.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/SnapshotSetup.cs diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/BasicRenderTests/RenderChangelogs_WithMultipleTypes_DoesNotIncludeCrossFileLinksInIndex.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/BasicRenderTests/RenderChangelogs_WithMultipleTypes_DoesNotIncludeCrossFileLinksInIndex.verified.txt new file mode 100644 index 0000000000..c58e75c106 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/BasicRenderTests/RenderChangelogs_WithMultipleTypes_DoesNotIncludeCrossFileLinksInIndex.verified.txt @@ -0,0 +1,23 @@ +=== 9.3.0/breaking-changes.md === +## 9.3.0 [elastic-9.3.0-breaking-changes] +_There are no breaking changes associated with this release._ +=== 9.3.0/deprecations.md === +## 9.3.0 [elastic-9.3.0-deprecations] + +* Deprecated API. + For more information, check [#200](https://github.com/elastic/elastic/pull/200). + +=== 9.3.0/highlights.md === +## 9.3.0 [elastic-9.3.0-highlights] + +* Highlighted feature. + For more information, check [#300](https://github.com/elastic/elastic/pull/300). + +=== 9.3.0/index.md === +## 9.3.0 [elastic-release-notes-9.3.0] +### Features and enhancements [elastic-9.3.0-features-enhancements] +* Test feature. [#100](https://github.com/elastic/elastic/pull/100) +* Highlighted feature. [#300](https://github.com/elastic/elastic/pull/300) +=== 9.3.0/known-issues.md === +## 9.3.0 [elastic-9.3.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.dropdowns.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.dropdowns.verified.txt new file mode 100644 index 0000000000..d6c5e8c0a5 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.dropdowns.verified.txt @@ -0,0 +1,17 @@ +=== 9.2.0.asciidoc === +[[release-notes-9.2.0]] +== 9.2.0 + +[[breaking-changes-9.2.0]] +[float] +=== Breaking changes + +* Breaking API change. {elastic-pull}123[#123] ++ +API has been changed + ++ +*Impact:* Existing API calls will fail + ++ +*Action:* Update your code diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.flattened.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.flattened.verified.txt new file mode 100644 index 0000000000..d6c5e8c0a5 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag.flattened.verified.txt @@ -0,0 +1,17 @@ +=== 9.2.0.asciidoc === +[[release-notes-9.2.0]] +== 9.2.0 + +[[breaking-changes-9.2.0]] +[float] +=== Breaking changes + +* Breaking API change. {elastic-pull}123[#123] ++ +API has been changed + ++ +*Impact:* Existing API calls will fail + ++ +*Action:* Update your code diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.dropdown.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.dropdown.verified.txt new file mode 100644 index 0000000000..b6c90cb220 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.dropdown.verified.txt @@ -0,0 +1,24 @@ +=== 9.2.0/breaking-changes.md === +## 9.2.0 [elastic-9.2.0-breaking-changes] +_There are no breaking changes associated with this release._ +=== 9.2.0/deprecations.md === +## 9.2.0 [elastic-9.2.0-deprecations] +_There are no deprecations associated with this release._ +=== 9.2.0/highlights.md === +## 9.2.0 [elastic-9.2.0-highlights] + +::::{dropdown} Amazing new feature. +This feature revolutionizes how you work with data + +For more information, check [#555](https://github.com/elastic/elastic/pull/555). + +:::: +=== 9.2.0/index.md === +## 9.2.0 [elastic-release-notes-9.2.0] +### Features and enhancements [elastic-9.2.0-features-enhancements] +* Amazing new feature. [#555](https://github.com/elastic/elastic/pull/555) + + This feature revolutionizes how you work with data +=== 9.2.0/known-issues.md === +## 9.2.0 [elastic-9.2.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.flattened.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.flattened.verified.txt new file mode 100644 index 0000000000..7876f078d7 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat.flattened.verified.txt @@ -0,0 +1,23 @@ +=== 9.2.0/breaking-changes.md === +## 9.2.0 [elastic-9.2.0-breaking-changes] +_There are no breaking changes associated with this release._ +=== 9.2.0/deprecations.md === +## 9.2.0 [elastic-9.2.0-deprecations] +_There are no deprecations associated with this release._ +=== 9.2.0/highlights.md === +## 9.2.0 [elastic-9.2.0-highlights] + +* Amazing new feature. + This feature revolutionizes how you work with data + + For more information, check [#555](https://github.com/elastic/elastic/pull/555). + +=== 9.2.0/index.md === +## 9.2.0 [elastic-release-notes-9.2.0] +### Features and enhancements [elastic-9.2.0-features-enhancements] +* Amazing new feature. [#555](https://github.com/elastic/elastic/pull/555) + + This feature revolutionizes how you work with data +=== 9.2.0/known-issues.md === +## 9.2.0 [elastic-9.2.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsFalse_RendersFlattendFormat.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsFalse_RendersFlattendFormat.verified.txt new file mode 100644 index 0000000000..4b9dac3dc6 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsFalse_RendersFlattendFormat.verified.txt @@ -0,0 +1,21 @@ +=== 9.2.0/breaking-changes.md === +## 9.2.0 [elastic-9.2.0-breaking-changes] +_There are no breaking changes associated with this release._ +=== 9.2.0/deprecations.md === +## 9.2.0 [elastic-9.2.0-deprecations] + +* Deprecated old API. + The old API is deprecated + + For more information, check [#456](https://github.com/elastic/elastic/pull/456) [#789](https://github.com/elastic/elastic/issues/789). + + **Impact:** API will be removed in future version + + **Action:** Migrate to the new API + +=== 9.2.0/index.md === +## 9.2.0 [elastic-release-notes-9.2.0] +_There are no new features, enhancements, or fixes associated with this release._ +=== 9.2.0/known-issues.md === +## 9.2.0 [elastic-9.2.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsTrue_RendersDropdownFormat.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsTrue_RendersDropdownFormat.verified.txt new file mode 100644 index 0000000000..b83d72101f --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/DropdownRenderTests/RenderChangelogs_WithDropdownsTrue_RendersDropdownFormat.verified.txt @@ -0,0 +1,21 @@ +=== 9.2.0/breaking-changes.md === +## 9.2.0 [elastic-9.2.0-breaking-changes] + +::::{dropdown} Breaking API change. +API has been changed to improve performance + +For more information, check [#123](https://github.com/elastic/elastic/pull/123). + +**Impact**
    Existing API calls will fail + +**Action**
    Update your code to use the new API endpoints +:::: +=== 9.2.0/deprecations.md === +## 9.2.0 [elastic-9.2.0-deprecations] +_There are no deprecations associated with this release._ +=== 9.2.0/index.md === +## 9.2.0 [elastic-release-notes-9.2.0] +_There are no new features, enhancements, or fixes associated with this release._ +=== 9.2.0/known-issues.md === +## 9.2.0 [elastic-9.2.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/GfmRenderTests/RenderChangelogs_WithGfmFileType_HandlesDescriptionsAndHideDescriptions.hidden-descriptions.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/GfmRenderTests/RenderChangelogs_WithGfmFileType_HandlesDescriptionsAndHideDescriptions.hidden-descriptions.verified.txt new file mode 100644 index 0000000000..3143e05df2 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/GfmRenderTests/RenderChangelogs_WithGfmFileType_HandlesDescriptionsAndHideDescriptions.hidden-descriptions.verified.txt @@ -0,0 +1,5 @@ +=== 9.2.0/changelog.md === +## 9.2.0 + +### Features and enhancements +* Feature with description. diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_BreakingChange_UsesBlockComments.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_BreakingChange_UsesBlockComments.verified.txt new file mode 100644 index 0000000000..7b4fe5f30d --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_BreakingChange_UsesBlockComments.verified.txt @@ -0,0 +1,25 @@ +=== 9.2.0/breaking-changes.md === +## 9.2.0 [elastic-9.2.0-breaking-changes] + + + +_There are no breaking changes associated with this release._ +=== 9.2.0/deprecations.md === +## 9.2.0 [elastic-9.2.0-deprecations] +_There are no deprecations associated with this release._ +=== 9.2.0/index.md === +## 9.2.0 [elastic-release-notes-9.2.0] +_There are no new features, enhancements, or fixes associated with this release._ +=== 9.2.0/known-issues.md === +## 9.2.0 [elastic-9.2.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_Deprecation_UsesBlockComments.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_Deprecation_UsesBlockComments.verified.txt new file mode 100644 index 0000000000..85d2743f6b --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HideFeaturesTests/RenderChangelogs_WithHideFeatures_Deprecation_UsesBlockComments.verified.txt @@ -0,0 +1,21 @@ +=== 9.2.0/breaking-changes.md === +## 9.2.0 [elastic-9.2.0-breaking-changes] +_There are no breaking changes associated with this release._ +=== 9.2.0/deprecations.md === +## 9.2.0 [elastic-9.2.0-deprecations] + + + +_There are no deprecations associated with this release._ +=== 9.2.0/index.md === +## 9.2.0 [elastic-release-notes-9.2.0] +_There are no new features, enhancements, or fixes associated with this release._ +=== 9.2.0/known-issues.md === +## 9.2.0 [elastic-9.2.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithHighlightedEntries_CreatesHighlightsFile.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithHighlightedEntries_CreatesHighlightsFile.verified.txt new file mode 100644 index 0000000000..26b41f5322 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithHighlightedEntries_CreatesHighlightsFile.verified.txt @@ -0,0 +1,23 @@ +=== 9.3.0/breaking-changes.md === +## 9.3.0 [elastic-9.3.0-breaking-changes] +_There are no breaking changes associated with this release._ +=== 9.3.0/deprecations.md === +## 9.3.0 [elastic-9.3.0-deprecations] +_There are no deprecations associated with this release._ +=== 9.3.0/highlights.md === +## 9.3.0 [elastic-9.3.0-highlights] + +* New Cloud Connect UI for self-managed installations. + Adds Cloud Connect functionality to Kibana + + For more information, check [#100](https://github.com/elastic/elastic/pull/100). + +=== 9.3.0/index.md === +## 9.3.0 [elastic-release-notes-9.3.0] +### Features and enhancements [elastic-9.3.0-features-enhancements] +* New Cloud Connect UI for self-managed installations. [#100](https://github.com/elastic/elastic/pull/100) + + Adds Cloud Connect functionality to Kibana +=== 9.3.0/known-issues.md === +## 9.3.0 [elastic-9.3.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithMultipleHighlightedEntries_GroupsByArea.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithMultipleHighlightedEntries_GroupsByArea.verified.txt new file mode 100644 index 0000000000..97678e73a8 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/HighlightsRenderTests/RenderChangelogs_WithMultipleHighlightedEntries_GroupsByArea.verified.txt @@ -0,0 +1,32 @@ +=== 9.3.0/breaking-changes.md === +## 9.3.0 [elastic-9.3.0-breaking-changes] +_There are no breaking changes associated with this release._ +=== 9.3.0/deprecations.md === +## 9.3.0 [elastic-9.3.0-deprecations] +_There are no deprecations associated with this release._ +=== 9.3.0/highlights.md === +## 9.3.0 [elastic-9.3.0-highlights] + +**Indexing** + +* Indexing highlight. + For more information, check [#200](https://github.com/elastic/elastic/pull/200). + + +**Search** + +* Search highlight. + For more information, check [#100](https://github.com/elastic/elastic/pull/100). + +=== 9.3.0/index.md === +## 9.3.0 [elastic-release-notes-9.3.0] +### Features and enhancements [elastic-9.3.0-features-enhancements] + +**Indexing** +* Indexing highlight. [#200](https://github.com/elastic/elastic/pull/200) + +**Search** +* Search highlight. [#100](https://github.com/elastic/elastic/pull/100) +=== 9.3.0/known-issues.md === +## 9.3.0 [elastic-9.3.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/OutputFormatTests/RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/OutputFormatTests/RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat.verified.txt new file mode 100644 index 0000000000..764c016b25 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/OutputFormatTests/RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat.verified.txt @@ -0,0 +1,35 @@ +=== 9.2.0.asciidoc === +[[release-notes-9.2.0]] +== 9.2.0 + +[[bug-fixes-9.2.0]] +[float] +=== Bug fixes + +* Fixed search bug. {elastic-pull}200[#200] ++ +Fixed a critical search issue + + +[[features-enhancements-9.2.0]] +[float] +=== New features and enhancements + +* New search feature. {elastic-pull}100[#100] ++ +Added new search capabilities + + +[[breaking-changes-9.2.0]] +[float] +=== Breaking changes + +* Breaking API change. {elastic-pull}300[#300] ++ +Changed API endpoint structure + ++ +*Impact:* Users need to update their API calls + ++ +*Action:* Update API client libraries diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/PrivateLinkBugTests/RenderChangelogs_WithMixedPrivateAndPublicLinks_RendersOnlyPublicLinks.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/PrivateLinkBugTests/RenderChangelogs_WithMixedPrivateAndPublicLinks_RendersOnlyPublicLinks.verified.txt new file mode 100644 index 0000000000..243bf80f4e --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/Render/PrivateLinkBugTests/RenderChangelogs_WithMixedPrivateAndPublicLinks_RendersOnlyPublicLinks.verified.txt @@ -0,0 +1,21 @@ +=== 9.3.0/breaking-changes.md === +## 9.3.0 [elasticsearch-9.3.0-breaking-changes] + +* Mixed links breaking change. + This change breaks compatibility. + + For more information, check [#123456](https://github.com/elastic/elasticsearch/pull/123456) [#654321](https://github.com/elastic/elasticsearch/issues/654321). + + **Impact:** Users must update. + + **Action:** Follow upgrade guide. + +=== 9.3.0/deprecations.md === +## 9.3.0 [elasticsearch-9.3.0-deprecations] +_There are no deprecations associated with this release._ +=== 9.3.0/index.md === +## 9.3.0 [elasticsearch-release-notes-9.3.0] +_There are no new features, enhancements, or fixes associated with this release._ +=== 9.3.0/known-issues.md === +## 9.3.0 [elasticsearch-9.3.0-known-issues] +_There are no known issues associated with this release._ diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs index d5cbf183a0..b35d017b65 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs @@ -5,6 +5,8 @@ using AwesomeAssertions; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -56,12 +58,28 @@ public async Task RenderChangelogs_WithValidBundle_CreatesMarkdownFiles() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("## 9.2.0"); - indexContent.Should().Contain("Test feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * Test feature. [#100](https://github.com/elastic/elastic/pull/100) + + This is a test feature + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -141,32 +159,7 @@ public async Task RenderChangelogs_WithMultipleTypes_DoesNotIncludeCrossFileLink Collector.Errors.Should().Be(0); // Verify index.md exists but does NOT contain cross-file links - var indexFile = FileSystem.Path.Join(outputDir, "9.3.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - - indexContent.Should().Contain("## 9.3.0"); - indexContent.Should().Contain("Test feature"); - indexContent.Should().Contain("Highlighted feature"); - - // Verify NO cross-file links are present - indexContent.Should().NotContain("[Highlights]"); - indexContent.Should().NotContain("[Deprecations]"); - indexContent.Should().NotContain("/release-notes/"); - - // Verify individual separated files are still generated - var deprecationsFile = FileSystem.Path.Join(outputDir, "9.3.0", "deprecations.md"); - FileSystem.File.Exists(deprecationsFile).Should().BeTrue(); - var deprecationsContent = await FileSystem.File.ReadAllTextAsync( - deprecationsFile, - TestContext.Current!.Execution.CancellationToken - ); - deprecationsContent.Should().Contain("Deprecated API"); - - var highlightsFile = FileSystem.Path.Join(outputDir, "9.3.0", "highlights.md"); - FileSystem.File.Exists(highlightsFile).Should().BeTrue(); - var highlightsContent = await FileSystem.File.ReadAllTextAsync(highlightsFile, TestContext.Current!.Execution.CancellationToken); - highlightsContent.Should().Contain("Highlighted feature"); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); } [Test] @@ -231,11 +224,26 @@ public async Task RenderChangelogs_WithMultipleBundles_MergesAndRenders() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("First feature"); - indexContent.Should().Contain("Second feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * First feature. [#100](https://github.com/elastic/elastic/pull/100) + * Second feature. [#200](https://github.com/elastic/elastic/pull/200) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs index 3cb15a8cc1..331fda0065 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs @@ -5,6 +5,8 @@ using AwesomeAssertions; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -80,12 +82,30 @@ public async Task MultipleAmendFiles_AllEntriesMergedAndRendered() // Verify all 3 entries were rendered var outputDir = input.Output ?? throw new InvalidOperationException("Output must be set"); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue("output should be rendered"); - var content = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Feature one"); - content.Should().Contain("Feature two"); - content.Should().Contain("Feature three"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * Feature one. [#101](https://github.com/elastic/elastic/pull/101) + * Feature two. [#102](https://github.com/elastic/elastic/pull/102) + + ### Fixes [elastic-9.2.0-fixes] + * Feature three. [#103](https://github.com/elastic/elastic/pull/103) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -128,9 +148,27 @@ public async Task AmendFileEntry_StaleProvenanceChecksum_NoWarning() Collector.Warnings.Should().Be(0, "provenance checksums are not verified at render time"); var outputDir = input.Output ?? throw new InvalidOperationException("Output must be set"); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var content = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Feature two"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * Feature one. [#101](https://github.com/elastic/elastic/pull/101) + * Feature two. [#102](https://github.com/elastic/elastic/pull/102) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -169,9 +207,27 @@ public async Task AmendFileEntry_WithInlineContent_MergedAndRendered() Collector.Warnings.Should().Be(0); var outputDir = input.Output ?? throw new InvalidOperationException("Output must be set"); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var content = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Resolved amend feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * Feature one. [#101](https://github.com/elastic/elastic/pull/101) + * Resolved amend feature. [#200](https://github.com/elastic/elastic/pull/200) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -207,10 +263,26 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var outputDir = input.Output ?? throw new InvalidOperationException("Output must be set"); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var content = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Feature one"); - content.Should().NotContain("Feature two"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * Feature one. [#101](https://github.com/elastic/elastic/pull/101) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } private string CreateBundleDir() diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs index 1d491b60a2..2552fa2514 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs @@ -5,6 +5,8 @@ using AwesomeAssertions; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -71,10 +73,26 @@ public async Task RenderChangelogs_ResolvedBundle_RendersWithoutReadingFiles() Collector.Errors.Should().Be(0); Collector.Warnings.Should().Be(0, "rendering never reads changelog files, so provenance checksums are not verified"); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("My new feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * My new feature. [#100](https://github.com/elastic/elastic/pull/100) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/DescriptionVisibilityTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/DescriptionVisibilityTests.cs index 5df83bb973..4358b6123b 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/DescriptionVisibilityTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/DescriptionVisibilityTests.cs @@ -7,6 +7,8 @@ using Elastic.Changelog.Bundling; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -60,13 +62,28 @@ public async Task RenderChangelogs_DefaultBehavior_IncludesDescriptionsInMarkdow result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexMarkdown = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexMarkdown).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexMarkdown, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("Test feature with description"); - indexContent.Should().Contain("This is a detailed description of the test feature that should be visible by default."); - indexContent.Should().Contain("[#100](https://github.com/elastic/elasticsearch/pull/100)"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elasticsearch-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elasticsearch-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elasticsearch-release-notes-9.2.0] + ### Features and enhancements [elasticsearch-9.2.0-features-enhancements] + * Test feature with description. [#100](https://github.com/elastic/elasticsearch/pull/100) + + This is a detailed description of the test feature that should be visible by default. + === 9.2.0/known-issues.md === + ## 9.2.0 [elasticsearch-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -117,17 +134,26 @@ public async Task RenderChangelogs_NoDescriptionsFlag_HidesDescriptionsInMarkdow result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexMarkdown = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexMarkdown).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexMarkdown, TestContext.Current!.Execution.CancellationToken); - - // Title and links should still be present - indexContent.Should().Contain("Test feature with hidden description"); - indexContent.Should().Contain("[#200](https://github.com/elastic/elasticsearch/pull/200)"); - - // Description should be hidden - indexContent.Should().NotContain("This description should be hidden when --no-descriptions flag is used."); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elasticsearch-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elasticsearch-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elasticsearch-release-notes-9.2.0] + ### Features and enhancements [elasticsearch-9.2.0-features-enhancements] + * Test feature with hidden description. [#200](https://github.com/elastic/elasticsearch/pull/200) + === 9.2.0/known-issues.md === + ## 9.2.0 [elasticsearch-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -181,14 +207,22 @@ public async Task RenderChangelogs_NoDescriptionsFlag_HidesDescriptionsInAsciido var asciidocFiles = FileSystem.Directory.GetFiles(outputDir, "*.asciidoc", SearchOption.AllDirectories); asciidocFiles.Should().HaveCount(1); - var asciidocContent = await FileSystem.File.ReadAllTextAsync(asciidocFiles[0], TestContext.Current!.Execution.CancellationToken); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0.asciidoc === + [[release-notes-9.2.0]] + == 9.2.0 - // Title and links should still be present - asciidocContent.Should().Contain("Test feature for asciidoc"); - asciidocContent.Should().Contain("{es-pull}300[#300]"); + [[features-enhancements-9.2.0]] + [float] + === New features and enhancements - // Description should be hidden - asciidocContent.Should().NotContain("This description should be hidden in asciidoc format when --no-descriptions is used."); + * Test feature for asciidoc. {es-pull}300[#300] + """ + ); } [Test] @@ -242,24 +276,32 @@ public async Task RenderChangelogs_NoDescriptionsFlag_PreservesImpactAndActionFo result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var breakingChangesMarkdown = FileSystem.Path.Join(outputDir, "9.2.0", "breaking-changes.md"); - FileSystem.File.Exists(breakingChangesMarkdown).Should().BeTrue(); - - var breakingChangesContent = await FileSystem.File.ReadAllTextAsync( - breakingChangesMarkdown, - TestContext.Current!.Execution.CancellationToken - ); - - // Title and links should be present - breakingChangesContent.Should().Contain("Breaking change test"); - breakingChangesContent.Should().Contain("[#400](https://github.com/elastic/elasticsearch/pull/400)"); - - // Description should be hidden - breakingChangesContent.Should().NotContain("This description should be hidden but Impact and Action should remain."); - - // Impact and Action should still be visible - breakingChangesContent.Should().Contain("**Impact:** This is the impact section that should always be visible."); - breakingChangesContent.Should().Contain("**Action:** This is the action section that should always be visible."); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elasticsearch-9.2.0-breaking-changes] + + * Breaking change test. + For more information, check [#400](https://github.com/elastic/elasticsearch/pull/400). + + **Impact:** This is the impact section that should always be visible. + + **Action:** This is the action section that should always be visible. + + === 9.2.0/deprecations.md === + ## 9.2.0 [elasticsearch-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elasticsearch-release-notes-9.2.0] + _There are no new features, enhancements, or fixes associated with this release._ + === 9.2.0/known-issues.md === + ## 9.2.0 [elasticsearch-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -313,28 +355,33 @@ public async Task RenderChangelogs_NoDescriptionsFlag_WorksWithDropdownsMode() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var breakingChangesMarkdown = FileSystem.Path.Join(outputDir, "9.2.0", "breaking-changes.md"); - FileSystem.File.Exists(breakingChangesMarkdown).Should().BeTrue(); - - var breakingChangesContent = await FileSystem.File.ReadAllTextAsync( - breakingChangesMarkdown, - TestContext.Current!.Execution.CancellationToken - ); - - // Should have dropdown structure - breakingChangesContent.Should().Contain("::::{dropdown} Breaking change for dropdown test"); - breakingChangesContent.Should().Contain("::::"); - - // Links should be present - breakingChangesContent.Should().Contain("[#500](https://github.com/elastic/elasticsearch/pull/500)"); - - // Description should be hidden (no placeholder text either) - breakingChangesContent.Should().NotContain("This description should be hidden in dropdown mode."); - breakingChangesContent.Should().NotContain("% Describe the functionality that changed"); - - // Impact and Action should still be visible - breakingChangesContent.Should().Contain("**Impact**
    Impact visible in dropdown mode."); - breakingChangesContent.Should().Contain("**Action**
    Action visible in dropdown mode."); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elasticsearch-9.2.0-breaking-changes] + + ::::{dropdown} Breaking change for dropdown test. + + For more information, check [#500](https://github.com/elastic/elasticsearch/pull/500). + + **Impact**
    Impact visible in dropdown mode. + + **Action**
    Action visible in dropdown mode. + :::: + === 9.2.0/deprecations.md === + ## 9.2.0 [elasticsearch-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elasticsearch-release-notes-9.2.0] + _There are no new features, enhancements, or fixes associated with this release._ + === 9.2.0/known-issues.md === + ## 9.2.0 [elasticsearch-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -387,19 +434,28 @@ regardless of the --no-descriptions flag. result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexMarkdown = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexMarkdown).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexMarkdown, TestContext.Current!.Execution.CancellationToken); - - // Bundle description should be visible - indexContent.Should().Contain("This is the bundle-level description that should always be visible"); - indexContent.Should().Contain("regardless of the --no-descriptions flag."); - - // Entry title should be present - indexContent.Should().Contain("Test feature"); - - // Entry description should be hidden - indexContent.Should().NotContain("This entry description should be hidden."); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elasticsearch-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elasticsearch-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elasticsearch-release-notes-9.2.0] + + This is the bundle-level description that should always be visible + regardless of the --no-descriptions flag. + ### Features and enhancements [elasticsearch-9.2.0-features-enhancements] + * Test feature. + === 9.2.0/known-issues.md === + ## 9.2.0 [elasticsearch-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs index c0e14d6d08..14c150ba21 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/DropdownRenderTests.cs @@ -6,6 +6,8 @@ using AwesomeAssertions; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -61,17 +63,7 @@ public async Task RenderChangelogs_WithDropdownsTrue_RendersDropdownFormat() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var breakingChangesFile = FileSystem.Path.Join(outputDir, "9.2.0", "breaking-changes.md"); - FileSystem.File.Exists(breakingChangesFile).Should().BeTrue(); - - var content = await FileSystem.File.ReadAllTextAsync(breakingChangesFile, TestContext.Current!.Execution.CancellationToken); - - // Verify dropdown format - content.Should().Contain("::::{dropdown} Breaking API change"); - content.Should().Contain("API has been changed to improve performance"); - content.Should().Contain("**Impact**
    Existing API calls will fail"); - content.Should().Contain("**Action**
    Update your code to use the new API endpoints"); - content.Should().Contain("::::"); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); } [Test] @@ -126,23 +118,7 @@ public async Task RenderChangelogs_WithDropdownsFalse_RendersFlattendFormat() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var deprecationsFile = FileSystem.Path.Join(outputDir, "9.2.0", "deprecations.md"); - FileSystem.File.Exists(deprecationsFile).Should().BeTrue(); - - var content = await FileSystem.File.ReadAllTextAsync(deprecationsFile, TestContext.Current!.Execution.CancellationToken); - - // Verify flattened format - content.Should().Contain("* Deprecated old API"); - content.Should().Contain("The old API is deprecated"); - content.Should().Contain(" For more information, check"); // Indented for list continuation - content.Should().Contain("#456"); - content.Should().Contain("#789"); - content.Should().Contain(" **Impact:** API will be removed in future version"); // Indented for list continuation - content.Should().Contain(" **Action:** Migrate to the new API"); // Indented for list continuation - - // Should NOT contain dropdown syntax - content.Should().NotContain("::::{dropdown}"); - content.Should().NotContain("::::"); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); } [Test] @@ -195,22 +171,33 @@ public async Task RenderChangelogs_DefaultDropdownsFalse_RendersFlattedFormat() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var knownIssuesFile = FileSystem.Path.Join(outputDir, "9.2.0", "known-issues.md"); - FileSystem.File.Exists(knownIssuesFile).Should().BeTrue(); - - var content = await FileSystem.File.ReadAllTextAsync(knownIssuesFile, TestContext.Current!.Execution.CancellationToken); - - // Verify flattened format (default behavior) - content.Should().Contain("* Known issue with search"); - content.Should().Contain("Search results are incomplete under certain conditions"); - content.Should().Contain(" For more information, check"); // Indented for list continuation - content.Should().Contain("#999"); - content.Should().Contain(" **Impact:** Some search results may be missing"); // Indented for list continuation - content.Should().Contain(" **Action:** Use the workaround provided in the documentation"); // Indented for list continuation - - // Should NOT contain dropdown syntax - content.Should().NotContain("::::{dropdown}"); - content.Should().NotContain("::::"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + _There are no new features, enhancements, or fixes associated with this release._ + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + + * Known issue with search. + Search results are incomplete under certain conditions + + For more information, check [#999](https://github.com/elastic/elastic/pull/999). + + **Impact:** Some search results may be missing + + **Action:** Use the workaround provided in the documentation + """ + ); } [Test] @@ -269,30 +256,10 @@ public async Task RenderChangelogs_HighlightsWithDropdowns_RendersCorrectFormat( result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var highlightsFile = FileSystem.Path.Join(subOutputDir, "9.2.0", "highlights.md"); - FileSystem.File.Exists(highlightsFile).Should().BeTrue(); - - var content = await FileSystem.File.ReadAllTextAsync(highlightsFile, TestContext.Current!.Execution.CancellationToken); - - if (testCase.ExpectDropdown) - { - // Verify dropdown format - content.Should().Contain("::::{dropdown} Amazing new feature"); - content.Should().Contain("This feature revolutionizes how you work with data"); - content.Should().Contain("::::"); - } - else - { - // Verify flattened format - content.Should().Contain("* Amazing new feature"); - content.Should().Contain("This feature revolutionizes how you work with data"); - content.Should().Contain(" For more information, check"); // Indented for list continuation - content.Should().Contain("#555"); - - // Should NOT contain dropdown syntax - content.Should().NotContain("::::{dropdown}"); - content.Should().NotContain("::::"); - } + MockFileSystemDump + .ToText(FileSystem, subOutputDir) + .Should() + .MatchSnapshot(name: testCase.ExpectDropdown ? "dropdown" : "flattened"); } } @@ -354,21 +321,8 @@ public async Task RenderChangelogs_AsciidocFormat_IgnoresDropdownsFlag() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - // Find the AsciiDoc file (path structure differs from Markdown) - var asciidocFiles = FileSystem.Directory.GetFiles(subOutputDir, "*.asciidoc", SearchOption.AllDirectories); - asciidocFiles.Should().HaveCount(1, "should create exactly one AsciiDoc file"); - - var asciidocFile = asciidocFiles[0]; - var content = await FileSystem.File.ReadAllTextAsync(asciidocFile, TestContext.Current!.Execution.CancellationToken); - - // AsciiDoc should always use bullet format regardless of dropdowns flag - content.Should().Contain("* Breaking API change"); - content.Should().Contain("*Impact:* Existing API calls will fail"); - content.Should().Contain("*Action:* Update your code"); - - // Should never contain MyST dropdown syntax - content.Should().NotContain("::::{dropdown}"); - content.Should().NotContain("::::"); + // AsciiDoc always uses the bullet format regardless of the dropdowns flag, and never MyST dropdown syntax + MockFileSystemDump.ToText(FileSystem, subOutputDir).Should().MatchSnapshot(name: dropdowns ? "dropdowns" : "flattened"); } } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs index e3597be206..9b04c752c5 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs @@ -7,6 +7,8 @@ using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -64,8 +66,16 @@ public async Task RenderChangelogs_WithDuplicateFileName_EmitsWarning() // Assert result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Warning && d.Message.Contains("appears in multiple bundles")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning Changelog file '1755268130-duplicate.yaml' appears in multiple bundles: {repo}/Guid_1/bundle1.yaml, {repo}/Guid_1/bundle2.yaml + warning PR '100' appears in multiple bundles: {repo}/Guid_1/bundle1.yaml, {repo}/Guid_1/bundle2.yaml + """ + ); } [Test] @@ -110,13 +120,12 @@ public async Task RenderChangelogs_WithDuplicateFileNameInSameBundle_EmitsWarnin // Assert result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains("appears multiple times in the same bundle") && d.File == - bundleFile + .MatchInlineSnapshot( + "warning {repo}/Guid_1/bundle.yaml Changelog file '1755268130-test-feature.yaml' appears multiple times in the same bundle" ); } @@ -181,7 +190,10 @@ public async Task RenderChangelogs_WithDuplicatePr_EmitsWarning() // Assert result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Warning && d.Message.Contains("appears in multiple bundles")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("warning PR '100' appears in multiple bundles: {repo}/Guid_1/bundle1.yaml, {repo}/Guid_1/bundle2.yaml"); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs index aff7bd8161..53e121b3a7 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs @@ -6,6 +6,8 @@ using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -28,8 +30,11 @@ public async Task RenderChangelogs_WithMissingBundleFile_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Bundle file does not exist")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/Guid_1/nonexistent.yaml Bundle file does not exist"); } [Test] @@ -65,14 +70,12 @@ public async Task RenderChangelogs_EntryWithOnlyFileBlock_EmitsNoInlineContentEr // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains( - "Entry '1755268130-feature.yaml' in bundle has no inline content: title and type are required" - ) && d.Message.Contains("Re-create the bundle with 'changelog bundle'") + .MatchInlineSnapshot( + "error {repo}/Guid_1/bundle.yaml Entry '1755268130-feature.yaml' in bundle has no inline content: title and type are required. Re-create the bundle with 'changelog bundle'." ); } @@ -101,11 +104,12 @@ public async Task RenderChangelogs_WithInvalidBundleStructure_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector - .Diagnostics - .Should() - .Contain(d => d.Message.Contains("No changelog entries to render") || d.Message.Contains("Failed to deserialize")); + // The message carries the parser's stack trace, so a snapshot would pin source line numbers: check the parts that matter + Collector.Errors.Should().Be(1); + var diagnostic = Collector.Diagnostics.Should().ContainSingle().Which; + diagnostic.Severity.Should().Be(Severity.Error); + diagnostic.Message.Should().StartWith("Failed to deserialize bundle file"); + diagnostic.Message.Should().Contain("Unknown key 'invalid_field'"); } [Test] @@ -139,14 +143,12 @@ public async Task RenderChangelogs_EntryMissingProducts_EmitsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains( - "Entry 'Feature without products' in bundle is missing required field: products" - ) + .MatchInlineSnapshot( + "error {repo}/Guid_1/bundle.yaml Entry 'Feature without products' in bundle is missing required field: products" ); } @@ -191,11 +193,26 @@ public async Task RenderChangelogs_WithResolvedEntry_ValidatesAndRenders() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("Resolved feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * Resolved feature. [#100](https://github.com/elastic/elastic/pull/100) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -237,14 +254,12 @@ public async Task RenderChangelogs_WithUnknownType_EmitsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("Unknown type feature") && d.Message.Contains( - "has no inline content: title and type are required" - ) + .MatchInlineSnapshot( + "error {repo}/Guid_1/bundle.yaml Entry 'Unknown type feature' in bundle has no inline content: title and type are required. Re-create the bundle with 'changelog bundle'." ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs index f8e6e292b7..4e10712f40 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/GfmRenderTests.cs @@ -6,6 +6,8 @@ using Elastic.Changelog.Bundling; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -59,24 +61,20 @@ public async Task RenderChangelogs_WithGfmFileType_CreatesSingleGfmFile() Collector.Errors.Should().Be(0); // Should create only changelog.md file, not multiple files like regular markdown - var outputFile = FileSystem.Path.Join(outputDir, "9.2.0", "changelog.md"); - FileSystem.File.Exists(outputFile).Should().BeTrue(); - - // Should NOT create separate files - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var breakingChangesFile = FileSystem.Path.Join(outputDir, "9.2.0", "breaking-changes.md"); - FileSystem.File.Exists(indexFile).Should().BeFalse(); - FileSystem.File.Exists(breakingChangesFile).Should().BeFalse(); - - var content = await FileSystem.File.ReadAllTextAsync(outputFile, TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("## 9.2.0"); - content.Should().Contain("### Features and enhancements"); - content.Should().Contain("Test feature"); - content.Should().Contain("[#100](https://github.com/elastic/elasticsearch/pull/100)"); - - // Should NOT contain anchor brackets in headings - content.Should().NotContain("## 9.2.0 ["); - content.Should().NotContain("### Features and enhancements ["); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/changelog.md === + ## 9.2.0 + + ### Features and enhancements + * Test feature. [#100](https://github.com/elastic/elasticsearch/pull/100) + + This is a test feature + """ + ); } [Test] @@ -171,34 +169,32 @@ public async Task RenderChangelogs_WithGfmFileType_IncludesAllSectionTypes() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var outputChangelogPath = FileSystem.Path.Join(outputDir, "9.2.0", "changelog.md"); - var content = await FileSystem.File.ReadAllTextAsync(outputChangelogPath, TestContext.Current!.Execution.CancellationToken); - - // Should include all section types in the proper order - content.Should().Contain("### Features and enhancements"); - content.Should().Contain("### Breaking changes"); - content.Should().Contain("### Deprecations"); - content.Should().Contain("### Bug fixes"); - content.Should().Contain("### Known issues"); - - // Should contain the entry titles - content.Should().Contain("New feature"); - content.Should().Contain("Breaking change"); - content.Should().Contain("Deprecated API"); - content.Should().Contain("Bug fix"); - content.Should().Contain("Known issue"); - - // Check section ordering (features should come before breaking changes) - var featuresIndex = content.IndexOf("### Features and enhancements", StringComparison.Ordinal); - var breakingIndex = content.IndexOf("### Breaking changes", StringComparison.Ordinal); - var deprecationIndex = content.IndexOf("### Deprecations", StringComparison.Ordinal); - var bugFixIndex = content.IndexOf("### Bug fixes", StringComparison.Ordinal); - var knownIssueIndex = content.IndexOf("### Known issues", StringComparison.Ordinal); - - featuresIndex.Should().BeLessThan(breakingIndex); - breakingIndex.Should().BeLessThan(deprecationIndex); - deprecationIndex.Should().BeLessThan(bugFixIndex); - bugFixIndex.Should().BeLessThan(knownIssueIndex); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/changelog.md === + ## 9.2.0 + + ### Features and enhancements + * New feature. + + ### Breaking changes + * Breaking change. + + ### Deprecations + * Deprecated API. + + ### Bug fixes + * Bug fix. + + ### Known issues + * Known issue. + """ + ); + + // The snapshot shows the section order: features, breaking changes, deprecations, bug fixes, known issues } [Test] @@ -261,21 +257,24 @@ public async Task RenderChangelogs_WithGfmFileType_HandlesHighlights() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var highlightsOutputFile = FileSystem.Path.Join(outputDir, "9.2.0", "changelog.md"); - var content = await FileSystem.File.ReadAllTextAsync(highlightsOutputFile, TestContext.Current!.Execution.CancellationToken); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/changelog.md === + ## 9.2.0 - // Should include highlights section first - content.Should().Contain("### Highlights"); - content.Should().Contain("### Features and enhancements"); + ### Highlights + * Important new feature. - // Highlights should come first - var highlightsIndex = content.IndexOf("### Highlights", StringComparison.Ordinal); - var featuresIndex = content.IndexOf("### Features and enhancements", StringComparison.Ordinal); - highlightsIndex.Should().BeLessThan(featuresIndex); + ### Features and enhancements + * Important new feature. + * Regular feature. + """ + ); - // Both features should be present - content.Should().Contain("Important new feature"); - content.Should().Contain("Regular feature"); + // The snapshot shows the highlights section ahead of the features } [Test] @@ -328,12 +327,21 @@ It spans multiple lines. result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var outputChangelogFile = FileSystem.Path.Join(outputDir, "9.2.0", "changelog.md"); - var content = await FileSystem.File.ReadAllTextAsync(outputChangelogFile, TestContext.Current!.Execution.CancellationToken); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/changelog.md === + ## 9.2.0 + + ### Features and enhancements + * Feature with description. - content.Should().Contain("Feature with description"); - content.Should().Contain("This is a detailed description of the feature."); - content.Should().Contain("It spans multiple lines."); + This is a detailed description of the feature. + It spans multiple lines. + """ + ); // Test with descriptions hidden var outputDir2 = FileSystem.Path.Join(Paths.WorkingDirectoryRoot.FullName, Guid.NewGuid().ToString()); @@ -350,12 +358,8 @@ It spans multiple lines. result2.Should().BeTrue(); Collector.Errors.Should().Be(0); - var changelogFile2 = FileSystem.Path.Join(outputDir2, "9.2.0", "changelog.md"); - var content2 = await FileSystem.File.ReadAllTextAsync(changelogFile2, TestContext.Current!.Execution.CancellationToken); - content2.Should().Contain("Feature with description"); - content2.Should().NotContain("This is a detailed description of the feature."); - content2.Should().NotContain("It spans multiple lines."); + MockFileSystemDump.ToText(FileSystem, outputDir2).Should().MatchSnapshot(name: "hidden-descriptions"); } [Test] @@ -406,12 +410,21 @@ public async Task RenderChangelogs_WithGfmFileType_HandlesBundleDescriptionAndRe result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var finalChangelogFile = FileSystem.Path.Join(outputDir, "9.2.0", "changelog.md"); - var content = await FileSystem.File.ReadAllTextAsync(finalChangelogFile, TestContext.Current!.Execution.CancellationToken); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/changelog.md === + ## 9.2.0 + + _Released: March 15, 2024_ - // Should include the bundle description and release date - content.Should().Contain("## 9.2.0"); - content.Should().Contain("_Released: March 15, 2024_"); - content.Should().Contain("This is a major release with many improvements."); + This is a major release with many improvements. + + ### Features and enhancements + * Simple feature. + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs index 17b22c8fda..2c3cd43fb6 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs @@ -7,6 +7,8 @@ using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -78,25 +80,39 @@ public async Task RenderChangelogs_WithHideFeatures_CommentsOutMatchingEntries() // Assert result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains("Hidden feature") && d.Message.Contains( - "feature:hidden-api" - ) && d.Message.Contains("will be commented out") + .MatchInlineSnapshot( + "warning Changelog entry 'Hidden feature' with feature-id 'feature:hidden-api' will be commented out in markdown output" ); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - // Hidden entry should be commented out with % prefix - indexContent.Should().Contain("% * Hidden feature"); - // Visible entry should not be commented - indexContent.Should().Contain("* Visible feature"); - indexContent.Should().NotContain("% * Visible feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + % * Hidden feature. [#100](https://github.com/elastic/elastic/pull/100) + + % This feature should be hidden + * Visible feature. [#101](https://github.com/elastic/elastic/pull/101) + + This feature should be visible + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -151,19 +167,9 @@ public async Task RenderChangelogs_WithHideFeatures_BreakingChange_UsesBlockComm result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var breakingFile = FileSystem.Path.Join(outputDir, "9.2.0", "breaking-changes.md"); - FileSystem.File.Exists(breakingFile).Should().BeTrue(); - - var breakingContent = await FileSystem.File.ReadAllTextAsync(breakingFile, TestContext.Current!.Execution.CancellationToken); - // Should use block comments - breakingContent.Should().Contain(""); - breakingContent.Should().Contain("Hidden breaking change"); - // Entry should be between comment markers - var commentStart = breakingContent.IndexOf("", StringComparison.Ordinal); - commentStart.Should().BeLessThan(commentEnd); - breakingContent.Substring(commentStart, commentEnd - commentStart).Should().Contain("Hidden breaking change"); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); + + // The hidden entry is wrapped in a block comment } [Test] @@ -216,17 +222,7 @@ public async Task RenderChangelogs_WithHideFeatures_Deprecation_UsesBlockComment result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var deprecationsFile = FileSystem.Path.Join(outputDir, "9.2.0", "deprecations.md"); - FileSystem.File.Exists(deprecationsFile).Should().BeTrue(); - - var deprecationsContent = await FileSystem.File.ReadAllTextAsync( - deprecationsFile, - TestContext.Current!.Execution.CancellationToken - ); - // Should use block comments - deprecationsContent.Should().Contain(""); - deprecationsContent.Should().Contain("Hidden deprecation"); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); } [Test] @@ -305,12 +301,28 @@ public async Task RenderChangelogs_WithHideFeatures_CommaSeparated_CommentsOutMa result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("% * First hidden feature"); - indexContent.Should().Contain("% * Second hidden feature"); - indexContent.Should().Contain("* Visible feature"); - indexContent.Should().NotContain("% * Visible feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + % * First hidden feature. [#100](https://github.com/elastic/elastic/pull/100) + % * Second hidden feature. [#101](https://github.com/elastic/elastic/pull/101) + * Visible feature. [#102](https://github.com/elastic/elastic/pull/102) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -371,9 +383,26 @@ await FileSystem.File.WriteAllTextAsync( result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("% * Hidden feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + % * Hidden feature. [#100](https://github.com/elastic/elastic/pull/100) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -425,10 +454,26 @@ public async Task RenderChangelogs_WithHideFeatures_CaseInsensitive_MatchesFeatu result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - // Should match case-insensitively - indexContent.Should().Contain("% * Hidden feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + % * Hidden feature. [#100](https://github.com/elastic/elastic/pull/100) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -496,13 +541,27 @@ public async Task RenderChangelogs_WithBundleHideFeatures_CommentsOutMatchingEnt result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - // Entry from bundle hide-features should be commented out - indexContent.Should().Contain("% * Hidden from bundle"); - // Visible entry should not be commented - indexContent.Should().Contain("* Visible feature"); - indexContent.Should().NotContain("% * Visible feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + % * Hidden from bundle. [#100](https://github.com/elastic/elastic/pull/100) + * Visible feature. [#101](https://github.com/elastic/elastic/pull/101) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -584,13 +643,27 @@ public async Task RenderChangelogs_MergesCLIAndBundleHideFeatures() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - // Both CLI and bundle hidden entries should be commented - indexContent.Should().Contain("% * Hidden from CLI"); - indexContent.Should().Contain("% * Hidden from bundle"); - // Visible entry should not be commented - indexContent.Should().Contain("* Visible feature"); - indexContent.Should().NotContain("% * Visible feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + % * Hidden from CLI. [#100](https://github.com/elastic/elastic/pull/100) + % * Hidden from bundle. [#101](https://github.com/elastic/elastic/pull/101) + * Visible feature. [#102](https://github.com/elastic/elastic/pull/102) + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs index 56f6f173b8..21a0dd37f5 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs @@ -6,6 +6,8 @@ using Elastic.Changelog.Bundling; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -62,20 +64,7 @@ public async Task RenderChangelogs_WithHighlightedEntries_CreatesHighlightsFile( result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var highlightsFile = FileSystem.Path.Join(outputDir, "9.3.0", "highlights.md"); - FileSystem.File.Exists(highlightsFile).Should().BeTrue("highlights.md should be created when entries have highlight: true"); - - var highlightsContent = await FileSystem.File.ReadAllTextAsync(highlightsFile, TestContext.Current!.Execution.CancellationToken); - highlightsContent.Should().Contain("## 9.3.0"); - highlightsContent.Should().Contain("New Cloud Connect UI"); - highlightsContent.Should().Contain("Adds Cloud Connect functionality"); - - // Verify the entry also appears in index.md - var indexFile = FileSystem.Path.Join(outputDir, "9.3.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("New Cloud Connect UI"); - // Note: Cross-file links like "[Highlights]" have been removed from index.md + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); } [Test] @@ -181,14 +170,33 @@ public async Task RenderChangelogs_WithHighlightedEntries_IncludesHighlightsInAs result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var asciidocFile = FileSystem.Path.Join(outputDir, "9.3.0.asciidoc"); - FileSystem.File.Exists(asciidocFile).Should().BeTrue(); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.3.0.asciidoc === + [[release-notes-9.3.0]] + == 9.3.0 + + [[highlights-9.3.0]] + [float] + === Highlights + + * Highlighted enhancement. {elastic-pull}200[#200] + + + This is a highlighted enhancement + - var asciidocContent = await FileSystem.File.ReadAllTextAsync(asciidocFile, TestContext.Current!.Execution.CancellationToken); - asciidocContent.Should().Contain("[[highlights-9.3.0]]"); - asciidocContent.Should().Contain("=== Highlights"); - asciidocContent.Should().Contain("Highlighted enhancement"); - asciidocContent.Should().Contain("This is a highlighted enhancement"); + [[features-enhancements-9.3.0]] + [float] + === New features and enhancements + + * Highlighted enhancement. {elastic-pull}200[#200] + + + This is a highlighted enhancement + """ + ); } [Test] @@ -259,13 +267,6 @@ public async Task RenderChangelogs_WithMultipleHighlightedEntries_GroupsByArea() result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var highlightsFile = FileSystem.Path.Join(outputDir, "9.3.0", "highlights.md"); - FileSystem.File.Exists(highlightsFile).Should().BeTrue(); - - var highlightsContent = await FileSystem.File.ReadAllTextAsync(highlightsFile, TestContext.Current!.Execution.CancellationToken); - highlightsContent.Should().Contain("Search highlight"); - highlightsContent.Should().Contain("Indexing highlight"); - highlightsContent.Should().Contain("**Search**"); - highlightsContent.Should().Contain("**Indexing**"); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs index 8c832c738b..2867c1b16b 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs @@ -5,6 +5,8 @@ using AwesomeAssertions; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -76,12 +78,28 @@ public async Task RenderChangelogs_WithCustomConfigPath_UsesSpecifiedConfigFile( result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var indexFile = FileSystem.Path.Join(outputDir, "9.2.0", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - // Verify changelog entry is rendered - indexContent.Should().Contain("* Test feature"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0/breaking-changes.md === + ## 9.2.0 [elastic-9.2.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.2.0/deprecations.md === + ## 9.2.0 [elastic-9.2.0-deprecations] + _There are no deprecations associated with this release._ + === 9.2.0/index.md === + ## 9.2.0 [elastic-release-notes-9.2.0] + ### Features and enhancements [elastic-9.2.0-features-enhancements] + * Test feature. [#100](https://github.com/elastic/elastic/pull/100) + + This is a test feature + === 9.2.0/known-issues.md === + ## 9.2.0 [elastic-9.2.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -135,16 +153,24 @@ public async Task RenderChangelogs_WithAsciidocFileType_CreatesSingleAsciidocFil var asciidocFiles = FileSystem.Directory.GetFiles(outputDir, "*.asciidoc", SearchOption.AllDirectories); asciidocFiles.Should().HaveCount(1, "asciidoc render should create a single file"); - var asciidocFile = asciidocFiles[0]; - var asciidocContent = await FileSystem.File.ReadAllTextAsync(asciidocFile, TestContext.Current!.Execution.CancellationToken); - - // Verify valid asciidoc format elements - asciidocContent.Should().Contain("[[release-notes-", "should contain anchor"); - asciidocContent.Should().Contain("== 9.2.0", "should contain section header"); - asciidocContent.Should().Contain("[[features-enhancements-", "should contain features section anchor"); - asciidocContent.Should().Contain("=== New features and enhancements", "should contain features section header"); - asciidocContent.Should().Contain("* Test feature", "should contain changelog entry"); - asciidocContent.Should().Contain("This is a test feature", "should contain description"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.2.0.asciidoc === + [[release-notes-9.2.0]] + == 9.2.0 + + [[features-enhancements-9.2.0]] + [float] + === New features and enhancements + + * Test feature. {elastic-pull}100[#100] + + + This is a test feature + """ + ); // Verify no markdown files are created var markdownFiles = FileSystem.Directory.GetFiles(outputDir, "*.md", SearchOption.AllDirectories); @@ -236,37 +262,9 @@ public async Task RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat( var asciidocFiles = FileSystem.Directory.GetFiles(outputDir, "*.asciidoc", SearchOption.AllDirectories); asciidocFiles.Should().HaveCount(1); - var asciidocContent = await FileSystem.File.ReadAllTextAsync(asciidocFiles[0], TestContext.Current!.Execution.CancellationToken); - - // Verify asciidoc structure - asciidocContent.Should().Contain("[[release-notes-9.2.0]]", "should contain main anchor"); - asciidocContent.Should().Contain("== 9.2.0", "should contain main header"); - - // Verify sections are present with proper asciidoc format - asciidocContent.Should().Contain("[[bug-fixes-9.2.0]]", "should contain bug fixes anchor"); - asciidocContent.Should().Contain("[float]", "should contain float attribute"); - asciidocContent.Should().Contain("=== Bug fixes", "should contain bug fixes header"); - - asciidocContent.Should().Contain("[[features-enhancements-9.2.0]]", "should contain features anchor"); - asciidocContent.Should().Contain("=== New features and enhancements", "should contain features header"); - - asciidocContent.Should().Contain("[[breaking-changes-9.2.0]]", "should contain breaking changes anchor"); - asciidocContent.Should().Contain("=== Breaking changes", "should contain breaking changes header"); - - // Verify entries are formatted correctly - asciidocContent.Should().Contain("* New search feature", "should contain feature entry"); - asciidocContent.Should().Contain("* Fixed search bug", "should contain bug fix entry"); - asciidocContent.Should().Contain("* Breaking API change", "should contain breaking change entry"); - - // Verify asciidoc list format (entries should start with *) - var lines = asciidocContent.Split('\n'); - var entryLines = lines.Where( - l => l.TrimStart().StartsWith("* ", StringComparison.Ordinal) && !l.TrimStart().StartsWith("* *", StringComparison.Ordinal) - ).ToList(); - entryLines.Should().HaveCountGreaterThanOrEqualTo(3, "should have at least 3 changelog entries"); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); + // The snapshot shows the asciidoc list format: entries start with * // Verify no invalid markdown syntax (like ##) is present - asciidocContent.Should().NotContain("##", "should not contain markdown headers"); - asciidocContent.Should().NotContain("###", "should not contain markdown headers"); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/PrivateLinkBugTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/PrivateLinkBugTests.cs index 6c31faa655..315682f74b 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/PrivateLinkBugTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/PrivateLinkBugTests.cs @@ -6,6 +6,8 @@ using Elastic.Changelog.Bundling; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -67,21 +69,32 @@ public async Task RenderChangelogs_WithOnlyPrivateLinks_DoesNotRenderIncompleteF result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var deprecationsFile = FileSystem.Path.Join(outputDir, "9.3.0", "deprecations.md"); - FileSystem.File.Exists(deprecationsFile).Should().BeTrue(); - - var content = await FileSystem.File.ReadAllTextAsync(deprecationsFile, TestContext.Current!.Execution.CancellationToken); - - // Should not contain the incomplete sentence - content.Should().NotContain("For more information, check."); - content.Should().NotContain("check ."); - content.Should().NotContain("check."); - - // Should still contain the entry content - content.Should().Contain("The v1 Costs API has been deprecated"); - content.Should().Contain("This API will be removed"); - content.Should().Contain("Users must update"); - content.Should().Contain("Follow the migration guide"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 9.3.0/breaking-changes.md === + ## 9.3.0 [elasticsearch-9.3.0-breaking-changes] + _There are no breaking changes associated with this release._ + === 9.3.0/deprecations.md === + ## 9.3.0 [elasticsearch-9.3.0-deprecations] + + * The v1 Costs API has been deprecated. Customers should migrate to the v2 Costs API. + This API will be removed in a future version. + + **Impact:** Users must update their integration. + + **Action:** Follow the migration guide. + + === 9.3.0/index.md === + ## 9.3.0 [elasticsearch-release-notes-9.3.0] + _There are no new features, enhancements, or fixes associated with this release._ + === 9.3.0/known-issues.md === + ## 9.3.0 [elasticsearch-9.3.0-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -140,20 +153,6 @@ public async Task RenderChangelogs_WithMixedPrivateAndPublicLinks_RendersOnlyPub result.Should().BeTrue(); Collector.Errors.Should().Be(0); - var breakingChangesFile = FileSystem.Path.Join(outputDir, "9.3.0", "breaking-changes.md"); - FileSystem.File.Exists(breakingChangesFile).Should().BeTrue(); - - var content = await FileSystem.File.ReadAllTextAsync(breakingChangesFile, TestContext.Current!.Execution.CancellationToken); - - // Should contain proper "For more information" with only public links - content.Should().Contain("For more information, check"); - content.Should().Contain("#123456"); - content.Should().Contain("#654321"); - - // Should not contain incomplete sentence - content.Should().NotContain("For more information, check."); - - // Should end properly after the links - content.Should().Contain("654321](https://github.com/elastic/elasticsearch/issues/654321)."); + MockFileSystemDump.ToText(FileSystem, outputDir).Should().MatchSnapshot(); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs index 5d525e9f2f..502c891545 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs @@ -6,6 +6,8 @@ using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Render; @@ -55,14 +57,12 @@ public async Task RenderChangelogs_WithoutTitleAndNoTargets_EmitsWarning() // Assert result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains("No --title option provided") && d.Message.Contains( - "default to 'unknown'" - ) + .MatchInlineSnapshot( + "warning No --title option provided and bundle files do not contain 'target' values. Output folder and markdown titles will default to 'unknown'. Consider using --title to specify a custom title." ); } @@ -111,7 +111,7 @@ public async Task RenderChangelogs_WithTitleAndNoTargets_NoWarning() result.Should().BeTrue(); Collector.Errors.Should().Be(0); // Should not have warning about missing title - Collector.Diagnostics.Should().NotContain(d => d.Severity == Severity.Warning && d.Message.Contains("No --title option provided")); + Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); } [Test] @@ -162,12 +162,26 @@ public async Task RenderChangelogs_WithIsoDateTarget_FormatsDateInHeading() Collector.Errors.Should().Be(0); // Check that output directory uses raw date slug - var indexFile = FileSystem.Path.Join(outputDir, "2026-05-04", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - // Check that heading uses formatted date but anchor uses raw date - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("## May 4, 2026 [elastic-release-notes-2026-05-04]"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 2026-05-04/breaking-changes.md === + ## May 4, 2026 [elastic-2026-05-04-breaking-changes] + _There are no breaking changes associated with this release._ + === 2026-05-04/deprecations.md === + ## May 4, 2026 [elastic-2026-05-04-deprecations] + _There are no deprecations associated with this release._ + === 2026-05-04/index.md === + ## May 4, 2026 [elastic-release-notes-2026-05-04] + ### Features and enhancements [elastic-2026-05-04-features-enhancements] + * Test feature. [#100](https://github.com/elastic/elastic/pull/100) + === 2026-05-04/known-issues.md === + ## May 4, 2026 [elastic-2026-05-04-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -218,12 +232,26 @@ public async Task RenderChangelogs_WithYearMonthTarget_FormatsDateInHeading() Collector.Errors.Should().Be(0); // Check that output directory uses raw date slug - var indexFile = FileSystem.Path.Join(outputDir, "2026-05", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - // Check that heading uses formatted date but anchor uses raw date - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("## May 2026 [elastic-release-notes-2026-05]"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 2026-05/breaking-changes.md === + ## May 2026 [elastic-2026-05-breaking-changes] + _There are no breaking changes associated with this release._ + === 2026-05/deprecations.md === + ## May 2026 [elastic-2026-05-deprecations] + _There are no deprecations associated with this release._ + === 2026-05/index.md === + ## May 2026 [elastic-release-notes-2026-05] + ### Features and enhancements [elastic-2026-05-features-enhancements] + * Test feature. [#100](https://github.com/elastic/elastic/pull/100) + === 2026-05/known-issues.md === + ## May 2026 [elastic-2026-05-known-issues] + _There are no known issues associated with this release._ + """ + ); } [Test] @@ -274,11 +302,25 @@ public async Task RenderChangelogs_WithExplicitDateTitle_DoesNotFormatTitle() Collector.Errors.Should().Be(0); // Check that output directory uses title slug - var indexFile = FileSystem.Path.Join(outputDir, "2026-05-04", "index.md"); - FileSystem.File.Exists(indexFile).Should().BeTrue(); - - // Check that heading uses literal title (no formatting applied) - var indexContent = await FileSystem.File.ReadAllTextAsync(indexFile, TestContext.Current!.Execution.CancellationToken); - indexContent.Should().Contain("## 2026-05-04 [elastic-release-notes-2026-05-04]"); + MockFileSystemDump + .ToText(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 2026-05-04/breaking-changes.md === + ## 2026-05-04 [elastic-2026-05-04-breaking-changes] + _There are no breaking changes associated with this release._ + === 2026-05-04/deprecations.md === + ## 2026-05-04 [elastic-2026-05-04-deprecations] + _There are no deprecations associated with this release._ + === 2026-05-04/index.md === + ## 2026-05-04 [elastic-release-notes-2026-05-04] + ### Features and enhancements [elastic-2026-05-04-features-enhancements] + * Test feature. [#100](https://github.com/elastic/elastic/pull/100) + === 2026-05-04/known-issues.md === + ## 2026-05-04 [elastic-2026-05-04-known-issues] + _There are no known issues associated with this release._ + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/SnapshotSetup.cs b/tests/Elastic.Changelog.Tests/SnapshotSetup.cs new file mode 100644 index 0000000000..cdf60e41b6 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/SnapshotSetup.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Runtime.CompilerServices; +using Nullean.Snapshots; + +namespace Elastic.Changelog.Tests; + +internal static class SnapshotSetup +{ + // Most tests work in a folder named after a fresh Guid, and its path shows up in file paths in the output. + // Number the Guids (Guid_1, Guid_2, ...) so that the same folder reads the same everywhere in one snapshot. + [ModuleInitializer] + internal static void ScrubGuids() => SnapshotDefaults.AddScrubber(Scrubbers.Guids); +} From fea2f975b2d15d8e3c01203c065d3c4ac8c584ec Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:45:53 +0100 Subject: [PATCH 04/12] Changelog create tests: snapshot the changelog files and diagnostics The create tests read the YAML file the service wrote and checked a few Contain fragments (title, type, a PR url...). Each one now snapshots the whole file inline, and the multi-file tests snapshot every entry under its file name. The snapshots are text, not YAML trees, so the quoting and key order of the written YAML stay pinned (several old checks were about exactly that, e.g. feature-id: "feature:foo"). Every changelog file starts with the same 78 lines documenting the fields; ChangelogEntryYaml.WithoutSchemaComments strips them so the snapshots are about the entry, and the Unpack test keeps its check that the block is there. The bundle's release-date is scrubbed. Warnings and errors that were looked up with Diagnostics.Should().Contain are inline diagnostics snapshots, so the exact message and any additional diagnostic is now part of the expectation. The tests became stricter: before accepting, every old Contain and NotContain fragment was checked against the received output. One old check was vacuous: the multiple-PRs-with-failing-fetch test accepted either PR url and only one is written; the snapshot pins the first. Co-Authored-By: Claude Sonnet 5.5 --- .../Changelogs/ChangelogEntryYaml.cs | 42 +++ .../Changelogs/Create/AddReportOptionTests.cs | 35 +- .../Changelogs/Create/BasicInputTests.cs | 65 +++- .../Changelogs/Create/BlockingLabelTests.cs | 4 +- .../Changelogs/Create/ChangelogUnpackTests.cs | 57 +++- .../Create/FlagsAndFeaturesTests.cs | 51 ++- .../Create/GhReleaseExtractionParityTests.cs | 151 ++++++++- .../Changelogs/Create/LabelMappingTests.cs | 230 +++++++++++-- .../Changelogs/Create/NoteCreationTests.cs | 72 +++- .../Changelogs/Create/PrFetchFailureTests.cs | 156 ++++++--- .../Changelogs/Create/PrIntegrationTests.cs | 158 ++++++--- .../Create/ReleaseNoteExtractionTests.cs | 310 +++++++++++++++--- .../Changelogs/Create/ReleaseVersionTests.cs | 17 +- .../Changelogs/Create/TitleProcessingTests.cs | 102 +++++- .../Changelogs/Create/ValidationTests.cs | 56 +++- 15 files changed, 1241 insertions(+), 265 deletions(-) create mode 100644 tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs new file mode 100644 index 0000000000..1e8c7a7505 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs @@ -0,0 +1,42 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions; +using System.Text; + +namespace Elastic.Changelog.Tests.Changelogs; + +public static class ChangelogEntryYaml +{ + /// + /// A changelog file changelog create wrote starts with the same documentation of every field. This is the entry + /// itself: everything after that block, so a snapshot of it is about the entry and does not change with the field docs. + /// Text without the block is returned as it is. + /// + public static string WithoutSchemaComments(this string yaml) + { + var subtype = yaml.IndexOf("# subtype:", StringComparison.Ordinal); + if (subtype < 0) + return yaml; + var end = yaml.IndexOf("\n\n", subtype, StringComparison.Ordinal); + return end < 0 ? yaml : yaml[(end + 2)..]; + } + + /// + /// The entries in , sorted by file name, each under an === name === header and + /// without the field documentation. For the tests that create one file per pull request, whose names are stable. + /// + public static string DumpEntries(IFileSystem fileSystem, string directory, string searchPattern = "*.yaml") + { + var sb = new StringBuilder(); + foreach (var file in fileSystem.Directory.GetFiles(directory, searchPattern).OrderBy(f => f, StringComparer.Ordinal)) + { + var content = fileSystem.File.ReadAllText(file).Replace("\r\n", "\n").WithoutSchemaComments(); + _ = sb.Append("=== ").Append(fileSystem.Path.GetFileName(file)).Append(" ===\n").Append(content); + if (!content.EndsWith('\n')) + _ = sb.Append('\n'); + } + return sb.ToString(); + } +} diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/AddReportOptionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/AddReportOptionTests.cs index 0f65f68e71..bfd2c57a74 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/AddReportOptionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/AddReportOptionTests.cs @@ -8,6 +8,7 @@ using Elastic.Changelog.GitHub; using Elastic.Documentation.Configuration; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -78,17 +79,29 @@ public async Task CreateChangelog_FromPromotionReportHtmlFile_CreatesOneYamlPerP var outputDir = input.Output!; var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().HaveCount(2); - Array.Sort(files, StringComparer.Ordinal); - Path.GetFileName(files[0]).Should().Be("7001.yaml"); - Path.GetFileName(files[1]).Should().Be("7002.yaml"); - - var yaml1 = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yaml1.Should().Contain("title: First from report"); - yaml1.Should().Contain("https://github.com/elastic/elasticsearch/pull/7001"); - - var yaml2 = await FileSystem.File.ReadAllTextAsync(files[1], TestContext.Current!.Execution.CancellationToken); - yaml2.Should().Contain("title: Second from report"); - yaml2.Should().Contain("https://github.com/elastic/elasticsearch/pull/7002"); + ChangelogEntryYaml + .DumpEntries(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 7001.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/7001" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: First from report + === 7002.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/7002" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: Second from report + """ + ); } [Test] diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/BasicInputTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/BasicInputTests.cs index e510368b81..3dc3a71cc9 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/BasicInputTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/BasicInputTests.cs @@ -4,6 +4,8 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -46,12 +48,21 @@ public async Task CreateChangelog_WithBasicInput_CreatesValidYamlFile() files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Add new search feature"); - yamlContent.Should().Contain("type: feature"); - yamlContent.Should().Contain("product: elasticsearch"); - // entries no longer carry a target/version — the origin branch is the address - yamlContent.Should().Contain("lifecycle: ga"); - yamlContent.Should().Contain("description: This is a new search feature"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Add new search feature + description: This is a new search feature + """ + ); } [Test] @@ -92,12 +103,22 @@ public async Task CreateChangelog_WithMultipleProducts_CreatesValidYaml() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("products:"); - // Should contain both products - var elasticsearchIndex = yamlContent.IndexOf("product: elasticsearch", StringComparison.Ordinal); - var kibanaIndex = yamlContent.IndexOf("product: kibana", StringComparison.Ordinal); - elasticsearchIndex.Should().BeGreaterThan(-1); - kibanaIndex.Should().BeGreaterThan(-1); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + - product: kibana + lifecycle: ga + title: Multi-product feature + """ + ); } [Test] @@ -137,9 +158,21 @@ public async Task CreateChangelog_WithBreakingChangeAndSubtype_CreatesValidYaml( FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("type: breaking-change"); - yamlContent.Should().Contain("subtype: api"); - yamlContent.Should().Contain("impact: API clients will need to update"); - yamlContent.Should().Contain("action: Update your API client code"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: breaking-change + subtype: api + products: + - product: elasticsearch + title: Breaking API change + impact: API clients will need to update + action: Update your API client code + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/BlockingLabelTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/BlockingLabelTests.cs index 90bbf3aced..c0e5f8aebe 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/BlockingLabelTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/BlockingLabelTests.cs @@ -6,7 +6,9 @@ using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -372,7 +374,7 @@ public async Task CreateChangelog_WithAllPrsSkippedByCreateRules_ReturnsSuccessW result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Diagnostics.Should().NotContain(d => d.Message.Contains("returned false without emitting errors")); + Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot(""); Collector.Diagnostics.Should().OnlyContain(d => d.Severity != Severity.Error); Logged.Should().Contain(l => l.Contains("entries: exclude")); if (!FileSystem.Directory.Exists(outputDir)) diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ChangelogUnpackTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ChangelogUnpackTests.cs index 6d9ebb0659..205fee77e1 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ChangelogUnpackTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ChangelogUnpackTests.cs @@ -8,6 +8,8 @@ using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -63,17 +65,22 @@ public async Task Unpack_PrEntry_WritesAddNamedFileAndStripsPrivateSentinel() var written = FileSystem.Path.Join(outputDir, "7606.yaml"); FileSystem.File.Exists(written).Should().BeTrue(); var yaml = await FileSystem.File.ReadAllTextAsync(written, TestContext.Current!.Execution.CancellationToken); - yaml.Should().Contain("title: Add ECS user.domain to serverless audit logs"); - yaml.Should().Contain("type: enhancement"); - yaml.Should().Contain("https://github.com/elastic/elasticsearch-serverless/pull/7606"); - yaml.Should().NotContain("# PRIVATE:"); - yaml.Should().NotContain("checksum:"); yaml.Should().Contain("##### Required fields"); - - var parsed = ReleaseNotesSerialization.DeserializeEntry(yaml); - parsed.Title.Should().Be("Add ECS user.domain to serverless audit logs"); - parsed.Type.Should().Be(ChangelogEntryType.Enhancement); - parsed.Prs.Should().ContainSingle().Which.Should().Be("https://github.com/elastic/elasticsearch-serverless/pull/7606"); + yaml + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch-serverless/pull/7606" + type: enhancement + products: + - product: cloud-serverless + areas: + - Security + title: Add ECS user.domain to serverless audit logs + """ + ); ComputeSha1(yaml).Should().NotBe("88dfd443adb87e70e71e7d8cb8417e4de1b91268"); } @@ -109,15 +116,28 @@ public async Task Unpack_VersionedEntryWithoutPrs_WritesNoteFile() Collector.Errors.Should().Be(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Warning && d.Message.Contains("note-alerts-are-not-generated-when-flapping-is-off.yml")); + .MatchInlineSnapshot( + "warning Unpack will write 'note-alerts-are-not-generated-when-flapping-is-off.yml' for provenance file 'note-known-issue.yaml' using changelog note naming." + ); var files = FileSystem.Directory.GetFiles(outputDir, "note-*.yml"); files.Should().ContainSingle(); var yaml = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - var parsed = ReleaseNotesSerialization.DeserializeEntry(yaml); - parsed.Type.Should().Be(ChangelogEntryType.KnownIssue); - parsed.Products.Should().ContainSingle().Which.Versions.Should().Contain("9.3.0"); + yaml + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + type: known-issue + products: + - product: elasticsearch + versions: + - 9.3.0 + title: Alerts are not generated when flapping is off + """ + ); } [Test] @@ -323,8 +343,13 @@ public async Task Unpack_ScrubbedEntryWithoutPrsOrVersions_Fails() var result = await Unpack(bundlePath, configPath, outputDir); result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("scrubbed during changelog upload")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/Guid_1/bundle.yaml Entry '7606.yaml' cannot be unpacked: after stripping private-link sentinels it has no pull-request references and its products have no versions. changelog add requires a PR; changelog note requires product versions. Bundles scrubbed during changelog upload often drop private PRs and issues — unpack the private-side bundle instead." + ); FileSystem.Directory.Exists(outputDir).Should().BeFalse(); } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/FlagsAndFeaturesTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/FlagsAndFeaturesTests.cs index fb6e48e9d4..eef7dd5e06 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/FlagsAndFeaturesTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/FlagsAndFeaturesTests.cs @@ -4,6 +4,8 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -44,7 +46,20 @@ public async Task CreateChangelog_WithHighlightFlag_CreatesValidYaml() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("highlight: true"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Important feature + highlight: true + """ + ); } [Test] @@ -82,7 +97,20 @@ public async Task CreateChangelog_WithFeatureId_CreatesValidYaml() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("feature-id: \"feature:new-search-api\""); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: New feature with flag + feature-id: "feature:new-search-api" + """ + ); } [Test] @@ -120,8 +148,21 @@ public async Task CreateChangelog_WithIssues_CreatesValidYaml() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("issues:"); - yamlContent.Should().Contain("- \"https://github.com/elastic/elasticsearch/issues/123\""); - yamlContent.Should().Contain("- \"https://github.com/elastic/elasticsearch/issues/456\""); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + issues: + - "https://github.com/elastic/elasticsearch/issues/123" + - "https://github.com/elastic/elasticsearch/issues/456" + type: bug-fix + products: + - product: elasticsearch + title: Fix multiple issues + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs index 4fd534db97..1f9bf1a088 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs @@ -3,13 +3,16 @@ // See the LICENSE file in the project root for more information using System.Net; +using System.Text.RegularExpressions; using AwesomeAssertions; using Elastic.Changelog.GitHub; using Elastic.Changelog.GithubRelease; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -139,7 +142,17 @@ public async Task PrWithCheckedInPoolEntry_UsesItVerbatimOverSynthesis() var entryPath = FileSystem.Path.Join(outputDir, "12345.yaml"); FileSystem.File.Exists(entryPath).Should().BeTrue("the pool entry keeps its original file name"); var content = await FileSystem.File.ReadAllTextAsync(entryPath, TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Curated checked-in title", "checked-in entries win over synthesis"); + // the checked-in entry wins over synthesis and is written as it is + content.Should().MatchInlineSnapshot( + """ + title: Curated checked-in title + type: feature + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + """ + ); A.CallTo(() => _prService.FetchPrInfoAsync(A._, A._, A._, A._)).MustNotHaveHappened(); } @@ -175,13 +188,28 @@ public async Task PrWithUnparseablePoolEntry_FallsBackToPrMetadataSynthesis() var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().ContainSingle("a synthesized entry must exist for the PR despite the unusable pool file"); var content = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Fix query parsing edge case"); + content + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: other + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Fix query parsing edge case + """ + ); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains("could not be parsed", StringComparison.Ordinal), - "the parse failure on the checked-in entry must still be surfaced" + .MatchInlineSnapshot( + "error 12345.yaml Checked-in changelog entry '12345.yaml' matches PR #12345 but could not be parsed: 12345.yaml: (Lin: 0, Col: 7, Chr: 7) - (Lin: 0, Col: 7, Chr: 7): Expected a Scalar token but found 'StartSequence'." ); } @@ -246,11 +274,33 @@ public async Task TwoPrsSharingAnUnparseablePoolEntry_BothFallBackToSynthesis() result.Success.Should().BeTrue("both PRs must fall back to PR-metadata synthesis despite the shared unparseable pool file"); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().HaveCount(2, "each PR must synthesize its own entry rather than one silently disappearing"); - var contents = await Task.WhenAll( - files.Select(f => FileSystem.File.ReadAllTextAsync(f, TestContext.Current!.Execution.CancellationToken)) - ); - contents.Should().Contain(c => c.Contains("Fix query parsing edge case", StringComparison.Ordinal)); - contents.Should().Contain(c => c.Contains("Improve indexing throughput", StringComparison.Ordinal)); + MockFileSystemDump + .ToText(FileSystem, outputDir, "*.yaml") + .Should() + .MatchInlineSnapshot( + """ + === 12345-other-fix-query-parsing-edge-case.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: other + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Fix query parsing edge case + === 12346-other-improve-indexing-throughput.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/12346" + type: other + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Improve indexing throughput + """ + ); } [Test] @@ -276,7 +326,23 @@ public async Task PrBodyReleaseNote_BecomesEntryDescription() var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().ContainSingle(); var content = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Queries with trailing wildcards no longer fail."); + content + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: other + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Fix query parsing edge case + description: Queries with trailing wildcards no longer fail. + """ + ); } [Test] @@ -301,7 +367,24 @@ public async Task LinkedIssues_AreCarriedOntoTheEntry() result.Success.Should().BeTrue(); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var content = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("https://github.com/elastic/elasticsearch/issues/999"); + content + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + issues: + - "https://github.com/elastic/elasticsearch/issues/999" + type: other + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Fix query parsing edge case + """ + ); } [Test] @@ -326,8 +409,22 @@ public async Task NoReleaseNoteInBody_FallsBackToTitleOnly() result.Success.Should().BeTrue(); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var content = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - content.Should().NotContain("description:"); - content.Should().Contain("Fix query parsing edge case"); + content + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: other + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Fix query parsing edge case + """ + ); Logged.Should().Contain(l => l.StartsWith("notes:") && l.Contains("no release note")); } @@ -373,8 +470,30 @@ public async Task Bundle_IncludesPoolEntriesWithScrubbedPrsReferences() var bundleFiles = FileSystem.Directory.GetFiles(bundlesDir, "*.yml"); bundleFiles.Should().ContainSingle(); var bundle = await FileSystem.File.ReadAllTextAsync(bundleFiles[0], TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("Curated checked-in title"); - bundle.Should().Contain("name: 12345.yaml"); + // the bundle is stamped with the day it was created + bundle.Should().MatchInlineSnapshot( + options: new SnapshotOptions().Scrub(new Regex(@"release-date: \d{4}-\d{2}-\d{2}"), "release-date: {today}"), + expected: """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + repo: elasticsearch + owner: elastic + release-date: {today} + entries: + - file: + name: 12345.yaml + checksum: 3e23f7fdf6d96be35d49fa93ad546d6165bf292f + type: feature + title: Curated checked-in title + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + """ + ); } private static HttpResponseMessage Json(string body) => diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/LabelMappingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/LabelMappingTests.cs index e7392055b0..04c43895be 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/LabelMappingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/LabelMappingTests.cs @@ -6,7 +6,9 @@ using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -67,7 +69,19 @@ public async Task CreateChangelog_WithPrOptionAndLabelMapping_MapsLabelsToType() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("type: bug-fix"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: bug-fix + products: + - product: elasticsearch + title: Fix memory leak in search + """ + ); } [Test] @@ -214,7 +228,19 @@ public async Task CreateChangelog_WithLabelProductMapping_DerviesProductsFromLab var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("- product: elasticsearch"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Add new search feature + """ + ); } [Test] @@ -269,8 +295,20 @@ public async Task CreateChangelog_WithLabelProductMapping_MultipleMatchingLabels FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("- product: elasticsearch"); - yamlContent.Should().Contain("- product: kibana"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + - product: kibana + title: Cross-product change + """ + ); } [Test] @@ -322,9 +360,21 @@ public async Task CreateChangelog_WithLabelProductMapping_AddsTheImpliedProducts FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("- product: security"); - yamlContent.Should().Contain("- product: kibana"); - yamlContent.Should().Contain("- product: cloud-serverless"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/kibana/pull/12345" + type: feature + products: + - product: security + - product: kibana + - product: cloud-serverless + title: Add a new feature + """ + ); } [Test] @@ -370,11 +420,10 @@ public async Task CreateChangelog_WithTwoMappedProductsAndNoDefault_FailsAPrWith result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains( - "No matching product label found on PR https://github.com/elastic/elasticsearch/pull/12345" - ) && d.Message.Contains(":stack/elasticsearch, :stack/kibana") + .MatchInlineSnapshot( + "error No matching product label found on PR https://github.com/elastic/elasticsearch/pull/12345. Add one of the following labels: :stack/elasticsearch, :stack/kibana. Or pass --products." ); var outputDir = input.Output!; if (!FileSystem.Directory.Exists(outputDir)) @@ -427,11 +476,10 @@ public async Task CreateChangelog_WithTwoMappedProductsAndNoDefault_FailsAnIssue result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains( - "No matching product label found on issue https://github.com/elastic/elasticsearch/issues/12345" - ) + .MatchInlineSnapshot( + "error No matching product label found on issue https://github.com/elastic/elasticsearch/issues/12345. Add one of the following labels: :stack/elasticsearch, :stack/kibana. Or pass --products." ); } @@ -482,8 +530,19 @@ public async Task CreateChangelog_WithLabelProductMapping_ExplicitProductsOverri FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("- product: kibana"); - yamlContent.Should().NotContain("- product: elasticsearch"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: bug-fix + products: + - product: kibana + title: Fix something + """ + ); } [Test] @@ -548,9 +607,22 @@ public async Task CreateChangelog_WithPrOptionAndAreaMapping_MapsLabelsToAreas() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("areas:"); - yamlContent.Should().Contain("- security"); - yamlContent.Should().Contain("- search"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: enhancement + products: + - product: elasticsearch + areas: + - security + - search + title: Add security enhancements + """ + ); } [Test] @@ -626,7 +698,21 @@ public async Task CreateChangelog_WithAreaNameContainingCommas_PreservesAreaName FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("- \"Alerting, connectors, and reporting\""); // Full name, not split + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: bug-fix + products: + - product: elasticsearch + areas: + - "Alerting, connectors, and reporting" + title: Fix alerting issues + """ + ); } [Test] @@ -678,8 +764,22 @@ public async Task CreateChangelog_WithOneLabelMappedToMultipleAreas_AddsAllAreas FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("- Search"); - yamlContent.Should().Contain("- Observability"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: enhancement + products: + - product: elasticsearch + areas: + - Search + - Observability + title: Cross-area change + """ + ); } [Test] @@ -728,7 +828,13 @@ public void MapLabelsToFeatureId_WithMultipleDistinctMatches_WarnsAndReturnsFirs result.Should().Be("feature:foo"); Collector.Warnings.Should().Be(1); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Multiple feature-id values matched")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "warning Multiple feature-id values matched from labels (feature:foo, feature:bar). Using first match 'feature:foo'. Provide --feature-id to override." + ); } [Test] @@ -790,7 +896,20 @@ public async Task CreateChangelog_WithLabelFeatureMapping_DerivesFeatureIdFromLa var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("feature-id: \"feature:new-search-api\""); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Add new search API + feature-id: "feature:new-search-api" + """ + ); } [Test] @@ -833,8 +952,20 @@ public async Task CreateChangelog_WithExplicitFeatureId_IgnoresLabelMapping() var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("feature-id: \"feature:cli-override\""); - yamlContent.Should().NotContain("feature-id: feature:new-search-api"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Add new search API + feature-id: "feature:cli-override" + """ + ); } [Test] @@ -875,12 +1006,30 @@ public async Task CreateChangelog_WithMultipleFeatureLabelMatches_WarnsAndUsesFi result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Multiple feature-id values matched")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "warning Multiple feature-id values matched from labels (feature:foo, feature:bar). Using first match 'feature:foo'. Provide --feature-id to override." + ); var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("feature-id: \"feature:foo\""); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Cross-feature change + feature-id: "feature:foo" + """ + ); } [Test] @@ -922,7 +1071,19 @@ public async Task CreateChangelog_WithNoMatchingFeatureLabels_OmitsFeatureId() var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Split('\n').Should().NotContain(line => line.TrimStart().StartsWith("feature-id:", StringComparison.Ordinal)); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Unrelated feature + """ + ); } private const string RequiredAreaConfig = @@ -962,11 +1123,10 @@ public async Task CreateChangelog_WithRequiredAreaAndNoAreaLabel_FailsAPr() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains( - "No matching area label found on PR https://github.com/elastic/kibana/pull/12345" - ) && d.Message.Contains("--areas") + .MatchInlineSnapshot( + "error No matching area label found on PR https://github.com/elastic/kibana/pull/12345. Add one of the following labels: :Search. Or pass --areas." ); } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/NoteCreationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/NoteCreationTests.cs index 13b4690a26..56051bb112 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/NoteCreationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/NoteCreationTests.cs @@ -6,7 +6,9 @@ using Elastic.Changelog.Creation; using Elastic.Documentation; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -38,8 +40,20 @@ public async Task CreateNote_WithAllRequiredFields_WritesNoteFile() files.Should().HaveCount(1); FileSystem.Path.GetFileName(files[0]).Should().StartWith("note-"); var content = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("Slow rollover fix"); - content.Should().Contain("bug-fix"); + content + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + type: bug-fix + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Slow rollover fix + """ + ); } [Test] @@ -61,8 +75,11 @@ public async Task CreateNote_ProductWithoutTarget_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("elasticsearch") && d.Message.Contains("version")); + .MatchInlineSnapshot( + "error Product 'elasticsearch' must have at least one specific version for 'changelog note'. Use --products 'product version[|version2...] [lifecycle]' with a concrete version (for example, 'elasticsearch 9.3.0|9.4.0 ga' or 'cloud-serverless 2026-05-15')." + ); } [Test] @@ -84,8 +101,11 @@ public async Task CreateNote_EmptyTarget_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("elasticsearch") && d.Message.Contains("version")); + .MatchInlineSnapshot( + "error Product 'elasticsearch' must have at least one specific version for 'changelog note'. Use --products 'product version[|version2...] [lifecycle]' with a concrete version (for example, 'elasticsearch 9.3.0|9.4.0 ga' or 'cloud-serverless 2026-05-15')." + ); } [Test] @@ -159,8 +179,9 @@ public async Task CreateNote_NumericPrWithoutOwnerRepo_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("--owner") && d.Message.Contains("--repo")); + .MatchInlineSnapshot("error When --prs is specified as just a number, both --owner and --repo must be provided"); } [Test] @@ -183,8 +204,9 @@ public async Task CreateNote_NumericIssueWithoutOwnerRepo_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("--owner") && d.Message.Contains("--repo")); + .MatchInlineSnapshot("error When --issues is specified as just a number, both --owner and --repo must be provided"); } [Test] @@ -214,7 +236,22 @@ public async Task CreateNote_WithPrs_AllowedWithoutError() files.Should().HaveCount(1); FileSystem.Path.GetFileName(files[0]).Should().Be("note-known-limitation.yml"); var content = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - content.Should().Contain("pull/12345"); + content + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: known-issue + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + title: Known limitation + """ + ); } [Test] @@ -237,8 +274,9 @@ public async Task CreateNote_MixedNumericAndUrlPrWithoutOwnerRepo_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("--owner") && d.Message.Contains("--repo")); + .MatchInlineSnapshot("error When --prs contains any bare number, both --owner and --repo must be provided"); } [Test] @@ -261,8 +299,9 @@ public async Task CreateNote_MixedNumericAndUrlIssueWithoutOwnerRepo_ReturnsErro result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("--owner") && d.Message.Contains("--repo")); + .MatchInlineSnapshot("error When --issues contains any bare number, both --owner and --repo must be provided"); } [Test] @@ -317,6 +356,19 @@ public async Task CreateNote_InCI_ExtractionDisabledByCli_ClearsCIDescription() var files = FileSystem.Directory.GetFiles(outputDir, "*.yml"); files.Should().HaveCount(1); var content = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - content.Should().NotContain("CI injected description that should be suppressed"); + content + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + type: known-issue + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + title: Known memory leak + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs index 896da76561..151e381764 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs @@ -6,7 +6,9 @@ using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -43,10 +45,19 @@ public async Task CreateChangelog_WithPrOptionAndTitleAndType_SkipsApiFetch() files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Manual title provided"); - yamlContent.Should().Contain("type: feature"); - yamlContent.Should().Contain("prs:"); - yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/12345"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Manual title provided + """ + ); } [Test] @@ -72,10 +83,17 @@ public async Task CreateChangelog_WithPrOptionButPrFetchFails_WithoutTitleAndTyp // Assert result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Failed to fetch PR information") && d.Severity == Severity.Warning); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Title is missing") && d.Severity == Severity.Warning); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Type is missing") && d.Severity == Severity.Warning); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/12345. Generating basic changelog with provided values. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + """ + ); // Verify changelog file was created with commented title/type var outputDir = input.Output ?? FileSystem.Directory.GetCurrentDirectory(); @@ -85,15 +103,20 @@ public async Task CreateChangelog_WithPrOptionButPrFetchFails_WithoutTitleAndTyp files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("# title: # TODO: Add title"); - yamlContent.Should().Contain("# type: # TODO: Add type"); - yamlContent.Should().Contain("prs:"); - yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/12345"); - yamlContent.Should().Contain("products:"); - // Should not contain uncommented title/type - var lines = yamlContent.Split('\n'); - lines.Should().NotContain(l => l.Trim().StartsWith("title:", StringComparison.Ordinal) && !l.Trim().StartsWith('#')); - lines.Should().NotContain(l => l.Trim().StartsWith("type:", StringComparison.Ordinal) && !l.Trim().StartsWith('#')); + // a text snapshot: title and type are written as commented fields, and a YAML snapshot ignores comments + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + # title: # TODO: Add title + # type: # TODO: Add type (e.g., feature, enhancement, bug-fix, breaking-change) + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + products: + - product: elasticsearch + """ + ); } [Test] @@ -138,13 +161,19 @@ public async Task CreateChangelog_WithMultiplePrsButPrFetchFails_GeneratesBasicC // Verify the file contains the provided title/type and at least one PR reference var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Shared title"); - yamlContent.Should().Contain("type: bug-fix"); - // Should reference at least one of the PRs (when filenames collide, the last one wins) - yamlContent.Should().Contain("prs:"); - yamlContent.Should().MatchRegex( - @"(https://github\.com/elastic/elasticsearch/pull/12345|https://github\.com/elastic/elasticsearch/pull/67890)" - ); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: bug-fix + products: + - product: elasticsearch + title: Shared title + """ + ); } [Test] @@ -172,11 +201,20 @@ public async Task CreateChangelog_WithMultiplePrsFetchFails_EmitsAggregateWarnin Collector.Errors.Should().Be(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains("2 of 2") && d.Message.Contains( - "could not be fetched from GitHub" - ) + .MatchInlineSnapshot( + """ + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/12345. Generating basic changelog with provided values. + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/12345. Generating basic changelog with provided values. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/67890. Generating basic changelog with provided values. + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/67890. Generating basic changelog with provided values. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + warning 2 of 2 pull request(s) could not be fetched from GitHub. Their changelogs were created without rules.create label filtering and may be missing title or type, which will cause 'changelog bundle' to fail. Verify GITHUB_TOKEN is set and can access the referenced repositories, then delete the generated changelog files and re-run. + """ ); } @@ -204,8 +242,23 @@ public async Task CreateChangelog_WithMultiplePrsFetchFailsAndStrictFetch_EmitsE // Assert: under --strict-fetch the bulk fetch failure escalates to an error (non-zero exit), // but the best-effort files are still written so they can be inspected. result.Should().BeTrue(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("could not be fetched from GitHub")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/12345. Generating basic changelog with provided values. + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/12345. Generating basic changelog with provided values. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/67890. Generating basic changelog with provided values. + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/67890. Generating basic changelog with provided values. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + error 2 of 2 pull request(s) could not be fetched from GitHub. Their changelogs were created without rules.create label filtering and may be missing title or type, which will cause 'changelog bundle' to fail. Verify GITHUB_TOKEN is set and can access the referenced repositories, then delete the generated changelog files and re-run. + """ + ); } [Test] @@ -234,9 +287,24 @@ public async Task CreateChangelog_WithMultipleIssuesFetchFailsAndStrictFetch_Emi // the caller must use 'changelog note' for issue-only entries. The aggregate // "changelogs were created" fetch summary is omitted because nothing was written. result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("changelog note")); - Collector.Diagnostics.Should().NotContain(d => d.Message.Contains("Their changelogs were created")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning Failed to fetch issue information from GitHub for issue: https://github.com/elastic/elasticsearch/issues/12345. Generating basic changelog with provided values. + warning Failed to fetch issue information from GitHub for issue: https://github.com/elastic/elasticsearch/issues/12345. Generating basic changelog with provided values. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + error Could not derive a PR number from the provided --prs values. Changelog entries must be anchored to a PR number. For items with no PR use 'changelog note' instead. + warning Failed to fetch issue information from GitHub for issue: https://github.com/elastic/elasticsearch/issues/67890. Generating basic changelog with provided values. + warning Failed to fetch issue information from GitHub for issue: https://github.com/elastic/elasticsearch/issues/67890. Generating basic changelog with provided values. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + error Could not derive a PR number from the provided --prs values. Changelog entries must be anchored to a PR number. For items with no PR use 'changelog note' instead. + """ + ); } [Test] @@ -262,8 +330,18 @@ public async Task CreateChangelog_WithSinglePrFetchFailsAndStrictFetch_EmitsErro // Assert result.Should().BeTrue(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("--strict-fetch")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning Failed to fetch PR information from GitHub for PR: https://github.com/elastic/elasticsearch/pull/12345. Generating basic changelog with provided values. + error Could not fetch pull request 'https://github.com/elastic/elasticsearch/pull/12345' from GitHub and --strict-fetch is set. Verify GITHUB_TOKEN is set and can access the repository, then re-run. + warning Title is missing. The changelog will be created with title commented out. Please manually update the title field. + warning Type is missing. The changelog will be created with type commented out. Please manually update the type field. + """ + ); } [Test] @@ -290,13 +368,13 @@ public async Task CreateChangelog_WithVersionedProductsOnBulkAdd_EmitsErrorWitho var result = await service.CreateChangelog(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("does not require or allow product versions")); - Collector.Diagnostics.Should().NotContain(d => d.Message.Contains("could not be fetched")); - Collector.Diagnostics.Should().NotContain(d => d.Message.Contains("Their changelogs were created")); + .MatchInlineSnapshot( + "error Product 'cloud-serverless' specifies version(s) '2026-08-27'. 'changelog add' does not require or allow product versions." + ); A.CallTo( () => MockGitHubService.FetchPrInfoAsync(A._, A._, A._, A._) ).MustNotHaveHappened(); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs index 8f4990aeb8..1db0b92c95 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs @@ -7,6 +7,7 @@ using Elastic.Changelog.GitHub; using Elastic.Documentation.Configuration; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -76,10 +77,20 @@ public async Task CreateChangelog_WithPrOption_FetchesPrInfoAndDerivesTitle() files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().Contain("type: feature"); - yamlContent.Should().Contain("prs:"); - yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/12345"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + """ + ); } [Test] @@ -142,9 +153,20 @@ public async Task CreateChangelog_WithUsePrNumber_CreatesFileWithPrNumberAsFilen fileName.Should().Be("140034.yaml", "the filename should be the PR number when UsePrNumber is true"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("type: bug-fix"); - yamlContent.Should().Contain("prs:"); - yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/140034"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/140034" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: Fix memory leak in search + """ + ); } [Test] @@ -330,13 +352,29 @@ public async Task CreateChangelog_WithMultiplePrs_CreatesOneFilePerPr() var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().HaveCount(2); - var yamlContents = new List(); - foreach (var file in files) - yamlContents.Add(await FileSystem.File.ReadAllTextAsync(file, TestContext.Current!.Execution.CancellationToken)); - - // Verify both PRs were processed - yamlContents.Should().Contain(c => c.Contains("title: First PR feature")); - yamlContents.Should().Contain(c => c.Contains("title: Second PR bug fix")); + ChangelogEntryYaml + .DumpEntries(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 1234.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/1234" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: First PR feature + === 5678.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/5678" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: Second PR bug fix + """ + ); } [Test] @@ -415,14 +453,37 @@ public async Task CreateChangelog_WithPrsFromFile_ProcessesAllPrsFromFile() var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().HaveCount(3); // One file per PR - var yamlContents = new List(); - foreach (var file in files) - yamlContents.Add(await FileSystem.File.ReadAllTextAsync(file, TestContext.Current!.Execution.CancellationToken)); - - // Verify all PRs were processed - yamlContents.Should().Contain(c => c.Contains("title: First PR from file")); - yamlContents.Should().Contain(c => c.Contains("title: Second PR from file")); - yamlContents.Should().Contain(c => c.Contains("title: Third PR from file")); + ChangelogEntryYaml + .DumpEntries(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 1111.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/1111" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: First PR from file + === 2222.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/2222" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: Second PR from file + === 3333.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/3333" + type: enhancement + products: + - product: elasticsearch + lifecycle: ga + title: Third PR from file + """ + ); } [Test] @@ -502,18 +563,29 @@ public async Task CreateChangelog_WithMixedPrsFromFileAndCommaSeparated_Processe var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().HaveCount(2); // One file per PR - var yamlContents = new List(); - foreach (var file in files) - { - var content = await FileSystem.File.ReadAllTextAsync(file, TestContext.Current!.Execution.CancellationToken); - yamlContents.Add(content); - } - - // Verify both PRs were processed - yamlContents.Should().Contain(c => c.Contains("title: PR from comma-separated")); - yamlContents.Should().Contain(c => c.Contains("title: PR from file")); - yamlContents.Should().Contain(c => c.Contains("prs:") && c.Contains("https://github.com/elastic/elasticsearch/pull/1111")); - yamlContents.Should().Contain(c => c.Contains("prs:") && c.Contains("https://github.com/elastic/elasticsearch/pull/2222")); + ChangelogEntryYaml + .DumpEntries(FileSystem, outputDir) + .Should() + .MatchInlineSnapshot( + """ + === 1111.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/1111" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: PR from comma-separated + === 2222.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/2222" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: PR from file + """ + ); } [Test] @@ -566,9 +638,19 @@ public async Task CreateChangelog_WithBareNumberPrAndOwnerRepo_WritesFullUrlInto files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("prs:"); - yamlContent.Should().Contain("https://github.com/elastic/cloud/pull/155500"); - yamlContent.Should().NotMatch("*prs:*\n- '155500'*"); - yamlContent.Should().NotMatch("*prs:*\n- 155500*"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/cloud/pull/155500" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: "Fix upload failures for extensions with >5GiB" + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs index 78347499c5..438fc7ef89 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs @@ -8,6 +8,7 @@ using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -73,8 +74,21 @@ public async Task CreateChangelog_WithExtractReleaseNotes_ShortReleaseNote_UsesP files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().Contain("description: Adds support for new aggregation types"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + description: Adds support for new aggregation types + """ + ); } [Test] @@ -139,9 +153,21 @@ public async Task CreateChangelog_WithExtractReleaseNotes_LongReleaseNote_UsesAs files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - // Commas trigger SharpYaml's more conservative plain-scalar quoting rules. - yamlContent.Should().Contain($"description: \"{longReleaseNote}\""); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + description: "Adds support for new aggregation types including date histogram, range aggregations, and nested aggregations with improved performance" + """ + ); } [Test] @@ -207,10 +233,24 @@ public async Task CreateChangelog_WithExtractReleaseNotes_MultiLineReleaseNote_U files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().Contain("description:"); - yamlContent.Should().Contain("Adds support for new aggregation types"); - yamlContent.Should().Contain("date histogram"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + description: |- + Adds support for new aggregation types + This includes date histogram and range aggregations + with improved performance + """ + ); } [Test] @@ -273,14 +313,20 @@ public async Task CreateChangelog_WithExtractReleaseNotes_NoReleaseNote_UsesPrTi files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - // Description should not be set when no release note is found - if (yamlContent.Contains("description:")) - { - // If description field exists, it should be empty or commented out - var descriptionLine = yamlContent.Split('\n').FirstOrDefault(l => l.Contains("description:")); - descriptionLine.Should().MatchRegex(@"description:\s*(#|$)"); - } + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + """ + ); } [Test] @@ -344,8 +390,21 @@ public async Task CreateChangelog_WithExtractReleaseNotes_ExplicitTitle_TakesPre files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Custom title"); - yamlContent.Should().Contain("description: Adds support for new aggregation types"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Custom title + description: Adds support for new aggregation types + """ + ); } [Test] @@ -411,15 +470,27 @@ public async Task CreateChangelog_WithExtractReleaseNotes_ExplicitDescription_Ta files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("description: Custom description"); - yamlContent.Should().NotContain(longReleaseNote); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + description: Custom description + """ + ); } [Test] public async Task CreateChangelog_WhenExtractNotSpecifiedByCli_UsesConfigExtractReleaseNotes() { // When CLI does not pass --no-extract-release-notes, config extract.release_notes applies. - // Config has release_notes: false, so PR title is used instead of extracted release note. var prInfo = new GitHubPrInfo { @@ -478,8 +549,20 @@ public async Task CreateChangelog_WhenExtractNotSpecifiedByCli_UsesConfigExtract files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().NotContain("Adds support for new aggregation types"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + """ + ); } [Test] @@ -548,8 +631,23 @@ public async Task CreateChangelog_InCI_ExtractionEnabledByConfig_PreservesCIDesc files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().Contain("Adds support for new aggregation types"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + description: Adds support for new aggregation types + impact: '' + action: '' + """ + ); } [Test] @@ -619,8 +717,22 @@ public async Task CreateChangelog_InCI_ExtractionDisabledByCli_ClearsCIDescripti files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().NotContain("Adds support for new aggregation types"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + impact: '' + action: '' + """ + ); } [Test] @@ -704,13 +816,29 @@ public async Task CreateChangelog_InCI_MultiplePrs_ExtractionDisabled_ClearsCIDe var files = FileSystem.Directory.GetFiles(output, "*.yaml"); files.Should().HaveCount(2); - foreach (var file in files) - { - var yamlContent = await FileSystem.File.ReadAllTextAsync(file, TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().NotContain("Leaked description"); - yamlContent.Should().NotContain("First extracted note"); - yamlContent.Should().NotContain("Second extracted note"); - } + ChangelogEntryYaml + .DumpEntries(FileSystem, output) + .Should() + .MatchInlineSnapshot( + """ + === 100.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: First PR title + === 200.yaml === + prs: + - "https://github.com/elastic/elasticsearch/pull/200" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: Second PR title + """ + ); } [Test] @@ -782,8 +910,22 @@ public async Task CreateChangelog_InCI_ExtractionDisabledByConfig_ClearsCIDescri files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().NotContain("Adds support for new aggregation types"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + impact: '' + action: '' + """ + ); } [Test] @@ -852,8 +994,23 @@ public async Task CreateChangelog_InCI_CliEnablesExtraction_OverridesConfigFalse files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Implement new aggregation API"); - yamlContent.Should().Contain("Adds support for new aggregation types"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + description: Adds support for new aggregation types + impact: '' + action: '' + """ + ); } [Test] @@ -921,8 +1078,23 @@ public async Task CreateChangelog_InCI_ExtractionDisabled_ExplicitCliDescription files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("description: My explicit description"); - yamlContent.Should().NotContain("CI extracted note"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Implement new aggregation API + description: My explicit description + impact: '' + action: '' + """ + ); } [Test] @@ -987,9 +1159,22 @@ public async Task CreateChangelog_InCI_ExtractionDisabledByConfig_ClearsCIImpact var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml"); files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().NotContain("Leaked impact"); - yamlContent.Should().NotContain("Leaked action"); - yamlContent.Should().NotContain("PRBody"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: breaking-change + products: + - product: elasticsearch + lifecycle: ga + title: Remove a parameter + impact: '' + action: '' + """ + ); } [Test] @@ -1051,9 +1236,22 @@ public async Task CreateChangelog_InCI_ExtractionDisabledByConfig_KeepsEntryFile var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml"); files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("Entry impact"); - yamlContent.Should().Contain("Entry action"); - yamlContent.Should().NotContain("Scraped impact"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: breaking-change + products: + - product: elasticsearch + lifecycle: ga + title: Remove a parameter + impact: Entry impact + action: Entry action + """ + ); } [Test] @@ -1107,7 +1305,21 @@ public async Task CreateChangelog_BreakingTypeSupplied_ExtractionEnabled_Fetches var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml"); files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("PRBody impact."); - yamlContent.Should().Contain("PRBody action."); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: breaking-change + products: + - product: elasticsearch + lifecycle: ga + title: Remove a parameter + impact: PRBody impact. + action: PRBody action. + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs index 0d472ff005..c32ddd003e 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseVersionTests.cs @@ -9,7 +9,9 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -103,7 +105,11 @@ public async Task ReleaseVersion_WithNoMatchingPrs_EmitsWarningAndSucceeds() // Assert result.Success.Should().BeTrue(); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("No PR") && d.Severity == Severity.Warning); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("warning No PRs found in commit range for this release. No changelogs will be created."); } // ----------------------------------------------------------------------- @@ -269,8 +275,13 @@ public async Task ReleaseVersion_UnknownRepo_ReturnsError() // Assert result.Success.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("unknown-repo")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Could not find a product for repository 'unknown-repo' in products.yml. Add a product to config/products.yml whose ID is 'unknown-repo', or set 'repository: unknown-repo' on one or more existing products." + ); } // ----------------------------------------------------------------------- diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/TitleProcessingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/TitleProcessingTests.cs index 16fa47d450..562d0f51a0 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/TitleProcessingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/TitleProcessingTests.cs @@ -5,7 +5,9 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -66,9 +68,20 @@ public async Task CreateChangelog_WithStripTitlePrefix_RemovesSquareBracketsAndC files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Update Vector Similarity To Support BFLOAT16"); - yamlContent.Should().NotContain("[ES|QL]"); - yamlContent.Should().NotContain("[ES|QL]:"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Update Vector Similarity To Support BFLOAT16 + """ + ); } [Test] @@ -126,8 +139,20 @@ public async Task CreateChangelog_WithStripTitlePrefix_RemovesSquareBracketsWith files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Improve authentication handling"); - yamlContent.Should().NotContain("[Security]"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + lifecycle: ga + title: Improve authentication handling + """ + ); } [Test] @@ -185,9 +210,20 @@ public async Task CreateChangelog_WithStripTitlePrefix_RemovesMultipleSquareBrac files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Fix filtering by multiline string fields"); - yamlContent.Should().NotContain("[Discover]"); - yamlContent.Should().NotContain("[ESQL]"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: bug-fix + products: + - product: elasticsearch + lifecycle: ga + title: Fix filtering by multiline string fields + """ + ); } [Test] @@ -236,8 +272,20 @@ public async Task CreateChangelog_WithStripTitlePrefix_StripsKibanaStyleTeamHyph files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Enable cases numerical id service"); - yamlContent.Should().NotContain("title: '- Enable"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/kibana/pull/238555" + type: feature + products: + - product: kibana + lifecycle: ga + title: Enable cases numerical id service + """ + ); } [Test] @@ -278,8 +326,19 @@ public async Task CreateChangelog_WithExplicitTitle_OverridesPrTitle() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("title: Custom Title Override"); - yamlContent.Should().NotContain("PR Title from GitHub"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: elasticsearch + title: Custom Title Override + """ + ); } [Test] @@ -317,8 +376,21 @@ public async Task CreateChangelog_WithIssues_CreatesValidYaml() FileSystem.Directory.CreateDirectory(outputDir); var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("issues:"); - yamlContent.Should().Contain("- \"https://github.com/elastic/elasticsearch/issues/123\""); - yamlContent.Should().Contain("- \"https://github.com/elastic/elasticsearch/issues/456\""); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + issues: + - "https://github.com/elastic/elasticsearch/issues/123" + - "https://github.com/elastic/elasticsearch/issues/456" + type: bug-fix + products: + - product: elasticsearch + title: Fix multiple issues + """ + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ValidationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ValidationTests.cs index b48a727adc..8806ddb2ff 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ValidationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ValidationTests.cs @@ -5,7 +5,9 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs.Create; @@ -50,8 +52,13 @@ public async Task CreateChangelog_WithPrOptionButNoLabelMapping_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Cannot derive type from PR")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Cannot derive type from PR https://github.com/elastic/elasticsearch/pull/12345 labels: no type mapping configured in changelog.yml. Please provide --type or configure pivot.types in changelog.yml." + ); } [Test] @@ -73,8 +80,13 @@ public async Task CreateChangelog_WithInvalidProduct_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("is not in the list of available products")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Product 'invalid-product' is not in the list of available products from config/products.yml. Available products: cloud-hosted, cloud-serverless, elasticsearch, kibana, security" + ); } [Test] @@ -96,8 +108,13 @@ public async Task CreateChangelog_WithInvalidType_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("is not in the list of available types")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Type 'invalid-type' is not in the list of available types. Available types: invalid, feature, enhancement, security, bug-fix, breaking-change, deprecation, known-issue, docs, regression, other" + ); } [Test] @@ -140,11 +157,16 @@ public async Task CreateChangelog_WithInvalidProductInAddBlockers_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Message.Contains("invalid-product") && d.Message.Contains("not in available products")); + .MatchInlineSnapshot( + """ + error {repo}/Guid_1/changelog.yml rules.create.products: 'invalid-product' not in available products. Available: cloud-hosted, cloud-serverless, elasticsearch, kibana, security + error Failed to load changelog configuration + """ + ); } [Test] @@ -182,7 +204,20 @@ public async Task CreateChangelog_WithRepoMatchingKnownProduct_InfersProduct() var files = FileSystem.Directory.GetFiles(outputDir, "*.yaml"); files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); - yamlContent.Should().Contain("- product: kibana"); + yamlContent + .WithoutSchemaComments() + .Should() + .MatchInlineSnapshot( + """ + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + type: feature + products: + - product: kibana + lifecycle: ga + title: Test inference from repo name + """ + ); } [Test] @@ -205,8 +240,7 @@ public async Task CreateChangelog_WithUnknownRepo_ReturnsProductError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("product")); + Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot("error At least one product is required"); } [Test] From 915bc5d6e34142c97c768d70fdf7ce9dcd52490f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:51:56 +0100 Subject: [PATCH 05/12] Changelog bundle tests: snapshot the bundle files and diagnostics The bundling tests read the bundle (or amend sidecar) the service wrote and checked a few Contain fragments, or split the text to count entries. Each one now snapshots the whole file, inline up to 35 lines and as a verified file beyond that. Errors and warnings looked up with Diagnostics.Should().Contain(...) are inline diagnostics snapshots. The tests became stricter: before accepting, every old Contain and NotContain fragment was checked against the received output (top-level product checks that applied only to the part before entries: still hold, the snapshot shows the entries with their own products). The snapshots also pin the deprecation and repository-name warnings several profile tests used to ignore. A bundle is stamped with the day it was created, so the project scrubs a release-date that equals today (a release-date a test sets stays visible). Also two comments describing the create tests are put back. The two checksum comparison tests (two bundles from the same data) and the calls that need two outputs in one test are left as they were. Co-Authored-By: Claude Sonnet 5.5 --- ...eProducts_IncludesAllProducts.verified.txt | 43 + ...llback_WritesBundleWithGitRef.verified.txt | 44 + .../Changelogs/BundleAmendCdnParentTests.cs | 89 +- .../Changelogs/BundleAmendCdnTests.cs | 15 +- .../Changelogs/BundleAmendTests.cs | 130 +- .../Changelogs/BundleCdnSourcingTests.cs | 262 +- .../Changelogs/BundleChangelogsTests.cs | 2344 ++++++++++++++--- .../Changelogs/BundleDescriptionInputTests.cs | 31 +- .../Changelogs/BundleFilesFilterTests.cs | 209 +- .../Changelogs/BundleGitRefTests.cs | 210 +- .../Changelogs/BundleOutputConventionTests.cs | 65 +- .../Changelogs/BundlePlanTests.cs | 16 +- .../BundleProfileGitHubReleaseTests.cs | 9 +- .../Changelogs/BundleReleaseVersionTests.cs | 64 +- .../Changelogs/CloudProfileFixtureTests.cs | 35 +- .../Create/GhReleaseExtractionParityTests.cs | 5 +- .../Create/ReleaseNoteExtractionTests.cs | 2 + .../Changelogs/UnifiedProductBundlerTests.cs | 33 +- .../Elastic.Changelog.Tests/SnapshotSetup.cs | 24 +- 19 files changed, 3087 insertions(+), 543 deletions(-) create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt create mode 100644 tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt new file mode 100644 index 0000000000..8a6e72facc --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt @@ -0,0 +1,43 @@ +products: + - product: elasticsearch + target: 9.2.0 + - product: kibana + target: 9.2.0 +release-date: {today} +entries: + - file: + name: 1755268130-elasticsearch.yaml + checksum: 953e2ec8fd6621dd6344173002e8f3911139b7bd + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-kibana.yaml + checksum: 5567d7bf362ba212f7ca11d9bb7df2eb47f365e7 + type: feature + title: Kibana feature + products: + - product: kibana + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/kibana/pull/200" + - file: + name: 1755268150-multi-product.yaml + checksum: ecd19ea9fd25964bc6d47e19ac9a150d80354485 + type: feature + title: Multi-product feature + products: + - product: elasticsearch + versions: + - 9.2.0 + - product: kibana + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/300" diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt new file mode 100644 index 0000000000..297055418f --- /dev/null +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt @@ -0,0 +1,44 @@ +products: + - product: cloud-hosted + target: 2026-08-13 + repo: widget + owner: elastic +release-date: {today} +git_ref: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 +entries: + - file: + name: 100.yaml + checksum: c2b6527c04530bf3cfffb127578481f3a1131e54 + type: feature + title: Faster hosted search + products: + - product: cloud-hosted + versions: + - 2026-08-13 + lifecycle: ga + - file: + name: sturdier-snapshots.yaml + checksum: 137a6ba241fd79b77b8323a3390121023d97c9c2 + type: bug-fix + title: Sturdier snapshots + products: + - product: cloud-hosted + versions: + - 2026-08-13 + lifecycle: ga + prs: + - "https://github.com/elastic/widget/pull/200" + - file: + name: 300.yaml + checksum: 89cf3f1b74b4c89f031f1cdf1486e323a6191ed2 + type: feature + title: Sharper autocomplete + products: + - product: cloud-hosted + versions: + - 2026-08-13 + description: Autocomplete now ranks recent indices first. + impact: Saved searches that relied on alphabetical order change position. + action: Pin the indices you need at the top. + prs: + - "https://github.com/elastic/widget/pull/300" diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs index 9e9b6dbc27..5bb0f85ebc 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs @@ -8,6 +8,8 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -58,8 +60,25 @@ public async Task Amend_CdnParent_WritesAmend1UnderOutput() var amendPath = FileSystem.Path.Join(outputDir, "9.3.0.amend-1.yaml"); FileSystem.File.Exists(amendPath).Should().BeTrue(); var amend = await FileSystem.File.ReadAllTextAsync(amendPath, TestContext.Current!.Execution.CancellationToken); - amend.Should().Contain("title: Late addition"); - amend.Should().Contain("name: late.yaml"); + amend.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + repo: elasticsearch + owner: elastic + entries: + - file: + name: late.yaml + checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + type: enhancement + title: Late addition + products: + - product: elasticsearch + versions: + - 9.3.0 + """ + ); } [Test] @@ -128,7 +147,13 @@ public async Task Amend_LocalParent_StillWritesBesideParent_IgnoringOutput() handler.RequestedPaths.Should().BeEmpty("a local parent with --force-local must not reach the CDN"); FileSystem.File.Exists(FileSystem.Path.Join(FileSystem.Path.GetDirectoryName(bundlePath), "bundle.amend-1.yaml")).Should().BeTrue(); FileSystem.Directory.GetFiles(outputDir).Should().BeEmpty(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Warning && d.Message.Contains("--output is ignored")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "warning {repo}/Guid_1 --output is ignored for a local parent bundle; the amend file is written next to the parent." + ); } [Test] @@ -150,7 +175,13 @@ public async Task Amend_LocalParent_MissingFile_FailsWithoutWriting() ); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("does not exist")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/Guid_1/missing.yaml Bundle file does not exist. Current directory: {repo}. A CDN parent must look like /bundle/{product}/{file}.yaml (or an http(s) URL with that path)." + ); FileSystem.Directory.GetFiles(outputDir).Should().BeEmpty(); } @@ -168,7 +199,13 @@ public async Task Amend_CdnParent_UnknownBundleFile_FailsWithoutWriting() ); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("not listed")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error /bundle/elasticsearch/missing.yaml Bundle 'missing.yaml' is not listed in the changelog registry for product 'elasticsearch'." + ); FileSystem.Directory.GetFiles(outputDir).Should().BeEmpty(); } @@ -191,7 +228,13 @@ public async Task Amend_CdnParent_AmendSidecarAsParent_FailsWithoutWriting() ); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("amend sidecar")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error /bundle/elasticsearch/9.3.0.amend-1.yaml The bundle path is an amend sidecar; pass the parent bundle instead." + ); handler.RequestedPaths.Should().BeEmpty(); FileSystem.Directory.GetFiles(outputDir).Should().BeEmpty(); } @@ -252,7 +295,25 @@ public async Task Amend_CdnParent_YmlExtension_IgnoresMismatchedExtensionSidecar var amendPath = FileSystem.Path.Join(outputDir, "9.3.0.amend-1.yml"); FileSystem.File.Exists(amendPath).Should().BeTrue(); var amend = await FileSystem.File.ReadAllTextAsync(amendPath, TestContext.Current!.Execution.CancellationToken); - amend.Should().NotContain("unrelated.yaml", "the mismatched-extension sidecar must not be merged in"); + amend.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + repo: elasticsearch + owner: elastic + entries: + - file: + name: late.yaml + checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + type: enhancement + title: Late addition + products: + - product: elasticsearch + versions: + - 9.3.0 + """ + ); } [Test] @@ -308,7 +369,11 @@ await FileSystem.File.WriteAllTextAsync( ); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("symlink")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/Guid_1/bundle.yaml The parent bundle must not be a symlink."); handler.RequestedPaths.Should().BeEmpty(); } @@ -331,7 +396,13 @@ public async Task Amend_BadPathShape_FailsWithoutWriting() ); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("/bundle/{product}/{file}.yaml")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error /changelog/elastic/elasticsearch/main/9.3.0.yaml Bundle file does not exist. Current directory: {repo}. A CDN parent must look like /bundle/{product}/{file}.yaml (or an http(s) URL with that path)." + ); handler.RequestedPaths.Should().BeEmpty(); FileSystem.Directory.GetFiles(outputDir).Should().BeEmpty(); } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs index 646f6965f1..73afd95056 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs @@ -8,6 +8,8 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -102,9 +104,10 @@ public async Task Amend_Add_CdnPoolMissingName_Fails() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("never-uploaded.yaml") && d.Message.Contains("could not be fetched") + .MatchInlineSnapshot( + "error Changelog entry 'never-uploaded.yaml' for 'elastic/elasticsearch/main' could not be fetched from https://d10xozp44eyz7q.cloudfront.net/changelog/elastic/elasticsearch/main/never-uploaded.yaml: 404 Not Found — entry does not exist in the pool. Ensure the entry was uploaded (changelog upload), or pass --force-local / --directory to bundle local files instead." ); } @@ -145,7 +148,13 @@ public async Task Amend_Remove_CdnChecksumMismatch_WithoutForce_Fails() ); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("different checksum")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error existing.yaml Bundle contains 'existing.yaml' but with a different checksum than the sourced changelog. Re-create the bundle or use --force to remove by file name only." + ); } [Test] diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs index df631d5128..42c72e708d 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs @@ -8,6 +8,7 @@ using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -115,11 +116,26 @@ public async Task AmendBundle_AddFile_WritesResolvedEntryWithProvenance() var amendContent = await FileSystem.File.ReadAllTextAsync(amendFiles[0], ct); // Amend file entries are always resolved (Title/Type inlined) with file provenance kept - amendContent.Should().Contain("title: New amended feature"); - amendContent.Should().Contain("type: enhancement"); - amendContent.Should().Contain("description: A new enhancement added via amend"); - amendContent.Should().Contain("name: 1755268200-new-feature.yaml"); - amendContent.Should().Contain("checksum:"); + amendContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: 1755268200-new-feature.yaml + checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + type: enhancement + title: New amended feature + products: + - product: elasticsearch + versions: + - 9.2.0 + description: A new enhancement added via amend + prs: + - "200" + """ + ); } [Test] @@ -142,10 +158,17 @@ public async Task AmendBundle_RemoveFromParent_CreatesExcludeEntries() amendFiles.Should().HaveCount(1); var amendContent = await FileSystem.File.ReadAllTextAsync(amendFiles[0], ct); - amendContent.Should().Contain("exclude-entries:"); - amendContent.Should().Contain("name: 1755268130-existing.yaml"); - // The parent's products are copied first; the exclusions follow. - amendContent.TrimStart().Should().StartWith("products:"); + amendContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + exclude-entries: + - file: + name: 1755268130-existing.yaml + checksum: b9292cbbc010ccd0c8629091e0fa750814e62c3c + """ + ); } [Test] @@ -171,8 +194,17 @@ public async Task AmendBundle_RemoveAfterAdd_ExcludesAmendedEntry() amendFiles.Should().HaveCount(2); var removeAmendContent = await FileSystem.File.ReadAllTextAsync(amendFiles[1], ct); - removeAmendContent.Should().Contain("exclude-entries:"); - removeAmendContent.Should().Contain("name: 1755268200-new-feature.yaml"); + removeAmendContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + exclude-entries: + - file: + name: 1755268200-new-feature.yaml + checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + """ + ); } [Test] @@ -231,10 +263,30 @@ public async Task AmendBundle_RemoveAndAdd_InSingleAmendFile() amendFiles.Should().HaveCount(1); var amendContent = await FileSystem.File.ReadAllTextAsync(amendFiles[0], ct); - amendContent.Should().Contain("exclude-entries:"); - amendContent.Should().Contain("name: 1755268130-existing.yaml"); - amendContent.Should().Contain("entries:"); - amendContent.Should().Contain("name: 1755268200-new-feature.yaml"); + amendContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: 1755268200-new-feature.yaml + checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + type: enhancement + title: New amended feature + products: + - product: elasticsearch + versions: + - 9.2.0 + description: A new enhancement added via amend + prs: + - "200" + exclude-entries: + - file: + name: 1755268130-existing.yaml + checksum: b9292cbbc010ccd0c8629091e0fa750814e62c3c + """ + ); } /// @@ -306,21 +358,29 @@ public async Task AmendBundle_Add_CopiesParentProductsIntoAmend() amendFiles.Should().HaveCount(1); var amendContent = await FileSystem.File.ReadAllTextAsync(amendFiles[0], ct); - var amend = ReleaseNotesSerialization.DeserializeBundle(amendContent); - - // The amend must be self-contained: complete parent products, including target, repo, and owner. - amend.Products.Should().ContainSingle(); - amend - .Products[0] - .Should() - .BeEquivalentTo(new BundledProduct - { - ProductId = "elasticsearch", - Target = "9.3.0", - Lifecycle = Lifecycle.Ga, - Repo = "elasticsearch", - Owner = "elastic" - }); + amendContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + owner: elastic + entries: + - file: + name: 1755268200-new-feature.yaml + checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + type: enhancement + title: New amended feature + products: + - product: elasticsearch + versions: + - 9.2.0 + description: A new enhancement added via amend + prs: + - "200" + """ + ); } [Test] @@ -388,8 +448,14 @@ public async Task AmendBundle_ClearDescription_WritesEmptyDescription() amendCollector.Errors.Should().Be(0); var yaml = await FileSystem.File.ReadAllTextAsync(ChangelogBundleAmendService.DiscoverAmendFiles(FileSystem, bundlePath)[0], ct); - yaml.Should().Contain("description:"); - ReleaseNotesSerialization.DeserializeBundle(yaml).Description.Should().BeEmpty(); + yaml.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + description: '' + """ + ); } [Test] diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs index 58f52d5528..d7d7dea7cb 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs @@ -10,7 +10,9 @@ using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -101,8 +103,41 @@ public async Task OptionMode_RepoResolvable_ProbesEntriesByPrNumber() handler.RequestedPaths.Should().NotContain(p => p.Contains("registry.json", StringComparison.Ordinal)); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("Alpha"); - bundle.Should().Contain("Bravo"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 100.yaml + checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + type: feature + title: Alpha + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 999.yaml + checksum: 6e589c7dca3e9a62fd9cd768eee589d7c7d4ee85 + type: feature + title: Bravo + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/999" + """ + ); } [Test] @@ -195,7 +230,28 @@ await FileSystem.File.WriteAllTextAsync( handler.RequestedPaths.Should().BeEmpty("local fallback must not reach the CDN"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: 1-local.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1-local.yaml + checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + type: feature + title: Alpha + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -239,7 +295,29 @@ await FileSystem.File.WriteAllTextAsync( handler.RequestedPaths.Should().BeEmpty("use_local_changelogs must not reach the CDN"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: 1-local.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 1-local.yaml + checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + type: feature + title: Alpha + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -254,8 +332,11 @@ public async Task CdnAll_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("--all") && d.Message.Contains("--force-local")); + .MatchInlineSnapshot( + "error --all is not supported when sourcing changelog entries from the CDN, because entries are fetched by key (one per PR) and there is no pool enumeration. Pass --force-local or --directory to bundle from a local checkout instead." + ); } [Test] @@ -275,8 +356,11 @@ public async Task CdnInputProducts_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("--input-products") && d.Message.Contains("--force-local")); + .MatchInlineSnapshot( + "error --input-products is not supported when sourcing changelog entries from the CDN, because entries are fetched by key (one per PR) and there is no pool enumeration. Pass --force-local or --directory to bundle from a local checkout instead." + ); } [Test] @@ -296,8 +380,11 @@ public async Task CdnIssues_ReturnsError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("--issues") && d.Message.Contains("--force-local")); + .MatchInlineSnapshot( + "error --issues is not supported when sourcing changelog entries from the CDN, because entries are fetched by key (one per PR) and there is no pool enumeration. Pass --force-local or --directory to bundle from a local checkout instead." + ); } [Test] @@ -336,7 +423,29 @@ public async Task ProbeMiss_WarnsAndSkips() .Should() .Be(1, "404 must not be retried on the probe path"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("Alpha", "the entry found by probe must appear in the bundle"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 100.yaml + checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + type: feature + title: Alpha + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -409,8 +518,29 @@ public async Task MarkerEntry_ResolvesDepthOne() ); Collector.Errors.Should().Be(0); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("Alpha", "the marker must resolve to its parent entry"); - bundle.Should().NotContain("link:", "the marker itself must not appear in the output"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 100.yaml + checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + type: feature + title: Alpha + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -446,7 +576,29 @@ public async Task MarkerEntry_DuplicateMarkersToSameParent_OneEntryInBundle() ); Collector.Errors.Should().Be(0); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Split("title: Alpha").Length.Should().Be(2, "exactly one Alpha entry must appear"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 100.yaml + checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + type: feature + title: Alpha + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -477,7 +629,11 @@ public async Task MarkerEntry_Chain_FailsBundle() ); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("Marker chain")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error Marker chain detected: '200.yaml' → '100.yaml' is also a marker. Marker chains are not allowed."); } [Test] @@ -508,8 +664,11 @@ public async Task MarkerEntry_ParentMissing_FailsBundle() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("marker") && d.Message.Contains("100")); + .MatchInlineSnapshot( + "error Changelog entry '200.yaml' is a marker pointing to PR 100, but that entry does not exist in elastic/elasticsearch/main." + ); } [Test] @@ -545,11 +704,10 @@ public async Task ProfileSourceGithubRelease_EmitsDeprecationWarning() Collector.Errors.Should().Be(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains("source") && d.Message.Contains( - "deprecated" - ) && d.Message.Contains("bundle.releases.github") + .MatchInlineSnapshot( + "warning {repo}/Guid_1/changelog.yml bundle.profiles.es-release.source is deprecated. Use bundle.releases.github to map release tags to profiles instead." ); } @@ -639,8 +797,27 @@ public async Task BackportCollision_MainBranchWins_WarnAndKeepMain() var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); // Only the main-branch note title should appear once. - bundle.Should().Contain("Known issue on main"); - bundle.Should().NotContain("Known issue on 9.4 branch"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: main/note-known-issue.yml + checksum: bd81e86814509f9260869ca8af921b405ae6658f + type: known-issue + title: Known issue on main + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -691,8 +868,27 @@ public async Task BackportCollision_NoMainOrMaster_KeepsOrdinalFirst() var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); // "9.4/note-known-issue.yml" sorts before "feature/note-known-issue.yml". - bundle.Should().Contain("Known issue on 9.4 branch"); - bundle.Should().NotContain("Known issue on feature branch"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 9.4/note-known-issue.yml + checksum: 11799cc69fef6c8db52121edbe36e656b3a5ccc2 + type: known-issue + title: Known issue on 9.4 branch (backport) + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -738,9 +934,27 @@ public async Task BackportCollision_IdenticalContent_ChecksumDedupHandlesIt() ); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("Known issue on main"); - // Note appears exactly once (not duplicated). - bundle.Split("Known issue on main", StringSplitOptions.None).Length.Should().Be(2, "note must appear exactly once"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: main/note-known-issue.yml + checksum: bd81e86814509f9260869ca8af921b405ae6658f + type: known-issue + title: Known issue on main + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } private static HttpResponseMessage Json(string body) => diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs index 6ba826963b..09eb884d5a 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs @@ -5,6 +5,8 @@ using System.Collections.Frozen; using System.Text; using AwesomeAssertions; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; using Elastic.Changelog.Bundling; using Elastic.Changelog.Utilities; using Elastic.Documentation.Configuration; @@ -83,14 +85,41 @@ public async Task BundleChangelogs_WithAllOption_CreatesValidBundle() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("products:"); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("product: kibana"); - bundleContent.Should().Contain("entries:"); - bundleContent.Should().Contain("file:"); - bundleContent.Should().Contain("name: 1755268130-first-changelog.yaml"); - bundleContent.Should().Contain("name: 1755268140-second-changelog.yaml"); - bundleContent.Should().Contain("checksum:"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + - product: kibana + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-first-changelog.yaml + checksum: a65016261a61152db249d295c2859270c5103765 + type: feature + title: First changelog + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-second-changelog.yaml + checksum: c1bd6cb897ac1f2920e24ff3df0432bf623f85fb + type: enhancement + title: Second changelog + products: + - product: kibana + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/kibana/pull/200" + """ + ); } [Test] @@ -143,10 +172,28 @@ public async Task BundleChangelogs_WithProductsFilter_FiltersCorrectly() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("target: 9.2.0"); - bundleContent.Should().Contain("name: 1755268130-elasticsearch-feature.yaml"); - bundleContent.Should().NotContain("name: 1755268140-kibana-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -210,9 +257,37 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-first-pr.yaml"); - bundleContent.Should().Contain("name: 1755268140-second-pr.yaml"); - bundleContent.Should().NotContain("name: 1755268150-third-pr.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-first-pr.yaml + checksum: 013f5493de6c3e2792ecaa371eb23ab191f782f3 + type: feature + title: First PR + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-second-pr.yaml + checksum: a9663d09ccc6614bca7e68d6dbb0453afa09b8eb + type: feature + title: Second PR + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/200" + """ + ); } [Test] @@ -242,7 +317,24 @@ public async Task BundleChangelogs_WithPrsFilter_MatchesFilenameDigitsWhenYamlPr result.Should().BeTrue(); Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 12345.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 12345.yaml + checksum: 43b5184607b93e16dd003a4c429c234f67752d29 + type: feature + title: Filename identity + products: + - product: elasticsearch + versions: + - 9.2.0 + """ + ); } [Test] @@ -274,7 +366,26 @@ public async Task BundleChangelogs_WithPrsFilter_MatchesTimestampFileViaYamlPrs( result.Should().BeTrue(); Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1735-foo.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1735-foo.yaml + checksum: 5784adf1f207c98d98ce3240054b72a4f091c71c + type: feature + title: Timestamp identity + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + """ + ); } [Test] @@ -337,9 +448,37 @@ public async Task BundleChangelogs_WithIssuesFilter_FiltersCorrectly() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-first-issue.yaml"); - bundleContent.Should().Contain("name: 1755268140-second-issue.yaml"); - bundleContent.Should().NotContain("name: 1755268150-third-issue.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-first-issue.yaml + checksum: d275812653428ee900546d0624ceabc690bbf15e + type: feature + title: First issue + products: + - product: elasticsearch + versions: + - 9.2.0 + issues: + - "https://github.com/elastic/elasticsearch/issues/100" + - file: + name: 1755268140-second-issue.yaml + checksum: 9fe1ccd05b9af020a6f9317045ab5bd59a2af2ac + type: feature + title: Second issue + products: + - product: elasticsearch + versions: + - 9.2.0 + issues: + - "https://github.com/elastic/elasticsearch/issues/200" + """ + ); } [Test] @@ -373,7 +512,26 @@ public async Task BundleChangelogs_WithOldPrFormat_StillMatchesWhenFilteringByPr Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-legacy-pr.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-legacy-pr.yaml + checksum: ff9d58350837176bd8e4980b8444e87adc83f2d3 + type: feature + title: Legacy PR changelog + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/999" + """ + ); } [Test] @@ -425,11 +583,13 @@ public async Task BundleChangelogs_WithPrsFilterAndUnmatchedPrs_EmitsWarnings() ); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains( - "No changelog file found for PR: https://github.com/elastic/elasticsearch/pull/300" - ) + .MatchInlineSnapshot( + """ + warning No changelog file found for PR: https://github.com/elastic/elasticsearch/pull/200 + warning No changelog file found for PR: https://github.com/elastic/elasticsearch/pull/300 + """ ); } @@ -492,8 +652,37 @@ public async Task BundleChangelogs_WithPrsFileFilter_FiltersCorrectly() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-first-pr.yaml"); - bundleContent.Should().Contain("name: 1755268140-second-pr.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-first-pr.yaml + checksum: 013f5493de6c3e2792ecaa371eb23ab191f782f3 + type: feature + title: First PR + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-second-pr.yaml + checksum: a9663d09ccc6614bca7e68d6dbb0453afa09b8eb + type: feature + title: Second PR + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/200" + """ + ); } [Test] @@ -533,7 +722,28 @@ public async Task BundleChangelogs_WithPrNumberAndOwnerRepo_FiltersCorrectly() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-pr-number.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + repo: elasticsearch + owner: elastic + release-date: {today} + entries: + - file: + name: 1755268130-pr-number.yaml + checksum: e8de710842df34ae2238ea319b4e6214306efa0b + type: feature + title: PR with number + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -571,7 +781,26 @@ public async Task BundleChangelogs_WithShortPrFormat_FiltersCorrectly() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-short-format.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-short-format.yaml + checksum: 87a7358a293fc63fe9d571bed3d41af8aad5bfb5 + type: feature + title: PR with short format + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/133609" + """ + ); } [Test] @@ -591,11 +820,7 @@ public async Task BundleChangelogs_WithNoMatchingFiles_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector - .Diagnostics - .Should() - .Contain(d => d.Message.Contains("No YAML files found") || d.Message.Contains("No changelog entries matched")); + Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot("error {repo}/Guid_1 No YAML files found in directory"); } [Test] @@ -616,8 +841,7 @@ public async Task BundleChangelogs_WithInvalidDirectory_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Directory does not exist")); + Collector.Diagnostics.ToSnapshotText().Should().MatchInlineSnapshot("error {repo}/Guid_1/nonexistent Directory does not exist"); } [Test] @@ -666,8 +890,13 @@ public async Task BundleChangelogs_WithNoFilterOption_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("At least one filter option must be specified")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error At least one filter option must be specified: --all, --input-products, --prs, --issues, or --files" + ); } [Test] @@ -688,8 +917,13 @@ public async Task BundleChangelogs_WithMultipleFilterOptions_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("Multiple filter options cannot be specified together")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Multiple filter options cannot be specified together. You specified: --all, --input-products. Please use only one filter option: --all, --input-products, --prs, --issues, or --files" + ); } [Test] @@ -744,11 +978,39 @@ public async Task BundleChangelogs_WithMultipleProducts_CreatesValidBundle() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: cloud-serverless"); - bundleContent.Should().Contain("target: 2025-12-02"); - bundleContent.Should().Contain("target: 2025-12-06"); - bundleContent.Should().Contain("name: 1755268130-cloud-feature1.yaml"); - bundleContent.Should().Contain("name: 1755268140-cloud-feature2.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-12-02 + - product: cloud-serverless + target: 2025-12-06 + release-date: {today} + entries: + - file: + name: 1755268130-cloud-feature1.yaml + checksum: e6ab478a8cf5a520992531e7f5fda93dbf19959d + type: feature + title: Cloud serverless feature 1 + products: + - product: cloud-serverless + versions: + - 2025-12-02 + prs: + - "https://github.com/elastic/cloud-serverless/pull/100" + - file: + name: 1755268140-cloud-feature2.yaml + checksum: d41de380d5e3827a96e56f39ef3c2892d810c242 + type: feature + title: Cloud serverless feature 2 + products: + - product: cloud-serverless + versions: + - 2025-12-06 + prs: + - "https://github.com/elastic/cloud-serverless/pull/200" + """ + ); } [Test] @@ -801,8 +1063,43 @@ public async Task BundleChangelogs_WithWildcardProductFilter_MatchesAllProducts( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-elasticsearch-feature.yaml"); - bundleContent.Should().Contain("name: 1755268140-kibana-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + - product: kibana + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-kibana-feature.yaml + checksum: 26138a1fba412c9a5b0ff0d7220c62162220aca5 + type: feature + title: Kibana feature + products: + - product: kibana + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/kibana/pull/200" + """ + ); } [Test] @@ -855,8 +1152,43 @@ public async Task BundleChangelogs_WithWildcardAllParts_EquivalentToAll() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-elasticsearch-feature.yaml"); - bundleContent.Should().Contain("name: 1755268140-kibana-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + - product: kibana + target: 9.3.0 + lifecycle: beta + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-kibana-feature.yaml + checksum: 0b2ef7378dcf24ef60f00fd7c873d6e4c0dd7239 + type: feature + title: Kibana feature + products: + - product: kibana + versions: + - 9.3.0 + lifecycle: beta + prs: + - "https://github.com/elastic/kibana/pull/200" + """ + ); } [Test] @@ -923,9 +1255,43 @@ public async Task BundleChangelogs_WithPrefixWildcardTarget_MatchesCorrectly() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-es-9.3.0.yaml"); - bundleContent.Should().Contain("name: 1755268140-es-9.3.1.yaml"); - bundleContent.Should().NotContain("name: 1755268150-es-9.2.0.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + - product: elasticsearch + target: 9.3.1 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-es-9.3.0.yaml + checksum: e6d486a763ea3e602402854d69cf7687013d0ea9 + type: feature + title: Elasticsearch 9.3.0 feature + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-es-9.3.1.yaml + checksum: 45f2cc90ed8a30e22478c392452168a2caa663a7 + type: feature + title: Elasticsearch 9.3.1 feature + products: + - product: elasticsearch + versions: + - 9.3.1 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/200" + """ + ); } [Test] @@ -948,8 +1314,11 @@ public async Task BundleChangelogs_WithNonExistentFileAsPrs_ReturnsError() // Assert // File doesn't exist and there are no other PRs, so should return error result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("File does not exist")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/Guid_1/nonexistent.txt File does not exist: {repo}/Guid_1/nonexistent.txt"); } [Test] @@ -989,7 +1358,26 @@ public async Task BundleChangelogs_WithUrlAsPrs_TreatsAsPrIdentifier() Collector.Warnings.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-test-pr.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-test-pr.yaml + checksum: 3ee6f675ab08fbe87f6dde7b63089f667ad95690 + type: feature + title: Test PR + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/123" + """ + ); } [Test] @@ -1033,7 +1421,26 @@ public async Task BundleChangelogs_WithNonExistentFileAndOtherPrs_EmitsWarning() fileWarning.Should().NotBeNull("Expected a warning about the non-existent file being skipped"); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-test-pr.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-test-pr.yaml + checksum: 3ee6f675ab08fbe87f6dde7b63089f667ad95690 + type: feature + title: Test PR + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/123" + """ + ); } [Test] @@ -1090,21 +1497,41 @@ public async Task BundleChangelogs_WithOutputProducts_OverridesChangelogProducts var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Output products should override changelog products at the bundle level - var topLevel = bundleContent[..bundleContent.IndexOf("entries:", StringComparison.Ordinal)]; - topLevel.Should().Contain("product: cloud-serverless"); - topLevel.Should().Contain("target: 2025-12-02"); - topLevel.Should().Contain("target: 2025-12-06"); - // Lifecycle values should be included in products array - topLevel.Should().Contain("lifecycle: ga"); - topLevel.Should().Contain("lifecycle: beta"); - // Bundle-level products come only from --output-products - topLevel.Should().NotContain("product: elasticsearch"); - topLevel.Should().NotContain("product: kibana"); - // Entries are always inlined (with file provenance) and keep their own products - bundleContent.Should().Contain("name: 1755268130-elasticsearch-feature.yaml"); - bundleContent.Should().Contain("name: 1755268140-kibana-feature.yaml"); - bundleContent.Should().Contain("title: Elasticsearch feature"); - bundleContent.Should().Contain("title: Kibana feature"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-12-02 + lifecycle: ga + - product: cloud-serverless + target: 2025-12-06 + lifecycle: beta + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-feature.yaml + checksum: 953e2ec8fd6621dd6344173002e8f3911139b7bd + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-kibana-feature.yaml + checksum: 5567d7bf362ba212f7ca11d9bb7df2eb47f365e7 + type: feature + title: Kibana feature + products: + - product: kibana + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/kibana/pull/200" + """ + ); } [Test] @@ -1170,12 +1597,7 @@ public async Task BundleChangelogs_WithMultipleProducts_IncludesAllProducts() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("product: kibana"); - bundleContent.Should().Contain("target: 9.2.0"); - // Should have 3 entries - var entryCount = bundleContent.Split("file:", StringSplitOptions.RemoveEmptyEntries).Length - 1; - entryCount.Should().Be(3); + bundleContent.Should().MatchSnapshot(); } [Test] @@ -1233,11 +1655,43 @@ public async Task BundleChangelogs_WithInputProducts_IncludesLifecycleInProducts var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify lifecycle is included in products array (extracted from changelog entries, not filter) - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("target: 9.2.0"); - bundleContent.Should().Contain("target: 9.3.0"); - bundleContent.Should().Contain("lifecycle: ga"); - bundleContent.Should().Contain("lifecycle: beta"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + - product: elasticsearch + target: 9.3.0 + lifecycle: beta + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-ga.yaml + checksum: 339473dfc13c9ece80602aa6f782411be88bb0eb + type: feature + title: Elasticsearch GA feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-elasticsearch-beta.yaml + checksum: 84adfd530b855f1858d771684ce576df36ee0464 + type: feature + title: Elasticsearch Beta feature + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: beta + prs: + - "https://github.com/elastic/elasticsearch/pull/200" + """ + ); } [Test] @@ -1281,11 +1735,30 @@ public async Task BundleChangelogs_WithOutputProducts_IncludesLifecycleInProduct var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify lifecycle is included in products array from --output-products - bundleContent.Should().Contain("product: cloud-serverless"); - bundleContent.Should().Contain("target: 2025-12-02"); - bundleContent.Should().Contain("target: 2025-12-06"); - bundleContent.Should().Contain("lifecycle: ga"); - bundleContent.Should().Contain("lifecycle: beta"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-12-02 + lifecycle: ga + - product: cloud-serverless + target: 2025-12-06 + lifecycle: beta + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch.yaml + checksum: 953e2ec8fd6621dd6344173002e8f3911139b7bd + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1339,11 +1812,43 @@ public async Task BundleChangelogs_ExtractsLifecycleFromChangelogEntries() var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify lifecycle is included in products array extracted from changelog entries - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("target: 9.2.0"); - bundleContent.Should().Contain("target: 9.3.0"); - bundleContent.Should().Contain("lifecycle: ga"); - bundleContent.Should().Contain("lifecycle: beta"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + - product: elasticsearch + target: 9.3.0 + lifecycle: beta + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-ga.yaml + checksum: 339473dfc13c9ece80602aa6f782411be88bb0eb + type: feature + title: Elasticsearch GA feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - file: + name: 1755268140-elasticsearch-beta.yaml + checksum: 84adfd530b855f1858d771684ce576df36ee0464 + type: feature + title: Elasticsearch Beta feature + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: beta + prs: + - "https://github.com/elastic/elasticsearch/pull/200" + """ + ); } [Test] @@ -1385,12 +1890,28 @@ public async Task BundleChangelogs_WithInputProductsWildcardLifecycle_ExtractsAc var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify that the actual lifecycle value "ga" from the changelog is included in products array, // not the wildcard "*" from the filter - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("target: 9.2.0"); - bundleContent.Should().Contain("lifecycle: ga"); - // Verify wildcard "*" is not included in the products array - bundleContent.Should().NotContain("lifecycle: *"); - bundleContent.Should().NotContain("lifecycle: '*\""); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1-feature.yaml + checksum: c25230be743423f1382f80ac428391ff7965cb4d + type: feature + title: A new feature was added + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1455,16 +1976,12 @@ public async Task BundleChangelogs_WithMultipleTargets_WarningIncludesLifecycle( // Assert result.Should().BeTrue(); Collector.Errors.Should().Be(0); - Collector.Warnings.Should().BeGreaterThan(0); // Verify warning message includes lifecycle values Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains("Product 'elasticsearch' has multiple targets in bundle") && d.Message.Contains( - "9.2.0" - ) && d.Message.Contains("9.2.0 beta") && d.Message.Contains("9.2.0 ga") - ); + .MatchInlineSnapshot("warning Product 'elasticsearch' has multiple targets in bundle: 9.2.0, 9.2.0 beta, 9.2.0 ga"); } [Test] @@ -1505,18 +2022,29 @@ public async Task BundleChangelogs_CopiesChangelogContents() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("file:"); - bundleContent.Should().Contain("name: 1755268130-test-feature.yaml"); - bundleContent.Should().Contain("checksum:"); - bundleContent.Should().Contain("type: feature"); - bundleContent.Should().Contain("title: Test feature"); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("target: 9.2.0"); - bundleContent.Should().Contain("prs:"); - bundleContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/100"); - bundleContent.Should().Contain("areas:"); - bundleContent.Should().Contain("- Search"); - bundleContent.Should().Contain("description: This is a test feature"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-test-feature.yaml + checksum: 0321f84ad57ef4a4ebc8747f19d0fe933257e70b + type: feature + title: Test feature + products: + - product: elasticsearch + versions: + - 9.2.0 + description: This is a test feature + areas: + - Search + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1566,43 +2094,34 @@ public async Task BundleChangelogs_PreservesSpecialCharactersInUtf8() // Verify special characters are preserved correctly (not corrupted) // The original issue reported "&o0" and "*o0" corruption, so we verify the characters are correct - bundleContent.Should().Contain("&"); // Ampersand should be preserved - bundleContent.Should().Contain("Feature with special characters & symbols"); // Ampersand in title - bundleContent.Should().Contain("Ampersand: & symbol"); // Ampersand in description - - // Check that asterisk appears correctly (not corrupted to "*o0") - bundleContent.Should().Contain("*"); // Asterisk should be preserved - bundleContent.Should().Contain("Asterisk: * symbol"); // Asterisk in description - - // Verify the ampersand and asterisk are not corrupted - // The corruption pattern would be "&o0" or "*o0" appearing where we expect "&" or "*" - // We check that the title contains the correct pattern, not the corrupted one - var titleLine = bundleContent.Split('\n').FirstOrDefault(l => l.Contains("title:")); - titleLine.Should().NotBeNull(); - titleLine.Should().Contain("&"); - titleLine.Should().NotContain("&o0"); // Should not be corrupted in title - - // Verify no corruption patterns exist (these would indicate encoding issues) - bundleContent.Should().NotContain("&o0"); // Should not contain corrupted ampersand - bundleContent.Should().NotContain("*o0"); // Should not contain corrupted asterisk - - // Verify other special characters are preserved - bundleContent.Should().Contain("<"); - bundleContent.Should().Contain(">"); - bundleContent.Should().Contain("\""); - - // Verify Unicode characters are preserved - bundleContent.Should().Contain("©"); - bundleContent.Should().Contain("®"); - bundleContent.Should().Contain("™"); - bundleContent.Should().Contain("€"); - - // Verify the content structure is correct - bundleContent.Should().Contain("title: \"Feature with special characters & symbols\""); - bundleContent.Should().Contain("type: feature"); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("target: 9.3.0"); - bundleContent.Should().Contain("lifecycle: ga"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-special-chars.yaml + checksum: ae87a7cc3d83bf715b227137a0ec318131b1eff8 + type: feature + title: "Feature with special characters & symbols" + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + description: |- + This feature includes special characters: + - Ampersand: & symbol + - Asterisk: * symbol + - Other special chars: < > " ' / \ + - Unicode: © ® ™ € £ ¥ + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1640,10 +2159,26 @@ public async Task BundleChangelogs_WithDirectoryOutputPath_CreatesDefaultFilenam FileSystem.File.Exists(outputPath).Should().BeTrue("Output file should be created"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("products:"); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("entries:"); - bundleContent.Should().Contain("name: 1755268130-test-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-test-feature.yaml + checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + type: feature + title: Test feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1675,8 +2210,11 @@ public async Task BundleChangelogs_WithResolveAndMissingTitle_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("missing required field: title")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/Guid_1/1755268130-test-feature.yaml Changelog file is missing required field: title"); } [Test] @@ -1708,8 +2246,11 @@ public async Task BundleChangelogs_WithResolveAndMissingType_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("missing required field: type")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/Guid_1/1755268130-test-feature.yaml Changelog file is missing required field: type"); } [Test] @@ -1738,8 +2279,11 @@ public async Task BundleChangelogs_WithResolveAndMissingProducts_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("missing required field: products")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error {repo}/Guid_1/1755268130-test-feature.yaml Changelog file is missing required field: products"); } [Test] @@ -1778,8 +2322,16 @@ public async Task BundleChangelogs_WithMultipleInvalidEntries_ReportsAllInOnePas // Assert: both problems are reported in a single run instead of aborting on the first. result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("missing required field: title")); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("missing required field: products")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + error {repo}/Guid_1/1755268130-missing-title.yaml Changelog file is missing required field: title + error {repo}/Guid_1/1755268140-missing-products.yaml Changelog file is missing required field: products + """ + ); } [Test] @@ -1811,8 +2363,13 @@ public async Task BundleChangelogs_WithResolveAndInvalidProduct_ReturnsError() // Assert result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("product entry missing required field: product")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/Guid_1/1755268130-test-feature.yaml Changelog file has product entry missing required field: product" + ); } [Test] @@ -1853,9 +2410,30 @@ public async Task BundleChangelogs_WithHideFeaturesOption_IncludesHideFeaturesIn var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify that hide-features field is included in the bundle output - bundleContent.Should().Contain("hide-features:"); - bundleContent.Should().Contain("- \"feature:hidden-api\""); - bundleContent.Should().Contain("- \"feature:another-hidden\""); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + hide-features: + - "feature:hidden-api" + - "feature:another-hidden" + entries: + - file: + name: 1755268130-feature.yaml + checksum: fb3fd2d00f74ed0d395253baacc5333a3754485f + type: feature + title: Feature with hidden flag + products: + - product: elasticsearch + versions: + - 9.2.0 + feature-id: "feature:hidden-api" + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1895,7 +2473,26 @@ public async Task BundleChangelogs_WithoutHideFeaturesOption_OmitsHideFeaturesFi var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify that hide-features field is NOT written when not specified - bundleContent.Should().NotContain("hide-features:"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: cb50b3480ba2f400e80e1f922cfbf4afe3758f86 + type: feature + title: Regular feature + products: + - product: elasticsearch + versions: + - 9.3.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1945,9 +2542,30 @@ await FileSystem.File.WriteAllTextAsync( var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify that hide-features field contains feature IDs from the file - bundleContent.Should().Contain("hide-features:"); - bundleContent.Should().Contain("- \"feature:from-file\""); - bundleContent.Should().Contain("- \"feature:another\""); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + hide-features: + - "feature:from-file" + - "feature:another" + entries: + - file: + name: 1755268130-feature.yaml + checksum: 815f1f1f0f1f8913132eb2f945a452b70906c270 + type: feature + title: Feature with hidden flag + products: + - product: elasticsearch + versions: + - 9.2.0 + feature-id: "feature:from-file" + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -1987,8 +2605,27 @@ public async Task BundleChangelogs_WithRepoOption_IncludesRepoInBundleProducts() var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify that repo field is included in the bundle output - bundleContent.Should().Contain("product: cloud-serverless"); - bundleContent.Should().Contain("repo: cloud"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-12-02 + repo: cloud + release-date: {today} + entries: + - file: + name: 1755268130-serverless-feature.yaml + checksum: 593ad5bb20a97b49cacd77f3ccd0e2bacd0d52e1 + type: feature + title: Serverless feature + products: + - product: cloud-serverless + versions: + - 2025-12-02 + prs: + - "https://github.com/elastic/cloud/pull/100" + """ + ); } [Test] @@ -2030,8 +2667,27 @@ public async Task BundleChangelogs_WithoutRepoOption_UsesResolvedAuthoringRepoIn var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify that the resolved authoring repo is written when not explicitly specified - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("repo: docs-builder"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + repo: docs-builder + release-date: {today} + entries: + - file: + name: 1755268130-es-feature.yaml + checksum: e990f1b768aa43ecedf7646bea84eb5b88f441a3 + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.3.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2088,7 +2744,30 @@ public async Task BundleChangelogs_WithBundleLevelRepoConfig_UsesConfigRepoWhenO Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: cloud", "bundle.repo config should be applied when --repo is not specified"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-06-01 + lifecycle: ga + repo: cloud + owner: elastic + release-date: {today} + entries: + - file: + name: 1755268130-serverless-feature.yaml + checksum: 92a0d7dbfae5efdbb9fd63a45032c70ab519fc18 + type: feature + title: Serverless feature + products: + - product: cloud-serverless + versions: + - 2025-06-01 + lifecycle: ga + prs: + - "https://github.com/elastic/cloud/pull/100" + """ + ); } [Test] @@ -2144,8 +2823,29 @@ public async Task BundleChangelogs_WithRepoOptionAndBundleLevelConfig_CliOptionT Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: cloud", "explicit --repo should override bundle.repo config"); - bundleContent.Should().NotContain("repo: wrong-repo"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-06-01 + lifecycle: ga + repo: cloud + release-date: {today} + entries: + - file: + name: 1755268130-serverless-feature.yaml + checksum: 92a0d7dbfae5efdbb9fd63a45032c70ab519fc18 + type: feature + title: Serverless feature + products: + - product: cloud-serverless + versions: + - 2025-06-01 + lifecycle: ga + prs: + - "https://github.com/elastic/cloud/pull/100" + """ + ); } [Test] @@ -2190,10 +2890,32 @@ public async Task BundleChangelogs_WithOutputProductsAndRepo_IncludesRepoInAllPr var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); // Verify that repo field is included for all products - bundleContent.Should().Contain("product: cloud-serverless"); - bundleContent.Should().Contain("product: elasticsearch-serverless"); - // The repo field should appear for each product (or at least once) - bundleContent.Should().Contain("repo: cloud"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-12-02 + lifecycle: ga + repo: cloud + - product: elasticsearch-serverless + target: 2025-12-02 + lifecycle: ga + repo: cloud + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: 80717b768423c3b14edc607a805eb7dd78f905ef + type: feature + title: Feature for serverless + products: + - product: elasticsearch + versions: + - 9.3.0 + prs: + - "https://github.com/elastic/cloud/pull/100" + """ + ); } [Test] @@ -2246,8 +2968,26 @@ public async Task BundleChangelogs_WithConfigOutputDirectory_WhenOutputNotSpecif FileSystem.File.Exists(expectedOutputPath).Should().BeTrue("Bundle should be created in config output_directory"); var bundleContent = await FileSystem.File.ReadAllTextAsync(expectedOutputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("name: 1755268130-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + type: feature + title: Test feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2301,8 +3041,26 @@ public async Task BundleChangelogs_WithConfigDirectory_WhenDirectoryNotSpecified FileSystem.File.Exists(expectedOutputPath).Should().BeTrue("Bundle should use config directory and output_directory"); var bundleContent = await FileSystem.File.ReadAllTextAsync(expectedOutputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("name: 1755268130-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + type: feature + title: Test feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2361,8 +3119,26 @@ public async Task BundleChangelogs_WithExplicitDirectory_OverridesConfigDirector Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("name: 1755268130-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + type: feature + title: Test feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2430,9 +3206,32 @@ public async Task BundleChangelogs_WithProfileHideFeatures_IncludesHideFeaturesI var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); // Verify that hide-features from the profile are written to the bundle - bundleContent.Should().Contain("hide-features:"); - bundleContent.Should().Contain("- \"feature:profile-hidden\""); - bundleContent.Should().Contain("- \"feature:another-profile-hidden\""); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + hide-features: + - "feature:profile-hidden" + - "feature:another-profile-hidden" + entries: + - file: + name: 1755268130-feature.yaml + checksum: b98627257eac4660c9f91c0c0d51cd2fe819b7a4 + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + feature-id: "feature:profile-hidden" + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2500,8 +3299,30 @@ public async Task BundleChangelogs_WithProfile_OnlyProfileHideFeaturesAreUsed() var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); // Verify that only the profile hide-features are present - bundleContent.Should().Contain("hide-features:"); - bundleContent.Should().Contain("- \"feature:from-profile\""); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + hide-features: + - "feature:from-profile" + entries: + - file: + name: 1755268130-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2566,8 +3387,31 @@ public async Task BundleChangelogs_WithProfileMultipleHideFeatures_AllProfileFea outputFiles.Should().NotBeEmpty("Expected an output file to be created"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("- \"feature:profile-one\""); - bundleContent.Should().Contain("- \"feature:profile-two\""); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + hide-features: + - "feature:profile-one" + - "feature:profile-two" + entries: + - file: + name: 1755268130-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2623,7 +3467,26 @@ public async Task BundleChangelogs_WithComments_ProducesNormalizedChecksum() // The checksum in the bundle should be the normalized hash (comments stripped) var expectedChecksum = ComputeSha1(changelogWithComments); - bundleContent.Should().Contain($"checksum: {expectedChecksum}"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268130-with-comments.yaml + checksum: 0d824e6d5d7484980513cf372a86e428b24a0f13 + type: feature + title: Feature with comments + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); // Both versions of the content should produce the same checksum (comments are normalized away) var checksumFromCommented = ComputeSha1(changelogWithComments); @@ -2833,11 +3696,25 @@ public async Task AmendBundle_WithComments_ProducesNormalizedChecksum() // The checksum should be the normalized hash var expectedChecksum = ComputeSha1(changelogWithComments); - amendContent.Should().Contain($"checksum: {expectedChecksum}"); - - // Resolved entries should include inline data - amendContent.Should().Contain("title: Resolved amend feature"); - amendContent.Should().Contain("type: feature"); + amendContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + entries: + - file: + name: 1755268140-resolved-feature.yaml + checksum: a4d71514c5bd914d9d0d9bb5549c746efe4938ea + type: feature + title: Resolved amend feature + products: + - product: elasticsearch + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/200" + """ + ); } [Test] @@ -2904,7 +3781,28 @@ public async Task BundleChangelogs_WithProfile_OutputProducts_OverridesProductsA // output_products overrides: the products array in the bundle output should have lifecycle: ga // even though the matched changelog has lifecycle: preview - bundleContent.Should().Contain("lifecycle: ga", "output_products should write lifecycle: ga to the bundle products array"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: bd342ad5f224e33c2cbfe2d9f198ffbe70888231 + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: preview + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -2953,15 +3851,16 @@ public async Task BundleChangelogs_WithProfile_MalformedOutputProducts_EmitsErro var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains("at most three space-separated fields", StringComparison.Ordinal) && d.Message.Contains( - "elasticsearch 9.2.0 ga extra-token", - StringComparison.Ordinal - ) + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.es-release.output_products is deprecated. Use 'product: ' instead. + warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other. + error Profile 'es-release': bundle.output_products could not be parsed: Each product entry must have at most three space-separated fields (product, target, lifecycle). Too many values in segment: 'elasticsearch 9.2.0 ga extra-token'. + """ ); } @@ -3010,15 +3909,12 @@ public async Task BundleChangelogs_WithProfile_MalformedProductsPattern_EmitsErr var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains("Profile 'es-release':", StringComparison.Ordinal) && d.Message.Contains( - "at most three space-separated fields", - StringComparison.Ordinal - ) && d.Message.Contains("elasticsearch 9.2.0 ga extra bad", StringComparison.Ordinal) + .MatchInlineSnapshot( + "error Profile 'es-release': Each product entry must have at most three space-separated fields (product, target, lifecycle). Too many values in segment: 'elasticsearch 9.2.0 ga extra bad'." ); } @@ -3086,9 +3982,28 @@ await FileSystem.File.WriteAllTextAsync( outputFiles.Should().NotBeEmpty("Expected an output file to be created"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("target: 2026-07-21"); - bundleContent.Should().Contain("lifecycle: ga", "ISO date version args should derive ga lifecycle, not preview"); - bundleContent.Should().NotContain("lifecycle: preview"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2026-07-21 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-serverless-feature.yaml + checksum: 808180779594f14a4b04ec2e1890370933efd46d + type: feature + title: Serverless feature + products: + - product: cloud-serverless + versions: + - 2026-07-21 + lifecycle: ga + prs: + - "https://github.com/elastic/kibana/pull/100" + """ + ); } [Test] @@ -3155,8 +4070,30 @@ public async Task BundleChangelogs_WithProfile_RepoAndOwner_WritesValuesToProduc outputFiles.Should().NotBeEmpty("Expected an output file to be created"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: cloud", "Profile repo should be written to bundle product entries"); - bundleContent.Should().Contain("owner: elastic", "Profile owner should be written to bundle product entries"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2025-06-01 + lifecycle: ga + repo: cloud + owner: elastic + release-date: {today} + entries: + - file: + name: 1755268130-serverless-feature.yaml + checksum: b9b886ae3bd3a6f6af2ec1eb1f890669f1792e0f + type: feature + title: Serverless feature + products: + - product: cloud-serverless + versions: + - 2025-06-01 + lifecycle: ga + prs: + - "https://github.com/elastic/cloud/pull/200" + """ + ); } [Test] @@ -3220,7 +4157,30 @@ public async Task BundleChangelogs_WithProfile_BundleLevelRepo_AppliesWhenProfil outputFiles.Should().NotBeEmpty(); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: elasticsearch", "bundle-level repo should be applied when profile omits repo"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + repo: elasticsearch + owner: elastic + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -3308,9 +4268,29 @@ public async Task BundleChangelogs_ServerlessProfile_WritesAuthoringRepoNotCatal outputFiles.Should().NotBeEmpty(); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: cloud-serverless"); - bundleContent.Should().Contain("repo: elasticsearch"); - bundleContent.Should().NotContain("repo: cloud"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2026-09-08 + repo: elasticsearch + owner: elastic + release-date: {today} + entries: + - file: + name: 158340.yaml + checksum: 9ab64a7018d5be24115c832e8c58d7ebb68c12f0 + type: feature + title: Serverless feature + products: + - product: cloud-serverless + versions: + - 2026-09-08 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/158340" + """ + ); } [Test] @@ -3374,8 +4354,29 @@ public async Task BundleChangelogs_WithProfile_ProfileRepoOverridesBundleRepo() outputFiles.Should().NotBeEmpty(); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: elasticsearch", "profile-level repo should override bundle-level repo"); - bundleContent.Should().NotContain("repo: wrong-repo", "bundle-level repo should be overridden by profile-level repo"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -3440,8 +4441,29 @@ public async Task BundleChangelogs_WithProfile_NoRepoOwner_UsesResolvedAuthoring outputFiles.Should().NotBeEmpty("Expected an output file to be created"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: docs-builder", "resolved authoring repo should be present when profile omits repo"); - bundleContent.Should().NotContain("owner:", "No owner field should be present when profile omits owner"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + repo: docs-builder + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -3503,9 +4525,30 @@ public async Task BundleChangelogs_WithProfile_OmittedRepoOwner_DescriptionUsesR outputFiles.Should().NotBeEmpty(); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("Release from elastic/elasticsearch"); - bundleContent.Should().Contain("repo: elasticsearch"); - bundleContent.Should().NotContain("owner:", "description default must not stamp owner onto YAML"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + repo: elasticsearch + description: Release from elastic/elasticsearch + release-date: {today} + entries: + - file: + name: 1755268131-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -3748,8 +4791,28 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(expectedOutputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("1755268130-matched.yaml"); - bundleContent.Should().NotContain("1755268140-unmatched.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-matched.yaml + checksum: d8a2c914c9c1a24407b66031748dcc45cdd7125f + type: feature + title: Matched PR + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -3820,8 +4883,28 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(expectedOutputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("1755268130-matched.yaml"); - bundleContent.Should().NotContain("1755268140-unmatched.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-matched.yaml + checksum: c2f71c044a3cd2e7a3fd45b3d43b9fafd9d7180a + type: feature + title: Matched Issue + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + issues: + - "https://github.com/elastic/elasticsearch/issues/100" + """ + ); } [Test] @@ -3870,13 +4953,12 @@ await FileSystem.File.WriteAllTextAsync( // Assert result.Should().BeFalse("Should fail when file contains bare numbers"); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("fully-qualified GitHub URLs"), - "Error should mention fully-qualified URLs requirement" + .MatchInlineSnapshot( + "error {repo}/Guid_1/prs.txt File must contain fully-qualified GitHub URLs (e.g. https://github.com/owner/repo/pull/123) or changelog YAML paths (.yaml/.yml). Numbers and short forms are not allowed. Found: 100" ); } @@ -3930,14 +5012,11 @@ await FileSystem.File.WriteAllTextAsync( // Assert result.Should().BeFalse("Should fail when file mixes PR and issue URLs"); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("only pull request URLs or only issue URLs"), - "Error should mention homogeneous URL requirement" - ); + .MatchInlineSnapshot("error {repo}/Guid_1/mixed.txt File must contain only pull request URLs or only issue URLs, not a mix."); } [Test] @@ -4021,11 +5100,27 @@ await FileSystem.File.WriteAllTextAsync( var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); // Only the matched PR should be bundled - bundleContent.Should().Contain("1755268130-feb.yaml"); - bundleContent.Should().NotContain("1755268140-other.yaml"); - // Output products should contain the version - bundleContent.Should().Contain("cloud-serverless"); - bundleContent.Should().Contain("2026-02"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2026-02 + release-date: {today} + entries: + - file: + name: 1755268130-feb.yaml + checksum: 60765bb02a3f8d994b4fe2c5dd0b68b91fc0afe2 + type: feature + title: Serverless Feb feature + products: + - product: cloud-serverless + versions: + - 2026-02-01 + lifecycle: ga + prs: + - "https://github.com/elastic/cloud/pull/100" + """ + ); } [Test] @@ -4068,13 +5163,12 @@ await FileSystem.File.WriteAllTextAsync( // Assert result.Should().BeFalse("Should fail when first arg looks like a report"); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("version string"), - "Error should mention that the first arg should be the version" + .MatchInlineSnapshot( + "error When two arguments are provided, the first must be a version string and the second must be a promotion report or URL list file. '{repo}/Guid_1/report.html' looks like a report path or URL. Did you mean: serverless-release {repo}/Guid_1/report.html?" ); } @@ -4113,13 +5207,12 @@ await FileSystem.File.WriteAllTextAsync( var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse("Should fail when profile has products pattern and a report is also provided"); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("products"), - "Error should mention the products pattern conflict" + .MatchInlineSnapshot( + "error Profile 'release' has a 'products' pattern configured. A promotion report, URL list file, or path list file cannot be combined with a products pattern filter." ); } @@ -4183,8 +5276,28 @@ public async Task BundleChangelogs_WithReportOption_ParsesPromotionReportAndFilt Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("1755268130-matched.yaml"); - bundleContent.Should().NotContain("1755268140-unmatched.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-matched.yaml + checksum: 166fb2233258f29de1dc954af5c75f9abd5d2e97 + type: feature + title: Matched PR 100 + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -4236,13 +5349,12 @@ await FileSystem.File.WriteAllTextAsync( // Assert result.Should().BeFalse("Should fail when prs file contains bare numbers"); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("fully-qualified GitHub URLs"), - "Error should mention fully-qualified URL requirement" + .MatchInlineSnapshot( + "error {repo}/Guid_1/prs.txt File must contain fully-qualified GitHub URLs (e.g. https://github.com/owner/repo/pull/123). Numbers and short forms are not allowed. Found: 100" ); } @@ -4286,13 +5398,12 @@ await FileSystem.File.WriteAllTextAsync( // Assert result.Should().BeFalse("Should fail when issues file contains short forms"); - Collector.Errors.Should().BeGreaterThan(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("fully-qualified GitHub URLs"), - "Error should mention fully-qualified URL requirement" + .MatchInlineSnapshot( + "error {repo}/Guid_1/issues.txt File must contain fully-qualified GitHub URLs (e.g. https://github.com/owner/repo/issues/123). Numbers and short forms are not allowed. Found: elastic/elasticsearch#100" ); } @@ -4393,8 +5504,28 @@ public async Task BundleChangelogs_WithRulesBundleExclude_ExcludesMatchingProduc Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-elasticsearch-feature.yaml"); - bundleContent.Should().NotContain("name: 1755268140-cloud-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); // Verify warning was emitted for the excluded entry Logged.Should().Contain(l => l.Contains("global product filter, exclude")); } @@ -4458,8 +5589,28 @@ public async Task BundleChangelogs_WithRulesBundleInclude_IncludesOnlyMatchingPr Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-elasticsearch-feature.yaml"); - bundleContent.Should().NotContain("name: 1755268140-kibana-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-feature.yaml + checksum: 9b6de513c2d6229297890fe701dec2f7f4eb3753 + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/300" + """ + ); Logged.Should().Contain(l => l.Contains("global product filter, include")); } @@ -4522,8 +5673,28 @@ public async Task BundleChangelogs_WithAllFilter_AppliesRulesBundle() Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-elasticsearch-feature.yaml"); - bundleContent.Should().NotContain("name: 1755268140-kibana-feature.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-elasticsearch-feature.yaml + checksum: 4be63d0aa14cc86f2133f1c690ec55f08a27d25c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/500" + """ + ); } [Test] @@ -4591,8 +5762,28 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("1755268001-kibana-only.yaml"); - bundleContent.Should().NotContain("1755268002-es-kibana.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: kibana + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268001-kibana-only.yaml + checksum: da3bff1a2322cdf7adcd78be156f33b117bd5490 + type: feature + title: Kibana only + products: + - product: kibana + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/kibana/pull/100" + """ + ); } [Test] @@ -4659,8 +5850,31 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("1755268012-es-sec.yaml"); - bundleContent.Should().NotContain("1755268011-es-only.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + - product: security + target: 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268012-es-sec.yaml + checksum: 97b1ed2ae369cd0106fb6dcd91c4332c4e5b071e + type: feature + title: ES and security + products: + - product: elasticsearch + versions: + - 9.2.0 + - product: security + versions: + - 9.2.0 + prs: + - "https://github.com/elastic/elasticsearch/pull/500" + """ + ); } [Test] @@ -4775,8 +5989,28 @@ public async Task BundleChangelogs_WithRulesBundleExcludeType_ExcludesMatchingTy Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-feature.yaml"); - bundleContent.Should().NotContain("name: 1755268140-enhancement.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-feature.yaml + checksum: a1c2dd0fdea66eea9537d32028965bf2b92720a6 + type: feature + title: Feature entry + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); Logged.Should().Contain(l => l.Contains("type/area filter")); } @@ -4843,8 +6077,30 @@ public async Task BundleChangelogs_WithRulesBundleIncludeArea_ExcludesNonMatchin Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("name: 1755268130-search-feature.yaml"); - bundleContent.Should().NotContain("name: 1755268140-internal-fix.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268130-search-feature.yaml + checksum: 0305a87a0c5da5c9f4a126a9302d2fa748fdcebc + type: feature + title: Search feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + areas: + - Search + prs: + - "https://github.com/elastic/elasticsearch/pull/300" + """ + ); } [Test] @@ -4928,9 +6184,30 @@ public async Task BundleChangelogs_WithRulesBundlePerProductOverride_AppliesProd Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().NotContain("name: 1755268130-es-enhancement.yaml"); - bundleContent.Should().Contain("name: 1755268140-serverless-search.yaml"); - bundleContent.Should().NotContain("name: 1755268150-serverless-other.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268140-serverless-search.yaml + checksum: f3846f2959e343a09c281d800d74078a9da85ac7 + type: feature + title: Serverless search feature + products: + - product: cloud-serverless + versions: + - 9.2.0 + lifecycle: ga + areas: + - Search + prs: + - "https://github.com/elastic/cloud-serverless/pull/600" + """ + ); } // ── Multi-product rule resolution: intersection + alphabetical first-match ──────────────────────── @@ -5124,13 +6401,29 @@ public async Task BundleChangelogs_WithOutputProducts_SharedProductEntry_UsesAlp result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().NotContain( - "name: 1755268160-shared.yaml", - "kibana rule (alphabetically first) should exclude the shared entry" - ); - bundleContent.Should().Contain( - "name: 1755268161-kibana-other.yaml", - "kibana entry with a different area should pass the exclude_areas rule" + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: kibana + target: 9.3.0 + - product: security + target: 9.3.0 + release-date: {today} + entries: + - file: + name: 1755268161-kibana-other.yaml + checksum: fbce2f7f936d0a7aa04ede803fa5d7186d9a4dfa + type: feature + title: Kibana core feature + products: + - product: kibana + versions: + - 9.3.0 + areas: + - Core + prs: + - "401" + """ ); Logged.Should().Contain(l => l.Contains("type/area filter")); } @@ -5204,10 +6497,25 @@ public async Task BundleChangelogs_WithoutOutputProducts_FallsBackToEntryProduct Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().NotContain("name: 1755268170-kibana-doc.yaml", "kibana docs should be excluded by its per-product rule"); - bundleContent.Should().Contain( - "name: 1755268180-es-doc.yaml", - "elasticsearch docs entry has no per-product rule and no global rule, so it is included" + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + release-date: {today} + entries: + - file: + name: 1755268180-es-doc.yaml + checksum: 52884ed8257759a6d453cc56f0b311a16717fa35 + type: docs + title: Elasticsearch docs entry + products: + - product: elasticsearch + versions: + - 9.3.0 + prs: + - "600" + """ ); } @@ -5321,16 +6629,25 @@ public async Task BundleChangelogs_WithPerProductIncludeProducts_IncludesOnlyCon var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); // Only security changelog should be included (it matches the bundle context "security") - bundleContent.Should().Contain( - "security-feature.yaml", - "security entry matches bundle context and should be included by security-specific rules" - ); - - // Disjoint changelogs are excluded entirely (not included via global fallback) - bundleContent.Should().NotContain("kibana-feature.yaml", "kibana entry is disjoint from security context and should be excluded"); - bundleContent.Should().NotContain( - "elasticsearch-feature.yaml", - "elasticsearch entry is disjoint from security context and should be excluded" + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: security + target: 9.3.0 + release-date: {today} + entries: + - file: + name: security-feature.yaml + checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + type: feature + title: Security feature + products: + - product: security + versions: + - 9.3.0 + prs: + - "123" + """ ); } @@ -5395,16 +6712,25 @@ public async Task BundleChangelogs_WithPerProductExcludeProducts_ExcludesContext // Rule context = "security" (from output products) // Disjoint entries (kibana, elasticsearch) are excluded entirely - bundleContent.Should().NotContain("kibana-feature.yaml", "kibana entry is disjoint from security context"); - bundleContent.Should().NotContain("elasticsearch-feature.yaml", "elasticsearch entry is disjoint from security context"); - - // security entry (matches security context) uses context rule: exclude_products=[kibana] (security not excluded) → INCLUDED - bundleContent.Should().Contain("security-feature.yaml", "security entry should be included (not in context exclude list)"); - - // Multi-product entry (security + kibana) matches security context and gets excluded by exclude_products=[kibana] → EXCLUDED - bundleContent.Should().NotContain( - "security-kibana-feature.yaml", - "security+kibana entry should be excluded by security context rule" + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: security + target: 9.3.0 + release-date: {today} + entries: + - file: + name: security-feature.yaml + checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + type: feature + title: Security feature + products: + - product: security + versions: + - 9.3.0 + prs: + - "123" + """ ); } @@ -5458,13 +6784,26 @@ public async Task BundleChangelogs_WithPerProductRules_FallsBackToGlobalWhenNoCo // Security-only entry is included (disjoint satisfied; no per-product product filter) result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain( - "security-feature.yaml", - "security entry should be included (Mode 3 pass-through when no per-product block for context)" + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: security + target: 9.3.0 + release-date: {today} + entries: + - file: + name: security-feature.yaml + checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + type: feature + title: Security feature + products: + - product: security + versions: + - 9.3.0 + prs: + - "123" + """ ); - // Disjoint entries are excluded entirely in single-product rule resolution - bundleContent.Should().NotContain("elasticsearch-feature.yaml", "elasticsearch entry is disjoint from security context"); - bundleContent.Should().NotContain("kibana-feature.yaml", "kibana entry is disjoint from security context"); } [Test] @@ -5514,11 +6853,26 @@ public async Task BundleChangelogs_WithPerProductRules_ContextRulesTakePrecedenc // Rule context = "security" (from output products) // Disjoint entries (kibana, elasticsearch) are excluded entirely - bundleContent.Should().NotContain("kibana-feature.yaml", "kibana entry is disjoint from security context"); - bundleContent.Should().NotContain("elasticsearch-feature.yaml", "elasticsearch entry is disjoint from security context"); - - // security entry (matches security context) uses context rule: include_products=[security, kibana] → INCLUDED - bundleContent.Should().Contain("security-feature.yaml", "security entry should be included by context rule"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: security + target: 9.3.0 + release-date: {today} + entries: + - file: + name: security-feature.yaml + checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + type: feature + title: Security feature + products: + - product: security + versions: + - 9.3.0 + prs: + - "123" + """ + ); } private async Task CreateTestEntry(string changelogDir, string filename, string title, string product) @@ -5595,7 +6949,16 @@ public async Task BundleChangelogs_WithNoProductsField_FallsBackToGlobalRules() Collector.Errors.Should().Be(1, "no entries remained after rules.bundle filter"); var errorMessages = string.Join("; ", Collector.Diagnostics.Select(d => d.Message)); errorMessages.Should().Contain("No changelog entries remained after applying rules.bundle filter"); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("[-bundle-global]") && d.Message.Contains("no products")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/1755268200-no-products.yaml [-bundle-global] Changelog has no products declared; product filters are skipped for this entry. See documentation for rules.bundle global mode. + error No changelog entries remained after applying rules.bundle filter + """ + ); } [Test] @@ -5656,8 +7019,43 @@ public async Task BundleChangelogs_GlobalMode_IncludeProductsAny_IncludesEntryMa result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("1755268205-es.yaml"); - bundleContent.Should().Contain("1755268206-kibana.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + - product: kibana + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268205-es.yaml + checksum: abe72a860e5f3538f03ce5751ab9d49fe881dec1 + type: feature + title: ES only + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/1" + - file: + name: 1755268206-kibana.yaml + checksum: e98b4ce7bdb6dff3c7afef4d02a8257723939392 + type: feature + title: Kibana only + products: + - product: kibana + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/kibana/pull/2" + """ + ); } [Test] @@ -5696,8 +7094,16 @@ public async Task BundleChangelogs_GlobalMode_EmptyProducts_WarnsThenFailsResolv var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("[-bundle-global]") && d.Message.Contains("no products")); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("missing required field: products")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/1755268207-no-products-feature.yaml [-bundle-global] Changelog has no products declared; product filters are skipped for this entry. See documentation for rules.bundle global mode. + error {repo}/Guid_1/1755268207-no-products-feature.yaml Changelog file is missing required field: products + """ + ); } [Test] @@ -5761,8 +7167,28 @@ await FileSystem.File.WriteAllTextAsync( result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("1755268208-es.yaml"); - bundleContent.Should().NotContain("1755268209-kibana.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: 1755268208-es.yaml + checksum: a280eae9bd4e6ff186792d036cad598f934c01f7 + type: feature + title: ES + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/1" + """ + ); } [Test] @@ -5890,7 +7316,17 @@ public async Task BundleChangelogs_WithMultipleProducts_UnifiedProductFiltering_ // When all entries are filtered out, the system reports this as an error Collector.Errors.Should().Be(1, "system reports error when no entries remain after filtering"); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("No changelog entries remained after applying rules.bundle filter")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml Configuration pattern 'match_products: any' with 'include_products' in per-product rule 'security' provides no selective filtering. Consider 'match_products: all' for strict filtering or 'exclude_products' for exclusion-based filtering. Refer to https://github.com/elastic/docs-builder/blob/main/docs/contribute/configure-changelogs-ref.md + hint Applied rules - kibana: 1 entries + error No changelog entries remained after applying rules.bundle filter + """ + ); // Should be excluded by kibana product rule (alphabetical first-match) Logged.Should().Contain(l => l.Contains("per-product filter, context-exclude") && l.Contains("multi-product")); @@ -6196,9 +7632,25 @@ public async Task BundleChangelogs_PartialPerProductRules_AllOrNothingReplacemen Collector.Errors.Should().Be(0, "no errors expected when entry is included"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain( - "1755268204-partial-rule.yaml", - "entry should be included - per-product rule ignores global type exclusions" + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: kibana + target: 9.3.0 + release-date: {today} + entries: + - file: + name: 1755268204-partial-rule.yaml + checksum: 1b2669d933cb74c99c2d81a652147b12eef6de6b + type: docs + title: Docs entry with partial per-product rule + products: + - product: kibana + versions: + - 9.3.0 + prs: + - "504" + """ ); } @@ -6224,11 +7676,10 @@ public async Task BundleChangelogs_OptionModeWithPlaceholdersButNoOutputProducts Collector.Errors.Should().Be(1, "should have exactly one validation error"); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "When using placeholders in bundle description in option-based mode, --output-products must be explicitly specified to ensure predictable substitution values." - ) + .MatchInlineSnapshot( + "error When using placeholders in bundle description in option-based mode, --output-products must be explicitly specified to ensure predictable substitution values." ); } @@ -6259,9 +7710,30 @@ public async Task BundleChangelogs_OptionModeWithPlaceholdersAndOutputProducts_S Collector.Errors.Should().Be(0, "no errors expected when validation passes"); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain( - "Release includes 9.2.0 with ga features from elastic/elasticsearch", - "placeholders should be substituted correctly" + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + repo: elasticsearch + owner: elastic + description: Release includes 9.2.0 with ga features from elastic/elasticsearch + release-date: {today} + entries: + - file: + name: changelog1.yaml + checksum: a65016261a61152db249d295c2859270c5103765 + type: feature + title: First changelog + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ ); } @@ -6310,9 +7782,31 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: elasticsearch"); - bundleContent.Should().Contain("Release from elastic/elasticsearch"); - bundleContent.Should().NotContain("elastic/cloud-serverless"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2026-09-08 + lifecycle: ga + repo: elasticsearch + owner: elastic + description: Release from elastic/elasticsearch + release-date: {today} + entries: + - file: + name: 158340.yaml + checksum: 9ab64a7018d5be24115c832e8c58d7ebb68c12f0 + type: feature + title: Serverless feature + products: + - product: cloud-serverless + versions: + - 2026-09-08 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/158340" + """ + ); } [Test] @@ -6359,9 +7853,29 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("repo: elasticsearch"); - bundleContent.Should().Contain("158340"); - bundleContent.Should().NotContain("# PRIVATE:"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-serverless + target: 2026-09-08 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 158340.yaml + checksum: 066e79ca0c16f67af5ca3e9631d10dc70f51fac1 + type: feature + title: Feature with bare PR + products: + - product: cloud-serverless + versions: + - 2026-09-08 + lifecycle: ga + prs: + - "158340" + """ + ); } [Test] @@ -6386,11 +7900,10 @@ public async Task BundleChangelogs_OptionModeWithConfigDescriptionAndPlaceholder Collector.Errors.Should().Be(1, "should have exactly one validation error"); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Message.Contains( - "When using placeholders in bundle description in option-based mode, --output-products must be explicitly specified to ensure predictable substitution values." - ) + .MatchInlineSnapshot( + "error When using placeholders in bundle description in option-based mode, --output-products must be explicitly specified to ensure predictable substitution values." ); } @@ -6454,7 +7967,31 @@ await FileSystem.File.WriteAllTextAsync( var outputFiles = FileSystem.Directory.GetFiles(outputDir, "*.yaml", System.IO.SearchOption.AllDirectories); outputFiles.Should().NotBeEmpty(); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("CLI intro for 9.2.0"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + repo: elasticsearch + owner: elastic + description: CLI intro for 9.2.0 + release-date: {today} + entries: + - file: + name: 1755268999-feature.yaml + checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + type: feature + title: Elasticsearch feature + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -6487,8 +8024,16 @@ await FileSystem.File.WriteAllTextAsync( var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("--description")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other. + error When using a profile, --description and --description-file are not allowed if bundle.description or the profile description is set. Remove the CLI description, or remove the description from changelog.yml. + """ + ); } [Test] @@ -6523,8 +8068,16 @@ await FileSystem.File.WriteAllTextAsync( var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("--description")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other. + error When using a profile, --description and --description-file are not allowed if bundle.description or the profile description is set. Remove the CLI description, or remove the description from changelog.yml. + """ + ); } [Test] @@ -6559,7 +8112,27 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().NotContain("release-date:", "release date should be suppressed when bundle.release_dates is false"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + entries: + - file: + name: changelog1.yaml + checksum: a65016261a61152db249d295c2859270c5103765 + type: feature + title: First changelog + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -6594,7 +8167,28 @@ await FileSystem.File.WriteAllTextAsync( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("release-date:", "release date should be auto-populated when bundle.release_dates is true"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: changelog1.yaml + checksum: a65016261a61152db249d295c2859270c5103765 + type: feature + title: First changelog + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + """ + ); } [Test] @@ -6646,8 +8240,28 @@ await FileSystem.File.WriteAllTextAsync( // Verify content refs (bundle uses file refs + checksum unless resolve inlines entries) var bundleContent = await FileSystem.File.ReadAllTextAsync(outputPath, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("changelog-with-bom.yaml"); - bundleContent.Should().Contain("entries:"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: changelog-with-bom.yaml + checksum: 0ed153a98884cc7807342b1937a157a56ad0d692 + type: feature + title: Test changelog with BOM + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/123" + """ + ); } private void CreateSampleChangelogs() diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleDescriptionInputTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleDescriptionInputTests.cs index be7f4429a1..ef59030257 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleDescriptionInputTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleDescriptionInputTests.cs @@ -5,6 +5,8 @@ using AwesomeAssertions; using Elastic.Changelog.Bundling; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -104,7 +106,11 @@ public async Task ResolveAsync_EmptyDescriptionFile_Fails() var result = await ResolveAsync(descriptionFile: " "); result.Success.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("--description-file requires a path")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error --description-file requires a path, or - to read from standard input."); } [Test] @@ -113,7 +119,11 @@ public async Task ResolveAsync_DescriptionAndEmptyFile_Fails() var result = await ResolveAsync(string.Empty, string.Empty); result.Success.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("mutually exclusive")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot("error --description, --description-file, and --clear-description are mutually exclusive."); } [Test] @@ -132,8 +142,13 @@ await FileSystem.File.WriteAllTextAsync( var result = await ResolveAsync(descriptionFile: path); result.Success.Should().BeFalse(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("NUL byte")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/Guid_1/desc.md The description read from {repo}/Guid_1/desc.md is not UTF-8 text because it contains a NUL byte. Save the source as UTF-8 and run the command again." + ); } [Test] @@ -144,6 +159,12 @@ public async Task ResolveAsync_StdinWithNulByte_Fails() var result = await ResolveAsync(descriptionFile: BundleDescriptionInput.StdinPath, stdin: stdin); result.Success.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("standard input")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error The description read from standard input is not UTF-8 text because it contains a NUL byte. Save the source as UTF-8 and run the command again." + ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs index a807c3ce8b..4fca4c09d7 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs @@ -8,6 +8,8 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -75,8 +77,26 @@ public async Task Bundle_WithFiles_IncludesOnlyNamedEntries() result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: keep.yaml"); - bundle.Should().NotContain("name: skip.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: keep.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -93,7 +113,13 @@ public async Task Bundle_WithFiles_MissingFile_ReturnsError() var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("File does not exist")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/Guid_1/missing.yaml File does not exist. Current directory: {repo}. Tip: Repeat --files for each file, or use comma-separated values (e.g., --files \"file1.yaml,file2.yaml\"). Paths support tilde (~) expansion and can be relative or absolute." + ); } [Test] @@ -113,7 +139,13 @@ public async Task Bundle_WithFilesAndPrs_ReturnsMutualExclusivityError() var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("Multiple filter options")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Multiple filter options cannot be specified together. You specified: --prs, --files. Please use only one filter option: --all, --input-products, --prs, --issues, or --files" + ); } [Test] @@ -135,8 +167,26 @@ public async Task Bundle_WithPathListFile_IncludesListedEntries() result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: keep.yaml"); - bundle.Should().NotContain("name: skip.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: keep.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -175,8 +225,26 @@ public async Task Bundle_WithProfile_PathListFile_FiltersCorrectly() result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); var bundle = await FileSystem.File.ReadAllTextAsync(expectedOutput, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: keep.yaml"); - bundle.Should().NotContain("name: skip.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: keep.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -210,7 +278,13 @@ await FileSystem.File.WriteAllTextAsync( var result = await ServiceWithConfig.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("not a mix")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error {repo}/Guid_1/mixed.txt File must contain only pull request URLs, only issue URLs, or only changelog YAML paths — not a mix." + ); } [Test] @@ -241,8 +315,26 @@ public async Task Bundle_WithFiles_RulesBundleStillApplies() result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: feature.yaml"); - bundle.Should().NotContain("name: bug-fix.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: feature.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -273,7 +365,27 @@ public async Task Bundle_WithFilesAndForceLocal_SourcesLocalEvenWhenRepoResolves result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); handler.RequestedPaths.Should().BeEmpty("--force-local must not reach the CDN"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("Keep me"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: keep.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -296,8 +408,27 @@ public async Task Bundle_WithFiles_RepoResolves_MatchesCdnPoolByFileName() handler.RequestedPaths.Should().Contain("/changelog/elastic/elasticsearch/main/keep.yaml"); handler.RequestedPaths.Should().NotContain("/changelog/elastic/elasticsearch/main/registry.json"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: keep.yaml"); - bundle.Should().NotContain("name: skip.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: keep.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } [Test] @@ -319,8 +450,11 @@ public async Task Bundle_WithFiles_CdnPoolMissingRequestedName_FailsBundle() // --files uses FetchNamedAsync: a 404 means "entry does not exist in the pool" Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("never-uploaded.yaml") && d.Message.Contains("pool")); + .MatchInlineSnapshot( + "error Changelog entry 'never-uploaded.yaml' for 'elastic/elasticsearch/main' could not be fetched from https://d10xozp44eyz7q.cloudfront.net/changelog/elastic/elasticsearch/main/never-uploaded.yaml: 404 Not Found — entry does not exist in the pool. Ensure the entry was uploaded (changelog upload), or pass --force-local / --directory to bundle local files instead." + ); } [Test] @@ -367,8 +501,27 @@ public async Task Bundle_WithProfile_PathListFile_RepoResolves_SourcesFromCdn() FileSystem.Path.Join(outputDir, "changelog-bundle.yaml"), TestContext.Current!.Execution.CancellationToken ); - bundle.Should().Contain("name: keep.yaml"); - bundle.Should().NotContain("name: skip.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: keep.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } private static StubHandler CdnPoolHandler() => @@ -440,7 +593,27 @@ public async Task Bundle_WithForceLocal_SourcesLocalDespiteResolvableRepo() result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); handler.RequestedPaths.Should().BeEmpty("--force-local must not reach the CDN"); var bundle = await FileSystem.File.ReadAllTextAsync(output, TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("name: 1-local.yaml"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + lifecycle: ga + repo: elasticsearch + release-date: {today} + entries: + - file: + name: 1-local.yaml + checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + type: feature + title: Keep me + products: + - product: elasticsearch + versions: + - 9.3.0 + lifecycle: ga + """ + ); } private sealed class StubHandler(Func responder) : HttpMessageHandler diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs index f5d20d2432..c3eb8e4117 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs @@ -9,7 +9,9 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -194,26 +196,21 @@ public async Task ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitR var bundle = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); // Pool entries win over inference. - bundle.Should().Contain("Faster hosted search"); - bundle.Should().Contain("Sturdier snapshots"); - - // The synthesized entry carries PR-body release-note text, the Impact and Action sections, - // the label-derived type, and the profile's output products. - bundle.Should().Contain("Sharper autocomplete"); - bundle.Should().Contain("Autocomplete now ranks recent indices first."); - bundle.Should().Contain("impact: Saved searches that relied on alphabetical order change position."); - bundle.Should().Contain("action: Pin the indices you need at the top."); - bundle.Should().Contain("name: 300.yaml"); - Logged.Should().Contain(l => l.StartsWith("notes:") && l.Contains("first paragraph only")); + bundle.Should().MatchSnapshot(); - // The published endpoint ref is recorded as bundle metadata. - bundle.Should().Contain($"git_ref: {EndRef}"); + Logged.Should().Contain(l => l.StartsWith("notes:") && l.Contains("first paragraph only")); // PR 400 is reported, not silently dropped. Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Warning && d.Message.Contains("pull/400") && d.Message.Contains("could not be fetched")); + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + warning No checked-in changelog entry was found for PR https://github.com/elastic/widget/pull/400 and its metadata could not be fetched from GitHub. The bundle will not include an entry for this PR. + """ + ); } [Test] @@ -271,8 +268,28 @@ public async Task ProfileMode_BackportLabelConfigured_BundlesTheOriginalInsteadO FileSystem.Directory.GetFiles(outputDir, "*.yaml", SearchOption.AllDirectories).Single(), TestContext.Current!.Execution.CancellationToken ); - bundle.Should().Contain("Faster hosted search"); - bundle.Should().NotContain("300"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-hosted + target: 2026-08-13 + repo: widget + owner: elastic + release-date: {today} + git_ref: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 + entries: + - file: + name: 100.yaml + checksum: c2b6527c04530bf3cfffb127578481f3a1131e54 + type: feature + title: Faster hosted search + products: + - product: cloud-hosted + versions: + - 2026-08-13 + lifecycle: ga + """ + ); A.CallTo(prService).MustNotHaveHappened(); } @@ -354,8 +371,11 @@ public async Task ProfileMode_WithADecisionLog_RecordsThePerPullRequestDecisions lines.Should().Contain("notes: first paragraph only #300 · no release note #301"); Collector .Diagnostics + .ToSnapshotText() .Should() - .NotContain(d => d.Message.Contains("No release note description") || d.Message.Contains("Only the first paragraph")); + .MatchInlineSnapshot( + "warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead." + ); } private sealed class LineLogger(List lines) : Microsoft.Extensions.Logging.ILogger @@ -511,8 +531,38 @@ public async Task ProfileMode_CdnNotesForTheTarget_AreMergedIntoTheBundle() FileSystem.Directory.GetFiles(outputDir, "*.yaml", SearchOption.AllDirectories).Single(), TestContext.Current!.Execution.CancellationToken ); - bundle.Should().Contain("Faster hosted search"); - bundle.Should().Contain("Heads up for the August release"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-hosted + target: 2026-08-13 + repo: widget + owner: elastic + release-date: {today} + git_ref: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 + entries: + - file: + name: 100.yaml + checksum: c2b6527c04530bf3cfffb127578481f3a1131e54 + type: feature + title: Faster hosted search + products: + - product: cloud-hosted + versions: + - 2026-08-13 + lifecycle: ga + - file: + name: main/note-heads-up.yml + checksum: c58b01d5eaf57a4b95f4686599684868e02f79ab + type: known-issue + title: Heads up for the August release + products: + - product: cloud-hosted + versions: + - 2026-08-13 + lifecycle: ga + """ + ); } [Test] @@ -551,7 +601,13 @@ public async Task StartRefWithoutEndRef_Errors() var result = await service.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("must be provided together")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error --start-git-ref and --end-git-ref must be provided together; the start ref is never inferred from previous bundles." + ); } [Test] @@ -572,8 +628,11 @@ public async Task GitRefCombinedWithOtherFilter_Errors() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Error && d.Message.Contains("cannot be combined with other filter sources")); + .MatchInlineSnapshot( + "error --start-git-ref/--end-git-ref cannot be combined with other filter sources: --prs. The PR list is derived from the commit range itself." + ); } [Test] @@ -613,7 +672,13 @@ public async Task ProfileWithProductsPattern_Errors() var result = await service.BundleChangelogs(Collector, input, TestContext.Current!.Execution.CancellationToken); result.Should().BeFalse(); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("products pattern")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Profile 'filtered' has a 'products' pattern configured. A git commit range cannot be combined with a products pattern filter; use 'output_products' and 'rules' to shape the bundle." + ); } [Test] @@ -652,9 +717,39 @@ public async Task InferredEntry_NoTypeLabel_DefaultsToOtherWithWarning() var outputFiles = FileSystem.Directory.GetFiles(outputDir, "*.yaml", SearchOption.AllDirectories); outputFiles.Should().ContainSingle(); var bundle = await FileSystem.File.ReadAllTextAsync(outputFiles[0], TestContext.Current!.Execution.CancellationToken); - bundle.Should().Contain("Unlabeled change"); - bundle.Should().Contain("type: other"); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Warning && d.Message.Contains("defaulting to 'other'")); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-hosted + target: 2026-08-13 + repo: widget + owner: elastic + release-date: {today} + git_ref: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 + entries: + - file: + name: 999.yaml + checksum: 4d4d007a3c2b6c3d737ef99d58bd66c2cfac8c14 + type: other + title: Unlabeled change + products: + - product: cloud-hosted + versions: + - 2026-08-13 + prs: + - "https://github.com/elastic/widget/pull/999" + """ + ); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + warning https://github.com/elastic/widget/pull/999 Could not derive a changelog type from the labels of PR #999; defaulting to 'other'. Configure pivot.types in changelog.yml to map labels to types. + """ + ); Logged.Should().Contain(l => l.StartsWith("notes:") && l.Contains("no release note")); } @@ -798,7 +893,17 @@ public async Task InferredEntry_Products_AStackOnlyLabelKeepsThePullRequestOutOf // The only entry carries elasticsearch alone, so rules.bundle drops it and the range bundle fails. result.Should().BeFalse(); bundle.Should().BeNull(); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("No changelog entries remained", StringComparison.Ordinal)); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + hint Applied rules - excluded_disjoint: 1 entries + error No changelog entries remained after applying rules.bundle filter + """ + ); } [Test] @@ -815,12 +920,13 @@ public async Task InferredEntry_Products_TwoMappedProductsAndNoDefault_FailAPull bundle.Should().BeNull(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains( - "No matching product label found on PR https://github.com/elastic/widget/pull/300", - StringComparison.Ordinal - ) && d.Message.Contains("stateful, serverless", StringComparison.Ordinal) + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + error https://github.com/elastic/widget/pull/300 No matching product label found on PR https://github.com/elastic/widget/pull/300. Add one of the following labels: stateful, serverless. + """ ); } @@ -835,7 +941,16 @@ public async Task InferredEntry_Products_TwoMappedProductsAndNoDefault_SkipLabel "cloud-serverless" ); - Collector.Diagnostics.Should().NotContain(d => d.Message.Contains("No matching product label found", StringComparison.Ordinal)); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + error No changelog entries could be resolved for commit range aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 of elastic/widget. + """ + ); } [Test] @@ -850,8 +965,16 @@ public async Task InferredEntry_Products_WildcardOutputProductsAndPerProductRule "*" ); - Collector.Diagnostics.Should().NotContain(d => d.Message.Contains("No matching product label found", StringComparison.Ordinal)); - Collector.Diagnostics.Should().NotContain(d => d.Message.Contains("Cannot determine products", StringComparison.Ordinal)); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + error No changelog entries could be resolved for commit range aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 of elastic/widget. + """ + ); } [Test] @@ -977,12 +1100,13 @@ public async Task InferredEntry_Areas_RequiredAndNoAreaLabel_FailAPullRequest() bundle.Should().BeNull(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains( - "No matching area label found on PR https://github.com/elastic/widget/pull/300", - StringComparison.Ordinal - ) + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + error https://github.com/elastic/widget/pull/300 No matching area label found on PR https://github.com/elastic/widget/pull/300. Add one of the following labels: :Search. + """ ); } @@ -1014,9 +1138,13 @@ public async Task InferredEntry_Products_RequiredDespiteProductsDefault_FailAPul bundle.Should().BeNull(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("No matching product label found on PR", StringComparison.Ordinal) + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + error https://github.com/elastic/widget/pull/300 No matching product label found on PR https://github.com/elastic/widget/pull/300. Add one of the following labels: stateful, serverless. + """ ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleOutputConventionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleOutputConventionTests.cs index fd312b3927..3054fdd721 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleOutputConventionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleOutputConventionTests.cs @@ -7,6 +7,8 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -77,11 +79,13 @@ public async Task ProfileWithOutputPattern_WarnsAndBundlesByConvention() ); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains("output is deprecated") && d.Message.Contains( - "{repo}-{product}-{version}.yaml" - ) + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.es-release.output is deprecated. Bundle output names are derived by convention as {repo}-{product}-{version}.yaml. Remove this field. + warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other. + """ ); FileSystem.File.Exists(FileSystem.Path.Join(_changelogDir, "elasticsearch", "elasticsearch-9.3.0.yaml")).Should().BeTrue(); } @@ -111,8 +115,14 @@ public async Task OutputPatternOnAnotherProfile_WarnsAndDoesNotBlock() ); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Warning && d.Message.Contains("bundle.profiles.legacy.output is deprecated")); + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.legacy.output is deprecated. Bundle output names are derived by convention as {repo}-{product}-{version}.yaml. Remove this field. + warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other. + """ + ); FileSystem.File.Exists(FileSystem.Path.Join(_changelogDir, "elasticsearch", "elasticsearch-9.3.0.yaml")).Should().BeTrue(); } @@ -140,11 +150,14 @@ public async Task ProfilesCollidingOnPrimaryProduct_EmitError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("'es-all', 'es-ga'") && d.Message.Contains( - "{repo}-elasticsearch-{version}.yaml" - ) + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.es-ga.output_products is deprecated. Use 'product: ' instead. + warning {repo}/Guid_1/changelog.yml bundle.profiles.es-all.output_products is deprecated. Use 'product: ' instead. + error Profiles 'es-all', 'es-ga' all resolve to the same '{repo}-elasticsearch-{version}.yaml' bundle target for any given version. Bundle names are derived by convention from the authoring repo and the profile's primary output product, so each profile must target a distinct product. + """ ); } @@ -176,8 +189,14 @@ public async Task ProfileWithoutOutput_WritesConventionalName() .BeTrue("when no authoring repo resolves, names fall back to the unprefixed product-version.yaml convention"); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Warning && d.Message.Contains("Could not resolve a repository name")); + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.es-release.output_products is deprecated. Use 'product: ' instead. + warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other. + """ + ); } [Test] @@ -400,7 +419,16 @@ public async Task Plan_ProfileWithOutputPattern_WarnsAndSucceeds() plan.Should().NotBeNull(); FileSystem.Path.GetFileName(plan.OutputPath).Should().Be("elasticsearch-9.3.0.yaml"); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Warning && d.Message.Contains("output is deprecated")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.es-release.output is deprecated. Bundle output names are derived by convention as {repo}-{product}-{version}.yaml. Remove this field. + warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other. + """ + ); } [Test] @@ -521,8 +549,11 @@ public async Task OptionMode_MissingProductAndVersion_WarnsAndUsesFallbackName() .BeTrue("option mode without a concrete product and version keeps the legacy fallback file name"); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Warning && d.Message.Contains("Could not resolve a product and version")); + .MatchInlineSnapshot( + "warning Could not resolve a product and version for the bundle file (pass --output-products or --input-products with a concrete target). Using 'changelog-bundle.yaml'." + ); } [Test] @@ -670,11 +701,13 @@ public async Task ProfileOutputDirectory_YamlFilePath_EmitsHardError() result.Should().BeFalse(); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Error && d.Message.Contains("'output_directory' must be a directory") && d.Message.Contains( - "{repo}-{product}-{version}.yaml" - ) + .MatchInlineSnapshot( + """ + warning {repo}/Guid_1/changelog.yml bundle.profiles.es-release.output_directory is deprecated. The output directory is derived automatically as bundle.output_directory/{product}. + error Profile 'es-release': 'output_directory' must be a directory, not a file path. The bundle file name is derived by convention as '{repo}-{product}-{version}.yaml'. To choose a folder, set output_directory the same way option-mode --output does when it is a directory. + """ ); } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs index 04e3a15de4..b71620fdf1 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundlePlanTests.cs @@ -7,6 +7,8 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -167,8 +169,11 @@ public async Task Plan_ProfileMode_NoRepo_ReturnsNoNetwork() result.CdnUrl.Should().Be("https://d10xozp44eyz7q.cloudfront.net/bundle/elasticsearch/elasticsearch-9.2.0.yaml"); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain(d => d.Severity == Severity.Warning && d.Message.Contains("Could not resolve a repository name")); + .MatchInlineSnapshot( + "warning Could not resolve a repository name for the bundle file (set bundle.repo, pass --repo, or run from a git checkout with a github.com origin). Using '{product}-{version}.yaml'; two repositories publishing the same product and version may overwrite each other." + ); } [Test] @@ -413,8 +418,13 @@ public async Task Plan_ProfileWithDescription_RejectsCliDescription() ); result.Should().BeNull(); - Collector.Errors.Should().BeGreaterThan(0); - Collector.Diagnostics.Should().Contain(d => d.Message.Contains("--description")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error When using a profile, --description and --description-file are not allowed if bundle.description or the profile description is set. Remove the CLI description, or remove the description from changelog.yml." + ); } [Test] diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs index e40717fe82..3638ec1c8b 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleProfileGitHubReleaseTests.cs @@ -6,6 +6,8 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -45,11 +47,10 @@ public async Task SourceGithubRelease_EmitsDeprecationWarning() Collector.Errors.Should().Be(0); Collector .Diagnostics + .ToSnapshotText() .Should() - .Contain( - d => d.Severity == Severity.Warning && d.Message.Contains("source") && d.Message.Contains( - "deprecated" - ) && d.Message.Contains("bundle.releases.github") + .MatchInlineSnapshot( + "warning {repo}/Guid_1/changelog.yml bundle.profiles.es-release.source is deprecated. Use bundle.releases.github to map release tags to profiles instead." ); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs index abee083cd6..b4cf94c39a 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.ReleaseNotes; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -95,8 +96,40 @@ await WriteChangelog( Collector.Errors.Should().Be(0); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("pr-12345.yaml"); - bundleContent.Should().Contain("pr-12346.yaml"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: pr-12345.yaml + checksum: 6cb20a9554c7d77cf5571d4c8f4ba75e97a89ac3 + type: bug-fix + title: Fix query parsing + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + - file: + name: pr-12346.yaml + checksum: bb5e93521f76b6774193a3765d27744bd283cd27 + type: feature + title: New aggregation API + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/12346" + """ + ); } // ----------------------------------------------------------------------- @@ -151,8 +184,31 @@ await WriteChangelog( // Assert result.Should().BeTrue(); var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current!.Execution.CancellationToken); - bundleContent.Should().Contain("product: elasticsearch"); - bundleContent.Should().Contain("product: cloud-hosted"); + bundleContent.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-hosted + target: 2025-10-31 + lifecycle: ga + - product: elasticsearch + target: 9.2.0 + lifecycle: ga + release-date: {today} + entries: + - file: + name: pr-12345.yaml + checksum: 6cb20a9554c7d77cf5571d4c8f4ba75e97a89ac3 + type: bug-fix + title: Fix query parsing + products: + - product: elasticsearch + versions: + - 9.2.0 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/12345" + """ + ); } // ----------------------------------------------------------------------- diff --git a/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs index 1dabfc62be..84b7e2d7bd 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs @@ -8,6 +8,7 @@ using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -181,17 +182,29 @@ await FileSystem.File.WriteAllTextAsync( // Sound bundle: the matching feature is present and resolved; the docs entry and the other-product // entry are excluded. - bundle.Should().Contain("Faster hosted search"); - bundle.Should().NotContain("Tidy up the hosted docs", "rules.bundle.exclude_types drops docs entries"); - bundle.Should().NotContain("Serverless-only change", "the cloud-hosted product filter excludes other products"); - - // Link sanitization is now handled exclusively by the scrubber Lambda, not at bundle creation time. - // Both the public and private repo links pass through unscrubbed into the private S3 bundle. - bundle.Should().Contain("- \"https://github.com/elastic/elasticsearch/pull/100\""); - bundle.Should().Contain("- \"https://github.com/elastic/widget-internal/pull/7\""); - - // release_dates: false → no auto-populated release date. - bundle.Should().NotContain("release_date"); + bundle.Should().MatchInlineSnapshot( + """ + products: + - product: cloud-hosted + target: 2026-05 + repo: widget + owner: elastic + entries: + - file: + name: 1-feature.yaml + checksum: 31e5ddcf2ce48521b213aee97c063e8402f6acca + type: feature + title: Faster hosted search + products: + - product: cloud-hosted + versions: + - 2026-05-15 + lifecycle: ga + prs: + - "https://github.com/elastic/elasticsearch/pull/100" + - "https://github.com/elastic/widget-internal/pull/7" + """ + ); } private static HttpResponseMessage Json(string body) => diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs index 1f9bf1a088..bbfc97d225 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using System.Net; -using System.Text.RegularExpressions; using AwesomeAssertions; using Elastic.Changelog.GitHub; using Elastic.Changelog.GithubRelease; @@ -470,10 +469,8 @@ public async Task Bundle_IncludesPoolEntriesWithScrubbedPrsReferences() var bundleFiles = FileSystem.Directory.GetFiles(bundlesDir, "*.yml"); bundleFiles.Should().ContainSingle(); var bundle = await FileSystem.File.ReadAllTextAsync(bundleFiles[0], TestContext.Current!.Execution.CancellationToken); - // the bundle is stamped with the day it was created bundle.Should().MatchInlineSnapshot( - options: new SnapshotOptions().Scrub(new Regex(@"release-date: \d{4}-\d{2}-\d{2}"), "release-date: {today}"), - expected: """ + """ products: - product: elasticsearch target: 9.2.0 diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs index 438fc7ef89..babc5a9157 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs @@ -153,6 +153,7 @@ public async Task CreateChangelog_WithExtractReleaseNotes_LongReleaseNote_UsesAs files.Should().HaveCount(1); var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current!.Execution.CancellationToken); + // Commas trigger SharpYaml's more conservative plain-scalar quoting rules. yamlContent .WithoutSchemaComments() .Should() @@ -491,6 +492,7 @@ public async Task CreateChangelog_WithExtractReleaseNotes_ExplicitDescription_Ta public async Task CreateChangelog_WhenExtractNotSpecifiedByCli_UsesConfigExtractReleaseNotes() { // When CLI does not pass --no-extract-release-notes, config extract.release_notes applies. + // Config has release_notes: false, so PR title is used instead of extracted release note. var prInfo = new GitHubPrInfo { diff --git a/tests/Elastic.Changelog.Tests/Changelogs/UnifiedProductBundlerTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/UnifiedProductBundlerTests.cs index dc6befef53..62f4243930 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/UnifiedProductBundlerTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/UnifiedProductBundlerTests.cs @@ -12,7 +12,9 @@ using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Configuration.ReleaseSchedule; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Testing; using FakeItEasy; +using Nullean.Snapshots; namespace Elastic.Changelog.Tests.Changelogs; @@ -175,7 +177,13 @@ public async Task Bundle_NoChangelogYmlWhenAsked_Fails() var outcome = await BundleAsync(Bundler(Cdn()), failWhenNotOnboarded: true); outcome.Should().Be(ProductBundleOutcome.Failed); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("has no changelog.yml")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + "error Product 'widget' (repo: elastic/widget@aaaaaaaa) has no changelog.yml — add docs/changelog.yml or changelog.yml to the repository, or provide a prototype in --changelog-configs." + ); } [Test] @@ -222,7 +230,16 @@ public async Task Bundle_NoEntryPoolWhenAsked_FailsWithTheRegistryError() var outcome = await BundleAsync(Bundler(Cdn(poolBranch: "elsewhere")), failWhenNotOnboarded: true); outcome.Should().Be(ProductBundleOutcome.Failed); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("registry")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/bundles/widget/changelog-config-widget-aaaaaaaa.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + error Could not fetch changelog entry registry for 'elastic/widget/main' from https://d10xozp44eyz7q.cloudfront.net/changelog/elastic/widget/main/registry.json: Response status code does not indicate success: 404 (Not Found). + """ + ); } [Test] @@ -341,7 +358,17 @@ public async Task Bundle_UnreadablePullRequest_StillFails() var outcome = await BundleAsync(Bundler(Cdn(), resolution: OnePullRequest(), summaries: summaries)); outcome.Should().Be(ProductBundleOutcome.Failed); - Collector.Diagnostics.Should().Contain(d => d.Severity == Severity.Error && d.Message.Contains("could be resolved")); + Collector + .Diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + warning {repo}/bundles/widget/changelog-config-widget-aaaaaaaa.yml bundle.profiles.promotion.output_products is deprecated. Use 'product: ' instead. + warning No checked-in changelog entry was found for PR https://github.com/elastic/widget/pull/100 and its metadata could not be fetched from GitHub. The bundle will not include an entry for this PR. + error No changelog entries could be resolved for commit range aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 of elastic/widget. + """ + ); } private string LocalChangelogDirectory() diff --git a/tests/Elastic.Changelog.Tests/SnapshotSetup.cs b/tests/Elastic.Changelog.Tests/SnapshotSetup.cs index cdf60e41b6..2ac640d8c8 100644 --- a/tests/Elastic.Changelog.Tests/SnapshotSetup.cs +++ b/tests/Elastic.Changelog.Tests/SnapshotSetup.cs @@ -12,5 +12,27 @@ internal static class SnapshotSetup // Most tests work in a folder named after a fresh Guid, and its path shows up in file paths in the output. // Number the Guids (Guid_1, Guid_2, ...) so that the same folder reads the same everywhere in one snapshot. [ModuleInitializer] - internal static void ScrubGuids() => SnapshotDefaults.AddScrubber(Scrubbers.Guids); + internal static void Register() + { + SnapshotDefaults.AddScrubber(Scrubbers.Guids); + SnapshotDefaults.AddScrubber(Today); + } + + // A bundle is stamped with the day it was created. Only that day is replaced, so a release-date a test sets on purpose stays visible. + private static string Today(string text) + { + const string key = "release-date: "; + if (!text.Contains(key, StringComparison.Ordinal)) + return text; + var days = new[] { DateTime.UtcNow, DateTime.Now } + .Select(d => d.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture)) + .Distinct() + .ToArray(); + return string.Join( + '\n', + text.Split('\n').Select( + line => line.StartsWith(key, StringComparison.Ordinal) && days.Contains(line[key.Length..].Trim()) ? key + "{today}" : line + ) + ); + } } From d4fd84028980711b26dce8db7e24bdd42f78ddc3 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 12:53:44 +0100 Subject: [PATCH 06/12] Changelog amend tests: snapshot the CDN amend sidecars The four CDN amend tests checked a few Contain fragments of the sidecar that ReadSingleAmendAsync returns. They snapshot it inline now. The tests became stricter; the old fragments (exclude-entries, the entry names, the title) were checked against the received output first. Co-Authored-By: Claude Sonnet 5.5 --- .../Changelogs/BundleAmendCdnTests.cs | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs index 73afd95056..42aa00c05d 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs @@ -62,8 +62,19 @@ public async Task Amend_Remove_CdnPath_NoLocalFile_ExcludesByChecksum() handler.RequestedPaths.Should().NotContain(p => p.EndsWith("/registry.json", StringComparison.Ordinal)); handler.RequestedPaths.Should().Contain("/changelog/elastic/elasticsearch/main/existing.yaml"); var amend = await ReadSingleAmendAsync(bundlePath); - amend.Should().Contain("exclude-entries:"); - amend.Should().Contain("name: existing.yaml"); + amend.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + repo: elasticsearch + owner: elastic + exclude-entries: + - file: + name: existing.yaml + checksum: 00aa5d9baec30f5521395470896238423863546e + """ + ); } [Test] @@ -85,8 +96,25 @@ public async Task Amend_Add_CdnBasename_NoLocalFile_EmbedsEntry() .Should() .NotContain(p => p.EndsWith("/existing.yaml", StringComparison.Ordinal), "only requested names are fetched"); var amend = await ReadSingleAmendAsync(bundlePath); - amend.Should().Contain("title: Late addition"); - amend.Should().Contain("name: late.yaml"); + amend.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + repo: elasticsearch + owner: elastic + entries: + - file: + name: late.yaml + checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + type: enhancement + title: Late addition + products: + - product: elasticsearch + versions: + - 9.3.0 + """ + ); } [Test] @@ -132,7 +160,25 @@ public async Task Amend_ForceLocal_DoesNotHitCdn() result.Should().BeTrue($"Errors: {string.Join("; ", Collector.Diagnostics.Select(d => d.Message))}"); handler.RequestedPaths.Should().BeEmpty("--force-local must not reach the CDN"); var amend = await ReadSingleAmendAsync(bundlePath); - amend.Should().Contain("title: Late addition"); + amend.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + repo: elasticsearch + owner: elastic + entries: + - file: + name: late.yaml + checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + type: enhancement + title: Late addition + products: + - product: elasticsearch + versions: + - 9.3.0 + """ + ); } [Test] @@ -194,8 +240,19 @@ await FileSystem.File.WriteAllTextAsync( handler.RequestedPaths.Should().Contain("/changelog/elastic/elasticsearch/main/300.yaml"); handler.RequestedPaths.Should().NotContain(p => p.EndsWith("/registry.json", StringComparison.Ordinal)); var amend = await ReadSingleAmendAsync(bundlePath); - amend.Should().Contain("name: 300.yaml"); - amend.Should().Contain("exclude-entries:"); + amend.Should().MatchInlineSnapshot( + """ + products: + - product: elasticsearch + target: 9.3.0 + repo: elasticsearch + owner: elastic + exclude-entries: + - file: + name: 300.yaml + checksum: inferred-placeholder + """ + ); } private ChangelogBundleAmendService ServiceWithCdn(StubHandler handler) From e3ff2669b3ae29739d5ece8b4ae66ebed0ace856 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 14:09:32 +0100 Subject: [PATCH 07/12] Scrub paths the same way on every operating system The repository and temp folder scrubbers replaced only the root, so a Windows run printed {repo}\docs\index.md where the verified text says {repo}/docs/index.md, and about 160 diagnostics snapshots failed there. The path that follows the root is now written with forward slashes. The temp scrubber also swallowed everything up to the next whitespace, including the :line after a file name. A path now ends at a colon too, and the repository root is tried before the temp root because the repository can sit under the temp folder. The doc comment claimed it scrubbed git SHAs; it does not, and no longer says so. Tested with a Windows-style root, which also runs on Linux and macOS. Removing the separator replacement makes that test fail. Co-Authored-By: Claude Sonnet 5.5 --- .../Linked/SnapshotScrubbing.cs | 29 ++++++++++++------- .../Testing/SnapshotHelpersTests.cs | 15 ++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs index 6a80bb71fc..44132cd7bd 100644 --- a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs +++ b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs @@ -9,8 +9,9 @@ namespace Elastic.Documentation.Testing.Linked; /// -/// Scrubbers every snapshot in docs-builder's tests gets: machine-specific paths and git SHAs never belong in a -/// verified file. Runs when any test project referencing this assembly loads it. +/// Scrubbers every snapshot in docs-builder's tests gets: the repository root and the temp folder are replaced by +/// {repo} and {temp}, and the rest of such a path is written with forward slashes, so a verified file +/// reads the same on every machine and operating system. Compiled into each test project. /// internal static class SnapshotScrubbing { @@ -19,14 +20,22 @@ internal static class SnapshotScrubbing #pragma warning restore CA2255 internal static void Register() { - SnapshotDefaults.AddScrubber( - Scrubbers.Regex( - new Regex(Regex.Escape(Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar)) + @"[^\s""'<>]*", RegexOptions.Compiled), - "{temp}" - ) - ); + // the more specific root first: the repository may sit under the temp folder if (FindSolutionRoot() is { } root) - SnapshotDefaults.AddScrubber(Scrubbers.Regex(new Regex(Regex.Escape(root), RegexOptions.Compiled), "{repo}")); + SnapshotDefaults.AddScrubber(Under(root, "{repo}")); + SnapshotDefaults.AddScrubber(Under(Path.GetTempPath(), "{temp}")); + } + + /// + /// Replaces with and writes the path that follows it with + /// forward slashes. The path ends at whitespace, a quote, an angle bracket or a colon, so file.md:3 keeps its line. + /// + internal static Func Under(string root, string token) + { + root = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var roots = new[] { root, root.Replace('\\', '/') }.Distinct().Select(Regex.Escape); + var pattern = new Regex($"(?:{string.Join('|', roots)})(?[^\\s\"'<>:*?|]*)", RegexOptions.Compiled); + return text => pattern.Replace(text, m => token + m.Groups["rest"].Value.Replace('\\', '/')); } private static string? FindSolutionRoot() @@ -34,6 +43,6 @@ internal static void Register() var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "docs-builder.slnx"))) dir = dir.Parent; - return dir?.FullName.TrimEnd(Path.DirectorySeparatorChar); + return dir?.FullName; } } diff --git a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs index 5897c67b4a..bef3f63064 100644 --- a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs +++ b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs @@ -6,6 +6,7 @@ using AwesomeAssertions; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Testing; +using Elastic.Documentation.Testing.Linked; using Nullean.Snapshots; using Nullean.Snapshots.Html; @@ -67,4 +68,18 @@ public void DocsHtmlSection_DropsFixtureHeadingAndHtmxAttributes() => """, DocsHtml.Section() ); + + [Test] + public void PathScrubber_WindowsRoot_WritesTheRestWithForwardSlashesAndKeepsTheLine() => + SnapshotScrubbing + .Under(@"D:\a\docs-builder\docs-builder", "{repo}")(@"error D:\a\docs-builder\docs-builder\docs\index.md:3 bad link") + .Should() + .MatchInlineSnapshot("error {repo}/docs/index.md:3 bad link"); + + [Test] + public void PathScrubber_UnixRootInsideText_LeavesOtherPathsAlone() => + SnapshotScrubbing + .Under("/work/repo/", "{repo}")("see /work/repo/docs/a.md and /elsewhere/b.md") + .Should() + .MatchInlineSnapshot("see {repo}/docs/a.md and /elsewhere/b.md"); } From 12c95f57cc576994e27cfe2084ffb1e61dc50507 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 14:12:40 +0100 Subject: [PATCH 08/12] Scrub random folder names under the temp directory A test that works in a Guid-named folder in the temp directory printed that name in its diagnostics, so a snapshot of them could not be stable. The temp scrubber now replaces a run of 32 hex digits in the path with {id}. Co-Authored-By: Claude Sonnet 5.5 --- .../Linked/SnapshotScrubbing.cs | 14 +++++++++++--- .../Testing/SnapshotHelpersTests.cs | 9 +++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs index 44132cd7bd..7c97d0d9b1 100644 --- a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs +++ b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs @@ -23,21 +23,29 @@ internal static void Register() // the more specific root first: the repository may sit under the temp folder if (FindSolutionRoot() is { } root) SnapshotDefaults.AddScrubber(Under(root, "{repo}")); - SnapshotDefaults.AddScrubber(Under(Path.GetTempPath(), "{temp}")); + // a test folder in the temp directory is usually named after a fresh Guid; that name is not part of what is verified + SnapshotDefaults.AddScrubber(Under(Path.GetTempPath(), "{temp}", RandomName)); } /// /// Replaces with and writes the path that follows it with /// forward slashes. The path ends at whitespace, a quote, an angle bracket or a colon, so file.md:3 keeps its line. /// - internal static Func Under(string root, string token) + internal static Func Under(string root, string token, Regex? randomName = null) { root = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var roots = new[] { root, root.Replace('\\', '/') }.Distinct().Select(Regex.Escape); var pattern = new Regex($"(?:{string.Join('|', roots)})(?[^\\s\"'<>:*?|]*)", RegexOptions.Compiled); - return text => pattern.Replace(text, m => token + m.Groups["rest"].Value.Replace('\\', '/')); + return text => pattern.Replace(text, m => + { + var rest = m.Groups["rest"].Value.Replace('\\', '/'); + return token + (randomName is null ? rest : randomName.Replace(rest, "{id}")); + }); } + /// A run of 32 hex digits: a Guid.ToString("N") used as a folder name. + private static readonly Regex RandomName = new("[0-9a-fA-F]{32}", RegexOptions.Compiled); + private static string? FindSolutionRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); diff --git a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs index bef3f63064..21b3ae758a 100644 --- a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs +++ b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs @@ -82,4 +82,13 @@ public void PathScrubber_UnixRootInsideText_LeavesOtherPathsAlone() => .Under("/work/repo/", "{repo}")("see /work/repo/docs/a.md and /elsewhere/b.md") .Should() .MatchInlineSnapshot("see {repo}/docs/a.md and /elsewhere/b.md"); + + [Test] + public void TempScrubber_GuidFolderName_IsReplacedSoTheSnapshotIsStable() => + SnapshotScrubbing + .Under("/tmp", "{temp}", new System.Text.RegularExpressions.Regex("[0-9a-fA-F]{32}"))( + "error /tmp/diff-validate-test-1d248de3468f4d58849398ff8b320802/docs/redirects.yml Missing" + ) + .Should() + .MatchInlineSnapshot("error {temp}/diff-validate-test-{id}/docs/redirects.yml Missing"); } From 021c3c0f9588533105b2572ba69aacd38ce1360f Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 14:28:53 +0100 Subject: [PATCH 09/12] Make diagnostics snapshots and snapshot paths the same on every operating system CI on Windows failed five of the stacked PRs for reasons a laptop never shows: - DiagnosticsSnapshot now writes paths with forward slashes and drops a leading drive letter, so `C:\docs\toc.yml`, `C:/docs/toc.yml` and `/docs/toc.yml` read the same, and `testing\req.md` inside a message reads `testing/req.md`. - ToSnapshotText(firstLineOnly: true) keeps only the first line of each message, for diagnostics that carry an exception: its stack trace has source line numbers and the exception type differs per platform. - SnapshotPathLengthTests fails when a verified snapshot's path, with the received file's longer folder and the CI checkout root, would pass Windows' 260 characters. git on the runner cannot check such a file out, so the whole job failed before any test ran. Each has a test. Lowering the length limit makes the guard fail. Co-Authored-By: Claude Sonnet 5.5 --- .../DiagnosticsSnapshot.cs | 29 +++++++-- .../Testing/SnapshotHelpersTests.cs | 43 +++++++++++++ .../Testing/SnapshotPathLengthTests.cs | 60 +++++++++++++++++++ 3 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 tests/Elastic.Documentation.Tests/Testing/SnapshotPathLengthTests.cs diff --git a/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs b/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs index 0e8f94f1f2..f53161eea3 100644 --- a/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs +++ b/tests-shared/Elastic.Documentation.Testing/DiagnosticsSnapshot.cs @@ -3,17 +3,28 @@ // See the LICENSE file in the project root for more information using System.Text; +using System.Text.RegularExpressions; using Elastic.Documentation.Diagnostics; namespace Elastic.Documentation.Testing; -public static class DiagnosticsSnapshot +public static partial class DiagnosticsSnapshot { /// /// One line per diagnostic, error|warning|hint file:line message, in the order they were written. Empty when /// there are none, so a clean run reads as MatchInlineSnapshot(""). + /// + /// Paths read the same on every operating system: backslashes become forward slashes and a leading drive letter is + /// dropped, so C:\docs\toc.yml, C:/docs/toc.yml and /docs/toc.yml are the same text. A message that + /// really contains a backslash reads with a slash, on Linux too. + /// /// - public static string ToSnapshotText(this IEnumerable diagnostics) + /// What a test's collector holds. + /// + /// Keep only the first line of each message. For diagnostics that carry an exception, whose stack trace (frames and + /// source line numbers) is no part of what a test verifies, and whose type and text can differ per platform. + /// + public static string ToSnapshotText(this IEnumerable diagnostics, bool firstLineOnly = false) { var sb = new StringBuilder(); foreach (var d in diagnostics) @@ -24,9 +35,19 @@ public static string ToSnapshotText(this IEnumerable diagnostics) Severity.Warning => "warning", _ => "hint" }; - var position = d.Line is { } line ? $"{d.File}:{line}" : d.File; - sb.Append(severity).Append(' ').Append(position).Append(' ').Append(d.Message).Append('\n'); + var file = Normalise(d.File); + var position = d.Line is { } line ? $"{file}:{line}" : file; + var message = d.Message ?? string.Empty; + if (firstLineOnly) + message = message.Split('\n')[0].TrimEnd('\r'); + _ = sb.Append(severity).Append(' ').Append(position).Append(' ').Append(Normalise(message)).Append('\n'); } return sb.ToString(); } + + private static string Normalise(string text) => DriveLetter().Replace(text.Replace('\\', '/'), string.Empty); + + // "C:/docs/..." at the start of a path; not the "s:" of "https://", which a letter or digit precedes + [GeneratedRegex("(? ) .Should() .MatchInlineSnapshot("error {temp}/diff-validate-test-{id}/docs/redirects.yml Missing"); + + [Test] + public void ToSnapshotText_WindowsPaths_ReadLikeUnixPaths() + { + var diagnostics = new List + { + new() + { + Severity = Severity.Error, + File = @"C:\docs\section-b\toc.yml", + Message = @"`sub-requirements2` does not exist in testing\req.md." + }, + new() { Severity = Severity.Error, File = "C:/docs/docset.yml", Line = 4, Message = "see https://elastic.co/docs and D:/x" }, + new() { Severity = Severity.Warning, File = @"/docs\docset.yml", Message = "mixed" } + }; + diagnostics + .ToSnapshotText() + .Should() + .MatchInlineSnapshot( + """ + error /docs/section-b/toc.yml `sub-requirements2` does not exist in testing/req.md. + error /docs/docset.yml:4 see https://elastic.co/docs and /x + warning /docs/docset.yml mixed + """ + ); + } + + [Test] + public void ToSnapshotText_FirstLineOnly_DropsTheStackTrace() + { + var diagnostics = new List + { + new() + { + Severity = Severity.Error, + File = "index.md", + Line = 3, + Message = + "registry could not be read\nSystem.IO.FileNotFoundException: Could not find file 'x'.\n at Some.Method() in /src/File.cs:line 209" + } + }; + diagnostics.ToSnapshotText(firstLineOnly: true).Should().MatchInlineSnapshot("error index.md:3 registry could not be read"); + } } diff --git a/tests/Elastic.Documentation.Tests/Testing/SnapshotPathLengthTests.cs b/tests/Elastic.Documentation.Tests/Testing/SnapshotPathLengthTests.cs new file mode 100644 index 0000000000..647cddfb22 --- /dev/null +++ b/tests/Elastic.Documentation.Tests/Testing/SnapshotPathLengthTests.cs @@ -0,0 +1,60 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using AwesomeAssertions; + +namespace Elastic.Documentation.Tests.Testing; + +/// +/// A verified snapshot is named after its test class and method, and a failed run writes the received file under +/// .artifacts/.snapshots/received/{Project}/, a longer path. Windows stops at 260 characters, and git on the CI runner +/// cannot even check out a file that is too long, so the whole job fails before any test runs. +/// +public class SnapshotPathLengthTests +{ + // "D:\a\docs-builder\docs-builder\" is where a GitHub runner puts the checkout. + private const int CheckoutRootLength = 31; + private const string ReceivedFolder = ".artifacts/.snapshots/received/"; + // Windows' MAX_PATH is 260 including the terminating character; keep a margin. + private const int MaxLength = 250; + + [Test] + public void VerifiedSnapshots_FitInAWindowsPath_EvenWhenTheReceivedFileIsWritten() + { + var root = FindRepositoryRoot(); + var tooLong = new List(); + foreach (var testsFolder in new[] { "tests", "tests-integration" }) + { + var folder = Path.Combine(root, testsFolder); + if (!Directory.Exists(folder)) + continue; + foreach (var project in Directory.EnumerateDirectories(folder)) + { + var snapshots = Path.Combine(project, ".snapshots"); + if (!Directory.Exists(snapshots)) + continue; + foreach (var file in Directory.EnumerateFiles(snapshots, "*", SearchOption.AllDirectories)) + { + var relative = Path.GetRelativePath(snapshots, file).Replace('\\', '/'); + var length = CheckoutRootLength + ReceivedFolder.Length + Path.GetFileName(project).Length + 1 + relative.Length; + if (length > MaxLength) + tooLong.Add($"{length} {Path.GetRelativePath(root, file).Replace('\\', '/')}"); + } + } + } + + tooLong.Should().BeEmpty( + "a snapshot path over {0} characters cannot be checked out on Windows; shorten the test's name or its class", + MaxLength + ); + } + + private static string FindRepositoryRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "docs-builder.slnx"))) + dir = dir.Parent; + return dir?.FullName ?? throw new InvalidOperationException("docs-builder.slnx not found above the test binaries"); + } +} From be85e9e517a3b8e923f7cd43177860a33f6f51b8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 14:31:18 +0100 Subject: [PATCH 10/12] Strip the changelog schema comments whatever the line ending WithoutSchemaComments looks for the blank line that ends the field documentation, "\n\n". The writer uses the platform's line ending, so on Windows it is "\r\n\r\n", the header stayed in, and 161 tests failed there. The helper now normalises line endings first. The new test runs both endings and fails for "\r\n" without the fix. Co-Authored-By: Claude Sonnet 5.5 --- .../Changelogs/ChangelogEntryYaml.cs | 2 ++ .../Changelogs/ChangelogEntryYamlTests.cs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYamlTests.cs diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs index 1e8c7a7505..6629cf3f43 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYaml.cs @@ -16,6 +16,8 @@ public static class ChangelogEntryYaml /// public static string WithoutSchemaComments(this string yaml) { + // the writer uses the platform's line ending, so on Windows the blank line that ends the header is "\r\n\r\n" + yaml = yaml.Replace("\r\n", "\n"); var subtype = yaml.IndexOf("# subtype:", StringComparison.Ordinal); if (subtype < 0) return yaml; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYamlTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYamlTests.cs new file mode 100644 index 0000000000..d650655784 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogEntryYamlTests.cs @@ -0,0 +1,23 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using AwesomeAssertions; +using Nullean.Snapshots; + +namespace Elastic.Changelog.Tests.Changelogs; + +public class ChangelogEntryYamlTests +{ + [Test] + [Arguments("\n")] + [Arguments("\r\n")] + public void WithoutSchemaComments_StripsTheFieldDocumentation_WhateverTheLineEnding(string newline) + { + var yaml = string.Join(newline, "# subtype: one of", "# a", "", "title: Feature", "type: feature", ""); + yaml.WithoutSchemaComments().Should().MatchInlineSnapshot(""" + title: Feature + type: feature + """); + } +} From 2b0c0d96482ca8657090d80cb9c58fe6556356eb Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 14:50:06 +0100 Subject: [PATCH 11/12] Match the repository root in the form DiagnosticsSnapshot writes it DiagnosticsSnapshot drops a leading drive letter and writes forward slashes, so on Windows D:\a\docs-builder\docs-builder\docs\index.md reaches the {repo} scrubber as /a/docs-builder/docs-builder/docs/index.md, which it no longer recognised. Every diagnostics snapshot that names a file failed on the Windows runner. The scrubber now also matches the root without its drive letter. Removing that variant makes the new test fail. Co-Authored-By: Claude Sonnet 5.5 --- .../Linked/SnapshotScrubbing.cs | 7 ++++++- .../Testing/SnapshotHelpersTests.cs | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs index 7c97d0d9b1..2a0413a362 100644 --- a/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs +++ b/tests-shared/Elastic.Documentation.Testing/Linked/SnapshotScrubbing.cs @@ -34,7 +34,10 @@ internal static void Register() internal static Func Under(string root, string token, Regex? randomName = null) { root = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - var roots = new[] { root, root.Replace('\\', '/') }.Distinct().Select(Regex.Escape); + // DiagnosticsSnapshot writes a path with forward slashes and without its drive letter, so the root has to match that way too + var forward = root.Replace('\\', '/'); + var withoutDrive = DriveLetter.Replace(forward, string.Empty); + var roots = new[] { root, forward, withoutDrive }.Where(r => r.Length > 1).Distinct().Select(Regex.Escape); var pattern = new Regex($"(?:{string.Join('|', roots)})(?[^\\s\"'<>:*?|]*)", RegexOptions.Compiled); return text => pattern.Replace(text, m => { @@ -43,6 +46,8 @@ internal static Func Under(string root, string token, Regex? ran }); } + private static readonly Regex DriveLetter = new("^[A-Za-z]:", RegexOptions.Compiled); + /// A run of 32 hex digits: a Guid.ToString("N") used as a folder name. private static readonly Regex RandomName = new("[0-9a-fA-F]{32}", RegexOptions.Compiled); diff --git a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs index ca2e42c247..c89933d40b 100644 --- a/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs +++ b/tests/Elastic.Documentation.Tests/Testing/SnapshotHelpersTests.cs @@ -134,4 +134,13 @@ public void ToSnapshotText_FirstLineOnly_DropsTheStackTrace() }; diagnostics.ToSnapshotText(firstLineOnly: true).Should().MatchInlineSnapshot("error index.md:3 registry could not be read"); } + + [Test] + public void PathScrubber_RootAfterDiagnosticsSnapshotNormalisedIt_StillReadsAsRepo() + { + // what a Windows run writes for D:\a\docs-builder\docs-builder\docs\index.md once the drive letter and backslashes are gone + var scrub = SnapshotScrubbing.Under(@"D:\a\docs-builder\docs-builder", "{repo}"); + scrub("error /a/docs-builder/docs-builder/docs/index.md:3 bad").Should().MatchInlineSnapshot("error {repo}/docs/index.md:3 bad"); + scrub(@"error D:\a\docs-builder\docs-builder\docs\index.md:3 bad").Should().MatchInlineSnapshot("error {repo}/docs/index.md:3 bad"); + } } From b86f2c87ab4c772d97f5f31fae38cabfcb64e894 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Thu, 5 Nov 2026 15:05:44 +0100 Subject: [PATCH 12/12] Scrub entry checksums from the changelog bundle snapshots A bundle records the SHA-1 of each entry file it was made from. The test inputs are raw string literals and the product writes files with the platform's line ending, so on Windows the same entry has other bytes and another checksum: 111 bundle snapshots failed there on the checksum line alone. The value is now replaced by {sha1} in the snapshots. What a checksum must be is still checked by the tests that compute the expected one from the file content. All 112 changed snapshots differ only in their checksum lines (reviewed before accepting). Co-Authored-By: Claude Sonnet 5.5 --- ...eProducts_IncludesAllProducts.verified.txt | 6 +- ...llback_WritesBundleWithGitRef.verified.txt | 6 +- .../Changelogs/BundleAmendCdnParentTests.cs | 4 +- .../Changelogs/BundleAmendCdnTests.cs | 6 +- .../Changelogs/BundleAmendTests.cs | 12 +- .../Changelogs/BundleCdnSourcingTests.cs | 20 +- .../Changelogs/BundleChangelogsTests.cs | 176 +++++++++--------- .../Changelogs/BundleFilesFilterTests.cs | 16 +- .../Changelogs/BundleGitRefTests.cs | 8 +- .../Changelogs/BundleReleaseVersionTests.cs | 6 +- .../Changelogs/CloudProfileFixtureTests.cs | 2 +- .../Create/GhReleaseExtractionParityTests.cs | 2 +- .../Elastic.Changelog.Tests/SnapshotSetup.cs | 7 + 13 files changed, 139 insertions(+), 132 deletions(-) diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt index 8a6e72facc..e1d12553a7 100644 --- a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleChangelogsTests/BundleChangelogs_WithMultipleProducts_IncludesAllProducts.verified.txt @@ -7,7 +7,7 @@ release-date: {today} entries: - file: name: 1755268130-elasticsearch.yaml - checksum: 953e2ec8fd6621dd6344173002e8f3911139b7bd + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -18,7 +18,7 @@ entries: - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-kibana.yaml - checksum: 5567d7bf362ba212f7ca11d9bb7df2eb47f365e7 + checksum: {sha1} type: feature title: Kibana feature products: @@ -29,7 +29,7 @@ entries: - "https://github.com/elastic/kibana/pull/200" - file: name: 1755268150-multi-product.yaml - checksum: ecd19ea9fd25964bc6d47e19ac9a150d80354485 + checksum: {sha1} type: feature title: Multi-product feature products: diff --git a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt index 297055418f..50c287de1a 100644 --- a/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt +++ b/tests/Elastic.Changelog.Tests/.snapshots/Changelogs/BundleGitRefTests/ProfileMode_PoolFirstWithInferredFallback_WritesBundleWithGitRef.verified.txt @@ -8,7 +8,7 @@ git_ref: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2 entries: - file: name: 100.yaml - checksum: c2b6527c04530bf3cfffb127578481f3a1131e54 + checksum: {sha1} type: feature title: Faster hosted search products: @@ -18,7 +18,7 @@ entries: lifecycle: ga - file: name: sturdier-snapshots.yaml - checksum: 137a6ba241fd79b77b8323a3390121023d97c9c2 + checksum: {sha1} type: bug-fix title: Sturdier snapshots products: @@ -30,7 +30,7 @@ entries: - "https://github.com/elastic/widget/pull/200" - file: name: 300.yaml - checksum: 89cf3f1b74b4c89f031f1cdf1486e323a6191ed2 + checksum: {sha1} type: feature title: Sharper autocomplete products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs index 5bb0f85ebc..3c405b308d 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnParentTests.cs @@ -70,7 +70,7 @@ public async Task Amend_CdnParent_WritesAmend1UnderOutput() entries: - file: name: late.yaml - checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + checksum: {sha1} type: enhancement title: Late addition products: @@ -305,7 +305,7 @@ public async Task Amend_CdnParent_YmlExtension_IgnoresMismatchedExtensionSidecar entries: - file: name: late.yaml - checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + checksum: {sha1} type: enhancement title: Late addition products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs index 42aa00c05d..e081646d00 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendCdnTests.cs @@ -72,7 +72,7 @@ public async Task Amend_Remove_CdnPath_NoLocalFile_ExcludesByChecksum() exclude-entries: - file: name: existing.yaml - checksum: 00aa5d9baec30f5521395470896238423863546e + checksum: {sha1} """ ); } @@ -106,7 +106,7 @@ public async Task Amend_Add_CdnBasename_NoLocalFile_EmbedsEntry() entries: - file: name: late.yaml - checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + checksum: {sha1} type: enhancement title: Late addition products: @@ -170,7 +170,7 @@ public async Task Amend_ForceLocal_DoesNotHitCdn() entries: - file: name: late.yaml - checksum: 36d7fa3de8781cf9b05f3cb857d956a7d1d9f718 + checksum: {sha1} type: enhancement title: Late addition products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs index 42c72e708d..ac831fe577 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs @@ -124,7 +124,7 @@ public async Task AmendBundle_AddFile_WritesResolvedEntryWithProvenance() entries: - file: name: 1755268200-new-feature.yaml - checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + checksum: {sha1} type: enhancement title: New amended feature products: @@ -166,7 +166,7 @@ public async Task AmendBundle_RemoveFromParent_CreatesExcludeEntries() exclude-entries: - file: name: 1755268130-existing.yaml - checksum: b9292cbbc010ccd0c8629091e0fa750814e62c3c + checksum: {sha1} """ ); } @@ -202,7 +202,7 @@ public async Task AmendBundle_RemoveAfterAdd_ExcludesAmendedEntry() exclude-entries: - file: name: 1755268200-new-feature.yaml - checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + checksum: {sha1} """ ); } @@ -271,7 +271,7 @@ public async Task AmendBundle_RemoveAndAdd_InSingleAmendFile() entries: - file: name: 1755268200-new-feature.yaml - checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + checksum: {sha1} type: enhancement title: New amended feature products: @@ -284,7 +284,7 @@ public async Task AmendBundle_RemoveAndAdd_InSingleAmendFile() exclude-entries: - file: name: 1755268130-existing.yaml - checksum: b9292cbbc010ccd0c8629091e0fa750814e62c3c + checksum: {sha1} """ ); } @@ -369,7 +369,7 @@ public async Task AmendBundle_Add_CopiesParentProductsIntoAmend() entries: - file: name: 1755268200-new-feature.yaml - checksum: 7f27409f310674a3d39fa303fa450feee030cca6 + checksum: {sha1} type: enhancement title: New amended feature products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs index d7d7dea7cb..10a4aa06b4 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleCdnSourcingTests.cs @@ -114,7 +114,7 @@ public async Task OptionMode_RepoResolvable_ProbesEntriesByPrNumber() entries: - file: name: 100.yaml - checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + checksum: {sha1} type: feature title: Alpha products: @@ -126,7 +126,7 @@ public async Task OptionMode_RepoResolvable_ProbesEntriesByPrNumber() - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 999.yaml - checksum: 6e589c7dca3e9a62fd9cd768eee589d7c7d4ee85 + checksum: {sha1} type: feature title: Bravo products: @@ -240,7 +240,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1-local.yaml - checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + checksum: {sha1} type: feature title: Alpha products: @@ -306,7 +306,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1-local.yaml - checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + checksum: {sha1} type: feature title: Alpha products: @@ -434,7 +434,7 @@ public async Task ProbeMiss_WarnsAndSkips() entries: - file: name: 100.yaml - checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + checksum: {sha1} type: feature title: Alpha products: @@ -529,7 +529,7 @@ public async Task MarkerEntry_ResolvesDepthOne() entries: - file: name: 100.yaml - checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + checksum: {sha1} type: feature title: Alpha products: @@ -587,7 +587,7 @@ public async Task MarkerEntry_DuplicateMarkersToSameParent_OneEntryInBundle() entries: - file: name: 100.yaml - checksum: b251ead159fddd9a1545d55bb4519db4ca519e0c + checksum: {sha1} type: feature title: Alpha products: @@ -808,7 +808,7 @@ public async Task BackportCollision_MainBranchWins_WarnAndKeepMain() entries: - file: name: main/note-known-issue.yml - checksum: bd81e86814509f9260869ca8af921b405ae6658f + checksum: {sha1} type: known-issue title: Known issue on main products: @@ -879,7 +879,7 @@ public async Task BackportCollision_NoMainOrMaster_KeepsOrdinalFirst() entries: - file: name: 9.4/note-known-issue.yml - checksum: 11799cc69fef6c8db52121edbe36e656b3a5ccc2 + checksum: {sha1} type: known-issue title: Known issue on 9.4 branch (backport) products: @@ -945,7 +945,7 @@ public async Task BackportCollision_IdenticalContent_ChecksumDedupHandlesIt() entries: - file: name: main/note-known-issue.yml - checksum: bd81e86814509f9260869ca8af921b405ae6658f + checksum: {sha1} type: known-issue title: Known issue on main products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs index 09eb884d5a..0cb40bbe65 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs @@ -97,7 +97,7 @@ public async Task BundleChangelogs_WithAllOption_CreatesValidBundle() entries: - file: name: 1755268130-first-changelog.yaml - checksum: a65016261a61152db249d295c2859270c5103765 + checksum: {sha1} type: feature title: First changelog products: @@ -109,7 +109,7 @@ public async Task BundleChangelogs_WithAllOption_CreatesValidBundle() - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-second-changelog.yaml - checksum: c1bd6cb897ac1f2920e24ff3df0432bf623f85fb + checksum: {sha1} type: enhancement title: Second changelog products: @@ -182,7 +182,7 @@ public async Task BundleChangelogs_WithProductsFilter_FiltersCorrectly() entries: - file: name: 1755268130-elasticsearch-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -266,7 +266,7 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly() entries: - file: name: 1755268130-first-pr.yaml - checksum: 013f5493de6c3e2792ecaa371eb23ab191f782f3 + checksum: {sha1} type: feature title: First PR products: @@ -277,7 +277,7 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly() - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-second-pr.yaml - checksum: a9663d09ccc6614bca7e68d6dbb0453afa09b8eb + checksum: {sha1} type: feature title: Second PR products: @@ -326,7 +326,7 @@ public async Task BundleChangelogs_WithPrsFilter_MatchesFilenameDigitsWhenYamlPr entries: - file: name: 12345.yaml - checksum: 43b5184607b93e16dd003a4c429c234f67752d29 + checksum: {sha1} type: feature title: Filename identity products: @@ -375,7 +375,7 @@ public async Task BundleChangelogs_WithPrsFilter_MatchesTimestampFileViaYamlPrs( entries: - file: name: 1735-foo.yaml - checksum: 5784adf1f207c98d98ce3240054b72a4f091c71c + checksum: {sha1} type: feature title: Timestamp identity products: @@ -457,7 +457,7 @@ public async Task BundleChangelogs_WithIssuesFilter_FiltersCorrectly() entries: - file: name: 1755268130-first-issue.yaml - checksum: d275812653428ee900546d0624ceabc690bbf15e + checksum: {sha1} type: feature title: First issue products: @@ -468,7 +468,7 @@ public async Task BundleChangelogs_WithIssuesFilter_FiltersCorrectly() - "https://github.com/elastic/elasticsearch/issues/100" - file: name: 1755268140-second-issue.yaml - checksum: 9fe1ccd05b9af020a6f9317045ab5bd59a2af2ac + checksum: {sha1} type: feature title: Second issue products: @@ -521,7 +521,7 @@ public async Task BundleChangelogs_WithOldPrFormat_StillMatchesWhenFilteringByPr entries: - file: name: 1755268130-legacy-pr.yaml - checksum: ff9d58350837176bd8e4980b8444e87adc83f2d3 + checksum: {sha1} type: feature title: Legacy PR changelog products: @@ -661,7 +661,7 @@ public async Task BundleChangelogs_WithPrsFileFilter_FiltersCorrectly() entries: - file: name: 1755268130-first-pr.yaml - checksum: 013f5493de6c3e2792ecaa371eb23ab191f782f3 + checksum: {sha1} type: feature title: First PR products: @@ -672,7 +672,7 @@ public async Task BundleChangelogs_WithPrsFileFilter_FiltersCorrectly() - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-second-pr.yaml - checksum: a9663d09ccc6614bca7e68d6dbb0453afa09b8eb + checksum: {sha1} type: feature title: Second PR products: @@ -733,7 +733,7 @@ public async Task BundleChangelogs_WithPrNumberAndOwnerRepo_FiltersCorrectly() entries: - file: name: 1755268130-pr-number.yaml - checksum: e8de710842df34ae2238ea319b4e6214306efa0b + checksum: {sha1} type: feature title: PR with number products: @@ -790,7 +790,7 @@ public async Task BundleChangelogs_WithShortPrFormat_FiltersCorrectly() entries: - file: name: 1755268130-short-format.yaml - checksum: 87a7358a293fc63fe9d571bed3d41af8aad5bfb5 + checksum: {sha1} type: feature title: PR with short format products: @@ -989,7 +989,7 @@ public async Task BundleChangelogs_WithMultipleProducts_CreatesValidBundle() entries: - file: name: 1755268130-cloud-feature1.yaml - checksum: e6ab478a8cf5a520992531e7f5fda93dbf19959d + checksum: {sha1} type: feature title: Cloud serverless feature 1 products: @@ -1000,7 +1000,7 @@ public async Task BundleChangelogs_WithMultipleProducts_CreatesValidBundle() - "https://github.com/elastic/cloud-serverless/pull/100" - file: name: 1755268140-cloud-feature2.yaml - checksum: d41de380d5e3827a96e56f39ef3c2892d810c242 + checksum: {sha1} type: feature title: Cloud serverless feature 2 products: @@ -1076,7 +1076,7 @@ public async Task BundleChangelogs_WithWildcardProductFilter_MatchesAllProducts( entries: - file: name: 1755268130-elasticsearch-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -1088,7 +1088,7 @@ public async Task BundleChangelogs_WithWildcardProductFilter_MatchesAllProducts( - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-kibana-feature.yaml - checksum: 26138a1fba412c9a5b0ff0d7220c62162220aca5 + checksum: {sha1} type: feature title: Kibana feature products: @@ -1165,7 +1165,7 @@ public async Task BundleChangelogs_WithWildcardAllParts_EquivalentToAll() entries: - file: name: 1755268130-elasticsearch-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -1177,7 +1177,7 @@ public async Task BundleChangelogs_WithWildcardAllParts_EquivalentToAll() - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-kibana-feature.yaml - checksum: 0b2ef7378dcf24ef60f00fd7c873d6e4c0dd7239 + checksum: {sha1} type: feature title: Kibana feature products: @@ -1268,7 +1268,7 @@ public async Task BundleChangelogs_WithPrefixWildcardTarget_MatchesCorrectly() entries: - file: name: 1755268130-es-9.3.0.yaml - checksum: e6d486a763ea3e602402854d69cf7687013d0ea9 + checksum: {sha1} type: feature title: Elasticsearch 9.3.0 feature products: @@ -1280,7 +1280,7 @@ public async Task BundleChangelogs_WithPrefixWildcardTarget_MatchesCorrectly() - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-es-9.3.1.yaml - checksum: 45f2cc90ed8a30e22478c392452168a2caa663a7 + checksum: {sha1} type: feature title: Elasticsearch 9.3.1 feature products: @@ -1367,7 +1367,7 @@ public async Task BundleChangelogs_WithUrlAsPrs_TreatsAsPrIdentifier() entries: - file: name: 1755268130-test-pr.yaml - checksum: 3ee6f675ab08fbe87f6dde7b63089f667ad95690 + checksum: {sha1} type: feature title: Test PR products: @@ -1430,7 +1430,7 @@ public async Task BundleChangelogs_WithNonExistentFileAndOtherPrs_EmitsWarning() entries: - file: name: 1755268130-test-pr.yaml - checksum: 3ee6f675ab08fbe87f6dde7b63089f667ad95690 + checksum: {sha1} type: feature title: Test PR products: @@ -1510,7 +1510,7 @@ public async Task BundleChangelogs_WithOutputProducts_OverridesChangelogProducts entries: - file: name: 1755268130-elasticsearch-feature.yaml - checksum: 953e2ec8fd6621dd6344173002e8f3911139b7bd + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -1521,7 +1521,7 @@ public async Task BundleChangelogs_WithOutputProducts_OverridesChangelogProducts - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-kibana-feature.yaml - checksum: 5567d7bf362ba212f7ca11d9bb7df2eb47f365e7 + checksum: {sha1} type: feature title: Kibana feature products: @@ -1668,7 +1668,7 @@ public async Task BundleChangelogs_WithInputProducts_IncludesLifecycleInProducts entries: - file: name: 1755268130-elasticsearch-ga.yaml - checksum: 339473dfc13c9ece80602aa6f782411be88bb0eb + checksum: {sha1} type: feature title: Elasticsearch GA feature products: @@ -1680,7 +1680,7 @@ public async Task BundleChangelogs_WithInputProducts_IncludesLifecycleInProducts - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-elasticsearch-beta.yaml - checksum: 84adfd530b855f1858d771684ce576df36ee0464 + checksum: {sha1} type: feature title: Elasticsearch Beta feature products: @@ -1748,7 +1748,7 @@ public async Task BundleChangelogs_WithOutputProducts_IncludesLifecycleInProduct entries: - file: name: 1755268130-elasticsearch.yaml - checksum: 953e2ec8fd6621dd6344173002e8f3911139b7bd + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -1825,7 +1825,7 @@ public async Task BundleChangelogs_ExtractsLifecycleFromChangelogEntries() entries: - file: name: 1755268130-elasticsearch-ga.yaml - checksum: 339473dfc13c9ece80602aa6f782411be88bb0eb + checksum: {sha1} type: feature title: Elasticsearch GA feature products: @@ -1837,7 +1837,7 @@ public async Task BundleChangelogs_ExtractsLifecycleFromChangelogEntries() - "https://github.com/elastic/elasticsearch/pull/100" - file: name: 1755268140-elasticsearch-beta.yaml - checksum: 84adfd530b855f1858d771684ce576df36ee0464 + checksum: {sha1} type: feature title: Elasticsearch Beta feature products: @@ -1900,7 +1900,7 @@ public async Task BundleChangelogs_WithInputProductsWildcardLifecycle_ExtractsAc entries: - file: name: 1-feature.yaml - checksum: c25230be743423f1382f80ac428391ff7965cb4d + checksum: {sha1} type: feature title: A new feature was added products: @@ -2031,7 +2031,7 @@ public async Task BundleChangelogs_CopiesChangelogContents() entries: - file: name: 1755268130-test-feature.yaml - checksum: 0321f84ad57ef4a4ebc8747f19d0fe933257e70b + checksum: {sha1} type: feature title: Test feature products: @@ -2104,7 +2104,7 @@ public async Task BundleChangelogs_PreservesSpecialCharactersInUtf8() entries: - file: name: 1755268130-special-chars.yaml - checksum: ae87a7cc3d83bf715b227137a0ec318131b1eff8 + checksum: {sha1} type: feature title: "Feature with special characters & symbols" products: @@ -2168,7 +2168,7 @@ public async Task BundleChangelogs_WithDirectoryOutputPath_CreatesDefaultFilenam entries: - file: name: 1755268130-test-feature.yaml - checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + checksum: {sha1} type: feature title: Test feature products: @@ -2422,7 +2422,7 @@ public async Task BundleChangelogs_WithHideFeaturesOption_IncludesHideFeaturesIn entries: - file: name: 1755268130-feature.yaml - checksum: fb3fd2d00f74ed0d395253baacc5333a3754485f + checksum: {sha1} type: feature title: Feature with hidden flag products: @@ -2482,7 +2482,7 @@ public async Task BundleChangelogs_WithoutHideFeaturesOption_OmitsHideFeaturesFi entries: - file: name: 1755268130-feature.yaml - checksum: cb50b3480ba2f400e80e1f922cfbf4afe3758f86 + checksum: {sha1} type: feature title: Regular feature products: @@ -2554,7 +2554,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268130-feature.yaml - checksum: 815f1f1f0f1f8913132eb2f945a452b70906c270 + checksum: {sha1} type: feature title: Feature with hidden flag products: @@ -2615,7 +2615,7 @@ public async Task BundleChangelogs_WithRepoOption_IncludesRepoInBundleProducts() entries: - file: name: 1755268130-serverless-feature.yaml - checksum: 593ad5bb20a97b49cacd77f3ccd0e2bacd0d52e1 + checksum: {sha1} type: feature title: Serverless feature products: @@ -2677,7 +2677,7 @@ public async Task BundleChangelogs_WithoutRepoOption_UsesResolvedAuthoringRepoIn entries: - file: name: 1755268130-es-feature.yaml - checksum: e990f1b768aa43ecedf7646bea84eb5b88f441a3 + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -2756,7 +2756,7 @@ public async Task BundleChangelogs_WithBundleLevelRepoConfig_UsesConfigRepoWhenO entries: - file: name: 1755268130-serverless-feature.yaml - checksum: 92a0d7dbfae5efdbb9fd63a45032c70ab519fc18 + checksum: {sha1} type: feature title: Serverless feature products: @@ -2834,7 +2834,7 @@ public async Task BundleChangelogs_WithRepoOptionAndBundleLevelConfig_CliOptionT entries: - file: name: 1755268130-serverless-feature.yaml - checksum: 92a0d7dbfae5efdbb9fd63a45032c70ab519fc18 + checksum: {sha1} type: feature title: Serverless feature products: @@ -2905,7 +2905,7 @@ public async Task BundleChangelogs_WithOutputProductsAndRepo_IncludesRepoInAllPr entries: - file: name: 1755268130-feature.yaml - checksum: 80717b768423c3b14edc607a805eb7dd78f905ef + checksum: {sha1} type: feature title: Feature for serverless products: @@ -2977,7 +2977,7 @@ public async Task BundleChangelogs_WithConfigOutputDirectory_WhenOutputNotSpecif entries: - file: name: 1755268130-feature.yaml - checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + checksum: {sha1} type: feature title: Test feature products: @@ -3050,7 +3050,7 @@ public async Task BundleChangelogs_WithConfigDirectory_WhenDirectoryNotSpecified entries: - file: name: 1755268130-feature.yaml - checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + checksum: {sha1} type: feature title: Test feature products: @@ -3128,7 +3128,7 @@ public async Task BundleChangelogs_WithExplicitDirectory_OverridesConfigDirector entries: - file: name: 1755268130-feature.yaml - checksum: d3eef56b8e2777c0a25dcc17dab815fb56da5def + checksum: {sha1} type: feature title: Test feature products: @@ -3219,7 +3219,7 @@ public async Task BundleChangelogs_WithProfileHideFeatures_IncludesHideFeaturesI entries: - file: name: 1755268130-feature.yaml - checksum: b98627257eac4660c9f91c0c0d51cd2fe819b7a4 + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -3311,7 +3311,7 @@ public async Task BundleChangelogs_WithProfile_OnlyProfileHideFeaturesAreUsed() entries: - file: name: 1755268130-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -3400,7 +3400,7 @@ public async Task BundleChangelogs_WithProfileMultipleHideFeatures_AllProfileFea entries: - file: name: 1755268130-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -3476,7 +3476,7 @@ public async Task BundleChangelogs_WithComments_ProducesNormalizedChecksum() entries: - file: name: 1755268130-with-comments.yaml - checksum: 0d824e6d5d7484980513cf372a86e428b24a0f13 + checksum: {sha1} type: feature title: Feature with comments products: @@ -3704,7 +3704,7 @@ public async Task AmendBundle_WithComments_ProducesNormalizedChecksum() entries: - file: name: 1755268140-resolved-feature.yaml - checksum: a4d71514c5bd914d9d0d9bb5549c746efe4938ea + checksum: {sha1} type: feature title: Resolved amend feature products: @@ -3791,7 +3791,7 @@ public async Task BundleChangelogs_WithProfile_OutputProducts_OverridesProductsA entries: - file: name: 1755268130-feature.yaml - checksum: bd342ad5f224e33c2cbfe2d9f198ffbe70888231 + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -3992,7 +3992,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268130-serverless-feature.yaml - checksum: 808180779594f14a4b04ec2e1890370933efd46d + checksum: {sha1} type: feature title: Serverless feature products: @@ -4082,7 +4082,7 @@ public async Task BundleChangelogs_WithProfile_RepoAndOwner_WritesValuesToProduc entries: - file: name: 1755268130-serverless-feature.yaml - checksum: b9b886ae3bd3a6f6af2ec1eb1f890669f1792e0f + checksum: {sha1} type: feature title: Serverless feature products: @@ -4169,7 +4169,7 @@ public async Task BundleChangelogs_WithProfile_BundleLevelRepo_AppliesWhenProfil entries: - file: name: 1755268130-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -4279,7 +4279,7 @@ public async Task BundleChangelogs_ServerlessProfile_WritesAuthoringRepoNotCatal entries: - file: name: 158340.yaml - checksum: 9ab64a7018d5be24115c832e8c58d7ebb68c12f0 + checksum: {sha1} type: feature title: Serverless feature products: @@ -4365,7 +4365,7 @@ public async Task BundleChangelogs_WithProfile_ProfileRepoOverridesBundleRepo() entries: - file: name: 1755268130-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -4452,7 +4452,7 @@ public async Task BundleChangelogs_WithProfile_NoRepoOwner_UsesResolvedAuthoring entries: - file: name: 1755268130-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -4537,7 +4537,7 @@ public async Task BundleChangelogs_WithProfile_OmittedRepoOwner_DescriptionUsesR entries: - file: name: 1755268131-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -4801,7 +4801,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268130-matched.yaml - checksum: d8a2c914c9c1a24407b66031748dcc45cdd7125f + checksum: {sha1} type: feature title: Matched PR products: @@ -4893,7 +4893,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268130-matched.yaml - checksum: c2f71c044a3cd2e7a3fd45b3d43b9fafd9d7180a + checksum: {sha1} type: feature title: Matched Issue products: @@ -5109,7 +5109,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268130-feb.yaml - checksum: 60765bb02a3f8d994b4fe2c5dd0b68b91fc0afe2 + checksum: {sha1} type: feature title: Serverless Feb feature products: @@ -5286,7 +5286,7 @@ public async Task BundleChangelogs_WithReportOption_ParsesPromotionReportAndFilt entries: - file: name: 1755268130-matched.yaml - checksum: 166fb2233258f29de1dc954af5c75f9abd5d2e97 + checksum: {sha1} type: feature title: Matched PR 100 products: @@ -5514,7 +5514,7 @@ public async Task BundleChangelogs_WithRulesBundleExclude_ExcludesMatchingProduc entries: - file: name: 1755268130-elasticsearch-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -5599,7 +5599,7 @@ public async Task BundleChangelogs_WithRulesBundleInclude_IncludesOnlyMatchingPr entries: - file: name: 1755268130-elasticsearch-feature.yaml - checksum: 9b6de513c2d6229297890fe701dec2f7f4eb3753 + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -5683,7 +5683,7 @@ public async Task BundleChangelogs_WithAllFilter_AppliesRulesBundle() entries: - file: name: 1755268130-elasticsearch-feature.yaml - checksum: 4be63d0aa14cc86f2133f1c690ec55f08a27d25c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -5772,7 +5772,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268001-kibana-only.yaml - checksum: da3bff1a2322cdf7adcd78be156f33b117bd5490 + checksum: {sha1} type: feature title: Kibana only products: @@ -5861,7 +5861,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268012-es-sec.yaml - checksum: 97b1ed2ae369cd0106fb6dcd91c4332c4e5b071e + checksum: {sha1} type: feature title: ES and security products: @@ -5999,7 +5999,7 @@ public async Task BundleChangelogs_WithRulesBundleExcludeType_ExcludesMatchingTy entries: - file: name: 1755268130-feature.yaml - checksum: a1c2dd0fdea66eea9537d32028965bf2b92720a6 + checksum: {sha1} type: feature title: Feature entry products: @@ -6087,7 +6087,7 @@ public async Task BundleChangelogs_WithRulesBundleIncludeArea_ExcludesNonMatchin entries: - file: name: 1755268130-search-feature.yaml - checksum: 0305a87a0c5da5c9f4a126a9302d2fa748fdcebc + checksum: {sha1} type: feature title: Search feature products: @@ -6194,7 +6194,7 @@ public async Task BundleChangelogs_WithRulesBundlePerProductOverride_AppliesProd entries: - file: name: 1755268140-serverless-search.yaml - checksum: f3846f2959e343a09c281d800d74078a9da85ac7 + checksum: {sha1} type: feature title: Serverless search feature products: @@ -6412,7 +6412,7 @@ public async Task BundleChangelogs_WithOutputProducts_SharedProductEntry_UsesAlp entries: - file: name: 1755268161-kibana-other.yaml - checksum: fbce2f7f936d0a7aa04ede803fa5d7186d9a4dfa + checksum: {sha1} type: feature title: Kibana core feature products: @@ -6506,7 +6506,7 @@ public async Task BundleChangelogs_WithoutOutputProducts_FallsBackToEntryProduct entries: - file: name: 1755268180-es-doc.yaml - checksum: 52884ed8257759a6d453cc56f0b311a16717fa35 + checksum: {sha1} type: docs title: Elasticsearch docs entry products: @@ -6638,7 +6638,7 @@ public async Task BundleChangelogs_WithPerProductIncludeProducts_IncludesOnlyCon entries: - file: name: security-feature.yaml - checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + checksum: {sha1} type: feature title: Security feature products: @@ -6721,7 +6721,7 @@ public async Task BundleChangelogs_WithPerProductExcludeProducts_ExcludesContext entries: - file: name: security-feature.yaml - checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + checksum: {sha1} type: feature title: Security feature products: @@ -6793,7 +6793,7 @@ public async Task BundleChangelogs_WithPerProductRules_FallsBackToGlobalWhenNoCo entries: - file: name: security-feature.yaml - checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + checksum: {sha1} type: feature title: Security feature products: @@ -6862,7 +6862,7 @@ public async Task BundleChangelogs_WithPerProductRules_ContextRulesTakePrecedenc entries: - file: name: security-feature.yaml - checksum: 7afea02ecd66a28429aa752ed9129317b629f981 + checksum: {sha1} type: feature title: Security feature products: @@ -7032,7 +7032,7 @@ public async Task BundleChangelogs_GlobalMode_IncludeProductsAny_IncludesEntryMa entries: - file: name: 1755268205-es.yaml - checksum: abe72a860e5f3538f03ce5751ab9d49fe881dec1 + checksum: {sha1} type: feature title: ES only products: @@ -7044,7 +7044,7 @@ public async Task BundleChangelogs_GlobalMode_IncludeProductsAny_IncludesEntryMa - "https://github.com/elastic/elasticsearch/pull/1" - file: name: 1755268206-kibana.yaml - checksum: e98b4ce7bdb6dff3c7afef4d02a8257723939392 + checksum: {sha1} type: feature title: Kibana only products: @@ -7177,7 +7177,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268208-es.yaml - checksum: a280eae9bd4e6ff186792d036cad598f934c01f7 + checksum: {sha1} type: feature title: ES products: @@ -7641,7 +7641,7 @@ public async Task BundleChangelogs_PartialPerProductRules_AllOrNothingReplacemen entries: - file: name: 1755268204-partial-rule.yaml - checksum: 1b2669d933cb74c99c2d81a652147b12eef6de6b + checksum: {sha1} type: docs title: Docs entry with partial per-product rule products: @@ -7723,7 +7723,7 @@ public async Task BundleChangelogs_OptionModeWithPlaceholdersAndOutputProducts_S entries: - file: name: changelog1.yaml - checksum: a65016261a61152db249d295c2859270c5103765 + checksum: {sha1} type: feature title: First changelog products: @@ -7795,7 +7795,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 158340.yaml - checksum: 9ab64a7018d5be24115c832e8c58d7ebb68c12f0 + checksum: {sha1} type: feature title: Serverless feature products: @@ -7864,7 +7864,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 158340.yaml - checksum: 066e79ca0c16f67af5ca3e9631d10dc70f51fac1 + checksum: {sha1} type: feature title: Feature with bare PR products: @@ -7980,7 +7980,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1755268999-feature.yaml - checksum: cd6f322277d9dbabbcfaa8776b9447cc1e7bca1c + checksum: {sha1} type: feature title: Elasticsearch feature products: @@ -8121,7 +8121,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: changelog1.yaml - checksum: a65016261a61152db249d295c2859270c5103765 + checksum: {sha1} type: feature title: First changelog products: @@ -8177,7 +8177,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: changelog1.yaml - checksum: a65016261a61152db249d295c2859270c5103765 + checksum: {sha1} type: feature title: First changelog products: @@ -8250,7 +8250,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: changelog-with-bom.yaml - checksum: 0ed153a98884cc7807342b1937a157a56ad0d692 + checksum: {sha1} type: feature title: Test changelog with BOM products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs index 4fca4c09d7..3afeef4a0f 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleFilesFilterTests.cs @@ -87,7 +87,7 @@ public async Task Bundle_WithFiles_IncludesOnlyNamedEntries() entries: - file: name: keep.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: @@ -177,7 +177,7 @@ public async Task Bundle_WithPathListFile_IncludesListedEntries() entries: - file: name: keep.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: @@ -235,7 +235,7 @@ public async Task Bundle_WithProfile_PathListFile_FiltersCorrectly() entries: - file: name: keep.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: @@ -325,7 +325,7 @@ public async Task Bundle_WithFiles_RulesBundleStillApplies() entries: - file: name: feature.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: @@ -376,7 +376,7 @@ public async Task Bundle_WithFilesAndForceLocal_SourcesLocalEvenWhenRepoResolves entries: - file: name: keep.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: @@ -419,7 +419,7 @@ public async Task Bundle_WithFiles_RepoResolves_MatchesCdnPoolByFileName() entries: - file: name: keep.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: @@ -512,7 +512,7 @@ public async Task Bundle_WithProfile_PathListFile_RepoResolves_SourcesFromCdn() entries: - file: name: keep.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: @@ -604,7 +604,7 @@ public async Task Bundle_WithForceLocal_SourcesLocalDespiteResolvableRepo() entries: - file: name: 1-local.yaml - checksum: 470ef2044c3b133b88ba37d83ea39a896d2b974c + checksum: {sha1} type: feature title: Keep me products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs index c3eb8e4117..3ce1129904 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleGitRefTests.cs @@ -280,7 +280,7 @@ public async Task ProfileMode_BackportLabelConfigured_BundlesTheOriginalInsteadO entries: - file: name: 100.yaml - checksum: c2b6527c04530bf3cfffb127578481f3a1131e54 + checksum: {sha1} type: feature title: Faster hosted search products: @@ -543,7 +543,7 @@ public async Task ProfileMode_CdnNotesForTheTarget_AreMergedIntoTheBundle() entries: - file: name: 100.yaml - checksum: c2b6527c04530bf3cfffb127578481f3a1131e54 + checksum: {sha1} type: feature title: Faster hosted search products: @@ -553,7 +553,7 @@ public async Task ProfileMode_CdnNotesForTheTarget_AreMergedIntoTheBundle() lifecycle: ga - file: name: main/note-heads-up.yml - checksum: c58b01d5eaf57a4b95f4686599684868e02f79ab + checksum: {sha1} type: known-issue title: Heads up for the August release products: @@ -729,7 +729,7 @@ public async Task InferredEntry_NoTypeLabel_DefaultsToOtherWithWarning() entries: - file: name: 999.yaml - checksum: 4d4d007a3c2b6c3d737ef99d58bd66c2cfac8c14 + checksum: {sha1} type: other title: Unlabeled change products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs index b4cf94c39a..32bc4fb772 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleReleaseVersionTests.cs @@ -106,7 +106,7 @@ await WriteChangelog( entries: - file: name: pr-12345.yaml - checksum: 6cb20a9554c7d77cf5571d4c8f4ba75e97a89ac3 + checksum: {sha1} type: bug-fix title: Fix query parsing products: @@ -118,7 +118,7 @@ await WriteChangelog( - "https://github.com/elastic/elasticsearch/pull/12345" - file: name: pr-12346.yaml - checksum: bb5e93521f76b6774193a3765d27744bd283cd27 + checksum: {sha1} type: feature title: New aggregation API products: @@ -197,7 +197,7 @@ await WriteChangelog( entries: - file: name: pr-12345.yaml - checksum: 6cb20a9554c7d77cf5571d4c8f4ba75e97a89ac3 + checksum: {sha1} type: bug-fix title: Fix query parsing products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs index 84b7e2d7bd..5a393b1716 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/CloudProfileFixtureTests.cs @@ -192,7 +192,7 @@ await FileSystem.File.WriteAllTextAsync( entries: - file: name: 1-feature.yaml - checksum: 31e5ddcf2ce48521b213aee97c063e8402f6acca + checksum: {sha1} type: feature title: Faster hosted search products: diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs index bbfc97d225..bcc8eae263 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/GhReleaseExtractionParityTests.cs @@ -481,7 +481,7 @@ public async Task Bundle_IncludesPoolEntriesWithScrubbedPrsReferences() entries: - file: name: 12345.yaml - checksum: 3e23f7fdf6d96be35d49fa93ad546d6165bf292f + checksum: {sha1} type: feature title: Curated checked-in title products: diff --git a/tests/Elastic.Changelog.Tests/SnapshotSetup.cs b/tests/Elastic.Changelog.Tests/SnapshotSetup.cs index 22975b860f..f0068b1078 100644 --- a/tests/Elastic.Changelog.Tests/SnapshotSetup.cs +++ b/tests/Elastic.Changelog.Tests/SnapshotSetup.cs @@ -19,9 +19,16 @@ internal static void Register() // read `repo: docs-builder` on one and carry a warning on the other. Tests that need it pass it in explicitly. Environment.SetEnvironmentVariable("GITHUB_REPOSITORY", null); SnapshotDefaults.AddScrubber(Scrubbers.Guids); + SnapshotDefaults.AddScrubber(Checksums); SnapshotDefaults.AddScrubber(Today); } + // A bundle records the SHA-1 of each entry file it was made from. The test inputs are raw string literals and the product writes files + // with the platform's line ending, so on Windows the same entry has other bytes and another checksum. The value is replaced; what a + // checksum must be is checked where a test computes the expected one itself. + private static string Checksums(string text) => + System.Text.RegularExpressions.Regex.Replace(text, "(?<=checksum: )[0-9a-f]{40}", "{sha1}"); + // A bundle is stamped with the day it was created. Only that day is replaced, so a release-date a test sets on purpose stays visible. private static string Today(string text) {