Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
130 changes: 130 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 @@ -193,6 +205,124 @@ 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 and its UIHelper
// accessibility helper 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.
var scenarios = ReadEmbeddedSnapshot("gallery")!.Scenarios;
ScenarioSanitizer.SanitizeAll(scenarios);

string[] privateSymbols = [@"\bSamplePage\d*", @"\bUIHelper\.", "x:Class=\"WinUIGallery", @"namespace\s+WinUIGallery"];

var leaked = scenarios
.Where(s => privateSymbols.Any(sym =>
Regex.IsMatch(s.Xaml ?? "", sym) || Regex.IsMatch(s.CSharp ?? "", 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))}");
}

// ------------------------------------------------------------------
// 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