Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/find-ui-corpus-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ jobs:
// reach for the skip label by reflex.
const CORPUS_SOURCE_PATTERNS = [
/^src\/winapp-CLI\/WinApp\.Cli\/Services\/Controls\/(Gallery|Toolkit|Reactor)(Fetcher|Provider)\.cs$/,
// Gallery and Reactor both build their corpus from a published sample index,
// so a change to the shared download or mapping re-shapes two sources at once.
/^src\/winapp-CLI\/WinApp\.Cli\/Services\/Controls\/SampleIndex(Fetcher|Parser)\.cs$/,
/^src\/winapp-CLI\/WinApp\.Cli\/Services\/Controls\/ScenarioSanitizer\.cs$/,
/^src\/winapp-CLI\/WinApp\.Cli\/Services\/Controls\/Models\.cs$/,
/^src\/winapp-CLI\/WinApp\.Cli\/Services\/Controls\/CacheVersion\.cs$/,
Expand Down
168 changes: 168 additions & 0 deletions src/winapp-CLI/WinApp.Cli.Tests/EmbeddedSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using WinApp.Cli.Services.Controls;

namespace WinApp.Cli.Tests;
Expand Down Expand Up @@ -33,6 +34,17 @@ public class EmbeddedSnapshotTests
/// </summary>
private const double MinCodeRetention = 0.90;

/// <summary>
/// Serving floors for the Gallery corpus, measured at 327 scenarios / 115 controls /
/// 290 XAML / 116 C# after sanitizing on the bake that introduced the published index.
/// Set roughly 10% below that so ordinary upstream churn doesn't fail the build, while
/// a partial or broken upstream publish does.
/// </summary>
private const int MinGalleryScenarios = 295;
private const int MinGalleryControls = 105;
private const int MinGalleryXaml = 260;
private const int MinGalleryCSharp = 100;

// ------------------------------------------------------------------
// Build gates: the committed snapshot must match the code that reads it
// ------------------------------------------------------------------
Expand Down Expand Up @@ -217,6 +229,162 @@ public void EmbeddedCorpus_SurvivesTheSanitizer()
$"Observed: {string.Join(" | ", report)}");
}

[TestMethod]
public void GalleryCorpus_MeetsItsServingFloors()
{
// The Gallery corpus is no longer reconstructed by a scraper — it is whatever
// microsoft/WinUI-Gallery publishes in catalog/windows-samples.json. That removes a
// whole class of parsing bugs, but it also means a bad upstream publish (a partial
// export, a schema change that silently drops a field) now lands in our corpus with
// nothing in between. These floors are that "nothing in between".
//
// They are floors, not equalities: upstream adds and removes samples continuously
// and a bake that tracks that is working as intended. They sit below the measured
// corpus with room for ordinary churn, so what trips them is a collapse, not a
// release. Re-measure and move a floor only after confirming upstream really did
// shrink; a floor that is edited every bake is not guarding anything.
var scenarios = ReadEmbeddedSnapshot("gallery")!.Scenarios;
ScenarioSanitizer.SanitizeAll(scenarios);

var controls = scenarios.Select(s => s.ControlId).Distinct(StringComparer.OrdinalIgnoreCase).Count();
var xaml = scenarios.Count(s => !string.IsNullOrWhiteSpace(s.Xaml));
var csharp = scenarios.Count(s => !string.IsNullOrWhiteSpace(s.CSharp));

var observed = $"observed {scenarios.Length} scenarios / {controls} controls / " +
$"{xaml} XAML / {csharp} C# after sanitizing";

Assert.IsTrue(scenarios.Length >= MinGalleryScenarios,
$"the Gallery corpus collapsed to {scenarios.Length} scenarios (floor {MinGalleryScenarios}). {observed}");
Assert.IsTrue(controls >= MinGalleryControls,
$"the Gallery corpus collapsed to {controls} controls (floor {MinGalleryControls}). {observed}");
Assert.IsTrue(xaml >= MinGalleryXaml,
$"only {xaml} Gallery scenarios still serve XAML (floor {MinGalleryXaml}). {observed}");
Assert.IsTrue(csharp >= MinGalleryCSharp,
$"only {csharp} Gallery scenarios still serve C# (floor {MinGalleryCSharp}). {observed}");
}

