Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a44efdc
Characterize blueprint verification
woksin Nov 11, 2026
5bbd4f7
Pin the order blueprint verification reports its groups in
woksin Nov 11, 2026
9f7f923
Split blueprint verification into focused rule sets
woksin Nov 11, 2026
745144d
Adopt C# 15 idioms in blueprint verification
woksin Nov 11, 2026
e0427be
Guard blueprint verification inputs from earlier calls
woksin Nov 11, 2026
49887da
Characterize package list branches and mutation
woksin Nov 11, 2026
9c987ed
Split the package list into focused helpers
woksin Nov 11, 2026
f4a32c5
Adopt C# 15 idioms in the package list
woksin Nov 11, 2026
a674216
Mark the package list raw goldens as JSON
woksin Nov 11, 2026
57fba70
Make the package list mutation guard capture content
woksin Nov 11, 2026
1c2fd49
Characterize package version range
woksin Nov 11, 2026
3d0db9c
Split package version range evaluation
woksin Nov 11, 2026
f107062
Characterize sparse profile fixes
woksin Nov 11, 2026
9ed23e0
Split profile blocker rules
woksin Nov 11, 2026
8ad3a7e
Split profile migration steps
woksin Nov 11, 2026
6a86ac7
Keep profile fix and version corpus specs out of assertions
woksin Nov 11, 2026
19466cd
Characterize shell resolution and switch planning edge cases in the s…
woksin Nov 11, 2026
d044b75
Break the shell switch planner into named steps in both engines
woksin Nov 11, 2026
d435dab
Break the shell resolver into named steps
woksin Nov 11, 2026
1fa0e8b
Break the effective shell resolver into named steps
woksin Nov 11, 2026
8dbd17a
Restore the version pin warning in the effective shell resolver
woksin Nov 11, 2026
4f53aad
Merge remote-tracking branch 'origin/main' into scene-294-pr2
woksin Nov 11, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ public class when_running_the_shared_corpus : Specification
Converters = { new JsonStringEnumConverter() }
};

static readonly JsonSerializerOptions _raw = new()
{
Converters = { new JsonStringEnumConverter() },
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

List<VerificationCase> _verification = null!;
List<CatalogCase> _catalog = null!;
List<PinCase> _pin = null!;
List<CompareCase> _compare = null!;
List<string> _pristine = null!;
List<(VerificationCase Case, IReadOnlyList<string> Problems)> _verified = null!;
List<(CatalogCase Case, IReadOnlyList<BlueprintCatalogEntry> Entries)> _described = null!;
List<(PinCase Case, BlueprintPinEvaluation Evaluation)> _evaluated = null!;
Expand All @@ -37,6 +44,7 @@ void Establish()
_catalog = document.RootElement.GetProperty("catalog").Deserialize<List<CatalogCase>>(_options);
_pin = document.RootElement.GetProperty("pin").Deserialize<List<PinCase>>(_options);
_compare = document.RootElement.GetProperty("compare").Deserialize<List<CompareCase>>(_options);
_pristine = [.. _verification.Select(Snapshot)];
}

void Because()
Expand All @@ -55,6 +63,24 @@ void should_verify_blueprints_like_the_typescript_engine()
}
}

[Fact]
void should_write_the_problems_in_the_order_they_are_found()
{
var fixture = _verification.Single(candidate => candidate.Name == "screen set that is inconsistent");

var written = JsonSerializer.Serialize(BlueprintVerification.Verify(fixture.Source, fixture.Library, fixture.Options), _raw);

written.ShouldEqual("[\"Blueprint '@cratis/scene.blueprint.crm' declares screen 'Leads' more than once\",\"Blueprint '@cratis/scene.blueprint.crm' declares a screen set entry without a name\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'Leads' uses template 'Missing', which is not available\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'Leads' declares no title\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'Orphan' has parent 'Nowhere', which is not in the screen set\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'Orphan' is named in the screen set but the package does not provide it\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'A' is named in the screen set but the package does not provide it\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'B' is named in the screen set but the package does not provide it\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'A' is its own ancestor\",\"Blueprint '@cratis/scene.blueprint.crm' screen 'B' is its own ancestor\"]");
}

[Fact]
void should_leave_the_blueprint_the_library_and_the_options_as_they_were()
{
var mismatches = _verification.Where((fixture, index) => Snapshot(fixture) != _pristine[index]).Select(fixture => fixture.Name);

mismatches.ShouldBeEmpty();
}

[Fact]
void should_describe_the_catalog_like_the_typescript_engine()
{
Expand Down Expand Up @@ -82,6 +108,8 @@ void should_compare_versions_like_the_typescript_engine()
}
}

static string Snapshot(VerificationCase fixture) => JsonSerializer.Serialize(new { fixture.Source, fixture.Library, fixture.Options }, _raw);

record VerificationCase(string Name, BlueprintSource Source, IReadOnlyList<TemplateSource> Library, BlueprintVerificationOptions? Options, IReadOnlyList<string> ExpectedProblems);

record CatalogCase(string Name, IReadOnlyList<TemplateSource> Sources, IReadOnlyList<BlueprintCatalogEntry> ExpectedEntries);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,12 @@ public class when_checking_against_the_shared_fixture_corpus : Specification
};

List<ScenePackage> _packages = null!;
string _pristineCatalog = null!;
JsonArray _cases = null!;
List<(string Name, JsonNode Expected, JsonNode Actual)> _results = null!;
List<string> _mutated = null!;
string _listing = null!;
string _themeEdit = null!;

void Establish()
{
Expand All @@ -47,18 +51,41 @@ void Establish()
var corpus = JsonNode.Parse(File.ReadAllText(Path.Combine(directory.FullName, "package-list-fixtures.json")));
_packages = corpus["packages"].Deserialize<List<ScenePackage>>(_readOptions);
_cases = corpus["cases"].AsArray();
_pristineCatalog = JsonSerializer.Serialize(_packages, _options);
}