[TestMethod]
public void GalleryCorpus_ServesNoScenarioWithoutCode()
{
// A scenario that survives sanitizing with neither XAML nor C# is worse than an
// absent one: search ranks it, the user asks for it by id, and find-ui answers with
// a header and no sample. The scraper shipped five of these because it created a
// scenario per documented sample whether or not it could recover the source; the
// index only describes samples it actually carries, so the expected count is zero.
var scenarios = ReadEmbeddedSnapshot("gallery")!.Scenarios;
ScenarioSanitizer.SanitizeAll(scenarios);

var empty = scenarios.Where(s => !HasCode(s)).Select(s => s.Id).ToList();

Assert.AreEqual(0, empty.Count,
$"{empty.Count} Gallery scenarios are served with no code at all: {string.Join(", ", empty.Take(10))}. " +
"Each one is a fetchable id that returns nothing useful.");
}

[TestMethod]
public void GalleryCorpus_ServesNoUnresolvedSubstitutionToken()
{
// Gallery's page XAML carries $(Name) substitution tokens that its own runtime
// resolves. They are not valid XAML or C#, so a sample containing one does not
// compile when pasted. SampleIndexParser suppresses those blocks; this asserts none
// reached the corpus by another route.
var scenarios = ReadEmbeddedSnapshot("gallery")!.Scenarios;
ScenarioSanitizer.SanitizeAll(scenarios);

var leaked = scenarios
.Where(s => (s.Xaml?.Contains("$(", StringComparison.Ordinal) ?? false)
|| (s.CSharp?.Contains("$(", StringComparison.Ordinal) ?? false))
.Select(s => s.Id)
.ToList();

Assert.AreEqual(0, leaked.Count,
$"these Gallery samples are served with an unresolved $(...) token in them: {string.Join(", ", leaked.Take(10))}");
}

[TestMethod]
public void GalleryCorpus_ServesNoUnbackedEventHandler()
{
// The index carries Gallery's XAML verbatim, including events wired to handlers
// that live in page code-behind it does not publish. Serving such an attribute
// hands the user markup that fails to compile with "handler not found", so
// GalleryProvider strips the ones the emitted C# doesn't declare. This asserts the
// corpus was baked with that step applied — the count guards above all pass without
// it, because nothing about them notices a sample that no longer builds.
var scenarios = ReadEmbeddedSnapshot("gallery")!.Scenarios;
ScenarioSanitizer.SanitizeAll(scenarios);

var unbacked = scenarios
.Where(s => !string.IsNullOrEmpty(s.Xaml)
&& ControlSnippetText.StripUnbackedEventHandlers(s.Xaml!, s.CSharp) != s.Xaml)
.Select(s => s.Id)
.ToList();

Assert.AreEqual(0, unbacked.Count,
$"{unbacked.Count} Gallery samples wire a XAML event to a handler they don't define: " +
$"{string.Join(", ", unbacked.Take(10))}. Pasting one of these does not compile.");
}

[TestMethod]
public void GalleryCorpus_ServesNoGalleryPrivateSymbol()
{
// Gallery's own page classes, its SamplePage navigation targets, its UIHelper
// accessibility helper and its CLR namespaces exist only inside the Gallery app. A
// sample that still names one compiles only if the user happens to have Gallery's
// source, so GalleryProvider rewrites them to obvious placeholders before baking.
// These match identifiers, not prose: a sample may legitimately mention a Gallery
// source file by name, and a "using:" or trailing-dot anchor keeps a source URL
// (".../main/WinUIGallery/ButtonPage.xaml.cs") out of the match.
var scenarios = ReadEmbeddedSnapshot("gallery")!.Scenarios;
ScenarioSanitizer.SanitizeAll(scenarios);

string[] privateSymbols =
[
@"\bSamplePage\d*",
@"\bUIHelper\.",
"x:Class=\"WinUIGallery",
@"namespace\s+WinUIGallery",
@"using:(?:WinUIGallery|AppUIBasics)",
@"\b(?:WinUIGallery|AppUIBasics)\.",
];

// XmlnsImports is checked alongside the snippets because SearchEngine renders it as
// the "Setup:" line: a Gallery namespace there is an instruction to declare a prefix
// that cannot resolve, which the two snippet fields would never reveal.
var leaked = scenarios
.Where(s => privateSymbols.Any(sym =>
Regex.IsMatch(s.Xaml ?? "", sym)
|| Regex.IsMatch(s.CSharp ?? "", sym)
|| s.XmlnsImports.Any(import => Regex.IsMatch(import, sym))))
.Select(s => s.Id)
.ToList();

Assert.AreEqual(0, leaked.Count,
$"these Gallery samples still name a Gallery-internal symbol: {string.Join(", ", leaked.Take(10))}");
}

[TestMethod]
public void GalleryCorpus_ServesNoGalleryOnlyAsset()
{
// Gallery ships its sample photos and videos under Assets/SampleMedia and its own app
// icon and tile logos under Assets/Tiles, inside its package. A pasted sample naming
// one of those paths compiles and then renders nothing — an <Image> that is silently
// blank, an AppWindow.SetIcon that quietly does not take — so GalleryProvider rewrites
// them to a placeholder that fails visibly instead. Nothing else in this file notices:
// the counts pass and the XAML is well-formed either way.
var scenarios = ReadEmbeddedSnapshot("gallery")!.Scenarios;
ScenarioSanitizer.SanitizeAll(scenarios);

var leaked = scenarios
.Where(s => Regex.IsMatch(s.Xaml ?? "", @"Assets/(?:SampleMedia|Tiles)/")
|| Regex.IsMatch(s.CSharp ?? "", @"Assets/(?:SampleMedia|Tiles)/"))
.Select(s => s.Id)
.ToList();

Assert.AreEqual(0, leaked.Count,
$"these Gallery samples point at an asset that only exists in Gallery's own package: " +
$"{string.Join(", ", leaked.Take(10))}");
}

// ------------------------------------------------------------------
// Load order: the snapshot is a floor, never a ceiling
// ------------------------------------------------------------------
Expand Down
58 changes: 0 additions & 58 deletions src/winapp-CLI/WinApp.Cli.Tests/FindUiSearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,62 +358,4 @@ public void Preprocess_ImageGridPhrase_RoutesToCollectionControls()
}
}

[TestMethod]
public void CleanGalleryContent_AttributePositionSubstitution_StaysWellFormed()
{
// Upstream writes an optional attribute as a bare $(Name) token inside the start tag
// (Button, ToggleButton, ProgressRing, CommandBar and six others do this). Flattening
// it to "..." like a value-position token produces `Click="Button_Click" .../>`, which
// the sanitizer rejects as malformed — so the control served a fetchable result with
// no code in it at all.
const string upstream = """<Button Content="Standard XAML button" Click="Button_Click" $(IsEnabled)/>""";

var cleaned = GalleryFetcher.CleanGalleryContent(upstream);

Assert.IsTrue(
ScenarioSanitizer.XamlIsWellFormed(cleaned),
$"flattened sample must survive structural validation, got: {cleaned}");
StringAssert.Contains(cleaned, "Content=\"Standard XAML button\"", "the real markup must survive");
Assert.IsFalse(cleaned.Contains("$("), "the substitution token must not be served raw");
}

[TestMethod]
public void CleanGalleryContent_ValuePositionSubstitution_DropsTheAttribute()
{
// Flattening a value-position token to "..." keeps the markup well-formed but not
// pasteable: a measured 140 attributes in the previous bake were typed properties
// (StrokeThickness, Height, Orientation, IsChecked...) where "..." is a compile
// error. Dropping the attribute lets the property fall back to its own default.
const string upstream = """<ProgressRing Value="$(DeterminateProgressValue)" $(Background)/>""";

var cleaned = GalleryFetcher.CleanGalleryContent(upstream);

Assert.IsTrue(ScenarioSanitizer.XamlIsWellFormed(cleaned), $"got: {cleaned}");
Assert.IsFalse(cleaned.Contains("\"...\""), $"the token must not be flattened, got: {cleaned}");
Assert.IsFalse(cleaned.Contains("Value="), $"the unresolvable attribute must be dropped, got: {cleaned}");
Assert.IsFalse(cleaned.Contains("$("), $"no token may be served raw, got: {cleaned}");
}

[TestMethod]
public void CleanGalleryContent_ContentPositionSubstitution_BecomesAComment()
{
// Every content-position token upstream ships injects markup into a property element,
// never text. Flattening it assigns the literal string "..." as the collection's
// content, which parses but does not compile when pasted.
const string upstream = """
<CommandBar>
<CommandBar.SecondaryCommands>
<AppBarButton Icon="Setting" Label="Settings"/>$(MultipleButtonsSecondaryCommands)
</CommandBar.SecondaryCommands>
</CommandBar>
""";

var cleaned = GalleryFetcher.CleanGalleryContent(upstream);

Assert.IsTrue(ScenarioSanitizer.XamlIsWellFormed(cleaned), $"got: {cleaned}");
StringAssert.Contains(cleaned, "<!-- ... -->", "the injection point must be marked, not stringified");
Assert.IsFalse(
Regex.IsMatch(cleaned, @">\s*\.\.\.\s*<"),
$"no bare '...' may sit in element content, got: {cleaned}");
}
}
128 changes: 0 additions & 128 deletions src/winapp-CLI/WinApp.Cli.Tests/GalleryFetcherCleanContentTests.cs

This file was deleted.

Loading
Loading