void Because() => _results = [.. _cases.Select(item =>
void Because()
{
_mutated = [];
_results = [.. _cases.Select(item =>
{
var profile = item["profile"].Deserialize<UiProfile>(_readOptions);
var packages = item["packages"]?.Deserialize<List<ScenePackage>>(_readOptions) ?? _packages;
var before = JsonSerializer.Serialize(new { profile, packages }, _options);
var result = Run(item, profile, packages);
if (JsonSerializer.Serialize(new { profile, packages }, _options) != before)
{
_mutated.Add(item["name"].GetValue<string>());
}

return result;
})];
_listing = JsonSerializer.Serialize(PackageList.Describe(CaseProfile("a package another needs"), _packages), _options);
_themeEdit = JsonSerializer.Serialize(PackageList.ChooseTheme(CaseProfile("choose a theme whose package is missing"), _packages, "Material Ocean"), _options);
}

UiProfile CaseProfile(string name) =>
_cases.Single(candidate => candidate["name"].GetValue<string>() == name)["profile"].Deserialize<UiProfile>(_readOptions)!;

static (string Name, JsonNode Expected, JsonNode Actual) Run(JsonNode item, UiProfile profile, List<ScenePackage> packages)
{
var profile = item["profile"].Deserialize<UiProfile>(_readOptions);
var action = item["action"];
var kind = action["kind"].GetValue<string>();
var expected = item["expected"];
JsonNode actual;
if (kind == "describe")
{
var listing = PackageList.Describe(profile, _packages);
var listing = PackageList.Describe(profile, packages);
actual = new JsonObject { ["rows"] = JsonSerializer.SerializeToNode(listing.Rows, _options), ["addable"] = JsonSerializer.SerializeToNode(listing.Addable, _options) };
foreach (var entry in actual["rows"].AsArray().Concat(actual["addable"].AsArray()))
{
Expand All @@ -77,11 +104,11 @@ void Because() => _results = [.. _cases.Select(item =>
var name = action["name"]?.GetValue<string>() ?? string.Empty;
var edit = kind switch
{
"add" => PackageList.Add(profile, _packages, name),
"remove" => PackageList.Remove(profile, _packages, name),
"move" => PackageList.Move(profile, _packages, name, action["direction"].GetValue<string>() == "up" ? MoveDirection.Up : MoveDirection.Down),
"blueprint" => PackageList.ChooseBlueprint(profile, _packages, action["blueprint"].GetValue<string>(), action["shell"]?.GetValue<string>()),
_ => PackageList.ChooseTheme(profile, _packages, action["theme"].GetValue<string>())
"add" => PackageList.Add(profile, packages, name),
"remove" => PackageList.Remove(profile, packages, name),
"move" => PackageList.Move(profile, packages, name, action["direction"].GetValue<string>() == "up" ? MoveDirection.Up : MoveDirection.Down),
"blueprint" => PackageList.ChooseBlueprint(profile, packages, action["blueprint"].GetValue<string>(), action["shell"]?.GetValue<string>()),
_ => PackageList.ChooseTheme(profile, packages, action["theme"].GetValue<string>())
};
var result = new JsonObject
{
Expand All @@ -105,11 +132,30 @@ void Because() => _results = [.. _cases.Select(item =>
}

return (item["name"].GetValue<string>(), expected.DeepClone(), actual);
})];
}

[Fact]
void should_have_cases_in_the_corpus() => _results.Count.ShouldBeGreaterThan(25);

[Fact]
void should_leave_the_profile_and_the_catalog_as_they_were()
{
_mutated.ShouldBeEmpty();
JsonSerializer.Serialize(_packages, _options).ShouldEqual(_pristineCatalog);
}

[Fact]
void should_write_the_listing_the_way_the_designer_reads_it()
{
_listing.ShouldEqual(/*lang=json,strict*/ """{"rows":[{"position":1,"name":"material","displayName":"Material Components","version":"1.0.0","kind":"componentLibrary","description":"Material components.","reason":{"kind":"chosen","packages":[],"text":"Chosen"},"canMoveUp":false,"canMoveDown":false},{"position":2,"name":"tailwind","displayName":"Tailwind","version":"1.0.0","kind":"styling","description":"Utility styling.","reason":{"kind":"neededBy","packages":["Material Components"],"text":"Needed by Material Components"},"canMoveUp":false,"canMoveDown":true,"lock":{"kind":"neededBy","packages":["Material Components"],"text":"Tailwind is needed by Material Components, so it stays while they are listed."}},{"position":3,"name":"primereact","displayName":"PrimeReact","version":"1.0.0","kind":"componentLibrary","description":"PrimeReact components.","reason":{"kind":"chosen","packages":[],"text":"Chosen"},"canMoveUp":true,"canMoveDown":false},{"position":4,"name":"core","displayName":"Core","version":"1.0.0","kind":"componentLibrary","description":"The fallback components.","reason":{"kind":"alwaysLast","packages":[],"text":"Always last"},"canMoveUp":false,"canMoveDown":false,"lock":{"kind":"core","packages":[],"text":"Core is the fallback every component name resolves against, so it is always last and cannot be removed."}}],"addable":[{"name":"material-themes","displayName":"Material Themes","version":"1.0.0","kind":"styling","description":"Material themes.","alsoAdds":[],"text":""},{"name":"bp.default","displayName":"Cratis Default","version":"1.4.2","kind":"blueprint","description":"The default blueprint.","alsoAdds":[],"text":""},{"name":"bp.dashboard","displayName":"Dashboard","version":"2.0.0","kind":"blueprint","description":"Horizontal navigation.","alsoAdds":[],"text":""},{"name":"broken","displayName":"Broken","version":"1.0.0","kind":"componentLibrary","description":"Needs a package nobody has.","alsoAdds":[],"text":""}],"suppliers":[{"component":"button","winner":"material","others":["primereact","core"]},{"component":"card","winner":"primereact","others":["core"]},{"component":"datatable","winner":"material","others":["primereact"]},{"component":"table","winner":"core","others":[]}]}""");
}

[Fact]
void should_write_an_edit_with_what_it_added_in_the_order_it_added_it()
{
_themeEdit.ShouldEqual(/*lang=json,strict*/ """{"profile":{"name":"Desktop","targetPlatform":"web","packages":["core","tailwind","material","material-themes","primereact"],"theme":"Material Ocean"},"outcome":"changed","added":["tailwind","material","material-themes"],"removed":[],"message":"Theme \u0027Material Ocean\u0027 needs Material Themes, which was added."}""");
}

[Fact]
void should_answer_what_the_corpus_expects_for_every_case()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Text.Json;
using Cratis.Scene.Engine.for_PackageDependencyResolver;
using Cratis.Scene.Engine.Packages;

namespace Cratis.Scene.Engine.for_PackageVersionRange;

public class when_validating_and_comparing_against_the_shared_fixture_corpus : Specification
{
List<(string? Range, bool Expected, bool Actual)> _ranges = null!;
List<(string? Version, bool Expected, bool Actual)> _versions = null!;
List<(string Left, string Right, int Expected, int Actual)> _comparisons = null!;

void Establish()
{
using var document = JsonDocument.Parse(File.ReadAllText(PackageFixtures.FixturePath));
var root = document.RootElement;
_ranges =
[
.. root.GetProperty("versionRangeValidityCases").EnumerateArray()
.Select(element => (Text(element, "range"), element.GetProperty("expected").GetBoolean(), false))
];
_versions =
[
.. root.GetProperty("versionValidityCases").EnumerateArray()
.Select(element => (Text(element, "version"), element.GetProperty("expected").GetBoolean(), false))
];
_comparisons =
[
.. root.GetProperty("versionComparisonCases").EnumerateArray()
.Select(element => (element.GetProperty("left").GetString()!, element.GetProperty("right").GetString()!, element.GetProperty("expected").GetInt32(), 0))
];
}

void Because()
{
_ranges = [.. _ranges.Select(row => (row.Range, row.Expected, PackageVersionRange.IsValidRange(row.Range)))];
_versions = [.. _versions.Select(row => (row.Version, row.Expected, PackageVersionRange.IsValidVersion(row.Version)))];
_comparisons = [.. _comparisons.Select(row => (row.Left, row.Right, row.Expected, Math.Sign(PackageVersionRange.CompareVersions(row.Left, row.Right))))];
}

[Fact]
void should_judge_every_range_as_the_corpus_does() =>
((List<(string, bool)>)[.. _ranges.Select(row => ($"range '{row.Range ?? "<null>"}'", row.Actual))]).ShouldEqual([.. _ranges.Select(row => ($"range '{row.Range ?? "<null>"}'", row.Expected))]);

[Fact]
void should_judge_every_version_as_the_corpus_does() =>
((List<(string, bool)>)[.. _versions.Select(row => ($"version '{row.Version ?? "<null>"}'", row.Actual))]).ShouldEqual([.. _versions.Select(row => ($"version '{row.Version ?? "<null>"}'", row.Expected))]);

[Fact]
void should_order_every_pair_as_the_corpus_does() =>
((List<(string, int)>)[.. _comparisons.Select(row => ($"'{row.Left}' against '{row.Right}'", row.Actual))]).ShouldEqual([.. _comparisons.Select(row => ($"'{row.Left}' against '{row.Right}'", row.Expected))]);

static string? Text(JsonElement element, string property) =>
element.GetProperty(property).ValueKind == JsonValueKind.Null ? null : element.GetProperty(property).GetString();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Cratis.Scene.Engine.Templates;
using Cratis.Scene.Model.Profiles;

namespace Cratis.Scene.Engine.Profiles.for_ProfileBlockers;

public class when_fixing_sparse_profiles_against_the_shared_fixture_corpus : Specification
{
static readonly JsonSerializerOptions _options = new()
{
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter() }
};

List<(string Name, UiProfile Profile, ProfileFix Fix, JsonNode? Expected)> _cases = null!;
List<(string Name, JsonNode? Expected, JsonNode? Actual)> _results = null!;
List<(string Name, string Before, string After)> _inputs = null!;
IReadOnlyList<ProfileBlocker> _problemsWithNothingToCheck = null!;

void Establish()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "Scene.slnx")))
{
directory = directory.Parent;
}

using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(directory!.FullName, "profile-blockers-fixtures.json")));
_cases =
[
.. document.RootElement.GetProperty("fixEdgeCases").EnumerateArray().Select(element => (
element.GetProperty("name").GetString()!,
element.GetProperty("profile").Deserialize<UiProfile>(_options)!,
element.GetProperty("fix").Deserialize<ProfileFix>(_options)!,
JsonNode.Parse(element.GetProperty("expected").GetRawText())))
];
}

void Because()
{
List<string> snapshots = [.. _cases.Select(item => UiProfileJson.Serialize(item.Profile))];
_results = [.. _cases.Select(item => (item.Name, item.Expected, JsonNode.Parse(UiProfileJson.Serialize(ProfileBlockers.ApplyFix(item.Profile, item.Fix, [])))))];
_inputs = [.. _cases.Select((item, index) => (item.Name, snapshots[index], UiProfileJson.Serialize(item.Profile)))];
_problemsWithNothingToCheck = ProfileBlockers.FindProblems(_cases[0].Profile, new ProfileBlockerContext([], new TemplateLibrary(null, [])));
}

[Fact]
void should_give_the_profile_the_corpus_expects_for_every_sparse_fix() =>
_results.Where(item => !JsonNode.DeepEquals(item.Expected, item.Actual)).Select(item => $"{item.Name}: expected {item.Expected?.ToJsonString()} but was {item.Actual?.ToJsonString()}").ShouldBeEmpty();

[Fact]
void should_leave_every_input_profile_untouched() =>
_inputs.Where(item => item.Before != item.After).Select(item => item.Name).ShouldBeEmpty();

[Fact]
void should_cover_every_fix_kind() => _cases.Select(item => item.Fix.Kind).Distinct().Count().ShouldEqual(Enum.GetValues<ProfileFixKind>().Length);

[Fact]
void should_find_no_problems_when_there_is_no_shell_theme_usage_or_own_library() => _problemsWithNothingToCheck.ShouldBeEmpty();
}
Loading