From 968a85845325c121a321681e8d4436c2c9e1fdca Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Tue, 11 Aug 2026 03:02:40 +0900 Subject: [PATCH 1/3] style: normalize C# formatting across Editor and test sources - Apply consistent indentation and line wrapping - Normalize conditional expressions, constructors, collections, and assertions - Reformat rendering, stencil, persistence, and validation test fixtures - Preserve runtime behavior and test contracts --- Editor/PureBaseCutoffElement.cs | 22 +- Editor/PureBaseRenderingMode.cs | 98 +- Editor/PureBaseRenderingModeElement.cs | 74 +- ...aseRenderingModeContractTests.Atomicity.cs | 355 +++++-- ...gModeContractTests.InspectorPersistence.cs | 379 ++++++-- ...enderingModeContractTests.MaterialState.cs | 192 +++- ...eringModeContractTests.ProductContracts.cs | 372 ++++++-- ...eBaseRenderingModeContractTests.Support.cs | 176 +++- .../PureBaseRenderingModeContractTests.cs | 4 +- ...gModeRenderingTests.D24S8StencilFixture.cs | 646 ++++++++++--- ...deringModeRenderingTests.FrameReadbacks.cs | 857 +++++++++-------- ...eringModeRenderingTests.SourceContracts.cs | 865 ++++++++++++------ ...BaseRenderingModeRenderingTests.Stencil.cs | 334 ++++++- ...gModeRenderingTests.ToonForwardAddScope.cs | 178 +++- .../PureBaseRenderingModeRenderingTests.cs | 513 +++++++++-- .../PureBaseValidationSceneRegressionTests.cs | 77 +- .../Editor/PureBaseConsumerReleaseTests.cs | 24 +- .../PureBaseConsumerRenderingModeTests.cs | 150 ++- 18 files changed, 4012 insertions(+), 1304 deletions(-) diff --git a/Editor/PureBaseCutoffElement.cs b/Editor/PureBaseCutoffElement.cs index c83bf60..b4b94de 100644 --- a/Editor/PureBaseCutoffElement.cs +++ b/Editor/PureBaseCutoffElement.cs @@ -46,14 +46,21 @@ private static void RegisterDrawer() /// The Cutoff material property. /// Unused drawer arguments. /// The property container that owns the drawer UI. - private static void Draw(SCMaterialEditor editor, SCMaterialProperty property, string _, VisualElement container) + private static void Draw( + SCMaterialEditor editor, + SCMaterialProperty property, + string _, + VisualElement container + ) { var rangeContainer = new VisualElement(); container.Add(rangeContainer); editor.ShaderProperty(rangeContainer, property, new[] { CutoffRangeAttribute }); UpdateVisibility(rangeContainer, property.targets); - rangeContainer.RegisterCallback(_ => UpdateVisibility(rangeContainer, property.targets)); + rangeContainer.RegisterCallback(_ => + UpdateVisibility(rangeContainer, property.targets) + ); } /// Updates visibility from current selected material values without modifying them. @@ -62,7 +69,9 @@ private static void Draw(SCMaterialEditor editor, SCMaterialProperty property, s private static void UpdateVisibility(VisualElement container, UnityEngine.Object[] targets) { SelectionDisplayState displayState = GetSelectionDisplayState(targets); - container.style.display = displayState.IsVisible ? DisplayStyle.Flex : DisplayStyle.None; + container.style.display = displayState.IsVisible + ? DisplayStyle.Flex + : DisplayStyle.None; } /// Gets the read-only Cutoff drawer state for the supplied material selection. @@ -75,10 +84,13 @@ internal static SelectionDisplayState GetSelectionDisplayState(UnityEngine.Objec for (int index = 0; index < targets.Length; index++) { - if (targets[index] is Material material + if ( + targets[index] is Material material && PureBaseRenderingModeElement.IsPureBaseMaterial(material) && material.HasProperty(RenderingModePropertyName) - && material.GetInteger(RenderingModePropertyName) == (int)PureBaseRenderingMode.Cutout) + && material.GetInteger(RenderingModePropertyName) + == (int)PureBaseRenderingMode.Cutout + ) { return new SelectionDisplayState(true); } diff --git a/Editor/PureBaseRenderingMode.cs b/Editor/PureBaseRenderingMode.cs index 8e44f7c..87eb762 100644 --- a/Editor/PureBaseRenderingMode.cs +++ b/Editor/PureBaseRenderingMode.cs @@ -82,7 +82,9 @@ public static class PureBaseMaterialRenderingMode private const string ResyncUndoName = "Resync PureBase Rendering Mode"; /// Lists the only stable public shader names owned by Pure-Base. - private static readonly HashSet PureBaseShaderNames = new HashSet(StringComparer.Ordinal) + private static readonly HashSet PureBaseShaderNames = new HashSet( + StringComparer.Ordinal + ) { "PureBase/Unlit", "PureBase/Toon", @@ -135,7 +137,9 @@ internal static void ApplyAll(IReadOnlyList materials) /// when the material uses one of the four supported shader names. internal static bool IsPureBaseMaterial(Material material) { - return material != null && material.shader != null && PureBaseShaderNames.Contains(material.shader.name); + return material != null + && material.shader != null + && PureBaseShaderNames.Contains(material.shader.name); } /// Validates one material without modifying its serialized state. @@ -146,21 +150,36 @@ internal static void Validate(Material material) throw new ArgumentNullException(nameof(material)); if (!IsPureBaseMaterial(material)) - throw CreateValidationException(material, "its shader is not a supported Pure-Base shader"); + throw CreateValidationException( + material, + "its shader is not a supported Pure-Base shader" + ); Shader shader = material.shader; if (!material.HasProperty(RenderingModePropertyName)) - throw CreateValidationException(material, "it does not expose the Pure-Base rendering-mode property"); + throw CreateValidationException( + material, + "it does not expose the Pure-Base rendering-mode property" + ); int renderingModePropertyIndex = shader.FindPropertyIndex(RenderingModePropertyName); - if (renderingModePropertyIndex < 0 || shader.GetPropertyType(renderingModePropertyIndex) != ShaderPropertyType.Int) - throw CreateValidationException(material, "it does not expose the Pure-Base integer rendering-mode property"); + if ( + renderingModePropertyIndex < 0 + || shader.GetPropertyType(renderingModePropertyIndex) != ShaderPropertyType.Int + ) + throw CreateValidationException( + material, + "it does not expose the Pure-Base integer rendering-mode property" + ); for (int index = 0; index < RequiredStatePropertyNames.Length; index++) { if (!material.HasProperty(RequiredStatePropertyNames[index])) - throw CreateValidationException(material, "it does not expose the complete Pure-Base rendering-mode state contract"); + throw CreateValidationException( + material, + "it does not expose the complete Pure-Base rendering-mode state contract" + ); } GetModeIndex(material); @@ -170,9 +189,14 @@ internal static void Validate(Material material) /// The non-null material that failed validation. /// The specific rendering-mode contract rejection reason. /// An exception that preserves the established validation exception type. - private static InvalidOperationException CreateValidationException(Material material, string reason) + private static InvalidOperationException CreateValidationException( + Material material, + string reason + ) { - return new InvalidOperationException("Material '" + material.name + "' was rejected because " + reason + "."); + return new InvalidOperationException( + "Material '" + material.name + "' was rejected because " + reason + "." + ); } /// Invokes selected-material resynchronization from Unity's Assets menu. @@ -331,7 +355,9 @@ private static int GetModeIndex(Material material) throw new ArgumentOutOfRangeException( RenderingModePropertyName, value, - "Material '" + material.name + "' has a rendering-mode value outside the supported range: 0, 1, or 2." + "Material '" + + material.name + + "' has a rendering-mode value outside the supported range: 0, 1, or 2." ); return value; @@ -405,7 +431,8 @@ private ModeState( int additiveDestinationBlend, string renderType, int rawRenderQueue, - ModeStateFlags flags) + ModeStateFlags flags + ) { SourceBlend = sourceBlend; DestinationBlend = destinationBlend; @@ -457,7 +484,11 @@ private readonly struct ModeStateFlags /// Whether the Opaque keyword is enabled. /// Whether the Transparent keyword is enabled. /// Whether ShadowCaster and Meta are enabled. - public ModeStateFlags(bool enableOpaqueKeyword, bool enableTransparentKeyword, bool enableContributionPasses) + public ModeStateFlags( + bool enableOpaqueKeyword, + bool enableTransparentKeyword, + bool enableContributionPasses + ) { EnableOpaqueKeyword = enableOpaqueKeyword; EnableTransparentKeyword = enableTransparentKeyword; @@ -490,7 +521,8 @@ private MaterialStateSnapshot( float depthWrite, float additiveSourceBlend, float additiveDestinationBlend, - MaterialStateSnapshotMetadata metadata) + MaterialStateSnapshotMetadata metadata + ) { SourceBlend = sourceBlend; DestinationBlend = destinationBlend; @@ -551,7 +583,10 @@ private MaterialStateSnapshot( /// A rollback snapshot for . public static MaterialStateSnapshot Capture(Material material) { - bool hasRenderTypeOverride = TryGetRawRenderTypeOverride(material, out string renderTypeOverride); + bool hasRenderTypeOverride = TryGetRawRenderTypeOverride( + material, + out string renderTypeOverride + ); return new MaterialStateSnapshot( material.GetFloat(SourceBlendPropertyName), material.GetFloat(DestinationBlendPropertyName), @@ -580,7 +615,10 @@ public void Restore(Material material) material.SetFloat(DepthWritePropertyName, DepthWrite); material.SetFloat(AdditiveSourceBlendPropertyName, AdditiveSourceBlend); material.SetFloat(AdditiveDestinationBlendPropertyName, AdditiveDestinationBlend); - material.SetOverrideTag(RenderTypeTagName, HasRenderTypeOverride ? RenderTypeOverride : string.Empty); + material.SetOverrideTag( + RenderTypeTagName, + HasRenderTypeOverride ? RenderTypeOverride : string.Empty + ); material.renderQueue = RawRenderQueue; SetKeyword(material, OpaqueKeyword, OpaqueKeywordEnabled); SetKeyword(material, TransparentKeyword, TransparentKeywordEnabled); @@ -611,7 +649,8 @@ public MaterialStateSnapshotMetadata( bool transparentKeywordEnabled, bool shadowCasterEnabled, bool metaEnabled, - bool wasDirty) + bool wasDirty + ) { HasRenderTypeOverride = hasRenderTypeOverride; RenderTypeOverride = renderTypeOverride; @@ -652,14 +691,25 @@ public MaterialStateSnapshotMetadata( /// The material whose serialized tag map is read. /// Receives the raw override value when one exists. /// when the material serializes an explicit RenderType override. - private static bool TryGetRawRenderTypeOverride(Material material, out string renderTypeOverride) + private static bool TryGetRawRenderTypeOverride( + Material material, + out string renderTypeOverride + ) { string serializedMaterial = EditorJsonUtility.ToJson(material); - Match tagMap = Regex.Match(serializedMaterial, @"""stringTagMap""\s*:\s*\{(?[^}]*)\}"); + Match tagMap = Regex.Match( + serializedMaterial, + @"""stringTagMap""\s*:\s*\{(?[^}]*)\}" + ); if (!tagMap.Success) - throw new InvalidOperationException("The material does not expose a serialized raw RenderType tag map."); + throw new InvalidOperationException( + "The material does not expose a serialized raw RenderType tag map." + ); - Match renderType = Regex.Match(tagMap.Groups["entries"].Value, @"""RenderType""\s*:\s*""(?[^""]*)"""); + Match renderType = Regex.Match( + tagMap.Groups["entries"].Value, + @"""RenderType""\s*:\s*""(?[^""]*)""" + ); renderTypeOverride = renderType.Success ? renderType.Groups["value"].Value : null; return renderType.Success; } @@ -671,9 +721,13 @@ private static int GetRawRenderQueue(Material material) { using (var serializedMaterial = new SerializedObject(material)) { - SerializedProperty rawRenderQueue = serializedMaterial.FindProperty("m_CustomRenderQueue"); + SerializedProperty rawRenderQueue = serializedMaterial.FindProperty( + "m_CustomRenderQueue" + ); if (rawRenderQueue == null) - throw new InvalidOperationException("The material does not expose a serialized raw render queue."); + throw new InvalidOperationException( + "The material does not expose a serialized raw render queue." + ); return rawRenderQueue.intValue; } diff --git a/Editor/PureBaseRenderingModeElement.cs b/Editor/PureBaseRenderingModeElement.cs index 0937e07..4324e5e 100644 --- a/Editor/PureBaseRenderingModeElement.cs +++ b/Editor/PureBaseRenderingModeElement.cs @@ -36,10 +36,12 @@ internal sealed class PureBaseRenderingModeElement : PopupField, IMaterialP private const string UndoName = "Set PureBase Rendering Mode"; /// Explains the derived state of one Transparent material selection. - private const string TransparentDescription = "Transparent materials use alpha blending. ZWrite, ShadowCaster, and Meta are disabled."; + private const string TransparentDescription = + "Transparent materials use alpha blending. ZWrite, ShadowCaster, and Meta are disabled."; /// Explains the derived state when a mixed selection includes Transparent materials. - private const string MixedTransparentDescription = "One or more selected materials are Transparent. Those materials use alpha blending, and their ZWrite, ShadowCaster, and Meta are disabled."; + private const string MixedTransparentDescription = + "One or more selected materials are Transparent. Those materials use alpha blending, and their ZWrite, ShadowCaster, and Meta are disabled."; /// Defines the mode values in their popup display order. private static readonly List ModeValues = new List @@ -50,12 +52,7 @@ internal sealed class PureBaseRenderingModeElement : PopupField, IMaterialP }; /// Defines the stable English mode labels used by the selection model. - private static readonly string[] ModeNames = - { - "Opaque", - "Cutout", - "Transparent", - }; + private static readonly string[] ModeNames = { "Opaque", "Cutout", "Transparent" }; /// Stores the material property currently represented by this field. public SCMaterialProperty Property { get; set; } @@ -90,7 +87,12 @@ private static void RegisterDrawer() /// The rendering-mode material property. /// Unused drawer arguments. /// The property container that owns the drawer UI. - private static void Draw(SCMaterialEditor _, SCMaterialProperty property, string arguments, VisualElement container) + private static void Draw( + SCMaterialEditor _, + SCMaterialProperty property, + string arguments, + VisualElement container + ) { var element = new PureBaseRenderingModeElement(property); container.Add(element.Root); @@ -105,8 +107,14 @@ public PureBaseRenderingModeElement(SCMaterialProperty property) formatListItemCallback = GetModeLabel; formatSelectedValueCallback = GetModeLabel; - TransparentHelpBox = new HelpBox(SCL10n.L(TransparentDescription), HelpBoxMessageType.Info); - MixedTransparentHelpBox = new HelpBox(SCL10n.L(MixedTransparentDescription), HelpBoxMessageType.Info); + TransparentHelpBox = new HelpBox( + SCL10n.L(TransparentDescription), + HelpBoxMessageType.Info + ); + MixedTransparentHelpBox = new HelpBox( + SCL10n.L(MixedTransparentDescription), + HelpBoxMessageType.Info + ); Root = new VisualElement(); Root.Add(this); Root.Add(TransparentHelpBox); @@ -132,8 +140,15 @@ internal static void ApplySelection(Material[] materials, int mode) { if (materials == null) throw new ArgumentNullException(nameof(materials)); - if (mode < (int)PureBaseRenderingMode.Opaque || mode > (int)PureBaseRenderingMode.Transparent) - throw new ArgumentOutOfRangeException(nameof(mode), mode, "The rendering mode must be Opaque, Cutout, or Transparent."); + if ( + mode < (int)PureBaseRenderingMode.Opaque + || mode > (int)PureBaseRenderingMode.Transparent + ) + throw new ArgumentOutOfRangeException( + nameof(mode), + mode, + "The rendering mode must be Opaque, Cutout, or Transparent." + ); for (int index = 0; index < materials.Length; index++) PureBaseMaterialRenderingMode.Validate(materials[index]); @@ -183,7 +198,12 @@ internal static SelectionDisplayState GetSelectionDisplayState(Material[] materi } } - return new SelectionDisplayState(selectedValue, hasMixedValue, containsTransparent, ModeNames); + return new SelectionDisplayState( + selectedValue, + hasMixedValue, + containsTransparent, + ModeNames + ); } /// Determines whether one material uses a stable Pure-Base shader. @@ -202,12 +222,15 @@ public void UpdateUI() showMixedValue = displayState.HasMixedValue; SetValueWithoutNotify(displayState.SelectedValue); textElement.text = GetModeLabel(displayState.SelectedValue); - TransparentHelpBox.style.display = !displayState.HasMixedValue && displayState.SelectedValue == (int)PureBaseRenderingMode.Transparent - ? DisplayStyle.Flex - : DisplayStyle.None; - MixedTransparentHelpBox.style.display = displayState.HasMixedValue && displayState.ContainsTransparent - ? DisplayStyle.Flex - : DisplayStyle.None; + TransparentHelpBox.style.display = + !displayState.HasMixedValue + && displayState.SelectedValue == (int)PureBaseRenderingMode.Transparent + ? DisplayStyle.Flex + : DisplayStyle.None; + MixedTransparentHelpBox.style.display = + displayState.HasMixedValue && displayState.ContainsTransparent + ? DisplayStyle.Flex + : DisplayStyle.None; } /// Creates localized labels for the fixed rendering-mode names. @@ -244,7 +267,9 @@ private static int GetDisplayModeValue(Material material) int mode = material.HasProperty(RenderingModePropertyName) ? material.GetInteger(RenderingModePropertyName) : (int)PureBaseRenderingMode.Cutout; - return mode >= (int)PureBaseRenderingMode.Opaque && mode <= (int)PureBaseRenderingMode.Transparent + return + mode >= (int)PureBaseRenderingMode.Opaque + && mode <= (int)PureBaseRenderingMode.Transparent ? mode : (int)PureBaseRenderingMode.Cutout; } @@ -276,7 +301,12 @@ internal readonly struct SelectionDisplayState /// Whether selected materials have different modes. /// Whether any selected material is Transparent. /// The stable labels displayed by the popup. - public SelectionDisplayState(int selectedValue, bool hasMixedValue, bool containsTransparent, IReadOnlyList choices) + public SelectionDisplayState( + int selectedValue, + bool hasMixedValue, + bool containsTransparent, + IReadOnlyList choices + ) { SelectedValue = selectedValue; HasMixedValue = hasMixedValue; diff --git a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Atomicity.cs b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Atomicity.cs index c5a82e8..b0a7ac2 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Atomicity.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Atomicity.cs @@ -29,7 +29,6 @@ using UnityEngine; using UnityEngine.Rendering; - namespace PureBase.Tests.Daily { public sealed partial class PureBaseRenderingModeContractTests @@ -46,21 +45,64 @@ public void InvalidNormalizerInputsAreAtomicForSingleAndMultipleTargets() var assertedPropertyTypes = new HashSet(); var unsupportedOwnership = CreateMaterial(RequireUnsupportedRenderingModeShader()); - AssertUnsupportedInputIsAtomic(apply, unsupportedOwnership, true, "The unsupported ownership input must expose _RenderingMode without being owned by Pure-Base.", "non-Pure-Base shader with _RenderingMode", seededPropertyTypes, capturedPropertyTypes, assertedPropertyTypes); + AssertUnsupportedInputIsAtomic( + apply, + unsupportedOwnership, + true, + "The unsupported ownership input must expose _RenderingMode without being owned by Pure-Base.", + "non-Pure-Base shader with _RenderingMode", + seededPropertyTypes, + capturedPropertyTypes, + assertedPropertyTypes + ); - var unsupportedMissingProperty = CreateMaterial(RequireUnsupportedShaderWithoutRenderingMode()); - AssertUnsupportedInputIsAtomic(apply, unsupportedMissingProperty, false, "The missing-property input must not expose _RenderingMode.", "non-Pure-Base shader without _RenderingMode", seededPropertyTypes, capturedPropertyTypes, assertedPropertyTypes); + var unsupportedMissingProperty = CreateMaterial( + RequireUnsupportedShaderWithoutRenderingMode() + ); + AssertUnsupportedInputIsAtomic( + apply, + unsupportedMissingProperty, + false, + "The missing-property input must not expose _RenderingMode.", + "non-Pure-Base shader without _RenderingMode", + seededPropertyTypes, + capturedPropertyTypes, + assertedPropertyTypes + ); var first = CreateMaterial(RequireProductShader("PureBase/Unlit")); var second = CreateMaterial(RequireProductShader("PureBase/Toon")); - AssertInvalidModesAreAtomic(apply, applyAll, first, second, seededPropertyTypes, capturedPropertyTypes, assertedPropertyTypes); + AssertInvalidModesAreAtomic( + apply, + applyAll, + first, + second, + seededPropertyTypes, + capturedPropertyTypes, + assertedPropertyTypes + ); - foreach (Material coverageMaterial in CreateAtomicityCoverageMaterials(seededPropertyTypes, capturedPropertyTypes, assertedPropertyTypes)) + foreach ( + Material coverageMaterial in CreateAtomicityCoverageMaterials( + seededPropertyTypes, + capturedPropertyTypes, + assertedPropertyTypes + ) + ) { SeedAtomicityState(coverageMaterial, seededPropertyTypes); - MaterialState before = MaterialState.Capture(coverageMaterial, capturedPropertyTypes); - Assert.Throws(() => InvokeApply(apply, coverageMaterial)); - before.AssertEqual(coverageMaterial, "non-Pure-Base property-type coverage target", assertedPropertyTypes); + MaterialState before = MaterialState.Capture( + coverageMaterial, + capturedPropertyTypes + ); + Assert.Throws(() => + InvokeApply(apply, coverageMaterial) + ); + before.AssertEqual( + coverageMaterial, + "non-Pure-Base property-type coverage target", + assertedPropertyTypes + ); } AssertCompleteAtomicityPropertyTypeCoverage(seededPropertyTypes, "seed"); @@ -77,10 +119,23 @@ public void InvalidNormalizerInputsAreAtomicForSingleAndMultipleTargets() /// The set that records seeded shader property types. /// The set that records captured shader property types. /// The set that records asserted shader property types. - private void AssertUnsupportedInputIsAtomic(MethodInfo apply, Material material, bool hasRenderingMode, string propertyMessage, string context, ISet seededPropertyTypes, ISet capturedPropertyTypes, ISet assertedPropertyTypes) + private void AssertUnsupportedInputIsAtomic( + MethodInfo apply, + Material material, + bool hasRenderingMode, + string propertyMessage, + string context, + ISet seededPropertyTypes, + ISet capturedPropertyTypes, + ISet assertedPropertyTypes + ) { SeedAtomicityState(material, seededPropertyTypes); - Assert.That(material.HasProperty("_RenderingMode"), Is.EqualTo(hasRenderingMode), propertyMessage); + Assert.That( + material.HasProperty("_RenderingMode"), + Is.EqualTo(hasRenderingMode), + propertyMessage + ); MaterialState before = MaterialState.Capture(material, capturedPropertyTypes); Assert.Throws(() => InvokeApply(apply, material)); before.AssertEqual(material, context, assertedPropertyTypes); @@ -94,7 +149,15 @@ private void AssertUnsupportedInputIsAtomic(MethodInfo apply, Material material, /// The set that records seeded shader property types. /// The set that records captured shader property types. /// The set that records asserted shader property types. - private void AssertInvalidModesAreAtomic(MethodInfo apply, MethodInfo applyAll, Material first, Material second, ISet seededPropertyTypes, ISet capturedPropertyTypes, ISet assertedPropertyTypes) + private void AssertInvalidModesAreAtomic( + MethodInfo apply, + MethodInfo applyAll, + Material first, + Material second, + ISet seededPropertyTypes, + ISet capturedPropertyTypes, + ISet assertedPropertyTypes + ) { SeedAtomicityState(first, seededPropertyTypes); SeedAtomicityState(second, seededPropertyTypes); @@ -106,14 +169,45 @@ private void AssertInvalidModesAreAtomic(MethodInfo apply, MethodInfo applyAll, MaterialState firstBefore = MaterialState.Capture(first, capturedPropertyTypes); MaterialState secondBefore = MaterialState.Capture(second, capturedPropertyTypes); firstBefore.AssertCapturesShaderProperty("_PureBaseShaderLabSentinel"); - ArgumentOutOfRangeException exception = Assert.Throws(() => InvokeApply(apply, first)); - AssertInvalidRenderingModeException(exception, first, invalidMode, "single-target invalid mode"); - firstBefore.AssertEqual(first, $"invalid mode {invalidMode}", assertedPropertyTypes); - secondBefore.AssertEqual(second, $"unrelated target after invalid mode {invalidMode}", assertedPropertyTypes); - exception = Assert.Throws(() => InvokeApplyAll(applyAll, new[] { first, second })); - AssertInvalidRenderingModeException(exception, first, invalidMode, "batch invalid mode"); - firstBefore.AssertEqual(first, $"batch invalid mode {invalidMode}", assertedPropertyTypes); - secondBefore.AssertEqual(second, $"unrelated target after batch invalid mode {invalidMode}", assertedPropertyTypes); + ArgumentOutOfRangeException exception = Assert.Throws( + () => + InvokeApply(apply, first) + ); + AssertInvalidRenderingModeException( + exception, + first, + invalidMode, + "single-target invalid mode" + ); + firstBefore.AssertEqual( + first, + $"invalid mode {invalidMode}", + assertedPropertyTypes + ); + secondBefore.AssertEqual( + second, + $"unrelated target after invalid mode {invalidMode}", + assertedPropertyTypes + ); + exception = Assert.Throws(() => + InvokeApplyAll(applyAll, new[] { first, second }) + ); + AssertInvalidRenderingModeException( + exception, + first, + invalidMode, + "batch invalid mode" + ); + firstBefore.AssertEqual( + first, + $"batch invalid mode {invalidMode}", + assertedPropertyTypes + ); + secondBefore.AssertEqual( + second, + $"unrelated target after batch invalid mode {invalidMode}", + assertedPropertyTypes + ); } } @@ -139,20 +233,56 @@ public void AtomicBatchRollbackRestoresRawRenderTypeOverridesAfterLateFailure() EditorUtility.ClearDirty(first); EditorUtility.ClearDirty(second); EditorUtility.ClearDirty(failing); - AssertDistinctFallbackRenderTypeOverrideStates(first, second, "before late batch rollback"); + AssertDistinctFallbackRenderTypeOverrideStates( + first, + second, + "before late batch rollback" + ); MaterialState firstBefore = MaterialState.Capture(first); MaterialState secondBefore = MaterialState.Capture(second); - var materials = new LateInvalidatingMaterialList(new[] { first, second, failing }, 2, invalidMode); + var materials = new LateInvalidatingMaterialList( + new[] { first, second, failing }, + 2, + invalidMode + ); - ArgumentOutOfRangeException exception = Assert.Throws(() => InvokeApplyAll(applyAll, materials)); - AssertInvalidRenderingModeException(exception, failing, invalidMode, "late batch invalid mode"); - Assert.That(materials.ObservedPriorMutations, Is.True, "The late invalidation must occur after prior materials are normalized."); + ArgumentOutOfRangeException exception = Assert.Throws( + () => + InvokeApplyAll(applyAll, materials) + ); + AssertInvalidRenderingModeException( + exception, + failing, + invalidMode, + "late batch invalid mode" + ); + Assert.That( + materials.ObservedPriorMutations, + Is.True, + "The late invalidation must occur after prior materials are normalized." + ); firstBefore.AssertEqual(first, "first material after late batch rollback"); secondBefore.AssertEqual(second, "second material after late batch rollback"); - AssertDistinctFallbackRenderTypeOverrideStates(first, second, "after late batch rollback"); - Assert.That(AssetDatabase.GetAssetPath(first), Is.Empty, "The rollback fixture must remain transient."); - Assert.That(AssetDatabase.GetAssetPath(second), Is.Empty, "The rollback fixture must remain transient."); - Assert.That(AssetDatabase.GetAssetPath(failing), Is.Empty, "The failure fixture must remain transient."); + AssertDistinctFallbackRenderTypeOverrideStates( + first, + second, + "after late batch rollback" + ); + Assert.That( + AssetDatabase.GetAssetPath(first), + Is.Empty, + "The rollback fixture must remain transient." + ); + Assert.That( + AssetDatabase.GetAssetPath(second), + Is.Empty, + "The rollback fixture must remain transient." + ); + Assert.That( + AssetDatabase.GetAssetPath(failing), + Is.Empty, + "The failure fixture must remain transient." + ); } } @@ -160,28 +290,68 @@ public void AtomicBatchRollbackRestoresRawRenderTypeOverridesAfterLateFailure() /// The material whose raw RenderType override is absent. /// The material whose raw override equals the shader fallback. /// The operation boundary described by the assertions. - private static void AssertDistinctFallbackRenderTypeOverrideStates(Material withoutOverride, Material withFallbackOverride, string context) + private static void AssertDistinctFallbackRenderTypeOverrideStates( + Material withoutOverride, + Material withFallbackOverride, + string context + ) { - bool hasAbsentOverride = TryGetSerializedRenderTypeOverride(withoutOverride, out string absentOverride); - bool hasFallbackOverride = TryGetSerializedRenderTypeOverride(withFallbackOverride, out string fallbackOverride); - Assert.That(hasAbsentOverride, Is.False, $"The {context} absent override fixture must not serialize RenderType."); - Assert.That(absentOverride, Is.Null, $"The {context} absent override fixture must not expose a RenderType value."); - Assert.That(hasFallbackOverride, Is.True, $"The {context} fallback override fixture must serialize RenderType."); - Assert.That(fallbackOverride, Is.EqualTo("TransparentCutout"), $"The {context} fallback override fixture must preserve its raw RenderType value."); - Assert.That(withoutOverride.GetTag("RenderType", false), Is.EqualTo("TransparentCutout"), $"The {context} absent override fixture must resolve the PureBase SubShader RenderType fallback."); - Assert.That(withFallbackOverride.GetTag("RenderType", false), Is.EqualTo("TransparentCutout"), $"The {context} fallback override fixture must resolve the same RenderType value."); + bool hasAbsentOverride = TryGetSerializedRenderTypeOverride( + withoutOverride, + out string absentOverride + ); + bool hasFallbackOverride = TryGetSerializedRenderTypeOverride( + withFallbackOverride, + out string fallbackOverride + ); + Assert.That( + hasAbsentOverride, + Is.False, + $"The {context} absent override fixture must not serialize RenderType." + ); + Assert.That( + absentOverride, + Is.Null, + $"The {context} absent override fixture must not expose a RenderType value." + ); + Assert.That( + hasFallbackOverride, + Is.True, + $"The {context} fallback override fixture must serialize RenderType." + ); + Assert.That( + fallbackOverride, + Is.EqualTo("TransparentCutout"), + $"The {context} fallback override fixture must preserve its raw RenderType value." + ); + Assert.That( + withoutOverride.GetTag("RenderType", false), + Is.EqualTo("TransparentCutout"), + $"The {context} absent override fixture must resolve the PureBase SubShader RenderType fallback." + ); + Assert.That( + withFallbackOverride.GetTag("RenderType", false), + Is.EqualTo("TransparentCutout"), + $"The {context} fallback override fixture must resolve the same RenderType value." + ); } /// Assigns distinguishable values to every shader property before atomicity snapshots without modifying persistent assets. /// The transient material that must remain unchanged after rejection. /// The optional set that records seeded shader property types. - private void SeedAtomicityState(Material material, ISet observedPropertyTypes = null) + private void SeedAtomicityState( + Material material, + ISet observedPropertyTypes = null + ) { Shader shader = material.shader; for (int index = 0; index < ShaderUtil.GetPropertyCount(shader); index++) { string propertyName = shader.GetPropertyName(index); - ShaderUtil.ShaderPropertyType propertyType = ShaderUtil.GetPropertyType(shader, index); + ShaderUtil.ShaderPropertyType propertyType = ShaderUtil.GetPropertyType( + shader, + index + ); ObserveAtomicityPropertyType(observedPropertyTypes, propertyType); switch (propertyType) { @@ -193,10 +363,16 @@ private void SeedAtomicityState(Material material, ISet CreateAtomicityCoverageMaterials( ISet seededPropertyTypes, ISet capturedPropertyTypes, - ISet assertedPropertyTypes) + ISet assertedPropertyTypes + ) { foreach (ShaderUtil.ShaderPropertyType propertyType in RequiredAtomicityPropertyTypes) { - if (seededPropertyTypes.Contains(propertyType) + if ( + seededPropertyTypes.Contains(propertyType) && capturedPropertyTypes.Contains(propertyType) - && assertedPropertyTypes.Contains(propertyType)) + && assertedPropertyTypes.Contains(propertyType) + ) continue; - yield return CreateMaterial(RequireSupportedNonProductShaderWithPropertyType(propertyType)); + yield return CreateMaterial( + RequireSupportedNonProductShaderWithPropertyType(propertyType) + ); } } /// Returns a deterministic supported non-Pure-Base shader that exposes one required property type. /// The property type required by atomicity coverage. /// An imported, supported non-Pure-Base shader. - private static Shader RequireSupportedNonProductShaderWithPropertyType(ShaderUtil.ShaderPropertyType propertyType) + private static Shader RequireSupportedNonProductShaderWithPropertyType( + ShaderUtil.ShaderPropertyType propertyType + ) { Shader shader = RequireUnsupportedRenderingModeShader(); for (int index = 0; index < ShaderUtil.GetPropertyCount(shader); index++) @@ -279,14 +466,19 @@ private static Shader RequireSupportedNonProductShaderWithPropertyType(ShaderUti return shader; } - Assert.Fail($"The deterministic non-Pure-Base fixture shader did not expose '{propertyType}' for atomicity coverage."); + Assert.Fail( + $"The deterministic non-Pure-Base fixture shader did not expose '{propertyType}' for atomicity coverage." + ); return null; } /// Records one property type observed by an atomicity execution path. /// The optional path-local observed type set. /// The property type encountered by the path. - private static void ObserveAtomicityPropertyType(ISet observedPropertyTypes, ShaderUtil.ShaderPropertyType propertyType) + private static void ObserveAtomicityPropertyType( + ISet observedPropertyTypes, + ShaderUtil.ShaderPropertyType propertyType + ) { if (observedPropertyTypes != null) observedPropertyTypes.Add(propertyType); @@ -295,7 +487,10 @@ private static void ObserveAtomicityPropertyType(ISetRequires one atomicity execution path to exercise every supported property type. /// The types observed by the execution path. /// The diagnostic name of the execution path. - private static void AssertCompleteAtomicityPropertyTypeCoverage(ISet observedPropertyTypes, string pathName) + private static void AssertCompleteAtomicityPropertyTypeCoverage( + ISet observedPropertyTypes, + string pathName + ) { CollectionAssert.AreEquivalent( RequiredAtomicityPropertyTypes, @@ -307,11 +502,17 @@ private static void AssertCompleteAtomicityPropertyTypeCoverage(ISetRecords every property type visible to one atomicity assertion path. /// The material whose shader properties are being asserted. /// The optional path-local observed type set. - private static void ObserveAtomicityPropertyTypes(Material material, ISet observedPropertyTypes) + private static void ObserveAtomicityPropertyTypes( + Material material, + ISet observedPropertyTypes + ) { Shader shader = material.shader; for (int index = 0; index < ShaderUtil.GetPropertyCount(shader); index++) - ObserveAtomicityPropertyType(observedPropertyTypes, ShaderUtil.GetPropertyType(shader, index)); + ObserveAtomicityPropertyType( + observedPropertyTypes, + ShaderUtil.GetPropertyType(shader, index) + ); } /// Returns one supported non-Pure-Base shader that has no rendering-mode property. @@ -320,7 +521,11 @@ private static Shader RequireUnsupportedShaderWithoutRenderingMode() { Shader shader = Shader.Find("Standard") ?? Shader.Find("Unlit/Color"); Assert.That(shader, Is.Not.Null, "No built-in unsupported shader was available."); - Assert.That(shader.FindPropertyIndex("_RenderingMode"), Is.LessThan(0), "The missing-property shader must not expose _RenderingMode."); + Assert.That( + shader.FindPropertyIndex("_RenderingMode"), + Is.LessThan(0), + "The missing-property shader must not expose _RenderingMode." + ); return shader; } @@ -328,13 +533,39 @@ private static Shader RequireUnsupportedShaderWithoutRenderingMode() /// A non-Pure-Base shader with _RenderingMode. private static Shader RequireUnsupportedRenderingModeShader() { - Shader shader = AssetDatabase.LoadAssetAtPath(UnsupportedRenderingModeFixturePath); - Assert.That(shader, Is.Not.Null, $"The unsupported-ownership fixture shader was not imported at '{UnsupportedRenderingModeFixturePath}'."); - Assert.That(shader.name, Is.EqualTo("PureBaseTests/Unsupported Rendering Mode"), "The unsupported-ownership fixture shader name changed."); - Assert.That(shader.name, Does.Not.StartWith("PureBase/"), "The unsupported-ownership fixture shader must not be owned by Pure-Base."); - Assert.That(ShaderUtil.ShaderHasError(shader), Is.False, "The unsupported-ownership fixture shader has import errors."); - Assert.That(shader.isSupported, Is.True, "The unsupported-ownership fixture shader is unsupported."); - Assert.That(shader.FindPropertyIndex("_RenderingMode"), Is.GreaterThanOrEqualTo(0), "The unsupported-ownership fixture shader must expose _RenderingMode."); + Shader shader = AssetDatabase.LoadAssetAtPath( + UnsupportedRenderingModeFixturePath + ); + Assert.That( + shader, + Is.Not.Null, + $"The unsupported-ownership fixture shader was not imported at '{UnsupportedRenderingModeFixturePath}'." + ); + Assert.That( + shader.name, + Is.EqualTo("PureBaseTests/Unsupported Rendering Mode"), + "The unsupported-ownership fixture shader name changed." + ); + Assert.That( + shader.name, + Does.Not.StartWith("PureBase/"), + "The unsupported-ownership fixture shader must not be owned by Pure-Base." + ); + Assert.That( + ShaderUtil.ShaderHasError(shader), + Is.False, + "The unsupported-ownership fixture shader has import errors." + ); + Assert.That( + shader.isSupported, + Is.True, + "The unsupported-ownership fixture shader is unsupported." + ); + Assert.That( + shader.FindPropertyIndex("_RenderingMode"), + Is.GreaterThanOrEqualTo(0), + "The unsupported-ownership fixture shader must expose _RenderingMode." + ); return shader; } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.InspectorPersistence.cs b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.InspectorPersistence.cs index 9208dcd..fc63e40 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.InspectorPersistence.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.InspectorPersistence.cs @@ -29,7 +29,6 @@ using UnityEngine; using UnityEngine.Rendering; - namespace PureBase.Tests.Daily { public sealed partial class PureBaseRenderingModeContractTests @@ -54,7 +53,11 @@ private static void AssertRenderingModeDrawerRegistration() ); Type attributeActionsType = FindLoadedType("jp.lilxyzw.shadercore.AttributeActions"); - Assert.That(attributeActionsType, Is.Not.Null, "Shader-Core AttributeActions was not loaded."); + Assert.That( + attributeActionsType, + Is.Not.Null, + "Shader-Core AttributeActions was not loaded." + ); MethodInfo containsKey = attributeActionsType.GetMethod( "ContainsKey", BindingFlags.Public | BindingFlags.Static, @@ -63,11 +66,17 @@ private static void AssertRenderingModeDrawerRegistration() null ); Assert.That(containsKey, Is.Not.Null); - Assert.That((bool)containsKey.Invoke(null, new object[] { "PureBaseRenderingMode" }), Is.True); + Assert.That( + (bool)containsKey.Invoke(null, new object[] { "PureBaseRenderingMode" }), + Is.True + ); } /// Asserts the complete read-only mixed-selection drawer workflow for two normalized material modes. - private static void AssertMixedSelectionDrawerReadsAreReadOnly(Material opaque, Material transparent) + private static void AssertMixedSelectionDrawerReadsAreReadOnly( + Material opaque, + Material transparent + ) { MethodInfo apply = RequireApplyMethod(); MethodInfo refreshSelection = RequireDrawerSelectionRefreshMethod(); @@ -77,36 +86,94 @@ private static void AssertMixedSelectionDrawerReadsAreReadOnly(Material opaque, NormalizeAndClearMixedSelectionTargets(apply, opaque, transparent); MaterialState opaqueBaseline = MaterialState.Capture(opaque); MaterialState transparentBaseline = MaterialState.Capture(transparent); - MaterialProperty property = MaterialEditor.GetMaterialProperty(new UnityEngine.Object[] { opaque, transparent }, "_RenderingMode"); - Assert.That(property.hasMixedValue, Is.True, "The rendering-mode field must expose mixed state before user selection."); + MaterialProperty property = MaterialEditor.GetMaterialProperty( + new UnityEngine.Object[] { opaque, transparent }, + "_RenderingMode" + ); + Assert.That( + property.hasMixedValue, + Is.True, + "The rendering-mode field must expose mixed state before user selection." + ); opaqueBaseline.AssertEqual(opaque, "Opaque target after mixed field binding"); - transparentBaseline.AssertEqual(transparent, "Transparent target after mixed field binding"); - object selectionDisplayState = InvokeDrawerSelectionDisplayState(getSelectionDisplayState, new[] { opaque, transparent }); - AssertSelectionDisplayState(selectionDisplayState, true, new[] { "Opaque", "Cutout", "Transparent" }); - opaqueBaseline.AssertEqual(opaque, "Opaque target after mixed drawer display-state read"); - transparentBaseline.AssertEqual(transparent, "Transparent target after mixed drawer display-state read"); + transparentBaseline.AssertEqual( + transparent, + "Transparent target after mixed field binding" + ); + object selectionDisplayState = InvokeDrawerSelectionDisplayState( + getSelectionDisplayState, + new[] { opaque, transparent } + ); + AssertSelectionDisplayState( + selectionDisplayState, + true, + new[] { "Opaque", "Cutout", "Transparent" } + ); + opaqueBaseline.AssertEqual( + opaque, + "Opaque target after mixed drawer display-state read" + ); + transparentBaseline.AssertEqual( + transparent, + "Transparent target after mixed drawer display-state read" + ); InvokeDrawerSelectionRefresh(refreshSelection, new[] { opaque, transparent }); opaqueBaseline.AssertEqual(opaque, "Opaque target after read-only mixed refresh"); - transparentBaseline.AssertEqual(transparent, "Transparent target after read-only mixed refresh"); + transparentBaseline.AssertEqual( + transparent, + "Transparent target after read-only mixed refresh" + ); } /// Explicitly normalizes each mixed-selection target and restores the clean read-only baseline. - private static void NormalizeAndClearMixedSelectionTargets(MethodInfo apply, Material opaque, Material transparent) + private static void NormalizeAndClearMixedSelectionTargets( + MethodInfo apply, + Material opaque, + Material transparent + ) { EditorUtility.ClearDirty(opaque); EditorUtility.ClearDirty(transparent); - Assert.That(EditorUtility.IsDirty(opaque), Is.False, "The Opaque resync target must be clean before explicit normalization."); - Assert.That(EditorUtility.IsDirty(transparent), Is.False, "The Transparent resync target must be clean before explicit normalization."); + Assert.That( + EditorUtility.IsDirty(opaque), + Is.False, + "The Opaque resync target must be clean before explicit normalization." + ); + Assert.That( + EditorUtility.IsDirty(transparent), + Is.False, + "The Transparent resync target must be clean before explicit normalization." + ); InvokeApply(apply, opaque); - Assert.That(EditorUtility.IsDirty(opaque), Is.True, "Explicit normalization must move the clean Opaque resync target to dirty."); + Assert.That( + EditorUtility.IsDirty(opaque), + Is.True, + "Explicit normalization must move the clean Opaque resync target to dirty." + ); EditorUtility.ClearDirty(transparent); - Assert.That(EditorUtility.IsDirty(transparent), Is.False, "The Transparent resync target must be clean immediately before its own explicit normalization."); + Assert.That( + EditorUtility.IsDirty(transparent), + Is.False, + "The Transparent resync target must be clean immediately before its own explicit normalization." + ); InvokeApply(apply, transparent); - Assert.That(EditorUtility.IsDirty(transparent), Is.True, "Explicit normalization must move the clean Transparent resync target to dirty."); + Assert.That( + EditorUtility.IsDirty(transparent), + Is.True, + "Explicit normalization must move the clean Transparent resync target to dirty." + ); EditorUtility.ClearDirty(opaque); EditorUtility.ClearDirty(transparent); - Assert.That(EditorUtility.IsDirty(opaque), Is.False, "The Opaque mixed-selection baseline must be clean."); - Assert.That(EditorUtility.IsDirty(transparent), Is.False, "The Transparent mixed-selection baseline must be clean."); + Assert.That( + EditorUtility.IsDirty(opaque), + Is.False, + "The Opaque mixed-selection baseline must be clean." + ); + Assert.That( + EditorUtility.IsDirty(transparent), + Is.False, + "The Transparent mixed-selection baseline must be clean." + ); } /// Requires the Cutoff drawer to register and report read-only visibility from supported Cutout selections only. @@ -114,7 +181,11 @@ private static void NormalizeAndClearMixedSelectionTargets(MethodInfo apply, Mat public void CutoffDrawerIsRegisteredAndVisibilityModelIsReadOnly() { Type attributeActionsType = FindLoadedType("jp.lilxyzw.shadercore.AttributeActions"); - Assert.That(attributeActionsType, Is.Not.Null, "Shader-Core AttributeActions was not loaded."); + Assert.That( + attributeActionsType, + Is.Not.Null, + "Shader-Core AttributeActions was not loaded." + ); MethodInfo containsKey = attributeActionsType.GetMethod( "ContainsKey", BindingFlags.Public | BindingFlags.Static, @@ -126,7 +197,11 @@ public void CutoffDrawerIsRegisteredAndVisibilityModelIsReadOnly() Assert.That((bool)containsKey.Invoke(null, new object[] { "PureBaseCutoff" }), Is.True); Type cutoffElementType = FindLoadedType("PureBase.Editor.PureBaseCutoffElement"); - Assert.That(cutoffElementType, Is.Not.Null, "The dedicated Cutoff Inspector drawer must be loaded."); + Assert.That( + cutoffElementType, + Is.Not.Null, + "The dedicated Cutoff Inspector drawer must be loaded." + ); MethodInfo getSelectionDisplayState = cutoffElementType.GetMethod( "GetSelectionDisplayState", BindingFlags.Static | BindingFlags.NonPublic, @@ -134,9 +209,20 @@ public void CutoffDrawerIsRegisteredAndVisibilityModelIsReadOnly() new[] { typeof(UnityEngine.Object[]) }, null ); - Assert.That(getSelectionDisplayState, Is.Not.Null, "The Cutoff drawer must expose its read-only selection display model."); - PropertyInfo isVisible = getSelectionDisplayState.ReturnType.GetProperty("IsVisible", BindingFlags.Public | BindingFlags.Instance); - Assert.That(isVisible, Is.Not.Null, "The Cutoff selection display model must expose visibility."); + Assert.That( + getSelectionDisplayState, + Is.Not.Null, + "The Cutoff drawer must expose its read-only selection display model." + ); + PropertyInfo isVisible = getSelectionDisplayState.ReturnType.GetProperty( + "IsVisible", + BindingFlags.Public | BindingFlags.Instance + ); + Assert.That( + isVisible, + Is.Not.Null, + "The Cutoff selection display model must expose visibility." + ); var opaque = CreateMaterial(RequireProductShader("PureBase/Unlit")); var transparent = CreateMaterial(RequireProductShader("PureBase/Toon")); @@ -151,14 +237,37 @@ public void CutoffDrawerIsRegisteredAndVisibilityModelIsReadOnly() MaterialState unsupportedBaseline = MaterialState.Capture(unsupported); Func getVisibility = targets => - (bool)isVisible.GetValue(getSelectionDisplayState.Invoke(null, new object[] { targets })); - Assert.That(getVisibility(new UnityEngine.Object[] { opaque, transparent }), Is.False, "All Opaque and Transparent supported targets must hide Cutoff."); - Assert.That(getVisibility(new UnityEngine.Object[] { opaque, transparent, unsupported }), Is.False, "Unsupported targets must not make Cutoff visible."); - Assert.That(getVisibility(new UnityEngine.Object[] { opaque, transparent, cutout, unsupported }), Is.True, "Any supported Cutout target must make Cutoff visible."); + (bool) + isVisible.GetValue( + getSelectionDisplayState.Invoke(null, new object[] { targets }) + ); + Assert.That( + getVisibility(new UnityEngine.Object[] { opaque, transparent }), + Is.False, + "All Opaque and Transparent supported targets must hide Cutoff." + ); + Assert.That( + getVisibility(new UnityEngine.Object[] { opaque, transparent, unsupported }), + Is.False, + "Unsupported targets must not make Cutoff visible." + ); + Assert.That( + getVisibility( + new UnityEngine.Object[] { opaque, transparent, cutout, unsupported } + ), + Is.True, + "Any supported Cutout target must make Cutoff visible." + ); opaqueBaseline.AssertEqual(opaque, "Opaque target after Cutoff display-state read"); - transparentBaseline.AssertEqual(transparent, "Transparent target after Cutoff display-state read"); + transparentBaseline.AssertEqual( + transparent, + "Transparent target after Cutoff display-state read" + ); cutoutBaseline.AssertEqual(cutout, "Cutout target after Cutoff display-state read"); - unsupportedBaseline.AssertEqual(unsupported, "Unsupported target after Cutoff display-state read"); + unsupportedBaseline.AssertEqual( + unsupported, + "Unsupported target after Cutoff display-state read" + ); } /// Requires the drawer's one-action multi-target boundary to validate, normalize, undo, redo, and refresh without incidental mutation. @@ -181,7 +290,15 @@ public void InspectorMultiTargetActionIsAtomicAndUndoRedoRefreshesAreReadOnly() MaterialState firstBefore = MaterialState.Capture(first); MaterialState secondBefore = MaterialState.Capture(second); MaterialState unsupportedBefore = MaterialState.Capture(unsupported); - AssertRejectedSelectionPreservesEveryTarget(applySelection, first, second, unsupported, firstBefore, secondBefore, unsupportedBefore); + AssertRejectedSelectionPreservesEveryTarget( + applySelection, + first, + second, + unsupported, + firstBefore, + secondBefore, + unsupportedBefore + ); InvokeDrawerSelectionApply(applySelection, new[] { first, second }, 2); int editUndoGroup = Undo.GetCurrentGroup(); @@ -192,7 +309,13 @@ public void InspectorMultiTargetActionIsAtomicAndUndoRedoRefreshesAreReadOnly() ); AssertModeState(first, Modes[2]); AssertModeState(second, Modes[2]); - AssertUndoRedoRefreshesAreReadOnly(refreshSelection, first, second, firstBefore, secondBefore); + AssertUndoRedoRefreshesAreReadOnly( + refreshSelection, + first, + second, + firstBefore, + secondBefore + ); } finally { @@ -201,16 +324,32 @@ public void InspectorMultiTargetActionIsAtomicAndUndoRedoRefreshesAreReadOnly() } /// Asserts that a rejected mixed selection leaves all targets and the Undo stack unchanged. - private static void AssertRejectedSelectionPreservesEveryTarget(MethodInfo applySelection, Material first, Material second, Material unsupported, MaterialState firstBefore, MaterialState secondBefore, MaterialState unsupportedBefore) + private static void AssertRejectedSelectionPreservesEveryTarget( + MethodInfo applySelection, + Material first, + Material second, + Material unsupported, + MaterialState firstBefore, + MaterialState secondBefore, + MaterialState unsupportedBefore + ) { int undoBeforeRejectedSelection = Undo.GetCurrentGroup(); Assert.Throws( - () => InvokeDrawerSelectionApply(applySelection, new[] { first, second, unsupported }, 2), + () => + InvokeDrawerSelectionApply( + applySelection, + new[] { first, second, unsupported }, + 2 + ), "The drawer must validate every selected material before mutating any valid target." ); firstBefore.AssertEqual(first, "valid target after rejected mixed selection"); secondBefore.AssertEqual(second, "second valid target after rejected mixed selection"); - unsupportedBefore.AssertEqual(unsupported, "unsupported target after rejected mixed selection"); + unsupportedBefore.AssertEqual( + unsupported, + "unsupported target after rejected mixed selection" + ); Assert.That( Undo.GetCurrentGroup(), Is.EqualTo(undoBeforeRejectedSelection), @@ -219,7 +358,13 @@ private static void AssertRejectedSelectionPreservesEveryTarget(MethodInfo apply } /// Asserts that Undo, Redo, and their subsequent drawer refreshes preserve established material state. - private static void AssertUndoRedoRefreshesAreReadOnly(MethodInfo refreshSelection, Material first, Material second, MaterialState firstBefore, MaterialState secondBefore) + private static void AssertUndoRedoRefreshesAreReadOnly( + MethodInfo refreshSelection, + Material first, + Material second, + MaterialState firstBefore, + MaterialState secondBefore + ) { Undo.PerformUndo(); firstBefore.AssertEqual(first, "first target after Undo"); @@ -246,7 +391,11 @@ public void ExplicitNormalizationPersistsThroughMaterialAndPrefabSaveReloadAndCl var retainedPaths = new List(); try { - Assert.That(AssetDatabase.IsValidFolder(TemporaryAssetRoot), Is.False, "Temporary asset root already exists."); + Assert.That( + AssetDatabase.IsValidFolder(TemporaryAssetRoot), + Is.False, + "Temporary asset root already exists." + ); AssetDatabase.CreateFolder("Assets", "PureBaseRenderingModeTests"); Material material = CreateAndPersistTransparentMaterial(materialPath); SaveMaterialAsPrefab(material, prefabPath); @@ -272,7 +421,11 @@ public void ExplicitNormalizationPersistsThroughMaterialAndPrefabSaveReloadAndCl retainedPaths.Add(materialPath); if (AssetDatabase.LoadAssetAtPath(prefabPath) != null) retainedPaths.Add(prefabPath); - Assert.That(retainedPaths, Is.Empty, $"Rendering-mode persistence test retained temporary assets: {string.Join(", ", retainedPaths)}."); + Assert.That( + retainedPaths, + Is.Empty, + $"Rendering-mode persistence test retained temporary assets: {string.Join(", ", retainedPaths)}." + ); } } @@ -283,7 +436,11 @@ private Material CreateAndPersistTransparentMaterial(string materialPath) AssetDatabase.CreateAsset(material, materialPath); material.SetInteger("_RenderingMode", 2); InvokeApply(RequireApplyMethod(), material); - Assert.That(EditorUtility.IsDirty(material), Is.True, "Explicit normalization must dirty the temporary material before the path-scoped save."); + Assert.That( + EditorUtility.IsDirty(material), + Is.True, + "Explicit normalization must dirty the temporary material before the path-scoped save." + ); SaveOnlyOwnedAssetAndReimport(material, materialPath); material = AssetDatabase.LoadAssetAtPath(materialPath); Assert.That(material, Is.Not.Null); @@ -310,11 +467,29 @@ private static void SaveMaterialAsPrefab(Material material, string prefabPath) private static MethodInfo RequireApplyMethod() { Type type = FindLoadedType("PureBase.Editor.PureBaseMaterialRenderingMode"); - Assert.That(type, Is.Not.Null, "PureBaseMaterialRenderingMode must be loaded from PureBase.Editor."); + Assert.That( + type, + Is.Not.Null, + "PureBaseMaterialRenderingMode must be loaded from PureBase.Editor." + ); Assert.That(type.IsPublic, Is.True, "PureBaseMaterialRenderingMode must be public."); - MethodInfo method = type.GetMethod("Apply", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(Material) }, null); - Assert.That(method, Is.Not.Null, "PureBaseMaterialRenderingMode must expose public static Apply(Material)."); - Assert.That(method.ReturnType, Is.EqualTo(typeof(void)), "PureBaseMaterialRenderingMode.Apply(Material) must return void."); + MethodInfo method = type.GetMethod( + "Apply", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(Material) }, + null + ); + Assert.That( + method, + Is.Not.Null, + "PureBaseMaterialRenderingMode must expose public static Apply(Material)." + ); + Assert.That( + method.ReturnType, + Is.EqualTo(typeof(void)), + "PureBaseMaterialRenderingMode.Apply(Material) must return void." + ); return method; } @@ -323,7 +498,11 @@ private static MethodInfo RequireApplyMethod() private static MethodInfo RequireApplyAllMethod() { Type type = FindLoadedType("PureBase.Editor.PureBaseMaterialRenderingMode"); - Assert.That(type, Is.Not.Null, "PureBaseMaterialRenderingMode must be loaded from PureBase.Editor."); + Assert.That( + type, + Is.Not.Null, + "PureBaseMaterialRenderingMode must be loaded from PureBase.Editor." + ); MethodInfo method = type.GetMethod( "ApplyAll", BindingFlags.NonPublic | BindingFlags.Static, @@ -331,7 +510,11 @@ private static MethodInfo RequireApplyAllMethod() new[] { typeof(IReadOnlyList) }, null ); - Assert.That(method, Is.Not.Null, "PureBaseMaterialRenderingMode must retain the validated batch boundary."); + Assert.That( + method, + Is.Not.Null, + "PureBaseMaterialRenderingMode must retain the validated batch boundary." + ); return method; } @@ -353,8 +536,15 @@ private static MethodInfo RequireDrawerSelectionRefreshMethod() /// The static GetSelectionDisplayState(Material[]) drawer operation. private static MethodInfo RequireDrawerSelectionDisplayStateMethod() { - MethodInfo method = RequireDrawerMethod("GetSelectionDisplayState", new[] { typeof(Material[]) }); - Assert.That(method.ReturnType, Is.Not.EqualTo(typeof(void)), "The drawer selection display-state boundary must return a readable UI model."); + MethodInfo method = RequireDrawerMethod( + "GetSelectionDisplayState", + new[] { typeof(Material[]) } + ); + Assert.That( + method.ReturnType, + Is.Not.EqualTo(typeof(void)), + "The drawer selection display-state boundary must return a readable UI model." + ); return method; } @@ -365,7 +555,11 @@ private static MethodInfo RequireDrawerSelectionDisplayStateMethod() private static MethodInfo RequireDrawerMethod(string methodName, Type[] parameterTypes) { Type type = FindLoadedType("PureBase.Editor.PureBaseRenderingModeElement"); - Assert.That(type, Is.Not.Null, "The dedicated rendering-mode Inspector drawer must be loaded."); + Assert.That( + type, + Is.Not.Null, + "The dedicated rendering-mode Inspector drawer must be loaded." + ); MethodInfo method = type.GetMethod( methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, @@ -376,7 +570,9 @@ private static MethodInfo RequireDrawerMethod(string methodName, Type[] paramete Assert.That( method, Is.Not.Null, - "PureBaseRenderingModeElement must expose the testable " + methodName + " selection boundary." + "PureBaseRenderingModeElement must expose the testable " + + methodName + + " selection boundary." ); return method; } @@ -402,20 +598,45 @@ private static void InvokeApplyAll(MethodInfo method, IReadOnlyList ma /// The rejected material identified by the exception. /// The rejected rendering-mode value. /// The operation context used in assertion diagnostics. - private static void AssertInvalidRenderingModeException(ArgumentOutOfRangeException exception, Material material, int value, string context) + private static void AssertInvalidRenderingModeException( + ArgumentOutOfRangeException exception, + Material material, + int value, + string context + ) { - Assert.That(exception, Is.Not.Null, context + " must throw an ArgumentOutOfRangeException."); - Assert.That(exception.ParamName, Is.EqualTo("_RenderingMode"), context + " exception parameter."); + Assert.That( + exception, + Is.Not.Null, + context + " must throw an ArgumentOutOfRangeException." + ); + Assert.That( + exception.ParamName, + Is.EqualTo("_RenderingMode"), + context + " exception parameter." + ); Assert.That(exception.ActualValue, Is.EqualTo(value), context + " exception value."); - StringAssert.Contains(material.name, exception.Message, context + " exception material identity."); - StringAssert.Contains("0, 1, or 2", exception.Message, context + " exception supported values."); + StringAssert.Contains( + material.name, + exception.Message, + context + " exception material identity." + ); + StringAssert.Contains( + "0, 1, or 2", + exception.Message, + context + " exception supported values." + ); } /// Invokes the drawer's one-action multi-target operation while preserving its original exception type. /// The reflected drawer operation. /// The selected material targets. /// The requested serialized rendering-mode value. - private static void InvokeDrawerSelectionApply(MethodInfo method, Material[] materials, int mode) + private static void InvokeDrawerSelectionApply( + MethodInfo method, + Material[] materials, + int mode + ) { InvokeReflectedMethod(method, new object[] { materials, mode }); } @@ -432,7 +653,10 @@ private static void InvokeDrawerSelectionRefresh(MethodInfo method, Material[] m /// The reflected drawer display-state operation. /// The selected material targets. /// The read-only drawer display model. - private static object InvokeDrawerSelectionDisplayState(MethodInfo method, Material[] materials) + private static object InvokeDrawerSelectionDisplayState( + MethodInfo method, + Material[] materials + ) { return InvokeReflectedMethod(method, new object[] { materials }); } @@ -457,14 +681,34 @@ private static object InvokeReflectedMethod(MethodInfo method, object[] argument /// The reflection-returned drawer selection model. /// Whether the selection must be displayed as mixed. /// The complete ordered mode labels presented by the popup. - private static void AssertSelectionDisplayState(object displayState, bool expectedMixed, string[] expectedChoices) + private static void AssertSelectionDisplayState( + object displayState, + bool expectedMixed, + string[] expectedChoices + ) { - Assert.That(displayState, Is.Not.Null, "The drawer must return a real selection display model."); - Assert.That(ReadDisplayStateMember(displayState, "HasMixedValue"), Is.EqualTo(expectedMixed), "The drawer display model mixed indicator."); + Assert.That( + displayState, + Is.Not.Null, + "The drawer must return a real selection display model." + ); + Assert.That( + ReadDisplayStateMember(displayState, "HasMixedValue"), + Is.EqualTo(expectedMixed), + "The drawer display model mixed indicator." + ); object choices = ReadDisplayStateMember(displayState, "Choices"); var labels = choices as IEnumerable; - Assert.That(labels, Is.Not.Null, "The drawer display model Choices member must be a readable string sequence."); - CollectionAssert.AreEqual(expectedChoices, labels, "The drawer popup must expose exactly the three supported rendering-mode choices."); + Assert.That( + labels, + Is.Not.Null, + "The drawer display model Choices member must be a readable string sequence." + ); + CollectionAssert.AreEqual( + expectedChoices, + labels, + "The drawer popup must expose exactly the three supported rendering-mode choices." + ); } /// Reads one field or property from a drawer-owned selection display model without depending on its accessibility. @@ -474,12 +718,19 @@ private static void AssertSelectionDisplayState(object displayState, bool expect private static object ReadDisplayStateMember(object displayState, string memberName) { Type type = displayState.GetType(); - const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + const BindingFlags Flags = + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; PropertyInfo property = type.GetProperty(memberName, Flags); if (property != null) return property.GetValue(displayState, null); FieldInfo field = type.GetField(memberName, Flags); - Assert.That(field, Is.Not.Null, "The drawer display model must expose " + memberName + " as a readable field or property."); + Assert.That( + field, + Is.Not.Null, + "The drawer display model must expose " + + memberName + + " as a readable field or property." + ); return field.GetValue(displayState); } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.MaterialState.cs b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.MaterialState.cs index f1de32f..f74772e 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.MaterialState.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.MaterialState.cs @@ -29,19 +29,21 @@ using UnityEngine; using UnityEngine.Rendering; - namespace PureBase.Tests.Daily { public sealed partial class PureBaseRenderingModeContractTests { - /// Captures every material field whose mutation must be rejected by invalid normalizer inputs. - private sealed class MaterialState - { + /// Captures every material field whose mutation must be rejected by invalid normalizer inputs. + private sealed class MaterialState + { /// Captures an immutable snapshot from one material. /// The material to snapshot. /// The optional set that records captured shader property types. /// The captured state. - public static MaterialState Capture(Material material, ISet observedPropertyTypes = null) + public static MaterialState Capture( + Material material, + ISet observedPropertyTypes = null + ) { MaterialState state = CreateBaseState(material); CaptureShaderPropertyState(state, material, observedPropertyTypes); @@ -54,7 +56,10 @@ private static MaterialState CreateBaseState(Material material) { return new MaterialState { - hasRenderTypeOverride = TryGetSerializedRenderTypeOverride(material, out string renderTypeOverride), + hasRenderTypeOverride = TryGetSerializedRenderTypeOverride( + material, + out string renderTypeOverride + ), renderTypeOverride = renderTypeOverride, resolvedRenderType = material.GetTag("RenderType", true), rawQueue = GetRawRenderQueue(material), @@ -67,13 +72,20 @@ private static MaterialState CreateBaseState(Material material) } /// Captures every visible shader property in declaration order. - private static void CaptureShaderPropertyState(MaterialState state, Material material, ISet observedPropertyTypes) + private static void CaptureShaderPropertyState( + MaterialState state, + Material material, + ISet observedPropertyTypes + ) { Shader shader = material.shader; for (int index = 0; index < ShaderUtil.GetPropertyCount(shader); index++) { string propertyName = shader.GetPropertyName(index); - ShaderUtil.ShaderPropertyType propertyType = ShaderUtil.GetPropertyType(shader, index); + ShaderUtil.ShaderPropertyType propertyType = ShaderUtil.GetPropertyType( + shader, + index + ); ObserveAtomicityPropertyType(observedPropertyTypes, propertyType); switch (propertyType) { @@ -91,10 +103,15 @@ private static void CaptureShaderPropertyState(MaterialState state, Material mat state.vectors[propertyName] = material.GetVector(propertyName); break; case ShaderUtil.ShaderPropertyType.TexEnv: - state.textures[propertyName] = TexturePropertyState.Capture(material, propertyName); + state.textures[propertyName] = TexturePropertyState.Capture( + material, + propertyName + ); break; default: - Assert.Fail($"Unsupported shader property type '{ShaderUtil.GetPropertyType(shader, index)}' for '{propertyName}'."); + Assert.Fail( + $"Unsupported shader property type '{ShaderUtil.GetPropertyType(shader, index)}' for '{propertyName}'." + ); break; } } @@ -117,35 +134,98 @@ private static void CaptureHiddenStateAndPasses(MaterialState state, Material ma /// The material to compare. /// The diagnostic operation context. /// The optional set that records asserted shader property types. - public void AssertEqual(Material material, string context, ISet observedPropertyTypes = null) + public void AssertEqual( + Material material, + string context, + ISet observedPropertyTypes = null + ) { ObserveAtomicityPropertyTypes(material, observedPropertyTypes); - bool actualHasRenderTypeOverride = TryGetSerializedRenderTypeOverride(material, out string actualRenderTypeOverride); - Assert.That(actualHasRenderTypeOverride, Is.EqualTo(hasRenderTypeOverride), context + " RenderType override presence."); + bool actualHasRenderTypeOverride = TryGetSerializedRenderTypeOverride( + material, + out string actualRenderTypeOverride + ); + Assert.That( + actualHasRenderTypeOverride, + Is.EqualTo(hasRenderTypeOverride), + context + " RenderType override presence." + ); if (actualHasRenderTypeOverride) - Assert.That(actualRenderTypeOverride, Is.EqualTo(renderTypeOverride), context + " RenderType override."); - Assert.That(material.GetTag("RenderType", true), Is.EqualTo(resolvedRenderType), context + " resolved RenderType tag."); - Assert.That(GetRawRenderQueue(material), Is.EqualTo(rawQueue), context + " raw render queue."); - Assert.That(material.renderQueue, Is.EqualTo(resolvedQueue), context + " resolved render queue."); - Assert.That(material.GetShaderPassEnabled("ShadowCaster"), Is.EqualTo(shadowCasterEnabled), context + " ShadowCaster state."); - Assert.That(material.GetShaderPassEnabled("Meta"), Is.EqualTo(metaEnabled), context + " Meta state."); - Assert.That(EditorUtility.IsDirty(material), Is.EqualTo(dirty), context + " dirty state."); - CollectionAssert.AreEquivalent(keywords, material.shaderKeywords, context + " keyword set."); + Assert.That( + actualRenderTypeOverride, + Is.EqualTo(renderTypeOverride), + context + " RenderType override." + ); + Assert.That( + material.GetTag("RenderType", true), + Is.EqualTo(resolvedRenderType), + context + " resolved RenderType tag." + ); + Assert.That( + GetRawRenderQueue(material), + Is.EqualTo(rawQueue), + context + " raw render queue." + ); + Assert.That( + material.renderQueue, + Is.EqualTo(resolvedQueue), + context + " resolved render queue." + ); + Assert.That( + material.GetShaderPassEnabled("ShadowCaster"), + Is.EqualTo(shadowCasterEnabled), + context + " ShadowCaster state." + ); + Assert.That( + material.GetShaderPassEnabled("Meta"), + Is.EqualTo(metaEnabled), + context + " Meta state." + ); + Assert.That( + EditorUtility.IsDirty(material), + Is.EqualTo(dirty), + context + " dirty state." + ); + CollectionAssert.AreEquivalent( + keywords, + material.shaderKeywords, + context + " keyword set." + ); foreach (KeyValuePair pair in floats) - Assert.That(material.GetFloat(pair.Key), Is.EqualTo(pair.Value), context + " property " + pair.Key + "."); + Assert.That( + material.GetFloat(pair.Key), + Is.EqualTo(pair.Value), + context + " property " + pair.Key + "." + ); foreach (KeyValuePair pair in integers) { int actual = material.GetInteger(pair.Key); - Assert.That(actual, Is.EqualTo(pair.Value), context + " int property " + pair.Key + "."); + Assert.That( + actual, + Is.EqualTo(pair.Value), + context + " int property " + pair.Key + "." + ); } foreach (KeyValuePair pair in colors) - Assert.That(material.GetColor(pair.Key), Is.EqualTo(pair.Value), context + " color property " + pair.Key + "."); + Assert.That( + material.GetColor(pair.Key), + Is.EqualTo(pair.Value), + context + " color property " + pair.Key + "." + ); foreach (KeyValuePair pair in vectors) - Assert.That(material.GetVector(pair.Key), Is.EqualTo(pair.Value), context + " vector property " + pair.Key + "."); + Assert.That( + material.GetVector(pair.Key), + Is.EqualTo(pair.Value), + context + " vector property " + pair.Key + "." + ); foreach (KeyValuePair pair in textures) pair.Value.AssertEqual(material, pair.Key, context); foreach (KeyValuePair pair in passes) - Assert.That(material.GetShaderPassEnabled(pair.Key), Is.EqualTo(pair.Value), context + " pass " + pair.Key + "."); + Assert.That( + material.GetShaderPassEnabled(pair.Key), + Is.EqualTo(pair.Value), + context + " pass " + pair.Key + "." + ); } /// Asserts that this snapshot includes one visible or hidden shader property. @@ -154,10 +234,10 @@ public void AssertCapturesShaderProperty(string propertyName) { Assert.That( floats.ContainsKey(propertyName) - || integers.ContainsKey(propertyName) - || colors.ContainsKey(propertyName) - || vectors.ContainsKey(propertyName) - || textures.ContainsKey(propertyName), + || integers.ContainsKey(propertyName) + || colors.ContainsKey(propertyName) + || vectors.ContainsKey(propertyName) + || textures.ContainsKey(propertyName), Is.True, "The material snapshot must include shader property '" + propertyName + "'." ); @@ -191,22 +271,35 @@ public void AssertCapturesShaderProperty(string propertyName) public string[] keywords; /// Stores captured float and range property values. - public readonly Dictionary floats = new Dictionary(StringComparer.Ordinal); + public readonly Dictionary floats = new Dictionary( + StringComparer.Ordinal + ); /// Stores captured integer property values. - public readonly Dictionary integers = new Dictionary(StringComparer.Ordinal); + public readonly Dictionary integers = new Dictionary( + StringComparer.Ordinal + ); /// Stores captured color property values. - public readonly Dictionary colors = new Dictionary(StringComparer.Ordinal); + public readonly Dictionary colors = new Dictionary( + StringComparer.Ordinal + ); /// Stores captured vector property values. - public readonly Dictionary vectors = new Dictionary(StringComparer.Ordinal); + public readonly Dictionary vectors = new Dictionary( + StringComparer.Ordinal + ); /// Stores captured texture property values and their UV transforms. - public readonly Dictionary textures = new Dictionary(StringComparer.Ordinal); + public readonly Dictionary textures = new Dictionary< + string, + TexturePropertyState + >(StringComparer.Ordinal); /// Stores captured enabled-state values for every rendering-mode-relevant pass. - public readonly Dictionary passes = new Dictionary(StringComparer.Ordinal); + public readonly Dictionary passes = new Dictionary( + StringComparer.Ordinal + ); } /// Returns valid materials during validation and snapshots, then makes one later target invalid during application. @@ -216,7 +309,11 @@ private sealed class LateInvalidatingMaterialList : IReadOnlyList /// The ordered batch materials. /// The later material index to invalidate. /// The unsupported mode assigned immediately before its application. - public LateInvalidatingMaterialList(Material[] materials, int invalidMaterialIndex, int invalidRenderingMode) + public LateInvalidatingMaterialList( + Material[] materials, + int invalidMaterialIndex, + int invalidRenderingMode + ) { this.materials = materials; this.invalidMaterialIndex = invalidMaterialIndex; @@ -249,7 +346,8 @@ public Material this[int index] { if (index == invalidMaterialIndex && ++invalidMaterialReadCount == 3) { - ObservedPriorMutations = materials[0].GetTag("RenderType", false) == "Opaque" + ObservedPriorMutations = + materials[0].GetTag("RenderType", false) == "Opaque" && materials[1].GetTag("RenderType", false) == "Transparent"; materials[index].SetInteger("_RenderingMode", invalidRenderingMode); } @@ -297,9 +395,21 @@ public static TexturePropertyState Capture(Material material, string propertyNam /// The diagnostic operation context. public void AssertEqual(Material material, string propertyName, string context) { - Assert.That(material.GetTexture(propertyName), Is.EqualTo(texture), context + " texture property " + propertyName + "."); - Assert.That(material.GetTextureScale(propertyName), Is.EqualTo(scale), context + " texture scale " + propertyName + "."); - Assert.That(material.GetTextureOffset(propertyName), Is.EqualTo(offset), context + " texture offset " + propertyName + "."); + Assert.That( + material.GetTexture(propertyName), + Is.EqualTo(texture), + context + " texture property " + propertyName + "." + ); + Assert.That( + material.GetTextureScale(propertyName), + Is.EqualTo(scale), + context + " texture scale " + propertyName + "." + ); + Assert.That( + material.GetTextureOffset(propertyName), + Is.EqualTo(offset), + context + " texture offset " + propertyName + "." + ); } /// Stores the captured texture object. diff --git a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.ProductContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.ProductContracts.cs index dd3597e..3b9f898 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.ProductContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.ProductContracts.cs @@ -29,7 +29,6 @@ using UnityEngine; using UnityEngine.Rendering; - namespace PureBase.Tests.Daily { public sealed partial class PureBaseRenderingModeContractTests @@ -66,8 +65,20 @@ public sealed partial class PureBaseRenderingModeContractTests "Packages/jp.penguin.purebase/Shaders/PureBaseUnlit_properties.hlsl", new[] { - "_BaseTexture", "_BaseColor", "_SharedMask", "_SharedGradients", "_RenderingMode", "_Cutoff", "_Cull", - "_StencilRef", "_StencilReadMask", "_StencilWriteMask", "_StencilComp", "_StencilPass", "_StencilFail", "_StencilZFail", + "_BaseTexture", + "_BaseColor", + "_SharedMask", + "_SharedGradients", + "_RenderingMode", + "_Cutoff", + "_Cull", + "_StencilRef", + "_StencilReadMask", + "_StencilWriteMask", + "_StencilComp", + "_StencilPass", + "_StencilFail", + "_StencilZFail", } ), new ProductContract( @@ -75,9 +86,22 @@ public sealed partial class PureBaseRenderingModeContractTests "Packages/jp.penguin.purebase/Shaders/PureBaseToon_properties.hlsl", new[] { - "_BaseTexture", "_BaseColor", "_SharedMask", "_SharedGradients", "_RenderingMode", "_Cutoff", "_Cull", - "_StencilRef", "_StencilReadMask", "_StencilWriteMask", "_StencilComp", "_StencilPass", "_StencilFail", "_StencilZFail", - "_NormalMap", "_NormalScale", + "_BaseTexture", + "_BaseColor", + "_SharedMask", + "_SharedGradients", + "_RenderingMode", + "_Cutoff", + "_Cull", + "_StencilRef", + "_StencilReadMask", + "_StencilWriteMask", + "_StencilComp", + "_StencilPass", + "_StencilFail", + "_StencilZFail", + "_NormalMap", + "_NormalScale", } ), new ProductContract( @@ -85,9 +109,24 @@ public sealed partial class PureBaseRenderingModeContractTests "Packages/jp.penguin.purebase/Shaders/PureBasePBR_properties.hlsl", new[] { - "_BaseTexture", "_BaseColor", "_SharedMask", "_SharedGradients", "_RenderingMode", "_Cutoff", "_Cull", - "_StencilRef", "_StencilReadMask", "_StencilWriteMask", "_StencilComp", "_StencilPass", "_StencilFail", "_StencilZFail", - "_NormalMap", "_NormalScale", "_Metallic", "_Roughness", + "_BaseTexture", + "_BaseColor", + "_SharedMask", + "_SharedGradients", + "_RenderingMode", + "_Cutoff", + "_Cull", + "_StencilRef", + "_StencilReadMask", + "_StencilWriteMask", + "_StencilComp", + "_StencilPass", + "_StencilFail", + "_StencilZFail", + "_NormalMap", + "_NormalScale", + "_Metallic", + "_Roughness", } ), new ProductContract( @@ -95,9 +134,24 @@ public sealed partial class PureBaseRenderingModeContractTests "Packages/jp.penguin.purebase/Shaders/PureBaseHybrid_properties.hlsl", new[] { - "_BaseTexture", "_BaseColor", "_SharedMask", "_SharedGradients", "_RenderingMode", "_Cutoff", "_Cull", - "_StencilRef", "_StencilReadMask", "_StencilWriteMask", "_StencilComp", "_StencilPass", "_StencilFail", "_StencilZFail", - "_NormalMap", "_NormalScale", "_Metallic", "_Roughness", + "_BaseTexture", + "_BaseColor", + "_SharedMask", + "_SharedGradients", + "_RenderingMode", + "_Cutoff", + "_Cull", + "_StencilRef", + "_StencilReadMask", + "_StencilWriteMask", + "_StencilComp", + "_StencilPass", + "_StencilFail", + "_StencilZFail", + "_NormalMap", + "_NormalScale", + "_Metallic", + "_Roughness", } ), }; @@ -145,7 +199,13 @@ public sealed partial class PureBaseRenderingModeContractTests new ModeContract( 0, "Opaque", - new BlendState((int)BlendMode.One, (int)BlendMode.Zero, 1, (int)BlendMode.One, (int)BlendMode.One), + new BlendState( + (int)BlendMode.One, + (int)BlendMode.Zero, + 1, + (int)BlendMode.One, + (int)BlendMode.One + ), new RenderTypeState("Opaque", true, "Opaque"), new QueueState(2000, 2000), new[] { "PUREBASE_RENDERING_OPAQUE" }, @@ -154,7 +214,13 @@ public sealed partial class PureBaseRenderingModeContractTests new ModeContract( 1, "Cutout", - new BlendState((int)BlendMode.One, (int)BlendMode.Zero, 1, (int)BlendMode.One, (int)BlendMode.One), + new BlendState( + (int)BlendMode.One, + (int)BlendMode.Zero, + 1, + (int)BlendMode.One, + (int)BlendMode.One + ), new RenderTypeState(string.Empty, false, "TransparentCutout"), new QueueState(-1, (int)RenderQueue.AlphaTest), Array.Empty(), @@ -163,7 +229,13 @@ public sealed partial class PureBaseRenderingModeContractTests new ModeContract( 2, "Transparent", - new BlendState((int)BlendMode.SrcAlpha, (int)BlendMode.OneMinusSrcAlpha, 0, (int)BlendMode.SrcAlpha, (int)BlendMode.One), + new BlendState( + (int)BlendMode.SrcAlpha, + (int)BlendMode.OneMinusSrcAlpha, + 0, + (int)BlendMode.SrcAlpha, + (int)BlendMode.One + ), new RenderTypeState("Transparent", true, "Transparent"), new QueueState(3000, 3000), new[] { "PUREBASE_RENDERING_TRANSPARENT" }, @@ -174,13 +246,62 @@ public sealed partial class PureBaseRenderingModeContractTests /// Lists the common visible Stencil ABI, imported defaults, required drawer attributes, and test overrides. private static readonly StencilPropertyContract[] StencilProperties = { - new StencilPropertyContract("_StencilRef", 0.0f, 37.0f, "SCRangeInt(0,255)", @"SCRangeInt\s*\(\s*0\s*,\s*255\s*\)", @"0(?:\.0+)?"), - new StencilPropertyContract("_StencilReadMask", 255.0f, 203.0f, "SCRangeInt(0,255)", @"SCRangeInt\s*\(\s*0\s*,\s*255\s*\)", @"255(?:\.0+)?"), - new StencilPropertyContract("_StencilWriteMask", 255.0f, 85.0f, "SCRangeInt(0,255)", @"SCRangeInt\s*\(\s*0\s*,\s*255\s*\)", @"255(?:\.0+)?"), - new StencilPropertyContract("_StencilComp", 8.0f, 3.0f, "SCEnum(UnityEngine.Rendering.CompareFunction)", @"SCEnum\s*\(\s*UnityEngine\.Rendering\.CompareFunction\s*\)", @"8(?:\.0+)?"), - new StencilPropertyContract("_StencilPass", 0.0f, 2.0f, "SCEnum(UnityEngine.Rendering.StencilOp)", @"SCEnum\s*\(\s*UnityEngine\.Rendering\.StencilOp\s*\)", @"0(?:\.0+)?"), - new StencilPropertyContract("_StencilFail", 0.0f, 5.0f, "SCEnum(UnityEngine.Rendering.StencilOp)", @"SCEnum\s*\(\s*UnityEngine\.Rendering\.StencilOp\s*\)", @"0(?:\.0+)?"), - new StencilPropertyContract("_StencilZFail", 0.0f, 4.0f, "SCEnum(UnityEngine.Rendering.StencilOp)", @"SCEnum\s*\(\s*UnityEngine\.Rendering\.StencilOp\s*\)", @"0(?:\.0+)?"), + new StencilPropertyContract( + "_StencilRef", + 0.0f, + 37.0f, + "SCRangeInt(0,255)", + @"SCRangeInt\s*\(\s*0\s*,\s*255\s*\)", + @"0(?:\.0+)?" + ), + new StencilPropertyContract( + "_StencilReadMask", + 255.0f, + 203.0f, + "SCRangeInt(0,255)", + @"SCRangeInt\s*\(\s*0\s*,\s*255\s*\)", + @"255(?:\.0+)?" + ), + new StencilPropertyContract( + "_StencilWriteMask", + 255.0f, + 85.0f, + "SCRangeInt(0,255)", + @"SCRangeInt\s*\(\s*0\s*,\s*255\s*\)", + @"255(?:\.0+)?" + ), + new StencilPropertyContract( + "_StencilComp", + 8.0f, + 3.0f, + "SCEnum(UnityEngine.Rendering.CompareFunction)", + @"SCEnum\s*\(\s*UnityEngine\.Rendering\.CompareFunction\s*\)", + @"8(?:\.0+)?" + ), + new StencilPropertyContract( + "_StencilPass", + 0.0f, + 2.0f, + "SCEnum(UnityEngine.Rendering.StencilOp)", + @"SCEnum\s*\(\s*UnityEngine\.Rendering\.StencilOp\s*\)", + @"0(?:\.0+)?" + ), + new StencilPropertyContract( + "_StencilFail", + 0.0f, + 5.0f, + "SCEnum(UnityEngine.Rendering.StencilOp)", + @"SCEnum\s*\(\s*UnityEngine\.Rendering\.StencilOp\s*\)", + @"0(?:\.0+)?" + ), + new StencilPropertyContract( + "_StencilZFail", + 0.0f, + 4.0f, + "SCEnum(UnityEngine.Rendering.StencilOp)", + @"SCEnum\s*\(\s*UnityEngine\.Rendering\.StencilOp\s*\)", + @"0(?:\.0+)?" + ), }; /// Requires the complete shader ABI, static Cutout defaults, pass ABI, and local-keyword declaration. @@ -200,16 +321,54 @@ public void ProductShadersExposeRenderingModeAndCutoutCompatibleStaticDefaults() /// The imported product shader. private static void AssertProductShaderAbi(ProductContract product, Shader shader) { - CollectionAssert.AreEqual(product.visiblePropertyNames, GetVisiblePropertyNames(shader), $"Product shader '{product.shaderName}' changed its public property ABI."); + CollectionAssert.AreEqual( + product.visiblePropertyNames, + GetVisiblePropertyNames(shader), + $"Product shader '{product.shaderName}' changed its public property ABI." + ); int modeIndex = shader.FindPropertyIndex("_RenderingMode"); - Assert.That(modeIndex, Is.GreaterThanOrEqualTo(0), $"Product shader '{product.shaderName}' must expose _RenderingMode."); - Assert.That(shader.GetPropertyType(modeIndex), Is.EqualTo(ShaderPropertyType.Int), $"Product shader '{product.shaderName}' must expose _RenderingMode as an Integer property."); - CollectionAssert.Contains(shader.GetPropertyAttributes(modeIndex), "PureBaseRenderingMode", $"Product shader '{product.shaderName}' must use the Pure-Base rendering-mode drawer."); - Assert.That(Regex.IsMatch(File.ReadAllText(product.propertySourcePath), RenderingModePropertySourcePattern), Is.True, $"Product property source '{product.propertySourcePath}' must declare _RenderingMode as SC_uint with default 1 and the PureBaseRenderingMode drawer."); + Assert.That( + modeIndex, + Is.GreaterThanOrEqualTo(0), + $"Product shader '{product.shaderName}' must expose _RenderingMode." + ); + Assert.That( + shader.GetPropertyType(modeIndex), + Is.EqualTo(ShaderPropertyType.Int), + $"Product shader '{product.shaderName}' must expose _RenderingMode as an Integer property." + ); + CollectionAssert.Contains( + shader.GetPropertyAttributes(modeIndex), + "PureBaseRenderingMode", + $"Product shader '{product.shaderName}' must use the Pure-Base rendering-mode drawer." + ); + Assert.That( + Regex.IsMatch( + File.ReadAllText(product.propertySourcePath), + RenderingModePropertySourcePattern + ), + Is.True, + $"Product property source '{product.propertySourcePath}' must declare _RenderingMode as SC_uint with default 1 and the PureBaseRenderingMode drawer." + ); int cutoffIndex = shader.FindPropertyIndex("_Cutoff"); - Assert.That(cutoffIndex, Is.GreaterThanOrEqualTo(0), $"Product shader '{product.shaderName}' must expose _Cutoff."); - CollectionAssert.Contains(shader.GetPropertyAttributes(cutoffIndex), "PureBaseCutoff", $"Product shader '{product.shaderName}' must use the Pure-Base Cutoff drawer."); - Assert.That(Regex.IsMatch(File.ReadAllText(product.propertySourcePath), CutoffPropertySourcePattern), Is.True, $"Product property source '{product.propertySourcePath}' must declare _Cutoff with the PureBaseCutoff drawer and SCRange(-0.001,1.001)."); + Assert.That( + cutoffIndex, + Is.GreaterThanOrEqualTo(0), + $"Product shader '{product.shaderName}' must expose _Cutoff." + ); + CollectionAssert.Contains( + shader.GetPropertyAttributes(cutoffIndex), + "PureBaseCutoff", + $"Product shader '{product.shaderName}' must use the Pure-Base Cutoff drawer." + ); + Assert.That( + Regex.IsMatch( + File.ReadAllText(product.propertySourcePath), + CutoffPropertySourcePattern + ), + Is.True, + $"Product property source '{product.propertySourcePath}' must declare _Cutoff with the PureBaseCutoff drawer and SCRange(-0.001,1.001)." + ); AssertStencilPropertyAbi(product, shader); } @@ -224,18 +383,51 @@ private static void AssertStencilPropertyAbi(ProductContract product, Shader sha foreach (StencilPropertyContract property in StencilProperties) { int shaderIndex = shader.FindPropertyIndex(property.name); - Assert.That(shaderIndex, Is.GreaterThanOrEqualTo(0), $"Product shader '{product.shaderName}' must expose {property.name}."); - Assert.That(shaderIndex, Is.GreaterThan(previousShaderIndex), $"Product shader '{product.shaderName}' must preserve the common Stencil property order."); - Assert.That(shader.GetPropertyType(shaderIndex), Is.EqualTo(ShaderPropertyType.Float), $"Product shader '{product.shaderName}' property '{property.name}' must be a Float."); - Assert.That(shader.GetPropertyDefaultFloatValue(shaderIndex), Is.EqualTo(property.defaultValue), $"Product shader '{product.shaderName}' property '{property.name}' default value."); - CollectionAssert.AreEqual(new[] { property.attribute }, shader.GetPropertyAttributes(shaderIndex), $"Product shader '{product.shaderName}' property '{property.name}' must expose exactly its permitted drawer attribute."); + Assert.That( + shaderIndex, + Is.GreaterThanOrEqualTo(0), + $"Product shader '{product.shaderName}' must expose {property.name}." + ); + Assert.That( + shaderIndex, + Is.GreaterThan(previousShaderIndex), + $"Product shader '{product.shaderName}' must preserve the common Stencil property order." + ); + Assert.That( + shader.GetPropertyType(shaderIndex), + Is.EqualTo(ShaderPropertyType.Float), + $"Product shader '{product.shaderName}' property '{property.name}' must be a Float." + ); + Assert.That( + shader.GetPropertyDefaultFloatValue(shaderIndex), + Is.EqualTo(property.defaultValue), + $"Product shader '{product.shaderName}' property '{property.name}' default value." + ); + CollectionAssert.AreEqual( + new[] { property.attribute }, + shader.GetPropertyAttributes(shaderIndex), + $"Product shader '{product.shaderName}' property '{property.name}' must expose exactly its permitted drawer attribute." + ); string declarationPattern = - @"SC_float\s*\(\s*" + Regex.Escape(property.name) + @"\s*,\s*" + property.sourceDefaultPattern + - @"\s*,\s*\[\s*" + property.sourceAttributePattern + @"\s*\]\s*,\s*""[^""\r\n]*""\s*,\s*""[^""\r\n]*""\s*\)"; + @"SC_float\s*\(\s*" + + Regex.Escape(property.name) + + @"\s*,\s*" + + property.sourceDefaultPattern + + @"\s*,\s*\[\s*" + + property.sourceAttributePattern + + @"\s*\]\s*,\s*""[^""\r\n]*""\s*,\s*""[^""\r\n]*""\s*\)"; Match declaration = Regex.Match(source, declarationPattern); - Assert.That(declaration.Success, Is.True, $"Product property source '{product.propertySourcePath}' must declare {property.name} with SC_float, its default, and exactly its permitted {property.attribute} drawer attribute."); - Assert.That(declaration.Index, Is.GreaterThan(previousSourceIndex), $"Product property source '{product.propertySourcePath}' must preserve the common Stencil property order."); + Assert.That( + declaration.Success, + Is.True, + $"Product property source '{product.propertySourcePath}' must declare {property.name} with SC_float, its default, and exactly its permitted {property.attribute} drawer attribute." + ); + Assert.That( + declaration.Index, + Is.GreaterThan(previousSourceIndex), + $"Product property source '{product.propertySourcePath}' must preserve the common Stencil property order." + ); previousShaderIndex = shaderIndex; previousSourceIndex = declaration.Index; @@ -256,7 +448,10 @@ private void AssertProductShaderStaticDefaults(ProductContract product, Shader s Assert.That(material.GetShaderPassEnabled("Meta"), Is.True); AssertRenderingKeywords(material, Array.Empty()); CollectionAssert.AreEqual(PassNames, GetPassNames(shader)); - AssertRenderingModeKeywordDeclarations(LoadGeneratedSource(product.shaderName), product.shaderName); + AssertRenderingModeKeywordDeclarations( + LoadGeneratedSource(product.shaderName), + product.shaderName + ); } /// Requires a new unsaved material to behave as Cutout without creating persistence dirtiness. @@ -268,7 +463,11 @@ public void NewMaterialWithoutSavedModeRemainsReadOnlyCutoutUntilExplicitNormali { Assert.That(shader.FindPropertyIndex("_RenderingMode"), Is.GreaterThanOrEqualTo(0)); EditorUtility.ClearDirty(material); - Assert.That(EditorUtility.IsDirty(material), Is.False, "The Inspector-bind test must establish a clean baseline."); + Assert.That( + EditorUtility.IsDirty(material), + Is.False, + "The Inspector-bind test must establish a clean baseline." + ); MaterialState baseline = MaterialState.Capture(material); MaterialEditor.GetMaterialProperties(new UnityEngine.Object[] { material }); baseline.AssertEqual(material, "Inspector bind"); @@ -287,16 +486,26 @@ public void LegacyCutoutFixtureRemainsByteAndStateIdenticalAcrossReadOnlyBindAnd { byte[] beforeBytes = File.ReadAllBytes(LegacyFixturePath); string beforeText = File.ReadAllText(LegacyFixturePath); - Assert.That(beforeText.IndexOf("_RenderingMode", StringComparison.Ordinal), Is.LessThan(0)); + Assert.That( + beforeText.IndexOf("_RenderingMode", StringComparison.Ordinal), + Is.LessThan(0) + ); AssetDatabase.ImportAsset(LegacyFixturePath, ImportAssetOptions.ForceSynchronousImport); Material material = AssetDatabase.LoadAssetAtPath(LegacyFixturePath); Assert.That(material, Is.Not.Null, "The legacy fixture did not import as a material."); - Assert.That(material.shader.FindPropertyIndex("_RenderingMode"), Is.GreaterThanOrEqualTo(0)); + Assert.That( + material.shader.FindPropertyIndex("_RenderingMode"), + Is.GreaterThanOrEqualTo(0) + ); MaterialState before = MaterialState.Capture(material); AssertLegacyState(before); MaterialEditor.GetMaterialProperties(new UnityEngine.Object[] { material }); - Assert.That(EditorUtility.IsDirty(material), Is.False, "Binding a legacy material must not normalize it."); + Assert.That( + EditorUtility.IsDirty(material), + Is.False, + "Binding a legacy material must not normalize it." + ); SaveOnlyOwnedAssetAndReimport(material, LegacyFixturePath); material = AssetDatabase.LoadAssetAtPath(LegacyFixturePath); @@ -318,14 +527,24 @@ public void ExplicitModeNormalizationMatchesTheCompleteFourByThreeStateTable() { material.SetInteger("_RenderingMode", mode.value); InvokeApply(apply, material); - Assert.That(material.GetInteger("_RenderingMode"), Is.EqualTo(mode.value), $"{product.shaderName} {mode.name} mode value."); + Assert.That( + material.GetInteger("_RenderingMode"), + Is.EqualTo(mode.value), + $"{product.shaderName} {mode.name} mode value." + ); AssertRenderTypeState(material, mode); Assert.That(GetRawRenderQueue(material), Is.EqualTo(mode.rawQueue)); Assert.That(material.renderQueue, Is.EqualTo(mode.resolvedQueue)); AssertHiddenState(material, mode); AssertRenderingKeywords(material, mode.enabledKeywords); - Assert.That(material.GetShaderPassEnabled("ShadowCaster"), Is.EqualTo(mode.enableContributionPasses)); - Assert.That(material.GetShaderPassEnabled("Meta"), Is.EqualTo(mode.enableContributionPasses)); + Assert.That( + material.GetShaderPassEnabled("ShadowCaster"), + Is.EqualTo(mode.enableContributionPasses) + ); + Assert.That( + material.GetShaderPassEnabled("Meta"), + Is.EqualTo(mode.enableContributionPasses) + ); } } } @@ -342,7 +561,14 @@ public void ExplicitModeNormalizationPreservesStencilOverridesAcrossProducts() var material = CreateMaterial(RequireProductShader(product.shaderName)); foreach (StencilPropertyContract property in StencilProperties) { - Assert.That(material.shader.FindPropertyIndex(property.name), Is.GreaterThanOrEqualTo(0), product.shaderName + " must expose " + property.name + " before normalizer retention is tested."); + Assert.That( + material.shader.FindPropertyIndex(property.name), + Is.GreaterThanOrEqualTo(0), + product.shaderName + + " must expose " + + property.name + + " before normalizer retention is tested." + ); material.SetFloat(property.name, property.overrideValue); } @@ -350,9 +576,15 @@ public void ExplicitModeNormalizationPreservesStencilOverridesAcrossProducts() { material.SetInteger("_RenderingMode", mode.value); InvokeApply(apply, material); - AssertStencilOverrides(material, product.shaderName + " " + mode.name + " after Apply"); + AssertStencilOverrides( + material, + product.shaderName + " " + mode.name + " after Apply" + ); InvokeDrawerSelectionRefresh(refreshSelection, new[] { material }); - AssertStencilOverrides(material, product.shaderName + " " + mode.name + " after Resync"); + AssertStencilOverrides( + material, + product.shaderName + " " + mode.name + " after Resync" + ); } } } @@ -364,8 +596,16 @@ private static void AssertStencilOverrides(Material material, string context) { foreach (StencilPropertyContract property in StencilProperties) { - Assert.That(material.HasProperty(property.name), Is.True, context + " must retain " + property.name + "."); - Assert.That(material.GetFloat(property.name), Is.EqualTo(property.overrideValue), context + " property " + property.name + "."); + Assert.That( + material.HasProperty(property.name), + Is.True, + context + " must retain " + property.name + "." + ); + Assert.That( + material.GetFloat(property.name), + Is.EqualTo(property.overrideValue), + context + " property " + property.name + "." + ); } } @@ -374,7 +614,11 @@ private static void AssertStencilOverrides(Material material, string context) public void PublicRenderingModeApiIsDiscoverableWithoutATestAssemblyDependency() { Type enumType = FindLoadedType("PureBase.Editor.PureBaseRenderingMode"); - Assert.That(enumType, Is.Not.Null, "PureBaseRenderingMode must be discoverable from the loaded Editor assemblies."); + Assert.That( + enumType, + Is.Not.Null, + "PureBaseRenderingMode must be discoverable from the loaded Editor assemblies." + ); Assert.That(enumType.IsPublic, Is.True, "PureBaseRenderingMode must be public."); Assert.That(enumType.IsEnum, Is.True, "PureBaseRenderingMode must be an enum."); CollectionAssert.AreEqual( @@ -392,8 +636,16 @@ public void PublicRenderingModeApiIsDiscoverableWithoutATestAssemblyDependency() "PureBaseRenderingMode must expose exactly the stable 0, 1, and 2 ABI values without aliases." ); Type normalizerType = FindLoadedType("PureBase.Editor.PureBaseMaterialRenderingMode"); - Assert.That(normalizerType, Is.Not.Null, "PureBaseMaterialRenderingMode must be discoverable from the loaded Editor assemblies."); - Assert.That(normalizerType.IsPublic, Is.True, "PureBaseMaterialRenderingMode must be public."); + Assert.That( + normalizerType, + Is.Not.Null, + "PureBaseMaterialRenderingMode must be discoverable from the loaded Editor assemblies." + ); + Assert.That( + normalizerType.IsPublic, + Is.True, + "PureBaseMaterialRenderingMode must be public." + ); Assert.That(RequireApplyMethod(), Is.Not.Null); } @@ -407,7 +659,14 @@ private sealed class StencilPropertyContract /// The exact imported drawer attribute. /// The whitespace-tolerant source attribute pattern. /// The whitespace-tolerant source default pattern. - public StencilPropertyContract(string name, float defaultValue, float overrideValue, string attribute, string sourceAttributePattern, string sourceDefaultPattern) + public StencilPropertyContract( + string name, + float defaultValue, + float overrideValue, + string attribute, + string sourceAttributePattern, + string sourceDefaultPattern + ) { this.name = name; this.defaultValue = defaultValue; @@ -435,6 +694,5 @@ public StencilPropertyContract(string name, float defaultValue, float overrideVa /// Stores the source default value pattern. public readonly string sourceDefaultPattern; } - } } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Support.cs b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Support.cs index c2a38bd..8a399e5 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Support.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.Support.cs @@ -29,7 +29,6 @@ using UnityEngine; using UnityEngine.Rendering; - namespace PureBase.Tests.Daily { public sealed partial class PureBaseRenderingModeContractTests @@ -37,10 +36,21 @@ public sealed partial class PureBaseRenderingModeContractTests /// Saves and synchronously reimports one test-owned asset without persisting unrelated dirty Editor assets. /// The exact fixture or temporary asset owned by this test. /// The expected project-relative path for . - private static void SaveOnlyOwnedAssetAndReimport(UnityEngine.Object asset, string assetPath) + private static void SaveOnlyOwnedAssetAndReimport( + UnityEngine.Object asset, + string assetPath + ) { - Assert.That(asset, Is.Not.Null, $"Test-owned asset '{assetPath}' must exist before persistence."); - Assert.That(AssetDatabase.GetAssetPath(asset), Is.EqualTo(assetPath), "Persistence must target only the supplied test-owned asset path."); + Assert.That( + asset, + Is.Not.Null, + $"Test-owned asset '{assetPath}' must exist before persistence." + ); + Assert.That( + AssetDatabase.GetAssetPath(asset), + Is.EqualTo(assetPath), + "Persistence must target only the supplied test-owned asset path." + ); AssetDatabase.SaveAssetIfDirty(asset); AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport); } @@ -52,8 +62,16 @@ private static Shader RequireProductShader(string shaderName) { Shader shader = Shader.Find(shaderName); Assert.That(shader, Is.Not.Null, $"Product shader '{shaderName}' was not imported."); - Assert.That(ShaderUtil.ShaderHasError(shader), Is.False, $"Product shader '{shaderName}' has compiler errors."); - Assert.That(shader.isSupported, Is.True, $"Product shader '{shaderName}' is unsupported."); + Assert.That( + ShaderUtil.ShaderHasError(shader), + Is.False, + $"Product shader '{shaderName}' has compiler errors." + ); + Assert.That( + shader.isSupported, + Is.True, + $"Product shader '{shaderName}' is unsupported." + ); return shader; } @@ -108,7 +126,12 @@ private static string[] GetVisiblePropertyNames(Shader shader) private static string[] GetPassNames(Shader shader) { var names = new List(); - foreach (Match match in Regex.Matches(LoadGeneratedSource(shader.name), "\\bName\\s+\\\"([^\\\"]+)\\\"")) + foreach ( + Match match in Regex.Matches( + LoadGeneratedSource(shader.name), + "\\bName\\s+\\\"([^\\\"]+)\\\"" + ) + ) names.Add(match.Groups[1].Value); return names.ToArray(); } @@ -119,26 +142,43 @@ private static string[] GetPassNames(Shader shader) private static string LoadGeneratedSource(string shaderName) { string path = null; - foreach (string guid in AssetDatabase.FindAssets("t:Shader", new[] { "Packages/jp.penguin.purebase/Shaders" })) + foreach ( + string guid in AssetDatabase.FindAssets( + "t:Shader", + new[] { "Packages/jp.penguin.purebase/Shaders" } + ) + ) { string candidate = AssetDatabase.GUIDToAssetPath(guid); Shader shader = AssetDatabase.LoadAssetAtPath(candidate); - if (shader != null && string.Equals(shader.name, shaderName, StringComparison.Ordinal)) + if ( + shader != null + && string.Equals(shader.name, shaderName, StringComparison.Ordinal) + ) { path = candidate; break; } } - Assert.That(path, Is.Not.Empty, $"Could not locate the Shader-Core source asset for '{shaderName}'."); + Assert.That( + path, + Is.Not.Empty, + $"Could not locate the Shader-Core source asset for '{shaderName}'." + ); foreach (UnityEngine.Object asset in AssetDatabase.LoadAllAssetsAtPath(path)) { var source = asset as TextAsset; - if (source != null && string.Equals(source.name, "Shader Source", StringComparison.Ordinal)) + if ( + source != null + && string.Equals(source.name, "Shader Source", StringComparison.Ordinal) + ) return source.text; } - Assert.Fail($"Shader-Core source asset '{path}' for '{shaderName}' has no generated Shader Source subasset."); + Assert.Fail( + $"Shader-Core source asset '{path}' for '{shaderName}' has no generated Shader Source subasset." + ); return null; } @@ -200,8 +240,14 @@ private static void AssertModeState(Material material, ModeContract mode) Assert.That(material.renderQueue, Is.EqualTo(mode.resolvedQueue)); AssertHiddenState(material, mode); AssertRenderingKeywords(material, mode.enabledKeywords); - Assert.That(material.GetShaderPassEnabled("ShadowCaster"), Is.EqualTo(mode.enableContributionPasses)); - Assert.That(material.GetShaderPassEnabled("Meta"), Is.EqualTo(mode.enableContributionPasses)); + Assert.That( + material.GetShaderPassEnabled("ShadowCaster"), + Is.EqualTo(mode.enableContributionPasses) + ); + Assert.That( + material.GetShaderPassEnabled("Meta"), + Is.EqualTo(mode.enableContributionPasses) + ); } /// Asserts all noncanonical fields that the legacy fixture must preserve unchanged. @@ -224,7 +270,11 @@ private static int GetRawRenderQueue(Material material) { var serializedMaterial = new SerializedObject(material); SerializedProperty queue = serializedMaterial.FindProperty("m_CustomRenderQueue"); - Assert.That(queue, Is.Not.Null, "Material serialization has no m_CustomRenderQueue property."); + Assert.That( + queue, + Is.Not.Null, + "Material serialization has no m_CustomRenderQueue property." + ); return queue.intValue; } @@ -233,23 +283,51 @@ private static int GetRawRenderQueue(Material material) /// The expected rendering-mode state. private static void AssertRenderTypeState(Material material, ModeContract mode) { - bool hasOverride = TryGetSerializedRenderTypeOverride(material, out string renderTypeOverride); - Assert.That(hasOverride, Is.EqualTo(mode.hasRenderTypeOverride), mode.name + " RenderType override presence."); + bool hasOverride = TryGetSerializedRenderTypeOverride( + material, + out string renderTypeOverride + ); + Assert.That( + hasOverride, + Is.EqualTo(mode.hasRenderTypeOverride), + mode.name + " RenderType override presence." + ); if (hasOverride) - Assert.That(renderTypeOverride, Is.EqualTo(mode.renderTypeOverride), mode.name + " RenderType override."); - Assert.That(material.GetTag("RenderType", false), Is.EqualTo(mode.resolvedRenderType), mode.name + " resolved RenderType tag."); + Assert.That( + renderTypeOverride, + Is.EqualTo(mode.renderTypeOverride), + mode.name + " RenderType override." + ); + Assert.That( + material.GetTag("RenderType", false), + Is.EqualTo(mode.resolvedRenderType), + mode.name + " resolved RenderType tag." + ); } /// Reads the raw RenderType override from Unity's serialized material tag map. /// The material whose serialized tag map is inspected. /// Receives the override value when it exists. /// Whether the material serializes an explicit RenderType override. - private static bool TryGetSerializedRenderTypeOverride(Material material, out string renderTypeOverride) + private static bool TryGetSerializedRenderTypeOverride( + Material material, + out string renderTypeOverride + ) { string serializedMaterial = EditorJsonUtility.ToJson(material); - Match tagMap = Regex.Match(serializedMaterial, @"""stringTagMap""\s*:\s*\{(?[^}]*)\}"); - Assert.That(tagMap.Success, Is.True, "Material serialization has no stringTagMap object."); - Match renderType = Regex.Match(tagMap.Groups["entries"].Value, @"""RenderType""\s*:\s*""(?[^""]*)"""); + Match tagMap = Regex.Match( + serializedMaterial, + @"""stringTagMap""\s*:\s*\{(?[^}]*)\}" + ); + Assert.That( + tagMap.Success, + Is.True, + "Material serialization has no stringTagMap object." + ); + Match renderType = Regex.Match( + tagMap.Groups["entries"].Value, + @"""RenderType""\s*:\s*""(?[^""]*)""" + ); renderTypeOverride = renderType.Success ? renderType.Groups["value"].Value : null; return renderType.Success; } @@ -260,9 +338,20 @@ private static bool TryGetSerializedRenderTypeOverride(Material material, out st private static void AssertRenderingModeKeywordDeclarations(string source, string shaderName) { var declaredKeywords = new HashSet(StringComparer.Ordinal); - foreach (Match declaration in Regex.Matches(source, @"^\s*#pragma\s+shader_feature_local\s+([^\r\n]+)", RegexOptions.Multiline)) + foreach ( + Match declaration in Regex.Matches( + source, + @"^\s*#pragma\s+shader_feature_local\s+([^\r\n]+)", + RegexOptions.Multiline + ) + ) { - foreach (Match keyword in Regex.Matches(declaration.Groups[1].Value, @"\bPUREBASE_RENDERING_[A-Z0-9_]+\b")) + foreach ( + Match keyword in Regex.Matches( + declaration.Groups[1].Value, + @"\bPUREBASE_RENDERING_[A-Z0-9_]+\b" + ) + ) declaredKeywords.Add(keyword.Value); } @@ -276,7 +365,9 @@ private static void AssertRenderingModeKeywordDeclarations(string source, string Assert.That( Regex.IsMatch( source, - "HLSLINCLUDE[\\s\\S]*?#pragma\\s+shader_feature_local\\s+(?:_\\s+)?PUREBASE_RENDERING_OPAQUE\\s+PUREBASE_RENDERING_TRANSPARENT[\\s\\S]*?ENDHLSL[\\s\\S]*?Name\\s+\\\"" + Regex.Escape(passName) + "\\\"" + "HLSLINCLUDE[\\s\\S]*?#pragma\\s+shader_feature_local\\s+(?:_\\s+)?PUREBASE_RENDERING_OPAQUE\\s+PUREBASE_RENDERING_TRANSPARENT[\\s\\S]*?ENDHLSL[\\s\\S]*?Name\\s+\\\"" + + Regex.Escape(passName) + + "\\\"" ), Is.True, $"Product shader '{shaderName}' pass '{passName}' must inherit the rendering-mode local shader feature from the shared HLSLINCLUDE block." @@ -290,7 +381,11 @@ private sealed class ProductContract /// Initializes one immutable product contract. /// The stable public shader name. /// The ordered visible property ABI. - public ProductContract(string shaderName, string propertySourcePath, string[] visiblePropertyNames) + public ProductContract( + string shaderName, + string propertySourcePath, + string[] visiblePropertyNames + ) { this.shaderName = shaderName; this.propertySourcePath = propertySourcePath; @@ -311,7 +406,15 @@ public ProductContract(string shaderName, string propertySourcePath, string[] vi private sealed class ModeContract { /// Initializes one immutable state-table row. - public ModeContract(int value, string name, BlendState blend, RenderTypeState renderType, QueueState queue, string[] enabledKeywords, bool enableContributionPasses) + public ModeContract( + int value, + string name, + BlendState blend, + RenderTypeState renderType, + QueueState queue, + string[] enabledKeywords, + bool enableContributionPasses + ) { this.value = value; this.name = name; @@ -376,7 +479,13 @@ public ModeContract(int value, string name, BlendState blend, RenderTypeState re private sealed class BlendState { /// Initializes one immutable blend-state value group. - public BlendState(int srcBlend, int dstBlend, int zWrite, int addSrcBlend, int addDstBlend) + public BlendState( + int srcBlend, + int dstBlend, + int zWrite, + int addSrcBlend, + int addDstBlend + ) { this.srcBlend = srcBlend; this.dstBlend = dstBlend; @@ -405,7 +514,11 @@ public BlendState(int srcBlend, int dstBlend, int zWrite, int addSrcBlend, int a private sealed class RenderTypeState { /// Initializes one immutable RenderType-state value group. - public RenderTypeState(string renderTypeOverride, bool hasRenderTypeOverride, string resolvedRenderType) + public RenderTypeState( + string renderTypeOverride, + bool hasRenderTypeOverride, + string resolvedRenderType + ) { this.renderTypeOverride = renderTypeOverride; this.hasRenderTypeOverride = hasRenderTypeOverride; @@ -438,6 +551,5 @@ public QueueState(int rawQueue, int resolvedQueue) /// Stores the shader-resolved render queue. public readonly int resolvedQueue; } - -} } +} diff --git a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.cs b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.cs index 9a79069..d80f891 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeContractTests.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeContractTests.cs @@ -19,7 +19,5 @@ namespace PureBase.Tests.Daily { /// Defines Editor-side rendering-mode contracts before the product normalizer is implemented. - public sealed partial class PureBaseRenderingModeContractTests - { - } + public sealed partial class PureBaseRenderingModeContractTests { } } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.D24S8StencilFixture.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.D24S8StencilFixture.cs index 8d25696..372cad5 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.D24S8StencilFixture.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.D24S8StencilFixture.cs @@ -37,7 +37,8 @@ private sealed class D24S8StencilFixture : IDisposable private const int FixtureLayer = 31; /// Identifies the imported RenderTexture asset that prevents compatible-format promotion. - private const string RenderTextureAssetPath = "Packages/jp.penguin.purebase/Tests/Daily/Editor/PureBaseD24S8StencilTarget.renderTexture"; + private const string RenderTextureAssetPath = + "Packages/jp.penguin.purebase/Tests/Daily/Editor/PureBaseD24S8StencilTarget.renderTexture"; /// Stores the isolated preview scene that prevents unrelated scene content from affecting readback. private Scene scene; @@ -92,17 +93,52 @@ public string FormatDescription { get { - return "Device=" + SystemInfo.graphicsDeviceType + - " AssetPath=" + RenderTextureAssetPath + - " RequestedColor=" + (renderTexture == null ? "" : renderTexture.descriptor.graphicsFormat.ToString()) + - " RequestedDepthStencil=" + GraphicsFormat.D24_UNorm_S8_UInt + - " ActualColor=" + (renderTexture == null ? "" : renderTexture.graphicsFormat.ToString()) + - " ActualDepthStencil=" + (renderTexture == null ? "" : renderTexture.depthStencilFormat.ToString()) + - " StencilBits=" + (renderTexture == null ? "" : GetStencilBitCount(renderTexture.depthStencilFormat).ToString()) + - " RequestedRenderingPath=" + (cameraObject == null ? "" : cameraObject.GetComponent().renderingPath.ToString()) + - " ActualRenderingPath=" + (cameraObject == null ? "" : cameraObject.GetComponent().actualRenderingPath.ToString()) + - " PixelLightCount=" + QualitySettings.pixelLightCount + - " IsCreated=" + (renderTexture != null && renderTexture.IsCreated()); + return "Device=" + + SystemInfo.graphicsDeviceType + + " AssetPath=" + + RenderTextureAssetPath + + " RequestedColor=" + + ( + renderTexture == null + ? "" + : renderTexture.descriptor.graphicsFormat.ToString() + ) + + " RequestedDepthStencil=" + + GraphicsFormat.D24_UNorm_S8_UInt + + " ActualColor=" + + ( + renderTexture == null + ? "" + : renderTexture.graphicsFormat.ToString() + ) + + " ActualDepthStencil=" + + ( + renderTexture == null + ? "" + : renderTexture.depthStencilFormat.ToString() + ) + + " StencilBits=" + + ( + renderTexture == null + ? "" + : GetStencilBitCount(renderTexture.depthStencilFormat).ToString() + ) + + " RequestedRenderingPath=" + + ( + cameraObject == null + ? "" + : cameraObject.GetComponent().renderingPath.ToString() + ) + + " ActualRenderingPath=" + + ( + cameraObject == null + ? "" + : cameraObject.GetComponent().actualRenderingPath.ToString() + ) + + " PixelLightCount=" + + QualitySettings.pixelLightCount + + " IsCreated=" + + (renderTexture != null && renderTexture.IsCreated()); } } @@ -120,9 +156,26 @@ public void Initialize() RenderSettings.ambientMode = AmbientMode.Flat; RenderSettings.ambientLight = Color.black; RenderSettings.reflectionIntensity = 0.0f; - Assert.That(SystemInfo.graphicsDeviceType, Is.EqualTo(GraphicsDeviceType.Direct3D11), "The D24S8 Stencil fixture requires D3D11 and must not silently run on " + SystemInfo.graphicsDeviceType + "."); - Assert.That(SystemInfo.IsFormatSupported(GraphicsFormat.D24_UNorm_S8_UInt, FormatUsage.Render), Is.True, "D3D11 must support D24_UNorm_S8_UInt as a render attachment for the Stencil fixture."); - Assert.That(SystemInfo.IsFormatSupported(GraphicsFormat.R8G8B8A8_UNorm, FormatUsage.Render), Is.True, "D3D11 must support the fixture color attachment format."); + Assert.That( + SystemInfo.graphicsDeviceType, + Is.EqualTo(GraphicsDeviceType.Direct3D11), + "The D24S8 Stencil fixture requires D3D11 and must not silently run on " + + SystemInfo.graphicsDeviceType + + "." + ); + Assert.That( + SystemInfo.IsFormatSupported( + GraphicsFormat.D24_UNorm_S8_UInt, + FormatUsage.Render + ), + Is.True, + "D3D11 must support D24_UNorm_S8_UInt as a render attachment for the Stencil fixture." + ); + Assert.That( + SystemInfo.IsFormatSupported(GraphicsFormat.R8G8B8A8_UNorm, FormatUsage.Render), + Is.True, + "D3D11 must support the fixture color attachment format." + ); renderTexture = LoadAndCreateD24S8RenderTextureAsset(); @@ -149,9 +202,21 @@ public void Initialize() /// The explicit material Stencil state. /// The Pure-Base rendering mode to apply. /// The center pixel after the product draw. - public Color RenderSingle(Shader shader, Color baseColor, byte clearStencil, StencilState stencilState, int renderingMode) + public Color RenderSingle( + Shader shader, + Color baseColor, + byte clearStencil, + StencilState stencilState, + int renderingMode + ) { - Material material = CreateProductMaterial(shader, baseColor, stencilState, renderingMode, "single draw"); + Material material = CreateProductMaterial( + shader, + baseColor, + stencilState, + renderingMode, + "single draw" + ); ClearTarget(clearStencil); RenderMaterial(material); return ReadCenterPixel(renderTexture, texture); @@ -167,22 +232,62 @@ public Color RenderSingle(Shader shader, Color baseColor, byte clearStencil, Ste /// The Pure-Base rendering mode to apply to both materials. /// Receives the center pixel from the same writer without a reader. /// The center pixel after the reader draw. - public Color RenderWriterThenReader(Shader shader, byte clearStencil, Color writerColor, StencilState writerState, Color readerColor, StencilState readerState, int renderingMode, out Color writerOnly) + public Color RenderWriterThenReader( + Shader shader, + byte clearStencil, + Color writerColor, + StencilState writerState, + Color readerColor, + StencilState readerState, + int renderingMode, + out Color writerOnly + ) { - Material writer = CreateProductMaterial(shader, writerColor, writerState, renderingMode, "Stencil writer"); - Material reader = CreateProductMaterial(shader, readerColor, readerState, renderingMode, "Stencil reader"); + Material writer = CreateProductMaterial( + shader, + writerColor, + writerState, + renderingMode, + "Stencil writer" + ); + Material reader = CreateProductMaterial( + shader, + readerColor, + readerState, + renderingMode, + "Stencil reader" + ); Material control = CreateProductMaterial( shader, readerColor, - new StencilState(0, 255, 0, CompareFunction.Always, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep), + new StencilState( + 0, + 255, + 0, + CompareFunction.Always, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ), renderingMode, "Always+Keep Stencil reader control" ); RenderStencilSequence(clearStencil, writer, RearDepth, control, FrontDepth); Color controlPixel = ReadCenterPixel(renderTexture, texture); - AssertFinite(controlPixel, shader.name + " Always+Keep reader control " + FormatDescription); - Assert.That(RgbMagnitude(controlPixel), Is.GreaterThan(0.05f), shader.name + " Always+Keep reader control after the writer must render before Equal/NotEqual is observed. " + FormatDescription + " Pixel=" + controlPixel); + AssertFinite( + controlPixel, + shader.name + " Always+Keep reader control " + FormatDescription + ); + Assert.That( + RgbMagnitude(controlPixel), + Is.GreaterThan(0.05f), + shader.name + + " Always+Keep reader control after the writer must render before Equal/NotEqual is observed. " + + FormatDescription + + " Pixel=" + + controlPixel + ); ClearTarget(clearStencil); RenderMaterial(writer); @@ -197,10 +302,21 @@ public Color RenderWriterThenReader(Shader shader, byte clearStencil, Color writ /// The material Stencil state. /// The number of directional lights to render. /// The center pixel after the Toon composite. - public Color RenderToonComposite(Shader shader, byte clearStencil, StencilState stencilState, int lightCount) + public Color RenderToonComposite( + Shader shader, + byte clearStencil, + StencilState stencilState, + int lightCount + ) { SetLightCount(lightCount); - Material material = CreateProductMaterial(shader, new Color(0.8f, 0.6f, 0.4f, 0.5f), stencilState, 2, "Toon ForwardAdd composite"); + Material material = CreateProductMaterial( + shader, + new Color(0.8f, 0.6f, 0.4f, 0.5f), + stencilState, + 2, + "Toon ForwardAdd composite" + ); AssertToonForwardAddPreconditions(lightCount, material); ClearTarget(clearStencil); RenderMaterial(material); @@ -339,32 +455,125 @@ private void RestoreRenderSettings() /// The exact tracked D24S8 asset with a created GPU resource. private static RenderTexture LoadAndCreateD24S8RenderTextureAsset() { - RenderTexture target = AssetDatabase.LoadAssetAtPath(RenderTextureAssetPath); - Assert.That(target, Is.Not.Null, "The D24S8 fixture asset must exist at '" + RenderTextureAssetPath + "'. This is fixture configuration, not product behavior."); + RenderTexture target = AssetDatabase.LoadAssetAtPath( + RenderTextureAssetPath + ); + Assert.That( + target, + Is.Not.Null, + "The D24S8 fixture asset must exist at '" + + RenderTextureAssetPath + + "'. This is fixture configuration, not product behavior." + ); AssertCompatibleFormatFallbackIsDisabled(target); RenderTextureDescriptor descriptor = target.descriptor; - Assert.That(descriptor.graphicsFormat, Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), "The D24S8 fixture asset descriptor must request R8G8B8A8_UNorm color without fallback. " + DescribeTarget(target)); - Assert.That(descriptor.depthStencilFormat, Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), "The D24S8 fixture asset descriptor must request D24_UNorm_S8_UInt depth-stencil without fallback. " + DescribeTarget(target)); - Assert.That(target.antiAliasing, Is.EqualTo(1), "The D24S8 fixture asset must use MSAA 1. " + DescribeTarget(target)); - Assert.That(descriptor.msaaSamples, Is.EqualTo(1), "The D24S8 fixture descriptor must use MSAA 1. " + DescribeTarget(target)); - Assert.That(target.sRGB, Is.False, "The D24S8 fixture asset must be linear and non-sRGB. " + DescribeTarget(target)); - Assert.That(descriptor.sRGB, Is.False, "The D24S8 fixture descriptor must be linear and non-sRGB. " + DescribeTarget(target)); - Assert.That(target.useMipMap, Is.False, "The D24S8 fixture asset must not use mipmaps. " + DescribeTarget(target)); - Assert.That(descriptor.useMipMap, Is.False, "The D24S8 fixture descriptor must not use mipmaps. " + DescribeTarget(target)); - Assert.That(target.useDynamicScale, Is.False, "The D24S8 fixture asset must not use dynamic scaling. " + DescribeTarget(target)); - Assert.That(descriptor.useDynamicScale, Is.False, "The D24S8 fixture descriptor must not use dynamic scaling. " + DescribeTarget(target)); - Assert.That(target.enableRandomWrite, Is.False, "The D24S8 fixture asset must not enable random writes. " + DescribeTarget(target)); - Assert.That(descriptor.enableRandomWrite, Is.False, "The D24S8 fixture descriptor must not enable random writes. " + DescribeTarget(target)); + Assert.That( + descriptor.graphicsFormat, + Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), + "The D24S8 fixture asset descriptor must request R8G8B8A8_UNorm color without fallback. " + + DescribeTarget(target) + ); + Assert.That( + descriptor.depthStencilFormat, + Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), + "The D24S8 fixture asset descriptor must request D24_UNorm_S8_UInt depth-stencil without fallback. " + + DescribeTarget(target) + ); + Assert.That( + target.antiAliasing, + Is.EqualTo(1), + "The D24S8 fixture asset must use MSAA 1. " + DescribeTarget(target) + ); + Assert.That( + descriptor.msaaSamples, + Is.EqualTo(1), + "The D24S8 fixture descriptor must use MSAA 1. " + DescribeTarget(target) + ); + Assert.That( + target.sRGB, + Is.False, + "The D24S8 fixture asset must be linear and non-sRGB. " + DescribeTarget(target) + ); + Assert.That( + descriptor.sRGB, + Is.False, + "The D24S8 fixture descriptor must be linear and non-sRGB. " + + DescribeTarget(target) + ); + Assert.That( + target.useMipMap, + Is.False, + "The D24S8 fixture asset must not use mipmaps. " + DescribeTarget(target) + ); + Assert.That( + descriptor.useMipMap, + Is.False, + "The D24S8 fixture descriptor must not use mipmaps. " + DescribeTarget(target) + ); + Assert.That( + target.useDynamicScale, + Is.False, + "The D24S8 fixture asset must not use dynamic scaling. " + + DescribeTarget(target) + ); + Assert.That( + descriptor.useDynamicScale, + Is.False, + "The D24S8 fixture descriptor must not use dynamic scaling. " + + DescribeTarget(target) + ); + Assert.That( + target.enableRandomWrite, + Is.False, + "The D24S8 fixture asset must not enable random writes. " + + DescribeTarget(target) + ); + Assert.That( + descriptor.enableRandomWrite, + Is.False, + "The D24S8 fixture descriptor must not enable random writes. " + + DescribeTarget(target) + ); try { - Assert.That(target.Create(), Is.True, "The configured D24S8 fixture asset must create its GPU resource without compatible-format fallback. " + DescribeTarget(target)); - Assert.That(target.IsCreated(), Is.True, "The configured D24S8 fixture asset GPU resource must be created. " + DescribeTarget(target)); - Assert.That(target.graphicsFormat, Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), "The created D24S8 fixture asset must allocate exact R8G8B8A8_UNorm color. " + DescribeTarget(target)); - Assert.That(target.depthStencilFormat, Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), "The created D24S8 fixture asset must allocate exact D24_UNorm_S8_UInt depth-stencil. " + DescribeTarget(target)); - Assert.That(RenderTexture.SupportsStencil(target), Is.True, "The created D24S8 fixture asset must provide a Stencil attachment. " + DescribeTarget(target)); - Assert.That(GetStencilBitCount(target.depthStencilFormat), Is.EqualTo(8), "The created D24S8 fixture asset must provide exactly eight Stencil bits. " + DescribeTarget(target)); + Assert.That( + target.Create(), + Is.True, + "The configured D24S8 fixture asset must create its GPU resource without compatible-format fallback. " + + DescribeTarget(target) + ); + Assert.That( + target.IsCreated(), + Is.True, + "The configured D24S8 fixture asset GPU resource must be created. " + + DescribeTarget(target) + ); + Assert.That( + target.graphicsFormat, + Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), + "The created D24S8 fixture asset must allocate exact R8G8B8A8_UNorm color. " + + DescribeTarget(target) + ); + Assert.That( + target.depthStencilFormat, + Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), + "The created D24S8 fixture asset must allocate exact D24_UNorm_S8_UInt depth-stencil. " + + DescribeTarget(target) + ); + Assert.That( + RenderTexture.SupportsStencil(target), + Is.True, + "The created D24S8 fixture asset must provide a Stencil attachment. " + + DescribeTarget(target) + ); + Assert.That( + GetStencilBitCount(target.depthStencilFormat), + Is.EqualTo(8), + "The created D24S8 fixture asset must provide exactly eight Stencil bits. " + + DescribeTarget(target) + ); return target; } catch @@ -377,41 +586,136 @@ private static RenderTexture LoadAndCreateD24S8RenderTextureAsset() /// Loads and conditionally creates the shared D24S8 asset for the active-scene Toon scope. /// Receives whether this call created the GPU resource. /// The exact tracked D24S8 asset with a created GPU resource. - public static RenderTexture LoadAndCreateD24S8RenderTextureAssetForToonScope(out bool createdRenderTextureResource) + public static RenderTexture LoadAndCreateD24S8RenderTextureAssetForToonScope( + out bool createdRenderTextureResource + ) { - RenderTexture target = AssetDatabase.LoadAssetAtPath(RenderTextureAssetPath); - Assert.That(target, Is.Not.Null, "The D24S8 fixture asset must exist at '" + RenderTextureAssetPath + "'. This is fixture configuration, not product behavior."); + RenderTexture target = AssetDatabase.LoadAssetAtPath( + RenderTextureAssetPath + ); + Assert.That( + target, + Is.Not.Null, + "The D24S8 fixture asset must exist at '" + + RenderTextureAssetPath + + "'. This is fixture configuration, not product behavior." + ); AssertCompatibleFormatFallbackIsDisabled(target); bool initiallyCreated = target.IsCreated(); createdRenderTextureResource = false; RenderTextureDescriptor descriptor = target.descriptor; - Assert.That(descriptor.graphicsFormat, Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), "The D24S8 fixture asset descriptor must request R8G8B8A8_UNorm color without fallback. " + DescribeTarget(target)); - Assert.That(descriptor.depthStencilFormat, Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), "The D24S8 fixture asset descriptor must request D24_UNorm_S8_UInt depth-stencil without fallback. " + DescribeTarget(target)); - Assert.That(target.antiAliasing, Is.EqualTo(1), "The D24S8 fixture asset must use MSAA 1. " + DescribeTarget(target)); - Assert.That(descriptor.msaaSamples, Is.EqualTo(1), "The D24S8 fixture descriptor must use MSAA 1. " + DescribeTarget(target)); - Assert.That(target.sRGB, Is.False, "The D24S8 fixture asset must be linear and non-sRGB. " + DescribeTarget(target)); - Assert.That(descriptor.sRGB, Is.False, "The D24S8 fixture descriptor must be linear and non-sRGB. " + DescribeTarget(target)); - Assert.That(target.useMipMap, Is.False, "The D24S8 fixture asset must not use mipmaps. " + DescribeTarget(target)); - Assert.That(descriptor.useMipMap, Is.False, "The D24S8 fixture descriptor must not use mipmaps. " + DescribeTarget(target)); - Assert.That(target.useDynamicScale, Is.False, "The D24S8 fixture asset must not use dynamic scaling. " + DescribeTarget(target)); - Assert.That(descriptor.useDynamicScale, Is.False, "The D24S8 fixture descriptor must not use dynamic scaling. " + DescribeTarget(target)); - Assert.That(target.enableRandomWrite, Is.False, "The D24S8 fixture asset must not enable random writes. " + DescribeTarget(target)); - Assert.That(descriptor.enableRandomWrite, Is.False, "The D24S8 fixture descriptor must not enable random writes. " + DescribeTarget(target)); + Assert.That( + descriptor.graphicsFormat, + Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), + "The D24S8 fixture asset descriptor must request R8G8B8A8_UNorm color without fallback. " + + DescribeTarget(target) + ); + Assert.That( + descriptor.depthStencilFormat, + Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), + "The D24S8 fixture asset descriptor must request D24_UNorm_S8_UInt depth-stencil without fallback. " + + DescribeTarget(target) + ); + Assert.That( + target.antiAliasing, + Is.EqualTo(1), + "The D24S8 fixture asset must use MSAA 1. " + DescribeTarget(target) + ); + Assert.That( + descriptor.msaaSamples, + Is.EqualTo(1), + "The D24S8 fixture descriptor must use MSAA 1. " + DescribeTarget(target) + ); + Assert.That( + target.sRGB, + Is.False, + "The D24S8 fixture asset must be linear and non-sRGB. " + DescribeTarget(target) + ); + Assert.That( + descriptor.sRGB, + Is.False, + "The D24S8 fixture descriptor must be linear and non-sRGB. " + + DescribeTarget(target) + ); + Assert.That( + target.useMipMap, + Is.False, + "The D24S8 fixture asset must not use mipmaps. " + DescribeTarget(target) + ); + Assert.That( + descriptor.useMipMap, + Is.False, + "The D24S8 fixture descriptor must not use mipmaps. " + DescribeTarget(target) + ); + Assert.That( + target.useDynamicScale, + Is.False, + "The D24S8 fixture asset must not use dynamic scaling. " + + DescribeTarget(target) + ); + Assert.That( + descriptor.useDynamicScale, + Is.False, + "The D24S8 fixture descriptor must not use dynamic scaling. " + + DescribeTarget(target) + ); + Assert.That( + target.enableRandomWrite, + Is.False, + "The D24S8 fixture asset must not enable random writes. " + + DescribeTarget(target) + ); + Assert.That( + descriptor.enableRandomWrite, + Is.False, + "The D24S8 fixture descriptor must not enable random writes. " + + DescribeTarget(target) + ); try { if (!initiallyCreated) { - Assert.That(target.Create(), Is.True, "The configured D24S8 fixture asset must create its GPU resource without compatible-format fallback. " + DescribeTarget(target)); + Assert.That( + target.Create(), + Is.True, + "The configured D24S8 fixture asset must create its GPU resource without compatible-format fallback. " + + DescribeTarget(target) + ); createdRenderTextureResource = true; } - Assert.That(target.IsCreated(), Is.True, "The configured D24S8 fixture asset GPU resource must be created. " + DescribeTarget(target)); - Assert.That(target.graphicsFormat, Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), "The created D24S8 fixture asset must allocate exact R8G8B8A8_UNorm color. " + DescribeTarget(target)); - Assert.That(target.depthStencilFormat, Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), "The created D24S8 fixture asset must allocate exact D24_UNorm_S8_UInt depth-stencil. " + DescribeTarget(target)); - Assert.That(RenderTexture.SupportsStencil(target), Is.True, "The created D24S8 fixture asset must provide a Stencil attachment. " + DescribeTarget(target)); - Assert.That(GetStencilBitCount(target.depthStencilFormat), Is.EqualTo(8), "The created D24S8 fixture asset must provide exactly eight Stencil bits. " + DescribeTarget(target)); + Assert.That( + target.IsCreated(), + Is.True, + "The configured D24S8 fixture asset GPU resource must be created. " + + DescribeTarget(target) + ); + Assert.That( + target.graphicsFormat, + Is.EqualTo(GraphicsFormat.R8G8B8A8_UNorm), + "The created D24S8 fixture asset must allocate exact R8G8B8A8_UNorm color. " + + DescribeTarget(target) + ); + Assert.That( + target.depthStencilFormat, + Is.EqualTo(GraphicsFormat.D24_UNorm_S8_UInt), + "The created D24S8 fixture asset must allocate exact D24_UNorm_S8_UInt depth-stencil. " + + DescribeTarget(target) + ); + Assert.That( + RenderTexture.SupportsStencil(target), + Is.True, + "The created D24S8 fixture asset must provide a Stencil attachment. " + + DescribeTarget(target) + ); + Assert.That( + GetStencilBitCount(target.depthStencilFormat), + Is.EqualTo(8), + "The created D24S8 fixture asset must provide exactly eight Stencil bits. " + + DescribeTarget(target) + ); return target; } catch @@ -432,9 +736,21 @@ private static void AssertCompatibleFormatFallbackIsDisabled(RenderTexture targe using (var serializedTarget = new SerializedObject(target)) { serializedTarget.Update(); - SerializedProperty compatibleFallback = serializedTarget.FindProperty("m_EnableCompatibleFormat"); - Assert.That(compatibleFallback, Is.Not.Null, "The D24S8 fixture asset must expose m_EnableCompatibleFormat through SerializedObject inspection. " + DescribeTarget(target)); - Assert.That(compatibleFallback.boolValue, Is.False, "The D24S8 fixture asset must disable compatible-format fallback. " + DescribeTarget(target)); + SerializedProperty compatibleFallback = serializedTarget.FindProperty( + "m_EnableCompatibleFormat" + ); + Assert.That( + compatibleFallback, + Is.Not.Null, + "The D24S8 fixture asset must expose m_EnableCompatibleFormat through SerializedObject inspection. " + + DescribeTarget(target) + ); + Assert.That( + compatibleFallback.boolValue, + Is.False, + "The D24S8 fixture asset must disable compatible-format fallback. " + + DescribeTarget(target) + ); } } @@ -457,23 +773,40 @@ public static string DescribeTarget(RenderTexture target) } RenderTextureDescriptor descriptor = target.descriptor; - return "AssetPath=" + RenderTextureAssetPath + - " DescriptorColor=" + descriptor.graphicsFormat + - " DescriptorDepthStencil=" + descriptor.depthStencilFormat + - " DescriptorMSAA=" + descriptor.msaaSamples + - " DescriptorSRGB=" + descriptor.sRGB + - " DescriptorMipMap=" + descriptor.useMipMap + - " DescriptorDynamicScale=" + descriptor.useDynamicScale + - " DescriptorRandomWrite=" + descriptor.enableRandomWrite + - " ActualColor=" + target.graphicsFormat + - " ActualDepthStencil=" + target.depthStencilFormat + - " ActualMSAA=" + target.antiAliasing + - " ActualSRGB=" + target.sRGB + - " ActualMipMap=" + target.useMipMap + - " ActualDynamicScale=" + target.useDynamicScale + - " ActualRandomWrite=" + target.enableRandomWrite + - " StencilBits=" + GetStencilBitCount(target.depthStencilFormat) + - " IsCreated=" + target.IsCreated(); + return "AssetPath=" + + RenderTextureAssetPath + + " DescriptorColor=" + + descriptor.graphicsFormat + + " DescriptorDepthStencil=" + + descriptor.depthStencilFormat + + " DescriptorMSAA=" + + descriptor.msaaSamples + + " DescriptorSRGB=" + + descriptor.sRGB + + " DescriptorMipMap=" + + descriptor.useMipMap + + " DescriptorDynamicScale=" + + descriptor.useDynamicScale + + " DescriptorRandomWrite=" + + descriptor.enableRandomWrite + + " ActualColor=" + + target.graphicsFormat + + " ActualDepthStencil=" + + target.depthStencilFormat + + " ActualMSAA=" + + target.antiAliasing + + " ActualSRGB=" + + target.sRGB + + " ActualMipMap=" + + target.useMipMap + + " ActualDynamicScale=" + + target.useDynamicScale + + " ActualRandomWrite=" + + target.enableRandomWrite + + " StencilBits=" + + GetStencilBitCount(target.depthStencilFormat) + + " IsCreated=" + + target.IsCreated(); } /// Configures the fixture camera to render only its isolated preview scene into the D24S8 target. @@ -512,7 +845,11 @@ private static void ConfigureQuad(GameObject quadObject, float depth) /// The required number of lights. private void SetLightCount(int lightCount) { - Assert.That(lightCount, Is.GreaterThanOrEqualTo(1), "The Stencil fixture must have at least one directional light."); + Assert.That( + lightCount, + Is.GreaterThanOrEqualTo(1), + "The Stencil fixture must have at least one directional light." + ); DestroyLights(); int cullingMask = 1 << FixtureLayer; for (int index = 0; index < lightCount; index++) @@ -527,7 +864,11 @@ private void SetLightCount(int lightCount) light.color = Color.white; light.intensity = 1.0f; light.cullingMask = cullingMask; - lightObject.transform.rotation = Quaternion.Euler(30.0f, index == 0 ? -30.0f : 30.0f, 0.0f); + lightObject.transform.rotation = Quaternion.Euler( + 30.0f, + index == 0 ? -30.0f : 30.0f, + 0.0f + ); } RenderSettings.sun = lightObjects[0].GetComponent(); @@ -539,21 +880,61 @@ private void SetLightCount(int lightCount) private void AssertToonForwardAddPreconditions(int lightCount, Material material) { Camera camera = cameraObject.GetComponent(); - Assert.That(camera.renderingPath, Is.EqualTo(RenderingPath.Forward), "The Toon Stencil fixture must force the BIRP Forward camera path before observing ForwardAdd."); - Assert.That(QualitySettings.pixelLightCount, Is.GreaterThanOrEqualTo(2), "The Toon Stencil fixture must allow at least two pixel lights before observing ForwardAdd."); - Assert.That(lightCount, Is.GreaterThanOrEqualTo(1), "The Toon Stencil fixture requires at least one directional light."); - Assert.That(lightObjects.Count, Is.EqualTo(lightCount), "The Toon Stencil fixture must create exactly the requested number of directional lights."); + Assert.That( + camera.renderingPath, + Is.EqualTo(RenderingPath.Forward), + "The Toon Stencil fixture must force the BIRP Forward camera path before observing ForwardAdd." + ); + Assert.That( + QualitySettings.pixelLightCount, + Is.GreaterThanOrEqualTo(2), + "The Toon Stencil fixture must allow at least two pixel lights before observing ForwardAdd." + ); + Assert.That( + lightCount, + Is.GreaterThanOrEqualTo(1), + "The Toon Stencil fixture requires at least one directional light." + ); + Assert.That( + lightObjects.Count, + Is.EqualTo(lightCount), + "The Toon Stencil fixture must create exactly the requested number of directional lights." + ); foreach (GameObject lightObject in lightObjects) { Light light = lightObject.GetComponent(); - Assert.That(light.type, Is.EqualTo(LightType.Directional), "The Toon Stencil fixture requires directional pixel lights."); - Assert.That(light.renderMode, Is.EqualTo(LightRenderMode.ForcePixel), "The Toon Stencil fixture must force each directional light to the pixel-light path."); - Assert.That(light.intensity, Is.GreaterThan(0.0f), "The Toon Stencil fixture requires a nonzero directional-light intensity."); + Assert.That( + light.type, + Is.EqualTo(LightType.Directional), + "The Toon Stencil fixture requires directional pixel lights." + ); + Assert.That( + light.renderMode, + Is.EqualTo(LightRenderMode.ForcePixel), + "The Toon Stencil fixture must force each directional light to the pixel-light path." + ); + Assert.That( + light.intensity, + Is.GreaterThan(0.0f), + "The Toon Stencil fixture requires a nonzero directional-light intensity." + ); } - Assert.That(material.HasProperty("_NormalMap"), Is.True, "The Toon Stencil fixture requires the public normal-map input before observing ForwardAdd."); - Assert.That(material.GetTexture("_NormalMap"), Is.EqualTo(Texture2D.normalTexture), "The Toon Stencil fixture must use the neutral normal map before observing ForwardAdd."); - Assert.That(material.GetFloat("_NormalScale"), Is.EqualTo(1.0f), "The Toon Stencil fixture must use unit normal-map scale before observing ForwardAdd."); + Assert.That( + material.HasProperty("_NormalMap"), + Is.True, + "The Toon Stencil fixture requires the public normal-map input before observing ForwardAdd." + ); + Assert.That( + material.GetTexture("_NormalMap"), + Is.EqualTo(Texture2D.normalTexture), + "The Toon Stencil fixture must use the neutral normal map before observing ForwardAdd." + ); + Assert.That( + material.GetFloat("_NormalScale"), + Is.EqualTo(1.0f), + "The Toon Stencil fixture must use unit normal-map scale before observing ForwardAdd." + ); } /// Destroys temporary directional lights in reverse creation order. @@ -574,13 +955,41 @@ private void DestroyLights() /// The Pure-Base rendering mode. /// The scenario used to distinguish missing product behavior from fixture failure. /// The configured material. - private Material CreateProductMaterial(Shader shader, Color baseColor, StencilState stencilState, int renderingMode, string scenario) + private Material CreateProductMaterial( + Shader shader, + Color baseColor, + StencilState stencilState, + int renderingMode, + string scenario + ) { - Assert.That(shader, Is.Not.Null, "The product shader is required for the D24S8 Stencil " + scenario + " fixture."); + Assert.That( + shader, + Is.Not.Null, + "The product shader is required for the D24S8 Stencil " + scenario + " fixture." + ); var material = new Material(shader); materials.Add(material); - Assert.That(material.HasProperty("_BaseColor"), Is.True, "Product shader '" + shader.name + "' must expose _BaseColor for D24S8 Stencil " + scenario + ". " + FormatDescription); - Assert.That(material.HasProperty("_Cutoff"), Is.True, "Product shader '" + shader.name + "' must expose _Cutoff for D24S8 Stencil " + scenario + ". " + FormatDescription); + Assert.That( + material.HasProperty("_BaseColor"), + Is.True, + "Product shader '" + + shader.name + + "' must expose _BaseColor for D24S8 Stencil " + + scenario + + ". " + + FormatDescription + ); + Assert.That( + material.HasProperty("_Cutoff"), + Is.True, + "Product shader '" + + shader.name + + "' must expose _Cutoff for D24S8 Stencil " + + scenario + + ". " + + FormatDescription + ); material.SetTexture("_BaseTexture", Texture2D.whiteTexture); material.SetColor("_BaseColor", baseColor); material.SetFloat("_Cutoff", 0.5f); @@ -599,7 +1008,11 @@ private Material CreateProductMaterial(Shader shader, Color baseColor, StencilSt /// The material receiving Stencil values. /// The required Stencil values. /// The scenario used in failure diagnostics. - private void ConfigureStencil(Material material, StencilState stencilState, string scenario) + private void ConfigureStencil( + Material material, + StencilState stencilState, + string scenario + ) { RequireStencilProperty(material, "_StencilRef", scenario); RequireStencilProperty(material, "_StencilReadMask", scenario); @@ -621,9 +1034,24 @@ private void ConfigureStencil(Material material, StencilState stencilState, stri /// The product material. /// The required public Stencil property. /// The scenario used in failure diagnostics. - private void RequireStencilProperty(Material material, string propertyName, string scenario) + private void RequireStencilProperty( + Material material, + string propertyName, + string scenario + ) { - Assert.That(material.HasProperty(propertyName), Is.True, "Product shader '" + material.shader.name + "' is missing Stencil ABI property '" + propertyName + "' for " + scenario + ". This is product behavior, not fixture format failure. " + FormatDescription); + Assert.That( + material.HasProperty(propertyName), + Is.True, + "Product shader '" + + material.shader.name + + "' is missing Stencil ABI property '" + + propertyName + + "' for " + + scenario + + ". This is product behavior, not fixture format failure. " + + FormatDescription + ); } /// Clears color, depth, and the exact requested Stencil byte through the owned command buffer. @@ -659,7 +1087,13 @@ private void RenderMaterial(Material material) /// The camera-relative depth of the writer. /// The material drawn in front of the writer. /// The camera-relative depth of the reader. - private void RenderStencilSequence(byte clearStencil, Material rearMaterial, float rearDepth, Material frontMaterial, float frontDepth) + private void RenderStencilSequence( + byte clearStencil, + Material rearMaterial, + float rearDepth, + Material frontMaterial, + float frontDepth + ) { ClearTarget(clearStencil); Renderer writerRenderer = writerObject.GetComponent(); diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.FrameReadbacks.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.FrameReadbacks.cs index f6175ca..bd69b9c 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.FrameReadbacks.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.FrameReadbacks.cs @@ -24,412 +24,505 @@ namespace PureBase.Tests.Daily { - public sealed partial class PureBaseRenderingModeRenderingTests - { - /// Defines finite, threshold, and numeric alpha metrics for the future BIRP mode rendering observations. - [Test] - public void NumericObservationMetricsRejectOpaqueAlphaLeakCutoutLeakAndTransparentDepthOrAddAlphaErrors() - { - Shader shader = RequireProductShader("PureBase/Unlit"); - var opaque = CreateConfiguredMaterial(shader, 0, new Color(0.8f, 0.2f, 0.1f, 0.1f)); - var cutoutBelow = CreateConfiguredMaterial(shader, 1, new Color(0.8f, 0.2f, 0.1f, 0.25f)); - var transparent = CreateConfiguredMaterial(shader, 2, new Color(0.8f, 0.2f, 0.1f, 0.25f)); - { - RequireRenderingModeProperty(opaque); - Color opaquePixel = RenderCenterPixel(opaque, Color.clear); - Color cutoutPixel = RenderCenterPixel(cutoutBelow, Color.clear); - Color transparentPixel = RenderCenterPixel(transparent, Color.clear); - AssertFinite(opaquePixel, "Opaque readback"); - AssertFinite(cutoutPixel, "Cutout readback"); - AssertFinite(transparentPixel, "Transparent readback"); - Assert.That(opaquePixel.a, Is.GreaterThan(0.95f), "Opaque output must ignore base alpha."); - Assert.That(cutoutPixel.a, Is.LessThan(0.02f), "Cutout coverage below _Cutoff must not contribute."); - Assert.That( - transparentPixel.a, - Is.EqualTo(0.0625f).Within(0.005f), - "Transparent source alpha 0.25 over clear destination alpha 0 must use standard alpha blending: 0.25 * 0.25." - ); - } - } + public sealed partial class PureBaseRenderingModeRenderingTests + { + /// Defines finite, threshold, and numeric alpha metrics for the future BIRP mode rendering observations. + [Test] + public void NumericObservationMetricsRejectOpaqueAlphaLeakCutoutLeakAndTransparentDepthOrAddAlphaErrors() + { + Shader shader = RequireProductShader("PureBase/Unlit"); + var opaque = CreateConfiguredMaterial(shader, 0, new Color(0.8f, 0.2f, 0.1f, 0.1f)); + var cutoutBelow = CreateConfiguredMaterial( + shader, + 1, + new Color(0.8f, 0.2f, 0.1f, 0.25f) + ); + var transparent = CreateConfiguredMaterial( + shader, + 2, + new Color(0.8f, 0.2f, 0.1f, 0.25f) + ); + { + RequireRenderingModeProperty(opaque); + Color opaquePixel = RenderCenterPixel(opaque, Color.clear); + Color cutoutPixel = RenderCenterPixel(cutoutBelow, Color.clear); + Color transparentPixel = RenderCenterPixel(transparent, Color.clear); + AssertFinite(opaquePixel, "Opaque readback"); + AssertFinite(cutoutPixel, "Cutout readback"); + AssertFinite(transparentPixel, "Transparent readback"); + Assert.That( + opaquePixel.a, + Is.GreaterThan(0.95f), + "Opaque output must ignore base alpha." + ); + Assert.That( + cutoutPixel.a, + Is.LessThan(0.02f), + "Cutout coverage below _Cutoff must not contribute." + ); + Assert.That( + transparentPixel.a, + Is.EqualTo(0.0625f).Within(0.005f), + "Transparent source alpha 0.25 over clear destination alpha 0 must use standard alpha blending: 0.25 * 0.25." + ); + } + } - /// Requires Transparent material sorting to produce the expected finite back-to-front two-layer readback without depth writes. - [Test] - public void TransparentDepthOrderingUsesBackToFrontCompositionWithoutDepthWrite() - { - Shader shader = RequireProductShader("PureBase/Unlit"); - var red = CreateConfiguredMaterial(shader, 2, new Color(1.0f, 0.0f, 0.0f, 0.25f)); - var blue = CreateConfiguredMaterial(shader, 2, new Color(0.0f, 0.0f, 1.0f, 0.25f)); - { - Color redInFront = RenderLayeredCenterPixel(red, blue); - Color blueInFront = RenderLayeredCenterPixel(blue, red); - AssertFinite(redInFront, "Red-front transparent depth readback"); - AssertFinite(blueInFront, "Blue-front transparent depth readback"); - Assert.That(redInFront.r, Is.EqualTo(0.25f).Within(0.05f)); - Assert.That(redInFront.b, Is.EqualTo(0.1875f).Within(0.05f)); - Assert.That(blueInFront.b, Is.EqualTo(0.25f).Within(0.05f)); - Assert.That(blueInFront.r, Is.EqualTo(0.1875f).Within(0.05f)); - Assert.That(redInFront.r, Is.GreaterThan(redInFront.b + 0.02f)); - Assert.That(blueInFront.b, Is.GreaterThan(blueInFront.r + 0.02f)); - } - } + /// Requires Transparent material sorting to produce the expected finite back-to-front two-layer readback without depth writes. + [Test] + public void TransparentDepthOrderingUsesBackToFrontCompositionWithoutDepthWrite() + { + Shader shader = RequireProductShader("PureBase/Unlit"); + var red = CreateConfiguredMaterial(shader, 2, new Color(1.0f, 0.0f, 0.0f, 0.25f)); + var blue = CreateConfiguredMaterial(shader, 2, new Color(0.0f, 0.0f, 1.0f, 0.25f)); + { + Color redInFront = RenderLayeredCenterPixel(red, blue); + Color blueInFront = RenderLayeredCenterPixel(blue, red); + AssertFinite(redInFront, "Red-front transparent depth readback"); + AssertFinite(blueInFront, "Blue-front transparent depth readback"); + Assert.That(redInFront.r, Is.EqualTo(0.25f).Within(0.05f)); + Assert.That(redInFront.b, Is.EqualTo(0.1875f).Within(0.05f)); + Assert.That(blueInFront.b, Is.EqualTo(0.25f).Within(0.05f)); + Assert.That(blueInFront.r, Is.EqualTo(0.1875f).Within(0.05f)); + Assert.That(redInFront.r, Is.GreaterThan(redInFront.b + 0.02f)); + Assert.That(blueInFront.b, Is.GreaterThan(blueInFront.r + 0.02f)); + } + } - /// Requires Transparent ForwardBase to leave depth unchanged so an explicitly later opaque marker behind it remains visible. - [Test] - public void TransparentDepthWriteDoesNotOccludeAnExplicitlyLaterOpaqueMarker() - { - Shader transparentShader = RequireProductShader("PureBase/Unlit"); - Shader markerShader = Shader.Find("Unlit/Color"); - Assert.That(markerShader, Is.Not.Null, "The Built-in Unlit/Color shader is unavailable for the Transparent depth-write probe."); - var transparent = CreateConfiguredMaterial(transparentShader, 2, new Color(1.0f, 0.0f, 0.0f, 0.25f)); - var marker = CreateMaterial(markerShader); - { - marker.SetColor("_Color", Color.green); - Color observed = RenderTransparentThenOpaqueDepthProbe(transparent, marker); - AssertFinite(observed, "Transparent explicit-depth probe readback"); - Assert.That( - observed.g, - Is.GreaterThan(0.85f), - "Transparent ZWrite Off must allow the explicitly later opaque marker behind the transparent surface to pass depth." - ); - Assert.That( - observed.r, - Is.LessThan(0.08f), - "The later opaque marker must replace the transparent probe color when Transparent does not write depth." - ); - } - } + /// Requires Transparent ForwardBase to leave depth unchanged so an explicitly later opaque marker behind it remains visible. + [Test] + public void TransparentDepthWriteDoesNotOccludeAnExplicitlyLaterOpaqueMarker() + { + Shader transparentShader = RequireProductShader("PureBase/Unlit"); + Shader markerShader = Shader.Find("Unlit/Color"); + Assert.That( + markerShader, + Is.Not.Null, + "The Built-in Unlit/Color shader is unavailable for the Transparent depth-write probe." + ); + var transparent = CreateConfiguredMaterial( + transparentShader, + 2, + new Color(1.0f, 0.0f, 0.0f, 0.25f) + ); + var marker = CreateMaterial(markerShader); + { + marker.SetColor("_Color", Color.green); + Color observed = RenderTransparentThenOpaqueDepthProbe(transparent, marker); + AssertFinite(observed, "Transparent explicit-depth probe readback"); + Assert.That( + observed.g, + Is.GreaterThan(0.85f), + "Transparent ZWrite Off must allow the explicitly later opaque marker behind the transparent surface to pass depth." + ); + Assert.That( + observed.r, + Is.LessThan(0.08f), + "The later opaque marker must replace the transparent probe color when Transparent does not write depth." + ); + } + } - /// Renders an isolated directional-light fixture with and without shadows and returns the measured receiver silhouette. - /// The configured material assigned to the shadow caster. - /// The controlled actual ShadowCaster readback. - private static ShadowReadback RenderShadowReadback(Material material) - { - var fixture = new ShadowReadbackFixture(); - try - { - fixture.Initialize(material); - return fixture.Render(); - } - finally - { - fixture.Dispose(); - } - } + /// Renders an isolated directional-light fixture with and without shadows and returns the measured receiver silhouette. + /// The configured material assigned to the shadow caster. + /// The controlled actual ShadowCaster readback. + private static ShadowReadback RenderShadowReadback(Material material) + { + var fixture = new ShadowReadbackFixture(); + try + { + fixture.Initialize(material); + return fixture.Render(); + } + finally + { + fixture.Dispose(); + } + } - /// Owns the temporary preview-scene resources for one ShadowCaster readback. - private sealed class ShadowReadbackFixture : System.IDisposable - { - private const int FixtureLayer = 31; - private Scene scene; - private GameObject cameraObject; - private GameObject lightObject; - private GameObject receiver; - private GameObject caster; - private Material receiverMaterial; - private RenderTexture renderTexture; - private Texture2D texture; + /// Owns the temporary preview-scene resources for one ShadowCaster readback. + private sealed class ShadowReadbackFixture : System.IDisposable + { + private const int FixtureLayer = 31; + private Scene scene; + private GameObject cameraObject; + private GameObject lightObject; + private GameObject receiver; + private GameObject caster; + private Material receiverMaterial; + private RenderTexture renderTexture; + private Texture2D texture; - /// Initializes an allocation-free ShadowCaster readback fixture. - public ShadowReadbackFixture() - { - } + /// Initializes an allocation-free ShadowCaster readback fixture. + public ShadowReadbackFixture() { } - /// Allocates and configures the ShadowCaster readback fixture. - /// The material assigned to the caster. - public void Initialize(Material material) - { - scene = EditorSceneManager.NewPreviewScene(); - CreateResources(); - MoveObjectsToFixtureScene(); - ConfigureCamera(); - ConfigureLight(); - ConfigureReceiver(); - ConfigureCaster(material); - renderTexture.Create(); - } + /// Allocates and configures the ShadowCaster readback fixture. + /// The material assigned to the caster. + public void Initialize(Material material) + { + scene = EditorSceneManager.NewPreviewScene(); + CreateResources(); + MoveObjectsToFixtureScene(); + ConfigureCamera(); + ConfigureLight(); + ConfigureReceiver(); + ConfigureCaster(material); + renderTexture.Create(); + } - /// Captures the receiver with shadows disabled and enabled. - /// The measured ShadowCaster silhouette. - public ShadowReadback Render() - { - Camera camera = cameraObject.GetComponent(); - Light light = lightObject.GetComponent(); - light.shadows = LightShadows.None; - camera.Render(); - Color[] withoutShadows = ReadPixels(renderTexture, texture); - light.shadows = LightShadows.Hard; - camera.Render(); - Color[] withShadows = ReadPixels(renderTexture, texture); - return AnalyzeShadowReadback(withoutShadows, withShadows); - } + /// Captures the receiver with shadows disabled and enabled. + /// The measured ShadowCaster silhouette. + public ShadowReadback Render() + { + Camera camera = cameraObject.GetComponent(); + Light light = lightObject.GetComponent(); + light.shadows = LightShadows.None; + camera.Render(); + Color[] withoutShadows = ReadPixels(renderTexture, texture); + light.shadows = LightShadows.Hard; + camera.Render(); + Color[] withShadows = ReadPixels(renderTexture, texture); + return AnalyzeShadowReadback(withoutShadows, withShadows); + } - /// Releases every preview-scene resource in its original ownership order. - public void Dispose() - { - Camera camera = cameraObject != null ? cameraObject.GetComponent() : null; - if (camera != null) - camera.targetTexture = null; - if (texture != null) - UnityEngine.Object.DestroyImmediate(texture); - if (renderTexture != null) - { - renderTexture.Release(); - UnityEngine.Object.DestroyImmediate(renderTexture); - } - if (receiverMaterial != null) - UnityEngine.Object.DestroyImmediate(receiverMaterial); - if (caster != null) - UnityEngine.Object.DestroyImmediate(caster); - if (receiver != null) - UnityEngine.Object.DestroyImmediate(receiver); - if (lightObject != null) - UnityEngine.Object.DestroyImmediate(lightObject); - if (cameraObject != null) - UnityEngine.Object.DestroyImmediate(cameraObject); - if (scene.IsValid() && scene.isLoaded) - EditorSceneManager.ClosePreviewScene(scene); - } + /// Releases every preview-scene resource in its original ownership order. + public void Dispose() + { + Camera camera = cameraObject != null ? cameraObject.GetComponent() : null; + if (camera != null) + camera.targetTexture = null; + if (texture != null) + UnityEngine.Object.DestroyImmediate(texture); + if (renderTexture != null) + { + renderTexture.Release(); + UnityEngine.Object.DestroyImmediate(renderTexture); + } + if (receiverMaterial != null) + UnityEngine.Object.DestroyImmediate(receiverMaterial); + if (caster != null) + UnityEngine.Object.DestroyImmediate(caster); + if (receiver != null) + UnityEngine.Object.DestroyImmediate(receiver); + if (lightObject != null) + UnityEngine.Object.DestroyImmediate(lightObject); + if (cameraObject != null) + UnityEngine.Object.DestroyImmediate(cameraObject); + if (scene.IsValid() && scene.isLoaded) + EditorSceneManager.ClosePreviewScene(scene); + } - /// Allocates every temporary Unity resource used by the fixture. - private void CreateResources() - { - cameraObject = new GameObject("PureBaseRenderingModeShadowCamera"); - lightObject = new GameObject("PureBaseRenderingModeShadowLight"); - receiver = GameObject.CreatePrimitive(PrimitiveType.Plane); - caster = GameObject.CreatePrimitive(PrimitiveType.Cube); - Shader receiverShader = Shader.Find("Standard"); - Assert.That(receiverShader, Is.Not.Null, "The Built-in Standard shader is unavailable for ShadowCaster readback."); - receiverMaterial = new Material(receiverShader); - renderTexture = new RenderTexture(RenderSize, RenderSize, 24, RenderTextureFormat.ARGBFloat); - texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBAFloat, false, true); - } + /// Allocates every temporary Unity resource used by the fixture. + private void CreateResources() + { + cameraObject = new GameObject("PureBaseRenderingModeShadowCamera"); + lightObject = new GameObject("PureBaseRenderingModeShadowLight"); + receiver = GameObject.CreatePrimitive(PrimitiveType.Plane); + caster = GameObject.CreatePrimitive(PrimitiveType.Cube); + Shader receiverShader = Shader.Find("Standard"); + Assert.That( + receiverShader, + Is.Not.Null, + "The Built-in Standard shader is unavailable for ShadowCaster readback." + ); + receiverMaterial = new Material(receiverShader); + renderTexture = new RenderTexture( + RenderSize, + RenderSize, + 24, + RenderTextureFormat.ARGBFloat + ); + texture = new Texture2D( + RenderSize, + RenderSize, + TextureFormat.RGBAFloat, + false, + true + ); + } - /// Moves every fixture object into the isolated preview scene and layer. - private void MoveObjectsToFixtureScene() - { - SceneManager.MoveGameObjectToScene(cameraObject, scene); - SceneManager.MoveGameObjectToScene(lightObject, scene); - SceneManager.MoveGameObjectToScene(receiver, scene); - SceneManager.MoveGameObjectToScene(caster, scene); - cameraObject.layer = FixtureLayer; - lightObject.layer = FixtureLayer; - receiver.layer = FixtureLayer; - caster.layer = FixtureLayer; - } + /// Moves every fixture object into the isolated preview scene and layer. + private void MoveObjectsToFixtureScene() + { + SceneManager.MoveGameObjectToScene(cameraObject, scene); + SceneManager.MoveGameObjectToScene(lightObject, scene); + SceneManager.MoveGameObjectToScene(receiver, scene); + SceneManager.MoveGameObjectToScene(caster, scene); + cameraObject.layer = FixtureLayer; + lightObject.layer = FixtureLayer; + receiver.layer = FixtureLayer; + caster.layer = FixtureLayer; + } - /// Configures the isolated ShadowCaster camera. - private void ConfigureCamera() - { - Camera camera = cameraObject.AddComponent(); - camera.enabled = false; - camera.cullingMask = 1 << FixtureLayer; - camera.overrideSceneCullingMask = EditorSceneManager.GetSceneCullingMask(scene); - camera.clearFlags = CameraClearFlags.SolidColor; - camera.backgroundColor = new Color(0.02f, 0.025f, 0.03f, 1.0f); - camera.transform.position = new Vector3(0.0f, 3.0f, -7.0f); - camera.transform.LookAt(new Vector3(0.0f, 0.5f, 0.0f)); - camera.fieldOfView = 45.0f; - camera.targetTexture = renderTexture; - } + /// Configures the isolated ShadowCaster camera. + private void ConfigureCamera() + { + Camera camera = cameraObject.AddComponent(); + camera.enabled = false; + camera.cullingMask = 1 << FixtureLayer; + camera.overrideSceneCullingMask = EditorSceneManager.GetSceneCullingMask(scene); + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = new Color(0.02f, 0.025f, 0.03f, 1.0f); + camera.transform.position = new Vector3(0.0f, 3.0f, -7.0f); + camera.transform.LookAt(new Vector3(0.0f, 0.5f, 0.0f)); + camera.fieldOfView = 45.0f; + camera.targetTexture = renderTexture; + } - /// Configures the directional light used by the ShadowCaster fixture. - private void ConfigureLight() - { - Light light = lightObject.AddComponent(); - light.type = LightType.Directional; - light.intensity = 1.5f; - light.cullingMask = 1 << FixtureLayer; - light.shadows = LightShadows.Hard; - lightObject.transform.rotation = Quaternion.Euler(55.0f, -35.0f, 0.0f); - } + /// Configures the directional light used by the ShadowCaster fixture. + private void ConfigureLight() + { + Light light = lightObject.AddComponent(); + light.type = LightType.Directional; + light.intensity = 1.5f; + light.cullingMask = 1 << FixtureLayer; + light.shadows = LightShadows.Hard; + lightObject.transform.rotation = Quaternion.Euler(55.0f, -35.0f, 0.0f); + } - /// Configures the receiver plane for directional-shadow measurements. - private void ConfigureReceiver() - { - receiver.transform.localScale = Vector3.one * 0.8f; - receiver.GetComponent().sharedMaterial = receiverMaterial; - } + /// Configures the receiver plane for directional-shadow measurements. + private void ConfigureReceiver() + { + receiver.transform.localScale = Vector3.one * 0.8f; + receiver.GetComponent().sharedMaterial = receiverMaterial; + } - /// Configures the measured caster with its effective ShadowCaster state. - /// The source material. - private void ConfigureCaster(Material material) - { - caster.transform.position = new Vector3(0.0f, 1.0f, 0.0f); - MeshRenderer casterRenderer = caster.GetComponent(); - casterRenderer.sharedMaterial = material; - casterRenderer.shadowCastingMode = material.GetShaderPassEnabled("ShadowCaster") - ? ShadowCastingMode.ShadowsOnly - : ShadowCastingMode.Off; - } - } + /// Configures the measured caster with its effective ShadowCaster state. + /// The source material. + private void ConfigureCaster(Material material) + { + caster.transform.position = new Vector3(0.0f, 1.0f, 0.0f); + MeshRenderer casterRenderer = caster.GetComponent(); + casterRenderer.sharedMaterial = material; + casterRenderer.shadowCastingMode = material.GetShaderPassEnabled("ShadowCaster") + ? ShadowCastingMode.ShadowsOnly + : ShadowCastingMode.Off; + } + } - /// Renders the actual Meta pass into a linear target and returns its center pixel without changing persistent assets. - /// The configured source material. - /// The linear Meta center readback. - private static Color RenderMetaCenterPixel(Material material) - { - MetaGlobalState globalState = MetaGlobalState.Capture(); - GameObject cameraObject = null; - GameObject quadObject = null; - RenderTexture renderTexture = null; - Texture2D texture = null; - CommandBuffer commandBuffer = null; - try - { - return RenderMetaReadback(material, out cameraObject, out quadObject, out renderTexture, out texture, out commandBuffer); - } - finally - { - globalState.Restore(); - ReleaseMetaReadbackResources(cameraObject, quadObject, renderTexture, texture, commandBuffer); - } - } + /// Renders the actual Meta pass into a linear target and returns its center pixel without changing persistent assets. + /// The configured source material. + /// The linear Meta center readback. + private static Color RenderMetaCenterPixel(Material material) + { + MetaGlobalState globalState = MetaGlobalState.Capture(); + GameObject cameraObject = null; + GameObject quadObject = null; + RenderTexture renderTexture = null; + Texture2D texture = null; + CommandBuffer commandBuffer = null; + try + { + return RenderMetaReadback( + material, + out cameraObject, + out quadObject, + out renderTexture, + out texture, + out commandBuffer + ); + } + finally + { + globalState.Restore(); + ReleaseMetaReadbackResources( + cameraObject, + quadObject, + renderTexture, + texture, + commandBuffer + ); + } + } - /// Creates and executes the actual Meta-pass command-buffer readback. - private static Color RenderMetaReadback(Material material, out GameObject cameraObject, out GameObject quadObject, out RenderTexture renderTexture, out Texture2D texture, out CommandBuffer commandBuffer) - { - cameraObject = new GameObject("PureBaseRenderingModeMetaCamera"); - quadObject = GameObject.CreatePrimitive(PrimitiveType.Quad); - renderTexture = new RenderTexture(RenderSize, RenderSize, 24, RenderTextureFormat.ARGBFloat); - texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBAFloat, false, true); - commandBuffer = new CommandBuffer { name = "PureBase Rendering Mode Meta Readback" }; - int pass = material.FindPass("Meta"); - Assert.That(pass, Is.GreaterThanOrEqualTo(0), "The material must expose an actual Meta pass."); - Camera camera = cameraObject.AddComponent(); - camera.enabled = false; - camera.cullingMask = 0; - camera.orthographic = true; - camera.orthographicSize = 1.0f; - camera.transform.position = new Vector3(0.0f, 0.0f, -5.0f); - camera.targetTexture = renderTexture; - renderTexture.Create(); - ApplyMetaGlobals(); - commandBuffer.SetRenderTarget(renderTexture); - commandBuffer.ClearRenderTarget(true, true, Color.clear); - if (material.GetShaderPassEnabled("Meta")) - commandBuffer.DrawMesh(quadObject.GetComponent().sharedMesh, Matrix4x4.identity, material, 0, pass); - camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); - camera.Render(); - return ReadCenterPixel(renderTexture, texture); - } + /// Creates and executes the actual Meta-pass command-buffer readback. + private static Color RenderMetaReadback( + Material material, + out GameObject cameraObject, + out GameObject quadObject, + out RenderTexture renderTexture, + out Texture2D texture, + out CommandBuffer commandBuffer + ) + { + cameraObject = new GameObject("PureBaseRenderingModeMetaCamera"); + quadObject = GameObject.CreatePrimitive(PrimitiveType.Quad); + renderTexture = new RenderTexture( + RenderSize, + RenderSize, + 24, + RenderTextureFormat.ARGBFloat + ); + texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBAFloat, false, true); + commandBuffer = new CommandBuffer { name = "PureBase Rendering Mode Meta Readback" }; + int pass = material.FindPass("Meta"); + Assert.That( + pass, + Is.GreaterThanOrEqualTo(0), + "The material must expose an actual Meta pass." + ); + Camera camera = cameraObject.AddComponent(); + camera.enabled = false; + camera.cullingMask = 0; + camera.orthographic = true; + camera.orthographicSize = 1.0f; + camera.transform.position = new Vector3(0.0f, 0.0f, -5.0f); + camera.targetTexture = renderTexture; + renderTexture.Create(); + ApplyMetaGlobals(); + commandBuffer.SetRenderTarget(renderTexture); + commandBuffer.ClearRenderTarget(true, true, Color.clear); + if (material.GetShaderPassEnabled("Meta")) + commandBuffer.DrawMesh( + quadObject.GetComponent().sharedMesh, + Matrix4x4.identity, + material, + 0, + pass + ); + camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); + camera.Render(); + return ReadCenterPixel(renderTexture, texture); + } - /// Sets the Meta pass globals required for the controlled albedo readback. - private static void ApplyMetaGlobals() - { - Shader.SetGlobalVector("unity_MetaVertexControl", new Vector4(1.0f, 0.0f, 0.0f, 0.0f)); - Shader.SetGlobalVector("unity_MetaFragmentControl", new Vector4(1.0f, 0.0f, 0.0f, 0.0f)); - Shader.SetGlobalVector("unity_LightmapST", new Vector4(1.0f, 1.0f, 0.0f, 0.0f)); - Shader.SetGlobalFloat("unity_OneOverOutputBoost", 1.0f); - Shader.SetGlobalFloat("unity_MaxOutputValue", 1.0f); - } + /// Sets the Meta pass globals required for the controlled albedo readback. + private static void ApplyMetaGlobals() + { + Shader.SetGlobalVector("unity_MetaVertexControl", new Vector4(1.0f, 0.0f, 0.0f, 0.0f)); + Shader.SetGlobalVector( + "unity_MetaFragmentControl", + new Vector4(1.0f, 0.0f, 0.0f, 0.0f) + ); + Shader.SetGlobalVector("unity_LightmapST", new Vector4(1.0f, 1.0f, 0.0f, 0.0f)); + Shader.SetGlobalFloat("unity_OneOverOutputBoost", 1.0f); + Shader.SetGlobalFloat("unity_MaxOutputValue", 1.0f); + } - /// Releases the Meta command buffer and transient rendering resources. - private static void ReleaseMetaReadbackResources(GameObject cameraObject, GameObject quadObject, RenderTexture renderTexture, Texture2D texture, CommandBuffer commandBuffer) - { - Camera camera = cameraObject != null ? cameraObject.GetComponent() : null; - if (camera != null && commandBuffer != null) - camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); - if (commandBuffer != null) - commandBuffer.Release(); - ReleaseQuadReadbackResources(cameraObject, quadObject, camera, renderTexture, texture); - } + /// Releases the Meta command buffer and transient rendering resources. + private static void ReleaseMetaReadbackResources( + GameObject cameraObject, + GameObject quadObject, + RenderTexture renderTexture, + Texture2D texture, + CommandBuffer commandBuffer + ) + { + Camera camera = cameraObject != null ? cameraObject.GetComponent() : null; + if (camera != null && commandBuffer != null) + camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); + if (commandBuffer != null) + commandBuffer.Release(); + ReleaseQuadReadbackResources(cameraObject, quadObject, camera, renderTexture, texture); + } - /// Captures the global state modified by one Meta-pass readback. - private sealed class MetaGlobalState - { - private readonly Vector4 vertexControl; - private readonly Vector4 fragmentControl; - private readonly Vector4 lightmapSt; - private readonly float outputBoost; - private readonly float maxOutput; + /// Captures the global state modified by one Meta-pass readback. + private sealed class MetaGlobalState + { + private readonly Vector4 vertexControl; + private readonly Vector4 fragmentControl; + private readonly Vector4 lightmapSt; + private readonly float outputBoost; + private readonly float maxOutput; - public MetaGlobalState(Vector4 vertexControl, Vector4 fragmentControl, Vector4 lightmapSt, float outputBoost, float maxOutput) - { - this.vertexControl = vertexControl; - this.fragmentControl = fragmentControl; - this.lightmapSt = lightmapSt; - this.outputBoost = outputBoost; - this.maxOutput = maxOutput; - } + public MetaGlobalState( + Vector4 vertexControl, + Vector4 fragmentControl, + Vector4 lightmapSt, + float outputBoost, + float maxOutput + ) + { + this.vertexControl = vertexControl; + this.fragmentControl = fragmentControl; + this.lightmapSt = lightmapSt; + this.outputBoost = outputBoost; + this.maxOutput = maxOutput; + } - /// Captures the current Meta globals before the temporary readback mutates them. - /// The state to restore. - public static MetaGlobalState Capture() - { - return new MetaGlobalState( - Shader.GetGlobalVector("unity_MetaVertexControl"), - Shader.GetGlobalVector("unity_MetaFragmentControl"), - Shader.GetGlobalVector("unity_LightmapST"), - Shader.GetGlobalFloat("unity_OneOverOutputBoost"), - Shader.GetGlobalFloat("unity_MaxOutputValue") - ); - } + /// Captures the current Meta globals before the temporary readback mutates them. + /// The state to restore. + public static MetaGlobalState Capture() + { + return new MetaGlobalState( + Shader.GetGlobalVector("unity_MetaVertexControl"), + Shader.GetGlobalVector("unity_MetaFragmentControl"), + Shader.GetGlobalVector("unity_LightmapST"), + Shader.GetGlobalFloat("unity_OneOverOutputBoost"), + Shader.GetGlobalFloat("unity_MaxOutputValue") + ); + } - /// Restores the Meta globals in their original mutation order. - public void Restore() - { - Shader.SetGlobalVector("unity_MetaVertexControl", vertexControl); - Shader.SetGlobalVector("unity_MetaFragmentControl", fragmentControl); - Shader.SetGlobalVector("unity_LightmapST", lightmapSt); - Shader.SetGlobalFloat("unity_OneOverOutputBoost", outputBoost); - Shader.SetGlobalFloat("unity_MaxOutputValue", maxOutput); - } - } + /// Restores the Meta globals in their original mutation order. + public void Restore() + { + Shader.SetGlobalVector("unity_MetaVertexControl", vertexControl); + Shader.SetGlobalVector("unity_MetaFragmentControl", fragmentControl); + Shader.SetGlobalVector("unity_LightmapST", lightmapSt); + Shader.SetGlobalFloat("unity_OneOverOutputBoost", outputBoost); + Shader.SetGlobalFloat("unity_MaxOutputValue", maxOutput); + } + } - /// Reads the center pixel of an already-rendered target without changing the active render target after completion. - /// The source render target. - /// The transient readback texture. - /// The center pixel. - private static Color ReadCenterPixel(RenderTexture renderTexture, Texture2D texture) - { - ReadPixels(renderTexture, texture); - return texture.GetPixel(RenderSize / 2, RenderSize / 2); - } + /// Reads the center pixel of an already-rendered target without changing the active render target after completion. + /// The source render target. + /// The transient readback texture. + /// The center pixel. + private static Color ReadCenterPixel(RenderTexture renderTexture, Texture2D texture) + { + ReadPixels(renderTexture, texture); + return texture.GetPixel(RenderSize / 2, RenderSize / 2); + } - /// Reads every pixel from one target while restoring the previous active render target. - /// The source render target. - /// The transient readback texture. - /// The copied target pixels. - private static Color[] ReadPixels(RenderTexture renderTexture, Texture2D texture) - { - RenderTexture previous = RenderTexture.active; - try - { - RenderTexture.active = renderTexture; - texture.ReadPixels(new Rect(0, 0, RenderSize, RenderSize), 0, 0); - texture.Apply(false, false); - return texture.GetPixels(); - } - finally - { - RenderTexture.active = previous; - } - } + /// Reads every pixel from one target while restoring the previous active render target. + /// The source render target. + /// The transient readback texture. + /// The copied target pixels. + private static Color[] ReadPixels(RenderTexture renderTexture, Texture2D texture) + { + RenderTexture previous = RenderTexture.active; + try + { + RenderTexture.active = renderTexture; + texture.ReadPixels(new Rect(0, 0, RenderSize, RenderSize), 0, 0); + texture.Apply(false, false); + return texture.GetPixels(); + } + finally + { + RenderTexture.active = previous; + } + } - /// Measures the maximum RGB delta and changed-pixel count caused by directional shadows. - /// The unshadowed readback pixels. - /// The shadowed readback pixels. - /// The measured directional-shadow silhouette. - private static ShadowReadback AnalyzeShadowReadback(Color[] withoutShadows, Color[] withShadows) - { - Assert.That(withShadows.Length, Is.EqualTo(withoutShadows.Length), "Directional-shadow readbacks must have matching dimensions."); - var changedPixelCount = 0; - var maxAbsoluteRgbDelta = 0.0f; - for (int index = 0; index < withoutShadows.Length; index++) - { - Color delta = withoutShadows[index] - withShadows[index]; - float maximumAbsoluteDelta = Mathf.Max( - Mathf.Abs(delta.r), - Mathf.Max(Mathf.Abs(delta.g), Mathf.Abs(delta.b)) - ); - if (maximumAbsoluteDelta > ShadowPixelNoiseThreshold) - changedPixelCount++; - if (maximumAbsoluteDelta > maxAbsoluteRgbDelta) - maxAbsoluteRgbDelta = maximumAbsoluteDelta; - } + /// Measures the maximum RGB delta and changed-pixel count caused by directional shadows. + /// The unshadowed readback pixels. + /// The shadowed readback pixels. + /// The measured directional-shadow silhouette. + private static ShadowReadback AnalyzeShadowReadback( + Color[] withoutShadows, + Color[] withShadows + ) + { + Assert.That( + withShadows.Length, + Is.EqualTo(withoutShadows.Length), + "Directional-shadow readbacks must have matching dimensions." + ); + var changedPixelCount = 0; + var maxAbsoluteRgbDelta = 0.0f; + for (int index = 0; index < withoutShadows.Length; index++) + { + Color delta = withoutShadows[index] - withShadows[index]; + float maximumAbsoluteDelta = Mathf.Max( + Mathf.Abs(delta.r), + Mathf.Max(Mathf.Abs(delta.g), Mathf.Abs(delta.b)) + ); + if (maximumAbsoluteDelta > ShadowPixelNoiseThreshold) + changedPixelCount++; + if (maximumAbsoluteDelta > maxAbsoluteRgbDelta) + maxAbsoluteRgbDelta = maximumAbsoluteDelta; + } - return new ShadowReadback(maxAbsoluteRgbDelta, changedPixelCount); - } - } + return new ShadowReadback(maxAbsoluteRgbDelta, changedPixelCount); + } + } } - diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs index 1b63b39..82b40fb 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.SourceContracts.cs @@ -26,296 +26,577 @@ namespace PureBase.Tests.Daily { - public sealed partial class PureBaseRenderingModeRenderingTests - { - /// Identifies the common BIRP fragment host whose ordering is part of the generated-source ABI. - private const string BirpHostPath = "Packages/jp.penguin.purebase/Shaders/Common/birp_host.hlsl"; - - /// Identifies the shared rendering-mode helper that owns mode clip and output-alpha semantics. - private const string RenderingModeHelperPath = "Packages/jp.penguin.purebase/Shaders/Common/rendering_mode.hlsl"; - - /// Identifies the shared operation that publishes the mode-specific output alpha. - private const string RenderingModeOutputAlphaOperation = "PureBaseApplyRenderingModeOutputAlpha"; - - /// Identifies the rendering-mode keyword whose output alpha preserves coverage. - private const string TransparentRenderingModeKeyword = "PUREBASE_RENDERING_TRANSPARENT"; - - /// Identifies the sole allowed generated rendering-mode variant declaration. - private const string ExpectedRenderingModeVariantDeclaration = "#pragma shader_feature_local _ PUREBASE_RENDERING_OPAQUE PUREBASE_RENDERING_TRANSPARENT"; - - /// Identifies the built-in Scene View variant retained by generated product sources. - private const string ExpectedEditorVisualizationVariantDeclaration = "#pragma shader_feature EDITOR_VISUALIZATION"; - - /// Lists the product shaders whose generated BIRP source shares the Stencil pass policy. - private static readonly string[] ProductShaderNames = - { - "PureBase/Unlit", - "PureBase/Toon", - "PureBase/PBR", - "PureBase/Hybrid", - }; - - /// Lists the exact pass ABI retained by every product generated source. - private static readonly string[] ExpectedPassNames = - { - "ForwardBase", - "ForwardAdd", - "ShadowCaster", - "Meta", - }; - - /// Identifies the release-only postpixel alpha probe source. - private const string PostPixelProbePath = "Packages/jp.penguin.purebase/Tests/Release/Modules/Standard/PostPixel/phase_postpixel.hlsl"; - - /// Requires the shared mode-alpha helper to run after add and before fog, postpixel, and return. - [Test] - public void BirpHostPreservesModeAlphaFogPostPixelAndForwardAddSourceOrder() - { - string host = File.ReadAllText(BirpHostPath); - string renderingModeHelper = File.ReadAllText(RenderingModeHelperPath); - int addPhase = RequireIndex(host, "__SC_PHASE_add__"); - Match modeOutputAlphaCall = Regex.Match(host, @"\b" + Regex.Escape(RenderingModeOutputAlphaOperation) + @"\s*\("); - Assert.That(modeOutputAlphaCall.Success, Is.True, "The BIRP host must call the shared rendering-mode output-alpha operation."); - int modeOutputAlpha = modeOutputAlphaCall.Index; - int fog = RequireIndex(host, "UNITY_APPLY_FOG"); - int postPixel = RequireIndex(host, "__SC_PHASE_postpixel__"); - int returnStatement = RequireIndex(host, "return sd.col;"); - StringAssert.Contains("#include \"Packages/jp.penguin.purebase/Shaders/Common/rendering_mode.hlsl\"", host); - Assert.That(modeOutputAlpha, Is.GreaterThan(addPhase), "The shared mode-alpha helper must run after the add phase."); - Assert.That(modeOutputAlpha, Is.LessThan(fog), "The shared mode-alpha helper must run before fog."); - Assert.That(fog, Is.LessThan(postPixel), "Fog must occur before postpixel."); - Assert.That(postPixel, Is.LessThan(returnStatement), "Postpixel must remain the final color mutation point before return."); - StringAssert.Contains(RenderingModeOutputAlphaOperation, renderingModeHelper); - StringAssert.Contains(TransparentRenderingModeKeyword, renderingModeHelper); - StringAssert.Contains("coverage", renderingModeHelper); - StringAssert.Contains(".a", renderingModeHelper); - Assert.That(Regex.IsMatch(renderingModeHelper, @"\b1(?:\.0+)?\b"), Is.True, "The shared helper must distinguish Transparent coverage alpha from Opaque and Cutout alpha one."); - string generatedProductSource = LoadProductSource("PureBase/Toon"); - Assert.That(Regex.IsMatch(generatedProductSource, @"\b" + Regex.Escape(RenderingModeOutputAlphaOperation) + @"\s*\("), Is.True, "The generated product source must retain the shared rendering-mode output-alpha operation."); - StringAssert.Contains("Blend [_AddSrcBlend] [_AddDstBlend]", generatedProductSource); - StringAssert.Contains("ColorMask RGB", generatedProductSource); - StringAssert.Contains("sd.col.a = half(0.25)", File.ReadAllText(PostPixelProbePath)); - } - - /// Requires pass-bounded Stencil policy while preserving the existing pass and rendering-mode keyword ABI. - [Test] - public void ProductGeneratedSourcesExposeStencilPassContractsWithoutNewVariantsOrPasses() - { - foreach (string shaderName in ProductShaderNames) - { - string source = LoadProductSource(shaderName); - AssertExpectedPassNames(source, shaderName); - AssertRenderingModeKeywordContracts(source, shaderName); - AssertNoStencilKeywordsOrPasses(source, shaderName); - - string forwardBasePrefix = ExtractRenderStatePrefix(ExtractNamedPass(source, "ForwardBase"), shaderName, "ForwardBase"); - string forwardAddPrefix = ExtractRenderStatePrefix(ExtractNamedPass(source, "ForwardAdd"), shaderName, "ForwardAdd"); - string shadowCasterPrefix = ExtractRenderStatePrefix(ExtractNamedPass(source, "ShadowCaster"), shaderName, "ShadowCaster"); - string metaPrefix = ExtractRenderStatePrefix(ExtractNamedPass(source, "Meta"), shaderName, "Meta"); - - AssertForwardBaseStencilBlock(forwardBasePrefix, shaderName); - AssertForwardAddStencilBlock(forwardAddPrefix, shaderName); - AssertNoStencilRenderState(shadowCasterPrefix, shaderName, "ShadowCaster"); - AssertNoStencilRenderState(metaPrefix, shaderName, "Meta"); - } - } - - /// Asserts that generated source has exactly the established four named passes. - /// The generated shader source. - /// The product shader name used in diagnostics. - private static void AssertExpectedPassNames(string source, string shaderName) - { - var passNames = new List(); - foreach (Match match in Regex.Matches(source, @"\bName\s+""(?[^""]+)""")) - { - passNames.Add(match.Groups["name"].Value); - } - - CollectionAssert.AreEqual(ExpectedPassNames, passNames, "Product shader '" + shaderName + "' must retain exactly the established pass order."); - } - - /// Asserts the exact rendering-mode variant declaration remains the complete allowed variant set. - /// The generated shader source. - /// The product shader name used in diagnostics. - private static void AssertRenderingModeKeywordContracts(string source, string shaderName) - { - var variantDeclarations = new List(); - foreach (Match declaration in Regex.Matches(source, @"^\s*#pragma\s+(?shader_feature(?:_local)?|multi_compile(?:_local)?)\s+(?[^\r\n]+?)\s*$", RegexOptions.Multiline)) - { - variantDeclarations.Add(Regex.Replace("#pragma " + declaration.Groups["directive"].Value + " " + declaration.Groups["keywords"].Value, @"\s+", " ")); - } - - CollectionAssert.AreEqual( - new[] { ExpectedRenderingModeVariantDeclaration, ExpectedEditorVisualizationVariantDeclaration }, - variantDeclarations, - "Product shader '" + shaderName + "' must retain exactly the established rendering-mode variant declaration without additional variants." - ); - } - - /// Rejects Stencil-specific keyword declarations and named passes without inspecting valid HLSL declarations. - /// The generated shader source. - /// The product shader name used in diagnostics. - private static void AssertNoStencilKeywordsOrPasses(string source, string shaderName) - { - Assert.That( - Regex.IsMatch(source, @"^\s*#pragma\s+[^\r\n]*(?:stencil|_stencil)[^\r\n]*$", RegexOptions.Multiline | RegexOptions.IgnoreCase), - Is.False, - "Product shader '" + shaderName + "' must not declare a Stencil keyword, shader_feature, or multi_compile variant." - ); - } - - /// Extracts one named Pass from its Name marker through the immediate next named Pass. - /// The generated shader source. - /// The required Pass name. - /// The source section belonging only to the requested Pass. - private static string ExtractNamedPass(string source, string passName) - { - MatchCollection names = Regex.Matches(source, @"\bName\s+""(?[^""]+)"""); - for (int index = 0; index < names.Count; index++) - { - Match name = names[index]; - if (!string.Equals(name.Groups["name"].Value, passName, StringComparison.Ordinal)) - { - continue; - } - - int end = index + 1 < names.Count ? names[index + 1].Index : source.Length; - return source.Substring(name.Index, end - name.Index); - } - - Assert.Fail("Generated source did not contain Pass '" + passName + "'."); - return null; - } - - /// Limits render-state assertions to the ShaderLab prefix before the Pass HLSLPROGRAM. - /// The source section for one named Pass. - /// The product shader name used in diagnostics. - /// The Pass name used in diagnostics. - /// The ShaderLab render-state prefix. - private static string ExtractRenderStatePrefix(string passSource, string shaderName, string passName) - { - int hlslProgram = passSource.IndexOf("HLSLPROGRAM", StringComparison.OrdinalIgnoreCase); - Assert.That(hlslProgram, Is.GreaterThanOrEqualTo(0), "Product shader '" + shaderName + "' Pass '" + passName + "' must contain HLSLPROGRAM."); - return passSource.Substring(0, hlslProgram); - } - - /// Asserts that ForwardBase uses the complete seven-property Stencil block. - /// The ForwardBase ShaderLab render-state prefix. - /// The product shader name used in diagnostics. - private static void AssertForwardBaseStencilBlock(string prefix, string shaderName) - { - string body = RequireStencilBody(prefix, shaderName, "ForwardBase"); - const string refDirective = @"\bRef\s*\[\s*_StencilRef\s*\]"; - const string readMaskDirective = @"\bReadMask\s*\[\s*_StencilReadMask\s*\]"; - const string writeMaskDirective = @"\bWriteMask\s*\[\s*_StencilWriteMask\s*\]"; - const string compDirective = @"\bComp\s*\[\s*_StencilComp\s*\]"; - const string passDirective = @"\bPass\s*\[\s*_StencilPass\s*\]"; - const string failDirective = @"\bFail\s*\[\s*_StencilFail\s*\]"; - const string zFailDirective = @"\bZFail\s*\[\s*_StencilZFail\s*\]"; - - AssertStencilDirectiveExactlyOnce(body, refDirective, shaderName, "ForwardBase", "Ref [_StencilRef]"); - AssertStencilDirectiveExactlyOnce(body, readMaskDirective, shaderName, "ForwardBase", "ReadMask [_StencilReadMask]"); - AssertStencilDirectiveExactlyOnce(body, writeMaskDirective, shaderName, "ForwardBase", "WriteMask [_StencilWriteMask]"); - AssertStencilDirectiveExactlyOnce(body, compDirective, shaderName, "ForwardBase", "Comp [_StencilComp]"); - AssertStencilDirectiveExactlyOnce(body, passDirective, shaderName, "ForwardBase", "Pass [_StencilPass]"); - AssertStencilDirectiveExactlyOnce(body, failDirective, shaderName, "ForwardBase", "Fail [_StencilFail]"); - AssertStencilDirectiveExactlyOnce(body, zFailDirective, shaderName, "ForwardBase", "ZFail [_StencilZFail]"); - - string unrecognizedState = Regex.Replace(body, refDirective + "|" + readMaskDirective + "|" + writeMaskDirective + "|" + compDirective + "|" + passDirective + "|" + failDirective + "|" + zFailDirective, string.Empty); - Assert.That(string.IsNullOrWhiteSpace(unrecognizedState), Is.True, "Product shader '" + shaderName + "' ForwardBase must contain only the fixed shared Stencil state directives."); - } - - /// Asserts that ForwardAdd compares the shared Stencil value without writing or repeating operations. - /// The ForwardAdd ShaderLab render-state prefix. - /// The product shader name used in diagnostics. - private static void AssertForwardAddStencilBlock(string prefix, string shaderName) - { - string body = RequireStencilBody(prefix, shaderName, "ForwardAdd"); - const string refDirective = @"\bRef\s*\[\s*_StencilRef\s*\]"; - const string readMaskDirective = @"\bReadMask\s*\[\s*_StencilReadMask\s*\]"; - const string compDirective = @"\bComp\s*\[\s*_StencilComp\s*\]"; - const string writeMaskDirective = @"\bWriteMask\s+0(?:\.0+)?\b"; - const string passDirective = @"\bPass\s+Keep\b"; - const string failDirective = @"\bFail\s+Keep\b"; - const string zFailDirective = @"\bZFail\s+Keep\b"; - - AssertStencilDirectiveExactlyOnce(body, refDirective, shaderName, "ForwardAdd", "Ref [_StencilRef]"); - AssertStencilDirectiveExactlyOnce(body, readMaskDirective, shaderName, "ForwardAdd", "ReadMask [_StencilReadMask]"); - AssertStencilDirectiveExactlyOnce(body, compDirective, shaderName, "ForwardAdd", "Comp [_StencilComp]"); - AssertStencilDirectiveExactlyOnce(body, writeMaskDirective, shaderName, "ForwardAdd", "WriteMask 0"); - AssertStencilDirectiveExactlyOnce(body, passDirective, shaderName, "ForwardAdd", "Pass Keep"); - AssertStencilDirectiveExactlyOnce(body, failDirective, shaderName, "ForwardAdd", "Fail Keep"); - AssertStencilDirectiveExactlyOnce(body, zFailDirective, shaderName, "ForwardAdd", "ZFail Keep"); - - string unrecognizedState = Regex.Replace(body, refDirective + "|" + readMaskDirective + "|" + compDirective + "|" + writeMaskDirective + "|" + passDirective + "|" + failDirective + "|" + zFailDirective, string.Empty); - Assert.That(string.IsNullOrWhiteSpace(unrecognizedState), Is.True, "Product shader '" + shaderName + "' ForwardAdd must contain only the fixed compare-only Stencil state directives."); - } - - /// Requires a single ShaderLab Stencil body in a screen-rendering Pass prefix. - /// The ShaderLab render-state prefix. - /// The product shader name used in diagnostics. - /// The Pass name used in diagnostics. - /// The contents of the Stencil block. - private static string RequireStencilBody(string prefix, string shaderName, string passName) - { - MatchCollection blocks = Regex.Matches(prefix, @"\bStencil\s*\{(?[^{}]*)\}", RegexOptions.Singleline); - Assert.That(blocks.Count, Is.EqualTo(1), "Product shader '" + shaderName + "' Pass '" + passName + "' must contain exactly one bounded Stencil block."); - return blocks[0].Groups["body"].Value; - } - - /// Asserts one fixed Stencil directive and rejects duplicate or alternate state. - /// The bounded Stencil block body. - /// The exact directive pattern. - /// The product shader name used in diagnostics. - /// The Pass name used in diagnostics. - /// The human-readable directive description. - private static void AssertStencilDirectiveExactlyOnce(string body, string pattern, string shaderName, string passName, string description) - { - Assert.That(Regex.Matches(body, pattern).Count, Is.EqualTo(1), "Product shader '" + shaderName + "' " + passName + " must contain exactly one " + description + " directive."); - } - - /// Rejects Stencil blocks and property directives from ShadowCaster and Meta render-state prefixes. - /// The ShaderLab render-state prefix. - /// The product shader name used in diagnostics. - /// The Pass name used in diagnostics. - private static void AssertNoStencilRenderState(string prefix, string shaderName, string passName) - { - Assert.That(Regex.IsMatch(prefix, @"\bStencil\b|_Stencil", RegexOptions.IgnoreCase), Is.False, "Product shader '" + shaderName + "' Pass '" + passName + "' must not apply Stencil before HLSLPROGRAM."); - } - - /// Loads one generated product source subasset without modifying its import state. - /// The product shader name. - /// The generated source text. - private static string LoadProductSource(string shaderName) - { - foreach (string guid in AssetDatabase.FindAssets("t:Shader", new[] { "Packages/jp.penguin.purebase/Shaders" })) - { - string path = AssetDatabase.GUIDToAssetPath(guid); - Shader shader = AssetDatabase.LoadAssetAtPath(path); - if (shader == null || !string.Equals(shader.name, shaderName, StringComparison.Ordinal)) - continue; - foreach (UnityEngine.Object asset in AssetDatabase.LoadAllAssetsAtPath(path)) - { - var source = asset as TextAsset; - if (source != null && source.name == "Shader Source") - return source.text; - } - } - - Assert.Fail("Generated source for product shader '" + shaderName + "' was unavailable."); - return null; - } - - /// Returns one required marker index with a diagnostic that keeps source-order failures local. - /// The source text to inspect. - /// The required marker. - /// The marker index. - private static int RequireIndex(string source, string marker) - { - int index = source.IndexOf(marker, StringComparison.Ordinal); - Assert.That(index, Is.GreaterThanOrEqualTo(0), "Required source marker '" + marker + "' was absent."); - return index; - } - } + public sealed partial class PureBaseRenderingModeRenderingTests + { + /// Identifies the common BIRP fragment host whose ordering is part of the generated-source ABI. + private const string BirpHostPath = + "Packages/jp.penguin.purebase/Shaders/Common/birp_host.hlsl"; + + /// Identifies the shared rendering-mode helper that owns mode clip and output-alpha semantics. + private const string RenderingModeHelperPath = + "Packages/jp.penguin.purebase/Shaders/Common/rendering_mode.hlsl"; + + /// Identifies the shared operation that publishes the mode-specific output alpha. + private const string RenderingModeOutputAlphaOperation = + "PureBaseApplyRenderingModeOutputAlpha"; + + /// Identifies the rendering-mode keyword whose output alpha preserves coverage. + private const string TransparentRenderingModeKeyword = "PUREBASE_RENDERING_TRANSPARENT"; + + /// Identifies the sole allowed generated rendering-mode variant declaration. + private const string ExpectedRenderingModeVariantDeclaration = + "#pragma shader_feature_local _ PUREBASE_RENDERING_OPAQUE PUREBASE_RENDERING_TRANSPARENT"; + + /// Identifies the built-in Scene View variant retained by generated product sources. + private const string ExpectedEditorVisualizationVariantDeclaration = + "#pragma shader_feature EDITOR_VISUALIZATION"; + + /// Lists the product shaders whose generated BIRP source shares the Stencil pass policy. + private static readonly string[] ProductShaderNames = + { + "PureBase/Unlit", + "PureBase/Toon", + "PureBase/PBR", + "PureBase/Hybrid", + }; + + /// Lists the exact pass ABI retained by every product generated source. + private static readonly string[] ExpectedPassNames = + { + "ForwardBase", + "ForwardAdd", + "ShadowCaster", + "Meta", + }; + + /// Identifies the release-only postpixel alpha probe source. + private const string PostPixelProbePath = + "Packages/jp.penguin.purebase/Tests/Release/Modules/Standard/PostPixel/phase_postpixel.hlsl"; + + /// Requires the shared mode-alpha helper to run after add and before fog, postpixel, and return. + [Test] + public void BirpHostPreservesModeAlphaFogPostPixelAndForwardAddSourceOrder() + { + string host = File.ReadAllText(BirpHostPath); + string renderingModeHelper = File.ReadAllText(RenderingModeHelperPath); + int addPhase = RequireIndex(host, "__SC_PHASE_add__"); + Match modeOutputAlphaCall = Regex.Match( + host, + @"\b" + Regex.Escape(RenderingModeOutputAlphaOperation) + @"\s*\(" + ); + Assert.That( + modeOutputAlphaCall.Success, + Is.True, + "The BIRP host must call the shared rendering-mode output-alpha operation." + ); + int modeOutputAlpha = modeOutputAlphaCall.Index; + int fog = RequireIndex(host, "UNITY_APPLY_FOG"); + int postPixel = RequireIndex(host, "__SC_PHASE_postpixel__"); + int returnStatement = RequireIndex(host, "return sd.col;"); + StringAssert.Contains( + "#include \"Packages/jp.penguin.purebase/Shaders/Common/rendering_mode.hlsl\"", + host + ); + Assert.That( + modeOutputAlpha, + Is.GreaterThan(addPhase), + "The shared mode-alpha helper must run after the add phase." + ); + Assert.That( + modeOutputAlpha, + Is.LessThan(fog), + "The shared mode-alpha helper must run before fog." + ); + Assert.That(fog, Is.LessThan(postPixel), "Fog must occur before postpixel."); + Assert.That( + postPixel, + Is.LessThan(returnStatement), + "Postpixel must remain the final color mutation point before return." + ); + StringAssert.Contains(RenderingModeOutputAlphaOperation, renderingModeHelper); + StringAssert.Contains(TransparentRenderingModeKeyword, renderingModeHelper); + StringAssert.Contains("coverage", renderingModeHelper); + StringAssert.Contains(".a", renderingModeHelper); + Assert.That( + Regex.IsMatch(renderingModeHelper, @"\b1(?:\.0+)?\b"), + Is.True, + "The shared helper must distinguish Transparent coverage alpha from Opaque and Cutout alpha one." + ); + string generatedProductSource = LoadProductSource("PureBase/Toon"); + Assert.That( + Regex.IsMatch( + generatedProductSource, + @"\b" + Regex.Escape(RenderingModeOutputAlphaOperation) + @"\s*\(" + ), + Is.True, + "The generated product source must retain the shared rendering-mode output-alpha operation." + ); + StringAssert.Contains("Blend [_AddSrcBlend] [_AddDstBlend]", generatedProductSource); + StringAssert.Contains("ColorMask RGB", generatedProductSource); + StringAssert.Contains("sd.col.a = half(0.25)", File.ReadAllText(PostPixelProbePath)); + } + + /// Requires pass-bounded Stencil policy while preserving the existing pass and rendering-mode keyword ABI. + [Test] + public void ProductGeneratedSourcesExposeStencilPassContractsWithoutNewVariantsOrPasses() + { + foreach (string shaderName in ProductShaderNames) + { + string source = LoadProductSource(shaderName); + AssertExpectedPassNames(source, shaderName); + AssertRenderingModeKeywordContracts(source, shaderName); + AssertNoStencilKeywordsOrPasses(source, shaderName); + + string forwardBasePrefix = ExtractRenderStatePrefix( + ExtractNamedPass(source, "ForwardBase"), + shaderName, + "ForwardBase" + ); + string forwardAddPrefix = ExtractRenderStatePrefix( + ExtractNamedPass(source, "ForwardAdd"), + shaderName, + "ForwardAdd" + ); + string shadowCasterPrefix = ExtractRenderStatePrefix( + ExtractNamedPass(source, "ShadowCaster"), + shaderName, + "ShadowCaster" + ); + string metaPrefix = ExtractRenderStatePrefix( + ExtractNamedPass(source, "Meta"), + shaderName, + "Meta" + ); + + AssertForwardBaseStencilBlock(forwardBasePrefix, shaderName); + AssertForwardAddStencilBlock(forwardAddPrefix, shaderName); + AssertNoStencilRenderState(shadowCasterPrefix, shaderName, "ShadowCaster"); + AssertNoStencilRenderState(metaPrefix, shaderName, "Meta"); + } + } + + /// Asserts that generated source has exactly the established four named passes. + /// The generated shader source. + /// The product shader name used in diagnostics. + private static void AssertExpectedPassNames(string source, string shaderName) + { + var passNames = new List(); + foreach (Match match in Regex.Matches(source, @"\bName\s+""(?[^""]+)""")) + { + passNames.Add(match.Groups["name"].Value); + } + + CollectionAssert.AreEqual( + ExpectedPassNames, + passNames, + "Product shader '" + + shaderName + + "' must retain exactly the established pass order." + ); + } + + /// Asserts the exact rendering-mode variant declaration remains the complete allowed variant set. + /// The generated shader source. + /// The product shader name used in diagnostics. + private static void AssertRenderingModeKeywordContracts(string source, string shaderName) + { + var variantDeclarations = new List(); + foreach ( + Match declaration in Regex.Matches( + source, + @"^\s*#pragma\s+(?shader_feature(?:_local)?|multi_compile(?:_local)?)\s+(?[^\r\n]+?)\s*$", + RegexOptions.Multiline + ) + ) + { + variantDeclarations.Add( + Regex.Replace( + "#pragma " + + declaration.Groups["directive"].Value + + " " + + declaration.Groups["keywords"].Value, + @"\s+", + " " + ) + ); + } + + CollectionAssert.AreEqual( + new[] + { + ExpectedRenderingModeVariantDeclaration, + ExpectedEditorVisualizationVariantDeclaration, + }, + variantDeclarations, + "Product shader '" + + shaderName + + "' must retain exactly the established rendering-mode variant declaration without additional variants." + ); + } + + /// Rejects Stencil-specific keyword declarations and named passes without inspecting valid HLSL declarations. + /// The generated shader source. + /// The product shader name used in diagnostics. + private static void AssertNoStencilKeywordsOrPasses(string source, string shaderName) + { + Assert.That( + Regex.IsMatch( + source, + @"^\s*#pragma\s+[^\r\n]*(?:stencil|_stencil)[^\r\n]*$", + RegexOptions.Multiline | RegexOptions.IgnoreCase + ), + Is.False, + "Product shader '" + + shaderName + + "' must not declare a Stencil keyword, shader_feature, or multi_compile variant." + ); + } + + /// Extracts one named Pass from its Name marker through the immediate next named Pass. + /// The generated shader source. + /// The required Pass name. + /// The source section belonging only to the requested Pass. + private static string ExtractNamedPass(string source, string passName) + { + MatchCollection names = Regex.Matches(source, @"\bName\s+""(?[^""]+)"""); + for (int index = 0; index < names.Count; index++) + { + Match name = names[index]; + if (!string.Equals(name.Groups["name"].Value, passName, StringComparison.Ordinal)) + { + continue; + } + + int end = index + 1 < names.Count ? names[index + 1].Index : source.Length; + return source.Substring(name.Index, end - name.Index); + } + + Assert.Fail("Generated source did not contain Pass '" + passName + "'."); + return null; + } + + /// Limits render-state assertions to the ShaderLab prefix before the Pass HLSLPROGRAM. + /// The source section for one named Pass. + /// The product shader name used in diagnostics. + /// The Pass name used in diagnostics. + /// The ShaderLab render-state prefix. + private static string ExtractRenderStatePrefix( + string passSource, + string shaderName, + string passName + ) + { + int hlslProgram = passSource.IndexOf("HLSLPROGRAM", StringComparison.OrdinalIgnoreCase); + Assert.That( + hlslProgram, + Is.GreaterThanOrEqualTo(0), + "Product shader '" + + shaderName + + "' Pass '" + + passName + + "' must contain HLSLPROGRAM." + ); + return passSource.Substring(0, hlslProgram); + } + + /// Asserts that ForwardBase uses the complete seven-property Stencil block. + /// The ForwardBase ShaderLab render-state prefix. + /// The product shader name used in diagnostics. + private static void AssertForwardBaseStencilBlock(string prefix, string shaderName) + { + string body = RequireStencilBody(prefix, shaderName, "ForwardBase"); + const string refDirective = @"\bRef\s*\[\s*_StencilRef\s*\]"; + const string readMaskDirective = @"\bReadMask\s*\[\s*_StencilReadMask\s*\]"; + const string writeMaskDirective = @"\bWriteMask\s*\[\s*_StencilWriteMask\s*\]"; + const string compDirective = @"\bComp\s*\[\s*_StencilComp\s*\]"; + const string passDirective = @"\bPass\s*\[\s*_StencilPass\s*\]"; + const string failDirective = @"\bFail\s*\[\s*_StencilFail\s*\]"; + const string zFailDirective = @"\bZFail\s*\[\s*_StencilZFail\s*\]"; + + AssertStencilDirectiveExactlyOnce( + body, + refDirective, + shaderName, + "ForwardBase", + "Ref [_StencilRef]" + ); + AssertStencilDirectiveExactlyOnce( + body, + readMaskDirective, + shaderName, + "ForwardBase", + "ReadMask [_StencilReadMask]" + ); + AssertStencilDirectiveExactlyOnce( + body, + writeMaskDirective, + shaderName, + "ForwardBase", + "WriteMask [_StencilWriteMask]" + ); + AssertStencilDirectiveExactlyOnce( + body, + compDirective, + shaderName, + "ForwardBase", + "Comp [_StencilComp]" + ); + AssertStencilDirectiveExactlyOnce( + body, + passDirective, + shaderName, + "ForwardBase", + "Pass [_StencilPass]" + ); + AssertStencilDirectiveExactlyOnce( + body, + failDirective, + shaderName, + "ForwardBase", + "Fail [_StencilFail]" + ); + AssertStencilDirectiveExactlyOnce( + body, + zFailDirective, + shaderName, + "ForwardBase", + "ZFail [_StencilZFail]" + ); + + string unrecognizedState = Regex.Replace( + body, + refDirective + + "|" + + readMaskDirective + + "|" + + writeMaskDirective + + "|" + + compDirective + + "|" + + passDirective + + "|" + + failDirective + + "|" + + zFailDirective, + string.Empty + ); + Assert.That( + string.IsNullOrWhiteSpace(unrecognizedState), + Is.True, + "Product shader '" + + shaderName + + "' ForwardBase must contain only the fixed shared Stencil state directives." + ); + } + + /// Asserts that ForwardAdd compares the shared Stencil value without writing or repeating operations. + /// The ForwardAdd ShaderLab render-state prefix. + /// The product shader name used in diagnostics. + private static void AssertForwardAddStencilBlock(string prefix, string shaderName) + { + string body = RequireStencilBody(prefix, shaderName, "ForwardAdd"); + const string refDirective = @"\bRef\s*\[\s*_StencilRef\s*\]"; + const string readMaskDirective = @"\bReadMask\s*\[\s*_StencilReadMask\s*\]"; + const string compDirective = @"\bComp\s*\[\s*_StencilComp\s*\]"; + const string writeMaskDirective = @"\bWriteMask\s+0(?:\.0+)?\b"; + const string passDirective = @"\bPass\s+Keep\b"; + const string failDirective = @"\bFail\s+Keep\b"; + const string zFailDirective = @"\bZFail\s+Keep\b"; + + AssertStencilDirectiveExactlyOnce( + body, + refDirective, + shaderName, + "ForwardAdd", + "Ref [_StencilRef]" + ); + AssertStencilDirectiveExactlyOnce( + body, + readMaskDirective, + shaderName, + "ForwardAdd", + "ReadMask [_StencilReadMask]" + ); + AssertStencilDirectiveExactlyOnce( + body, + compDirective, + shaderName, + "ForwardAdd", + "Comp [_StencilComp]" + ); + AssertStencilDirectiveExactlyOnce( + body, + writeMaskDirective, + shaderName, + "ForwardAdd", + "WriteMask 0" + ); + AssertStencilDirectiveExactlyOnce( + body, + passDirective, + shaderName, + "ForwardAdd", + "Pass Keep" + ); + AssertStencilDirectiveExactlyOnce( + body, + failDirective, + shaderName, + "ForwardAdd", + "Fail Keep" + ); + AssertStencilDirectiveExactlyOnce( + body, + zFailDirective, + shaderName, + "ForwardAdd", + "ZFail Keep" + ); + + string unrecognizedState = Regex.Replace( + body, + refDirective + + "|" + + readMaskDirective + + "|" + + compDirective + + "|" + + writeMaskDirective + + "|" + + passDirective + + "|" + + failDirective + + "|" + + zFailDirective, + string.Empty + ); + Assert.That( + string.IsNullOrWhiteSpace(unrecognizedState), + Is.True, + "Product shader '" + + shaderName + + "' ForwardAdd must contain only the fixed compare-only Stencil state directives." + ); + } + + /// Requires a single ShaderLab Stencil body in a screen-rendering Pass prefix. + /// The ShaderLab render-state prefix. + /// The product shader name used in diagnostics. + /// The Pass name used in diagnostics. + /// The contents of the Stencil block. + private static string RequireStencilBody(string prefix, string shaderName, string passName) + { + MatchCollection blocks = Regex.Matches( + prefix, + @"\bStencil\s*\{(?[^{}]*)\}", + RegexOptions.Singleline + ); + Assert.That( + blocks.Count, + Is.EqualTo(1), + "Product shader '" + + shaderName + + "' Pass '" + + passName + + "' must contain exactly one bounded Stencil block." + ); + return blocks[0].Groups["body"].Value; + } + + /// Asserts one fixed Stencil directive and rejects duplicate or alternate state. + /// The bounded Stencil block body. + /// The exact directive pattern. + /// The product shader name used in diagnostics. + /// The Pass name used in diagnostics. + /// The human-readable directive description. + private static void AssertStencilDirectiveExactlyOnce( + string body, + string pattern, + string shaderName, + string passName, + string description + ) + { + Assert.That( + Regex.Matches(body, pattern).Count, + Is.EqualTo(1), + "Product shader '" + + shaderName + + "' " + + passName + + " must contain exactly one " + + description + + " directive." + ); + } + + /// Rejects Stencil blocks and property directives from ShadowCaster and Meta render-state prefixes. + /// The ShaderLab render-state prefix. + /// The product shader name used in diagnostics. + /// The Pass name used in diagnostics. + private static void AssertNoStencilRenderState( + string prefix, + string shaderName, + string passName + ) + { + Assert.That( + Regex.IsMatch(prefix, @"\bStencil\b|_Stencil", RegexOptions.IgnoreCase), + Is.False, + "Product shader '" + + shaderName + + "' Pass '" + + passName + + "' must not apply Stencil before HLSLPROGRAM." + ); + } + + /// Loads one generated product source subasset without modifying its import state. + /// The product shader name. + /// The generated source text. + private static string LoadProductSource(string shaderName) + { + foreach ( + string guid in AssetDatabase.FindAssets( + "t:Shader", + new[] { "Packages/jp.penguin.purebase/Shaders" } + ) + ) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + Shader shader = AssetDatabase.LoadAssetAtPath(path); + if ( + shader == null + || !string.Equals(shader.name, shaderName, StringComparison.Ordinal) + ) + continue; + foreach (UnityEngine.Object asset in AssetDatabase.LoadAllAssetsAtPath(path)) + { + var source = asset as TextAsset; + if (source != null && source.name == "Shader Source") + return source.text; + } + } + + Assert.Fail( + "Generated source for product shader '" + shaderName + "' was unavailable." + ); + return null; + } + + /// Returns one required marker index with a diagnostic that keeps source-order failures local. + /// The source text to inspect. + /// The required marker. + /// The marker index. + private static int RequireIndex(string source, string marker) + { + int index = source.IndexOf(marker, StringComparison.Ordinal); + Assert.That( + index, + Is.GreaterThanOrEqualTo(0), + "Required source marker '" + marker + "' was absent." + ); + return index; + } + } } - diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.Stencil.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.Stencil.cs index 0e275c9..353daf0 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.Stencil.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.Stencil.cs @@ -40,22 +40,70 @@ public void D24S8StencilDefaultsRenderIndependentlyOfClearStencilAcrossProductSh shader, new Color(0.8f, 0.6f, 0.4f, 1.0f), 0, - new StencilState(37, 255, 255, CompareFunction.Always, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep), + new StencilState( + 37, + 255, + 255, + CompareFunction.Always, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ), 0 ); Color nonzeroClear = fixture.RenderSingle( shader, new Color(0.8f, 0.6f, 0.4f, 1.0f), 203, - new StencilState(37, 255, 255, CompareFunction.Always, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep), + new StencilState( + 37, + 255, + 255, + CompareFunction.Always, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ), 0 ); - AssertFinite(zeroClear, shaderName + " default-Always clear-0 " + fixture.FormatDescription); - AssertFinite(nonzeroClear, shaderName + " default-Always clear-203 " + fixture.FormatDescription); - Assert.That(RgbMagnitude(zeroClear), Is.GreaterThan(0.05f), shaderName + " default Always+Keep must draw over clear stencil 0. " + fixture.FormatDescription + " Pixel=" + zeroClear); - Assert.That(RgbMagnitude(nonzeroClear), Is.GreaterThan(0.05f), shaderName + " default Always+Keep must draw over clear stencil 203. " + fixture.FormatDescription + " Pixel=" + nonzeroClear); - Assert.That(RgbMagnitude(zeroClear - nonzeroClear), Is.LessThan(0.02f), shaderName + " default Always+Keep must not depend on the cleared stencil value. " + fixture.FormatDescription + " Clear0=" + zeroClear + " Clear203=" + nonzeroClear); + AssertFinite( + zeroClear, + shaderName + " default-Always clear-0 " + fixture.FormatDescription + ); + AssertFinite( + nonzeroClear, + shaderName + " default-Always clear-203 " + fixture.FormatDescription + ); + Assert.That( + RgbMagnitude(zeroClear), + Is.GreaterThan(0.05f), + shaderName + + " default Always+Keep must draw over clear stencil 0. " + + fixture.FormatDescription + + " Pixel=" + + zeroClear + ); + Assert.That( + RgbMagnitude(nonzeroClear), + Is.GreaterThan(0.05f), + shaderName + + " default Always+Keep must draw over clear stencil 203. " + + fixture.FormatDescription + + " Pixel=" + + nonzeroClear + ); + Assert.That( + RgbMagnitude(zeroClear - nonzeroClear), + Is.LessThan(0.02f), + shaderName + + " default Always+Keep must not depend on the cleared stencil value. " + + fixture.FormatDescription + + " Clear0=" + + zeroClear + + " Clear203=" + + nonzeroClear + ); } finally { @@ -68,9 +116,33 @@ public void D24S8StencilDefaultsRenderIndependentlyOfClearStencilAcrossProductSh [Test] public void D24S8StencilForwardBaseReplaceControlsEqualAndMismatchedReadersAcrossProductShaders() { - var writerState = new StencilState(60, 255, 255, CompareFunction.Always, StencilOp.Replace, StencilOp.Keep, StencilOp.Keep); - var matchingReaderState = new StencilState(60, 255, 0, CompareFunction.Equal, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); - var mismatchedReaderState = new StencilState(61, 255, 0, CompareFunction.Equal, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); + var writerState = new StencilState( + 60, + 255, + 255, + CompareFunction.Always, + StencilOp.Replace, + StencilOp.Keep, + StencilOp.Keep + ); + var matchingReaderState = new StencilState( + 60, + 255, + 0, + CompareFunction.Equal, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ); + var mismatchedReaderState = new StencilState( + 61, + 255, + 0, + CompareFunction.Equal, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ); foreach (string shaderName in ProductShaderNames) { var fixture = new D24S8StencilFixture(); @@ -101,10 +173,38 @@ out matchingWriter out mismatchedWriter ); - AssertFinite(matching, shaderName + " Replace/Equal matching reader " + fixture.FormatDescription); - AssertFinite(mismatched, shaderName + " Replace/Equal mismatched reader " + fixture.FormatDescription); - Assert.That(RgbMagnitude(matching), Is.GreaterThan(0.05f), shaderName + " Replace writer followed by Equal reader with ref 60 must render. " + fixture.FormatDescription + " Pixel=" + matching); - Assert.That(RgbMagnitude(mismatched - mismatchedWriter), Is.LessThan(RgbMagnitude(matching - matchingWriter) * 0.2f), shaderName + " Replace writer followed by Equal reader with ref 61 must reject without adding reader color beyond the black-writer baseline. " + fixture.FormatDescription + " Matching=" + matching + " MatchingWriter=" + matchingWriter + " Mismatched=" + mismatched + " MismatchedWriter=" + mismatchedWriter); + AssertFinite( + matching, + shaderName + " Replace/Equal matching reader " + fixture.FormatDescription + ); + AssertFinite( + mismatched, + shaderName + " Replace/Equal mismatched reader " + fixture.FormatDescription + ); + Assert.That( + RgbMagnitude(matching), + Is.GreaterThan(0.05f), + shaderName + + " Replace writer followed by Equal reader with ref 60 must render. " + + fixture.FormatDescription + + " Pixel=" + + matching + ); + Assert.That( + RgbMagnitude(mismatched - mismatchedWriter), + Is.LessThan(RgbMagnitude(matching - matchingWriter) * 0.2f), + shaderName + + " Replace writer followed by Equal reader with ref 61 must reject without adding reader color beyond the black-writer baseline. " + + fixture.FormatDescription + + " Matching=" + + matching + + " MatchingWriter=" + + matchingWriter + + " Mismatched=" + + mismatched + + " MismatchedWriter=" + + mismatchedWriter + ); } finally { @@ -117,9 +217,33 @@ out mismatchedWriter [Test] public void D24S8StencilHonorsPartialReadAndWriteMasksAcrossProductShaders() { - var writerState = new StencilState(0x12, 255, 0x0f, CompareFunction.Always, StencilOp.Replace, StencilOp.Keep, StencilOp.Keep); - var matchingReaderState = new StencilState(0xf2, 0x0f, 0, CompareFunction.Equal, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); - var mismatchedReaderState = new StencilState(0xf1, 0x0f, 0, CompareFunction.Equal, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); + var writerState = new StencilState( + 0x12, + 255, + 0x0f, + CompareFunction.Always, + StencilOp.Replace, + StencilOp.Keep, + StencilOp.Keep + ); + var matchingReaderState = new StencilState( + 0xf2, + 0x0f, + 0, + CompareFunction.Equal, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ); + var mismatchedReaderState = new StencilState( + 0xf1, + 0x0f, + 0, + CompareFunction.Equal, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ); foreach (string shaderName in ProductShaderNames) { var fixture = new D24S8StencilFixture(); @@ -150,10 +274,38 @@ out matchingWriter out mismatchedWriter ); - AssertFinite(matching, shaderName + " partial-mask matching reader " + fixture.FormatDescription); - AssertFinite(mismatched, shaderName + " partial-mask mismatched reader " + fixture.FormatDescription); - Assert.That(RgbMagnitude(matching), Is.GreaterThan(0.05f), shaderName + " WriteMask 0x0f must write low bits that ReadMask 0x0f can match. " + fixture.FormatDescription + " Pixel=" + matching); - Assert.That(RgbMagnitude(mismatched - mismatchedWriter), Is.LessThan(RgbMagnitude(matching - matchingWriter) * 0.2f), shaderName + " ReadMask 0x0f must reject a mismatched low-bit reference without adding reader color beyond the black-writer baseline. " + fixture.FormatDescription + " Matching=" + matching + " MatchingWriter=" + matchingWriter + " Mismatched=" + mismatched + " MismatchedWriter=" + mismatchedWriter); + AssertFinite( + matching, + shaderName + " partial-mask matching reader " + fixture.FormatDescription + ); + AssertFinite( + mismatched, + shaderName + " partial-mask mismatched reader " + fixture.FormatDescription + ); + Assert.That( + RgbMagnitude(matching), + Is.GreaterThan(0.05f), + shaderName + + " WriteMask 0x0f must write low bits that ReadMask 0x0f can match. " + + fixture.FormatDescription + + " Pixel=" + + matching + ); + Assert.That( + RgbMagnitude(mismatched - mismatchedWriter), + Is.LessThan(RgbMagnitude(matching - matchingWriter) * 0.2f), + shaderName + + " ReadMask 0x0f must reject a mismatched low-bit reference without adding reader color beyond the black-writer baseline. " + + fixture.FormatDescription + + " Matching=" + + matching + + " MatchingWriter=" + + matchingWriter + + " Mismatched=" + + mismatched + + " MismatchedWriter=" + + mismatchedWriter + ); } finally { @@ -167,30 +319,127 @@ out mismatchedWriter public void D24S8StencilToonForwardAddRecomparesPostBaseStencilWithoutWriting() { Shader toon = RequireProductShader("PureBase/Toon"); - var equalKeep = new StencilState(0, 255, 255, CompareFunction.Equal, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); - var notEqualReplace = new StencilState(1, 255, 255, CompareFunction.NotEqual, StencilOp.Replace, StencilOp.Keep, StencilOp.Keep); + var equalKeep = new StencilState( + 0, + 255, + 255, + CompareFunction.Equal, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ); + var notEqualReplace = new StencilState( + 1, + 255, + 255, + CompareFunction.NotEqual, + StencilOp.Replace, + StencilOp.Keep, + StencilOp.Keep + ); var fixture = new ToonForwardAddScope(); try { fixture.Initialize(); - var alwaysKeep = new StencilState(0, 255, 255, CompareFunction.Always, StencilOp.Keep, StencilOp.Keep, StencilOp.Keep); + var alwaysKeep = new StencilState( + 0, + 255, + 255, + CompareFunction.Always, + StencilOp.Keep, + StencilOp.Keep, + StencilOp.Keep + ); Color alwaysKeepOneLight = fixture.RenderToonComposite(toon, 0, alwaysKeep, 1); - AssertFinite(alwaysKeepOneLight, "Toon Always+Keep one-light " + fixture.FormatDescription); - Assert.That(RgbMagnitude(alwaysKeepOneLight), Is.GreaterThan(0.05f), "Toon Always+Keep transparent one-light control must render before testing ForwardAdd Stencil recompare. " + fixture.FormatDescription + " Pixel=" + alwaysKeepOneLight); + AssertFinite( + alwaysKeepOneLight, + "Toon Always+Keep one-light " + fixture.FormatDescription + ); + Assert.That( + RgbMagnitude(alwaysKeepOneLight), + Is.GreaterThan(0.05f), + "Toon Always+Keep transparent one-light control must render before testing ForwardAdd Stencil recompare. " + + fixture.FormatDescription + + " Pixel=" + + alwaysKeepOneLight + ); Color equalKeepOneLight = fixture.RenderToonComposite(toon, 0, equalKeep, 1); Color equalKeepTwoLights = fixture.RenderToonComposite(toon, 0, equalKeep, 2); float retainedAddDelta = RgbMagnitude(equalKeepTwoLights - equalKeepOneLight); - AssertFinite(equalKeepOneLight, "Toon Equal+Keep one-light " + fixture.FormatDescription); - AssertFinite(equalKeepTwoLights, "Toon Equal+Keep two-light " + fixture.FormatDescription); - Assert.That(retainedAddDelta, Is.GreaterThan(0.01f), "Toon two-light control must expose a measurable ForwardAdd contribution before testing Stencil recompare. " + fixture.FormatDescription + " OneLight=" + equalKeepOneLight + " TwoLights=" + equalKeepTwoLights + " Delta=" + retainedAddDelta); - Color notEqualReplaceOneLight = fixture.RenderToonComposite(toon, 0, notEqualReplace, 1); - Color notEqualReplaceTwoLights = fixture.RenderToonComposite(toon, 0, notEqualReplace, 2); - float rejectedAddDelta = RgbMagnitude(notEqualReplaceTwoLights - notEqualReplaceOneLight); - - AssertFinite(notEqualReplaceOneLight, "Toon NotEqual+Replace one-light " + fixture.FormatDescription); - AssertFinite(notEqualReplaceTwoLights, "Toon NotEqual+Replace two-light " + fixture.FormatDescription); - Assert.That(rejectedAddDelta, Is.LessThan(retainedAddDelta * 0.2f), "Toon NotEqual+Replace must make ForwardAdd recompare the post-ForwardBase value and reject its add contribution. " + fixture.FormatDescription + " OneLight=" + notEqualReplaceOneLight + " TwoLights=" + notEqualReplaceTwoLights + " RetainedDelta=" + retainedAddDelta + " RejectedDelta=" + rejectedAddDelta); - TestContext.Progress.WriteLine("Toon D24S8 controls: AlwaysKeepOneLight=" + alwaysKeepOneLight + " EqualKeepOneLight=" + equalKeepOneLight + " EqualKeepTwoLights=" + equalKeepTwoLights + " NotEqualReplaceOneLight=" + notEqualReplaceOneLight + " NotEqualReplaceTwoLights=" + notEqualReplaceTwoLights + " RetainedAddDelta=" + retainedAddDelta + " RejectedAddDelta=" + rejectedAddDelta); + AssertFinite( + equalKeepOneLight, + "Toon Equal+Keep one-light " + fixture.FormatDescription + ); + AssertFinite( + equalKeepTwoLights, + "Toon Equal+Keep two-light " + fixture.FormatDescription + ); + Assert.That( + retainedAddDelta, + Is.GreaterThan(0.01f), + "Toon two-light control must expose a measurable ForwardAdd contribution before testing Stencil recompare. " + + fixture.FormatDescription + + " OneLight=" + + equalKeepOneLight + + " TwoLights=" + + equalKeepTwoLights + + " Delta=" + + retainedAddDelta + ); + Color notEqualReplaceOneLight = fixture.RenderToonComposite( + toon, + 0, + notEqualReplace, + 1 + ); + Color notEqualReplaceTwoLights = fixture.RenderToonComposite( + toon, + 0, + notEqualReplace, + 2 + ); + float rejectedAddDelta = RgbMagnitude( + notEqualReplaceTwoLights - notEqualReplaceOneLight + ); + + AssertFinite( + notEqualReplaceOneLight, + "Toon NotEqual+Replace one-light " + fixture.FormatDescription + ); + AssertFinite( + notEqualReplaceTwoLights, + "Toon NotEqual+Replace two-light " + fixture.FormatDescription + ); + Assert.That( + rejectedAddDelta, + Is.LessThan(retainedAddDelta * 0.2f), + "Toon NotEqual+Replace must make ForwardAdd recompare the post-ForwardBase value and reject its add contribution. " + + fixture.FormatDescription + + " OneLight=" + + notEqualReplaceOneLight + + " TwoLights=" + + notEqualReplaceTwoLights + + " RetainedDelta=" + + retainedAddDelta + + " RejectedDelta=" + + rejectedAddDelta + ); + TestContext.Progress.WriteLine( + "Toon D24S8 controls: AlwaysKeepOneLight=" + + alwaysKeepOneLight + + " EqualKeepOneLight=" + + equalKeepOneLight + + " EqualKeepTwoLights=" + + equalKeepTwoLights + + " NotEqualReplaceOneLight=" + + notEqualReplaceOneLight + + " NotEqualReplaceTwoLights=" + + notEqualReplaceTwoLights + + " RetainedAddDelta=" + + retainedAddDelta + + " RejectedAddDelta=" + + rejectedAddDelta + ); } finally { @@ -209,7 +458,15 @@ private sealed class StencilState /// The operation when Stencil and depth tests pass. /// The operation when the Stencil test fails. /// The operation when the depth test fails. - public StencilState(byte referenceValue, byte readMask, byte writeMask, CompareFunction comparison, StencilOp passOperation, StencilOp failOperation, StencilOp depthFailOperation) + public StencilState( + byte referenceValue, + byte readMask, + byte writeMask, + CompareFunction comparison, + StencilOp passOperation, + StencilOp failOperation, + StencilOp depthFailOperation + ) { this.referenceValue = referenceValue; this.readMask = readMask; @@ -241,6 +498,5 @@ public StencilState(byte referenceValue, byte readMask, byte writeMask, CompareF /// Stores the Stencil operation when the depth test fails. public readonly StencilOp depthFailOperation; } - } } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.ToonForwardAddScope.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.ToonForwardAddScope.cs index d86fd8e..07e6194 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.ToonForwardAddScope.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.ToonForwardAddScope.cs @@ -101,12 +101,17 @@ public string FormatDescription { get { - Camera camera = cameraObject == null ? null : cameraObject.GetComponent(); - return D24S8StencilFixture.DescribeTarget(renderTexture) + - " RequestedRenderingPath=" + (camera == null ? "" : camera.renderingPath.ToString()) + - " ActualRenderingPath=" + (camera == null ? "" : camera.actualRenderingPath.ToString()) + - " PixelLightCount=" + QualitySettings.pixelLightCount + - " FixtureLayer=" + fixtureLayer; + Camera camera = + cameraObject == null ? null : cameraObject.GetComponent(); + return D24S8StencilFixture.DescribeTarget(renderTexture) + + " RequestedRenderingPath=" + + (camera == null ? "" : camera.renderingPath.ToString()) + + " ActualRenderingPath=" + + (camera == null ? "" : camera.actualRenderingPath.ToString()) + + " PixelLightCount=" + + QualitySettings.pixelLightCount + + " FixtureLayer=" + + fixtureLayer; } } @@ -115,10 +120,16 @@ public void Initialize() { CaptureSharedState(); fixtureLayer = FindUnusedLayer(); - renderTexture = D24S8StencilFixture.LoadAndCreateD24S8RenderTextureAssetForToonScope(out createdRenderTextureResource); + renderTexture = + D24S8StencilFixture.LoadAndCreateD24S8RenderTextureAssetForToonScope( + out createdRenderTextureResource + ); commandBuffer = new CommandBuffer { name = "PureBase Toon ForwardAdd D24S8 Clear" }; cameraObject = CreateHiddenObject("PureBaseToonForwardAddCamera"); - texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBA32, false, true) { hideFlags = HideFlags.HideAndDontSave }; + texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBA32, false, true) + { + hideFlags = HideFlags.HideAndDontSave, + }; quadObject = GameObject.CreatePrimitive(PrimitiveType.Quad); quadObject.hideFlags = HideFlags.HideAndDontSave; quadObject.layer = fixtureLayer; @@ -135,7 +146,11 @@ public void Initialize() camera.clearFlags = CameraClearFlags.Nothing; camera.transform.position = new Vector3(0.0f, 0.0f, -2.0f); camera.targetTexture = renderTexture; - Assert.That(camera.renderingPath, Is.EqualTo(RenderingPath.Forward), "The Toon D24S8 scope must request the BIRP Forward camera path."); + Assert.That( + camera.renderingPath, + Is.EqualTo(RenderingPath.Forward), + "The Toon D24S8 scope must request the BIRP Forward camera path." + ); } /// Renders one transparent Toon material with the requested isolated directional-light count. @@ -144,7 +159,12 @@ public void Initialize() /// The explicit Stencil configuration. /// The number of ForcePixel directional lights. /// The rendered center pixel. - public Color RenderToonComposite(Shader shader, byte clearStencil, StencilState stencilState, int lightCount) + public Color RenderToonComposite( + Shader shader, + byte clearStencil, + StencilState stencilState, + int lightCount + ) { SetLightCount(lightCount); Material material = CreateToonMaterial(shader, stencilState); @@ -156,7 +176,12 @@ public Color RenderToonComposite(Shader shader, byte clearStencil, StencilState try { camera.Render(); - Assert.That(camera.actualRenderingPath, Is.EqualTo(RenderingPath.Forward), "The Toon D24S8 scope must actually use the BIRP Forward camera path. " + FormatDescription); + Assert.That( + camera.actualRenderingPath, + Is.EqualTo(RenderingPath.Forward), + "The Toon D24S8 scope must actually use the BIRP Forward camera path. " + + FormatDescription + ); return ReadCenterPixel(renderTexture, texture); } finally @@ -267,11 +292,23 @@ private void ReleaseCommandBuffer() /// Verifies that every temporary Unity object was destroyed. private void AssertTemporaryObjectsDestroyed() { - Assert.That(quadObject == null, Is.True, "The Toon D24S8 scope must destroy its temporary quad."); - Assert.That(cameraObject == null, Is.True, "The Toon D24S8 scope must destroy its temporary camera."); + Assert.That( + quadObject == null, + Is.True, + "The Toon D24S8 scope must destroy its temporary quad." + ); + Assert.That( + cameraObject == null, + Is.True, + "The Toon D24S8 scope must destroy its temporary camera." + ); foreach (GameObject lightObject in lightObjects) { - Assert.That(lightObject == null, Is.True, "The Toon D24S8 scope must destroy every temporary directional light."); + Assert.That( + lightObject == null, + Is.True, + "The Toon D24S8 scope must destroy every temporary directional light." + ); } } @@ -303,24 +340,48 @@ private void CaptureSharedState() /// Restores the active scene and verifies every loaded-scene dirty state remained unchanged. private void RestoreSceneState() { - Assert.That(SceneManager.sceneCount, Is.EqualTo(sceneCount), "The Toon D24S8 scope must not add or remove loaded scenes."); + Assert.That( + SceneManager.sceneCount, + Is.EqualTo(sceneCount), + "The Toon D24S8 scope must not add or remove loaded scenes." + ); if (activeScene.IsValid() && activeScene.isLoaded) { if (SceneManager.GetActiveScene() != activeScene) { - Assert.That(SceneManager.SetActiveScene(activeScene), Is.True, "The Toon D24S8 scope must restore the original active scene."); + Assert.That( + SceneManager.SetActiveScene(activeScene), + Is.True, + "The Toon D24S8 scope must restore the original active scene." + ); } - Assert.That(SceneManager.GetActiveScene(), Is.EqualTo(activeScene), "The Toon D24S8 scope must preserve the original active scene."); + Assert.That( + SceneManager.GetActiveScene(), + Is.EqualTo(activeScene), + "The Toon D24S8 scope must preserve the original active scene." + ); } foreach (SceneState state in sceneStates) { - Assert.That(state.scene.isLoaded, Is.True, "The Toon D24S8 scope must keep every initially loaded scene loaded."); + Assert.That( + state.scene.isLoaded, + Is.True, + "The Toon D24S8 scope must keep every initially loaded scene loaded." + ); if (state.isDirty) { - Assert.That(EditorSceneManager.MarkSceneDirty(state.scene), Is.True, "The Toon D24S8 scope must restore initially dirty scene state."); + Assert.That( + EditorSceneManager.MarkSceneDirty(state.scene), + Is.True, + "The Toon D24S8 scope must restore initially dirty scene state." + ); } - Assert.That(state.scene.isDirty, Is.EqualTo(state.isDirty), "The Toon D24S8 scope must preserve clean and dirty scene states."); + Assert.That( + state.scene.isDirty, + Is.EqualTo(state.isDirty), + "The Toon D24S8 scope must preserve clean and dirty scene states." + ); } } @@ -336,7 +397,9 @@ private static int FindUnusedLayer() } } - Assert.Fail("The Toon D24S8 scope requires one unused user layer to exclude existing scene rendering."); + Assert.Fail( + "The Toon D24S8 scope requires one unused user layer to exclude existing scene rendering." + ); return 0; } @@ -400,13 +463,19 @@ private GameObject CreateHiddenObject(string name) /// The required light count. private void SetLightCount(int lightCount) { - Assert.That(lightCount, Is.GreaterThanOrEqualTo(1), "The Toon D24S8 scope requires at least one directional light."); + Assert.That( + lightCount, + Is.GreaterThanOrEqualTo(1), + "The Toon D24S8 scope requires at least one directional light." + ); DestroyLights(); QualitySettings.pixelLightCount = Math.Max(2, pixelLightCount); int cullingMask = 1 << fixtureLayer; for (int index = 0; index < lightCount; index++) { - GameObject lightObject = CreateHiddenObject("PureBaseToonForwardAddLight" + index); + GameObject lightObject = CreateHiddenObject( + "PureBaseToonForwardAddLight" + index + ); lightObjects.Add(lightObject); Light light = lightObject.AddComponent(); light.type = LightType.Directional; @@ -414,10 +483,18 @@ private void SetLightCount(int lightCount) light.color = Color.white; light.intensity = 1.0f; light.cullingMask = cullingMask; - lightObject.transform.rotation = Quaternion.Euler(30.0f, index == 0 ? -30.0f : 30.0f, 0.0f); + lightObject.transform.rotation = Quaternion.Euler( + 30.0f, + index == 0 ? -30.0f : 30.0f, + 0.0f + ); } - Assert.That(QualitySettings.pixelLightCount, Is.GreaterThanOrEqualTo(2), "The Toon D24S8 scope must allow at least two pixel lights."); + Assert.That( + QualitySettings.pixelLightCount, + Is.GreaterThanOrEqualTo(2), + "The Toon D24S8 scope must allow at least two pixel lights." + ); } /// Destroys all temporary directional lights in reverse creation order. @@ -437,12 +514,28 @@ private void DestroyLights() /// The caller-owned configured material. private Material CreateToonMaterial(Shader shader, StencilState stencilState) { - Assert.That(shader, Is.Not.Null, "The Toon shader is required for the D24S8 ForwardAdd scope."); + Assert.That( + shader, + Is.Not.Null, + "The Toon shader is required for the D24S8 ForwardAdd scope." + ); var material = new Material(shader) { hideFlags = HideFlags.HideAndDontSave }; materials.Add(material); - Assert.That(material.HasProperty("_BaseColor"), Is.True, "Toon must expose _BaseColor for the D24S8 ForwardAdd scope."); - Assert.That(material.HasProperty("_Cutoff"), Is.True, "Toon must expose _Cutoff for the D24S8 ForwardAdd scope."); - Assert.That(material.HasProperty("_NormalMap"), Is.True, "Toon must expose _NormalMap for the D24S8 ForwardAdd scope."); + Assert.That( + material.HasProperty("_BaseColor"), + Is.True, + "Toon must expose _BaseColor for the D24S8 ForwardAdd scope." + ); + Assert.That( + material.HasProperty("_Cutoff"), + Is.True, + "Toon must expose _Cutoff for the D24S8 ForwardAdd scope." + ); + Assert.That( + material.HasProperty("_NormalMap"), + Is.True, + "Toon must expose _NormalMap for the D24S8 ForwardAdd scope." + ); material.SetTexture("_BaseTexture", Texture2D.whiteTexture); material.SetColor("_BaseColor", new Color(0.8f, 0.6f, 0.4f, 0.5f)); material.SetFloat("_Cutoff", 0.5f); @@ -458,10 +551,26 @@ private Material CreateToonMaterial(Shader shader, StencilState stencilState) /// The requested Stencil configuration. private void ConfigureStencil(Material material, StencilState stencilState) { - string[] properties = { "_StencilRef", "_StencilReadMask", "_StencilWriteMask", "_StencilComp", "_StencilPass", "_StencilFail", "_StencilZFail" }; + string[] properties = + { + "_StencilRef", + "_StencilReadMask", + "_StencilWriteMask", + "_StencilComp", + "_StencilPass", + "_StencilFail", + "_StencilZFail", + }; foreach (string property in properties) { - Assert.That(material.HasProperty(property), Is.True, "Toon is missing Stencil ABI property '" + property + "' for the D24S8 ForwardAdd scope. " + FormatDescription); + Assert.That( + material.HasProperty(property), + Is.True, + "Toon is missing Stencil ABI property '" + + property + + "' for the D24S8 ForwardAdd scope. " + + FormatDescription + ); } material.SetFloat("_StencilRef", stencilState.referenceValue); material.SetFloat("_StencilReadMask", stencilState.readMask); @@ -478,7 +587,12 @@ private void ClearTarget(byte clearStencil) { commandBuffer.Clear(); commandBuffer.SetRenderTarget(renderTexture); - commandBuffer.ClearRenderTarget(RTClearFlags.All, new Color(0.0f, 0.0f, 0.0f, 0.6f), 1.0f, clearStencil); + commandBuffer.ClearRenderTarget( + RTClearFlags.All, + new Color(0.0f, 0.0f, 0.0f, 0.6f), + 1.0f, + clearStencil + ); Graphics.ExecuteCommandBuffer(commandBuffer); } } diff --git a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.cs b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.cs index 05d760f..ef1767e 100644 --- a/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.cs +++ b/Tests/Daily/Editor/PureBaseRenderingModeRenderingTests.cs @@ -62,7 +62,10 @@ public void RepresentativeModesHaveNumericAlphaDepthAndContributionObservationPr Assert.That(opaque.GetFloat("_ZWrite"), Is.EqualTo(1.0f)); Assert.That(cutout.GetFloat("_ZWrite"), Is.EqualTo(1.0f)); Assert.That(transparent.GetFloat("_ZWrite"), Is.EqualTo(0.0f)); - Assert.That(transparent.GetFloat("_AddSrcBlend"), Is.EqualTo((float)BlendMode.SrcAlpha)); + Assert.That( + transparent.GetFloat("_AddSrcBlend"), + Is.EqualTo((float)BlendMode.SrcAlpha) + ); Assert.That(transparentToon.GetShaderPassEnabled("ShadowCaster"), Is.False); Assert.That(transparentToon.GetShaderPassEnabled("Meta"), Is.False); } @@ -76,8 +79,16 @@ public void OpaqueCutoutAndTransparentModesHaveObservedShadowCasterAndMetaContri Color contributingBaseColor = new Color(0.8f, 0.2f, 0.1f, 1.0f); var opaque = CreateConfiguredMaterial(shader, 0, contributingBaseColor); var cutout = CreateConfiguredMaterial(shader, 1, contributingBaseColor); - var cutoutBelow = CreateConfiguredMaterial(shader, 1, new Color(0.8f, 0.2f, 0.1f, 0.25f)); - var transparent = CreateConfiguredMaterial(shader, 2, new Color(0.8f, 0.2f, 0.1f, 0.25f)); + var cutoutBelow = CreateConfiguredMaterial( + shader, + 1, + new Color(0.8f, 0.2f, 0.1f, 0.25f) + ); + var transparent = CreateConfiguredMaterial( + shader, + 2, + new Color(0.8f, 0.2f, 0.1f, 0.25f) + ); { AssertShadowContributions(opaque, cutout, cutoutBelow, transparent); AssertMetaContributions(opaque, cutout, transparent, contributingBaseColor.linear); @@ -85,70 +96,200 @@ public void OpaqueCutoutAndTransparentModesHaveObservedShadowCasterAndMetaContri } /// Asserts ShadowCaster enablement and measured contribution boundaries for all rendering modes. - private static void AssertShadowContributions(Material opaque, Material cutout, Material cutoutBelow, Material transparent) - { - Assert.That(opaque.GetShaderPassEnabled("ShadowCaster"), Is.True, "Opaque ShadowCaster must be enabled before its silhouette is observed."); - Assert.That(cutout.GetShaderPassEnabled("ShadowCaster"), Is.True, "Cutout ShadowCaster must be enabled before its silhouette is observed."); - Assert.That(cutoutBelow.GetShaderPassEnabled("ShadowCaster"), Is.True, "Cutout below-cutoff ShadowCaster must remain enabled so clip behavior is observed at runtime."); - Assert.That(transparent.GetShaderPassEnabled("ShadowCaster"), Is.False, "Transparent ShadowCaster must be disabled before its missing silhouette is observed."); + private static void AssertShadowContributions( + Material opaque, + Material cutout, + Material cutoutBelow, + Material transparent + ) + { + Assert.That( + opaque.GetShaderPassEnabled("ShadowCaster"), + Is.True, + "Opaque ShadowCaster must be enabled before its silhouette is observed." + ); + Assert.That( + cutout.GetShaderPassEnabled("ShadowCaster"), + Is.True, + "Cutout ShadowCaster must be enabled before its silhouette is observed." + ); + Assert.That( + cutoutBelow.GetShaderPassEnabled("ShadowCaster"), + Is.True, + "Cutout below-cutoff ShadowCaster must remain enabled so clip behavior is observed at runtime." + ); + Assert.That( + transparent.GetShaderPassEnabled("ShadowCaster"), + Is.False, + "Transparent ShadowCaster must be disabled before its missing silhouette is observed." + ); ShadowReadback opaqueShadow = RenderShadowReadback(opaque); ShadowReadback cutoutShadow = RenderShadowReadback(cutout); ShadowReadback cutoutBelowShadow = RenderShadowReadback(cutoutBelow); ShadowReadback transparentShadow = RenderShadowReadback(transparent); AssertFinite(opaqueShadow.maxAbsoluteRgbDelta, "Opaque ShadowCaster maximum RGB delta"); AssertFinite(cutoutShadow.maxAbsoluteRgbDelta, "Cutout ShadowCaster maximum RGB delta"); - AssertFinite(cutoutBelowShadow.maxAbsoluteRgbDelta, "Cutout below-cutoff ShadowCaster maximum RGB delta"); - AssertFinite(transparentShadow.maxAbsoluteRgbDelta, "Transparent ShadowCaster maximum RGB delta"); + AssertFinite( + cutoutBelowShadow.maxAbsoluteRgbDelta, + "Cutout below-cutoff ShadowCaster maximum RGB delta" + ); + AssertFinite( + transparentShadow.maxAbsoluteRgbDelta, + "Transparent ShadowCaster maximum RGB delta" + ); AssertContributingShadowReadbacks(opaqueShadow, cutoutShadow); - AssertNoncontributingShadowReadbacks(opaqueShadow, cutoutShadow, cutoutBelowShadow, transparentShadow); + AssertNoncontributingShadowReadbacks( + opaqueShadow, + cutoutShadow, + cutoutBelowShadow, + transparentShadow + ); } /// Asserts that Opaque and Cutout ShadowCaster measurements retain meaningful silhouettes. - private static void AssertContributingShadowReadbacks(ShadowReadback opaqueShadow, ShadowReadback cutoutShadow) - { - Assert.That(opaqueShadow.maxAbsoluteRgbDelta, Is.GreaterThan(ShadowPixelNoiseThreshold), opaqueShadow.Describe("Opaque")); - Assert.That(opaqueShadow.changedPixelCount, Is.GreaterThan(MinimumShadowSilhouettePixelCount), opaqueShadow.Describe("Opaque")); - Assert.That(cutoutShadow.maxAbsoluteRgbDelta, Is.GreaterThan(ShadowPixelNoiseThreshold), cutoutShadow.Describe("Cutout")); - Assert.That(cutoutShadow.changedPixelCount, Is.GreaterThan(MinimumShadowSilhouettePixelCount), cutoutShadow.Describe("Cutout")); - Assert.That(cutoutShadow.maxAbsoluteRgbDelta, Is.GreaterThan(opaqueShadow.maxAbsoluteRgbDelta * 0.25f), cutoutShadow.Describe("Cutout") + " must retain a visible silhouette relative to Opaque."); - Assert.That(cutoutShadow.changedPixelCount, Is.GreaterThan(opaqueShadow.changedPixelCount * 0.25f), cutoutShadow.Describe("Cutout") + " must retain sufficient changed pixels relative to Opaque."); + private static void AssertContributingShadowReadbacks( + ShadowReadback opaqueShadow, + ShadowReadback cutoutShadow + ) + { + Assert.That( + opaqueShadow.maxAbsoluteRgbDelta, + Is.GreaterThan(ShadowPixelNoiseThreshold), + opaqueShadow.Describe("Opaque") + ); + Assert.That( + opaqueShadow.changedPixelCount, + Is.GreaterThan(MinimumShadowSilhouettePixelCount), + opaqueShadow.Describe("Opaque") + ); + Assert.That( + cutoutShadow.maxAbsoluteRgbDelta, + Is.GreaterThan(ShadowPixelNoiseThreshold), + cutoutShadow.Describe("Cutout") + ); + Assert.That( + cutoutShadow.changedPixelCount, + Is.GreaterThan(MinimumShadowSilhouettePixelCount), + cutoutShadow.Describe("Cutout") + ); + Assert.That( + cutoutShadow.maxAbsoluteRgbDelta, + Is.GreaterThan(opaqueShadow.maxAbsoluteRgbDelta * 0.25f), + cutoutShadow.Describe("Cutout") + + " must retain a visible silhouette relative to Opaque." + ); + Assert.That( + cutoutShadow.changedPixelCount, + Is.GreaterThan(opaqueShadow.changedPixelCount * 0.25f), + cutoutShadow.Describe("Cutout") + + " must retain sufficient changed pixels relative to Opaque." + ); } /// Asserts that below-cutoff and Transparent ShadowCaster measurements remain noncontributing. - private static void AssertNoncontributingShadowReadbacks(ShadowReadback opaqueShadow, ShadowReadback cutoutShadow, ShadowReadback cutoutBelowShadow, ShadowReadback transparentShadow) - { - Assert.That(cutoutBelowShadow.maxAbsoluteRgbDelta, Is.LessThanOrEqualTo(ShadowPixelNoiseThreshold), cutoutBelowShadow.Describe("Cutout below cutoff")); - Assert.That(cutoutBelowShadow.changedPixelCount, Is.LessThanOrEqualTo(MinimumShadowSilhouettePixelCount), cutoutBelowShadow.Describe("Cutout below cutoff")); - Assert.That(transparentShadow.maxAbsoluteRgbDelta, Is.LessThanOrEqualTo(ShadowPixelNoiseThreshold), transparentShadow.Describe("Transparent")); - Assert.That(transparentShadow.changedPixelCount, Is.LessThanOrEqualTo(MinimumShadowSilhouettePixelCount), transparentShadow.Describe("Transparent")); - float minimumContributingShadowDelta = Mathf.Min(opaqueShadow.maxAbsoluteRgbDelta, cutoutShadow.maxAbsoluteRgbDelta); - int minimumContributingShadowPixels = Mathf.Min(opaqueShadow.changedPixelCount, cutoutShadow.changedPixelCount); - Assert.That(transparentShadow.maxAbsoluteRgbDelta, Is.LessThan(minimumContributingShadowDelta * 0.25f), transparentShadow.Describe("Transparent") + " must remain below the Opaque and Cutout contribution boundary."); - Assert.That(transparentShadow.changedPixelCount, Is.LessThan(minimumContributingShadowPixels * 0.25f), transparentShadow.Describe("Transparent") + " must remain below the Opaque and Cutout changed-pixel contribution boundary."); + private static void AssertNoncontributingShadowReadbacks( + ShadowReadback opaqueShadow, + ShadowReadback cutoutShadow, + ShadowReadback cutoutBelowShadow, + ShadowReadback transparentShadow + ) + { + Assert.That( + cutoutBelowShadow.maxAbsoluteRgbDelta, + Is.LessThanOrEqualTo(ShadowPixelNoiseThreshold), + cutoutBelowShadow.Describe("Cutout below cutoff") + ); + Assert.That( + cutoutBelowShadow.changedPixelCount, + Is.LessThanOrEqualTo(MinimumShadowSilhouettePixelCount), + cutoutBelowShadow.Describe("Cutout below cutoff") + ); + Assert.That( + transparentShadow.maxAbsoluteRgbDelta, + Is.LessThanOrEqualTo(ShadowPixelNoiseThreshold), + transparentShadow.Describe("Transparent") + ); + Assert.That( + transparentShadow.changedPixelCount, + Is.LessThanOrEqualTo(MinimumShadowSilhouettePixelCount), + transparentShadow.Describe("Transparent") + ); + float minimumContributingShadowDelta = Mathf.Min( + opaqueShadow.maxAbsoluteRgbDelta, + cutoutShadow.maxAbsoluteRgbDelta + ); + int minimumContributingShadowPixels = Mathf.Min( + opaqueShadow.changedPixelCount, + cutoutShadow.changedPixelCount + ); + Assert.That( + transparentShadow.maxAbsoluteRgbDelta, + Is.LessThan(minimumContributingShadowDelta * 0.25f), + transparentShadow.Describe("Transparent") + + " must remain below the Opaque and Cutout contribution boundary." + ); + Assert.That( + transparentShadow.changedPixelCount, + Is.LessThan(minimumContributingShadowPixels * 0.25f), + transparentShadow.Describe("Transparent") + + " must remain below the Opaque and Cutout changed-pixel contribution boundary." + ); } /// Asserts Meta readback contribution boundaries for Opaque, Cutout, and Transparent materials. - private static void AssertMetaContributions(Material opaque, Material cutout, Material transparent, Color expectedContributingMeta) - { - float opaqueMetaMagnitude = AssertContributingMeta(RenderMetaCenterPixel(opaque), expectedContributingMeta, "Opaque"); - float cutoutMetaMagnitude = AssertContributingMeta(RenderMetaCenterPixel(cutout), expectedContributingMeta, "Cutout"); + private static void AssertMetaContributions( + Material opaque, + Material cutout, + Material transparent, + Color expectedContributingMeta + ) + { + float opaqueMetaMagnitude = AssertContributingMeta( + RenderMetaCenterPixel(opaque), + expectedContributingMeta, + "Opaque" + ); + float cutoutMetaMagnitude = AssertContributingMeta( + RenderMetaCenterPixel(cutout), + expectedContributingMeta, + "Cutout" + ); Color transparentMeta = RenderMetaCenterPixel(transparent); AssertFinite(transparentMeta, "Transparent Meta readback"); float transparentMetaMagnitude = RgbMagnitude(transparentMeta); - Assert.That(transparentMetaMagnitude, Is.LessThan(0.02f), "Transparent Meta must not contribute effective albedo data in the actual BIRP readback."); - float minimumContributingMetaMagnitude = Mathf.Min(opaqueMetaMagnitude, cutoutMetaMagnitude); - Assert.That(transparentMetaMagnitude, Is.LessThan(minimumContributingMetaMagnitude * 0.25f), "Transparent Meta must remain below the Opaque and Cutout contribution boundary."); + Assert.That( + transparentMetaMagnitude, + Is.LessThan(0.02f), + "Transparent Meta must not contribute effective albedo data in the actual BIRP readback." + ); + float minimumContributingMetaMagnitude = Mathf.Min( + opaqueMetaMagnitude, + cutoutMetaMagnitude + ); + Assert.That( + transparentMetaMagnitude, + Is.LessThan(minimumContributingMetaMagnitude * 0.25f), + "Transparent Meta must remain below the Opaque and Cutout contribution boundary." + ); } /// Asserts one Meta contribution's expected linear albedo and returns its RGB magnitude. - private static float AssertContributingMeta(Color observedMeta, Color expectedMeta, string label) + private static float AssertContributingMeta( + Color observedMeta, + Color expectedMeta, + string label + ) { AssertFinite(observedMeta, label + " Meta readback"); Assert.That(observedMeta.r, Is.EqualTo(expectedMeta.r).Within(0.08f)); Assert.That(observedMeta.g, Is.EqualTo(expectedMeta.g).Within(0.08f)); Assert.That(observedMeta.b, Is.EqualTo(expectedMeta.b).Within(0.08f)); float magnitude = RgbMagnitude(observedMeta); - Assert.That(magnitude, Is.GreaterThan(0.2f), label + " Meta pass must contribute non-clear albedo data."); + Assert.That( + magnitude, + Is.GreaterThan(0.2f), + label + " Meta pass must contribute non-clear albedo data." + ); return magnitude; } @@ -157,8 +298,16 @@ private static float AssertContributingMeta(Color observedMeta, Color expectedMe public void TransparentToonForwardAddAccumulatesRgbBySourceAlphaWithoutChangingDestinationAlpha() { Shader toon = RequireProductShader("PureBase/Toon"); - var lowAlphaMaterial = CreateConfiguredMaterial(toon, 2, new Color(0.8f, 0.6f, 0.4f, 0.25f)); - var highAlphaMaterial = CreateConfiguredMaterial(toon, 2, new Color(0.8f, 0.6f, 0.4f, 0.5f)); + var lowAlphaMaterial = CreateConfiguredMaterial( + toon, + 2, + new Color(0.8f, 0.6f, 0.4f, 0.25f) + ); + var highAlphaMaterial = CreateConfiguredMaterial( + toon, + 2, + new Color(0.8f, 0.6f, 0.4f, 0.5f) + ); { Color oneLowAlphaLight = RenderTransparentToonPixel(lowAlphaMaterial, 1); Color twoLowAlphaLights = RenderTransparentToonPixel(lowAlphaMaterial, 2); @@ -209,12 +358,28 @@ public void TransparentToonForwardAddAccumulatesRgbBySourceAlphaWithoutChangingD [Test] public void TransparentMaterialsHaveNoEffectiveShadowCasterOrMetaContribution() { - foreach (string shaderName in new[] { "PureBase/Unlit", "PureBase/Toon", "PureBase/PBR", "PureBase/Hybrid" }) + foreach ( + string shaderName in new[] + { + "PureBase/Unlit", + "PureBase/Toon", + "PureBase/PBR", + "PureBase/Hybrid", + } + ) { var material = CreateMaterial(RequireProductShader(shaderName)); ConfigureMode(material, 2); - Assert.That(material.GetShaderPassEnabled("ShadowCaster"), Is.False, shaderName + " Transparent ShadowCaster contribution."); - Assert.That(material.GetShaderPassEnabled("Meta"), Is.False, shaderName + " Transparent Meta contribution."); + Assert.That( + material.GetShaderPassEnabled("ShadowCaster"), + Is.False, + shaderName + " Transparent ShadowCaster contribution." + ); + Assert.That( + material.GetShaderPassEnabled("Meta"), + Is.False, + shaderName + " Transparent Meta contribution." + ); } } @@ -262,9 +427,23 @@ private static void ConfigureMode(Material material, int mode) { material.SetInteger("_RenderingMode", mode); Type type = FindLoadedType("PureBase.Editor.PureBaseMaterialRenderingMode"); - Assert.That(type, Is.Not.Null, "PureBaseMaterialRenderingMode is required for rendering observations."); - var apply = type.GetMethod("Apply", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new[] { typeof(Material) }, null); - Assert.That(apply, Is.Not.Null, "PureBaseMaterialRenderingMode.Apply(Material) is required for rendering observations."); + Assert.That( + type, + Is.Not.Null, + "PureBaseMaterialRenderingMode is required for rendering observations." + ); + var apply = type.GetMethod( + "Apply", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, + null, + new[] { typeof(Material) }, + null + ); + Assert.That( + apply, + Is.Not.Null, + "PureBaseMaterialRenderingMode.Apply(Material) is required for rendering observations." + ); apply.Invoke(null, new object[] { material }); } @@ -283,8 +462,19 @@ private static Color RenderCenterPixel(Material material, Color background) { cameraObject = new GameObject("PureBaseRenderingModeCamera"); quadObject = GameObject.CreatePrimitive(PrimitiveType.Quad); - renderTexture = new RenderTexture(RenderSize, RenderSize, 24, RenderTextureFormat.ARGBFloat); - texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBAFloat, false, true); + renderTexture = new RenderTexture( + RenderSize, + RenderSize, + 24, + RenderTextureFormat.ARGBFloat + ); + texture = new Texture2D( + RenderSize, + RenderSize, + TextureFormat.RGBAFloat, + false, + true + ); camera = cameraObject.AddComponent(); ConfigureCenterPixelCamera(camera, renderTexture, background); quadObject.GetComponent().sharedMaterial = material; @@ -293,12 +483,22 @@ private static Color RenderCenterPixel(Material material, Color background) } finally { - ReleaseQuadReadbackResources(cameraObject, quadObject, camera, renderTexture, texture); + ReleaseQuadReadbackResources( + cameraObject, + quadObject, + camera, + renderTexture, + texture + ); } } /// Configures the temporary camera used for one center-pixel readback. - private static void ConfigureCenterPixelCamera(Camera camera, RenderTexture renderTexture, Color background) + private static void ConfigureCenterPixelCamera( + Camera camera, + RenderTexture renderTexture, + Color background + ) { camera.orthographic = true; camera.orthographicSize = 0.5f; @@ -309,7 +509,13 @@ private static void ConfigureCenterPixelCamera(Camera camera, RenderTexture rend } /// Releases one temporary quad readback fixture in its original ownership order. - private static void ReleaseQuadReadbackResources(GameObject cameraObject, GameObject quadObject, Camera camera, RenderTexture renderTexture, Texture2D texture) + private static void ReleaseQuadReadbackResources( + GameObject cameraObject, + GameObject quadObject, + Camera camera, + RenderTexture renderTexture, + Texture2D texture + ) { if (camera != null) camera.targetTexture = null; @@ -342,8 +548,19 @@ private static Color RenderLayeredCenterPixel(Material frontMaterial, Material r cameraObject = new GameObject("PureBaseRenderingModeDepthCamera"); frontObject = GameObject.CreatePrimitive(PrimitiveType.Quad); rearObject = GameObject.CreatePrimitive(PrimitiveType.Quad); - renderTexture = new RenderTexture(RenderSize, RenderSize, 24, RenderTextureFormat.ARGBFloat); - texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBAFloat, false, true); + renderTexture = new RenderTexture( + RenderSize, + RenderSize, + 24, + RenderTextureFormat.ARGBFloat + ); + texture = new Texture2D( + RenderSize, + RenderSize, + TextureFormat.RGBAFloat, + false, + true + ); Camera camera = cameraObject.AddComponent(); camera.orthographic = true; camera.orthographicSize = 0.5f; @@ -383,7 +600,10 @@ private static Color RenderLayeredCenterPixel(Material frontMaterial, Material r /// The configured Transparent material drawn first. /// The opaque marker material drawn after Transparent. /// The center pixel after the controlled explicit draw order. - private static Color RenderTransparentThenOpaqueDepthProbe(Material transparentMaterial, Material markerMaterial) + private static Color RenderTransparentThenOpaqueDepthProbe( + Material transparentMaterial, + Material markerMaterial + ) { GameObject cameraObject = null; GameObject quadObject = null; @@ -395,24 +615,49 @@ private static Color RenderTransparentThenOpaqueDepthProbe(Material transparentM { cameraObject = new GameObject("PureBaseRenderingModeExplicitDepthCamera"); quadObject = GameObject.CreatePrimitive(PrimitiveType.Quad); - renderTexture = new RenderTexture(RenderSize, RenderSize, 24, RenderTextureFormat.ARGBFloat); - texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBAFloat, false, true); + renderTexture = new RenderTexture( + RenderSize, + RenderSize, + 24, + RenderTextureFormat.ARGBFloat + ); + texture = new Texture2D( + RenderSize, + RenderSize, + TextureFormat.RGBAFloat, + false, + true + ); camera = cameraObject.AddComponent(); ConfigureExplicitDepthProbeCamera(camera, renderTexture); renderTexture.Create(); - commandBuffer = CreateExplicitDepthProbeCommandBuffer(quadObject, transparentMaterial, markerMaterial); + commandBuffer = CreateExplicitDepthProbeCommandBuffer( + quadObject, + transparentMaterial, + markerMaterial + ); camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); camera.Render(); return ReadCenterPixel(renderTexture, texture); } finally { - ReleaseExplicitDepthProbeResources(cameraObject, quadObject, camera, commandBuffer, renderTexture, texture); + ReleaseExplicitDepthProbeResources( + cameraObject, + quadObject, + camera, + commandBuffer, + renderTexture, + texture + ); } } /// Configures the camera used by the explicit ForwardBase depth probe. - private static void ConfigureExplicitDepthProbeCamera(Camera camera, RenderTexture renderTexture) + private static void ConfigureExplicitDepthProbeCamera( + Camera camera, + RenderTexture renderTexture + ) { camera.enabled = false; camera.cullingMask = 0; @@ -425,19 +670,49 @@ private static void ConfigureExplicitDepthProbeCamera(Camera camera, RenderTextu } /// Creates the command buffer that draws Transparent before the farther opaque marker. - private static CommandBuffer CreateExplicitDepthProbeCommandBuffer(GameObject quadObject, Material transparentMaterial, Material markerMaterial) + private static CommandBuffer CreateExplicitDepthProbeCommandBuffer( + GameObject quadObject, + Material transparentMaterial, + Material markerMaterial + ) { int transparentPass = transparentMaterial.FindPass("ForwardBase"); - Assert.That(transparentPass, Is.GreaterThanOrEqualTo(0), "The Transparent depth probe requires ForwardBase."); - var commandBuffer = new CommandBuffer { name = "PureBase Rendering Mode Explicit Depth Probe" }; + Assert.That( + transparentPass, + Is.GreaterThanOrEqualTo(0), + "The Transparent depth probe requires ForwardBase." + ); + var commandBuffer = new CommandBuffer + { + name = "PureBase Rendering Mode Explicit Depth Probe", + }; Mesh quadMesh = quadObject.GetComponent().sharedMesh; - commandBuffer.DrawMesh(quadMesh, Matrix4x4.identity, transparentMaterial, 0, transparentPass); - commandBuffer.DrawMesh(quadMesh, Matrix4x4.Translate(new Vector3(0.0f, 0.0f, 0.1f)), markerMaterial, 0, 0); + commandBuffer.DrawMesh( + quadMesh, + Matrix4x4.identity, + transparentMaterial, + 0, + transparentPass + ); + commandBuffer.DrawMesh( + quadMesh, + Matrix4x4.Translate(new Vector3(0.0f, 0.0f, 0.1f)), + markerMaterial, + 0, + 0 + ); return commandBuffer; } /// Releases the explicit depth probe command buffer and transient render resources. - private static void ReleaseExplicitDepthProbeResources(GameObject cameraObject, GameObject quadObject, Camera camera, CommandBuffer commandBuffer, RenderTexture renderTexture, Texture2D texture) + private static void ReleaseExplicitDepthProbeResources( + GameObject cameraObject, + GameObject quadObject, + Camera camera, + CommandBuffer commandBuffer, + RenderTexture renderTexture, + Texture2D texture + ) { if (camera != null && commandBuffer != null) camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); @@ -464,8 +739,19 @@ private static Color RenderTransparentToonPixel(Material material, int lightCoun { cameraObject = new GameObject("PureBaseRenderingModeToonCamera"); quadObject = GameObject.CreatePrimitive(PrimitiveType.Quad); - renderTexture = new RenderTexture(RenderSize, RenderSize, 24, RenderTextureFormat.ARGBFloat); - texture = new Texture2D(RenderSize, RenderSize, TextureFormat.RGBAFloat, false, true); + renderTexture = new RenderTexture( + RenderSize, + RenderSize, + 24, + RenderTextureFormat.ARGBFloat + ); + texture = new Texture2D( + RenderSize, + RenderSize, + TextureFormat.RGBAFloat, + false, + true + ); camera = cameraObject.AddComponent(); ConfigureTransparentToonCamera(camera, renderTexture, cullingMask); quadObject.layer = renderingLayer; @@ -476,12 +762,23 @@ private static Color RenderTransparentToonPixel(Material material, int lightCoun } finally { - ReleaseTransparentToonResources(lightObjects, cameraObject, quadObject, camera, renderTexture, texture); + ReleaseTransparentToonResources( + lightObjects, + cameraObject, + quadObject, + camera, + renderTexture, + texture + ); } } /// Configures the temporary camera used for Transparent Toon light accumulation. - private static void ConfigureTransparentToonCamera(Camera camera, RenderTexture renderTexture, int cullingMask) + private static void ConfigureTransparentToonCamera( + Camera camera, + RenderTexture renderTexture, + int cullingMask + ) { camera.orthographic = true; camera.orthographicSize = 0.5f; @@ -493,7 +790,12 @@ private static void ConfigureTransparentToonCamera(Camera camera, RenderTexture } /// Creates the directional lights used to isolate ForwardAdd alpha behavior. - private static void CreateTransparentToonLights(List lightObjects, int lightCount, int renderingLayer, int cullingMask) + private static void CreateTransparentToonLights( + List lightObjects, + int lightCount, + int renderingLayer, + int cullingMask + ) { for (int index = 0; index < lightCount; index++) { @@ -505,12 +807,23 @@ private static void CreateTransparentToonLights(List lightObjects, i light.color = Color.white; light.intensity = 1.0f; light.cullingMask = cullingMask; - lightObject.transform.rotation = Quaternion.Euler(30.0f, index == 0 ? -30.0f : 30.0f, 0.0f); + lightObject.transform.rotation = Quaternion.Euler( + 30.0f, + index == 0 ? -30.0f : 30.0f, + 0.0f + ); } } /// Releases Transparent Toon lights and temporary render resources in their original order. - private static void ReleaseTransparentToonResources(List lightObjects, GameObject cameraObject, GameObject quadObject, Camera camera, RenderTexture renderTexture, Texture2D texture) + private static void ReleaseTransparentToonResources( + List lightObjects, + GameObject cameraObject, + GameObject quadObject, + Camera camera, + RenderTexture renderTexture, + Texture2D texture + ) { foreach (GameObject lightObject in lightObjects) UnityEngine.Object.DestroyImmediate(lightObject); @@ -530,10 +843,26 @@ private static float RgbMagnitude(Color color) /// The observation label. private static void AssertFinite(Color color, string label) { - Assert.That(float.IsNaN(color.r) || float.IsInfinity(color.r), Is.False, label + " red is non-finite."); - Assert.That(float.IsNaN(color.g) || float.IsInfinity(color.g), Is.False, label + " green is non-finite."); - Assert.That(float.IsNaN(color.b) || float.IsInfinity(color.b), Is.False, label + " blue is non-finite."); - Assert.That(float.IsNaN(color.a) || float.IsInfinity(color.a), Is.False, label + " alpha is non-finite."); + Assert.That( + float.IsNaN(color.r) || float.IsInfinity(color.r), + Is.False, + label + " red is non-finite." + ); + Assert.That( + float.IsNaN(color.g) || float.IsInfinity(color.g), + Is.False, + label + " green is non-finite." + ); + Assert.That( + float.IsNaN(color.b) || float.IsInfinity(color.b), + Is.False, + label + " blue is non-finite." + ); + Assert.That( + float.IsNaN(color.a) || float.IsInfinity(color.a), + Is.False, + label + " alpha is non-finite." + ); } /// Asserts that one scalar readback metric is finite. @@ -541,7 +870,11 @@ private static void AssertFinite(Color color, string label) /// The observation label. private static void AssertFinite(float value, string label) { - Assert.That(float.IsNaN(value) || float.IsInfinity(value), Is.False, label + " is non-finite."); + Assert.That( + float.IsNaN(value) || float.IsInfinity(value), + Is.False, + label + " is non-finite." + ); } /// Requires one imported public shader with no compiler errors. @@ -550,8 +883,16 @@ private static void AssertFinite(float value, string label) private static Shader RequireProductShader(string shaderName) { Shader shader = Shader.Find(shaderName); - Assert.That(shader, Is.Not.Null, "Product shader '" + shaderName + "' was not imported."); - Assert.That(ShaderUtil.ShaderHasError(shader), Is.False, "Product shader '" + shaderName + "' has compiler errors."); + Assert.That( + shader, + Is.Not.Null, + "Product shader '" + shaderName + "' was not imported." + ); + Assert.That( + ShaderUtil.ShaderHasError(shader), + Is.False, + "Product shader '" + shaderName + "' has compiler errors." + ); return shader; } @@ -559,7 +900,11 @@ private static Shader RequireProductShader(string shaderName) /// The material to inspect. private static void RequireRenderingModeProperty(Material material) { - Assert.That(material.HasProperty("_RenderingMode"), Is.True, "Rendering observations require the public _RenderingMode property."); + Assert.That( + material.HasProperty("_RenderingMode"), + Is.True, + "Rendering observations require the public _RenderingMode property." + ); } /// Finds a loaded type without adding a compile-time dependency on the future Editor assembly. @@ -599,7 +944,11 @@ public ShadowReadback(float maxAbsoluteRgbDelta, int changedPixelCount) /// The mode label associated with this measurement. /// The formatted measurement. public string Describe(string label) => - label + ": maxAbsoluteRgbDelta=" + maxAbsoluteRgbDelta + ", changedPixels=" + changedPixelCount; + label + + ": maxAbsoluteRgbDelta=" + + maxAbsoluteRgbDelta + + ", changedPixels=" + + changedPixelCount; } } } diff --git a/Tests/Daily/Editor/PureBaseValidationSceneRegressionTests.cs b/Tests/Daily/Editor/PureBaseValidationSceneRegressionTests.cs index caee352..1865d1d 100644 --- a/Tests/Daily/Editor/PureBaseValidationSceneRegressionTests.cs +++ b/Tests/Daily/Editor/PureBaseValidationSceneRegressionTests.cs @@ -1041,10 +1041,7 @@ public void CanonicalStaticLightmapCountIgnoresLoadedPersistedOwnerScene() SceneRegressionBaseline baseline = LoadBaseline(); Scene ownerScene = default; Scene validationScene = default; - var fixtureScope = new ControlledFixtureSceneScope( - TestOwnerScenePath, - ScenePath - ); + var fixtureScope = new ControlledFixtureSceneScope(TestOwnerScenePath, ScenePath); try { ownerScene = fixtureScope.GetLoadedFixture(TestOwnerScenePath); @@ -1083,7 +1080,10 @@ public void CanonicalStaticLightmapCountIgnoresLoadedPersistedOwnerScene() Is.EqualTo(baseline.staticLightmapCount) ); - ownerScene = EditorSceneManager.OpenScene(TestOwnerScenePath, OpenSceneMode.Additive); + ownerScene = EditorSceneManager.OpenScene( + TestOwnerScenePath, + OpenSceneMode.Additive + ); int ownerAndCanonicalGlobalLightmapCount = LightmapSettings.lightmaps.Length; int ownerAndCanonicalStaticLightmapCount = CountAssignedStaticLightmaps( GetStaticRenderers(validationScene) @@ -1519,9 +1519,7 @@ private static List GetStaticRenderers(Scene scene) /// Counts the unique valid static-lightmap assignments used by canonical scene renderers. /// The enabled static renderers from the canonical validation scene. /// The number of committed static lightmaps referenced by the canonical scene. - private static int CountAssignedStaticLightmaps( - IReadOnlyList staticRenderers - ) + private static int CountAssignedStaticLightmaps(IReadOnlyList staticRenderers) { LightmapData[] lightmaps = LightmapSettings.lightmaps; Assert.That(lightmaps, Is.Not.Null, "The current lightmap settings are unavailable."); @@ -2931,8 +2929,16 @@ public Scene GetLoadedFixture(string fixturePath) /// The valid loaded fixture scene to activate. public void SetActiveFixture(Scene fixtureScene) { - Assert.That(fixtureScene.IsValid(), Is.True, "The controlled fixture scene was invalid."); - Assert.That(fixtureScene.isLoaded, Is.True, "The controlled fixture scene was not loaded."); + Assert.That( + fixtureScene.IsValid(), + Is.True, + "The controlled fixture scene was invalid." + ); + Assert.That( + fixtureScene.isLoaded, + Is.True, + "The controlled fixture scene was not loaded." + ); if (SceneManager.GetActiveScene().Equals(fixtureScene)) return; Assert.That( @@ -3028,11 +3034,10 @@ bool wasActive public static FixtureSceneState Capture(string path, Scene activeScene) { Scene scene = SceneManager.GetSceneByPath(path); - FixtureScenePresence presence = !scene.IsValid() - ? FixtureScenePresence.Absent - : scene.isLoaded - ? FixtureScenePresence.Loaded - : FixtureScenePresence.Unloaded; + FixtureScenePresence presence = + !scene.IsValid() ? FixtureScenePresence.Absent + : scene.isLoaded ? FixtureScenePresence.Loaded + : FixtureScenePresence.Unloaded; bool isActive = scene.IsValid() && scene.Equals(activeScene); return new FixtureSceneState(path, presence, isActive); @@ -3056,8 +3061,16 @@ public Scene GetOrOpenLoadedScene() EditorSceneManager.OpenScene(Path, OpenSceneMode.Additive); scene = SceneManager.GetSceneByPath(Path); - Assert.That(scene.IsValid(), Is.True, $"Fixture '{Path}' was invalid after reopening."); - Assert.That(scene.isLoaded, Is.True, $"Fixture '{Path}' was not loaded after reopening."); + Assert.That( + scene.IsValid(), + Is.True, + $"Fixture '{Path}' was invalid after reopening." + ); + Assert.That( + scene.isLoaded, + Is.True, + $"Fixture '{Path}' was not loaded after reopening." + ); return scene; } @@ -3087,15 +3100,35 @@ public void AssertRestored() switch (originalPresence) { case FixtureScenePresence.Loaded: - Assert.That(scene.IsValid(), Is.True, $"Fixture '{Path}' was removed during restoration."); - Assert.That(scene.isLoaded, Is.True, $"Fixture '{Path}' was not restored as loaded."); + Assert.That( + scene.IsValid(), + Is.True, + $"Fixture '{Path}' was removed during restoration." + ); + Assert.That( + scene.isLoaded, + Is.True, + $"Fixture '{Path}' was not restored as loaded." + ); break; case FixtureScenePresence.Unloaded: - Assert.That(scene.IsValid(), Is.True, $"Fixture '{Path}' was removed instead of restored as unloaded."); - Assert.That(scene.isLoaded, Is.False, $"Fixture '{Path}' was not restored as unloaded."); + Assert.That( + scene.IsValid(), + Is.True, + $"Fixture '{Path}' was removed instead of restored as unloaded." + ); + Assert.That( + scene.isLoaded, + Is.False, + $"Fixture '{Path}' was not restored as unloaded." + ); break; case FixtureScenePresence.Absent: - Assert.That(scene.IsValid(), Is.False, $"Fixture '{Path}' was left registered after restoration."); + Assert.That( + scene.IsValid(), + Is.False, + $"Fixture '{Path}' was left registered after restoration." + ); break; default: throw new ArgumentOutOfRangeException(); diff --git a/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerReleaseTests.cs b/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerReleaseTests.cs index 1e39431..6c03952 100644 --- a/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerReleaseTests.cs +++ b/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerReleaseTests.cs @@ -47,10 +47,26 @@ public sealed class PureBaseConsumerModuleFreeImportTests new StencilPropertyContract("_StencilRef", 0.0f, "SCRangeInt(0,255)"), new StencilPropertyContract("_StencilReadMask", 255.0f, "SCRangeInt(0,255)"), new StencilPropertyContract("_StencilWriteMask", 255.0f, "SCRangeInt(0,255)"), - new StencilPropertyContract("_StencilComp", 8.0f, "SCEnum(UnityEngine.Rendering.CompareFunction)"), - new StencilPropertyContract("_StencilPass", 0.0f, "SCEnum(UnityEngine.Rendering.StencilOp)"), - new StencilPropertyContract("_StencilFail", 0.0f, "SCEnum(UnityEngine.Rendering.StencilOp)"), - new StencilPropertyContract("_StencilZFail", 0.0f, "SCEnum(UnityEngine.Rendering.StencilOp)"), + new StencilPropertyContract( + "_StencilComp", + 8.0f, + "SCEnum(UnityEngine.Rendering.CompareFunction)" + ), + new StencilPropertyContract( + "_StencilPass", + 0.0f, + "SCEnum(UnityEngine.Rendering.StencilOp)" + ), + new StencilPropertyContract( + "_StencilFail", + 0.0f, + "SCEnum(UnityEngine.Rendering.StencilOp)" + ), + new StencilPropertyContract( + "_StencilZFail", + 0.0f, + "SCEnum(UnityEngine.Rendering.StencilOp)" + ), }; /// Imports all runner-configured module-free products and checks their public and generated contracts. diff --git a/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerRenderingModeTests.cs b/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerRenderingModeTests.cs index e854af1..0e8c0f2 100644 --- a/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerRenderingModeTests.cs +++ b/Tests/Release/ConsumerProject/Assets/Editor/PureBaseConsumerRenderingModeTests.cs @@ -30,7 +30,8 @@ namespace PureBase.Release.Consumer.Tests public sealed class PureBaseConsumerRenderingModeTests { /// Identifies the only release module selected by the postpixel alpha consumer invocation. - private const string PostPixelAlphaProbeId = "jp.penguin.purebase.release.fixture.products.postpixel"; + private const string PostPixelAlphaProbeId = + "jp.penguin.purebase.release.fixture.products.postpixel"; /// Lists every local keyword owned by the rendering-mode contract. private static readonly string[] RenderingModeKeywords = @@ -76,8 +77,14 @@ public void PostPixelAlphaConsumerInvocationSelectsTheTransparentToonProbeContra product, contract.runLabel ); - CollectionAssert.AreEqual(SourcePassNames, ConsumerValidationSupport.GetPassNames(shader)); - string generatedSource = ConsumerValidationSupport.LoadGeneratedSource(product, contract.runLabel); + CollectionAssert.AreEqual( + SourcePassNames, + ConsumerValidationSupport.GetPassNames(shader) + ); + string generatedSource = ConsumerValidationSupport.LoadGeneratedSource( + product, + contract.runLabel + ); PureBaseConsumerModuleFreeImportTests.AssertGlobalFragments( contract, product, @@ -145,10 +152,7 @@ string generatedSource "The ForwardBase fragment must contain the transparent toon alpha probe contract." ); int alphaProbe = alphaProbeMatch.Index; - Match returnStatement = Regex.Match( - fragmentBody.Substring(alphaProbe), - @"\breturn\b" - ); + Match returnStatement = Regex.Match(fragmentBody.Substring(alphaProbe), @"\breturn\b"); Assert.That(modeAlphaOperation.Success, Is.True); Assert.That(alphaProbe, Is.GreaterThan(modeAlphaOperation.Index)); Assert.That(returnStatement.Success, Is.True); @@ -172,10 +176,7 @@ private static string GetFragmentBody(string passSource, string runLabel, string Is.True, $"Consumer run '{runLabel}' product '{shaderName}' did not contain a generated ForwardBase frag function." ); - int openingBrace = passSource.IndexOf( - '{', - declaration.Index + declaration.Length - ); + int openingBrace = passSource.IndexOf('{', declaration.Index + declaration.Length); Assert.That( openingBrace, Is.GreaterThanOrEqualTo(0), @@ -212,7 +213,10 @@ public void ColdImportedPublicNormalizerMatchesTheFourByThreeStateTable() foreach (ConsumerProductContract product in contract.products) { - Shader shader = ConsumerValidationSupport.ImportProductShader(product, contract.runLabel); + Shader shader = ConsumerValidationSupport.ImportProductShader( + product, + contract.runLabel + ); AssertRenderingModeAbi(product, shader, contract.runLabel); var material = new Material(shader); try @@ -245,7 +249,10 @@ private static void AssertRenderingModeAbi( string runLabel ) { - CollectionAssert.AreEqual(SourcePassNames, ConsumerValidationSupport.GetPassNames(shader)); + CollectionAssert.AreEqual( + SourcePassNames, + ConsumerValidationSupport.GetPassNames(shader) + ); CollectionAssert.Contains( ConsumerValidationSupport.GetVisiblePropertyNames(shader), "_RenderingMode" @@ -253,7 +260,10 @@ string runLabel int modeIndex = shader.FindPropertyIndex("_RenderingMode"); Assert.That(modeIndex, Is.GreaterThanOrEqualTo(0)); Assert.That(shader.GetPropertyType(modeIndex), Is.EqualTo(ShaderPropertyType.Int)); - CollectionAssert.Contains(shader.GetPropertyAttributes(modeIndex), "PureBaseRenderingMode"); + CollectionAssert.Contains( + shader.GetPropertyAttributes(modeIndex), + "PureBaseRenderingMode" + ); foreach (string propertyName in HiddenStatePropertyNames) { Assert.That(shader.FindPropertyIndex(propertyName), Is.GreaterThanOrEqualTo(0)); @@ -263,7 +273,10 @@ string runLabel ); } - string generatedSource = ConsumerValidationSupport.LoadGeneratedSource(product, runLabel); + string generatedSource = ConsumerValidationSupport.LoadGeneratedSource( + product, + runLabel + ); StringAssert.Contains( "#pragma shader_feature_local _ PUREBASE_RENDERING_OPAQUE PUREBASE_RENDERING_TRANSPARENT", generatedSource @@ -273,11 +286,14 @@ string runLabel Is.LessThan(0), product.shaderName + " must keep Cutout keyword-free." ); - string propertySourcePath = Path.ChangeExtension(product.shaderAssetPath, null) - + "_properties.hlsl"; + string propertySourcePath = + Path.ChangeExtension(product.shaderAssetPath, null) + "_properties.hlsl"; string projectRoot = Directory.GetParent(Application.dataPath).FullName; string propertySource = File.ReadAllText( - Path.Combine(projectRoot, propertySourcePath.Replace('/', Path.DirectorySeparatorChar)) + Path.Combine( + projectRoot, + propertySourcePath.Replace('/', Path.DirectorySeparatorChar) + ) ); Assert.That( Regex.IsMatch(propertySource, RenderingModePropertySourcePattern), @@ -301,7 +317,11 @@ private static void AssertCutoutDefaults(Material material, string shaderName) /// The public rendering-mode value. private static void AssertModeState(Material material, string shaderName, int mode) { - Assert.That(material.GetInteger("_RenderingMode"), Is.EqualTo(mode), shaderName + " rendering mode."); + Assert.That( + material.GetInteger("_RenderingMode"), + Is.EqualTo(mode), + shaderName + " rendering mode." + ); var expectedState = GetExpectedModeState(mode); AssertDerivedModeState(material, expectedState); } @@ -326,18 +346,42 @@ bool contributionPasses { case 0: return ( - (int)BlendMode.One, (int)BlendMode.Zero, 1, (int)BlendMode.One, - (int)BlendMode.One, "Opaque", 2000, true, false, true + (int)BlendMode.One, + (int)BlendMode.Zero, + 1, + (int)BlendMode.One, + (int)BlendMode.One, + "Opaque", + 2000, + true, + false, + true ); case 1: return ( - (int)BlendMode.One, (int)BlendMode.Zero, 1, (int)BlendMode.One, - (int)BlendMode.One, "TransparentCutout", (int)RenderQueue.AlphaTest, false, false, true + (int)BlendMode.One, + (int)BlendMode.Zero, + 1, + (int)BlendMode.One, + (int)BlendMode.One, + "TransparentCutout", + (int)RenderQueue.AlphaTest, + false, + false, + true ); case 2: return ( - (int)BlendMode.SrcAlpha, (int)BlendMode.OneMinusSrcAlpha, 0, - (int)BlendMode.SrcAlpha, (int)BlendMode.One, "Transparent", 3000, false, true, false + (int)BlendMode.SrcAlpha, + (int)BlendMode.OneMinusSrcAlpha, + 0, + (int)BlendMode.SrcAlpha, + (int)BlendMode.One, + "Transparent", + 3000, + false, + true, + false ); default: throw new ArgumentOutOfRangeException(nameof(mode)); @@ -363,30 +407,58 @@ bool contributionPasses ) expectedState ) { - Assert.That(material.GetFloat("_SrcBlend"), Is.EqualTo((float)expectedState.sourceBlend)); - Assert.That(material.GetFloat("_DstBlend"), Is.EqualTo((float)expectedState.destinationBlend)); + Assert.That( + material.GetFloat("_SrcBlend"), + Is.EqualTo((float)expectedState.sourceBlend) + ); + Assert.That( + material.GetFloat("_DstBlend"), + Is.EqualTo((float)expectedState.destinationBlend) + ); Assert.That(material.GetFloat("_ZWrite"), Is.EqualTo((float)expectedState.depthWrite)); - Assert.That(material.GetFloat("_AddSrcBlend"), Is.EqualTo((float)expectedState.additiveSourceBlend)); - Assert.That(material.GetFloat("_AddDstBlend"), Is.EqualTo((float)expectedState.additiveDestinationBlend)); + Assert.That( + material.GetFloat("_AddSrcBlend"), + Is.EqualTo((float)expectedState.additiveSourceBlend) + ); + Assert.That( + material.GetFloat("_AddDstBlend"), + Is.EqualTo((float)expectedState.additiveDestinationBlend) + ); Assert.That(material.GetTag("RenderType", false), Is.EqualTo(expectedState.renderType)); Assert.That(material.renderQueue, Is.EqualTo(expectedState.renderQueue)); - Assert.That(material.IsKeywordEnabled(RenderingModeKeywords[0]), Is.EqualTo(expectedState.opaqueKeyword)); - Assert.That(material.IsKeywordEnabled(RenderingModeKeywords[1]), Is.EqualTo(expectedState.transparentKeyword)); - Assert.That(material.GetShaderPassEnabled("ShadowCaster"), Is.EqualTo(expectedState.contributionPasses)); - Assert.That(material.GetShaderPassEnabled("Meta"), Is.EqualTo(expectedState.contributionPasses)); + Assert.That( + material.IsKeywordEnabled(RenderingModeKeywords[0]), + Is.EqualTo(expectedState.opaqueKeyword) + ); + Assert.That( + material.IsKeywordEnabled(RenderingModeKeywords[1]), + Is.EqualTo(expectedState.transparentKeyword) + ); + Assert.That( + material.GetShaderPassEnabled("ShadowCaster"), + Is.EqualTo(expectedState.contributionPasses) + ); + Assert.That( + material.GetShaderPassEnabled("Meta"), + Is.EqualTo(expectedState.contributionPasses) + ); } /// Requires invalid public mode values to leave all derived state from the prior valid mode unchanged. /// The reusable transient material. /// The material's public shader name. /// The unsupported public mode value. - private static void AssertInvalidModeIsAtomic(Material material, string shaderName, int invalidMode) + private static void AssertInvalidModeIsAtomic( + Material material, + string shaderName, + int invalidMode + ) { material.SetInteger("_RenderingMode", 0); PureBaseMaterialRenderingMode.Apply(material); material.SetInteger("_RenderingMode", invalidMode); - Assert.Throws( - () => PureBaseMaterialRenderingMode.Apply(material) + Assert.Throws(() => + PureBaseMaterialRenderingMode.Apply(material) ); Assert.That(material.GetInteger("_RenderingMode"), Is.EqualTo(invalidMode)); Assert.That(material.GetFloat("_SrcBlend"), Is.EqualTo((float)BlendMode.One)); @@ -399,7 +471,11 @@ private static void AssertInvalidModeIsAtomic(Material material, string shaderNa Assert.That(material.IsKeywordEnabled(RenderingModeKeywords[0]), Is.True); Assert.That(material.IsKeywordEnabled(RenderingModeKeywords[1]), Is.False); Assert.That(material.GetShaderPassEnabled("ShadowCaster"), Is.True); - Assert.That(material.GetShaderPassEnabled("Meta"), Is.True, shaderName + " invalid mode must not disable Meta."); + Assert.That( + material.GetShaderPassEnabled("Meta"), + Is.True, + shaderName + " invalid mode must not disable Meta." + ); } } } From 45074e9f10f474dd966b1c2793c0751b60b0c4e5 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Tue, 11 Aug 2026 03:03:12 +0900 Subject: [PATCH 2/3] style: normalize PowerShell formatting across automation and release tests - Align indentation and property spacing - Normalize comparison operators and pipeline formatting - Add missing trailing newlines to PowerShell files --- .../Install-PinnedUnityCli.ps1 | 2 +- .../scripts/Export-PureBaseValidationZip.ps1 | 5 +- .../Get-ReleaseAuthorizationPredicate.ps1 | 53 +- ...nstall-VerifiedShaderCoreRelease.Tests.ps1 | 1 + .../Install-VerifiedShaderCoreRelease.ps1 | 1 + .github/scripts/Invoke-PureBaseRelease.ps1 | 1 + .github/scripts/New-PureBaseCiProject.ps1 | 47 +- .github/scripts/PureBase.Automation.psm1 | 21 +- .../scripts/PureBase.ReleasePublication.psm1 | 9 +- .github/scripts/Resolve-UnityEditorPath.ps1 | 1 + .../scripts/Test-RepositoryLineEndings.ps1 | 8 +- .github/scripts/UnityWatchdogProxy.ps1 | 1 + .../Export-PureBaseValidationZip.Tests.ps1 | 2 +- .../HostedUnityReviewContracts.Tests.ps1 | 25 +- .github/tests/InstallPinnedUnityCli.Tests.ps1 | 2 +- .github/tests/New-PureBaseCiProject.Tests.ps1 | 3 +- .github/tests/PureBase.Automation.Tests.ps1 | 107 +- .github/tests/ReleaseAuthorization.Tests.ps1 | 9 +- .github/tests/ReleasePublishTag.Tests.ps1 | 9 +- .../ReleaseValidationIsolation.Tests.ps1 | 1 + .github/tests/RepositoryLineEndings.Tests.ps1 | 4 +- .../ShaderCorePhaseCompatibility.Tests.ps1 | 3 +- .github/tests/UnityMetadata.Tests.ps1 | 19 +- .../Parity/Validate-PureBaseParity.Oracle.ps1 | 2 +- .../Parity/Validate-PureBaseParity.Tests.ps1 | 988 ++++---- Tests/Parity/Validate-PureBaseParity.ps1 | 68 +- Tests/Release/Build-PureBaseRelease.Tests.ps1 | 20 +- Tests/Release/Build-PureBaseRelease.ps1 | 92 +- .../Run-PureBaseReleaseValidation.Tests.ps1 | 2133 +++++++++-------- .../Release/Run-PureBaseReleaseValidation.ps1 | 13 +- Tests/Run-PureBaseRegression.ps1 | 6 +- 31 files changed, 1838 insertions(+), 1818 deletions(-) diff --git a/.github/actions/lookup-unity-editor-cache/Install-PinnedUnityCli.ps1 b/.github/actions/lookup-unity-editor-cache/Install-PinnedUnityCli.ps1 index 3f81b33..8e4aad5 100644 --- a/.github/actions/lookup-unity-editor-cache/Install-PinnedUnityCli.ps1 +++ b/.github/actions/lookup-unity-editor-cache/Install-PinnedUnityCli.ps1 @@ -74,4 +74,4 @@ function Install-PinnedUnityCli { finally { Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue } -} \ No newline at end of file +} diff --git a/.github/scripts/Export-PureBaseValidationZip.ps1 b/.github/scripts/Export-PureBaseValidationZip.ps1 index c79e1ff..0bdb347 100644 --- a/.github/scripts/Export-PureBaseValidationZip.ps1 +++ b/.github/scripts/Export-PureBaseValidationZip.ps1 @@ -72,7 +72,7 @@ catch { throw "package.json version must be valid strict SemVer: '$version'." } $sourceZips = @( Get-ChildItem -LiteralPath $validationArtifactRoot -Filter 'jp.penguin.purebase-*.zip' -File -Recurse | - Where-Object { $_.Directory.Name -ceq 'archive' } + Where-Object { $_.Directory.Name -ceq 'archive' } ) if ($sourceZips.Count -ne 1) { throw "Release validation must produce exactly one audited package ZIP below '$validationArtifactRoot/archive'." @@ -83,7 +83,7 @@ Add-Type -AssemblyName System.IO.Compression.FileSystem $archive = $null try { $archive = [IO.Compression.ZipFile]::OpenRead($sourceZip.FullName) - $manifestEntries = @($archive.Entries | Where-Object FullName -ceq 'package.json') + $manifestEntries = @($archive.Entries | Where-Object FullName -CEQ 'package.json') if ($manifestEntries.Count -ne 1) { throw 'Audited package ZIP must contain exactly one package.json.' } $reader = [IO.StreamReader]::new($manifestEntries[0].Open(), [Text.UTF8Encoding]::new($false, $true)) try { @@ -137,3 +137,4 @@ finally { Write-Output "Validated package ZIP: $destinationZip" Write-Output "SHA-256: $sha256" + diff --git a/.github/scripts/Get-ReleaseAuthorizationPredicate.ps1 b/.github/scripts/Get-ReleaseAuthorizationPredicate.ps1 index 28d3bb1..d69a003 100644 --- a/.github/scripts/Get-ReleaseAuthorizationPredicate.ps1 +++ b/.github/scripts/Get-ReleaseAuthorizationPredicate.ps1 @@ -84,45 +84,46 @@ if ($actualSha256 -cne $expectedSha256) { $predicate = [ordered]@{ schemaVersion = 1 authorization = [ordered]@{ - method = 'github-actions-workflow-dispatch' - actor = [ordered]@{ + method = 'github-actions-workflow-dispatch' + actor = [ordered]@{ login = $ActorLogin - id = $ActorId + id = $ActorId } triggeringActor = $TriggeringActorLogin - eventName = $EventName + eventName = $EventName } - release = [ordered]@{ - repository = $Repository - repositoryId = $RepositoryId - repositoryOwner = $RepositoryOwner + release = [ordered]@{ + repository = $Repository + repositoryId = $RepositoryId + repositoryOwner = $RepositoryOwner repositoryOwnerId = $RepositoryOwnerId - version = $ConfirmedVersion - commitSha = [string]$state.commitSha - releaseUrl = [string]$state.releaseUrl - vpmRepository = [string]$state.vpmRepository - artifact = [ordered]@{ - name = $assetName + version = $ConfirmedVersion + commitSha = [string]$state.commitSha + releaseUrl = [string]$state.releaseUrl + vpmRepository = [string]$state.vpmRepository + artifact = [ordered]@{ + name = $assetName sha256 = $actualSha256 } } - workflow = [ordered]@{ - name = $WorkflowName - ref = $WorkflowRef - sha = $WorkflowSha - runId = $RunId - runNumber = $RunNumber - runAttempt = $RunAttempt + workflow = [ordered]@{ + name = $WorkflowName + ref = $WorkflowRef + sha = $WorkflowSha + runId = $RunId + runNumber = $RunNumber + runAttempt = $RunAttempt environment = 'release' } - request = [ordered]@{ - ref = $DispatchRef - refName = $DispatchRefName - refType = $DispatchRefType - resume = [bool]$Resume + request = [ordered]@{ + ref = $DispatchRef + refName = $DispatchRefName + refType = $DispatchRefType + resume = [bool]$Resume preflightOnly = $false } recordedAtUtc = [DateTime]::UtcNow.ToString('o') } $predicate | ConvertTo-Json -Depth 8 + diff --git a/.github/scripts/Install-VerifiedShaderCoreRelease.Tests.ps1 b/.github/scripts/Install-VerifiedShaderCoreRelease.Tests.ps1 index ece3df1..359db62 100644 --- a/.github/scripts/Install-VerifiedShaderCoreRelease.Tests.ps1 +++ b/.github/scripts/Install-VerifiedShaderCoreRelease.Tests.ps1 @@ -222,3 +222,4 @@ Describe 'Install-VerifiedShaderCoreRelease' { Assert-RecoverableBackupPreserved } } + diff --git a/.github/scripts/Install-VerifiedShaderCoreRelease.ps1 b/.github/scripts/Install-VerifiedShaderCoreRelease.ps1 index 801e5a4..657558d 100644 --- a/.github/scripts/Install-VerifiedShaderCoreRelease.ps1 +++ b/.github/scripts/Install-VerifiedShaderCoreRelease.ps1 @@ -264,3 +264,4 @@ function Install-VerifiedShaderCoreRelease { if ($MyInvocation.InvocationName -ne '.') { Install-VerifiedShaderCoreRelease @PSBoundParameters } + diff --git a/.github/scripts/Invoke-PureBaseRelease.ps1 b/.github/scripts/Invoke-PureBaseRelease.ps1 index 12f0015..1d681fe 100644 --- a/.github/scripts/Invoke-PureBaseRelease.ps1 +++ b/.github/scripts/Invoke-PureBaseRelease.ps1 @@ -319,3 +319,4 @@ $dispatchPayload = New-PureBaseDispatchPayload -PackageName $packageName -Reposi Invoke-Api POST "$apiRoot/repos/$VpmRepository/dispatches" $dispatchToken $dispatchPayload | Out-Null Write-State 'completed' @{ commitSha = $releaseTargetSha; validationRunId = [long]$run.id; validationRunAttempt = [int]$run.run_attempt; releaseUrl = [string]$release.html_url; vpmRepository = $VpmRepository; sha256 = $artifact.Sha256; mode = $releaseMode.Mode } Write-Output "Release completed: $($release.html_url)" + diff --git a/.github/scripts/New-PureBaseCiProject.ps1 b/.github/scripts/New-PureBaseCiProject.ps1 index c08d659..f4cabd1 100644 --- a/.github/scripts/New-PureBaseCiProject.ps1 +++ b/.github/scripts/New-PureBaseCiProject.ps1 @@ -14,8 +14,8 @@ [CmdletBinding()] param( - [Parameter(Mandatory = $true)] - [string]$ProjectRoot + [Parameter(Mandatory = $true)] + [string]$ProjectRoot ) Set-StrictMode -Version Latest @@ -26,54 +26,54 @@ $packageRoot = Join-Path $projectRootFullPath 'Packages/jp.penguin.purebase' $shaderCoreRoot = Join-Path $projectRootFullPath 'Packages/jp.lilxyzw.shadercore' foreach ($requiredPath in @($packageRoot, $shaderCoreRoot)) { - if (-not (Test-Path -LiteralPath $requiredPath -PathType Container)) { - throw "Required package checkout is missing: '$requiredPath'." - } + if (-not (Test-Path -LiteralPath $requiredPath -PathType Container)) { + throw "Required package checkout is missing: '$requiredPath'." + } } $packageJson = Get-Content -LiteralPath (Join-Path $packageRoot 'package.json') -Raw | ConvertFrom-Json if ([string]$packageJson.name -ne 'jp.penguin.purebase') { - throw "Unexpected Pure-Base package identity '$($packageJson.name)'." + throw "Unexpected Pure-Base package identity '$($packageJson.name)'." } $shaderCoreJson = Get-Content -LiteralPath (Join-Path $shaderCoreRoot 'package.json') -Raw | ConvertFrom-Json if ([string]$shaderCoreJson.name -ne 'jp.lilxyzw.shadercore' -or [string]$shaderCoreJson.version -ne '0.1.9') { - throw "The CI workspace requires jp.lilxyzw.shadercore exactly 0.1.9." + throw "The CI workspace requires jp.lilxyzw.shadercore exactly 0.1.9." } $ownerLightingDataRelativePath = 'Tests/Fixtures/Scenes/PureBaseValidation/OwnerLightingData.asset' $ownerLightingDataAssetPath = Join-Path $packageRoot $ownerLightingDataRelativePath if (-not (Test-Path -LiteralPath $ownerLightingDataAssetPath -PathType Leaf)) { - throw "Owner LightingData fixture is missing: '$ownerLightingDataRelativePath'." + throw "Owner LightingData fixture is missing: '$ownerLightingDataRelativePath'." } $ownerLightingDataMetaRelativePath = "$ownerLightingDataRelativePath.meta" $ownerLightingDataMetaPath = Join-Path $packageRoot $ownerLightingDataMetaRelativePath if (-not (Test-Path -LiteralPath $ownerLightingDataMetaPath -PathType Leaf)) { - throw "Owner LightingData metadata is missing: '$ownerLightingDataMetaRelativePath'." + throw "Owner LightingData metadata is missing: '$ownerLightingDataMetaRelativePath'." } $ownerLightingDataGuidLines = @([regex]::Matches((Get-Content -LiteralPath $ownerLightingDataMetaPath -Raw), '(?m)^guid:\s*(\S+)\s*$')) if ($ownerLightingDataGuidLines.Count -ne 1) { - throw "Owner LightingData metadata must contain exactly one GUID: '$ownerLightingDataMetaRelativePath'." + throw "Owner LightingData metadata must contain exactly one GUID: '$ownerLightingDataMetaRelativePath'." } $ownerLightingDataGuid = $ownerLightingDataGuidLines[0].Groups[1].Value if ($ownerLightingDataGuid -notmatch '^[0-9a-fA-F]{32}$') { - throw "Owner LightingData metadata contains a malformed GUID: '$ownerLightingDataMetaRelativePath'." + throw "Owner LightingData metadata contains a malformed GUID: '$ownerLightingDataMetaRelativePath'." } $assetsRoot = Join-Path $projectRootFullPath 'Assets' $projectSettingsRoot = Join-Path $projectRootFullPath 'ProjectSettings' $packagesRoot = Join-Path $projectRootFullPath 'Packages' foreach ($directory in @($assetsRoot, $projectSettingsRoot, $packagesRoot)) { - New-Item -ItemType Directory -Path $directory -Force | Out-Null + New-Item -ItemType Directory -Path $directory -Force | Out-Null } $consumerProjectSettingsRoot = Join-Path $packageRoot 'Tests/Release/ConsumerProject/ProjectSettings' $projectVersionSource = Join-Path $consumerProjectSettingsRoot 'ProjectVersion.txt' if (-not (Test-Path -LiteralPath $projectVersionSource -PathType Leaf)) { - throw "Pinned Unity ProjectVersion source is missing: '$projectVersionSource'." + throw "Pinned Unity ProjectVersion source is missing: '$projectVersionSource'." } Copy-Item -LiteralPath $projectVersionSource -Destination (Join-Path $projectSettingsRoot 'ProjectVersion.txt') -Force @@ -85,20 +85,20 @@ Copy-Item -LiteralPath $projectVersionSource -Destination (Join-Path $projectSet # Recompare and refresh this fixture whenever the VRChat SDK or its Project Setup changes. $qualitySettingsSource = Join-Path $consumerProjectSettingsRoot 'QualitySettings.asset' if (-not (Test-Path -LiteralPath $qualitySettingsSource -PathType Leaf)) { - throw "Reviewed VRChat-project QualitySettings snapshot is missing: '$qualitySettingsSource'." + throw "Reviewed VRChat-project QualitySettings snapshot is missing: '$qualitySettingsSource'." } Copy-Item -LiteralPath $qualitySettingsSource -Destination (Join-Path $projectSettingsRoot 'QualitySettings.asset') -Force $manifest = [ordered]@{ - dependencies = [ordered]@{ - 'com.unity.test-framework' = '1.1.33' - } + dependencies = [ordered]@{ + 'com.unity.test-framework' = '1.1.33' + } } $manifestText = ($manifest | ConvertTo-Json -Depth 4) + "`n" [System.IO.File]::WriteAllText( - (Join-Path $packagesRoot 'manifest.json'), - $manifestText, - [System.Text.UTF8Encoding]::new($false) + (Join-Path $packagesRoot 'manifest.json'), + $manifestText, + [System.Text.UTF8Encoding]::new($false) ) $ownerSceneText = @" @@ -233,9 +233,9 @@ SceneRoots: m_Roots: [] "@ [System.IO.File]::WriteAllText( - (Join-Path $assetsRoot 'Pure-Base.unity'), - $ownerSceneText.Replace("`r`n", "`n") + "`n", - [System.Text.UTF8Encoding]::new($false) + (Join-Path $assetsRoot 'Pure-Base.unity'), + $ownerSceneText.Replace("`r`n", "`n") + "`n", + [System.Text.UTF8Encoding]::new($false) ) Write-Output "Prepared Pure-Base CI Unity project: $projectRootFullPath" @@ -246,3 +246,4 @@ Write-Output "VRChat SDK packages installed in generated CI project: none" # Normalize only an accepted native-command status when the caller has one. if ((Test-Path Variable:LASTEXITCODE) -and $LASTEXITCODE -lt 8) { $global:LASTEXITCODE = 0 } + diff --git a/.github/scripts/PureBase.Automation.psm1 b/.github/scripts/PureBase.Automation.psm1 index 3917461..c8efb0a 100644 --- a/.github/scripts/PureBase.Automation.psm1 +++ b/.github/scripts/PureBase.Automation.psm1 @@ -38,12 +38,12 @@ function ConvertTo-PureBaseSemVer { } return [pscustomobject][ordered]@{ - original = $Value - major = $major - minor = $minor - patch = $patch - prerelease = $prerelease - isPrerelease = $prerelease.Count -ne 0 + original = $Value + major = $major + minor = $minor + patch = $patch + prerelease = $prerelease + isPrerelease = $prerelease.Count -ne 0 prereleaseKind = if ($prerelease.Count -eq 0) { '' } else { $prerelease[0] } } } @@ -505,7 +505,7 @@ function Resolve-PureBasePublishedArtifact { throw 'GitHub did not report the published release as immutable.' } - $assets = @($Release.assets | Where-Object name -eq $AssetName) + $assets = @($Release.assets | Where-Object name -EQ $AssetName) if ($assets.Count -ne 1) { throw "Published release must contain exactly one asset named '$AssetName'." } @@ -548,9 +548,9 @@ function Select-PureBaseReleaseValidationRun { try { $runWorkflowPath = ConvertTo-PureBaseReleaseValidationWorkflowPath -Value ([string]$_.path) } catch { return $false } return $runWorkflowPath -ceq $expectedWorkflowPath -and - [string]::Equals([string]$_.head_sha, $HeadSha, [StringComparison]::OrdinalIgnoreCase) -and - [string]$_.head_branch -ceq $Branch -and [string]$_.event -ceq 'workflow_dispatch' -and - [int]$_.run_number -gt 0 -and [int]$_.run_attempt -gt 0 + [string]::Equals([string]$_.head_sha, $HeadSha, [StringComparison]::OrdinalIgnoreCase) -and + [string]$_.head_branch -ceq $Branch -and [string]$_.event -ceq 'workflow_dispatch' -and + [int]$_.run_number -gt 0 -and [int]$_.run_attempt -gt 0 }) if ($candidates.Count -eq 0) { throw 'No matching validation run was found for the exact workflow, branch, and commit.' } $latest = @($candidates | Sort-Object @{ Expression = { [int]$_.run_number }; Descending = $true }, @{ Expression = { [int]$_.run_attempt }; Descending = $true })[0] @@ -843,3 +843,4 @@ Export-ModuleMember -Function @( 'Read-PureBaseVpmYankPolicy', 'Invoke-PureBaseYankDispatch' ) + diff --git a/.github/scripts/PureBase.ReleasePublication.psm1 b/.github/scripts/PureBase.ReleasePublication.psm1 index f37f8e8..bb05c87 100644 --- a/.github/scripts/PureBase.ReleasePublication.psm1 +++ b/.github/scripts/PureBase.ReleasePublication.psm1 @@ -86,8 +86,9 @@ Set-Alias -Name New-PureBaseReleasePublicationBody -Value ConvertTo-PureBaseRele Export-ModuleMember ` -Function @( - 'ConvertTo-PureBaseReleasePublicationBody', - 'Assert-PureBasePublishedReleaseIdentity', - 'Invoke-PureBaseReleaseLookupWithRetry' - ) ` + 'ConvertTo-PureBaseReleasePublicationBody', + 'Assert-PureBasePublishedReleaseIdentity', + 'Invoke-PureBaseReleaseLookupWithRetry' +) ` -Alias 'New-PureBaseReleasePublicationBody' + diff --git a/.github/scripts/Resolve-UnityEditorPath.ps1 b/.github/scripts/Resolve-UnityEditorPath.ps1 index f523e9f..447179e 100644 --- a/.github/scripts/Resolve-UnityEditorPath.ps1 +++ b/.github/scripts/Resolve-UnityEditorPath.ps1 @@ -193,3 +193,4 @@ exit /b %ERRORLEVEL% Write-Host "Unity watchdog proxy: $proxyCommandPath" Write-Host "Unity watchdog target: $normalizedPath" Write-Output $proxyCommandPath + diff --git a/.github/scripts/Test-RepositoryLineEndings.ps1 b/.github/scripts/Test-RepositoryLineEndings.ps1 index 9c26d78..b7bf422 100644 --- a/.github/scripts/Test-RepositoryLineEndings.ps1 +++ b/.github/scripts/Test-RepositoryLineEndings.ps1 @@ -80,7 +80,7 @@ function Invoke-RepositoryGitBytes { } throw "$diagnostic)." } - return ,$stdout.ToArray() + return , $stdout.ToArray() } finally { $process.Dispose() @@ -124,7 +124,7 @@ function Add-RepositoryViolation { ) $Violations.Add([pscustomobject]@{ - Path = $Path + Path = $Path Reason = $Reason }) } @@ -143,7 +143,7 @@ function Read-RepositoryIndexBlobs { $blobs = [Collections.Generic.Dictionary[string, byte[]]]::new([StringComparer]::OrdinalIgnoreCase) if ($ObjectIds.Count -eq 0) { - return ,$blobs + return , $blobs } $batchInput = [Text.Encoding]::ASCII.GetBytes((($ObjectIds -join "`n") + "`n")) $output = Invoke-RepositoryGitBytes -Root $Root -Arguments @('cat-file', '--batch') -StandardInputBytes $batchInput @@ -318,4 +318,4 @@ try { catch { Write-Output "repository: $($_.Exception.Message)" exit 1 -} \ No newline at end of file +} diff --git a/.github/scripts/UnityWatchdogProxy.ps1 b/.github/scripts/UnityWatchdogProxy.ps1 index 5e5260b..d8cc807 100644 --- a/.github/scripts/UnityWatchdogProxy.ps1 +++ b/.github/scripts/UnityWatchdogProxy.ps1 @@ -175,3 +175,4 @@ catch { finally { $process.Dispose() } + diff --git a/.github/tests/Export-PureBaseValidationZip.Tests.ps1 b/.github/tests/Export-PureBaseValidationZip.Tests.ps1 index 613c577..edd737a 100644 --- a/.github/tests/Export-PureBaseValidationZip.Tests.ps1 +++ b/.github/tests/Export-PureBaseValidationZip.Tests.ps1 @@ -117,4 +117,4 @@ Describe 'Validated package exporter contracts' { { & $exporterPath @invalidArguments } | Should -Throw "*$Name must be valid*" Test-Path -LiteralPath (Join-Path $validationRoot 'validated-package') | Should -BeFalse } -} \ No newline at end of file +} diff --git a/.github/tests/HostedUnityReviewContracts.Tests.ps1 b/.github/tests/HostedUnityReviewContracts.Tests.ps1 index b149ce0..0d3a477 100644 --- a/.github/tests/HostedUnityReviewContracts.Tests.ps1 +++ b/.github/tests/HostedUnityReviewContracts.Tests.ps1 @@ -121,15 +121,15 @@ Describe 'Hosted Unity review contracts' { $selectorPattern = "(?m)^(?: - | {6})(?:$selectorName|`"$selectorName`"|'$selectorName')[ \t]*:[ \t]*(?:$selectorValue|`"$selectorValue`"|'$selectorValue')[ \t]*(?:#.*)?$" return @( [regex]::Matches(($steps -join "`n"), '(?ms)^ - .*?(?=^ - |\z)') | - Where-Object { - $stepMappingHeader = [regex]::Split( - $_.Value, - '(?m)^ {6}(?:run|"run"|''run'')[ \t]*:[ \t]*\|[-+]?[ \t]*(?:#.*)?$', - 2 - )[0] - $stepMappingHeader -match $selectorPattern - } | - ForEach-Object Value + Where-Object { + $stepMappingHeader = [regex]::Split( + $_.Value, + '(?m)^ {6}(?:run|"run"|''run'')[ \t]*:[ \t]*\|[-+]?[ \t]*(?:#.*)?$', + 2 + )[0] + $stepMappingHeader -match $selectorPattern + } | + ForEach-Object Value ) } @@ -419,7 +419,7 @@ Describe 'Hosted Unity review contracts' { $cacheActionReferences = @( Get-ActionReferences -Source $lookupAction | - Where-Object { $_ -match '^actions/cache(?:/restore)?@' } + Where-Object { $_ -match '^actions/cache(?:/restore)?@' } ) $cacheActionReferences.Count | Should -Be 1 $cacheActionReferences[0] | Should -Be 'actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' @@ -725,8 +725,8 @@ Describe 'Hosted Unity review contracts' { $pathMatch.Success | Should -BeTrue $pathLines = @( $pathMatch.Groups['paths'].Value -split "\r?\n" | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { $_.Trim() } + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + ForEach-Object { $_.Trim() } ) $artifactRoot = '${{ runner.temp }}\PureBase-Release-Validation-${{ github.run_id }}-${{ github.run_attempt }}' $pathLines.Count | Should -Be 2 @@ -875,3 +875,4 @@ Describe 'Hosted Unity review contracts' { $shadowDiagnostics.Contains('shadowRange') | Should -BeTrue } } + diff --git a/.github/tests/InstallPinnedUnityCli.Tests.ps1 b/.github/tests/InstallPinnedUnityCli.Tests.ps1 index b006a60..c75be9a 100644 --- a/.github/tests/InstallPinnedUnityCli.Tests.ps1 +++ b/.github/tests/InstallPinnedUnityCli.Tests.ps1 @@ -184,4 +184,4 @@ Describe 'Install-PinnedUnityCli' { Assert-MockCalled Move-Item -Times 1 -Exactly Assert-FailurePreservesDestination -ExpectedBytes ([byte[]](9, 9)) } -} \ No newline at end of file +} diff --git a/.github/tests/New-PureBaseCiProject.Tests.ps1 b/.github/tests/New-PureBaseCiProject.Tests.ps1 index 86692ad..a00514a 100644 --- a/.github/tests/New-PureBaseCiProject.Tests.ps1 +++ b/.github/tests/New-PureBaseCiProject.Tests.ps1 @@ -38,7 +38,7 @@ Describe 'Pure-Base CI Unity project generation' { $ownerLightingDataAssetPath = Join-Path $ownerLightingDataDirectory 'OwnerLightingData.asset' $ownerLightingDataMetaPath = "$ownerLightingDataAssetPath.meta" $ownerLightingDataGuid = [guid]::NewGuid().ToString('N') - New-Item -ItemType Directory -Path $pureBaseRoot,$shaderCoreRoot,$consumerSettings,$ownerLightingDataDirectory -Force | Out-Null + New-Item -ItemType Directory -Path $pureBaseRoot, $shaderCoreRoot, $consumerSettings, $ownerLightingDataDirectory -Force | Out-Null [IO.File]::WriteAllText( (Join-Path $pureBaseRoot 'package.json'), '{"name":"jp.penguin.purebase","version":"0.1.0"}', @@ -178,3 +178,4 @@ QualitySettings: Assert-CiProjectHarness -Condition ($null -ne $failure -and $failure.Exception.Message -like '*malformed GUID*') -Message 'The CI project builder accepted malformed owner LightingData metadata.' } } + diff --git a/.github/tests/PureBase.Automation.Tests.ps1 b/.github/tests/PureBase.Automation.Tests.ps1 index cdc377c..185c762 100644 --- a/.github/tests/PureBase.Automation.Tests.ps1 +++ b/.github/tests/PureBase.Automation.Tests.ps1 @@ -41,7 +41,7 @@ Describe 'Resume tag handling' { It 'rejects a tag pointing to another commit' { { Resolve-PureBaseResumeTagAction -HeadSha 'abcdef' -ExistingTagSha '123456' } | - Should -Throw '*different commit*' + Should -Throw '*different commit*' } } @@ -109,7 +109,7 @@ Describe 'VPM dispatch payload' { -Repository 'Penguin-Repository/Pure-Base' ` -Version '0.2.0' ` -AssetName 'jp.penguin.purebase-0.2.0.zip' | - Should -Be 'https://github.com/Penguin-Repository/Pure-Base/releases/download/0.2.0/jp.penguin.purebase-0.2.0.zip' + Should -Be 'https://github.com/Penguin-Repository/Pure-Base/releases/download/0.2.0/jp.penguin.purebase-0.2.0.zip' } It 'includes the URL and SHA-256 in repository_dispatch data' { @@ -264,7 +264,7 @@ Describe 'Immutable Releases preflight' { -Repository 'Penguin-Repository/Pure-Base' ` -Token 'token' ` -ApiInvoker $invoker } | - Should -Throw '*must be enabled*' + Should -Throw '*must be enabled*' } It 'rejects an unexpected disabled response' { @@ -274,7 +274,7 @@ Describe 'Immutable Releases preflight' { -Repository 'Penguin-Repository/Pure-Base' ` -Token 'token' ` -ApiInvoker $invoker } | - Should -Throw '*did not confirm*' + Should -Throw '*did not confirm*' } } @@ -308,7 +308,7 @@ Describe 'Published immutable release reuse' { $release = $publishedRelease.PSObject.Copy() $release.immutable = $false { Resolve-PureBasePublishedArtifact -Release $release -AssetName $assetName } | - Should -Throw '*immutable*' + Should -Throw '*immutable*' } It 'rejects a published asset without a valid digest' { @@ -318,12 +318,12 @@ Describe 'Published immutable release reuse' { assets = @([pscustomobject]@{ name = $assetName; state = 'uploaded'; digest = ''; browser_download_url = 'https://example.invalid/file.zip' }) } { Resolve-PureBasePublishedArtifact -Release $release -AssetName $assetName } | - Should -Throw '*valid SHA-256 digest*' + Should -Throw '*valid SHA-256 digest*' } It 'validates an expected digest when one is supplied' { { Resolve-PureBasePublishedArtifact -Release $publishedRelease -AssetName $assetName -ExpectedSha256 ('b' * 64) } | - Should -Throw '*expected SHA-256*' + Should -Throw '*expected SHA-256*' } } @@ -365,7 +365,7 @@ Describe 'Production workflow integration' { $activityTypes = @( [regex]::Matches($triggerMatch.Groups['body'].Value, '(?m)^\s+-\s+([^\s]+)\s*$') | - ForEach-Object { $_.Groups[1].Value } + ForEach-Object { $_.Groups[1].Value } ) $activityTypes.Count | Should -Be 4 ($activityTypes -join ',') | Should -Be 'opened,synchronize,reopened,ready_for_review' @@ -461,19 +461,19 @@ Describe 'Validated artifact fresh release orchestration' { } $expectedAssetDigest = '' if ($null -ne $Release) { - $matchingAssets = @($Release.assets | Where-Object name -eq $AssetName | Select-Object -First 1) + $matchingAssets = @($Release.assets | Where-Object name -EQ $AssetName | Select-Object -First 1) if ($matchingAssets.Count -eq 1 -and $null -ne $matchingAssets[0].PSObject.Properties['digest']) { $expectedAssetDigest = [string]$matchingAssets[0].digest } } [pscustomobject]@{ - Exists = ($null -ne $Release) - TagName = if ($null -eq $Release) { '' } else { [string]$Release.tag_name } - TargetCommitish = if ($null -eq $Release) { '' } else { [string]$Release.target_commitish } - Draft = if ($null -eq $Release) { $null } else { [bool]$Release.draft } - Immutable = if ($null -eq $Release) { $null } else { [bool]$Release.immutable } - Body = if ($null -eq $Release) { '' } else { [string]$Release.body } - Assets = $assets + Exists = ($null -ne $Release) + TagName = if ($null -eq $Release) { '' } else { [string]$Release.tag_name } + TargetCommitish = if ($null -eq $Release) { '' } else { [string]$Release.target_commitish } + Draft = if ($null -eq $Release) { $null } else { [bool]$Release.draft } + Immutable = if ($null -eq $Release) { $null } else { [bool]$Release.immutable } + Body = if ($null -eq $Release) { '' } else { [string]$Release.body } + Assets = $assets ExpectedAssetDigest = $expectedAssetDigest } } @@ -569,7 +569,7 @@ Describe 'Validated artifact fresh release orchestration' { $validationArtifactPayload = Join-Path $validationArtifactStaging 'validated-package' New-Item -ItemType Directory -Path $validationArtifactPayload -Force | Out-Null Get-ChildItem -LiteralPath $validatedPackageDirectory -Force | - Copy-Item -Destination $validationArtifactPayload -Recurse -Force + Copy-Item -Destination $validationArtifactPayload -Recurse -Force [IO.Compression.ZipFile]::CreateFromDirectory($validationArtifactStaging, $validationArtifactArchive) $hookLogPath = $pushLogPath.Replace('\', '/') @@ -647,20 +647,20 @@ Describe 'Validated artifact fresh release orchestration' { param($Method, $Uri, $Body, $InFile) $assetDigest = '' if ($null -ne $release) { - $matchingAssets = @($release.assets | Where-Object name -eq $assetName | Select-Object -First 1) + $matchingAssets = @($release.assets | Where-Object name -EQ $assetName | Select-Object -First 1) if ($matchingAssets.Count -eq 1 -and $null -ne $matchingAssets[0].PSObject.Properties['digest']) { $assetDigest = [string]$matchingAssets[0].digest } } $apiCall = [pscustomobject]@{ - Method = $Method; Uri = $Uri; Body = $Body; InFile = $InFile - ReleaseDraft = if ($null -eq $release) { $null } else { $release.draft } - ReleaseImmutable = if ($null -eq $release) { $null } else { $release.immutable } - AssetDigest = $assetDigest - CreateResponseHasAssets = $null - StateBefore = Get-ValidatedArtifactReleaseState -Release $release -AssetName $assetName - StateAfter = $null - } + Method = $Method; Uri = $Uri; Body = $Body; InFile = $InFile + ReleaseDraft = if ($null -eq $release) { $null } else { $release.draft } + ReleaseImmutable = if ($null -eq $release) { $null } else { $release.immutable } + AssetDigest = $assetDigest + CreateResponseHasAssets = $null + StateBefore = Get-ValidatedArtifactReleaseState -Release $release -AssetName $assetName + StateAfter = $null + } $apiCalls.Add($apiCall) | Out-Null $operationLog.Add([pscustomobject]@{ Kind = 'api'; Boundary = ''; ApiCall = $apiCall }) | Out-Null if ($Uri -match '/immutable-releases$') { @@ -671,7 +671,7 @@ Describe 'Validated artifact fresh release orchestration' { $operationLog.Add([pscustomobject]@{ Kind = 'validation-run'; Boundary = ''; ApiCall = $apiCall }) | Out-Null $apiCall.StateAfter = Get-ValidatedArtifactReleaseState -Release $release -AssetName $assetName return [pscustomobject]@{ - workflow_runs = @([pscustomobject]@{ + workflow_runs = @([pscustomobject]@{ id = 11; path = '.github/workflows/release-validation.yml'; head_sha = $eventSha; head_branch = 'master' event = 'workflow_dispatch'; run_number = 11; run_attempt = 2; status = 'completed'; conclusion = 'success' }) @@ -682,7 +682,7 @@ Describe 'Validated artifact fresh release orchestration' { $apiCall.StateAfter = Get-ValidatedArtifactReleaseState -Release $release -AssetName $assetName return [pscustomobject]@{ total_count = 1 - artifacts = @([pscustomobject]@{ + artifacts = @([pscustomobject]@{ id = 7; node_id = 'MDg6QXJ0aWZhY3Q3'; name = 'pure-base-release-validation-11-2'; size_in_bytes = 1024 url = 'https://api.example.invalid/repos/test/Pure-Base/actions/artifacts/7'; archive_download_url = 'https://api.example.invalid/repos/test/Pure-Base/actions/artifacts/7/zip' expired = $false; created_at = '2026-08-01T00:00:00Z'; updated_at = '2026-08-01T00:00:00Z'; expires_at = '2026-08-31T00:00:00Z' @@ -715,7 +715,7 @@ Describe 'Validated artifact fresh release orchestration' { } if ($Uri -match '/releases\?per_page=100$') { $apiCall.StateAfter = Get-ValidatedArtifactReleaseState -Release $release -AssetName $assetName - return ,([object[]]@()) + return , ([object[]]@()) } if ($Method -eq 'POST' -and $Uri -match '/repos/test/Pure-Base/releases$') { $create = $Body | ConvertFrom-Json @@ -863,7 +863,7 @@ Describe 'Validated artifact fresh release orchestration' { $eventPackage = (& git -C $fixture.RemoteRoot show "$($fixture.EventSha):package.json" | ConvertFrom-Json) $archive = [IO.Compression.ZipFile]::OpenRead($fixture.ZipPath) try { - $entry = @($archive.Entries | Where-Object FullName -ceq 'package.json') + $entry = @($archive.Entries | Where-Object FullName -CEQ 'package.json') $entry.Count | Should -Be 1 $reader = [IO.StreamReader]::new($entry[0].Open(), [Text.UTF8Encoding]::new($false, $true)) try { $archivePackage = $reader.ReadToEnd() | ConvertFrom-Json } @@ -902,14 +902,14 @@ Describe 'Validated artifact fresh release orchestration' { $fixture = New-ValidatedArtifactReleaseFixture try { $fixture.Failure | Should -BeNullOrEmpty - $firstMutationIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]]{ + $firstMutationIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]] { param($entry) $entry.Kind -eq 'gate' -and $entry.Boundary -eq 'tag-push' }) $firstMutationIndex | Should -BeGreaterThan -1 foreach ($validationOperation in @('validation-run', 'validation-artifact', 'validation-archive-request', 'validation-archive-download')) { - $operationIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]]{ + $operationIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]] { param($entry) $entry.Kind -eq $validationOperation }.GetNewClosure()) @@ -917,7 +917,7 @@ Describe 'Validated artifact fresh release orchestration' { $operationIndex | Should -BeLessThan $firstMutationIndex } - $preflightIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]]{ + $preflightIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]] { param($entry) $entry.Kind -eq 'api' -and $entry.ApiCall.Uri -match '/immutable-releases$' }) @@ -985,23 +985,23 @@ Describe 'Validated artifact fresh release orchestration' { $fixture = New-ValidatedArtifactReleaseFixture -CreateResponseWithoutAssets try { $fixture.Failure | Should -BeNullOrEmpty - $draftCreateIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ + $draftCreateIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'POST' -and $call.Uri -eq 'https://api.example.invalid/repos/test/Pure-Base/releases' }) - $canonicalReleaseIndex = [array]::FindIndex($fixture.ApiCalls, $draftCreateIndex + 1, [Predicate[object]]{ + $canonicalReleaseIndex = [array]::FindIndex($fixture.ApiCalls, $draftCreateIndex + 1, [Predicate[object]] { param($call) $call.Method -eq 'GET' -and $call.Uri -eq 'https://api.example.invalid/repos/test/Pure-Base/releases/42' }) - $assetUploadIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ + $assetUploadIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'POST' -and $call.Uri -match '/assets\?name=' }) - $publishIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ + $publishIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'PATCH' -and $call.Uri -match '/releases/42$' }) - $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ + $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'POST' -and $call.Uri -match '/dispatches$' }) @@ -1077,7 +1077,7 @@ Describe 'Validated artifact fresh release orchestration' { $fixture.Failure | Should -Not -BeNullOrEmpty $fixture.Failure.Exception.Message | Should -Match 'remote release branch.*advanced' $fixture.MutationBoundaries | Should -Contain $Boundary - $gateIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]]{ param($entry) $entry.Kind -eq 'gate' -and $entry.Boundary -eq $Boundary }) + $gateIndex = [array]::FindIndex($fixture.OperationLog, [Predicate[object]] { param($entry) $entry.Kind -eq 'gate' -and $entry.Boundary -eq $Boundary }) $gateIndex | Should -BeGreaterThan -1 $postGateMutations = @($fixture.OperationLog | Select-Object -Skip ($gateIndex + 1) | Where-Object { $_.Kind -eq 'api' -and $_.ApiCall.Method -in @('POST', 'PATCH', 'DELETE') @@ -1270,7 +1270,7 @@ Describe 'Validated artifact fresh release orchestration' { @($fixture.ApiCalls | Where-Object { $_.Method -eq 'DELETE' -or ($_.Method -eq 'POST' -and $_.Uri -match '/assets\?name=') -or ($_.Method -eq 'PATCH' -and $_.Uri -match '/releases/42$') }).Count | Should -Be 0 $fixture.Release.draft | Should -BeTrue $fixture.Release.immutable | Should -BeFalse - @($fixture.Release.assets | Where-Object name -eq $fixture.AssetName).Count | Should -Be $ExpectedAssetCount + @($fixture.Release.assets | Where-Object name -EQ $fixture.AssetName).Count | Should -Be $ExpectedAssetCount $fixture.MutationBoundaries | Should -Not -Contain 'publish' $fixture.MutationBoundaries | Should -Not -Contain 'vpm-dispatch' } @@ -1284,8 +1284,8 @@ Describe 'Validated artifact fresh release orchestration' { @($fixture.ApiCalls | Where-Object { $_.Method -eq 'PATCH' -or $_.Method -eq 'DELETE' -or ($_.Method -eq 'POST' -and ($_.Uri -match '/releases$' -or $_.Uri -match '/assets\?name=')) }).Count | Should -Be 0 - $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'POST' -and $call.Uri -match '/dispatches$' }) - $verifiedPublishedIndex = [array]::FindLastIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/tags/' -and $call.StateBefore.Exists -and -not $call.StateBefore.Draft -and $call.StateBefore.Immutable -and $call.StateBefore.TagName -eq $fixture.Version -and $call.StateBefore.TargetCommitish -eq $fixture.EventSha -and $call.StateBefore.ExpectedAssetDigest -eq "sha256:$($fixture.ZipSha256)" }) + $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'POST' -and $call.Uri -match '/dispatches$' }) + $verifiedPublishedIndex = [array]::FindLastIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/tags/' -and $call.StateBefore.Exists -and -not $call.StateBefore.Draft -and $call.StateBefore.Immutable -and $call.StateBefore.TagName -eq $fixture.Version -and $call.StateBefore.TargetCommitish -eq $fixture.EventSha -and $call.StateBefore.ExpectedAssetDigest -eq "sha256:$($fixture.ZipSha256)" }) $verifiedPublishedIndex | Should -BeGreaterThan -1 $dispatchIndex | Should -BeGreaterThan $verifiedPublishedIndex $fixture.DispatchPayloads.Count | Should -Be 1 @@ -1309,8 +1309,8 @@ Describe 'Validated artifact fresh release orchestration' { $_.Method -in @('POST', 'PATCH', 'DELETE') -and ($_.Uri -match '/releases$' -or $_.Uri -match '/releases/42$' -or $_.Uri -match '/assets\?name=') }).Count | Should -Be 0 - $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'POST' -and $call.Uri -match '/dispatches$' }) - $verifiedPublishedIndex = [array]::FindLastIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/tags/' -and $call.StateBefore.Exists -and -not $call.StateBefore.Draft -and $call.StateBefore.Immutable -and $call.StateBefore.TagName -eq $fixture.Version -and $call.StateBefore.TargetCommitish -eq $fixture.EventSha -and $call.StateBefore.ExpectedAssetDigest -eq "sha256:$($fixture.ZipSha256)" }) + $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'POST' -and $call.Uri -match '/dispatches$' }) + $verifiedPublishedIndex = [array]::FindLastIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/tags/' -and $call.StateBefore.Exists -and -not $call.StateBefore.Draft -and $call.StateBefore.Immutable -and $call.StateBefore.TagName -eq $fixture.Version -and $call.StateBefore.TargetCommitish -eq $fixture.EventSha -and $call.StateBefore.ExpectedAssetDigest -eq "sha256:$($fixture.ZipSha256)" }) $verifiedPublishedIndex | Should -BeGreaterThan -1 $dispatchIndex | Should -BeGreaterThan $verifiedPublishedIndex $fixture.DispatchPayloads.Count | Should -Be 1 @@ -1336,9 +1336,9 @@ Describe 'Validated artifact fresh release orchestration' { $fixture = New-ValidatedArtifactReleaseFixture -ReleaseState 'draft-resume' -DraftAssetState 'absent' try { $fixture.Failure | Should -BeNullOrEmpty - $uploadIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'POST' -and $call.Uri -match '/assets\?name=' }) - $publishIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'PATCH' -and $call.Uri -match '/releases/42$' -and $call.Body -match '"draft":false' }) - $verificationIndex = [array]::FindIndex($fixture.ApiCalls, $uploadIndex + 1, $publishIndex - $uploadIndex - 1, [Predicate[object]]{ param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/42$' -and $call.AssetDigest -eq "sha256:$($fixture.ZipSha256)" }) + $uploadIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'POST' -and $call.Uri -match '/assets\?name=' }) + $publishIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'PATCH' -and $call.Uri -match '/releases/42$' -and $call.Body -match '"draft":false' }) + $verificationIndex = [array]::FindIndex($fixture.ApiCalls, $uploadIndex + 1, $publishIndex - $uploadIndex - 1, [Predicate[object]] { param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/42$' -and $call.AssetDigest -eq "sha256:$($fixture.ZipSha256)" }) $uploadIndex | Should -BeGreaterThan -1 $verificationIndex | Should -BeGreaterThan $uploadIndex $publishIndex | Should -BeGreaterThan $verificationIndex @@ -1357,9 +1357,9 @@ Describe 'Validated artifact fresh release orchestration' { $fixture = New-ValidatedArtifactReleaseFixture try { $fixture.Failure | Should -BeNullOrEmpty - $publishIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'PATCH' -and $call.Uri -match '/releases/42$' -and $call.Body -match '"draft":false' }) - $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]]{ param($call) $call.Method -eq 'POST' -and $call.Uri -match '/dispatches$' }) - $confirmationIndex = [array]::FindIndex($fixture.ApiCalls, $publishIndex + 1, $dispatchIndex - $publishIndex - 1, [Predicate[object]]{ param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/tags/' -and -not $call.ReleaseDraft -and $call.ReleaseImmutable -and $call.AssetDigest -eq "sha256:$($fixture.ZipSha256)" }) + $publishIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'PATCH' -and $call.Uri -match '/releases/42$' -and $call.Body -match '"draft":false' }) + $dispatchIndex = [array]::FindIndex($fixture.ApiCalls, [Predicate[object]] { param($call) $call.Method -eq 'POST' -and $call.Uri -match '/dispatches$' }) + $confirmationIndex = [array]::FindIndex($fixture.ApiCalls, $publishIndex + 1, $dispatchIndex - $publishIndex - 1, [Predicate[object]] { param($call) $call.Method -eq 'GET' -and $call.Uri -match '/releases/tags/' -and -not $call.ReleaseDraft -and $call.ReleaseImmutable -and $call.AssetDigest -eq "sha256:$($fixture.ZipSha256)" }) $publishIndex | Should -BeGreaterThan -1 $confirmationIndex | Should -BeGreaterThan $publishIndex $dispatchIndex | Should -BeGreaterThan $confirmationIndex @@ -1385,7 +1385,7 @@ Describe 'Prerelease release naming and dispatch' { It 'preserves exact prerelease text in the package URL' { New-PureBasePackageUrl -Repository 'Penguin-Repository/Pure-Base' -Version $version -AssetName $assetName | - Should -Be "https://github.com/Penguin-Repository/Pure-Base/releases/download/$version/$assetName" + Should -Be "https://github.com/Penguin-Repository/Pure-Base/releases/download/$version/$assetName" } It 'preserves exact prerelease text in the tag, name, ZIP, release URL, and dispatch payload' { @@ -1451,7 +1451,7 @@ Describe 'VPM yank policy dispatch preflight' { $apiInvoker = { param($Method, $Uri, $Token, $Body) $calls.Add($Uri) | Out-Null }.GetNewClosure() { Invoke-PureBaseYankDispatch -PolicyPath $policyPath -PolicyCommitSha ('a' * 40) -ApiInvoker $apiInvoker } | - Should -Throw + Should -Throw $calls.Count | Should -Be 0 } @@ -2060,7 +2060,7 @@ Describe 'Validated artifact archive and mutation gate contracts' { $requestInvoker = { param($Method, $Uri, $Headers, $OutFile, $MaximumRedirection) return $null } { Invoke-PureBaseArtifactRequestWithoutRedirect -RequestInvoker $requestInvoker -Uri 'https://objects.example.invalid/archive.zip' -Headers @{ 'User-Agent' = 'Pure-Base-Actions' } -OutFile 'null-response.zip' } | - Should -Throw '*response contract violation*' + Should -Throw '*response contract violation*' } } @@ -2158,3 +2158,4 @@ Describe 'Validated artifact archive and mutation gate contracts' { } } + diff --git a/.github/tests/ReleaseAuthorization.Tests.ps1 b/.github/tests/ReleaseAuthorization.Tests.ps1 index 44f9b51..56fa6f3 100644 --- a/.github/tests/ReleaseAuthorization.Tests.ps1 +++ b/.github/tests/ReleaseAuthorization.Tests.ps1 @@ -108,11 +108,11 @@ Describe 'Release authorization workflow contract' { [IO.File]::WriteAllBytes($assetPath, [byte[]](1, 2, 3, 4)) $sha256 = (Get-FileHash -LiteralPath $assetPath -Algorithm SHA256).Hash.ToLowerInvariant() $state = [ordered]@{ - phase = 'completed' - commitSha = 'b' * 40 - releaseUrl = 'https://github.com/Penguin-Repository/Pure-Base/releases/tag/0.1.0' + phase = 'completed' + commitSha = 'b' * 40 + releaseUrl = 'https://github.com/Penguin-Repository/Pure-Base/releases/tag/0.1.0' vpmRepository = 'Penguin-Repository/Pure-Base-Repository' - sha256 = $sha256 + sha256 = $sha256 } [IO.File]::WriteAllText( (Join-Path $artifactRoot 'release-state.json'), @@ -140,3 +140,4 @@ Describe 'Release authorization workflow contract' { ([regex]::Matches($workflow, '(?m)^ if: inputs\.preflight_only == false$')).Count | Should -Be 3 } } + diff --git a/.github/tests/ReleasePublishTag.Tests.ps1 b/.github/tests/ReleasePublishTag.Tests.ps1 index 1fd7132..9c756be 100644 --- a/.github/tests/ReleasePublishTag.Tests.ps1 +++ b/.github/tests/ReleasePublishTag.Tests.ps1 @@ -34,8 +34,8 @@ Describe 'Release publication tag preservation' { It 'accepts a published release whose SHA differs only by case' { $release = [pscustomobject]@{ - id = 42 - tag_name = '0.1.0-beta.4' + id = 42 + tag_name = '0.1.0-beta.4' target_commitish = 'A' * 40 } @@ -50,8 +50,8 @@ Describe 'Release publication tag preservation' { It 'rejects an unexpected release tag or release ID' { $release = [pscustomobject]@{ - id = 43 - tag_name = 'untagged-f42a63fceb89b817fe6d' + id = 43 + tag_name = 'untagged-f42a63fceb89b817fe6d' target_commitish = 'a' * 40 } @@ -86,3 +86,4 @@ Describe 'Release publication tag preservation' { $delays.ToArray() | Should -Be @(250, 500, 1000, 2000) } } + diff --git a/.github/tests/ReleaseValidationIsolation.Tests.ps1 b/.github/tests/ReleaseValidationIsolation.Tests.ps1 index d471662..e188d05 100644 --- a/.github/tests/ReleaseValidationIsolation.Tests.ps1 +++ b/.github/tests/ReleaseValidationIsolation.Tests.ps1 @@ -52,3 +52,4 @@ Describe 'Release validation Unity import isolation' { $workflow | Should -Match ([regex]::Escape("Write-Host 'Repository working tree changes:'")) } } + diff --git a/.github/tests/RepositoryLineEndings.Tests.ps1 b/.github/tests/RepositoryLineEndings.Tests.ps1 index 5a4a098..93053f4 100644 --- a/.github/tests/RepositoryLineEndings.Tests.ps1 +++ b/.github/tests/RepositoryLineEndings.Tests.ps1 @@ -111,7 +111,7 @@ BeforeAll { $output = [string[]]@(@($stdout, $stderr) | Where-Object { -not [string]::IsNullOrEmpty($_) }) return [pscustomobject]@{ ExitCode = $process.ExitCode - Output = $output + Output = $output } } finally { @@ -369,4 +369,4 @@ Describe 'Repository line-ending checker' { $result.ExitCode | Should -Be 0 $result.Output | Should -BeNullOrEmpty } -} \ No newline at end of file +} diff --git a/.github/tests/ShaderCorePhaseCompatibility.Tests.ps1 b/.github/tests/ShaderCorePhaseCompatibility.Tests.ps1 index 4e505d9..dd73ea9 100644 --- a/.github/tests/ShaderCorePhaseCompatibility.Tests.ps1 +++ b/.github/tests/ShaderCorePhaseCompatibility.Tests.ps1 @@ -27,7 +27,7 @@ Describe 'Shader-Core phase compatibility' { Get-ChildItem -LiteralPath $releaseModuleRoot -Recurse -File -Filter '*.hlsl' | ForEach-Object { [pscustomobject]@{ - Path = $_.FullName + Path = $_.FullName Source = Get-Content -LiteralPath $_.FullName -Raw } } @@ -72,3 +72,4 @@ Describe 'Shader-Core phase compatibility' { $tail | Should -Not -Match 'UNITY_APPLY_FOG' } } + diff --git a/.github/tests/UnityMetadata.Tests.ps1 b/.github/tests/UnityMetadata.Tests.ps1 index 8427881..b2d02a6 100644 --- a/.github/tests/UnityMetadata.Tests.ps1 +++ b/.github/tests/UnityMetadata.Tests.ps1 @@ -52,9 +52,9 @@ Describe 'Unity documentation metadata' { ) $missingMetadata = @( $documentationAssets | - Where-Object { -not $_.Name.EndsWith('.meta', [StringComparison]::Ordinal) } | - Where-Object { -not (Test-Path -LiteralPath ($_.FullName + '.meta') -PathType Leaf) } | - ForEach-Object { $_.FullName.Substring($repositoryRoot.Length + 1).Replace('\', '/') } + Where-Object { -not $_.Name.EndsWith('.meta', [StringComparison]::Ordinal) } | + Where-Object { -not (Test-Path -LiteralPath ($_.FullName + '.meta') -PathType Leaf) } | + ForEach-Object { $_.FullName.Substring($repositoryRoot.Length + 1).Replace('\', '/') } ) $missingMetadata | Should -BeNullOrEmpty @@ -63,15 +63,16 @@ Describe 'Unity documentation metadata' { It 'uses unique 32-character hexadecimal GUIDs for documentation metadata' { $guidEntries = @( Get-DocumentationMetadataFiles | - ForEach-Object { - [pscustomobject]@{ - Path = $_.FullName.Substring($repositoryRoot.Length + 1).Replace('\', '/') - Guid = Get-UnityMetadataGuid -Path $_.FullName + ForEach-Object { + [pscustomobject]@{ + Path = $_.FullName.Substring($repositoryRoot.Length + 1).Replace('\', '/') + Guid = Get-UnityMetadataGuid -Path $_.FullName + } } - } ) @($guidEntries | Where-Object { [string]::IsNullOrEmpty($_.Guid) }).Path | Should -BeNullOrEmpty - @($guidEntries | Group-Object Guid | Where-Object Count -gt 1).Name | Should -BeNullOrEmpty + @($guidEntries | Group-Object Guid | Where-Object Count -GT 1).Name | Should -BeNullOrEmpty } } + diff --git a/Tests/Parity/Validate-PureBaseParity.Oracle.ps1 b/Tests/Parity/Validate-PureBaseParity.Oracle.ps1 index ac83f78..cadd6de 100644 --- a/Tests/Parity/Validate-PureBaseParity.Oracle.ps1 +++ b/Tests/Parity/Validate-PureBaseParity.Oracle.ps1 @@ -106,4 +106,4 @@ function Get-ValidationSceneOracleValue { } default { throw "Unsupported fixed validation-scene oracle '$Name'." } } -} \ No newline at end of file +} diff --git a/Tests/Parity/Validate-PureBaseParity.Tests.ps1 b/Tests/Parity/Validate-PureBaseParity.Tests.ps1 index 067b046..f25905a 100644 --- a/Tests/Parity/Validate-PureBaseParity.Tests.ps1 +++ b/Tests/Parity/Validate-PureBaseParity.Tests.ps1 @@ -22,250 +22,250 @@ Describe 'Validate-PureBaseParity synthetic artifact contracts' { BeforeAll { Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - . (Join-Path $PSScriptRoot 'Validate-PureBaseParity.Oracle.ps1') + . (Join-Path $PSScriptRoot 'Validate-PureBaseParity.Oracle.ps1') -function Assert-Harness { - param([Parameter(Mandatory = $true)][bool]$Condition, [Parameter(Mandatory = $true)][string]$Message) - if (-not $Condition) { throw $Message } -} + function Assert-Harness { + param([Parameter(Mandatory = $true)][bool]$Condition, [Parameter(Mandatory = $true)][string]$Message) + if (-not $Condition) { throw $Message } + } -function Write-Json { - param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)]$Value) - [void](New-Item -ItemType Directory -Path (Split-Path -Parent $Path) -Force) - $Value | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $Path -Encoding UTF8 -} + function Write-Json { + param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)]$Value) + [void](New-Item -ItemType Directory -Path (Split-Path -Parent $Path) -Force) + $Value | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $Path -Encoding UTF8 + } -function Write-NUnit { - param([Parameter(Mandatory = $true)][string]$Path, [Parameter()][string]$FullName = 'PureBase.Tests.Daily.Synthetic', [Parameter()][int]$Failed = 0) - [void](New-Item -ItemType Directory -Path (Split-Path -Parent $Path) -Force) - $result = if ($Failed -eq 0) { 'Passed' } else { 'Failed' } - $passed = if ($Failed -eq 0) { 1 } else { 0 } - $xml = "" - [System.IO.File]::WriteAllText($Path, $xml, (New-Object System.Text.UTF8Encoding($false))) -} + function Write-NUnit { + param([Parameter(Mandatory = $true)][string]$Path, [Parameter()][string]$FullName = 'PureBase.Tests.Daily.Synthetic', [Parameter()][int]$Failed = 0) + [void](New-Item -ItemType Directory -Path (Split-Path -Parent $Path) -Force) + $result = if ($Failed -eq 0) { 'Passed' } else { 'Failed' } + $passed = if ($Failed -eq 0) { 1 } else { 0 } + $xml = "" + [System.IO.File]::WriteAllText($Path, $xml, (New-Object System.Text.UTF8Encoding($false))) + } -function Add-ZipEntry { - param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$EntryName) - Add-Type -AssemblyName System.IO.Compression.FileSystem - $archive = [System.IO.Compression.ZipFile]::Open($Path, [System.IO.Compression.ZipArchiveMode]::Update) - try { - $entry = $archive.CreateEntry($EntryName) - $writer = New-Object System.IO.StreamWriter($entry.Open()) - try { $writer.Write('forbidden') } finally { $writer.Dispose() } - } - finally { $archive.Dispose() } -} + function Add-ZipEntry { + param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$EntryName) + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::Open($Path, [System.IO.Compression.ZipArchiveMode]::Update) + try { + $entry = $archive.CreateEntry($EntryName) + $writer = New-Object System.IO.StreamWriter($entry.Open()) + try { $writer.Write('forbidden') } finally { $writer.Dispose() } + } + finally { $archive.Dispose() } + } -function Remove-ZipEntry { - param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$EntryName) - Add-Type -AssemblyName System.IO.Compression.FileSystem - $archive = [System.IO.Compression.ZipFile]::Open($Path, [System.IO.Compression.ZipArchiveMode]::Update) - try { - $entry = @($archive.Entries | Where-Object { $_.FullName.Replace('\', '/') -eq $EntryName }) | Select-Object -First 1 - if ($null -eq $entry) { throw "Synthetic ZIP entry '$EntryName' does not exist." } - $entry.Delete() - } - finally { $archive.Dispose() } -} + function Remove-ZipEntry { + param([Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$EntryName) + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::Open($Path, [System.IO.Compression.ZipArchiveMode]::Update) + try { + $entry = @($archive.Entries | Where-Object { $_.FullName.Replace('\', '/') -eq $EntryName }) | Select-Object -First 1 + if ($null -eq $entry) { throw "Synthetic ZIP entry '$EntryName' does not exist." } + $entry.Delete() + } + finally { $archive.Dispose() } + } -function New-Hash { - param([Parameter(Mandatory = $true)][string]$Character) - return $Character * 64 -} + function New-Hash { + param([Parameter(Mandatory = $true)][string]$Character) + return $Character * 64 + } -function New-PathEntries { - param([Parameter(Mandatory = $true)][int]$Count, [Parameter(Mandatory = $true)][string]$Prefix, [Parameter(Mandatory = $true)][string]$HashCharacter) - return @( - 1..$Count | ForEach-Object { - [ordered]@{ path = ('{0}/{1:D2}.asset' -f $Prefix, $_); sha256 = New-Hash -Character $HashCharacter } + function New-PathEntries { + param([Parameter(Mandatory = $true)][int]$Count, [Parameter(Mandatory = $true)][string]$Prefix, [Parameter(Mandatory = $true)][string]$HashCharacter) + return @( + 1..$Count | ForEach-Object { + [ordered]@{ path = ('{0}/{1:D2}.asset' -f $Prefix, $_); sha256 = New-Hash -Character $HashCharacter } + } + ) } - ) -} -function New-ChangedPathEntries { - return @( - [ordered]@{ path = 'ProjectSettings/Changed01.asset'; preBootstrapSha256 = New-Hash -Character 'd'; postBootstrapSha256 = New-Hash -Character 'e' }, - [ordered]@{ path = 'ProjectSettings/Changed02.asset'; preBootstrapSha256 = New-Hash -Character 'f'; postBootstrapSha256 = New-Hash -Character '0' } - ) -} + function New-ChangedPathEntries { + return @( + [ordered]@{ path = 'ProjectSettings/Changed01.asset'; preBootstrapSha256 = New-Hash -Character 'd'; postBootstrapSha256 = New-Hash -Character 'e' }, + [ordered]@{ path = 'ProjectSettings/Changed02.asset'; preBootstrapSha256 = New-Hash -Character 'f'; postBootstrapSha256 = New-Hash -Character '0' } + ) + } -function New-SyntheticArtifacts { - param( - [Parameter(Mandatory = $true)]$Manifest, - [Parameter(Mandatory = $true)]$ReleaseContentContract, - [Parameter()][string]$PackageVersion = '0.1.0' - ) + function New-SyntheticArtifacts { + param( + [Parameter(Mandatory = $true)]$Manifest, + [Parameter(Mandatory = $true)]$ReleaseContentContract, + [Parameter()][string]$PackageVersion = '0.1.0' + ) - $root = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseParity-' + [guid]::NewGuid().ToString('N')) - $legacy = Join-Path $root 'legacy' - $daily = Join-Path $root 'daily' - $release = Join-Path $root 'release' - [void](New-Item -ItemType Directory -Path $legacy, $daily, $release -Force) - foreach ($invocation in @($Manifest.nunitInvocations)) { - $id = [string]$invocation.id - Write-NUnit -Path (Join-Path $legacy ($id + '/' + $id + '.NUnit.xml')) -FullName ([string]$invocation.nunitFullName) - } - Write-Json -Path (Join-Path $legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json') -Value ([ordered]@{ - staticLightmapCount = 2 - staticLightmaps = @(1..20 | ForEach-Object { [ordered]@{ renderer = "Renderer$_"; lightmapIndex = 0; scaleOffsetX = 0.1; scaleOffsetY = 0.1; scaleOffsetZ = 0.0; scaleOffsetW = 0.0 } }) - metaAlbedo = @(1..18 | ForEach-Object { [ordered]@{ material = "Material$_"; shader = 'PureBase/Unlit'; meanLuminance = 0.25 } }) - shadowChangedPixelCount = 967 - variants = @(1..56 | ForEach-Object { [ordered]@{ shader = 'PureBase/Unlit'; pass = 'ForwardBase'; label = "Variant$_"; keywords = @(); added = $true; warmed = $true; variantCount = 1 } }) - }) - Write-Json -Path (Join-Path $legacy 'validation-scene-birp-feasibility.json') -Value ([ordered]@{ - check = 'Fixed BIRP validation scene synchronous bake, static lightmap, Meta readback, shadow silhouette, and representative variant warmup' - status = 'PASSED' - unityVersion = '2022.3.22f1' - graphicsApi = 'D3D11' - testName = 'PureBase.Integration.Tests.PureBaseValidationSceneTests.FixedValidationSceneBakesAndRequestsRepresentativeBirpVariants' - artifactPath = 'C:/Temp/PureBaseParity/validation-scene-birp' - dynamicLightmapStatus = 'NOT_DETERMINISTIC_IN_BATCH_EDITMODE' - }) - Write-Json -Path (Join-Path $legacy 'birp-probe-feasibility.json') -Value ([ordered]@{ - check = 'BIRP probe finite black and box-projected reflection-probe readback' - status = 'PASSED' - unityVersion = '2022.3.22f1' - graphicsApi = 'D3D11' - testNames = @('PureBase.Integration.Tests.BirpGiProbeReadbackTests.BlackProbePathProducesFiniteHdrReadbackWithMeshCoverage', 'PureBase.Integration.Tests.BirpGiProbeReadbackTests.BoxProjectedReflectionProbePathProducesFiniteHdrReadbackWithMeshCoverage') - artifactPaths = @('C:/Temp/PureBaseParity/birp-black', 'C:/Temp/PureBaseParity/birp-box') - }) - Write-Json -Path (Join-Path $legacy 'release-boundary-audit.json') -Value ([ordered]@{ - check = 'PureBase release boundary and metallic property ABI' - status = 'PASSED' - packagePath = 'C:/Temp/PureBaseParity/package' - trackedScmodulePaths = @('Tests/Fixtures/Hosts/Phase/base/test.scmodule') - approvedTrackedScmodulePaths = @('Tests/Fixtures/Hosts/Phase/base/test.scmodule') - unapprovedTrackedScmodulePaths = @() - missingTrackedScmodulePaths = @() - trackedScmodulePathsExactlyApproved = $true - shaderCoreDependency = '0.1.9' - urpDependencyPresent = $false - packageContainsPureBaseTestAssets = $false - pbrHybridPropertiesByteIdentical = $true - requiredProperties = @([ordered]@{ name = '_Metallic'; present = $true }) - forbiddenProperties = @([ordered]@{ name = '_Emission'; present = $false }) - roughnessAbi = $true - }) + $root = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseParity-' + [guid]::NewGuid().ToString('N')) + $legacy = Join-Path $root 'legacy' + $daily = Join-Path $root 'daily' + $release = Join-Path $root 'release' + [void](New-Item -ItemType Directory -Path $legacy, $daily, $release -Force) + foreach ($invocation in @($Manifest.nunitInvocations)) { + $id = [string]$invocation.id + Write-NUnit -Path (Join-Path $legacy ($id + '/' + $id + '.NUnit.xml')) -FullName ([string]$invocation.nunitFullName) + } + Write-Json -Path (Join-Path $legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json') -Value ([ordered]@{ + staticLightmapCount = 2 + staticLightmaps = @(1..20 | ForEach-Object { [ordered]@{ renderer = "Renderer$_"; lightmapIndex = 0; scaleOffsetX = 0.1; scaleOffsetY = 0.1; scaleOffsetZ = 0.0; scaleOffsetW = 0.0 } }) + metaAlbedo = @(1..18 | ForEach-Object { [ordered]@{ material = "Material$_"; shader = 'PureBase/Unlit'; meanLuminance = 0.25 } }) + shadowChangedPixelCount = 967 + variants = @(1..56 | ForEach-Object { [ordered]@{ shader = 'PureBase/Unlit'; pass = 'ForwardBase'; label = "Variant$_"; keywords = @(); added = $true; warmed = $true; variantCount = 1 } }) + }) + Write-Json -Path (Join-Path $legacy 'validation-scene-birp-feasibility.json') -Value ([ordered]@{ + check = 'Fixed BIRP validation scene synchronous bake, static lightmap, Meta readback, shadow silhouette, and representative variant warmup' + status = 'PASSED' + unityVersion = '2022.3.22f1' + graphicsApi = 'D3D11' + testName = 'PureBase.Integration.Tests.PureBaseValidationSceneTests.FixedValidationSceneBakesAndRequestsRepresentativeBirpVariants' + artifactPath = 'C:/Temp/PureBaseParity/validation-scene-birp' + dynamicLightmapStatus = 'NOT_DETERMINISTIC_IN_BATCH_EDITMODE' + }) + Write-Json -Path (Join-Path $legacy 'birp-probe-feasibility.json') -Value ([ordered]@{ + check = 'BIRP probe finite black and box-projected reflection-probe readback' + status = 'PASSED' + unityVersion = '2022.3.22f1' + graphicsApi = 'D3D11' + testNames = @('PureBase.Integration.Tests.BirpGiProbeReadbackTests.BlackProbePathProducesFiniteHdrReadbackWithMeshCoverage', 'PureBase.Integration.Tests.BirpGiProbeReadbackTests.BoxProjectedReflectionProbePathProducesFiniteHdrReadbackWithMeshCoverage') + artifactPaths = @('C:/Temp/PureBaseParity/birp-black', 'C:/Temp/PureBaseParity/birp-box') + }) + Write-Json -Path (Join-Path $legacy 'release-boundary-audit.json') -Value ([ordered]@{ + check = 'PureBase release boundary and metallic property ABI' + status = 'PASSED' + packagePath = 'C:/Temp/PureBaseParity/package' + trackedScmodulePaths = @('Tests/Fixtures/Hosts/Phase/base/test.scmodule') + approvedTrackedScmodulePaths = @('Tests/Fixtures/Hosts/Phase/base/test.scmodule') + unapprovedTrackedScmodulePaths = @() + missingTrackedScmodulePaths = @() + trackedScmodulePathsExactlyApproved = $true + shaderCoreDependency = '0.1.9' + urpDependencyPresent = $false + packageContainsPureBaseTestAssets = $false + pbrHybridPropertiesByteIdentical = $true + requiredProperties = @([ordered]@{ name = '_Metallic'; present = $true }) + forbiddenProperties = @([ordered]@{ name = '_Emission'; present = $false }) + roughnessAbi = $true + }) - Write-NUnit -Path (Join-Path $daily 'Daily.NUnit.xml') - [System.IO.File]::WriteAllText((Join-Path $daily 'Daily.Unity.log'), '', (New-Object System.Text.UTF8Encoding($false))) - [System.IO.File]::WriteAllText((Join-Path $daily 'Daily.Process.log'), '', (New-Object System.Text.UTF8Encoding($false))) + Write-NUnit -Path (Join-Path $daily 'Daily.NUnit.xml') + [System.IO.File]::WriteAllText((Join-Path $daily 'Daily.Unity.log'), '', (New-Object System.Text.UTF8Encoding($false))) + [System.IO.File]::WriteAllText((Join-Path $daily 'Daily.Process.log'), '', (New-Object System.Text.UTF8Encoding($false))) - $archiveDirectory = Join-Path $release 'archive' - $stagingDirectory = Join-Path $root 'zip-staging' - [void](New-Item -ItemType Directory -Path $archiveDirectory -Force) - foreach ($requiredEntry in @($ReleaseContentContract.requiredEntries)) { - $stagePath = Join-Path $stagingDirectory ([string]$requiredEntry).Replace('/', [System.IO.Path]::DirectorySeparatorChar) - [void](New-Item -ItemType Directory -Path (Split-Path -Parent $stagePath) -Force) - $content = if ($requiredEntry -eq 'package.json') { '{"name":"jp.penguin.purebase","version":"' + $PackageVersion + '"}' } else { [string]$requiredEntry } - [System.IO.File]::WriteAllText($stagePath, $content, (New-Object System.Text.UTF8Encoding($false))) - } - Add-Type -AssemblyName System.IO.Compression.FileSystem - $releaseZipPath = Join-Path $archiveDirectory ('jp.penguin.purebase-' + $PackageVersion + '.zip') - [System.IO.Compression.ZipFile]::CreateFromDirectory($stagingDirectory, $releaseZipPath) - $releaseLabels = @($Manifest.releaseArtifactLayout.fullMatrixLabels) - $releaseRunDirectories = $Manifest.releaseArtifactLayout.fullMatrixRunDirectories - Write-Json -Path (Join-Path $release 'run-summary.json') -Value ([ordered]@{ validationScope = 'full-release-validation-matrix'; outcomes = @($releaseLabels | ForEach-Object { [ordered]@{ label = [string]$_ } }) }) - Write-Json -Path (Join-Path $release 'cleanup-summary.json') -Value ([ordered]@{ failed = $false; consumerDirectoryRemovalFailed = $false }) - $bootstrap = Join-Path $release 'bootstrap' - Write-NUnit -Path (Join-Path $bootstrap 'NUnit.xml') - foreach ($releaseLabel in $releaseLabels) { - $runDirectoryLabel = [string]$releaseRunDirectories.PSObject.Properties[[string]$releaseLabel].Value - Write-NUnit -Path (Join-Path $release ('runs/' + $runDirectoryLabel + '/NUnit.xml')) - } - Write-Json -Path (Join-Path $bootstrap 'staging-receipt.json') -Value ([ordered]@{ - schemaName = 'purebase-consumer-staging-receipt'; schemaVersion = 1; pathOrdering = 'System.StringComparer.Ordinal'; entries = @([ordered]@{ destination = 'Assets/Synthetic.txt'; sourceKind = 'synthetic'; source = 'Synthetic.txt'; sha256 = New-Hash -Character 'a' }) - }) - Write-Json -Path (Join-Path $bootstrap 'immutable-input-manifest-bootstrap-delta.json') -Value ([ordered]@{ - schemaName = 'purebase-immutable-manifest-bootstrap-delta'; schemaVersion = 1; classification = 'observed'; pathOrdering = 'System.StringComparer.Ordinal'; preBootstrapRootSha256 = New-Hash -Character 'b'; postBootstrapRootSha256 = New-Hash -Character 'c'; added = New-PathEntries -Count 34 -Prefix 'ProjectSettings/Added' -HashCharacter 'a'; changed = New-ChangedPathEntries; removed = @() - }) - Write-Json -Path (Join-Path $bootstrap 'semantic-transition-report.json') -Value ([ordered]@{ schemaName = 'purebase-first-bootstrap-semantic-transition'; schemaVersion = 1; verdict = 'accepted'; summary = [ordered]@{ accepted = 34; rejected = 0; unclassified = 0 } }) - Write-Json -Path (Join-Path $bootstrap 'second-bootstrap/fixed-point-report.json') -Value ([ordered]@{ schemaName = 'purebase-second-bootstrap-fixed-point'; schemaVersion = 1; rootsEqual = $true; added = @(); changed = @(); removed = @() }) - return [pscustomobject]@{ root = $root; legacy = $legacy; daily = $daily; release = $release; releaseZipPath = $releaseZipPath } -} + $archiveDirectory = Join-Path $release 'archive' + $stagingDirectory = Join-Path $root 'zip-staging' + [void](New-Item -ItemType Directory -Path $archiveDirectory -Force) + foreach ($requiredEntry in @($ReleaseContentContract.requiredEntries)) { + $stagePath = Join-Path $stagingDirectory ([string]$requiredEntry).Replace('/', [System.IO.Path]::DirectorySeparatorChar) + [void](New-Item -ItemType Directory -Path (Split-Path -Parent $stagePath) -Force) + $content = if ($requiredEntry -eq 'package.json') { '{"name":"jp.penguin.purebase","version":"' + $PackageVersion + '"}' } else { [string]$requiredEntry } + [System.IO.File]::WriteAllText($stagePath, $content, (New-Object System.Text.UTF8Encoding($false))) + } + Add-Type -AssemblyName System.IO.Compression.FileSystem + $releaseZipPath = Join-Path $archiveDirectory ('jp.penguin.purebase-' + $PackageVersion + '.zip') + [System.IO.Compression.ZipFile]::CreateFromDirectory($stagingDirectory, $releaseZipPath) + $releaseLabels = @($Manifest.releaseArtifactLayout.fullMatrixLabels) + $releaseRunDirectories = $Manifest.releaseArtifactLayout.fullMatrixRunDirectories + Write-Json -Path (Join-Path $release 'run-summary.json') -Value ([ordered]@{ validationScope = 'full-release-validation-matrix'; outcomes = @($releaseLabels | ForEach-Object { [ordered]@{ label = [string]$_ } }) }) + Write-Json -Path (Join-Path $release 'cleanup-summary.json') -Value ([ordered]@{ failed = $false; consumerDirectoryRemovalFailed = $false }) + $bootstrap = Join-Path $release 'bootstrap' + Write-NUnit -Path (Join-Path $bootstrap 'NUnit.xml') + foreach ($releaseLabel in $releaseLabels) { + $runDirectoryLabel = [string]$releaseRunDirectories.PSObject.Properties[[string]$releaseLabel].Value + Write-NUnit -Path (Join-Path $release ('runs/' + $runDirectoryLabel + '/NUnit.xml')) + } + Write-Json -Path (Join-Path $bootstrap 'staging-receipt.json') -Value ([ordered]@{ + schemaName = 'purebase-consumer-staging-receipt'; schemaVersion = 1; pathOrdering = 'System.StringComparer.Ordinal'; entries = @([ordered]@{ destination = 'Assets/Synthetic.txt'; sourceKind = 'synthetic'; source = 'Synthetic.txt'; sha256 = New-Hash -Character 'a' }) + }) + Write-Json -Path (Join-Path $bootstrap 'immutable-input-manifest-bootstrap-delta.json') -Value ([ordered]@{ + schemaName = 'purebase-immutable-manifest-bootstrap-delta'; schemaVersion = 1; classification = 'observed'; pathOrdering = 'System.StringComparer.Ordinal'; preBootstrapRootSha256 = New-Hash -Character 'b'; postBootstrapRootSha256 = New-Hash -Character 'c'; added = New-PathEntries -Count 34 -Prefix 'ProjectSettings/Added' -HashCharacter 'a'; changed = New-ChangedPathEntries; removed = @() + }) + Write-Json -Path (Join-Path $bootstrap 'semantic-transition-report.json') -Value ([ordered]@{ schemaName = 'purebase-first-bootstrap-semantic-transition'; schemaVersion = 1; verdict = 'accepted'; summary = [ordered]@{ accepted = 34; rejected = 0; unclassified = 0 } }) + Write-Json -Path (Join-Path $bootstrap 'second-bootstrap/fixed-point-report.json') -Value ([ordered]@{ schemaName = 'purebase-second-bootstrap-fixed-point'; schemaVersion = 1; rootsEqual = $true; added = @(); changed = @(); removed = @() }) + return [pscustomobject]@{ root = $root; legacy = $legacy; daily = $daily; release = $release; releaseZipPath = $releaseZipPath } + } -function Invoke-ValidatorCase { - param( - [Parameter(Mandatory = $true)][string]$Name, - [Parameter(Mandatory = $true)]$Manifest, - [Parameter(Mandatory = $true)][string]$ValidatorPath, - [Parameter(Mandatory = $true)][string]$ManifestPath, - [Parameter(Mandatory = $true)][string]$PackageRoot, - [Parameter(Mandatory = $true)][bool]$ExpectedEligible, - [Parameter()][string]$ExpectedFailureCode, - [Parameter()][scriptblock]$Mutate, - [Parameter()][switch]$ReportUnderPackageRoot - ) + function Invoke-ValidatorCase { + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)]$Manifest, + [Parameter(Mandatory = $true)][string]$ValidatorPath, + [Parameter(Mandatory = $true)][string]$ManifestPath, + [Parameter(Mandatory = $true)][string]$PackageRoot, + [Parameter(Mandatory = $true)][bool]$ExpectedEligible, + [Parameter()][string]$ExpectedFailureCode, + [Parameter()][scriptblock]$Mutate, + [Parameter()][switch]$ReportUnderPackageRoot + ) - $artifacts = New-SyntheticArtifacts -Manifest $Manifest -ReleaseContentContract $script:releaseContentContract - try { - $effectiveManifestPath = $ManifestPath - if ($null -ne $Mutate) { - $mutatedManifestPath = & $Mutate $artifacts - if ($mutatedManifestPath -is [string] -and -not [string]::IsNullOrWhiteSpace($mutatedManifestPath)) { $effectiveManifestPath = $mutatedManifestPath } - } - $reportPath = if ($ReportUnderPackageRoot) { Join-Path $PackageRoot ('parity-report-' + $Name + '-' + [guid]::NewGuid().ToString('N') + '.json') } else { Join-Path $artifacts.root ('report-' + $Name + '.json') } - $previousErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'Continue' - $hostExecutableName = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh.exe' } else { 'powershell.exe' } - $hostExecutable = Join-Path $PSHOME $hostExecutableName - & $hostExecutable -NoProfile -ExecutionPolicy Bypass -File $ValidatorPath -LegacyArtifactRoot $artifacts.legacy -DailyArtifactRoot $artifacts.daily -ReleaseArtifactRoot $artifacts.release -ManifestPath $effectiveManifestPath -ReportPath $reportPath 2>$null | Out-Null - $ErrorActionPreference = $previousErrorActionPreference - $eligible = $LASTEXITCODE -eq 0 - $diagnostic = '' - if ($eligible -ne $ExpectedEligible -and -not $ReportUnderPackageRoot -and (Test-Path -LiteralPath $reportPath -PathType Leaf)) { - $diagnostic = ' Report: ' + (Get-Content -LiteralPath $reportPath -Raw) - } - Assert-Harness -Condition ($eligible -eq $ExpectedEligible) -Message "Case '$Name' returned eligible=$eligible, expected $ExpectedEligible.$diagnostic" - if (-not $ReportUnderPackageRoot) { - $report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json - Assert-Harness -Condition ([bool]$report.deletionEligible -eq $ExpectedEligible) -Message "Case '$Name' report eligibility differs from expected value." - if (-not $ExpectedEligible) { Assert-Harness -Condition ([int]$report.failureCount -gt 0 -and @($report.failures).Count -gt 0) -Message "Case '$Name' did not persist a nonzero failure report." } - if (-not [string]::IsNullOrWhiteSpace($ExpectedFailureCode)) { Assert-Harness -Condition (@($report.failures | Where-Object { $_.code -eq $ExpectedFailureCode }).Count -gt 0) -Message "Case '$Name' did not persist failure code '$ExpectedFailureCode'." } - } - else { - Assert-Harness -Condition (-not (Test-Path -LiteralPath $reportPath)) -Message "Case '$Name' wrote a report under the package root." + $artifacts = New-SyntheticArtifacts -Manifest $Manifest -ReleaseContentContract $script:releaseContentContract + try { + $effectiveManifestPath = $ManifestPath + if ($null -ne $Mutate) { + $mutatedManifestPath = & $Mutate $artifacts + if ($mutatedManifestPath -is [string] -and -not [string]::IsNullOrWhiteSpace($mutatedManifestPath)) { $effectiveManifestPath = $mutatedManifestPath } + } + $reportPath = if ($ReportUnderPackageRoot) { Join-Path $PackageRoot ('parity-report-' + $Name + '-' + [guid]::NewGuid().ToString('N') + '.json') } else { Join-Path $artifacts.root ('report-' + $Name + '.json') } + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + $hostExecutableName = if ($PSVersionTable.PSEdition -eq 'Core') { 'pwsh.exe' } else { 'powershell.exe' } + $hostExecutable = Join-Path $PSHOME $hostExecutableName + & $hostExecutable -NoProfile -ExecutionPolicy Bypass -File $ValidatorPath -LegacyArtifactRoot $artifacts.legacy -DailyArtifactRoot $artifacts.daily -ReleaseArtifactRoot $artifacts.release -ManifestPath $effectiveManifestPath -ReportPath $reportPath 2>$null | Out-Null + $ErrorActionPreference = $previousErrorActionPreference + $eligible = $LASTEXITCODE -eq 0 + $diagnostic = '' + if ($eligible -ne $ExpectedEligible -and -not $ReportUnderPackageRoot -and (Test-Path -LiteralPath $reportPath -PathType Leaf)) { + $diagnostic = ' Report: ' + (Get-Content -LiteralPath $reportPath -Raw) + } + Assert-Harness -Condition ($eligible -eq $ExpectedEligible) -Message "Case '$Name' returned eligible=$eligible, expected $ExpectedEligible.$diagnostic" + if (-not $ReportUnderPackageRoot) { + $report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json + Assert-Harness -Condition ([bool]$report.deletionEligible -eq $ExpectedEligible) -Message "Case '$Name' report eligibility differs from expected value." + if (-not $ExpectedEligible) { Assert-Harness -Condition ([int]$report.failureCount -gt 0 -and @($report.failures).Count -gt 0) -Message "Case '$Name' did not persist a nonzero failure report." } + if (-not [string]::IsNullOrWhiteSpace($ExpectedFailureCode)) { Assert-Harness -Condition (@($report.failures | Where-Object { $_.code -eq $ExpectedFailureCode }).Count -gt 0) -Message "Case '$Name' did not persist failure code '$ExpectedFailureCode'." } + } + else { + Assert-Harness -Condition (-not (Test-Path -LiteralPath $reportPath)) -Message "Case '$Name' wrote a report under the package root." + } + } + finally { + Remove-Item -LiteralPath $artifacts.root -Recurse -Force -ErrorAction SilentlyContinue + } } - } - finally { - Remove-Item -LiteralPath $artifacts.root -Recurse -Force -ErrorAction SilentlyContinue - } -} -$script:scriptRoot = Split-Path -Parent $PSCommandPath -$script:validatorPath = Join-Path $script:scriptRoot 'Validate-PureBaseParity.ps1' -$script:manifestPath = Join-Path $script:scriptRoot 'pure-base-validation-parity.json' -$script:packageRoot = [System.IO.Path]::GetFullPath((Join-Path $script:scriptRoot '../..')) -$script:manifest = Get-Content -LiteralPath $script:manifestPath -Raw | ConvertFrom-Json -$releaseContentContractPath = Join-Path $script:scriptRoot '../Release/release-content.json' -$script:releaseContentContract = Get-Content -LiteralPath $releaseContentContractPath -Raw | ConvertFrom-Json + $script:scriptRoot = Split-Path -Parent $PSCommandPath + $script:validatorPath = Join-Path $script:scriptRoot 'Validate-PureBaseParity.ps1' + $script:manifestPath = Join-Path $script:scriptRoot 'pure-base-validation-parity.json' + $script:packageRoot = [System.IO.Path]::GetFullPath((Join-Path $script:scriptRoot '../..')) + $script:manifest = Get-Content -LiteralPath $script:manifestPath -Raw | ConvertFrom-Json + $releaseContentContractPath = Join-Path $script:scriptRoot '../Release/release-content.json' + $script:releaseContentContract = Get-Content -LiteralPath $releaseContentContractPath -Raw | ConvertFrom-Json -function Invoke-ValidationSceneOracleCase { - param( - [Parameter(Mandatory = $true)][string]$Name, - [Parameter(Mandatory = $true)][string]$MissingProperty, - [Parameter(Mandatory = $true)][string]$ValidatorPath - ) + function Invoke-ValidationSceneOracleCase { + param( + [Parameter(Mandatory = $true)][string]$Name, + [Parameter(Mandatory = $true)][string]$MissingProperty, + [Parameter(Mandatory = $true)][string]$ValidatorPath + ) - $variant = [pscustomobject][ordered]@{ - shader = 'PureBase/Unlit' - pass = 'ForwardBase' - label = 'DirectOracleVariant' - keywords = @() - added = $true - warmed = $true - variantCount = 1 - } - $variant.PSObject.Properties.Remove($MissingProperty) - $scene = [pscustomobject][ordered]@{ variants = @($variant) } - $failures = New-Object 'System.Collections.Generic.List[object]' - $actual = Get-ValidationSceneOracleValue -Scene $scene -Name 'warmedRepresentativeVariantCount' -Failures $failures -Code 'direct-oracle' + $variant = [pscustomobject][ordered]@{ + shader = 'PureBase/Unlit' + pass = 'ForwardBase' + label = 'DirectOracleVariant' + keywords = @() + added = $true + warmed = $true + variantCount = 1 + } + $variant.PSObject.Properties.Remove($MissingProperty) + $scene = [pscustomobject][ordered]@{ variants = @($variant) } + $failures = New-Object 'System.Collections.Generic.List[object]' + $actual = Get-ValidationSceneOracleValue -Scene $scene -Name 'warmedRepresentativeVariantCount' -Failures $failures -Code 'direct-oracle' - Assert-Harness -Condition ($null -eq $actual) -Message "Case '$Name' did not reject the malformed oracle fixture." - Assert-Harness -Condition ($failures.Count -eq 1) -Message "Case '$Name' recorded an unexpected number of oracle failures." - Assert-Harness -Condition ([string]$failures[0].code -eq 'direct-oracle') -Message "Case '$Name' recorded an unexpected oracle failure code." - Assert-Harness -Condition ([string]$failures[0].message -eq 'Variant evidence is missing a warmed representative variant.') -Message "Case '$Name' recorded an unexpected oracle failure message." -} + Assert-Harness -Condition ($null -eq $actual) -Message "Case '$Name' did not reject the malformed oracle fixture." + Assert-Harness -Condition ($failures.Count -eq 1) -Message "Case '$Name' recorded an unexpected number of oracle failures." + Assert-Harness -Condition ([string]$failures[0].code -eq 'direct-oracle') -Message "Case '$Name' recorded an unexpected oracle failure code." + Assert-Harness -Condition ([string]$failures[0].message -eq 'Variant evidence is missing a warmed representative variant.') -Message "Case '$Name' recorded an unexpected oracle failure message." + } } @@ -275,275 +275,275 @@ function Invoke-ValidationSceneOracleCase { $manifestPath = $script:manifestPath $packageRoot = $script:packageRoot -Invoke-ValidatorCase -Name 'happy-path' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $true -Invoke-ValidatorCase -Name 'missing-package-test-assets-boolean' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'legacy-release-boundary-test-assets' -Mutate { - param($artifacts) - $auditPath = Join-Path $artifacts.legacy 'release-boundary-audit.json' - $audit = Get-Content -LiteralPath $auditPath -Raw | ConvertFrom-Json - $audit.PSObject.Properties.Remove('packageContainsPureBaseTestAssets') - Write-Json -Path $auditPath -Value $audit -} -Invoke-ValidatorCase -Name 'package-test-assets-present' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'legacy-release-boundary-test-assets' -Mutate { - param($artifacts) - $auditPath = Join-Path $artifacts.legacy 'release-boundary-audit.json' - $audit = Get-Content -LiteralPath $auditPath -Raw | ConvertFrom-Json - $audit.packageContainsPureBaseTestAssets = $true - Write-Json -Path $auditPath -Value $audit -} -Invoke-ValidatorCase -Name 'incomplete-legacy-row' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json - $copy.nunitInvocations[0].nunitFullName = '' - $changedManifest = Join-Path $artifacts.root 'incomplete-legacy-row.json' - Write-Json -Path $changedManifest -Value $copy - $changedManifest -} -Invoke-ValidatorCase -Name 'duplicate-legacy-row' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json - $copy.nunitInvocations[1].id = $copy.nunitInvocations[0].id - $changedManifest = Join-Path $artifacts.root 'duplicate-legacy-row.json' - Write-Json -Path $changedManifest -Value $copy - $changedManifest -} -Invoke-ValidatorCase -Name 'duplicate-mapping' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json - $copy.parityMappings[1].legacyId = $copy.parityMappings[0].legacyId - $changedManifest = Join-Path $artifacts.root 'duplicate-mapping.json' - Write-Json -Path $changedManifest -Value $copy - $changedManifest -} -Invoke-ValidatorCase -Name 'failing-legacy-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $first = $manifest.nunitInvocations[0] - Write-NUnit -Path (Join-Path $artifacts.legacy ($first.id + '/' + $first.id + '.NUnit.xml')) -FullName $first.nunitFullName -Failed 1 -} -Invoke-ValidatorCase -Name 'missing-legacy-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $first = $manifest.nunitInvocations[0] - Remove-Item -LiteralPath (Join-Path $artifacts.legacy ($first.id + '/' + $first.id + '.NUnit.xml')) -Force -} -Invoke-ValidatorCase -Name 'failing-daily-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-NUnit -Path (Join-Path $artifacts.daily 'Daily.NUnit.xml') -Failed 1 -} -Invoke-ValidatorCase -Name 'missing-daily-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Remove-Item -LiteralPath (Join-Path $artifacts.daily 'Daily.NUnit.xml') -Force -} -Invoke-ValidatorCase -Name 'failing-bootstrap-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-NUnit -Path (Join-Path $artifacts.release 'bootstrap/NUnit.xml') -Failed 1 -} -Invoke-ValidatorCase -Name 'missing-bootstrap-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Remove-Item -LiteralPath (Join-Path $artifacts.release 'bootstrap/NUnit.xml') -Force -} -Invoke-ValidatorCase -Name 'failing-release-matrix-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-NUnit -Path (Join-Path $artifacts.release ('runs/' + [string]$manifest.releaseArtifactLayout.fullMatrixLabels[1] + '/NUnit.xml')) -Failed 1 -} -Invoke-ValidatorCase -Name 'missing-release-matrix-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Remove-Item -LiteralPath (Join-Path $artifacts.release ('runs/' + [string]$manifest.releaseArtifactLayout.fullMatrixLabels[1] + '/NUnit.xml')) -Force -} -Invoke-ValidatorCase -Name 'missing-expected-release-matrix-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $summaryPath = Join-Path $artifacts.release 'run-summary.json' - $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json - $missingLabel = [string]$manifest.releaseArtifactLayout.fullMatrixLabels[0] - $summary.outcomes = @($summary.outcomes | Where-Object { [string]$_.label -ne $missingLabel }) - Write-Json -Path $summaryPath -Value $summary -} -Invoke-ValidatorCase -Name 'unexpected-extra-release-matrix-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $summaryPath = Join-Path $artifacts.release 'run-summary.json' - $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json - $summary.outcomes += [pscustomobject][ordered]@{ label = 'unexpected-release-matrix-label' } - Write-NUnit -Path (Join-Path $artifacts.release 'runs/unexpected-release-matrix-label/NUnit.xml') - Write-Json -Path $summaryPath -Value $summary -} -Invoke-ValidatorCase -Name 'declared-release-run-directory-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $true -Mutate { - param($artifacts) - $summaryPath = Join-Path $artifacts.release 'run-summary.json' - $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json - foreach ($outcome in @($summary.outcomes)) { - $outcome | Add-Member -NotePropertyName runDirectoryLabel -NotePropertyValue ([string]$manifest.releaseArtifactLayout.fullMatrixRunDirectories.PSObject.Properties[[string]$outcome.label].Value) - } - Write-Json -Path $summaryPath -Value $summary -} -Invoke-ValidatorCase -Name 'mismatched-release-run-directory-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $summaryPath = Join-Path $artifacts.release 'run-summary.json' - $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json - $summary.outcomes[0] | Add-Member -NotePropertyName runDirectoryLabel -NotePropertyValue 'wrong-directory' - Write-Json -Path $summaryPath -Value $summary -} -Invoke-ValidatorCase -Name 'unwarmed-variant-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' - $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json - $scene.variants[0].warmed = $false - Write-Json -Path $scenePath -Value $scene -} -Invoke-ValidatorCase -Name 'missing-warmed-variant-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' - $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json - $scene.variants[0].PSObject.Properties.Remove('warmed') - Write-Json -Path $scenePath -Value $scene -} -Invoke-ValidatorCase -Name 'missing-variant-count-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' - $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json - $scene.variants[0].PSObject.Properties.Remove('variantCount') - Write-Json -Path $scenePath -Value $scene -} -Invoke-ValidationSceneOracleCase -Name 'direct-missing-warmed-property' -MissingProperty 'warmed' -ValidatorPath $validatorPath -Invoke-ValidationSceneOracleCase -Name 'direct-missing-variant-count-property' -MissingProperty 'variantCount' -ValidatorPath $validatorPath -Invoke-ValidatorCase -Name 'fixed-evidence-mismatch' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' - $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json - $scene.staticLightmapCount = 1 - Write-Json -Path $scenePath -Value $scene -} -Invoke-ValidatorCase -Name 'dynamic-limitation-mismatch' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-Json -Path (Join-Path $artifacts.legacy 'validation-scene-birp-feasibility.json') -Value ([ordered]@{ status = 'SUPPORTED' }) -} -Invoke-ValidatorCase -Name 'malformed-legacy-validation-scene' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - [System.IO.File]::WriteAllText((Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json'), '{', (New-Object System.Text.UTF8Encoding($false))) -} -Invoke-ValidatorCase -Name 'null-static-lightmap-entry' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' - $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json - $scene.staticLightmaps[0] = $null - Write-Json -Path $scenePath -Value $scene -} -Invoke-ValidatorCase -Name 'incomplete-meta-albedo-entry' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' - $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json - $scene.metaAlbedo[0].PSObject.Properties.Remove('shader') - Write-Json -Path $scenePath -Value $scene -} -Invoke-ValidatorCase -Name 'malformed-dynamic-lightmaps-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - [System.IO.File]::WriteAllText((Join-Path $artifacts.legacy 'validation-scene-birp-feasibility.json'), '{', (New-Object System.Text.UTF8Encoding($false))) -} -Invoke-ValidatorCase -Name 'incomplete-birp-probe-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-Json -Path (Join-Path $artifacts.legacy 'birp-probe-feasibility.json') -Value ([ordered]@{ status = 'PASSED' }) -} -Invoke-ValidatorCase -Name 'incomplete-release-boundary-audit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $auditPath = Join-Path $artifacts.legacy 'release-boundary-audit.json' - $audit = Get-Content -LiteralPath $auditPath -Raw | ConvertFrom-Json - $audit.PSObject.Properties.Remove('requiredProperties') - Write-Json -Path $auditPath -Value $audit -} -Invoke-ValidatorCase -Name 'zip-tests-forbidden' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Add-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'Tests/forbidden.txt' -} -Invoke-ValidatorCase -Name 'zip-scmodule-forbidden' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Add-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'Modules/forbidden.scmodule' -} -Invoke-ValidatorCase -Name 'zip-required-release-entry-missing' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-zip-required-entry' -Mutate { - param($artifacts) - Remove-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'Shaders/PureBaseToon.scshader' -} -Invoke-ValidatorCase -Name 'zip-yank-policy-forbidden' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-zip-content' -Mutate { - param($artifacts) - Add-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'vpm-yanks.json' -} -Invoke-ValidatorCase -Name 'cleanup-status-missing' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-cleanup-status' -Mutate { - param($artifacts) - $cleanupPath = Join-Path $artifacts.release 'cleanup-summary.json' - $cleanup = Get-Content -LiteralPath $cleanupPath -Raw | ConvertFrom-Json - $cleanup.PSObject.Properties.Remove('failed') - Write-Json -Path $cleanupPath -Value $cleanup -} -Invoke-ValidatorCase -Name 'cleanup-status-nonboolean' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-cleanup-status' -Mutate { - param($artifacts) - $cleanupPath = Join-Path $artifacts.release 'cleanup-summary.json' - $cleanup = Get-Content -LiteralPath $cleanupPath -Raw | ConvertFrom-Json - $cleanup.failed = 'false' - Write-Json -Path $cleanupPath -Value $cleanup -} -Invoke-ValidatorCase -Name 'cleanup-status-true' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-cleanup-status' -Mutate { - param($artifacts) - $cleanupPath = Join-Path $artifacts.release 'cleanup-summary.json' - $cleanup = Get-Content -LiteralPath $cleanupPath -Raw | ConvertFrom-Json - $cleanup.consumerDirectoryRemovalFailed = $true - Write-Json -Path $cleanupPath -Value $cleanup -} -Invoke-ValidatorCase -Name 'migration-status-drift' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'manifest-migration-status' -Mutate { - param($artifacts) - $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json - $copy.migration.status = 'unexpected-status' - $changedManifest = Join-Path $artifacts.root 'migration-status-drift.json' - Write-Json -Path $changedManifest -Value $copy - $changedManifest -} -Invoke-ValidatorCase -Name 'receipt-integrity-failure' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $receiptPath = Join-Path $artifacts.release 'bootstrap/staging-receipt.json' - $receipt = Get-Content -LiteralPath $receiptPath -Raw | ConvertFrom-Json - $receipt.entries += $receipt.entries[0] - Write-Json -Path $receiptPath -Value $receipt -} -Invoke-ValidatorCase -Name 'malformed-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - [System.IO.File]::WriteAllText((Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json'), '{', (New-Object System.Text.UTF8Encoding($false))) -} -Invoke-ValidatorCase -Name 'rejected-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' - $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json - $delta.classification = 'rejected' - Write-Json -Path $deltaPath -Value $delta -} -Invoke-ValidatorCase -Name 'invalid-immutable-delta-schema' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' - $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json - $delta.schemaVersion = 999 - Write-Json -Path $deltaPath -Value $delta -} -Invoke-ValidatorCase -Name 'noncanonical-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' - $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json - $delta.added = @($delta.added | Sort-Object path -Descending) - Write-Json -Path $deltaPath -Value $delta -} -Invoke-ValidatorCase -Name 'unexpected-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' - $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json - $delta.added += [pscustomobject]@{ path = 'ProjectSettings/Unexpected.asset'; sha256 = New-Hash -Character '1' } - Write-Json -Path $deltaPath -Value $delta -} -Invoke-ValidatorCase -Name 'nonzero-fixed-point' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-Json -Path (Join-Path $artifacts.release 'bootstrap/second-bootstrap/fixed-point-report.json') -Value ([ordered]@{ schemaName = 'purebase-second-bootstrap-fixed-point'; schemaVersion = 1; rootsEqual = $true; added = @('unexpected'); changed = @(); removed = @() }) -} -Invoke-ValidatorCase -Name 'semantic-rejection' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-Json -Path (Join-Path $artifacts.release 'bootstrap/semantic-transition-report.json') -Value ([ordered]@{ schemaName = 'purebase-first-bootstrap-semantic-transition'; schemaVersion = 1; verdict = 'rejected'; summary = [ordered]@{ accepted = 33; rejected = 1; unclassified = 0 } }) -} -Invoke-ValidatorCase -Name 'semantic-unclassified' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { - param($artifacts) - Write-Json -Path (Join-Path $artifacts.release 'bootstrap/semantic-transition-report.json') -Value ([ordered]@{ schemaName = 'purebase-first-bootstrap-semantic-transition'; schemaVersion = 1; verdict = 'accepted'; summary = [ordered]@{ accepted = 33; rejected = 0; unclassified = 1 } }) -} -Invoke-ValidatorCase -Name 'package-root-report' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ReportUnderPackageRoot + Invoke-ValidatorCase -Name 'happy-path' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $true + Invoke-ValidatorCase -Name 'missing-package-test-assets-boolean' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'legacy-release-boundary-test-assets' -Mutate { + param($artifacts) + $auditPath = Join-Path $artifacts.legacy 'release-boundary-audit.json' + $audit = Get-Content -LiteralPath $auditPath -Raw | ConvertFrom-Json + $audit.PSObject.Properties.Remove('packageContainsPureBaseTestAssets') + Write-Json -Path $auditPath -Value $audit + } + Invoke-ValidatorCase -Name 'package-test-assets-present' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'legacy-release-boundary-test-assets' -Mutate { + param($artifacts) + $auditPath = Join-Path $artifacts.legacy 'release-boundary-audit.json' + $audit = Get-Content -LiteralPath $auditPath -Raw | ConvertFrom-Json + $audit.packageContainsPureBaseTestAssets = $true + Write-Json -Path $auditPath -Value $audit + } + Invoke-ValidatorCase -Name 'incomplete-legacy-row' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json + $copy.nunitInvocations[0].nunitFullName = '' + $changedManifest = Join-Path $artifacts.root 'incomplete-legacy-row.json' + Write-Json -Path $changedManifest -Value $copy + $changedManifest + } + Invoke-ValidatorCase -Name 'duplicate-legacy-row' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json + $copy.nunitInvocations[1].id = $copy.nunitInvocations[0].id + $changedManifest = Join-Path $artifacts.root 'duplicate-legacy-row.json' + Write-Json -Path $changedManifest -Value $copy + $changedManifest + } + Invoke-ValidatorCase -Name 'duplicate-mapping' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json + $copy.parityMappings[1].legacyId = $copy.parityMappings[0].legacyId + $changedManifest = Join-Path $artifacts.root 'duplicate-mapping.json' + Write-Json -Path $changedManifest -Value $copy + $changedManifest + } + Invoke-ValidatorCase -Name 'failing-legacy-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $first = $manifest.nunitInvocations[0] + Write-NUnit -Path (Join-Path $artifacts.legacy ($first.id + '/' + $first.id + '.NUnit.xml')) -FullName $first.nunitFullName -Failed 1 + } + Invoke-ValidatorCase -Name 'missing-legacy-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $first = $manifest.nunitInvocations[0] + Remove-Item -LiteralPath (Join-Path $artifacts.legacy ($first.id + '/' + $first.id + '.NUnit.xml')) -Force + } + Invoke-ValidatorCase -Name 'failing-daily-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-NUnit -Path (Join-Path $artifacts.daily 'Daily.NUnit.xml') -Failed 1 + } + Invoke-ValidatorCase -Name 'missing-daily-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Remove-Item -LiteralPath (Join-Path $artifacts.daily 'Daily.NUnit.xml') -Force + } + Invoke-ValidatorCase -Name 'failing-bootstrap-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-NUnit -Path (Join-Path $artifacts.release 'bootstrap/NUnit.xml') -Failed 1 + } + Invoke-ValidatorCase -Name 'missing-bootstrap-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Remove-Item -LiteralPath (Join-Path $artifacts.release 'bootstrap/NUnit.xml') -Force + } + Invoke-ValidatorCase -Name 'failing-release-matrix-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-NUnit -Path (Join-Path $artifacts.release ('runs/' + [string]$manifest.releaseArtifactLayout.fullMatrixLabels[1] + '/NUnit.xml')) -Failed 1 + } + Invoke-ValidatorCase -Name 'missing-release-matrix-nunit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Remove-Item -LiteralPath (Join-Path $artifacts.release ('runs/' + [string]$manifest.releaseArtifactLayout.fullMatrixLabels[1] + '/NUnit.xml')) -Force + } + Invoke-ValidatorCase -Name 'missing-expected-release-matrix-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $summaryPath = Join-Path $artifacts.release 'run-summary.json' + $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json + $missingLabel = [string]$manifest.releaseArtifactLayout.fullMatrixLabels[0] + $summary.outcomes = @($summary.outcomes | Where-Object { [string]$_.label -ne $missingLabel }) + Write-Json -Path $summaryPath -Value $summary + } + Invoke-ValidatorCase -Name 'unexpected-extra-release-matrix-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $summaryPath = Join-Path $artifacts.release 'run-summary.json' + $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json + $summary.outcomes += [pscustomobject][ordered]@{ label = 'unexpected-release-matrix-label' } + Write-NUnit -Path (Join-Path $artifacts.release 'runs/unexpected-release-matrix-label/NUnit.xml') + Write-Json -Path $summaryPath -Value $summary + } + Invoke-ValidatorCase -Name 'declared-release-run-directory-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $true -Mutate { + param($artifacts) + $summaryPath = Join-Path $artifacts.release 'run-summary.json' + $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json + foreach ($outcome in @($summary.outcomes)) { + $outcome | Add-Member -NotePropertyName runDirectoryLabel -NotePropertyValue ([string]$manifest.releaseArtifactLayout.fullMatrixRunDirectories.PSObject.Properties[[string]$outcome.label].Value) + } + Write-Json -Path $summaryPath -Value $summary + } + Invoke-ValidatorCase -Name 'mismatched-release-run-directory-label' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $summaryPath = Join-Path $artifacts.release 'run-summary.json' + $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json + $summary.outcomes[0] | Add-Member -NotePropertyName runDirectoryLabel -NotePropertyValue 'wrong-directory' + Write-Json -Path $summaryPath -Value $summary + } + Invoke-ValidatorCase -Name 'unwarmed-variant-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' + $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json + $scene.variants[0].warmed = $false + Write-Json -Path $scenePath -Value $scene + } + Invoke-ValidatorCase -Name 'missing-warmed-variant-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' + $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json + $scene.variants[0].PSObject.Properties.Remove('warmed') + Write-Json -Path $scenePath -Value $scene + } + Invoke-ValidatorCase -Name 'missing-variant-count-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' + $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json + $scene.variants[0].PSObject.Properties.Remove('variantCount') + Write-Json -Path $scenePath -Value $scene + } + Invoke-ValidationSceneOracleCase -Name 'direct-missing-warmed-property' -MissingProperty 'warmed' -ValidatorPath $validatorPath + Invoke-ValidationSceneOracleCase -Name 'direct-missing-variant-count-property' -MissingProperty 'variantCount' -ValidatorPath $validatorPath + Invoke-ValidatorCase -Name 'fixed-evidence-mismatch' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' + $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json + $scene.staticLightmapCount = 1 + Write-Json -Path $scenePath -Value $scene + } + Invoke-ValidatorCase -Name 'dynamic-limitation-mismatch' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-Json -Path (Join-Path $artifacts.legacy 'validation-scene-birp-feasibility.json') -Value ([ordered]@{ status = 'SUPPORTED' }) + } + Invoke-ValidatorCase -Name 'malformed-legacy-validation-scene' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + [System.IO.File]::WriteAllText((Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json'), '{', (New-Object System.Text.UTF8Encoding($false))) + } + Invoke-ValidatorCase -Name 'null-static-lightmap-entry' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' + $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json + $scene.staticLightmaps[0] = $null + Write-Json -Path $scenePath -Value $scene + } + Invoke-ValidatorCase -Name 'incomplete-meta-albedo-entry' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $scenePath = Join-Path $artifacts.legacy 'validation-scene-birp/Artifacts/purebase-validation-scene-birp.json' + $scene = Get-Content -LiteralPath $scenePath -Raw | ConvertFrom-Json + $scene.metaAlbedo[0].PSObject.Properties.Remove('shader') + Write-Json -Path $scenePath -Value $scene + } + Invoke-ValidatorCase -Name 'malformed-dynamic-lightmaps-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + [System.IO.File]::WriteAllText((Join-Path $artifacts.legacy 'validation-scene-birp-feasibility.json'), '{', (New-Object System.Text.UTF8Encoding($false))) + } + Invoke-ValidatorCase -Name 'incomplete-birp-probe-evidence' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-Json -Path (Join-Path $artifacts.legacy 'birp-probe-feasibility.json') -Value ([ordered]@{ status = 'PASSED' }) + } + Invoke-ValidatorCase -Name 'incomplete-release-boundary-audit' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $auditPath = Join-Path $artifacts.legacy 'release-boundary-audit.json' + $audit = Get-Content -LiteralPath $auditPath -Raw | ConvertFrom-Json + $audit.PSObject.Properties.Remove('requiredProperties') + Write-Json -Path $auditPath -Value $audit + } + Invoke-ValidatorCase -Name 'zip-tests-forbidden' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Add-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'Tests/forbidden.txt' + } + Invoke-ValidatorCase -Name 'zip-scmodule-forbidden' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Add-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'Modules/forbidden.scmodule' + } + Invoke-ValidatorCase -Name 'zip-required-release-entry-missing' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-zip-required-entry' -Mutate { + param($artifacts) + Remove-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'Shaders/PureBaseToon.scshader' + } + Invoke-ValidatorCase -Name 'zip-yank-policy-forbidden' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-zip-content' -Mutate { + param($artifacts) + Add-ZipEntry -Path $artifacts.releaseZipPath -EntryName 'vpm-yanks.json' + } + Invoke-ValidatorCase -Name 'cleanup-status-missing' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-cleanup-status' -Mutate { + param($artifacts) + $cleanupPath = Join-Path $artifacts.release 'cleanup-summary.json' + $cleanup = Get-Content -LiteralPath $cleanupPath -Raw | ConvertFrom-Json + $cleanup.PSObject.Properties.Remove('failed') + Write-Json -Path $cleanupPath -Value $cleanup + } + Invoke-ValidatorCase -Name 'cleanup-status-nonboolean' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-cleanup-status' -Mutate { + param($artifacts) + $cleanupPath = Join-Path $artifacts.release 'cleanup-summary.json' + $cleanup = Get-Content -LiteralPath $cleanupPath -Raw | ConvertFrom-Json + $cleanup.failed = 'false' + Write-Json -Path $cleanupPath -Value $cleanup + } + Invoke-ValidatorCase -Name 'cleanup-status-true' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'release-cleanup-status' -Mutate { + param($artifacts) + $cleanupPath = Join-Path $artifacts.release 'cleanup-summary.json' + $cleanup = Get-Content -LiteralPath $cleanupPath -Raw | ConvertFrom-Json + $cleanup.consumerDirectoryRemovalFailed = $true + Write-Json -Path $cleanupPath -Value $cleanup + } + Invoke-ValidatorCase -Name 'migration-status-drift' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ExpectedFailureCode 'manifest-migration-status' -Mutate { + param($artifacts) + $copy = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json + $copy.migration.status = 'unexpected-status' + $changedManifest = Join-Path $artifacts.root 'migration-status-drift.json' + Write-Json -Path $changedManifest -Value $copy + $changedManifest + } + Invoke-ValidatorCase -Name 'receipt-integrity-failure' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $receiptPath = Join-Path $artifacts.release 'bootstrap/staging-receipt.json' + $receipt = Get-Content -LiteralPath $receiptPath -Raw | ConvertFrom-Json + $receipt.entries += $receipt.entries[0] + Write-Json -Path $receiptPath -Value $receipt + } + Invoke-ValidatorCase -Name 'malformed-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + [System.IO.File]::WriteAllText((Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json'), '{', (New-Object System.Text.UTF8Encoding($false))) + } + Invoke-ValidatorCase -Name 'rejected-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' + $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json + $delta.classification = 'rejected' + Write-Json -Path $deltaPath -Value $delta + } + Invoke-ValidatorCase -Name 'invalid-immutable-delta-schema' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' + $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json + $delta.schemaVersion = 999 + Write-Json -Path $deltaPath -Value $delta + } + Invoke-ValidatorCase -Name 'noncanonical-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' + $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json + $delta.added = @($delta.added | Sort-Object path -Descending) + Write-Json -Path $deltaPath -Value $delta + } + Invoke-ValidatorCase -Name 'unexpected-immutable-delta' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + $deltaPath = Join-Path $artifacts.release 'bootstrap/immutable-input-manifest-bootstrap-delta.json' + $delta = Get-Content -LiteralPath $deltaPath -Raw | ConvertFrom-Json + $delta.added += [pscustomobject]@{ path = 'ProjectSettings/Unexpected.asset'; sha256 = New-Hash -Character '1' } + Write-Json -Path $deltaPath -Value $delta + } + Invoke-ValidatorCase -Name 'nonzero-fixed-point' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-Json -Path (Join-Path $artifacts.release 'bootstrap/second-bootstrap/fixed-point-report.json') -Value ([ordered]@{ schemaName = 'purebase-second-bootstrap-fixed-point'; schemaVersion = 1; rootsEqual = $true; added = @('unexpected'); changed = @(); removed = @() }) + } + Invoke-ValidatorCase -Name 'semantic-rejection' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-Json -Path (Join-Path $artifacts.release 'bootstrap/semantic-transition-report.json') -Value ([ordered]@{ schemaName = 'purebase-first-bootstrap-semantic-transition'; schemaVersion = 1; verdict = 'rejected'; summary = [ordered]@{ accepted = 33; rejected = 1; unclassified = 0 } }) + } + Invoke-ValidatorCase -Name 'semantic-unclassified' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -Mutate { + param($artifacts) + Write-Json -Path (Join-Path $artifacts.release 'bootstrap/semantic-transition-report.json') -Value ([ordered]@{ schemaName = 'purebase-first-bootstrap-semantic-transition'; schemaVersion = 1; verdict = 'accepted'; summary = [ordered]@{ accepted = 33; rejected = 0; unclassified = 1 } }) + } + Invoke-ValidatorCase -Name 'package-root-report' -Manifest $manifest -ValidatorPath $validatorPath -ManifestPath $manifestPath -PackageRoot $packageRoot -ExpectedEligible $false -ReportUnderPackageRoot } It 'finds a prerelease archive whose filename exactly matches its package manifest version' { @@ -579,4 +579,4 @@ Invoke-ValidatorCase -Name 'package-root-report' -Manifest $manifest -ValidatorP return $changedManifest } } -} \ No newline at end of file +} diff --git a/Tests/Parity/Validate-PureBaseParity.ps1 b/Tests/Parity/Validate-PureBaseParity.ps1 index 13be995..5ce854c 100644 --- a/Tests/Parity/Validate-PureBaseParity.ps1 +++ b/Tests/Parity/Validate-PureBaseParity.ps1 @@ -43,7 +43,7 @@ function Test-PathEqualOrDescendant { $normalizedRoot = (Get-FullPath -Path $Root).TrimEnd('\', '/') $normalizedCandidate = (Get-FullPath -Path $Candidate).TrimEnd('\', '/') return [string]::Equals($normalizedRoot, $normalizedCandidate, [System.StringComparison]::OrdinalIgnoreCase) -or - $normalizedCandidate.StartsWith($normalizedRoot + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase) + $normalizedCandidate.StartsWith($normalizedRoot + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase) } function Get-JsonArtifact { @@ -323,7 +323,7 @@ function Test-ReleaseZip { Add-Failure -Failures $Failures -Code 'release-zip-required-entry' -Message "Release ZIP omits required contract entry '$requiredEntry'." } } - $manifestEntries = @($archive.Entries | Where-Object FullName -ceq 'package.json') + $manifestEntries = @($archive.Entries | Where-Object FullName -CEQ 'package.json') if ($manifestEntries.Count -ne 1) { Add-Failure -Failures $Failures -Code 'release-zip-version' -Message 'Release ZIP must contain exactly one package.json.' } @@ -577,10 +577,10 @@ function Test-Manifest { foreach ($mapping in $mappings) { $mappedId = if ($null -eq $mapping) { '' } else { [string]$mapping.legacyId } $mappingValid = $null -ne $mapping -and $invocationsById.ContainsKey($mappedId) -and $mappedIds.Add($mappedId) -and - -not [string]::IsNullOrWhiteSpace([string]$mapping.newOwner) -and -not [string]::IsNullOrWhiteSpace([string]$mapping.newLane) -and - $mapping.oracle -eq 'nunit-and-release-bootstrap' -and $mapping.status -eq 'required' -and - (@('covered', 'intentionally-replaced-with-equivalent-oracle') -contains [string]$mapping.classification) -and - @($mapping.artifacts).Count -ge $requiredArtifacts.Count -and @($mapping.evidence).Count -ge $requiredEvidence.Count + -not [string]::IsNullOrWhiteSpace([string]$mapping.newOwner) -and -not [string]::IsNullOrWhiteSpace([string]$mapping.newLane) -and + $mapping.oracle -eq 'nunit-and-release-bootstrap' -and $mapping.status -eq 'required' -and + (@('covered', 'intentionally-replaced-with-equivalent-oracle') -contains [string]$mapping.classification) -and + @($mapping.artifacts).Count -ge $requiredArtifacts.Count -and @($mapping.evidence).Count -ge $requiredEvidence.Count if ($mappingValid) { foreach ($artifact in $requiredArtifacts) { if (-not (@($mapping.artifacts) -contains $artifact)) { $mappingValid = $false } } foreach ($evidence in $requiredEvidence) { if (-not (@($mapping.evidence) -contains $evidence)) { $mappingValid = $false } } @@ -626,22 +626,22 @@ function Test-LegacyEvidence { $feasibility = Get-JsonArtifact -Path $feasibilityPath -Failures $Failures -Code 'legacy-dynamic-lightmaps' if ($null -ne $feasibility) { $dynamicValid = $null -ne (Get-StringProperty -Object $feasibility -Name 'check' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -and - (Get-StringProperty -Object $feasibility -Name 'status' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -eq 'PASSED' -and - $null -ne (Get-StringProperty -Object $feasibility -Name 'unityVersion' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -and - $null -ne (Get-StringPropertyAlias -Object $feasibility -Names @('graphicsApi', 'graphicsDevice') -Failures $Failures -Code 'legacy-dynamic-lightmaps') -and - (Get-StringProperty -Object $feasibility -Name 'testName' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -eq 'PureBase.Integration.Tests.PureBaseValidationSceneTests.FixedValidationSceneBakesAndRequestsRepresentativeBirpVariants' -and - $null -ne (Get-StringProperty -Object $feasibility -Name 'artifactPath' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -and - (Get-StringProperty -Object $feasibility -Name 'dynamicLightmapStatus' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -eq 'NOT_DETERMINISTIC_IN_BATCH_EDITMODE' + (Get-StringProperty -Object $feasibility -Name 'status' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -eq 'PASSED' -and + $null -ne (Get-StringProperty -Object $feasibility -Name 'unityVersion' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -and + $null -ne (Get-StringPropertyAlias -Object $feasibility -Names @('graphicsApi', 'graphicsDevice') -Failures $Failures -Code 'legacy-dynamic-lightmaps') -and + (Get-StringProperty -Object $feasibility -Name 'testName' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -eq 'PureBase.Integration.Tests.PureBaseValidationSceneTests.FixedValidationSceneBakesAndRequestsRepresentativeBirpVariants' -and + $null -ne (Get-StringProperty -Object $feasibility -Name 'artifactPath' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -and + (Get-StringProperty -Object $feasibility -Name 'dynamicLightmapStatus' -Failures $Failures -Code 'legacy-dynamic-lightmaps') -eq 'NOT_DETERMINISTIC_IN_BATCH_EDITMODE' if (-not $dynamicValid) { Add-Failure -Failures $Failures -Code 'legacy-dynamic-lightmaps' -Message 'Dynamic-lightmaps evidence does not match the required JSON schema and known limitation.' } } $probe = Get-JsonArtifact -Path (Join-Path $Root 'birp-probe-feasibility.json') -Failures $Failures -Code 'legacy-birp-probe' if ($null -ne $probe) { $probeValid = $null -ne (Get-StringProperty -Object $probe -Name 'check' -Failures $Failures -Code 'legacy-birp-probe') -and - (Get-StringProperty -Object $probe -Name 'status' -Failures $Failures -Code 'legacy-birp-probe') -eq 'PASSED' -and - $null -ne (Get-StringProperty -Object $probe -Name 'unityVersion' -Failures $Failures -Code 'legacy-birp-probe') -and - $null -ne (Get-StringPropertyAlias -Object $probe -Names @('graphicsApi', 'graphicsDevice') -Failures $Failures -Code 'legacy-birp-probe') -and - (Test-StringArrayProperty -Object $probe -Name 'testNames' -Failures $Failures -Code 'legacy-birp-probe' -ExpectedCount 2 -RequiredValues @('PureBase.Integration.Tests.BirpGiProbeReadbackTests.BlackProbePathProducesFiniteHdrReadbackWithMeshCoverage', 'PureBase.Integration.Tests.BirpGiProbeReadbackTests.BoxProjectedReflectionProbePathProducesFiniteHdrReadbackWithMeshCoverage')) -and - (Test-StringArrayProperty -Object $probe -Name 'artifactPaths' -Failures $Failures -Code 'legacy-birp-probe' -ExpectedCount 2) + (Get-StringProperty -Object $probe -Name 'status' -Failures $Failures -Code 'legacy-birp-probe') -eq 'PASSED' -and + $null -ne (Get-StringProperty -Object $probe -Name 'unityVersion' -Failures $Failures -Code 'legacy-birp-probe') -and + $null -ne (Get-StringPropertyAlias -Object $probe -Names @('graphicsApi', 'graphicsDevice') -Failures $Failures -Code 'legacy-birp-probe') -and + (Test-StringArrayProperty -Object $probe -Name 'testNames' -Failures $Failures -Code 'legacy-birp-probe' -ExpectedCount 2 -RequiredValues @('PureBase.Integration.Tests.BirpGiProbeReadbackTests.BlackProbePathProducesFiniteHdrReadbackWithMeshCoverage', 'PureBase.Integration.Tests.BirpGiProbeReadbackTests.BoxProjectedReflectionProbePathProducesFiniteHdrReadbackWithMeshCoverage')) -and + (Test-StringArrayProperty -Object $probe -Name 'artifactPaths' -Failures $Failures -Code 'legacy-birp-probe' -ExpectedCount 2) if (-not $probeValid) { Add-Failure -Failures $Failures -Code 'legacy-birp-probe' -Message 'BIRP probe evidence does not match the required JSON schema and passing verdict.' } } $audit = Get-JsonArtifact -Path (Join-Path $Root 'release-boundary-audit.json') -Failures $Failures -Code 'legacy-release-boundary' @@ -652,18 +652,18 @@ function Test-LegacyEvidence { Add-Failure -Failures $Failures -Code 'legacy-release-boundary-test-assets' -Message "Release-boundary audit property 'packageContainsPureBaseTestAssets' must be Boolean false." } $auditValid = $packageContainsPureBaseTestAssetsValid -and - $null -ne (Get-StringProperty -Object $audit -Name 'check' -Failures $Failures -Code 'legacy-release-boundary') -and - (Get-StringProperty -Object $audit -Name 'status' -Failures $Failures -Code 'legacy-release-boundary') -eq 'PASSED' -and - $null -ne (Get-StringProperty -Object $audit -Name 'packagePath' -Failures $Failures -Code 'legacy-release-boundary') -and - (Test-StringArrayProperty -Object $audit -Name 'trackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary') -and - (Test-StringArrayProperty -Object $audit -Name 'approvedTrackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary') -and - (Test-StringArrayProperty -Object $audit -Name 'unapprovedTrackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary' -ExpectedCount 0) -and - (Test-StringArrayProperty -Object $audit -Name 'missingTrackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary' -ExpectedCount 0) -and - $true -eq (Get-BooleanProperty -Object $audit -Name 'trackedScmodulePathsExactlyApproved' -Failures $Failures -Code 'legacy-release-boundary') -and - $null -ne (Get-StringProperty -Object $audit -Name 'shaderCoreDependency' -Failures $Failures -Code 'legacy-release-boundary') -and - $false -eq (Get-BooleanProperty -Object $audit -Name 'urpDependencyPresent' -Failures $Failures -Code 'legacy-release-boundary') -and - $true -eq (Get-BooleanProperty -Object $audit -Name 'pbrHybridPropertiesByteIdentical' -Failures $Failures -Code 'legacy-release-boundary') -and - $true -eq (Get-BooleanProperty -Object $audit -Name 'roughnessAbi' -Failures $Failures -Code 'legacy-release-boundary') + $null -ne (Get-StringProperty -Object $audit -Name 'check' -Failures $Failures -Code 'legacy-release-boundary') -and + (Get-StringProperty -Object $audit -Name 'status' -Failures $Failures -Code 'legacy-release-boundary') -eq 'PASSED' -and + $null -ne (Get-StringProperty -Object $audit -Name 'packagePath' -Failures $Failures -Code 'legacy-release-boundary') -and + (Test-StringArrayProperty -Object $audit -Name 'trackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary') -and + (Test-StringArrayProperty -Object $audit -Name 'approvedTrackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary') -and + (Test-StringArrayProperty -Object $audit -Name 'unapprovedTrackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary' -ExpectedCount 0) -and + (Test-StringArrayProperty -Object $audit -Name 'missingTrackedScmodulePaths' -Failures $Failures -Code 'legacy-release-boundary' -ExpectedCount 0) -and + $true -eq (Get-BooleanProperty -Object $audit -Name 'trackedScmodulePathsExactlyApproved' -Failures $Failures -Code 'legacy-release-boundary') -and + $null -ne (Get-StringProperty -Object $audit -Name 'shaderCoreDependency' -Failures $Failures -Code 'legacy-release-boundary') -and + $false -eq (Get-BooleanProperty -Object $audit -Name 'urpDependencyPresent' -Failures $Failures -Code 'legacy-release-boundary') -and + $true -eq (Get-BooleanProperty -Object $audit -Name 'pbrHybridPropertiesByteIdentical' -Failures $Failures -Code 'legacy-release-boundary') -and + $true -eq (Get-BooleanProperty -Object $audit -Name 'roughnessAbi' -Failures $Failures -Code 'legacy-release-boundary') foreach ($name in @('requiredProperties', 'forbiddenProperties')) { $entries = @(Get-ArrayPropertyItems -Object $audit -Name $name -Failures $Failures -Code 'legacy-release-boundary') $expectedPresent = $name -eq 'requiredProperties' @@ -715,15 +715,15 @@ catch { } $report = [ordered]@{ - schemaVersion = 1 - validator = 'Validate-PureBaseParity.ps1' + schemaVersion = 1 + validator = 'Validate-PureBaseParity.ps1' deletionEligible = ($failures.Count -eq 0) - failureCount = $failures.Count - failures = $failures.ToArray() + failureCount = $failures.Count + failures = $failures.ToArray() } $report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $reportPath -Encoding UTF8 if ($failures.Count -ne 0) { Write-Error "PureBase parity validation failed. Report: '$reportPath'." exit 1 } -Write-Output "PureBase parity validation passed. Report: '$reportPath'." \ No newline at end of file +Write-Output "PureBase parity validation passed. Report: '$reportPath'." diff --git a/Tests/Release/Build-PureBaseRelease.Tests.ps1 b/Tests/Release/Build-PureBaseRelease.Tests.ps1 index 0626484..c0b7122 100644 --- a/Tests/Release/Build-PureBaseRelease.Tests.ps1 +++ b/Tests/Release/Build-PureBaseRelease.Tests.ps1 @@ -223,15 +223,15 @@ Describe 'Deterministic release archive contracts' { $utf8NoBom = [Text.UTF8Encoding]::new($false) $files = [ordered]@{ - 'LICENSE' = "license fixture`n" - 'NOTICE' = "notice fixture`n" - 'README.md' = "# Fixture`n" - 'Editor/.gitkeep' = '' + 'LICENSE' = "license fixture`n" + 'NOTICE' = "notice fixture`n" + 'README.md' = "# Fixture`n" + 'Editor/.gitkeep' = '' 'Shaders/PureBaseHybrid.scshader' = "Shader fixture Hybrid`n" - 'Shaders/PureBasePBR.scshader' = "Shader fixture PBR`n" - 'Shaders/PureBaseToon.scshader' = "Shader fixture Toon`n" - 'Shaders/PureBaseUnlit.scshader' = "Shader fixture Unlit`n" - 'package.json' = "{`"name`":`"jp.penguin.purebase`",`"version`":`"0.2.0`",`"vpmDependencies`":{`"jp.lilxyzw.shadercore`":`"0.1.9`"}}`n" + 'Shaders/PureBasePBR.scshader' = "Shader fixture PBR`n" + 'Shaders/PureBaseToon.scshader' = "Shader fixture Toon`n" + 'Shaders/PureBaseUnlit.scshader' = "Shader fixture Unlit`n" + 'package.json' = "{`"name`":`"jp.penguin.purebase`",`"version`":`"0.2.0`",`"vpmDependencies`":{`"jp.lilxyzw.shadercore`":`"0.1.9`"}}`n" } foreach ($entry in $files.GetEnumerator()) { $path = Join-Path $packageRoot $entry.Key @@ -364,6 +364,6 @@ Describe 'Deterministic release archive contracts' { $archive = Get-ChildItem -LiteralPath $outputDirectory -Filter 'jp.penguin.purebase-0.2.0.zip' -File | Select-Object -First 1 $archive | Should -Not -BeNullOrEmpty (Get-FileHash -LiteralPath $archive.FullName -Algorithm SHA256).Hash.ToLowerInvariant() | - Should -Be 'b9ea2454a4dc12be358824865bac7bd8beba293a83c8ec9c129083bc950130a1' + Should -Be 'b9ea2454a4dc12be358824865bac7bd8beba293a83c8ec9c129083bc950130a1' } -} \ No newline at end of file +} diff --git a/Tests/Release/Build-PureBaseRelease.ps1 b/Tests/Release/Build-PureBaseRelease.ps1 index 8c60962..ab0a487 100644 --- a/Tests/Release/Build-PureBaseRelease.ps1 +++ b/Tests/Release/Build-PureBaseRelease.ps1 @@ -532,59 +532,59 @@ finally { $zipStream.Dispose() } - $zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath) - try { - $zipEntries = New-Object System.Collections.Generic.List[string] - foreach ($entry in $zip.Entries) { - if ($entry.FullName.EndsWith('/')) { - continue - } - $entryPath = Get-NormalizedRelativePath -Path $entry.FullName - if (-not (Test-ContractPath -Path $entryPath -Contract $contract)) { - throw "ZIP contains excluded or unapproved entry '$entryPath'." - } - if ((($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000) { - throw "ZIP entry '$entryPath' is a symbolic link." - } - $zipEntries.Add($entryPath) - } - - $duplicates = @($zipEntries | Group-Object | Where-Object { $_.Count -gt 1 }) - if ($duplicates.Count -ne 0) { - throw ('ZIP contains duplicate normalized entries: ' + (($duplicates | ForEach-Object { $_.Name }) -join ', ')) +$zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath) +try { + $zipEntries = New-Object System.Collections.Generic.List[string] + foreach ($entry in $zip.Entries) { + if ($entry.FullName.EndsWith('/')) { + continue } - $sortedReleaseFiles = @(Get-OrdinalSortedStrings -Values $releaseFiles.ToArray()) - $sortedZipEntries = @(Get-OrdinalSortedStrings -Values $zipEntries.ToArray()) - $zipEntriesMatchReleaseFiles = $sortedReleaseFiles.Count -eq $sortedZipEntries.Count - if ($zipEntriesMatchReleaseFiles) { - for ($index = 0; $index -lt $sortedReleaseFiles.Count; $index++) { - if (-not [string]::Equals($sortedReleaseFiles[$index], $sortedZipEntries[$index], [System.StringComparison]::Ordinal)) { - $zipEntriesMatchReleaseFiles = $false - break - } - } + $entryPath = Get-NormalizedRelativePath -Path $entry.FullName + if (-not (Test-ContractPath -Path $entryPath -Contract $contract)) { + throw "ZIP contains excluded or unapproved entry '$entryPath'." } - if (-not $zipEntriesMatchReleaseFiles) { - throw 'ZIP entries do not exactly match the audited tracked release source set.' + if ((($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000) { + throw "ZIP entry '$entryPath' is a symbolic link." } - foreach ($requiredEntry in $contract.requiredEntries) { - if (-not $zipEntries.Contains([string]$requiredEntry)) { - throw "ZIP omits required release entry '$requiredEntry'." + $zipEntries.Add($entryPath) + } + + $duplicates = @($zipEntries | Group-Object | Where-Object { $_.Count -gt 1 }) + if ($duplicates.Count -ne 0) { + throw ('ZIP contains duplicate normalized entries: ' + (($duplicates | ForEach-Object { $_.Name }) -join ', ')) + } + $sortedReleaseFiles = @(Get-OrdinalSortedStrings -Values $releaseFiles.ToArray()) + $sortedZipEntries = @(Get-OrdinalSortedStrings -Values $zipEntries.ToArray()) + $zipEntriesMatchReleaseFiles = $sortedReleaseFiles.Count -eq $sortedZipEntries.Count + if ($zipEntriesMatchReleaseFiles) { + for ($index = 0; $index -lt $sortedReleaseFiles.Count; $index++) { + if (-not [string]::Equals($sortedReleaseFiles[$index], $sortedZipEntries[$index], [System.StringComparison]::Ordinal)) { + $zipEntriesMatchReleaseFiles = $false + break } } - $packageEntry = @($zip.Entries | Where-Object FullName -ceq 'package.json') - if ($packageEntry.Count -ne 1) { throw 'ZIP must contain exactly one package.json.' } - $reader = [IO.StreamReader]::new($packageEntry[0].Open(), [Text.UTF8Encoding]::new($false, $true)) - try { $zipPackageVersion = [string](($reader.ReadToEnd() | ConvertFrom-Json).version) } - finally { $reader.Dispose() } - if ($zipPackageVersion -cne [string]$packageJson.version) { throw 'ZIP package.json version does not match the archive filename.' } } - finally { - $zip.Dispose() + if (-not $zipEntriesMatchReleaseFiles) { + throw 'ZIP entries do not exactly match the audited tracked release source set.' } + foreach ($requiredEntry in $contract.requiredEntries) { + if (-not $zipEntries.Contains([string]$requiredEntry)) { + throw "ZIP omits required release entry '$requiredEntry'." + } + } + $packageEntry = @($zip.Entries | Where-Object FullName -CEQ 'package.json') + if ($packageEntry.Count -ne 1) { throw 'ZIP must contain exactly one package.json.' } + $reader = [IO.StreamReader]::new($packageEntry[0].Open(), [Text.UTF8Encoding]::new($false, $true)) + try { $zipPackageVersion = [string](($reader.ReadToEnd() | ConvertFrom-Json).version) } + finally { $reader.Dispose() } + if ($zipPackageVersion -cne [string]$packageJson.version) { throw 'ZIP package.json version does not match the archive filename.' } +} +finally { + $zip.Dispose() +} - $zipHash = Get-Sha256Hex -Path $zipPath - Set-Content -LiteralPath $hashPath -Value $zipHash -Encoding ASCII +$zipHash = Get-Sha256Hex -Path $zipPath +Set-Content -LiteralPath $hashPath -Value $zipHash -Encoding ASCII Write-Output "Release ZIP: $zipPath" Write-Output "SHA-256: $zipHash" -Write-Output "Audited entries: $($releaseFiles.Count)" \ No newline at end of file +Write-Output "Audited entries: $($releaseFiles.Count)" diff --git a/Tests/Release/Run-PureBaseReleaseValidation.Tests.ps1 b/Tests/Release/Run-PureBaseReleaseValidation.Tests.ps1 index f80541e..dc53f01 100644 --- a/Tests/Release/Run-PureBaseReleaseValidation.Tests.ps1 +++ b/Tests/Release/Run-PureBaseReleaseValidation.Tests.ps1 @@ -50,26 +50,26 @@ Describe 'Release validation runner contracts' { It 'emits the shared Stencil ABI and pass policies for every product' { $stencilProperties = @('_StencilRef', '_StencilReadMask', '_StencilWriteMask', '_StencilComp', '_StencilPass', '_StencilFail', '_StencilZFail') $expectedVisibleProperties = [ordered]@{ - 'PureBase/Unlit' = @('_BaseTexture', '_BaseColor', '_SharedMask', '_SharedGradients', '_RenderingMode', '_Cutoff', '_Cull') + $stencilProperties - 'PureBase/Toon' = @('_BaseTexture', '_BaseColor', '_SharedMask', '_SharedGradients', '_RenderingMode', '_Cutoff', '_Cull') + $stencilProperties + @('_NormalMap', '_NormalScale') - 'PureBase/PBR' = @('_BaseTexture', '_BaseColor', '_SharedMask', '_SharedGradients', '_RenderingMode', '_Cutoff', '_Cull') + $stencilProperties + @('_NormalMap', '_NormalScale', '_Metallic', '_Roughness') + 'PureBase/Unlit' = @('_BaseTexture', '_BaseColor', '_SharedMask', '_SharedGradients', '_RenderingMode', '_Cutoff', '_Cull') + $stencilProperties + 'PureBase/Toon' = @('_BaseTexture', '_BaseColor', '_SharedMask', '_SharedGradients', '_RenderingMode', '_Cutoff', '_Cull') + $stencilProperties + @('_NormalMap', '_NormalScale') + 'PureBase/PBR' = @('_BaseTexture', '_BaseColor', '_SharedMask', '_SharedGradients', '_RenderingMode', '_Cutoff', '_Cull') + $stencilProperties + @('_NormalMap', '_NormalScale', '_Metallic', '_Roughness') 'PureBase/Hybrid' = @('_BaseTexture', '_BaseColor', '_SharedMask', '_SharedGradients', '_RenderingMode', '_Cutoff', '_Cull') + $stencilProperties + @('_NormalMap', '_NormalScale', '_Metallic', '_Roughness') } $expectedPassContracts = [ordered]@{ - 'ForwardBase' = [ordered]@{ - requiredFragments = @('ZWrite [_ZWrite]', 'Blend [_SrcBlend] [_DstBlend]', 'Ref [_StencilRef]', 'ReadMask [_StencilReadMask]', 'WriteMask [_StencilWriteMask]', 'Comp [_StencilComp]', 'Pass [_StencilPass]', 'Fail [_StencilFail]', 'ZFail [_StencilZFail]') + 'ForwardBase' = [ordered]@{ + requiredFragments = @('ZWrite [_ZWrite]', 'Blend [_SrcBlend] [_DstBlend]', 'Ref [_StencilRef]', 'ReadMask [_StencilReadMask]', 'WriteMask [_StencilWriteMask]', 'Comp [_StencilComp]', 'Pass [_StencilPass]', 'Fail [_StencilFail]', 'ZFail [_StencilZFail]') forbiddenFragments = @() } - 'ForwardAdd' = [ordered]@{ - requiredFragments = @('ZWrite Off', 'Blend [_AddSrcBlend] [_AddDstBlend]', 'ColorMask RGB', 'Ref [_StencilRef]', 'ReadMask [_StencilReadMask]', 'Comp [_StencilComp]', 'WriteMask 0', 'Pass Keep', 'Fail Keep', 'ZFail Keep') + 'ForwardAdd' = [ordered]@{ + requiredFragments = @('ZWrite Off', 'Blend [_AddSrcBlend] [_AddDstBlend]', 'ColorMask RGB', 'Ref [_StencilRef]', 'ReadMask [_StencilReadMask]', 'Comp [_StencilComp]', 'WriteMask 0', 'Pass Keep', 'Fail Keep', 'ZFail Keep') forbiddenFragments = @('WriteMask [_StencilWriteMask]', 'Pass [_StencilPass]', 'Fail [_StencilFail]', 'ZFail [_StencilZFail]') } 'ShadowCaster' = [ordered]@{ - requiredFragments = @() + requiredFragments = @() forbiddenFragments = @('Ref [_StencilRef]', 'ReadMask [_StencilReadMask]', 'WriteMask [_StencilWriteMask]', 'Comp [_StencilComp]', 'Pass [_StencilPass]', 'Fail [_StencilFail]', 'ZFail [_StencilZFail]') } - 'Meta' = [ordered]@{ - requiredFragments = @() + 'Meta' = [ordered]@{ + requiredFragments = @() forbiddenFragments = @('Ref [_StencilRef]', 'ReadMask [_StencilReadMask]', 'WriteMask [_StencilWriteMask]', 'Comp [_StencilComp]', 'Pass [_StencilPass]', 'Fail [_StencilFail]', 'ZFail [_StencilZFail]') } } @@ -98,361 +98,361 @@ Describe 'Release validation runner contracts' { } It 'executes the Unity-free immutable manifest harness' { -$preStagedPureBaseMetaPaths = @( - '_LocalPackages/jp.penguin.purebase/CHANGELOG.meta', - '_LocalPackages/jp.penguin.purebase/Editor.meta', - '_LocalPackages/jp.penguin.purebase/LICENSE.meta', - '_LocalPackages/jp.penguin.purebase/NOTICE.meta', - '_LocalPackages/jp.penguin.purebase/README.md.meta', - '_LocalPackages/jp.penguin.purebase/Shaders.meta', - '_LocalPackages/jp.penguin.purebase/package.json.meta' -) -$expectedFirstBootstrapAddedPaths = @(Get-ExpectedFirstBootstrapAddedPaths) -$firstBootstrapGeneratedMetaProfile = Get-FirstBootstrapGeneratedMetaProfile -foreach ($path in $preStagedPureBaseMetaPaths) { - Assert-Harness -Condition ($expectedFirstBootstrapAddedPaths -notcontains $path) -Message "Pre-staged PureBase meta remains classified as a first-bootstrap addition: '$path'." - Assert-Harness -Condition (-not $firstBootstrapGeneratedMetaProfile.Contains($path)) -Message "Pre-staged PureBase meta remains classified as Unity-generated: '$path'." -} - -function Assert-HarnessSemanticRejection { - param( - [Parameter(Mandatory = $true)]$SuccessfulCase, - [Parameter(Mandatory = $true)][string]$Mutation - ) - - $mutationRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseSemanticMutation-' + [guid]::NewGuid().ToString('N')) - try { - $consumerRoot = Join-Path $mutationRoot 'ConsumerProject' - Copy-Item -LiteralPath (Join-Path $SuccessfulCase.root 'ConsumerProject') -Destination $consumerRoot -Recurse -Force - $bootstrapDirectory = $SuccessfulCase.bootstrapDirectory - $pre = Get-Content -LiteralPath (Join-Path $bootstrapDirectory 'immutable-input-manifest-pre-bootstrap.json') -Raw | ConvertFrom-Json - $post = Get-Content -LiteralPath (Join-Path $bootstrapDirectory 'immutable-input-manifest-quiescent.json') -Raw | ConvertFrom-Json - $receipt = Get-Content -LiteralPath (Join-Path $bootstrapDirectory 'staging-receipt.json') -Raw | ConvertFrom-Json - if ($Mutation.StartsWith('invalid-generated-project-settings:', [System.StringComparison]::Ordinal)) { - $relativePath = $Mutation.Substring('invalid-generated-project-settings:'.Length) - $settingsProfile = Get-FirstBootstrapProjectSettingsProfile - if (-not $settingsProfile.Contains($relativePath)) { - throw "No generated ProjectSettings projection exists for semantic mutation '$relativePath'." - } - $projectionLine = [string]@($settingsProfile[$relativePath].requiredLines)[0] - $invalidProjectionLine = [regex]::Replace($projectionLine, '(:\s*).+$', '$1__invalid__') - $settingsPath = Join-Path $consumerRoot $relativePath.Replace('/', '\') - $settingsText = Get-Content -LiteralPath $settingsPath -Raw - if ($settingsText.IndexOf($projectionLine, [System.StringComparison]::Ordinal) -lt 0) { - throw "Harness ProjectSettings projection line is missing: '$projectionLine' in '$relativePath'." - } - [System.IO.File]::WriteAllText($settingsPath, $settingsText.Replace($projectionLine, $invalidProjectionLine), (New-Object System.Text.UTF8Encoding($false))) + $preStagedPureBaseMetaPaths = @( + '_LocalPackages/jp.penguin.purebase/CHANGELOG.meta', + '_LocalPackages/jp.penguin.purebase/Editor.meta', + '_LocalPackages/jp.penguin.purebase/LICENSE.meta', + '_LocalPackages/jp.penguin.purebase/NOTICE.meta', + '_LocalPackages/jp.penguin.purebase/README.md.meta', + '_LocalPackages/jp.penguin.purebase/Shaders.meta', + '_LocalPackages/jp.penguin.purebase/package.json.meta' + ) + $expectedFirstBootstrapAddedPaths = @(Get-ExpectedFirstBootstrapAddedPaths) + $firstBootstrapGeneratedMetaProfile = Get-FirstBootstrapGeneratedMetaProfile + foreach ($path in $preStagedPureBaseMetaPaths) { + Assert-Harness -Condition ($expectedFirstBootstrapAddedPaths -notcontains $path) -Message "Pre-staged PureBase meta remains classified as a first-bootstrap addition: '$path'." + Assert-Harness -Condition (-not $firstBootstrapGeneratedMetaProfile.Contains($path)) -Message "Pre-staged PureBase meta remains classified as Unity-generated: '$path'." } - else { - switch ($Mutation) { - 'source-mutation' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\manifest.json'), '{"dependencies":{}}', (New-Object System.Text.UTF8Encoding($false))) } - 'uri-escape' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\manifest.json'), '{"dependencies":{"com.unity.test-framework":"1.1.33","jp.lilxyzw.shadercore":"file:../../escape","jp.penguin.purebase":"file:../_LocalPackages/jp.penguin.purebase"}}', (New-Object System.Text.UTF8Encoding($false))) } - 'unknown-unity-manifest-dependency' { - $manifest = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\manifest.json') -Raw | ConvertFrom-Json - $manifest.dependencies | Add-Member -NotePropertyName 'com.unity.classifier-probe' -NotePropertyValue '1.0.0' - [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\manifest.json'), ($manifest | ConvertTo-Json -Depth 8 -Compress), (New-Object System.Text.UTF8Encoding($false))) - } - 'wrong-revision' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'ProjectSettings\ProjectVersion.txt'), "m_EditorVersion: 2022.3.22f1`nm_EditorVersionWithRevision: 2022.3.22f1 (000000000000)", (New-Object System.Text.UTF8Encoding($false))) } - 'lock-mismatch' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), '{"dependencies":{"com.unity.test-framework":{},"jp.lilxyzw.shadercore":{"source":"registry","path":"../_LocalPackages/jp.lilxyzw.shadercore"},"jp.penguin.purebase":{"source":"local","path":"../_LocalPackages/jp.penguin.purebase"}}}', (New-Object System.Text.UTF8Encoding($false))) } - 'lock-version-mismatch' { - $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json - $lock.dependencies.'com.unity.test-framework'.version = '1.1.34' - [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) - } - 'lock-source-mismatch' { - $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json - $lock.dependencies.'com.unity.test-framework'.source = 'builtin' - [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) - } - 'lock-newtonsoft-depth-mismatch' { - $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json - $lock.dependencies.'com.unity.nuget.newtonsoft-json'.depth = 2 - [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) - } - 'lock-shader-core-newtonsoft-edge-missing' { - $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json - $lock.dependencies.'jp.lilxyzw.shadercore'.dependencies.PSObject.Properties.Remove('com.unity.nuget.newtonsoft-json') - [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) - } - 'lock-shader-core-newtonsoft-edge-mismatch' { - $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json - $lock.dependencies.'jp.lilxyzw.shadercore'.dependencies.'com.unity.nuget.newtonsoft-json' = '3.0.1' - [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) - } - 'lock-added-entry' { - $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json - $lock.dependencies | Add-Member -NotePropertyName 'com.unity.classifier-probe' -NotePropertyValue ([pscustomobject][ordered]@{ version = '1.0.0'; depth = 0; source = 'registry'; dependencies = [pscustomobject][ordered]@{} }) - [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) - } - 'invalid-meta' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Assets\ReleaseConsumer\Fixtures.meta'), 'invalid', (New-Object System.Text.UTF8Encoding($false))) } - 'orphan-meta' { Remove-Item -LiteralPath (Join-Path $consumerRoot 'Assets\ReleaseModules') -Recurse -Force } - 'generated-meta-item-type-mismatch' { - $releaseModulesPath = Join-Path $consumerRoot 'Assets\ReleaseModules' - Remove-Item -LiteralPath $releaseModulesPath -Recurse -Force - [System.IO.File]::WriteAllText($releaseModulesPath, 'not a generated directory', (New-Object System.Text.UTF8Encoding($false))) - } - 'duplicate-meta' { - $firstGuid = [regex]::Match((Get-Content -LiteralPath (Join-Path $consumerRoot 'Assets\ReleaseConsumer\Fixtures.meta') -Raw), '(?m)^guid:\s*([0-9a-f]{32})\s*$').Groups[1].Value - $secondMetaPath = Join-Path $consumerRoot 'Assets\ReleaseModules.meta' - $secondMeta = Get-Content -LiteralPath $secondMetaPath -Raw - [System.IO.File]::WriteAllText($secondMetaPath, [regex]::Replace($secondMeta, '(?m)^guid:\s*[0-9a-f]{32}\s*$', ('guid: ' + $firstGuid)), (New-Object System.Text.UTF8Encoding($false))) - } - 'receipt-meta-collision' { - $receiptGuid = [regex]::Match((Get-Content -LiteralPath (Join-Path $consumerRoot 'Assets\ReceiptAnchor.asset.meta') -Raw), '(?m)^guid:\s*([0-9a-f]{32})\s*$').Groups[1].Value - $generatedMetaPath = Join-Path $consumerRoot 'Assets\ReleaseModules.meta' - $generatedMeta = Get-Content -LiteralPath $generatedMetaPath -Raw - [System.IO.File]::WriteAllText($generatedMetaPath, [regex]::Replace($generatedMeta, '(?m)^guid:\s*[0-9a-f]{32}\s*$', ('guid: ' + $receiptGuid)), (New-Object System.Text.UTF8Encoding($false))) - } - 'unknown-add' { $post.entries = @($post.entries) + [pscustomobject][ordered]@{ path = 'ProjectSettings/UnknownBootstrap.asset'; sha256 = 'unknown' } } - 'invalid-billing-mode' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Assets\Resources\BillingMode.json'), '{"mode":"Enterprise"}', (New-Object System.Text.UTF8Encoding($false))) } - 'invalid-project-settings' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'ProjectSettings\ProjectSettings.asset'), "%YAML 1.1`n%TAG !u! tag:unity3d.com,2011:`n--- !u!129 &1`nPlayerSettings:`n serializedVersion: 26`n companyName: DifferentCompany`n productName: ConsumerProject`n defaultScreenWidth: 1920`n defaultScreenHeight: 1080`n m_ActiveColorSpace: 0`n bundleVersion: 1.0", (New-Object System.Text.UTF8Encoding($false))) } - 'invalid-shader-core-settings' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset'), "%YAML 1.1`n%TAG !u! tag:unity3d.com,2011:`n--- !u!114 &1`nMonoBehaviour:`n shaderSettings:`n - shadername: PureBase/Invalid`n modules:`n -`n - shadername: PureBase/Toon`n modules:`n -`n - shadername: PureBase/PBR`n modules:`n -`n - shadername: PureBase/Hybrid`n modules:`n -`n - shadername: PureBase/Tests/FixtureRegistration`n modules:`n - jp.penguin.purebase.tests.fixture.registration", (New-Object System.Text.UTF8Encoding($false))) } - 'missing-fixed-shader-core-host' { - $settingsPath = Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset' - $settingsText = Get-Content -LiteralPath $settingsPath -Raw - $settingsText = [regex]::Replace($settingsText, '(?ms)^ - shadername: PureBase/Tests/ShaderCore/Phase/Morph\r?\n modules:\r?\n - jp\.penguin\.purebase\.tests\.shadercore\.phase\.morph\r?\n?', '') - [System.IO.File]::WriteAllText($settingsPath, $settingsText, (New-Object System.Text.UTF8Encoding($false))) - } - 'reversed-shader-core-module-order' { - $settingsPath = Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset' + + function Assert-HarnessSemanticRejection { + param( + [Parameter(Mandatory = $true)]$SuccessfulCase, + [Parameter(Mandatory = $true)][string]$Mutation + ) + + $mutationRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseSemanticMutation-' + [guid]::NewGuid().ToString('N')) + try { + $consumerRoot = Join-Path $mutationRoot 'ConsumerProject' + Copy-Item -LiteralPath (Join-Path $SuccessfulCase.root 'ConsumerProject') -Destination $consumerRoot -Recurse -Force + $bootstrapDirectory = $SuccessfulCase.bootstrapDirectory + $pre = Get-Content -LiteralPath (Join-Path $bootstrapDirectory 'immutable-input-manifest-pre-bootstrap.json') -Raw | ConvertFrom-Json + $post = Get-Content -LiteralPath (Join-Path $bootstrapDirectory 'immutable-input-manifest-quiescent.json') -Raw | ConvertFrom-Json + $receipt = Get-Content -LiteralPath (Join-Path $bootstrapDirectory 'staging-receipt.json') -Raw | ConvertFrom-Json + if ($Mutation.StartsWith('invalid-generated-project-settings:', [System.StringComparison]::Ordinal)) { + $relativePath = $Mutation.Substring('invalid-generated-project-settings:'.Length) + $settingsProfile = Get-FirstBootstrapProjectSettingsProfile + if (-not $settingsProfile.Contains($relativePath)) { + throw "No generated ProjectSettings projection exists for semantic mutation '$relativePath'." + } + $projectionLine = [string]@($settingsProfile[$relativePath].requiredLines)[0] + $invalidProjectionLine = [regex]::Replace($projectionLine, '(:\s*).+$', '$1__invalid__') + $settingsPath = Join-Path $consumerRoot $relativePath.Replace('/', '\') $settingsText = Get-Content -LiteralPath $settingsPath -Raw - $expectedOrder = " - jp.penguin.purebase.tests.shadercore.moduleorder.zeta`n - jp.penguin.purebase.tests.shadercore.moduleorder.alpha" - $reversedOrder = " - jp.penguin.purebase.tests.shadercore.moduleorder.alpha`n - jp.penguin.purebase.tests.shadercore.moduleorder.zeta" - if ($settingsText.IndexOf($expectedOrder, [System.StringComparison]::Ordinal) -lt 0) { - throw 'Harness Shader-Core ModuleOrder mapping did not contain the canonical zeta, alpha sequence.' + if ($settingsText.IndexOf($projectionLine, [System.StringComparison]::Ordinal) -lt 0) { + throw "Harness ProjectSettings projection line is missing: '$projectionLine' in '$relativePath'." } - [System.IO.File]::WriteAllText($settingsPath, $settingsText.Replace($expectedOrder, $reversedOrder), (New-Object System.Text.UTF8Encoding($false))) + [System.IO.File]::WriteAllText($settingsPath, $settingsText.Replace($projectionLine, $invalidProjectionLine), (New-Object System.Text.UTF8Encoding($false))) } - 'unexpected-shader-core-host' { - $settingsPath = Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset' - $settingsText = Get-Content -LiteralPath $settingsPath -Raw - [System.IO.File]::WriteAllText($settingsPath, ($settingsText.TrimEnd("`r", "`n") + "`n - shadername: PureBase/Unexpected`n modules: []`n"), (New-Object System.Text.UTF8Encoding($false))) + else { + switch ($Mutation) { + 'source-mutation' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\manifest.json'), '{"dependencies":{}}', (New-Object System.Text.UTF8Encoding($false))) } + 'uri-escape' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\manifest.json'), '{"dependencies":{"com.unity.test-framework":"1.1.33","jp.lilxyzw.shadercore":"file:../../escape","jp.penguin.purebase":"file:../_LocalPackages/jp.penguin.purebase"}}', (New-Object System.Text.UTF8Encoding($false))) } + 'unknown-unity-manifest-dependency' { + $manifest = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\manifest.json') -Raw | ConvertFrom-Json + $manifest.dependencies | Add-Member -NotePropertyName 'com.unity.classifier-probe' -NotePropertyValue '1.0.0' + [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\manifest.json'), ($manifest | ConvertTo-Json -Depth 8 -Compress), (New-Object System.Text.UTF8Encoding($false))) + } + 'wrong-revision' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'ProjectSettings\ProjectVersion.txt'), "m_EditorVersion: 2022.3.22f1`nm_EditorVersionWithRevision: 2022.3.22f1 (000000000000)", (New-Object System.Text.UTF8Encoding($false))) } + 'lock-mismatch' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), '{"dependencies":{"com.unity.test-framework":{},"jp.lilxyzw.shadercore":{"source":"registry","path":"../_LocalPackages/jp.lilxyzw.shadercore"},"jp.penguin.purebase":{"source":"local","path":"../_LocalPackages/jp.penguin.purebase"}}}', (New-Object System.Text.UTF8Encoding($false))) } + 'lock-version-mismatch' { + $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json + $lock.dependencies.'com.unity.test-framework'.version = '1.1.34' + [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) + } + 'lock-source-mismatch' { + $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json + $lock.dependencies.'com.unity.test-framework'.source = 'builtin' + [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) + } + 'lock-newtonsoft-depth-mismatch' { + $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json + $lock.dependencies.'com.unity.nuget.newtonsoft-json'.depth = 2 + [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) + } + 'lock-shader-core-newtonsoft-edge-missing' { + $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json + $lock.dependencies.'jp.lilxyzw.shadercore'.dependencies.PSObject.Properties.Remove('com.unity.nuget.newtonsoft-json') + [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) + } + 'lock-shader-core-newtonsoft-edge-mismatch' { + $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json + $lock.dependencies.'jp.lilxyzw.shadercore'.dependencies.'com.unity.nuget.newtonsoft-json' = '3.0.1' + [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) + } + 'lock-added-entry' { + $lock = Get-Content -LiteralPath (Join-Path $consumerRoot 'Packages\packages-lock.json') -Raw | ConvertFrom-Json + $lock.dependencies | Add-Member -NotePropertyName 'com.unity.classifier-probe' -NotePropertyValue ([pscustomobject][ordered]@{ version = '1.0.0'; depth = 0; source = 'registry'; dependencies = [pscustomobject][ordered]@{} }) + [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Packages\packages-lock.json'), ($lock | ConvertTo-Json -Depth 12 -Compress), (New-Object System.Text.UTF8Encoding($false))) + } + 'invalid-meta' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Assets\ReleaseConsumer\Fixtures.meta'), 'invalid', (New-Object System.Text.UTF8Encoding($false))) } + 'orphan-meta' { Remove-Item -LiteralPath (Join-Path $consumerRoot 'Assets\ReleaseModules') -Recurse -Force } + 'generated-meta-item-type-mismatch' { + $releaseModulesPath = Join-Path $consumerRoot 'Assets\ReleaseModules' + Remove-Item -LiteralPath $releaseModulesPath -Recurse -Force + [System.IO.File]::WriteAllText($releaseModulesPath, 'not a generated directory', (New-Object System.Text.UTF8Encoding($false))) + } + 'duplicate-meta' { + $firstGuid = [regex]::Match((Get-Content -LiteralPath (Join-Path $consumerRoot 'Assets\ReleaseConsumer\Fixtures.meta') -Raw), '(?m)^guid:\s*([0-9a-f]{32})\s*$').Groups[1].Value + $secondMetaPath = Join-Path $consumerRoot 'Assets\ReleaseModules.meta' + $secondMeta = Get-Content -LiteralPath $secondMetaPath -Raw + [System.IO.File]::WriteAllText($secondMetaPath, [regex]::Replace($secondMeta, '(?m)^guid:\s*[0-9a-f]{32}\s*$', ('guid: ' + $firstGuid)), (New-Object System.Text.UTF8Encoding($false))) + } + 'receipt-meta-collision' { + $receiptGuid = [regex]::Match((Get-Content -LiteralPath (Join-Path $consumerRoot 'Assets\ReceiptAnchor.asset.meta') -Raw), '(?m)^guid:\s*([0-9a-f]{32})\s*$').Groups[1].Value + $generatedMetaPath = Join-Path $consumerRoot 'Assets\ReleaseModules.meta' + $generatedMeta = Get-Content -LiteralPath $generatedMetaPath -Raw + [System.IO.File]::WriteAllText($generatedMetaPath, [regex]::Replace($generatedMeta, '(?m)^guid:\s*[0-9a-f]{32}\s*$', ('guid: ' + $receiptGuid)), (New-Object System.Text.UTF8Encoding($false))) + } + 'unknown-add' { $post.entries = @($post.entries) + [pscustomobject][ordered]@{ path = 'ProjectSettings/UnknownBootstrap.asset'; sha256 = 'unknown' } } + 'invalid-billing-mode' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'Assets\Resources\BillingMode.json'), '{"mode":"Enterprise"}', (New-Object System.Text.UTF8Encoding($false))) } + 'invalid-project-settings' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'ProjectSettings\ProjectSettings.asset'), "%YAML 1.1`n%TAG !u! tag:unity3d.com,2011:`n--- !u!129 &1`nPlayerSettings:`n serializedVersion: 26`n companyName: DifferentCompany`n productName: ConsumerProject`n defaultScreenWidth: 1920`n defaultScreenHeight: 1080`n m_ActiveColorSpace: 0`n bundleVersion: 1.0", (New-Object System.Text.UTF8Encoding($false))) } + 'invalid-shader-core-settings' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset'), "%YAML 1.1`n%TAG !u! tag:unity3d.com,2011:`n--- !u!114 &1`nMonoBehaviour:`n shaderSettings:`n - shadername: PureBase/Invalid`n modules:`n -`n - shadername: PureBase/Toon`n modules:`n -`n - shadername: PureBase/PBR`n modules:`n -`n - shadername: PureBase/Hybrid`n modules:`n -`n - shadername: PureBase/Tests/FixtureRegistration`n modules:`n - jp.penguin.purebase.tests.fixture.registration", (New-Object System.Text.UTF8Encoding($false))) } + 'missing-fixed-shader-core-host' { + $settingsPath = Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset' + $settingsText = Get-Content -LiteralPath $settingsPath -Raw + $settingsText = [regex]::Replace($settingsText, '(?ms)^ - shadername: PureBase/Tests/ShaderCore/Phase/Morph\r?\n modules:\r?\n - jp\.penguin\.purebase\.tests\.shadercore\.phase\.morph\r?\n?', '') + [System.IO.File]::WriteAllText($settingsPath, $settingsText, (New-Object System.Text.UTF8Encoding($false))) + } + 'reversed-shader-core-module-order' { + $settingsPath = Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset' + $settingsText = Get-Content -LiteralPath $settingsPath -Raw + $expectedOrder = " - jp.penguin.purebase.tests.shadercore.moduleorder.zeta`n - jp.penguin.purebase.tests.shadercore.moduleorder.alpha" + $reversedOrder = " - jp.penguin.purebase.tests.shadercore.moduleorder.alpha`n - jp.penguin.purebase.tests.shadercore.moduleorder.zeta" + if ($settingsText.IndexOf($expectedOrder, [System.StringComparison]::Ordinal) -lt 0) { + throw 'Harness Shader-Core ModuleOrder mapping did not contain the canonical zeta, alpha sequence.' + } + [System.IO.File]::WriteAllText($settingsPath, $settingsText.Replace($expectedOrder, $reversedOrder), (New-Object System.Text.UTF8Encoding($false))) + } + 'unexpected-shader-core-host' { + $settingsPath = Join-Path $consumerRoot 'ProjectSettings\jp.lilxyzw.shadercore.asset' + $settingsText = Get-Content -LiteralPath $settingsPath -Raw + [System.IO.File]::WriteAllText($settingsPath, ($settingsText.TrimEnd("`r", "`n") + "`n - shadername: PureBase/Unexpected`n modules: []`n"), (New-Object System.Text.UTF8Encoding($false))) + } + default { throw "Unknown semantic mutation '$Mutation'." } + } } - default { throw "Unknown semantic mutation '$Mutation'." } + + $report = Get-ConsumerFirstBootstrapTransitionReport -ConsumerRoot $consumerRoot -StagingReceipt $receipt -PreBootstrap $pre -PostBootstrap $post + Assert-Harness -Condition ($report.verdict -eq 'rejected' -and ($report.summary.rejected -gt 0 -or $report.summary.unclassified -gt 0)) -Message "Semantic mutation '$Mutation' did not produce a rejected report." + $failure = $null + try { Assert-ConsumerFirstBootstrapTransitionReport -Report $report } + catch { $failure = $_ } + Assert-Harness -Condition ($null -ne $failure) -Message "Semantic mutation '$Mutation' did not fail closed." + } + finally { + Remove-Item -LiteralPath $mutationRoot -Recurse -Force -ErrorAction SilentlyContinue } } - $report = Get-ConsumerFirstBootstrapTransitionReport -ConsumerRoot $consumerRoot -StagingReceipt $receipt -PreBootstrap $pre -PostBootstrap $post - Assert-Harness -Condition ($report.verdict -eq 'rejected' -and ($report.summary.rejected -gt 0 -or $report.summary.unclassified -gt 0)) -Message "Semantic mutation '$Mutation' did not produce a rejected report." - $failure = $null - try { Assert-ConsumerFirstBootstrapTransitionReport -Report $report } - catch { $failure = $_ } - Assert-Harness -Condition ($null -ne $failure) -Message "Semantic mutation '$Mutation' did not fail closed." - } - finally { - Remove-Item -LiteralPath $mutationRoot -Recurse -Force -ErrorAction SilentlyContinue - } -} + function New-HarnessManifest { + param( + [Parameter(Mandatory = $true)][string]$RootHash, + [Parameter(Mandatory = $true)][string]$ConsumerRoot, + [Parameter()][string]$Transition = 'approved' + ) -function New-HarnessManifest { - param( - [Parameter(Mandatory = $true)][string]$RootHash, - [Parameter(Mandatory = $true)][string]$ConsumerRoot, - [Parameter()][string]$Transition = 'approved' - ) - - $entries = @( - [ordered]@{ path = 'Packages/manifest.json'; sha256 = if ($Transition -eq 'pre-bootstrap') { 'pre-manifest' } else { 'post-manifest' } }, - [ordered]@{ path = 'ProjectSettings/ProjectVersion.txt'; sha256 = if ($Transition -eq 'pre-bootstrap') { 'pre-project-version' } else { 'post-project-version' } }, - [ordered]@{ path = 'ProjectSettings/QualitySettings.asset'; sha256 = Get-Sha256Hex -Path (Join-Path $ConsumerRoot 'ProjectSettings\QualitySettings.asset') } - ) - if ($Transition -ne 'pre-bootstrap') { - foreach ($path in Get-ExpectedFirstBootstrapAddedPaths) { - $absolutePath = Join-Path $ConsumerRoot $path.Replace('/', '\') - $entries += [ordered]@{ path = $path; sha256 = Get-Sha256Hex -Path $absolutePath } - } - } - if ($Transition -eq 'unexpected-new-immutable-entry') { - $entries += [ordered]@{ path = 'ProjectSettings/UnexpectedBootstrapSettings.json'; sha256 = 'unexpected-bootstrap-settings' } - } - if ($Transition -eq 'removal') { - $entries = @($entries | Where-Object { $_.path -ne 'ProjectSettings/ProjectVersion.txt' }) - } - if ($Transition -eq 'existing-entry-content-mutation') { - $entries += [ordered]@{ path = 'ProjectSettings/jp.lilxyzw.shadercore.asset'; sha256 = 'unexpected-source-mutation' } - } + $entries = @( + [ordered]@{ path = 'Packages/manifest.json'; sha256 = if ($Transition -eq 'pre-bootstrap') { 'pre-manifest' } else { 'post-manifest' } }, + [ordered]@{ path = 'ProjectSettings/ProjectVersion.txt'; sha256 = if ($Transition -eq 'pre-bootstrap') { 'pre-project-version' } else { 'post-project-version' } }, + [ordered]@{ path = 'ProjectSettings/QualitySettings.asset'; sha256 = Get-Sha256Hex -Path (Join-Path $ConsumerRoot 'ProjectSettings\QualitySettings.asset') } + ) + if ($Transition -ne 'pre-bootstrap') { + foreach ($path in Get-ExpectedFirstBootstrapAddedPaths) { + $absolutePath = Join-Path $ConsumerRoot $path.Replace('/', '\') + $entries += [ordered]@{ path = $path; sha256 = Get-Sha256Hex -Path $absolutePath } + } + } + if ($Transition -eq 'unexpected-new-immutable-entry') { + $entries += [ordered]@{ path = 'ProjectSettings/UnexpectedBootstrapSettings.json'; sha256 = 'unexpected-bootstrap-settings' } + } + if ($Transition -eq 'removal') { + $entries = @($entries | Where-Object { $_.path -ne 'ProjectSettings/ProjectVersion.txt' }) + } + if ($Transition -eq 'existing-entry-content-mutation') { + $entries += [ordered]@{ path = 'ProjectSettings/jp.lilxyzw.shadercore.asset'; sha256 = 'unexpected-source-mutation' } + } - return [ordered]@{ - schemaVersion = 1 - pathOrdering = 'System.StringComparer.Ordinal' - immutableRoots = @('Assets', 'Packages', 'ProjectSettings', '_LocalPackages') - excludedMutablePathPrefixes = @('Assets/Artifacts/', 'Library/') - rootSha256 = $RootHash - entries = $entries - releaseZipSha256 = 'release-zip' - shaderCore = [ordered]@{ - packageName = 'jp.lilxyzw.shadercore' - packageVersion = '0.1.9' - expectedIdentitySha256 = 'shader-core' - treeSha256 = 'shader-core' + return [ordered]@{ + schemaVersion = 1 + pathOrdering = 'System.StringComparer.Ordinal' + immutableRoots = @('Assets', 'Packages', 'ProjectSettings', '_LocalPackages') + excludedMutablePathPrefixes = @('Assets/Artifacts/', 'Library/') + rootSha256 = $RootHash + entries = $entries + releaseZipSha256 = 'release-zip' + shaderCore = [ordered]@{ + packageName = 'jp.lilxyzw.shadercore' + packageVersion = '0.1.9' + expectedIdentitySha256 = 'shader-core' + treeSha256 = 'shader-core' + } + } } - } -} -function New-HarnessStagingReceipt { - param([Parameter(Mandatory = $true)][string]$ConsumerRoot) - - $entries = @() - foreach ($file in Get-ChildItem -LiteralPath $ConsumerRoot -File -Recurse -Force | Sort-Object -Property FullName) { - $destination = Get-NormalizedRelativePath -Path $file.FullName.Substring($ConsumerRoot.Length).TrimStart('\', '/') - $entries += [ordered]@{ - destination = $destination - sourceKind = 'consumer-scaffold' - source = $file.FullName - sha256 = Get-Sha256Hex -Path $file.FullName + function New-HarnessStagingReceipt { + param([Parameter(Mandatory = $true)][string]$ConsumerRoot) + + $entries = @() + foreach ($file in Get-ChildItem -LiteralPath $ConsumerRoot -File -Recurse -Force | Sort-Object -Property FullName) { + $destination = Get-NormalizedRelativePath -Path $file.FullName.Substring($ConsumerRoot.Length).TrimStart('\', '/') + $entries += [ordered]@{ + destination = $destination + sourceKind = 'consumer-scaffold' + source = $file.FullName + sha256 = Get-Sha256Hex -Path $file.FullName + } + } + return [ordered]@{ + schemaName = 'purebase-consumer-staging-receipt' + schemaVersion = 1 + pathOrdering = 'System.StringComparer.Ordinal' + entries = $entries + } } - } - return [ordered]@{ - schemaName = 'purebase-consumer-staging-receipt' - schemaVersion = 1 - pathOrdering = 'System.StringComparer.Ordinal' - entries = $entries - } -} -function Initialize-HarnessFirstBootstrapFiles { - param([Parameter(Mandatory = $true)][string]$ConsumerRoot) + function Initialize-HarnessFirstBootstrapFiles { + param([Parameter(Mandatory = $true)][string]$ConsumerRoot) - function Write-HarnessBootstrapFile { - param([Parameter(Mandatory = $true)][string]$RelativePath, [Parameter(Mandatory = $true)][string]$Content) + function Write-HarnessBootstrapFile { + param([Parameter(Mandatory = $true)][string]$RelativePath, [Parameter(Mandatory = $true)][string]$Content) - $path = Join-Path $ConsumerRoot $RelativePath.Replace('/', '\') - New-Item -ItemType Directory -Path (Split-Path -Parent $path) -Force | Out-Null - [System.IO.File]::WriteAllText($path, $Content, (New-Object System.Text.UTF8Encoding($false))) - } + $path = Join-Path $ConsumerRoot $RelativePath.Replace('/', '\') + New-Item -ItemType Directory -Path (Split-Path -Parent $path) -Force | Out-Null + [System.IO.File]::WriteAllText($path, $Content, (New-Object System.Text.UTF8Encoding($false))) + } - $packageProfile = Get-FirstBootstrapPackageProfile - $manifestDependencies = [ordered]@{} - foreach ($dependencyName in $packageProfile.manifestDependencies.Keys) { $manifestDependencies[$dependencyName] = $packageProfile.manifestDependencies[$dependencyName] } - Write-HarnessBootstrapFile -RelativePath 'Packages/manifest.json' -Content (([ordered]@{ dependencies = $manifestDependencies }) | ConvertTo-Json -Depth 4 -Compress) - Write-HarnessBootstrapFile -RelativePath 'ProjectSettings/ProjectVersion.txt' -Content @' + $packageProfile = Get-FirstBootstrapPackageProfile + $manifestDependencies = [ordered]@{} + foreach ($dependencyName in $packageProfile.manifestDependencies.Keys) { $manifestDependencies[$dependencyName] = $packageProfile.manifestDependencies[$dependencyName] } + Write-HarnessBootstrapFile -RelativePath 'Packages/manifest.json' -Content (([ordered]@{ dependencies = $manifestDependencies }) | ConvertTo-Json -Depth 4 -Compress) + Write-HarnessBootstrapFile -RelativePath 'ProjectSettings/ProjectVersion.txt' -Content @' m_EditorVersion: 2022.3.22f1 m_EditorVersionWithRevision: 2022.3.22f1 (887be4894c44) '@ - $lockDependencies = [ordered]@{} - foreach ($dependencyName in $packageProfile.lockDependencies.Keys) { - $expected = $packageProfile.lockDependencies[$dependencyName] - $lockDependencies[$dependencyName] = [ordered]@{ version = $expected.version; depth = $expected.depth; source = $expected.source; dependencies = $expected.dependencies } - } - Write-HarnessBootstrapFile -RelativePath 'Packages/packages-lock.json' -Content (([ordered]@{ dependencies = $lockDependencies }) | ConvertTo-Json -Depth 8 -Compress) - Write-HarnessBootstrapFile -RelativePath 'Assets/Resources/BillingMode.json' -Content '{"androidStore":"GooglePlay"}' - foreach ($path in Get-ExpectedFirstBootstrapAddedPaths) { - if ($path -eq 'Packages/packages-lock.json' -or $path -eq 'Assets/Resources/BillingMode.json') { - continue - } - if ($path.EndsWith('.meta', [System.StringComparison]::Ordinal)) { - $assetPath = $path.Substring(0, $path.Length - '.meta'.Length) - $assetAbsolutePath = Join-Path $ConsumerRoot $assetPath.Replace('/', '\') - if ([System.IO.Path]::HasExtension($assetAbsolutePath) -or $assetPath.EndsWith('/LICENSE', [System.StringComparison]::Ordinal) -or $assetPath.EndsWith('/NOTICE', [System.StringComparison]::Ordinal)) { - New-Item -ItemType Directory -Path (Split-Path -Parent $assetAbsolutePath) -Force | Out-Null - if (-not (Test-Path -LiteralPath $assetAbsolutePath)) { [System.IO.File]::WriteAllText($assetAbsolutePath, 'asset', (New-Object System.Text.UTF8Encoding($false))) } - } - else { - New-Item -ItemType Directory -Path $assetAbsolutePath -Force | Out-Null - } - $guid = ([guid]::NewGuid().ToString('N')) - Write-HarnessBootstrapFile -RelativePath $path -Content ("fileFormatVersion: 2`nGUID: $guid`nDefaultImporter:") - $metaPath = Join-Path $ConsumerRoot $path.Replace('/', '\') - (Get-Content -LiteralPath $metaPath -Raw).Replace('GUID:', 'guid:') | Set-Content -LiteralPath $metaPath -Encoding UTF8 - continue - } - if ($path -eq 'ProjectSettings/SceneTemplateSettings.json') { - $dependencyTypeInfos = @(1..22 | ForEach-Object { [ordered]@{ userAdded = $false; type = 'UnityEngine.GameObject'; defaultInstantiationMode = 0 } }) - $sceneTemplateProfile = [ordered]@{ templatePinStates = @(); dependencyTypeInfos = $dependencyTypeInfos; defaultDependencyTypeInfo = [ordered]@{ userAdded = $false; type = ''; defaultInstantiationMode = 1 }; newSceneOverride = 0 } - Write-HarnessBootstrapFile -RelativePath $path -Content ($sceneTemplateProfile | ConvertTo-Json -Depth 6 -Compress) - continue - } - if ($path -eq 'ProjectSettings/jp.lilxyzw.shadercore.asset') { - $shaderCoreLines = @("%YAML 1.1", '%TAG !u! tag:unity3d.com,2011:', '--- !u!114 &1', 'MonoBehaviour:', ' shaderSettings:') - foreach ($shaderName in (Get-FirstBootstrapShaderCoreSettingsProfile).Keys) { - $modules = @((Get-FirstBootstrapShaderCoreSettingsProfile)[$shaderName]) - $shaderCoreLines += ' - shadername: ' + $shaderName - if ($modules.Count -eq 0) { - $shaderCoreLines += ' modules: []' + $lockDependencies = [ordered]@{} + foreach ($dependencyName in $packageProfile.lockDependencies.Keys) { + $expected = $packageProfile.lockDependencies[$dependencyName] + $lockDependencies[$dependencyName] = [ordered]@{ version = $expected.version; depth = $expected.depth; source = $expected.source; dependencies = $expected.dependencies } + } + Write-HarnessBootstrapFile -RelativePath 'Packages/packages-lock.json' -Content (([ordered]@{ dependencies = $lockDependencies }) | ConvertTo-Json -Depth 8 -Compress) + Write-HarnessBootstrapFile -RelativePath 'Assets/Resources/BillingMode.json' -Content '{"androidStore":"GooglePlay"}' + foreach ($path in Get-ExpectedFirstBootstrapAddedPaths) { + if ($path -eq 'Packages/packages-lock.json' -or $path -eq 'Assets/Resources/BillingMode.json') { + continue } - else { - $shaderCoreLines += ' modules:' - $shaderCoreLines += @($modules | ForEach-Object { ' - ' + $_ }) + if ($path.EndsWith('.meta', [System.StringComparison]::Ordinal)) { + $assetPath = $path.Substring(0, $path.Length - '.meta'.Length) + $assetAbsolutePath = Join-Path $ConsumerRoot $assetPath.Replace('/', '\') + if ([System.IO.Path]::HasExtension($assetAbsolutePath) -or $assetPath.EndsWith('/LICENSE', [System.StringComparison]::Ordinal) -or $assetPath.EndsWith('/NOTICE', [System.StringComparison]::Ordinal)) { + New-Item -ItemType Directory -Path (Split-Path -Parent $assetAbsolutePath) -Force | Out-Null + if (-not (Test-Path -LiteralPath $assetAbsolutePath)) { [System.IO.File]::WriteAllText($assetAbsolutePath, 'asset', (New-Object System.Text.UTF8Encoding($false))) } + } + else { + New-Item -ItemType Directory -Path $assetAbsolutePath -Force | Out-Null + } + $guid = ([guid]::NewGuid().ToString('N')) + Write-HarnessBootstrapFile -RelativePath $path -Content ("fileFormatVersion: 2`nGUID: $guid`nDefaultImporter:") + $metaPath = Join-Path $ConsumerRoot $path.Replace('/', '\') + (Get-Content -LiteralPath $metaPath -Raw).Replace('GUID:', 'guid:') | Set-Content -LiteralPath $metaPath -Encoding UTF8 + continue + } + if ($path -eq 'ProjectSettings/SceneTemplateSettings.json') { + $dependencyTypeInfos = @(1..22 | ForEach-Object { [ordered]@{ userAdded = $false; type = 'UnityEngine.GameObject'; defaultInstantiationMode = 0 } }) + $sceneTemplateProfile = [ordered]@{ templatePinStates = @(); dependencyTypeInfos = $dependencyTypeInfos; defaultDependencyTypeInfo = [ordered]@{ userAdded = $false; type = ''; defaultInstantiationMode = 1 }; newSceneOverride = 0 } + Write-HarnessBootstrapFile -RelativePath $path -Content ($sceneTemplateProfile | ConvertTo-Json -Depth 6 -Compress) + continue } + if ($path -eq 'ProjectSettings/jp.lilxyzw.shadercore.asset') { + $shaderCoreLines = @("%YAML 1.1", '%TAG !u! tag:unity3d.com,2011:', '--- !u!114 &1', 'MonoBehaviour:', ' shaderSettings:') + foreach ($shaderName in (Get-FirstBootstrapShaderCoreSettingsProfile).Keys) { + $modules = @((Get-FirstBootstrapShaderCoreSettingsProfile)[$shaderName]) + $shaderCoreLines += ' - shadername: ' + $shaderName + if ($modules.Count -eq 0) { + $shaderCoreLines += ' modules: []' + } + else { + $shaderCoreLines += ' modules:' + $shaderCoreLines += @($modules | ForEach-Object { ' - ' + $_ }) + } + } + Write-HarnessBootstrapFile -RelativePath $path -Content ($shaderCoreLines -join "`n") + continue + } + $settingsProfile = Get-FirstBootstrapProjectSettingsProfile + $projection = $settingsProfile[$path] + $projectionLines = @($projection.requiredLines) + Write-HarnessBootstrapFile -RelativePath $path -Content ((@("%YAML 1.1", '%TAG !u! tag:unity3d.com,2011:', '--- !u!129 &1', ($projection.root + ':')) + $projectionLines) -join "`n") } - Write-HarnessBootstrapFile -RelativePath $path -Content ($shaderCoreLines -join "`n") - continue } - $settingsProfile = Get-FirstBootstrapProjectSettingsProfile - $projection = $settingsProfile[$path] - $projectionLines = @($projection.requiredLines) - Write-HarnessBootstrapFile -RelativePath $path -Content ((@("%YAML 1.1", '%TAG !u! tag:unity3d.com,2011:', '--- !u!129 &1', ($projection.root + ':')) + $projectionLines) -join "`n") - } -} -$script:HarnessManifestHashes = @() -$script:HarnessManifestIndex = 0 -$script:HarnessBootstrapTransition = 'approved' -$script:HarnessBootstrapSemanticMutation = 'none' -$script:HarnessBaselineMismatch = $false -$script:HarnessEditorGuardFailure = $false -$script:HarnessResetCalls = 0 -$script:HarnessCimProcesses = @() -$script:HarnessCimException = $null -$script:coldLibraryResetCount = 0 - -function Assert-EditorClosed { - param([Parameter(Mandatory = $true)][string]$ProjectRoot) - - if ($script:HarnessEditorGuardFailure) { - throw "Synthetic editor guard failure for '$ProjectRoot'." - } -} + $script:HarnessManifestHashes = @() + $script:HarnessManifestIndex = 0 + $script:HarnessBootstrapTransition = 'approved' + $script:HarnessBootstrapSemanticMutation = 'none' + $script:HarnessBaselineMismatch = $false + $script:HarnessEditorGuardFailure = $false + $script:HarnessResetCalls = 0 + $script:HarnessCimProcesses = @() + $script:HarnessCimException = $null + $script:coldLibraryResetCount = 0 -function Get-CimInstance { - [CmdletBinding()] - param( - [Parameter(Position = 0, Mandatory = $true)][string]$ClassName, - [Parameter()][string]$Filter - ) + function Assert-EditorClosed { + param([Parameter(Mandatory = $true)][string]$ProjectRoot) - if ($null -ne $script:HarnessCimException) { - throw $script:HarnessCimException - } - return $script:HarnessCimProcesses -} + if ($script:HarnessEditorGuardFailure) { + throw "Synthetic editor guard failure for '$ProjectRoot'." + } + } -function Write-ConsumerContract { - param([Parameter(Mandatory = $true)][string]$ConsumerRoot, [Parameter(Mandatory = $true)]$Contract) -} + function Get-CimInstance { + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = $true)][string]$ClassName, + [Parameter()][string]$Filter + ) + + if ($null -ne $script:HarnessCimException) { + throw $script:HarnessCimException + } + return $script:HarnessCimProcesses + } -function Reset-ConsumerLibrary { - param([Parameter(Mandatory = $true)][string]$ConsumerRoot) + function Write-ConsumerContract { + param([Parameter(Mandatory = $true)][string]$ConsumerRoot, [Parameter(Mandatory = $true)]$Contract) + } - $script:HarnessResetCalls++ - return [ordered]@{ libraryPath = (Join-Path $ConsumerRoot 'Library'); priorLibraryPresent = $true; libraryPresentAfterReset = $false } -} + function Reset-ConsumerLibrary { + param([Parameter(Mandatory = $true)][string]$ConsumerRoot) -function Get-ConsumerImmutableManifest { - param([Parameter(Mandatory = $true)][string]$ConsumerRoot, [Parameter(Mandatory = $true)][string]$ZipPath, [Parameter(Mandatory = $true)][string]$ShaderCoreManifestPath) - - $callIndex = $script:HarnessManifestIndex - $hashIndex = if ($callIndex -le 4) { 0 } else { [Math]::Min($callIndex - 3, $script:HarnessManifestHashes.Count - 1) } - $transition = if ($callIndex -eq 0) { 'pre-bootstrap' } elseif ($callIndex -eq 1) { $script:HarnessBootstrapTransition } else { 'approved' } - if ($callIndex -eq 1) { - Initialize-HarnessFirstBootstrapFiles -ConsumerRoot $ConsumerRoot - if ($script:HarnessBootstrapSemanticMutation -eq 'receipt-meta-collision') { - $receiptGuid = [regex]::Match((Get-Content -LiteralPath (Join-Path $ConsumerRoot 'Assets\ReceiptAnchor.asset.meta') -Raw), '(?m)^guid:\s*([0-9a-f]{32})\s*$').Groups[1].Value - $generatedMetaPath = Join-Path $ConsumerRoot 'Assets\ReleaseModules.meta' - $generatedMeta = Get-Content -LiteralPath $generatedMetaPath -Raw - [System.IO.File]::WriteAllText($generatedMetaPath, [regex]::Replace($generatedMeta, '(?m)^guid:\s*[0-9a-f]{32}\s*$', ('guid: ' + $receiptGuid)), (New-Object System.Text.UTF8Encoding($false))) + $script:HarnessResetCalls++ + return [ordered]@{ libraryPath = (Join-Path $ConsumerRoot 'Library'); priorLibraryPresent = $true; libraryPresentAfterReset = $false } + } + + function Get-ConsumerImmutableManifest { + param([Parameter(Mandatory = $true)][string]$ConsumerRoot, [Parameter(Mandatory = $true)][string]$ZipPath, [Parameter(Mandatory = $true)][string]$ShaderCoreManifestPath) + + $callIndex = $script:HarnessManifestIndex + $hashIndex = if ($callIndex -le 4) { 0 } else { [Math]::Min($callIndex - 3, $script:HarnessManifestHashes.Count - 1) } + $transition = if ($callIndex -eq 0) { 'pre-bootstrap' } elseif ($callIndex -eq 1) { $script:HarnessBootstrapTransition } else { 'approved' } + if ($callIndex -eq 1) { + Initialize-HarnessFirstBootstrapFiles -ConsumerRoot $ConsumerRoot + if ($script:HarnessBootstrapSemanticMutation -eq 'receipt-meta-collision') { + $receiptGuid = [regex]::Match((Get-Content -LiteralPath (Join-Path $ConsumerRoot 'Assets\ReceiptAnchor.asset.meta') -Raw), '(?m)^guid:\s*([0-9a-f]{32})\s*$').Groups[1].Value + $generatedMetaPath = Join-Path $ConsumerRoot 'Assets\ReleaseModules.meta' + $generatedMeta = Get-Content -LiteralPath $generatedMetaPath -Raw + [System.IO.File]::WriteAllText($generatedMetaPath, [regex]::Replace($generatedMeta, '(?m)^guid:\s*[0-9a-f]{32}\s*$', ('guid: ' + $receiptGuid)), (New-Object System.Text.UTF8Encoding($false))) + } + } + $manifest = New-HarnessManifest -RootHash $script:HarnessManifestHashes[$hashIndex] -ConsumerRoot $ConsumerRoot -Transition $transition + $script:HarnessManifestIndex++ + if ($script:HarnessBaselineMismatch -and $callIndex -gt 0) { + $manifest.shaderCore.treeSha256 = 'unexpected-shader-core' + } + return $manifest } - } - $manifest = New-HarnessManifest -RootHash $script:HarnessManifestHashes[$hashIndex] -ConsumerRoot $ConsumerRoot -Transition $transition - $script:HarnessManifestIndex++ - if ($script:HarnessBaselineMismatch -and $callIndex -gt 0) { - $manifest.shaderCore.treeSha256 = 'unexpected-shader-core' - } - return $manifest -} -$fakeUnityPath = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseFakeUnity-' + [guid]::NewGuid().ToString('N') + '.cmd') -$fakeUnitySource = @' + $fakeUnityPath = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseFakeUnity-' + [guid]::NewGuid().ToString('N') + '.cmd') + $fakeUnitySource = @' @echo off set RESULTS= set LOG= @@ -488,768 +488,769 @@ if /I "%PUREBASE_HARNESS_BOOTSTRAP_NUNIT_RESULT%"=="Failed" goto exit if not "%RESULTS%"=="" > "%RESULTS%" echo ^ exit /b %PUREBASE_HARNESS_BOOTSTRAP_EXIT% '@ -[System.IO.File]::WriteAllText($fakeUnityPath, $fakeUnitySource, (New-Object System.Text.ASCIIEncoding)) - -function Invoke-HarnessCase { - param( - [Parameter(Mandatory = $true)][string]$Label, - [Parameter()]$Contract = $null, - [Parameter()][hashtable]$Selections = @{}, - [Parameter()][switch]$RequireColdLibraryReset, - [Parameter()][switch]$SkipColdLibraryReset, - [Parameter(Mandatory = $true)][string[]]$ManifestHashes, - [Parameter()][switch]$BaselineMismatch, - [Parameter()][switch]$EditorGuardFailure, - [Parameter()][int]$BootstrapExitCode = 0, - [Parameter()][string]$BootstrapNUnitResult = 'Passed', - [Parameter()][int]$InitializerExitCode = 0, - [Parameter()][int]$UnityExitCode = 0, - [Parameter()][string]$NUnitResult = 'Passed', - [Parameter()][int]$NUnitPassed = 1, - [Parameter()][int]$NUnitFailed = 0, - [Parameter()][string]$TestFilter = 'Harness', - [Parameter()][switch]$AllowObservationEvidence, - [Parameter()][ValidateSet('approved', 'existing-entry-content-mutation', 'unexpected-new-immutable-entry', 'removal')][string]$BootstrapTransition = 'approved', - [Parameter()][ValidateSet('none', 'receipt-meta-collision')][string]$BootstrapSemanticMutation = 'none', - [Parameter()][ValidateSet('approved', 'missing', 'extra', 'hash-mismatch')][string]$StagingReceiptTransition = 'approved', - [Parameter()][switch]$OmitStagedCanonicalConfig - ) - - $caseRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseHarness-' + [guid]::NewGuid().ToString('N')) - $consumerRoot = Join-Path $caseRoot 'ConsumerProject' - New-Item -ItemType Directory -Path $consumerRoot -Force | Out-Null - $settingsDirectory = Join-Path $consumerRoot 'ProjectSettings' - New-Item -ItemType Directory -Path $settingsDirectory -Force | Out-Null - $settingsPath = Join-Path $settingsDirectory 'jp.lilxyzw.shadercore.asset' - $receiptProbePath = Join-Path $settingsDirectory 'ProjectVersion.txt' - [System.IO.File]::WriteAllText($receiptProbePath, 'pre-bootstrap project version', (New-Object System.Text.UTF8Encoding($false))) - [System.IO.File]::WriteAllText((Join-Path $settingsDirectory 'QualitySettings.asset'), 'preexisting quality settings', (New-Object System.Text.UTF8Encoding($false))) - $manifestDirectory = Join-Path $consumerRoot 'Packages' - New-Item -ItemType Directory -Path $manifestDirectory -Force | Out-Null - [System.IO.File]::WriteAllText((Join-Path $manifestDirectory 'manifest.json'), '{"dependencies":{}}', (New-Object System.Text.UTF8Encoding($false))) - $receiptAssetPath = Join-Path $consumerRoot 'Assets\ReceiptAnchor.asset' - New-Item -ItemType Directory -Path (Split-Path -Parent $receiptAssetPath) -Force | Out-Null - [System.IO.File]::WriteAllText($receiptAssetPath, 'receipt asset', (New-Object System.Text.UTF8Encoding($false))) - [System.IO.File]::WriteAllText(($receiptAssetPath + '.meta'), "fileFormatVersion: 2`nguid: 11111111111111111111111111111111`nDefaultImporter:", (New-Object System.Text.UTF8Encoding($false))) - $zipPath = Join-Path $caseRoot 'release.zip' - $shaderCoreManifestPath = Join-Path $caseRoot 'shader-core.json' - [System.IO.File]::WriteAllText($zipPath, 'zip') - [System.IO.File]::WriteAllText($shaderCoreManifestPath, '{}') - $canonicalManifestSource = Join-Path (Split-Path -Parent $PSScriptRoot) 'Config\shader-core-test-hosts.json' - $canonicalManifestDestination = Join-Path $consumerRoot (Get-CanonicalShaderCoreConfigDestination).Replace('/', '\') - if (-not (Test-Path -LiteralPath $canonicalManifestSource -PathType Leaf)) { - throw "Harness canonical Shader-Core test-host manifest is missing: '$canonicalManifestSource'." - } - New-Item -ItemType Directory -Path (Split-Path -Parent $canonicalManifestDestination) -Force | Out-Null - Copy-Item -LiteralPath $canonicalManifestSource -Destination $canonicalManifestDestination -Force - $stagingReceipt = New-HarnessStagingReceipt -ConsumerRoot $consumerRoot - $canonicalReceiptEntry = @($stagingReceipt.entries | Where-Object { $_.destination -eq (Get-CanonicalShaderCoreConfigDestination) }) - if ($canonicalReceiptEntry.Count -ne 1) { - throw 'Harness canonical Shader-Core config receipt entry is missing.' - } - $canonicalReceiptEntry[0].sourceKind = 'workspace-canonical-shader-core-config' - $canonicalReceiptEntry[0].source = $canonicalManifestSource - $canonicalReceiptEntry[0].sha256 = Get-Sha256Hex -Path $canonicalManifestSource - switch ($StagingReceiptTransition) { - 'missing' { Remove-Item -LiteralPath $receiptProbePath -Force } - 'extra' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'extra.txt'), 'extra', (New-Object System.Text.UTF8Encoding($false))) } - 'hash-mismatch' { [System.IO.File]::WriteAllText($receiptProbePath, 'mismatched project version', (New-Object System.Text.UTF8Encoding($false))) } - } - $script:HarnessManifestHashes = $ManifestHashes - $script:HarnessManifestIndex = 0 - $script:HarnessBootstrapTransition = $BootstrapTransition - $script:HarnessBootstrapSemanticMutation = $BootstrapSemanticMutation - $script:HarnessBaselineMismatch = [bool]$BaselineMismatch - $script:HarnessEditorGuardFailure = [bool]$EditorGuardFailure - $script:HarnessResetCalls = 0 - $env:PUREBASE_HARNESS_BOOTSTRAP_EXIT = [string]$BootstrapExitCode - $env:PUREBASE_HARNESS_BOOTSTRAP_NUNIT_RESULT = $BootstrapNUnitResult - $env:PUREBASE_HARNESS_INITIALIZER_EXIT = [string]$InitializerExitCode - $env:PUREBASE_HARNESS_UNITY_EXIT = [string]$UnityExitCode - $env:PUREBASE_HARNESS_NUNIT_RESULT = $NUnitResult - $env:PUREBASE_HARNESS_NUNIT_PASSED = [string]$NUnitPassed - $env:PUREBASE_HARNESS_NUNIT_FAILED = [string]$NUnitFailed - $consumerTestContract = if ($null -eq $Contract) { [ordered]@{ runLabel = $Label; runKind = 'harness'; products = @() } } else { $Contract } - $failure = $null - try { - if ($OmitStagedCanonicalConfig) { - Remove-Item -LiteralPath $canonicalManifestDestination -Force - } - $null = Invoke-ConsumerBootstrapImport -UnityEditor $fakeUnityPath -ConsumerRoot $consumerRoot -RunRoot $caseRoot -ZipPath $zipPath -ShaderCoreManifestPath $shaderCoreManifestPath -StagingReceipt $stagingReceipt - $summary = Invoke-ConsumerTest -UnityEditor $fakeUnityPath -ConsumerRoot $consumerRoot -RunRoot $caseRoot -ZipPath $zipPath -ShaderCoreManifestPath $shaderCoreManifestPath -Contract $consumerTestContract -TestFilter $TestFilter -Selections $Selections -RequireColdLibraryReset:$RequireColdLibraryReset -SkipColdLibraryReset:$SkipColdLibraryReset -AllowObservationEvidence:$AllowObservationEvidence - } - catch { - $failure = $_ - $summary = $null - } - return [ordered]@{ root = $caseRoot; consumerRoot = $consumerRoot; bootstrapDirectory = (Join-Path $caseRoot 'bootstrap'); runDirectory = (Join-Path $caseRoot ('runs/' + $Label)); settingsPath = $settingsPath; failure = $failure; summary = $summary; resetCalls = $script:HarnessResetCalls } -} + [System.IO.File]::WriteAllText($fakeUnityPath, $fakeUnitySource, (New-Object System.Text.ASCIIEncoding)) -function Invoke-HarnessCleanupPath { - param( - [Parameter(Mandatory = $true)][bool]$ExecutionFailed, - [Parameter()][ValidateSet('none', 'active', 'query-error', 'missing-command-line')][string]$ProcessDiscoveryScenario = 'none' - ) - - $runRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseCleanup-' + [guid]::NewGuid().ToString('N')) - $consumerRoot = Join-Path $runRoot 'ConsumerProject' - $lockPath = Join-Path $consumerRoot 'Temp\UnityLockfile' - $editorInstancePath = Join-Path $consumerRoot 'Library\EditorInstance.json' - New-Item -ItemType Directory -Path (Split-Path -Parent $lockPath) -Force | Out-Null - New-Item -ItemType Directory -Path (Split-Path -Parent $editorInstancePath) -Force | Out-Null - [System.IO.File]::WriteAllText($lockPath, 'stale lock', (New-Object System.Text.UTF8Encoding($false))) - [System.IO.File]::WriteAllText($editorInstancePath, '{"stale":true}', (New-Object System.Text.UTF8Encoding($false))) - $consumerRemoved = 0 - $cleanupFailure = $null - $script:HarnessCimException = $null - $script:HarnessCimProcesses = switch ($ProcessDiscoveryScenario) { - 'active' { @([pscustomobject][ordered]@{ ProcessId = 4242; CommandLine = ('-projectPath "' + $consumerRoot + '"') }) } - 'missing-command-line' { @([pscustomobject][ordered]@{ ProcessId = 4243; CommandLine = $null }, [pscustomobject][ordered]@{ ProcessId = 4244; CommandLine = '' }) } - default { @() } - } - if ($ProcessDiscoveryScenario -eq 'query-error') { - $script:HarnessCimException = [System.InvalidOperationException]::new('Synthetic CIM access failure.') - } - $cleanupStatus = 'not-required' - $cleanupReason = 'Consumer project was not present for cleanup.' - $discovery = Get-ConsumerUnityProcess -ProjectRoot $consumerRoot - try { - $cleanupResult = Remove-ConsumerProject -ConsumerRoot $consumerRoot - $consumerRemoved++ - $cleanupStatus = $cleanupResult.cleanupStatus - $cleanupReason = $cleanupResult.cleanupReason - } - catch { - $cleanupFailure = $_ - $cleanupStatus = if ($_.Exception.Data.Contains('cleanupStatus')) { [string]$_.Exception.Data['cleanupStatus'] } else { 'failed' } - $cleanupReason = if ($_.Exception.Data.Contains('cleanupReason')) { [string]$_.Exception.Data['cleanupReason'] } else { $_.Exception.Message } - } - finally { - $script:HarnessCimProcesses = @() - $script:HarnessCimException = $null - } - $cleanupFailureMessage = if ($null -ne $cleanupFailure) { $cleanupFailure.Exception.Message } else { '' } - if ($null -ne $cleanupFailure) { - Write-ConsumerJsonArtifact -Path (Join-Path $runRoot 'cleanup-failure.json') -Value ([ordered]@{ cleanupStatus = $cleanupStatus; cleanupReason = $cleanupReason; cleanupFailure = $cleanupFailureMessage; executionFailure = if ($ExecutionFailed) { 'Synthetic Unity failure.' } else { '' } }) - } - Write-ReleaseCleanupSummary -RunRoot $runRoot -ConsumerCreated 1 -ConsumerRemoved $consumerRemoved -KeepConsumer $false -Failed ($ExecutionFailed -or ($null -ne $cleanupFailure)) -CleanupFailure $cleanupFailureMessage -CleanupStatus $cleanupStatus -CleanupReason $cleanupReason - $summary = Get-Content -LiteralPath (Join-Path $runRoot 'cleanup-summary.json') -Raw | ConvertFrom-Json - return [ordered]@{ root = $runRoot; consumerRoot = $consumerRoot; discovery = $discovery; failure = $cleanupFailure; summary = $summary } -} + function Invoke-HarnessCase { + param( + [Parameter(Mandatory = $true)][string]$Label, + [Parameter()]$Contract = $null, + [Parameter()][hashtable]$Selections = @{}, + [Parameter()][switch]$RequireColdLibraryReset, + [Parameter()][switch]$SkipColdLibraryReset, + [Parameter(Mandatory = $true)][string[]]$ManifestHashes, + [Parameter()][switch]$BaselineMismatch, + [Parameter()][switch]$EditorGuardFailure, + [Parameter()][int]$BootstrapExitCode = 0, + [Parameter()][string]$BootstrapNUnitResult = 'Passed', + [Parameter()][int]$InitializerExitCode = 0, + [Parameter()][int]$UnityExitCode = 0, + [Parameter()][string]$NUnitResult = 'Passed', + [Parameter()][int]$NUnitPassed = 1, + [Parameter()][int]$NUnitFailed = 0, + [Parameter()][string]$TestFilter = 'Harness', + [Parameter()][switch]$AllowObservationEvidence, + [Parameter()][ValidateSet('approved', 'existing-entry-content-mutation', 'unexpected-new-immutable-entry', 'removal')][string]$BootstrapTransition = 'approved', + [Parameter()][ValidateSet('none', 'receipt-meta-collision')][string]$BootstrapSemanticMutation = 'none', + [Parameter()][ValidateSet('approved', 'missing', 'extra', 'hash-mismatch')][string]$StagingReceiptTransition = 'approved', + [Parameter()][switch]$OmitStagedCanonicalConfig + ) -function New-HarnessStandardMorphContract { - param([Parameter(Mandatory = $true)][string]$RunLabel) + $caseRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseHarness-' + [guid]::NewGuid().ToString('N')) + $consumerRoot = Join-Path $caseRoot 'ConsumerProject' + New-Item -ItemType Directory -Path $consumerRoot -Force | Out-Null + $settingsDirectory = Join-Path $consumerRoot 'ProjectSettings' + New-Item -ItemType Directory -Path $settingsDirectory -Force | Out-Null + $settingsPath = Join-Path $settingsDirectory 'jp.lilxyzw.shadercore.asset' + $receiptProbePath = Join-Path $settingsDirectory 'ProjectVersion.txt' + [System.IO.File]::WriteAllText($receiptProbePath, 'pre-bootstrap project version', (New-Object System.Text.UTF8Encoding($false))) + [System.IO.File]::WriteAllText((Join-Path $settingsDirectory 'QualitySettings.asset'), 'preexisting quality settings', (New-Object System.Text.UTF8Encoding($false))) + $manifestDirectory = Join-Path $consumerRoot 'Packages' + New-Item -ItemType Directory -Path $manifestDirectory -Force | Out-Null + [System.IO.File]::WriteAllText((Join-Path $manifestDirectory 'manifest.json'), '{"dependencies":{}}', (New-Object System.Text.UTF8Encoding($false))) + $receiptAssetPath = Join-Path $consumerRoot 'Assets\ReceiptAnchor.asset' + New-Item -ItemType Directory -Path (Split-Path -Parent $receiptAssetPath) -Force | Out-Null + [System.IO.File]::WriteAllText($receiptAssetPath, 'receipt asset', (New-Object System.Text.UTF8Encoding($false))) + [System.IO.File]::WriteAllText(($receiptAssetPath + '.meta'), "fileFormatVersion: 2`nguid: 11111111111111111111111111111111`nDefaultImporter:", (New-Object System.Text.UTF8Encoding($false))) + $zipPath = Join-Path $caseRoot 'release.zip' + $shaderCoreManifestPath = Join-Path $caseRoot 'shader-core.json' + [System.IO.File]::WriteAllText($zipPath, 'zip') + [System.IO.File]::WriteAllText($shaderCoreManifestPath, '{}') + $canonicalManifestSource = Join-Path (Split-Path -Parent $PSScriptRoot) 'Config\shader-core-test-hosts.json' + $canonicalManifestDestination = Join-Path $consumerRoot (Get-CanonicalShaderCoreConfigDestination).Replace('/', '\') + if (-not (Test-Path -LiteralPath $canonicalManifestSource -PathType Leaf)) { + throw "Harness canonical Shader-Core test-host manifest is missing: '$canonicalManifestSource'." + } + New-Item -ItemType Directory -Path (Split-Path -Parent $canonicalManifestDestination) -Force | Out-Null + Copy-Item -LiteralPath $canonicalManifestSource -Destination $canonicalManifestDestination -Force + $stagingReceipt = New-HarnessStagingReceipt -ConsumerRoot $consumerRoot + $canonicalReceiptEntry = @($stagingReceipt.entries | Where-Object { $_.destination -eq (Get-CanonicalShaderCoreConfigDestination) }) + if ($canonicalReceiptEntry.Count -ne 1) { + throw 'Harness canonical Shader-Core config receipt entry is missing.' + } + $canonicalReceiptEntry[0].sourceKind = 'workspace-canonical-shader-core-config' + $canonicalReceiptEntry[0].source = $canonicalManifestSource + $canonicalReceiptEntry[0].sha256 = Get-Sha256Hex -Path $canonicalManifestSource + switch ($StagingReceiptTransition) { + 'missing' { Remove-Item -LiteralPath $receiptProbePath -Force } + 'extra' { [System.IO.File]::WriteAllText((Join-Path $consumerRoot 'extra.txt'), 'extra', (New-Object System.Text.UTF8Encoding($false))) } + 'hash-mismatch' { [System.IO.File]::WriteAllText($receiptProbePath, 'mismatched project version', (New-Object System.Text.UTF8Encoding($false))) } + } + $script:HarnessManifestHashes = $ManifestHashes + $script:HarnessManifestIndex = 0 + $script:HarnessBootstrapTransition = $BootstrapTransition + $script:HarnessBootstrapSemanticMutation = $BootstrapSemanticMutation + $script:HarnessBaselineMismatch = [bool]$BaselineMismatch + $script:HarnessEditorGuardFailure = [bool]$EditorGuardFailure + $script:HarnessResetCalls = 0 + $env:PUREBASE_HARNESS_BOOTSTRAP_EXIT = [string]$BootstrapExitCode + $env:PUREBASE_HARNESS_BOOTSTRAP_NUNIT_RESULT = $BootstrapNUnitResult + $env:PUREBASE_HARNESS_INITIALIZER_EXIT = [string]$InitializerExitCode + $env:PUREBASE_HARNESS_UNITY_EXIT = [string]$UnityExitCode + $env:PUREBASE_HARNESS_NUNIT_RESULT = $NUnitResult + $env:PUREBASE_HARNESS_NUNIT_PASSED = [string]$NUnitPassed + $env:PUREBASE_HARNESS_NUNIT_FAILED = [string]$NUnitFailed + $consumerTestContract = if ($null -eq $Contract) { [ordered]@{ runLabel = $Label; runKind = 'harness'; products = @() } } else { $Contract } + $failure = $null + try { + if ($OmitStagedCanonicalConfig) { + Remove-Item -LiteralPath $canonicalManifestDestination -Force + } + $null = Invoke-ConsumerBootstrapImport -UnityEditor $fakeUnityPath -ConsumerRoot $consumerRoot -RunRoot $caseRoot -ZipPath $zipPath -ShaderCoreManifestPath $shaderCoreManifestPath -StagingReceipt $stagingReceipt + $summary = Invoke-ConsumerTest -UnityEditor $fakeUnityPath -ConsumerRoot $consumerRoot -RunRoot $caseRoot -ZipPath $zipPath -ShaderCoreManifestPath $shaderCoreManifestPath -Contract $consumerTestContract -TestFilter $TestFilter -Selections $Selections -RequireColdLibraryReset:$RequireColdLibraryReset -SkipColdLibraryReset:$SkipColdLibraryReset -AllowObservationEvidence:$AllowObservationEvidence + } + catch { + $failure = $_ + $summary = $null + } + return [ordered]@{ root = $caseRoot; consumerRoot = $consumerRoot; bootstrapDirectory = (Join-Path $caseRoot 'bootstrap'); runDirectory = (Join-Path $caseRoot ('runs/' + $Label)); settingsPath = $settingsPath; failure = $failure; summary = $summary; resetCalls = $script:HarnessResetCalls } + } - $module = [ordered]@{ - label = 'standard-morph' - phase = 'morph' - uniqueId = 'jp.penguin.purebase.release.fixture.products.morph' - propertyName = '' - sentinel = 'PUREBASE_ALL_PRODUCT_PHASE_SENTINEL_MORPH' - } - $contract = New-PhaseContract -Module $module -SelectedProducts $ProductNames - $contract.runLabel = $RunLabel - return $contract -} + function Invoke-HarnessCleanupPath { + param( + [Parameter(Mandatory = $true)][bool]$ExecutionFailed, + [Parameter()][ValidateSet('none', 'active', 'query-error', 'missing-command-line')][string]$ProcessDiscoveryScenario = 'none' + ) -function New-HarnessGeneratedSource { - param( - [Parameter(Mandatory = $true)][int[]]$PassCounts, - [Parameter()][string]$Sentinel = '' - ) - - $source = New-Object System.Text.StringBuilder - for ($index = 0; $index -lt $ProductPasses.Count; $index++) { - [void]$source.Append('Name "' + $ProductPasses[$index] + '"' + "`n") - for ($count = 0; $count -lt $PassCounts[$index]; $count++) { - [void]$source.Append($Sentinel + "`n") + $runRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseCleanup-' + [guid]::NewGuid().ToString('N')) + $consumerRoot = Join-Path $runRoot 'ConsumerProject' + $lockPath = Join-Path $consumerRoot 'Temp\UnityLockfile' + $editorInstancePath = Join-Path $consumerRoot 'Library\EditorInstance.json' + New-Item -ItemType Directory -Path (Split-Path -Parent $lockPath) -Force | Out-Null + New-Item -ItemType Directory -Path (Split-Path -Parent $editorInstancePath) -Force | Out-Null + [System.IO.File]::WriteAllText($lockPath, 'stale lock', (New-Object System.Text.UTF8Encoding($false))) + [System.IO.File]::WriteAllText($editorInstancePath, '{"stale":true}', (New-Object System.Text.UTF8Encoding($false))) + $consumerRemoved = 0 + $cleanupFailure = $null + $script:HarnessCimException = $null + $script:HarnessCimProcesses = switch ($ProcessDiscoveryScenario) { + 'active' { @([pscustomobject][ordered]@{ ProcessId = 4242; CommandLine = ('-projectPath "' + $consumerRoot + '"') }) } + 'missing-command-line' { @([pscustomobject][ordered]@{ ProcessId = 4243; CommandLine = $null }, [pscustomobject][ordered]@{ ProcessId = 4244; CommandLine = '' }) } + default { @() } + } + if ($ProcessDiscoveryScenario -eq 'query-error') { + $script:HarnessCimException = [System.InvalidOperationException]::new('Synthetic CIM access failure.') + } + $cleanupStatus = 'not-required' + $cleanupReason = 'Consumer project was not present for cleanup.' + $discovery = Get-ConsumerUnityProcess -ProjectRoot $consumerRoot + try { + $cleanupResult = Remove-ConsumerProject -ConsumerRoot $consumerRoot + $consumerRemoved++ + $cleanupStatus = $cleanupResult.cleanupStatus + $cleanupReason = $cleanupResult.cleanupReason + } + catch { + $cleanupFailure = $_ + $cleanupStatus = if ($_.Exception.Data.Contains('cleanupStatus')) { [string]$_.Exception.Data['cleanupStatus'] } else { 'failed' } + $cleanupReason = if ($_.Exception.Data.Contains('cleanupReason')) { [string]$_.Exception.Data['cleanupReason'] } else { $_.Exception.Message } + } + finally { + $script:HarnessCimProcesses = @() + $script:HarnessCimException = $null + } + $cleanupFailureMessage = if ($null -ne $cleanupFailure) { $cleanupFailure.Exception.Message } else { '' } + if ($null -ne $cleanupFailure) { + Write-ConsumerJsonArtifact -Path (Join-Path $runRoot 'cleanup-failure.json') -Value ([ordered]@{ cleanupStatus = $cleanupStatus; cleanupReason = $cleanupReason; cleanupFailure = $cleanupFailureMessage; executionFailure = if ($ExecutionFailed) { 'Synthetic Unity failure.' } else { '' } }) + } + Write-ReleaseCleanupSummary -RunRoot $runRoot -ConsumerCreated 1 -ConsumerRemoved $consumerRemoved -KeepConsumer $false -Failed ($ExecutionFailed -or ($null -ne $cleanupFailure)) -CleanupFailure $cleanupFailureMessage -CleanupStatus $cleanupStatus -CleanupReason $cleanupReason + $summary = Get-Content -LiteralPath (Join-Path $runRoot 'cleanup-summary.json') -Raw | ConvertFrom-Json + return [ordered]@{ root = $runRoot; consumerRoot = $consumerRoot; discovery = $discovery; failure = $cleanupFailure; summary = $summary } } - } - return $source.ToString() -} -function Write-HarnessComparisonEvidence { - param( - [Parameter(Mandatory = $true)][string]$Root, - [Parameter(Mandatory = $true)]$WarmContract, - [Parameter(Mandatory = $true)]$ColdContract, - [Parameter(Mandatory = $true)][hashtable]$WarmCounts, - [Parameter(Mandatory = $true)][hashtable]$ColdCounts - ) - - $moduleFreeDirectory = Join-Path $Root 'runs/module-free-clean-import/consumer-evidence' - $warmDirectory = Join-Path $Root ('runs/' + $WarmContract.runLabel + '/consumer-evidence') - $coldDirectory = Join-Path $Root ('runs/' + $ColdContract.runLabel + '/consumer-evidence') - foreach ($directory in @($moduleFreeDirectory, $warmDirectory, $coldDirectory)) { - New-Item -ItemType Directory -Path $directory -Force | Out-Null - } + function New-HarnessStandardMorphContract { + param([Parameter(Mandatory = $true)][string]$RunLabel) - $products = @() - foreach ($productName in $ProductNames) { - $moduleFreeFileName = Get-ExpectedGeneratedSourceArtifactFileName -RunLabel 'module-free-clean-import' -ShaderName $productName - [System.IO.File]::WriteAllText((Join-Path $moduleFreeDirectory $moduleFreeFileName), (New-HarnessGeneratedSource -PassCounts @(0, 0, 0, 0)), (New-Object System.Text.UTF8Encoding($false))) - - $warmFileName = Get-ExpectedGeneratedSourceArtifactFileName -RunLabel $WarmContract.runLabel -ShaderName $productName - $warmPassCounts = [int[]]$WarmCounts[$productName] - [System.IO.File]::WriteAllText((Join-Path $warmDirectory $warmFileName), (New-HarnessGeneratedSource -PassCounts $warmPassCounts -Sentinel $WarmContract.selectedModule.sentinel), (New-Object System.Text.UTF8Encoding($false))) - $products += [ordered]@{ - shaderName = $productName - compiled = $true - supported = $true - generatedSourceArtifactFileName = $warmFileName - passCounts = @( - [ordered]@{ passName = 'ForwardBase'; selectedSentinelCount = $warmPassCounts[0] }, - [ordered]@{ passName = 'ForwardAdd'; selectedSentinelCount = $warmPassCounts[1] }, - [ordered]@{ passName = 'ShadowCaster'; selectedSentinelCount = $warmPassCounts[2] }, - [ordered]@{ passName = 'Meta'; selectedSentinelCount = $warmPassCounts[3] } - ) - inactiveSentinels = @($WarmContract.inactiveSentinels | ForEach-Object { [ordered]@{ sentinel = $_; occurrenceCount = 0 } }) + $module = [ordered]@{ + label = 'standard-morph' + phase = 'morph' + uniqueId = 'jp.penguin.purebase.release.fixture.products.morph' + propertyName = '' + sentinel = 'PUREBASE_ALL_PRODUCT_PHASE_SENTINEL_MORPH' + } + $contract = New-PhaseContract -Module $module -SelectedProducts $ProductNames + $contract.runLabel = $RunLabel + return $contract } - $coldFileName = Get-ExpectedGeneratedSourceArtifactFileName -RunLabel $ColdContract.runLabel -ShaderName $productName - [System.IO.File]::WriteAllText((Join-Path $coldDirectory $coldFileName), (New-HarnessGeneratedSource -PassCounts ([int[]]$ColdCounts[$productName]) -Sentinel $ColdContract.selectedModule.sentinel), (New-Object System.Text.UTF8Encoding($false))) - } - [ordered]@{ - schemaName = 'purebase-standard-morph-observation' - schemaVersion = 1 - runLabel = $WarmContract.runLabel - runKind = 'product-phase' - selectedModulePhase = 'morph' - selectedModuleUniqueId = $WarmContract.selectedModule.moduleUniqueId - selectedModuleSentinel = $WarmContract.selectedModule.sentinel - products = $products - } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $warmDirectory 'standard-morph-observation.json') -Encoding UTF8 -} + function New-HarnessGeneratedSource { + param( + [Parameter(Mandatory = $true)][int[]]$PassCounts, + [Parameter()][string]$Sentinel = '' + ) -function Invoke-HarnessCompletionPath { - param( - [Parameter(Mandatory = $true)][string]$ValidationScope, - [Parameter()][switch]$ModuleFreeOnly, - [Parameter()][switch]$ForceRunSummaryFailure - ) - - $runRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseCompletion-' + [guid]::NewGuid().ToString('N')) - New-Item -ItemType Directory -Path $runRoot -Force | Out-Null - $consumerCreated = 1 - $consumerRemoved = 1 - $failed = $true - $comparisonVerdict = $null - $failure = $null - $outcomes = @([ordered]@{ label = 'synthetic-success'; runDirectoryLabel = 'synthetic-success'; nunit = [ordered]@{ result = 'Passed' } }) - try { - if ($ForceRunSummaryFailure) { - New-Item -ItemType Directory -Path (Join-Path $runRoot 'run-summary.json') -Force | Out-Null + $source = New-Object System.Text.StringBuilder + for ($index = 0; $index -lt $ProductPasses.Count; $index++) { + [void]$source.Append('Name "' + $ProductPasses[$index] + '"' + "`n") + for ($count = 0; $count -lt $PassCounts[$index]; $count++) { + [void]$source.Append($Sentinel + "`n") + } + } + return $source.ToString() } - Write-ReleaseRunSummary -RunRoot $runRoot -ConsumerCreated $consumerCreated -ConsumerRemoved $consumerRemoved -ValidationScope $ValidationScope -ComparisonMode $false -ModuleFreeOnly ([bool]$ModuleFreeOnly) -Outcomes $outcomes -ComparisonVerdict $comparisonVerdict - $failed = $false - } - catch { - $failure = $_ - } - finally { - Write-ReleaseCleanupSummary -RunRoot $runRoot -ConsumerCreated $consumerCreated -ConsumerRemoved $consumerRemoved -KeepConsumer $false -Failed $failed - } - $summaryPath = Join-Path $runRoot 'run-summary.json' - $summary = if (Test-Path -LiteralPath $summaryPath -PathType Leaf) { Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json } else { $null } - $cleanup = Get-Content -LiteralPath (Join-Path $runRoot 'cleanup-summary.json') -Raw | ConvertFrom-Json - return [ordered]@{ root = $runRoot; failure = $failure; summary = $summary; cleanup = $cleanup } -} + function Write-HarnessComparisonEvidence { + param( + [Parameter(Mandatory = $true)][string]$Root, + [Parameter(Mandatory = $true)]$WarmContract, + [Parameter(Mandatory = $true)]$ColdContract, + [Parameter(Mandatory = $true)][hashtable]$WarmCounts, + [Parameter(Mandatory = $true)][hashtable]$ColdCounts + ) -try { - $conflictArtifactDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseConflict-' + [guid]::NewGuid().ToString('N')) - $conflictFailure = $null - try { - & $runnerPath -UnityEditorPath 'not-a-unity-editor.exe' -ArtifactDirectory $conflictArtifactDirectory -ModuleFreeOnly -CompareWarmAndColdStandardMorph - } - catch { - $conflictFailure = $_ - } - Assert-Harness -Condition ($null -ne $conflictFailure) -Message 'Incompatible runner switches unexpectedly passed.' - Assert-Harness -Condition ($conflictFailure.Exception.Message -eq '-ModuleFreeOnly cannot be combined with -CompareWarmAndColdStandardMorph because the latter requires the five-row standard-morph comparison: module-free import, rendering-mode contract, module-free Toon runtime observation, warm, and cold.') -Message 'Incompatible runner switches did not report the deterministic conflict error before Unity validation.' - Assert-Harness -Condition (-not (Test-Path -LiteralPath $conflictArtifactDirectory)) -Message 'Incompatible runner switches created an artifact directory before failing.' - - $toonBaseConflictArtifactDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseToonBaseConflict-' + [guid]::NewGuid().ToString('N')) - $toonBaseConflictFailure = $null - try { - & $runnerPath -UnityEditorPath 'not-a-unity-editor.exe' -ArtifactDirectory $toonBaseConflictArtifactDirectory -ToonBaseOnly -ModuleFreeOnly - } - catch { - $toonBaseConflictFailure = $_ - } - Assert-Harness -Condition ($null -ne $toonBaseConflictFailure) -Message 'Incompatible Toon-base runner switches unexpectedly passed.' - Assert-Harness -Condition ($toonBaseConflictFailure.Exception.Message -eq '-ToonBaseOnly cannot be combined with -ModuleFreeOnly because it requires the Toon base product-phase row.') -Message 'Incompatible Toon-base runner switches did not report the deterministic conflict error before Unity validation.' - Assert-Harness -Condition (-not (Test-Path -LiteralPath $toonBaseConflictArtifactDirectory)) -Message 'Incompatible Toon-base runner switches created an artifact directory before failing.' - - $selectionMatrix = @( - [ordered]@{ label = 'module-free-clean-import' }, - [ordered]@{ label = 'unlit-forward-add-fog' }, - [ordered]@{ label = 'progressive-cpu-bake' } - ) - $fogOnlyMatrix = Select-ValidationMatrix -Matrix $selectionMatrix -FogOnly - Assert-Harness -Condition ($fogOnlyMatrix.Count -eq 1 -and $fogOnlyMatrix[0].label -eq 'unlit-forward-add-fog') -Message 'Fog-only matrix selection no longer returns exactly the unlit-forward-add-fog row.' - $bakeOnlyMatrix = Select-ValidationMatrix -Matrix $selectionMatrix -BakeOnly - Assert-Harness -Condition ($bakeOnlyMatrix.Count -eq 1 -and $bakeOnlyMatrix[0].label -eq 'progressive-cpu-bake') -Message 'Bake-only matrix selection did not return exactly the progressive-cpu-bake row.' - - $initialMatrix = New-InitialValidationMatrix - Assert-Harness -Condition ($initialMatrix.Count -eq 3 -and [string]::Join('|', @($initialMatrix | ForEach-Object { [string]$_.label })) -eq 'module-free-clean-import|rendering-mode-contract|module-free-toon-runtime-observation') -Message 'The initial validation matrix must order module-free import, rendering-mode contract, and module-free Toon runtime observation rows.' - $moduleFreeEntry = $initialMatrix[0] - Assert-Harness -Condition ($moduleFreeEntry.contract.runLabel -eq 'module-free-clean-import' -and $moduleFreeEntry.contract.runKind -eq 'module-free' -and -not $moduleFreeEntry.contract.hasSelectedModule -and $null -eq $moduleFreeEntry.contract.selectedModule -and @($moduleFreeEntry.contract.runtimeSamples).Count -eq 0) -Message 'The existing module-free clean-import contract changed while adding the Toon runtime observation.' - Assert-Harness -Condition ($moduleFreeEntry.filter -eq 'PureBase.Release.Consumer.Tests.PureBaseConsumerModuleFreeImportTests.ModuleFreeProductsCompileWithConfiguredPassPropertyAndSourceContracts' -and @($moduleFreeEntry.selections.Keys).Count -eq 0 -and -not $moduleFreeEntry.skipColdLibraryReset) -Message 'The existing module-free clean-import matrix row changed while adding the Toon runtime observation.' - $renderingModeEntry = $initialMatrix[1] - Assert-Harness -Condition ($renderingModeEntry.contract.runLabel -eq 'module-free-clean-import' -and $renderingModeEntry.contract.runKind -eq 'module-free' -and -not $renderingModeEntry.contract.hasSelectedModule -and $null -eq $renderingModeEntry.contract.selectedModule -and @($renderingModeEntry.contract.runtimeSamples).Count -eq 0) -Message 'The rendering-mode contract must retain the module-free import contract.' - Assert-Harness -Condition ($renderingModeEntry.filter -eq 'PureBase.Release.Consumer.Tests.PureBaseConsumerRenderingModeTests.ColdImportedPublicNormalizerMatchesTheFourByThreeStateTable' -and @($renderingModeEntry.selections.Keys).Count -eq 0 -and -not $renderingModeEntry.skipColdLibraryReset) -Message 'The rendering-mode contract row did not use the deterministic cold module-free test configuration.' - $moduleFreeToonRuntimeEntry = $initialMatrix[2] - $moduleFreeToonRuntimeContract = $moduleFreeToonRuntimeEntry.contract - $moduleFreeToonRuntimeSample = $moduleFreeToonRuntimeContract.runtimeSamples[0] - Assert-Harness -Condition ($moduleFreeToonRuntimeEntry.filter -eq 'PureBase.Release.Consumer.Tests.PureBaseConsumerRuntimeTests.ConfiguredRuntimeSamplesProduceExpectedBirpReadbacks' -and @($moduleFreeToonRuntimeEntry.selections.Keys).Count -eq 0 -and -not $moduleFreeToonRuntimeEntry.skipColdLibraryReset) -Message 'The module-free Toon runtime observation row did not use the deterministic cold runtime test configuration.' - Assert-Harness -Condition ($moduleFreeToonRuntimeContract.runLabel -eq 'module-free-toon-runtime-observation' -and $moduleFreeToonRuntimeContract.runKind -eq 'module-free-toon-runtime-observation' -and -not $moduleFreeToonRuntimeContract.hasSelectedModule -and $null -eq $moduleFreeToonRuntimeContract.selectedModule) -Message 'The module-free Toon runtime observation contract unexpectedly selected a module.' - Assert-Harness -Condition (@($moduleFreeToonRuntimeContract.runtimeSamples).Count -eq 1 -and $moduleFreeToonRuntimeSample.label -eq 'module-free-toon-center-pixel' -and $moduleFreeToonRuntimeSample.shaderName -eq 'PureBase/Toon' -and $moduleFreeToonRuntimeSample.shaderAssetPath -eq 'Packages/jp.penguin.purebase/Shaders/PureBaseToon.scshader' -and $moduleFreeToonRuntimeSample.includePointLight) -Message 'The module-free Toon runtime observation contract did not configure exactly one Toon readback sample.' - foreach ($channel in @($moduleFreeToonRuntimeSample.red, $moduleFreeToonRuntimeSample.green, $moduleFreeToonRuntimeSample.blue, $moduleFreeToonRuntimeSample.alpha)) { - Assert-Harness -Condition ([double]::IsFinite($channel.minimum) -and [double]::IsFinite($channel.maximum) -and $channel.minimum -lt $channel.maximum) -Message 'The module-free Toon runtime observation contract did not use finite, non-empty readback ranges.' - } - Assert-Harness -Condition ($moduleFreeToonRuntimeSample.red.minimum -eq 0.0 -and $moduleFreeToonRuntimeSample.red.maximum -eq 1000.0 -and $moduleFreeToonRuntimeSample.green.minimum -eq 0.0 -and $moduleFreeToonRuntimeSample.green.maximum -eq 1000.0 -and $moduleFreeToonRuntimeSample.blue.minimum -eq 0.0 -and $moduleFreeToonRuntimeSample.blue.maximum -eq 1000.0 -and $moduleFreeToonRuntimeSample.alpha.minimum -eq 0.99 -and $moduleFreeToonRuntimeSample.alpha.maximum -eq 1.01) -Message 'The module-free Toon runtime observation ranges changed from their finite structural baseline.' - $moduleFreeOnlyInitialMatrix = New-InitialValidationMatrix -ModuleFreeOnly - Assert-Harness -Condition ($moduleFreeOnlyInitialMatrix.Count -eq 2 -and [string]::Join('|', @($moduleFreeOnlyInitialMatrix | ForEach-Object { [string]$_.label })) -eq 'module-free-clean-import|rendering-mode-contract') -Message 'Module-free-only validation must select module-free import and rendering-mode contract rows in order.' - $comparisonMatrix = New-InitialValidationMatrix - $comparisonContracts = Add-StandardMorphComparisonMatrixRows -Matrix $comparisonMatrix - $comparisonLabels = @($comparisonMatrix | ForEach-Object { [string]$_.label }) - Assert-Harness -Condition ($comparisonMatrix.Count -eq 5 -and [string]::Join('|', $comparisonLabels) -eq 'module-free-clean-import|rendering-mode-contract|module-free-toon-runtime-observation|standard-morph-warm-library-duplicate-evidence|standard-morph-cold-library-legacy-counts' -and $comparisonContracts.warmContract.runLabel -eq $comparisonLabels[3] -and $comparisonContracts.coldContract.runLabel -eq $comparisonLabels[4]) -Message 'Standard-morph comparison matrix must retain module-free import, rendering-mode contract, and Toon runtime observation rows before the warm and cold rows.' - Assert-Harness -Condition ($moduleFreeToonRuntimeEntry.requiresColdLibraryReset) -Message 'The module-free Toon runtime observation row did not explicitly require a cold Library reset.' - $moduleFreeToonRuntimeCase = Invoke-HarnessCase -Label $moduleFreeToonRuntimeEntry.label -Contract $moduleFreeToonRuntimeContract -Selections $moduleFreeToonRuntimeEntry.selections -RequireColdLibraryReset:$moduleFreeToonRuntimeEntry.requiresColdLibraryReset -SkipColdLibraryReset:$moduleFreeToonRuntimeEntry.skipColdLibraryReset -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row', 'row') -TestFilter $moduleFreeToonRuntimeEntry.filter - Assert-Harness -Condition ($null -eq $moduleFreeToonRuntimeCase.failure) -Message 'Module-free Toon runtime observation harness case unexpectedly failed.' - $moduleFreeToonRuntimeResetEvidence = Get-Content -LiteralPath (Join-Path $moduleFreeToonRuntimeCase.runDirectory 'library-reset.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition ($moduleFreeToonRuntimeResetEvidence.required -and $moduleFreeToonRuntimeResetEvidence.attempted -and $moduleFreeToonRuntimeResetEvidence.completed) -Message 'Module-free Toon runtime observation did not persist required, attempted, and completed cold Library reset evidence.' - - foreach ($conflict in @( - [ordered]@{ label = 'module-free'; parameters = @{ ModuleFreeOnly = $true }; message = '-BakeOnly cannot be combined with -ModuleFreeOnly because it requires the progressive-cpu-bake row.' }, - [ordered]@{ label = 'toon-base'; parameters = @{ ToonBaseOnly = $true }; message = '-BakeOnly cannot be combined with -ToonBaseOnly because it requires the progressive-cpu-bake row.' }, - [ordered]@{ label = 'fog'; parameters = @{ FogOnly = $true }; message = '-BakeOnly cannot be combined with -FogOnly because it requires the progressive-cpu-bake row.' }, - [ordered]@{ label = 'warm-cold-comparison'; parameters = @{ CompareWarmAndColdStandardMorph = $true }; message = '-BakeOnly cannot be combined with -CompareWarmAndColdStandardMorph because it requires the progressive-cpu-bake row.' } - )) { - $conflictArtifactDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseBakeConflict-' + $conflict.label + '-' + [guid]::NewGuid().ToString('N')) - $runnerParameters = @{ - UnityEditorPath = 'not-a-unity-editor.exe' - ArtifactDirectory = $conflictArtifactDirectory - BakeOnly = $true - } - foreach ($parameterName in $conflict.parameters.Keys) { - $runnerParameters[$parameterName] = $conflict.parameters[$parameterName] - } - $conflictFailure = $null - try { - & $runnerPath @runnerParameters - } - catch { - $conflictFailure = $_ - } - Assert-Harness -Condition ($null -ne $conflictFailure) -Message "Incompatible Bake-only '$($conflict.label)' runner switches unexpectedly passed." - Assert-Harness -Condition ($conflictFailure.Exception.Message -eq $conflict.message) -Message "Incompatible Bake-only '$($conflict.label)' runner switches did not report the deterministic conflict error before Unity validation." - Assert-Harness -Condition (-not (Test-Path -LiteralPath $conflictArtifactDirectory)) -Message "Incompatible Bake-only '$($conflict.label)' runner switches created an artifact directory before failing." - } + $moduleFreeDirectory = Join-Path $Root 'runs/module-free-clean-import/consumer-evidence' + $warmDirectory = Join-Path $Root ('runs/' + $WarmContract.runLabel + '/consumer-evidence') + $coldDirectory = Join-Path $Root ('runs/' + $ColdContract.runLabel + '/consumer-evidence') + foreach ($directory in @($moduleFreeDirectory, $warmDirectory, $coldDirectory)) { + New-Item -ItemType Directory -Path $directory -Force | Out-Null + } - $toonPropertyMappings = @( - [ordered]@{ phase = 'base'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.base'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_base_ProductPhaseValue' }, - [ordered]@{ phase = 'light'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.light'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_light_ProductPhaseValue' }, - [ordered]@{ phase = 'modifylight'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.modifylight'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_modifylight_ProductPhaseValue' }, - [ordered]@{ phase = 'shade'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.shade'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_shade_ProductPhaseValue' } - ) - foreach ($mapping in $toonPropertyMappings) { - $propertyName = Get-ShaderCoreNamespacedPropertyName -ModuleUniqueId $mapping.uniqueId -RawPropertyName '_ProductPhaseValue' - Assert-Harness -Condition ($propertyName -eq $mapping.expectedPropertyName) -Message "Toon '$($mapping.phase)' property ABI mapping changed." - Assert-Harness -Condition ($propertyName -ne '_ProductPhaseValue') -Message "Toon '$($mapping.phase)' contract regressed to the raw property name." - $module = [ordered]@{ label = 'harness-toon-' + $mapping.phase; phase = $mapping.phase; uniqueId = $mapping.uniqueId; propertyName = $propertyName; sentinel = 'PUREBASE_TOON_PRODUCT_PHASE_SENTINEL_' + $mapping.phase.ToUpperInvariant() } - $contract = New-PhaseContract -Module $module -SelectedProducts @('PureBase/Toon') - Assert-Harness -Condition ($contract.selectedModule.propertyName -eq $mapping.expectedPropertyName) -Message "Toon '$($mapping.phase)' phase contract did not retain the visible property ABI." - } - $fogContract = New-FogContract - $fogAssignmentPropertyName = $fogContract.unlitForwardAddFog.floatAssignments[0].propertyName - Assert-Harness -Condition ($fogAssignmentPropertyName -eq '_jp_penguin_purebase_release_fixture_unlit_forwardaddfog_ForwardAddFogSignalProperty') -Message 'Fog contract did not map its float assignment to the expected namespaced property ABI.' - Assert-Harness -Condition ($fogAssignmentPropertyName -ne '_ForwardAddFogSignalProperty') -Message 'Fog contract regressed to the raw property name.' - $toonBaseModule = [ordered]@{ label = 'toon-base'; phase = 'base'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.base'; propertyName = '_jp_penguin_purebase_release_fixture_toon_phase_base_ProductPhaseValue'; sentinel = 'PUREBASE_TOON_PRODUCT_PHASE_SENTINEL_BASE' } - $toonBaseRuntimeContract = New-ToonRuntimeContract -Module $toonBaseModule - $toonBaseRuntimeSample = $toonBaseRuntimeContract.runtimeSamples[0] - $toonBaseRuntimeDelta = $toonBaseRuntimeContract.runtimeDelta.selectedMinusModuleFree - $toonBaseModuleFreeReference = $toonBaseRuntimeContract.runtimeDelta.moduleFreeReference - Assert-Harness -Condition ($toonBaseRuntimeSample.red.minimum -eq 3.59 -and $toonBaseRuntimeSample.red.maximum -eq 3.61) -Message 'Toon base runtime absolute red range must remain the evidence-backed 3.59-3.61 interval.' - Assert-Harness -Condition ($toonBaseRuntimeDelta.red.minimum -eq 0.70 -and $toonBaseRuntimeDelta.red.maximum -eq 0.73) -Message 'Toon base selected-minus-module-free red range must remain the evidence-backed 0.70-0.73 interval.' - Assert-Harness -Condition ($toonBaseModuleFreeReference.red -eq 2.87890625 -and $toonBaseModuleFreeReference.green -eq 2.837890625 -and $toonBaseModuleFreeReference.blue -eq 2.72265625 -and $toonBaseModuleFreeReference.alpha -eq 1.0) -Message 'Toon base module-free reference must remain the recorded BIRP readback until the observation is evaluated.' - Assert-Harness -Condition ($toonBaseRuntimeSample.red.minimum -le 3.599609375 -and $toonBaseRuntimeSample.red.maximum -ge 3.599609375) -Message 'Toon base runtime absolute red range excludes the recorded BIRP readback.' - Assert-Harness -Condition ($toonBaseRuntimeDelta.red.minimum -le 0.712890625 -and $toonBaseRuntimeDelta.red.maximum -ge 0.712890625) -Message 'Toon base selected-minus-module-free red range excludes the recorded BIRP delta.' - Assert-Harness -Condition ($toonBaseRuntimeSample.red.maximum -lt 4.2 -and $toonBaseRuntimeDelta.red.maximum -lt 1.3) -Message 'Toon base runtime contract regressed to the direct-add red expectation.' - foreach ($invalidAbiInput in @( - [ordered]@{ uniqueId = ''; propertyName = '_ProductPhaseValue' }, - [ordered]@{ uniqueId = 'jp..penguin'; propertyName = '_ProductPhaseValue' }, - [ordered]@{ uniqueId = 'jp.penguin'; propertyName = 'ProductPhaseValue' } - )) { - $invalidAbiFailure = $null - try { - Get-ShaderCoreNamespacedPropertyName -ModuleUniqueId $invalidAbiInput.uniqueId -RawPropertyName $invalidAbiInput.propertyName | Out-Null - } - catch { - $invalidAbiFailure = $_ + $products = @() + foreach ($productName in $ProductNames) { + $moduleFreeFileName = Get-ExpectedGeneratedSourceArtifactFileName -RunLabel 'module-free-clean-import' -ShaderName $productName + [System.IO.File]::WriteAllText((Join-Path $moduleFreeDirectory $moduleFreeFileName), (New-HarnessGeneratedSource -PassCounts @(0, 0, 0, 0)), (New-Object System.Text.UTF8Encoding($false))) + + $warmFileName = Get-ExpectedGeneratedSourceArtifactFileName -RunLabel $WarmContract.runLabel -ShaderName $productName + $warmPassCounts = [int[]]$WarmCounts[$productName] + [System.IO.File]::WriteAllText((Join-Path $warmDirectory $warmFileName), (New-HarnessGeneratedSource -PassCounts $warmPassCounts -Sentinel $WarmContract.selectedModule.sentinel), (New-Object System.Text.UTF8Encoding($false))) + $products += [ordered]@{ + shaderName = $productName + compiled = $true + supported = $true + generatedSourceArtifactFileName = $warmFileName + passCounts = @( + [ordered]@{ passName = 'ForwardBase'; selectedSentinelCount = $warmPassCounts[0] }, + [ordered]@{ passName = 'ForwardAdd'; selectedSentinelCount = $warmPassCounts[1] }, + [ordered]@{ passName = 'ShadowCaster'; selectedSentinelCount = $warmPassCounts[2] }, + [ordered]@{ passName = 'Meta'; selectedSentinelCount = $warmPassCounts[3] } + ) + inactiveSentinels = @($WarmContract.inactiveSentinels | ForEach-Object { [ordered]@{ sentinel = $_; occurrenceCount = 0 } }) + } + + $coldFileName = Get-ExpectedGeneratedSourceArtifactFileName -RunLabel $ColdContract.runLabel -ShaderName $productName + [System.IO.File]::WriteAllText((Join-Path $coldDirectory $coldFileName), (New-HarnessGeneratedSource -PassCounts ([int[]]$ColdCounts[$productName]) -Sentinel $ColdContract.selectedModule.sentinel), (New-Object System.Text.UTF8Encoding($false))) + } + [ordered]@{ + schemaName = 'purebase-standard-morph-observation' + schemaVersion = 1 + runLabel = $WarmContract.runLabel + runKind = 'product-phase' + selectedModulePhase = 'morph' + selectedModuleUniqueId = $WarmContract.selectedModule.moduleUniqueId + selectedModuleSentinel = $WarmContract.selectedModule.sentinel + products = $products + } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $warmDirectory 'standard-morph-observation.json') -Encoding UTF8 } - Assert-Harness -Condition ($null -ne $invalidAbiFailure) -Message 'Malformed Shader-Core property ABI input unexpectedly passed.' - } - foreach ($completionCase in @( - [ordered]@{ validationScope = 'full-release-validation-matrix'; moduleFreeOnly = $false }, - [ordered]@{ validationScope = 'module-free-diagnostic-only'; moduleFreeOnly = $true }, - [ordered]@{ validationScope = 'progressive-cpu-bake-diagnostic-only'; moduleFreeOnly = $false } - )) { - $completion = Invoke-HarnessCompletionPath -ValidationScope $completionCase.validationScope -ModuleFreeOnly:$completionCase.moduleFreeOnly - Assert-Harness -Condition ($null -eq $completion.failure) -Message "Non-comparison completion '$($completionCase.validationScope)' unexpectedly failed under StrictMode." - Assert-Harness -Condition ($null -ne $completion.summary) -Message "Non-comparison completion '$($completionCase.validationScope)' did not persist run-summary.json." - Assert-Harness -Condition ($completion.summary.validationScope -eq $completionCase.validationScope) -Message "Non-comparison completion '$($completionCase.validationScope)' changed validationScope." - Assert-Harness -Condition ($completion.summary.outcomes[0].runDirectoryLabel -eq 'synthetic-success') -Message "Non-comparison completion '$($completionCase.validationScope)' did not preserve the run directory label." - Assert-Harness -Condition ($completion.summary.consumerDirectoryCreationCount -eq 1 -and $completion.summary.consumerDirectoryRemovalCount -eq 1) -Message "Non-comparison completion '$($completionCase.validationScope)' changed consumer lifecycle counts." - Assert-Harness -Condition (-not $completion.summary.comparisonMode -and ([bool]$completion.summary.moduleFreeOnly -eq [bool]$completionCase.moduleFreeOnly)) -Message "Non-comparison completion '$($completionCase.validationScope)' changed comparison flags." - Assert-Harness -Condition ($null -eq $completion.summary.comparisonVerdict) -Message "Non-comparison completion '$($completionCase.validationScope)' claimed a comparison verdict." - Assert-Harness -Condition ($completion.cleanup.consumerDirectoryCreationCount -eq 1 -and $completion.cleanup.consumerDirectoryRemovalCount -eq 1) -Message "Non-comparison cleanup '$($completionCase.validationScope)' changed consumer lifecycle counts." - Assert-Harness -Condition (-not $completion.cleanup.failed) -Message "Non-comparison completion '$($completionCase.validationScope)' wrote an incorrect cleanup failure state." - } + function Invoke-HarnessCompletionPath { + param( + [Parameter(Mandatory = $true)][string]$ValidationScope, + [Parameter()][switch]$ModuleFreeOnly, + [Parameter()][switch]$ForceRunSummaryFailure + ) - $runSummaryFailure = Invoke-HarnessCompletionPath -ValidationScope 'full-release-validation-matrix' -ForceRunSummaryFailure - Assert-Harness -Condition ($null -ne $runSummaryFailure.failure) -Message 'Synthetic run-summary failure unexpectedly passed.' - Assert-Harness -Condition ($runSummaryFailure.cleanup.failed) -Message 'Run-summary failure wrote cleanup-summary.json with failed=false.' - - $staleLockCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true - Assert-Harness -Condition ($null -eq $staleLockCleanup.failure -and -not (Test-Path -LiteralPath $staleLockCleanup.consumerRoot)) -Message 'Stale Unity lock state prevented consumer cleanup after a failed Unity execution without an active process.' - Assert-Harness -Condition ($staleLockCleanup.summary.consumerDirectoryCreationCount -eq 1 -and $staleLockCleanup.summary.consumerDirectoryRemovalCount -eq 1 -and $staleLockCleanup.summary.cleanupStatus -eq 'removed' -and -not $staleLockCleanup.summary.consumerDirectoryRemovalFailed -and $staleLockCleanup.summary.failed) -Message 'Stale-lock cleanup summary did not accurately record creation, removal, and execution failure.' - - $activeProcessCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true -ProcessDiscoveryScenario active - Assert-Harness -Condition ($activeProcessCleanup.discovery.status -eq 'active' -and $activeProcessCleanup.discovery.process.ProcessId -eq 4242 -and $null -ne $activeProcessCleanup.failure -and (Test-Path -LiteralPath $activeProcessCleanup.consumerRoot)) -Message 'Active Unity process did not block consumer cleanup.' - Assert-Harness -Condition ($activeProcessCleanup.summary.consumerDirectoryRemovalCount -eq 0 -and $activeProcessCleanup.summary.cleanupStatus -eq 'active' -and $activeProcessCleanup.summary.consumerDirectoryRemovalFailed -and $activeProcessCleanup.summary.failed) -Message 'Active-process cleanup summary did not record the refused deletion.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $activeProcessCleanup.root 'cleanup-failure.json') -PathType Leaf) -Message 'Active-process cleanup did not persist cleanup failure evidence.' - $activeCleanupEvidence = Get-Content -LiteralPath (Join-Path $activeProcessCleanup.root 'cleanup-failure.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition ($activeCleanupEvidence.executionFailure -eq 'Synthetic Unity failure.' -and $activeCleanupEvidence.cleanupFailure -match 'Unity process 4242') -Message 'Active-process cleanup evidence did not retain the execution and cleanup failures.' - - $cimFailureCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true -ProcessDiscoveryScenario query-error - Assert-Harness -Condition ($cimFailureCleanup.discovery.status -eq 'indeterminate' -and $cimFailureCleanup.discovery.reason -match 'Synthetic CIM access failure' -and $null -ne $cimFailureCleanup.failure -and (Test-Path -LiteralPath $cimFailureCleanup.consumerRoot)) -Message 'Get-CimInstance failure did not return indeterminate or block consumer cleanup.' - Assert-Harness -Condition ($cimFailureCleanup.summary.consumerDirectoryRemovalCount -eq 0 -and $cimFailureCleanup.summary.cleanupStatus -eq 'indeterminate' -and $cimFailureCleanup.summary.cleanupReason -match 'Synthetic CIM access failure' -and $cimFailureCleanup.summary.consumerDirectoryRemovalFailed -and $cimFailureCleanup.summary.failed) -Message 'Get-CimInstance failure cleanup summary did not record the refused deletion and reason.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $cimFailureCleanup.root 'cleanup-failure.json') -PathType Leaf) -Message 'Get-CimInstance failure did not persist cleanup failure evidence.' - $cimFailureCleanupEvidence = Get-Content -LiteralPath (Join-Path $cimFailureCleanup.root 'cleanup-failure.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition ($cimFailureCleanupEvidence.cleanupStatus -eq 'indeterminate' -and $cimFailureCleanupEvidence.cleanupReason -match 'Synthetic CIM access failure' -and $cimFailureCleanupEvidence.cleanupFailure -match 'Cannot verify whether a Unity process') -Message 'Get-CimInstance failure evidence did not retain the discovery failure.' - - $missingCommandLineCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true -ProcessDiscoveryScenario missing-command-line - Assert-Harness -Condition ($missingCommandLineCleanup.discovery.status -eq 'indeterminate' -and $missingCommandLineCleanup.discovery.reason -match 'Cannot inspect CommandLine.*4243.*4244' -and $null -ne $missingCommandLineCleanup.failure -and (Test-Path -LiteralPath $missingCommandLineCleanup.consumerRoot)) -Message 'Null or empty Unity CommandLine candidates did not return indeterminate or block consumer cleanup.' - Assert-Harness -Condition ($missingCommandLineCleanup.summary.consumerDirectoryRemovalCount -eq 0 -and $missingCommandLineCleanup.summary.cleanupStatus -eq 'indeterminate' -and $missingCommandLineCleanup.summary.cleanupReason -match 'Cannot inspect CommandLine.*4243.*4244' -and $missingCommandLineCleanup.summary.consumerDirectoryRemovalFailed -and $missingCommandLineCleanup.summary.failed) -Message 'Missing CommandLine cleanup summary did not record the refused deletion and reason.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $missingCommandLineCleanup.root 'cleanup-failure.json') -PathType Leaf) -Message 'Missing CommandLine cleanup did not persist cleanup failure evidence.' - $missingCommandLineCleanupEvidence = Get-Content -LiteralPath (Join-Path $missingCommandLineCleanup.root 'cleanup-failure.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition ($missingCommandLineCleanupEvidence.cleanupStatus -eq 'indeterminate' -and $missingCommandLineCleanupEvidence.cleanupReason -match 'Cannot inspect CommandLine.*4243.*4244' -and $missingCommandLineCleanupEvidence.cleanupFailure -match 'Cannot verify whether a Unity process') -Message 'Missing CommandLine cleanup evidence did not retain the discovery failure.' - - $expectedFirstBootstrapAddedCount = @(Get-ExpectedFirstBootstrapAddedPaths).Count - $expectedFirstBootstrapChangedCount = @(Get-ExpectedFirstBootstrapChangedPaths).Count - $expectedFirstBootstrapAcceptedCount = $expectedFirstBootstrapAddedCount + $expectedFirstBootstrapChangedCount - Assert-Harness -Condition ($expectedFirstBootstrapAddedCount -eq 25 -and $expectedFirstBootstrapChangedCount -eq 2 -and $expectedFirstBootstrapAcceptedCount -eq 27) -Message 'First-bootstrap expected transition counts do not match the hosted consumer contract.' - - foreach ($successfulLabel in @('module-free-clean-import', 'progressive-cpu-bake')) { - $case = Invoke-HarnessCase -Label $successfulLabel -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') - Assert-Harness -Condition ($null -eq $case.failure) -Message "Successful row '$successfulLabel' unexpectedly failed." - Assert-Harness -Condition ($case.resetCalls -eq 1) -Message "Successful row '$successfulLabel' did not reset only the bootstrap Library." - $resetEvidence = Get-Content -LiteralPath (Join-Path $case.runDirectory 'library-reset.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition (-not $resetEvidence.required -and -not $resetEvidence.attempted -and -not $resetEvidence.completed) -Message "Successful row '$successfulLabel' unexpectedly attempted a selected-module cold Library reset." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'staging-receipt.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its staging receipt." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-pre-bootstrap.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not collect the pre-bootstrap immutable manifest." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-after-scene-bootstrap.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not preserve the pre-initialization scene-bootstrap observation." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-quiescent.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not collect the canonical post-bootstrap manifest." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its bootstrap delta report." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'semantic-transition-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its first-bootstrap semantic report." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-command.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its Shader-Core initialization command." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its Shader-Core initialization report." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-config.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its Shader-Core initialization config receipt." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-test-hosts.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not preserve its staged Shader-Core config." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/shader-core-state-initialization-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its second Shader-Core initialization report." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-after-library-reset.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not compare immutable inputs after its bootstrap Library reset." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/fixed-point-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist second-bootstrap fixed-point evidence." - $preBootstrapManifest = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-pre-bootstrap.json') -Raw | ConvertFrom-Json - $bootstrapManifest = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-quiescent.json') -Raw | ConvertFrom-Json - $afterResetManifest = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-after-library-reset.json') -Raw | ConvertFrom-Json - $rowManifest = Get-Content -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-before.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition ($bootstrapManifest.rootSha256 -eq 'bootstrap' -and $rowManifest.rootSha256 -eq 'row') -Message "Successful row '$successfulLabel' did not collect its row baseline after bootstrap." - $bootstrapCommand = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'unity-command.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition ($bootstrapCommand.arguments -contains 'PureBase.Release.Consumer.Tests.PureBaseConsumerSceneTemplateBootstrapTests.DisposableSceneLifecycleMaterializesSceneTemplateSettings') -Message "Successful row '$successfulLabel' did not select the scene-template bootstrap test." - $bootstrapSceneTemplateEntry = @($bootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) - $preBootstrapSceneTemplateEntry = @($preBootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) - $afterResetSceneTemplateEntry = @($afterResetManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) - $bootstrapQualitySettingsEntry = @($bootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/QualitySettings.asset' }) - $preBootstrapQualitySettingsEntry = @($preBootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/QualitySettings.asset' }) - $bootstrapDelta = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -Raw | ConvertFrom-Json - $semanticTransition = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'semantic-transition-report.json') -Raw | ConvertFrom-Json - $initializationCommand = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-command.json') -Raw | ConvertFrom-Json - $initializationReport = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-report.json') -Raw | ConvertFrom-Json - $initializationConfig = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-config.json') -Raw | ConvertFrom-Json - $stagingReceipt = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'staging-receipt.json') -Raw | ConvertFrom-Json - $secondInitializationReport = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/shader-core-state-initialization-report.json') -Raw | ConvertFrom-Json - $fixedPoint = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/fixed-point-report.json') -Raw | ConvertFrom-Json - Assert-Harness -Condition ($preBootstrapSceneTemplateEntry.Count -eq 0 -and $bootstrapSceneTemplateEntry.Count -eq 1 -and $afterResetSceneTemplateEntry.Count -eq 1 -and $afterResetSceneTemplateEntry[0].sha256 -eq $bootstrapSceneTemplateEntry[0].sha256) -Message "Successful row '$successfulLabel' did not preserve the materialized SceneTemplateSettings entry across the Library reset." - Assert-Harness -Condition ($preBootstrapQualitySettingsEntry.Count -eq 1 -and $bootstrapQualitySettingsEntry.Count -eq 1 -and $preBootstrapQualitySettingsEntry[0].sha256 -eq $bootstrapQualitySettingsEntry[0].sha256) -Message "Successful row '$successfulLabel' did not preserve the preexisting QualitySettings scaffold input." - Assert-Harness -Condition ($bootstrapDelta.classification -eq 'observed' -and @($bootstrapDelta.added).Count -eq $expectedFirstBootstrapAddedCount -and @($bootstrapDelta.changed).Count -eq $expectedFirstBootstrapChangedCount -and @($bootstrapDelta.removed).Count -eq 0) -Message "Successful row '$successfulLabel' did not report the observed first-bootstrap delta." - Assert-Harness -Condition ($semanticTransition.verdict -eq 'accepted' -and $semanticTransition.summary.accepted -eq $expectedFirstBootstrapAcceptedCount -and $semanticTransition.summary.rejected -eq 0 -and $semanticTransition.summary.unclassified -eq 0) -Message "Successful row '$successfulLabel' did not accept the exact first-bootstrap semantic transition." - $canonicalConfigDestination = Get-CanonicalShaderCoreConfigDestination - $canonicalReceiptEntry = @($stagingReceipt.entries | Where-Object { $_.destination -eq $canonicalConfigDestination }) - Assert-Harness -Condition ($initializationCommand.arguments -contains '-executeMethod' -and $initializationCommand.arguments -contains 'PureBase.Release.Consumer.Tests.PureBaseConsumerShaderCoreInitializer.InitializeForBatchMode') -Message "Successful row '$successfulLabel' did not execute the consumer-owned Shader-Core initializer." - Assert-Harness -Condition ($initializationConfig.destination -eq $canonicalConfigDestination -and $initializationConfig.expectedSourceKind -eq 'workspace-canonical-shader-core-config' -and $initializationConfig.receiptEntryCount -eq 1) -Message "Successful row '$successfulLabel' did not record its canonical Shader-Core config receipt." - Assert-Harness -Condition ($canonicalReceiptEntry.Count -eq 1 -and $canonicalReceiptEntry[0].sourceKind -eq 'workspace-canonical-shader-core-config' -and $canonicalReceiptEntry[0].sha256 -eq (Get-Sha256Hex -Path (Join-Path $case.consumerRoot $canonicalConfigDestination.Replace('/', '\')))) -Message "Successful row '$successfulLabel' did not stage the canonical Shader-Core config at its receipt-owned destination." - Assert-Harness -Condition ($initializationReport.schemaName -eq 'purebase-shader-core-bootstrap-initialization' -and $initializationReport.phase -eq 'first-bootstrap' -and $initializationReport.canonicalConfigDestination -eq $canonicalConfigDestination -and $initializationReport.rowCount -eq 15) -Message "Successful row '$successfulLabel' did not report the canonical first Shader-Core initialization mapping." - Assert-Harness -Condition (@($initializationReport.mapping.modules.'PureBase/Tests/ShaderCore/ModuleOrder').Count -eq 2 -and $initializationReport.mapping.modules.'PureBase/Tests/ShaderCore/ModuleOrder'[0] -eq 'jp.penguin.purebase.tests.shadercore.moduleorder.zeta' -and $initializationReport.mapping.modules.'PureBase/Tests/ShaderCore/ModuleOrder'[1] -eq 'jp.penguin.purebase.tests.shadercore.moduleorder.alpha') -Message "Successful row '$successfulLabel' did not preserve the canonical Shader-Core ModuleOrder mapping." - Assert-Harness -Condition ($secondInitializationReport.phase -eq 'second-bootstrap' -and $secondInitializationReport.rowCount -eq 15) -Message "Successful row '$successfulLabel' did not revalidate the canonical Shader-Core state after the Library reset." - Assert-Harness -Condition ($fixedPoint.rootsEqual -and @($fixedPoint.added).Count -eq 0 -and @($fixedPoint.changed).Count -eq 0 -and @($fixedPoint.removed).Count -eq 0) -Message "Successful row '$successfulLabel' did not reach the second-bootstrap immutable fixed point." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-before.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its before manifest." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-after.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its after manifest." - } + $runRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseCompletion-' + [guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $runRoot -Force | Out-Null + $consumerCreated = 1 + $consumerRemoved = 1 + $failed = $true + $comparisonVerdict = $null + $failure = $null + $outcomes = @([ordered]@{ label = 'synthetic-success'; runDirectoryLabel = 'synthetic-success'; nunit = [ordered]@{ result = 'Passed' } }) + try { + if ($ForceRunSummaryFailure) { + New-Item -ItemType Directory -Path (Join-Path $runRoot 'run-summary.json') -Force | Out-Null + } + Write-ReleaseRunSummary -RunRoot $runRoot -ConsumerCreated $consumerCreated -ConsumerRemoved $consumerRemoved -ValidationScope $ValidationScope -ComparisonMode $false -ModuleFreeOnly ([bool]$ModuleFreeOnly) -Outcomes $outcomes -ComparisonVerdict $comparisonVerdict + $failed = $false + } + catch { + $failure = $_ + } + finally { + Write-ReleaseCleanupSummary -RunRoot $runRoot -ConsumerCreated $consumerCreated -ConsumerRemoved $consumerRemoved -KeepConsumer $false -Failed $failed + } - $deltaPreBootstrap = [ordered]@{ - rootSha256 = 'pre-root' - entries = @( - [ordered]@{ path = 'Assets/Changed.asset'; sha256 = 'before-change' }, - [ordered]@{ path = 'Assets/Removed.asset'; sha256 = 'removed-hash' } - ) - } - $deltaPostBootstrap = [ordered]@{ - rootSha256 = 'post-root' - entries = @( - [ordered]@{ path = 'Assets/Added.asset'; sha256 = 'added-hash' }, - [ordered]@{ path = 'Assets/Changed.asset'; sha256 = 'after-change' } - ) - } - $deltaReport = Get-ConsumerImmutableManifestDeltaReport -PreBootstrap $deltaPreBootstrap -PostBootstrap $deltaPostBootstrap - $deltaReportRepeat = Get-ConsumerImmutableManifestDeltaReport -PreBootstrap $deltaPreBootstrap -PostBootstrap $deltaPostBootstrap - $deltaReportArtifact = $deltaReport | ConvertTo-Json -Depth 8 | ConvertFrom-Json - Assert-ExactJsonPropertyNames -Value $deltaReportArtifact -ExpectedNames @('schemaName', 'schemaVersion', 'classification', 'pathOrdering', 'preBootstrapRootSha256', 'postBootstrapRootSha256', 'added', 'removed', 'changed') -Description 'Bootstrap delta report' - Assert-Harness -Condition ($deltaReport.schemaName -eq 'purebase-immutable-manifest-bootstrap-delta' -and $deltaReport.schemaVersion -eq 1 -and $deltaReport.classification -eq 'observed' -and $deltaReport.pathOrdering -eq 'System.StringComparer.Ordinal' -and $deltaReport.preBootstrapRootSha256 -eq 'pre-root' -and $deltaReport.postBootstrapRootSha256 -eq 'post-root') -Message 'Bootstrap delta report schema changed.' - Assert-Harness -Condition (@($deltaReport.added).Count -eq 1 -and $deltaReport.added[0].path -eq 'Assets/Added.asset' -and $deltaReport.added[0].sha256 -eq 'added-hash') -Message 'Bootstrap delta report omitted the added path hash.' - Assert-Harness -Condition (@($deltaReport.removed).Count -eq 1 -and $deltaReport.removed[0].path -eq 'Assets/Removed.asset' -and $deltaReport.removed[0].sha256 -eq 'removed-hash') -Message 'Bootstrap delta report omitted the removed path hash.' - Assert-Harness -Condition (@($deltaReport.changed).Count -eq 1 -and $deltaReport.changed[0].path -eq 'Assets/Changed.asset' -and $deltaReport.changed[0].preBootstrapSha256 -eq 'before-change' -and $deltaReport.changed[0].postBootstrapSha256 -eq 'after-change') -Message 'Bootstrap delta report omitted the changed path hashes.' - Assert-Harness -Condition (($deltaReport | ConvertTo-Json -Depth 8 -Compress) -eq ($deltaReportRepeat | ConvertTo-Json -Depth 8 -Compress)) -Message 'Bootstrap delta report ordering is not deterministic.' - - $receiptSourceRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReceiptSources-' + [guid]::NewGuid().ToString('N')) - try { - $receiptScaffoldRoot = Join-Path $receiptSourceRoot 'ConsumerProject' - $receiptShaderCoreRoot = Join-Path $receiptSourceRoot 'ShaderCore' - $receiptModulesRoot = Join-Path $receiptSourceRoot 'Modules' - $receiptFixturesRoot = Join-Path $receiptSourceRoot 'Fixtures' - $receiptZipSourceRoot = Join-Path $receiptSourceRoot 'ReleasePackage' - $receiptCanonicalConfigPath = Join-Path $receiptSourceRoot 'Canonical/shader-core-test-hosts.json' - foreach ($sourceFile in @( - [ordered]@{ root = $receiptScaffoldRoot; relativePath = 'ProjectSettings/ProjectVersion.txt'; content = 'scaffold' }, - [ordered]@{ root = $receiptShaderCoreRoot; relativePath = 'package.json'; content = 'shader-core' }, - [ordered]@{ root = $receiptModulesRoot; relativePath = 'RootModule/module.scmodule'; content = 'module' }, - [ordered]@{ root = $receiptFixturesRoot; relativePath = 'TestFixture.mat'; content = 'fixture' }, - [ordered]@{ root = $receiptZipSourceRoot; relativePath = 'package.json'; content = 'release-package' } - )) { - $sourcePath = Join-Path $sourceFile.root $sourceFile.relativePath - New-Item -ItemType Directory -Path (Split-Path -Parent $sourcePath) -Force | Out-Null - [System.IO.File]::WriteAllText($sourcePath, $sourceFile.content, (New-Object System.Text.UTF8Encoding($false))) - } - New-Item -ItemType Directory -Path (Split-Path -Parent $receiptCanonicalConfigPath) -Force | Out-Null - [System.IO.File]::WriteAllText($receiptCanonicalConfigPath, '{"schemaVersion":1,"hosts":[]}', (New-Object System.Text.UTF8Encoding($false))) - - $receiptZipPath = Join-Path $receiptSourceRoot 'release.zip' - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::CreateFromDirectory($receiptZipSourceRoot, $receiptZipPath) - $actualReceipt = Get-ConsumerStagingReceipt -ZipPath $receiptZipPath -ScaffoldRoot $receiptScaffoldRoot -ShaderCoreRoot $receiptShaderCoreRoot -ModulesRoot $receiptModulesRoot -FixturesRoot $receiptFixturesRoot -CanonicalShaderCoreConfigPath $receiptCanonicalConfigPath - $expectedReceiptEntries = @( - [ordered]@{ destination = 'ProjectSettings/ProjectVersion.txt'; sourceKind = 'consumer-scaffold' }, - [ordered]@{ destination = '_LocalPackages/jp.lilxyzw.shadercore/package.json'; sourceKind = 'shader-core-tree' }, - [ordered]@{ destination = 'Assets/ReleaseModules/RootModule/module.scmodule'; sourceKind = 'release-modules' }, - [ordered]@{ destination = 'Assets/ReleaseConsumer/Fixtures/TestFixture.mat'; sourceKind = 'release-fixtures' }, - [ordered]@{ destination = 'Assets/ReleaseConsumer/Fixtures/ShaderCore/shader-core-test-hosts.json'; sourceKind = 'workspace-canonical-shader-core-config' }, - [ordered]@{ destination = '_LocalPackages/jp.penguin.purebase/package.json'; sourceKind = 'release-zip-entry' } - ) - Assert-Harness -Condition (@($actualReceipt.entries).Count -eq $expectedReceiptEntries.Count) -Message 'Actual staging receipt did not contain every source tree entry.' - foreach ($expectedReceiptEntry in $expectedReceiptEntries) { - $actualReceiptEntry = @($actualReceipt.entries | Where-Object { $_.destination -eq $expectedReceiptEntry.destination }) - Assert-Harness -Condition ($actualReceiptEntry.Count -eq 1 -and $actualReceiptEntry[0].sourceKind -eq $expectedReceiptEntry.sourceKind) -Message "Actual staging receipt did not preserve '$($expectedReceiptEntry.destination)' from '$($expectedReceiptEntry.sourceKind)'." + $summaryPath = Join-Path $runRoot 'run-summary.json' + $summary = if (Test-Path -LiteralPath $summaryPath -PathType Leaf) { Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json } else { $null } + $cleanup = Get-Content -LiteralPath (Join-Path $runRoot 'cleanup-summary.json') -Raw | ConvertFrom-Json + return [ordered]@{ root = $runRoot; failure = $failure; summary = $summary; cleanup = $cleanup } } - $canonicalReceiptEntry = @($actualReceipt.entries | Where-Object { $_.destination -eq (Get-CanonicalShaderCoreConfigDestination) }) - Assert-Harness -Condition ($canonicalReceiptEntry.Count -eq 1 -and $canonicalReceiptEntry[0].source -eq $receiptCanonicalConfigPath -and $canonicalReceiptEntry[0].sha256 -eq (Get-Sha256Hex -Path $receiptCanonicalConfigPath)) -Message 'Actual staging receipt did not preserve the canonical config source and hash.' - Assert-Harness -Condition (@($actualReceipt.entries | Where-Object { $_.destination -like 'consumer-scaffold/*' }).Count -eq 0) -Message 'Actual staging receipt prefixed the consumer scaffold root unexpectedly.' - $nullPrefixFailure = $null try { - Add-ConsumerStagingReceiptTreeEntries -EntriesByDestination @{} -SourceRoot $receiptScaffoldRoot -DestinationPrefix $null -SourceKind 'consumer-scaffold' - } - catch { - $nullPrefixFailure = $_ - } - Assert-Harness -Condition ($null -ne $nullPrefixFailure) -Message 'Staging receipt destination prefix accepted null.' - } - finally { - Remove-Item -LiteralPath $receiptSourceRoot -Recurse -Force -ErrorAction SilentlyContinue - } + $conflictArtifactDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseConflict-' + [guid]::NewGuid().ToString('N')) + $conflictFailure = $null + try { + & $runnerPath -UnityEditorPath 'not-a-unity-editor.exe' -ArtifactDirectory $conflictArtifactDirectory -ModuleFreeOnly -CompareWarmAndColdStandardMorph + } + catch { + $conflictFailure = $_ + } + Assert-Harness -Condition ($null -ne $conflictFailure) -Message 'Incompatible runner switches unexpectedly passed.' + Assert-Harness -Condition ($conflictFailure.Exception.Message -eq '-ModuleFreeOnly cannot be combined with -CompareWarmAndColdStandardMorph because the latter requires the five-row standard-morph comparison: module-free import, rendering-mode contract, module-free Toon runtime observation, warm, and cold.') -Message 'Incompatible runner switches did not report the deterministic conflict error before Unity validation.' + Assert-Harness -Condition (-not (Test-Path -LiteralPath $conflictArtifactDirectory)) -Message 'Incompatible runner switches created an artifact directory before failing.' + + $toonBaseConflictArtifactDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseToonBaseConflict-' + [guid]::NewGuid().ToString('N')) + $toonBaseConflictFailure = $null + try { + & $runnerPath -UnityEditorPath 'not-a-unity-editor.exe' -ArtifactDirectory $toonBaseConflictArtifactDirectory -ToonBaseOnly -ModuleFreeOnly + } + catch { + $toonBaseConflictFailure = $_ + } + Assert-Harness -Condition ($null -ne $toonBaseConflictFailure) -Message 'Incompatible Toon-base runner switches unexpectedly passed.' + Assert-Harness -Condition ($toonBaseConflictFailure.Exception.Message -eq '-ToonBaseOnly cannot be combined with -ModuleFreeOnly because it requires the Toon base product-phase row.') -Message 'Incompatible Toon-base runner switches did not report the deterministic conflict error before Unity validation.' + Assert-Harness -Condition (-not (Test-Path -LiteralPath $toonBaseConflictArtifactDirectory)) -Message 'Incompatible Toon-base runner switches created an artifact directory before failing.' + + $selectionMatrix = @( + [ordered]@{ label = 'module-free-clean-import' }, + [ordered]@{ label = 'unlit-forward-add-fog' }, + [ordered]@{ label = 'progressive-cpu-bake' } + ) + $fogOnlyMatrix = Select-ValidationMatrix -Matrix $selectionMatrix -FogOnly + Assert-Harness -Condition ($fogOnlyMatrix.Count -eq 1 -and $fogOnlyMatrix[0].label -eq 'unlit-forward-add-fog') -Message 'Fog-only matrix selection no longer returns exactly the unlit-forward-add-fog row.' + $bakeOnlyMatrix = Select-ValidationMatrix -Matrix $selectionMatrix -BakeOnly + Assert-Harness -Condition ($bakeOnlyMatrix.Count -eq 1 -and $bakeOnlyMatrix[0].label -eq 'progressive-cpu-bake') -Message 'Bake-only matrix selection did not return exactly the progressive-cpu-bake row.' + + $initialMatrix = New-InitialValidationMatrix + Assert-Harness -Condition ($initialMatrix.Count -eq 3 -and [string]::Join('|', @($initialMatrix | ForEach-Object { [string]$_.label })) -eq 'module-free-clean-import|rendering-mode-contract|module-free-toon-runtime-observation') -Message 'The initial validation matrix must order module-free import, rendering-mode contract, and module-free Toon runtime observation rows.' + $moduleFreeEntry = $initialMatrix[0] + Assert-Harness -Condition ($moduleFreeEntry.contract.runLabel -eq 'module-free-clean-import' -and $moduleFreeEntry.contract.runKind -eq 'module-free' -and -not $moduleFreeEntry.contract.hasSelectedModule -and $null -eq $moduleFreeEntry.contract.selectedModule -and @($moduleFreeEntry.contract.runtimeSamples).Count -eq 0) -Message 'The existing module-free clean-import contract changed while adding the Toon runtime observation.' + Assert-Harness -Condition ($moduleFreeEntry.filter -eq 'PureBase.Release.Consumer.Tests.PureBaseConsumerModuleFreeImportTests.ModuleFreeProductsCompileWithConfiguredPassPropertyAndSourceContracts' -and @($moduleFreeEntry.selections.Keys).Count -eq 0 -and -not $moduleFreeEntry.skipColdLibraryReset) -Message 'The existing module-free clean-import matrix row changed while adding the Toon runtime observation.' + $renderingModeEntry = $initialMatrix[1] + Assert-Harness -Condition ($renderingModeEntry.contract.runLabel -eq 'module-free-clean-import' -and $renderingModeEntry.contract.runKind -eq 'module-free' -and -not $renderingModeEntry.contract.hasSelectedModule -and $null -eq $renderingModeEntry.contract.selectedModule -and @($renderingModeEntry.contract.runtimeSamples).Count -eq 0) -Message 'The rendering-mode contract must retain the module-free import contract.' + Assert-Harness -Condition ($renderingModeEntry.filter -eq 'PureBase.Release.Consumer.Tests.PureBaseConsumerRenderingModeTests.ColdImportedPublicNormalizerMatchesTheFourByThreeStateTable' -and @($renderingModeEntry.selections.Keys).Count -eq 0 -and -not $renderingModeEntry.skipColdLibraryReset) -Message 'The rendering-mode contract row did not use the deterministic cold module-free test configuration.' + $moduleFreeToonRuntimeEntry = $initialMatrix[2] + $moduleFreeToonRuntimeContract = $moduleFreeToonRuntimeEntry.contract + $moduleFreeToonRuntimeSample = $moduleFreeToonRuntimeContract.runtimeSamples[0] + Assert-Harness -Condition ($moduleFreeToonRuntimeEntry.filter -eq 'PureBase.Release.Consumer.Tests.PureBaseConsumerRuntimeTests.ConfiguredRuntimeSamplesProduceExpectedBirpReadbacks' -and @($moduleFreeToonRuntimeEntry.selections.Keys).Count -eq 0 -and -not $moduleFreeToonRuntimeEntry.skipColdLibraryReset) -Message 'The module-free Toon runtime observation row did not use the deterministic cold runtime test configuration.' + Assert-Harness -Condition ($moduleFreeToonRuntimeContract.runLabel -eq 'module-free-toon-runtime-observation' -and $moduleFreeToonRuntimeContract.runKind -eq 'module-free-toon-runtime-observation' -and -not $moduleFreeToonRuntimeContract.hasSelectedModule -and $null -eq $moduleFreeToonRuntimeContract.selectedModule) -Message 'The module-free Toon runtime observation contract unexpectedly selected a module.' + Assert-Harness -Condition (@($moduleFreeToonRuntimeContract.runtimeSamples).Count -eq 1 -and $moduleFreeToonRuntimeSample.label -eq 'module-free-toon-center-pixel' -and $moduleFreeToonRuntimeSample.shaderName -eq 'PureBase/Toon' -and $moduleFreeToonRuntimeSample.shaderAssetPath -eq 'Packages/jp.penguin.purebase/Shaders/PureBaseToon.scshader' -and $moduleFreeToonRuntimeSample.includePointLight) -Message 'The module-free Toon runtime observation contract did not configure exactly one Toon readback sample.' + foreach ($channel in @($moduleFreeToonRuntimeSample.red, $moduleFreeToonRuntimeSample.green, $moduleFreeToonRuntimeSample.blue, $moduleFreeToonRuntimeSample.alpha)) { + Assert-Harness -Condition ([double]::IsFinite($channel.minimum) -and [double]::IsFinite($channel.maximum) -and $channel.minimum -lt $channel.maximum) -Message 'The module-free Toon runtime observation contract did not use finite, non-empty readback ranges.' + } + Assert-Harness -Condition ($moduleFreeToonRuntimeSample.red.minimum -eq 0.0 -and $moduleFreeToonRuntimeSample.red.maximum -eq 1000.0 -and $moduleFreeToonRuntimeSample.green.minimum -eq 0.0 -and $moduleFreeToonRuntimeSample.green.maximum -eq 1000.0 -and $moduleFreeToonRuntimeSample.blue.minimum -eq 0.0 -and $moduleFreeToonRuntimeSample.blue.maximum -eq 1000.0 -and $moduleFreeToonRuntimeSample.alpha.minimum -eq 0.99 -and $moduleFreeToonRuntimeSample.alpha.maximum -eq 1.01) -Message 'The module-free Toon runtime observation ranges changed from their finite structural baseline.' + $moduleFreeOnlyInitialMatrix = New-InitialValidationMatrix -ModuleFreeOnly + Assert-Harness -Condition ($moduleFreeOnlyInitialMatrix.Count -eq 2 -and [string]::Join('|', @($moduleFreeOnlyInitialMatrix | ForEach-Object { [string]$_.label })) -eq 'module-free-clean-import|rendering-mode-contract') -Message 'Module-free-only validation must select module-free import and rendering-mode contract rows in order.' + $comparisonMatrix = New-InitialValidationMatrix + $comparisonContracts = Add-StandardMorphComparisonMatrixRows -Matrix $comparisonMatrix + $comparisonLabels = @($comparisonMatrix | ForEach-Object { [string]$_.label }) + Assert-Harness -Condition ($comparisonMatrix.Count -eq 5 -and [string]::Join('|', $comparisonLabels) -eq 'module-free-clean-import|rendering-mode-contract|module-free-toon-runtime-observation|standard-morph-warm-library-duplicate-evidence|standard-morph-cold-library-legacy-counts' -and $comparisonContracts.warmContract.runLabel -eq $comparisonLabels[3] -and $comparisonContracts.coldContract.runLabel -eq $comparisonLabels[4]) -Message 'Standard-morph comparison matrix must retain module-free import, rendering-mode contract, and Toon runtime observation rows before the warm and cold rows.' + Assert-Harness -Condition ($moduleFreeToonRuntimeEntry.requiresColdLibraryReset) -Message 'The module-free Toon runtime observation row did not explicitly require a cold Library reset.' + $moduleFreeToonRuntimeCase = Invoke-HarnessCase -Label $moduleFreeToonRuntimeEntry.label -Contract $moduleFreeToonRuntimeContract -Selections $moduleFreeToonRuntimeEntry.selections -RequireColdLibraryReset:$moduleFreeToonRuntimeEntry.requiresColdLibraryReset -SkipColdLibraryReset:$moduleFreeToonRuntimeEntry.skipColdLibraryReset -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row', 'row') -TestFilter $moduleFreeToonRuntimeEntry.filter + Assert-Harness -Condition ($null -eq $moduleFreeToonRuntimeCase.failure) -Message 'Module-free Toon runtime observation harness case unexpectedly failed.' + $moduleFreeToonRuntimeResetEvidence = Get-Content -LiteralPath (Join-Path $moduleFreeToonRuntimeCase.runDirectory 'library-reset.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition ($moduleFreeToonRuntimeResetEvidence.required -and $moduleFreeToonRuntimeResetEvidence.attempted -and $moduleFreeToonRuntimeResetEvidence.completed) -Message 'Module-free Toon runtime observation did not persist required, attempted, and completed cold Library reset evidence.' + + foreach ($conflict in @( + [ordered]@{ label = 'module-free'; parameters = @{ ModuleFreeOnly = $true }; message = '-BakeOnly cannot be combined with -ModuleFreeOnly because it requires the progressive-cpu-bake row.' }, + [ordered]@{ label = 'toon-base'; parameters = @{ ToonBaseOnly = $true }; message = '-BakeOnly cannot be combined with -ToonBaseOnly because it requires the progressive-cpu-bake row.' }, + [ordered]@{ label = 'fog'; parameters = @{ FogOnly = $true }; message = '-BakeOnly cannot be combined with -FogOnly because it requires the progressive-cpu-bake row.' }, + [ordered]@{ label = 'warm-cold-comparison'; parameters = @{ CompareWarmAndColdStandardMorph = $true }; message = '-BakeOnly cannot be combined with -CompareWarmAndColdStandardMorph because it requires the progressive-cpu-bake row.' } + )) { + $conflictArtifactDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReleaseBakeConflict-' + $conflict.label + '-' + [guid]::NewGuid().ToString('N')) + $runnerParameters = @{ + UnityEditorPath = 'not-a-unity-editor.exe' + ArtifactDirectory = $conflictArtifactDirectory + BakeOnly = $true + } + foreach ($parameterName in $conflict.parameters.Keys) { + $runnerParameters[$parameterName] = $conflict.parameters[$parameterName] + } + $conflictFailure = $null + try { + & $runnerPath @runnerParameters + } + catch { + $conflictFailure = $_ + } + Assert-Harness -Condition ($null -ne $conflictFailure) -Message "Incompatible Bake-only '$($conflict.label)' runner switches unexpectedly passed." + Assert-Harness -Condition ($conflictFailure.Exception.Message -eq $conflict.message) -Message "Incompatible Bake-only '$($conflict.label)' runner switches did not report the deterministic conflict error before Unity validation." + Assert-Harness -Condition (-not (Test-Path -LiteralPath $conflictArtifactDirectory)) -Message "Incompatible Bake-only '$($conflict.label)' runner switches created an artifact directory before failing." + } - foreach ($receiptCase in @( - [ordered]@{ label = 'missing'; expectedMessage = 'Staged consumer destination is missing' }, - [ordered]@{ label = 'extra'; expectedMessage = 'Staged consumer destination is extra' }, - [ordered]@{ label = 'hash-mismatch'; expectedMessage = 'Staged consumer destination content mismatches' } - )) { - $case = Invoke-HarnessCase -Label ('staging-receipt-' + $receiptCase.label) -ManifestHashes @('bootstrap') -StagingReceiptTransition $receiptCase.label - Assert-Harness -Condition ($null -ne $case.failure -and $case.failure.Exception.Message -match $receiptCase.expectedMessage) -Message "Staging receipt '$($receiptCase.label)' unexpectedly passed or reported the wrong failure." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'staging-receipt.json') -PathType Leaf) -Message "Staging receipt '$($receiptCase.label)' did not persist its receipt." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'failure-evidence/staging-receipt.json') -PathType Leaf) -Message "Staging receipt '$($receiptCase.label)' did not retain receipt failure evidence." - Assert-Harness -Condition (-not (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'unity-command.json') -PathType Leaf)) -Message "Staging receipt '$($receiptCase.label)' launched Unity before rejecting the staged consumer." - } + $toonPropertyMappings = @( + [ordered]@{ phase = 'base'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.base'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_base_ProductPhaseValue' }, + [ordered]@{ phase = 'light'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.light'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_light_ProductPhaseValue' }, + [ordered]@{ phase = 'modifylight'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.modifylight'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_modifylight_ProductPhaseValue' }, + [ordered]@{ phase = 'shade'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.shade'; expectedPropertyName = '_jp_penguin_purebase_release_fixture_toon_phase_shade_ProductPhaseValue' } + ) + foreach ($mapping in $toonPropertyMappings) { + $propertyName = Get-ShaderCoreNamespacedPropertyName -ModuleUniqueId $mapping.uniqueId -RawPropertyName '_ProductPhaseValue' + Assert-Harness -Condition ($propertyName -eq $mapping.expectedPropertyName) -Message "Toon '$($mapping.phase)' property ABI mapping changed." + Assert-Harness -Condition ($propertyName -ne '_ProductPhaseValue') -Message "Toon '$($mapping.phase)' contract regressed to the raw property name." + $module = [ordered]@{ label = 'harness-toon-' + $mapping.phase; phase = $mapping.phase; uniqueId = $mapping.uniqueId; propertyName = $propertyName; sentinel = 'PUREBASE_TOON_PRODUCT_PHASE_SENTINEL_' + $mapping.phase.ToUpperInvariant() } + $contract = New-PhaseContract -Module $module -SelectedProducts @('PureBase/Toon') + Assert-Harness -Condition ($contract.selectedModule.propertyName -eq $mapping.expectedPropertyName) -Message "Toon '$($mapping.phase)' phase contract did not retain the visible property ABI." + } + $fogContract = New-FogContract + $fogAssignmentPropertyName = $fogContract.unlitForwardAddFog.floatAssignments[0].propertyName + Assert-Harness -Condition ($fogAssignmentPropertyName -eq '_jp_penguin_purebase_release_fixture_unlit_forwardaddfog_ForwardAddFogSignalProperty') -Message 'Fog contract did not map its float assignment to the expected namespaced property ABI.' + Assert-Harness -Condition ($fogAssignmentPropertyName -ne '_ForwardAddFogSignalProperty') -Message 'Fog contract regressed to the raw property name.' + $toonBaseModule = [ordered]@{ label = 'toon-base'; phase = 'base'; uniqueId = 'jp.penguin.purebase.release.fixture.toon.phase.base'; propertyName = '_jp_penguin_purebase_release_fixture_toon_phase_base_ProductPhaseValue'; sentinel = 'PUREBASE_TOON_PRODUCT_PHASE_SENTINEL_BASE' } + $toonBaseRuntimeContract = New-ToonRuntimeContract -Module $toonBaseModule + $toonBaseRuntimeSample = $toonBaseRuntimeContract.runtimeSamples[0] + $toonBaseRuntimeDelta = $toonBaseRuntimeContract.runtimeDelta.selectedMinusModuleFree + $toonBaseModuleFreeReference = $toonBaseRuntimeContract.runtimeDelta.moduleFreeReference + Assert-Harness -Condition ($toonBaseRuntimeSample.red.minimum -eq 3.59 -and $toonBaseRuntimeSample.red.maximum -eq 3.61) -Message 'Toon base runtime absolute red range must remain the evidence-backed 3.59-3.61 interval.' + Assert-Harness -Condition ($toonBaseRuntimeDelta.red.minimum -eq 0.70 -and $toonBaseRuntimeDelta.red.maximum -eq 0.73) -Message 'Toon base selected-minus-module-free red range must remain the evidence-backed 0.70-0.73 interval.' + Assert-Harness -Condition ($toonBaseModuleFreeReference.red -eq 2.87890625 -and $toonBaseModuleFreeReference.green -eq 2.837890625 -and $toonBaseModuleFreeReference.blue -eq 2.72265625 -and $toonBaseModuleFreeReference.alpha -eq 1.0) -Message 'Toon base module-free reference must remain the recorded BIRP readback until the observation is evaluated.' + Assert-Harness -Condition ($toonBaseRuntimeSample.red.minimum -le 3.599609375 -and $toonBaseRuntimeSample.red.maximum -ge 3.599609375) -Message 'Toon base runtime absolute red range excludes the recorded BIRP readback.' + Assert-Harness -Condition ($toonBaseRuntimeDelta.red.minimum -le 0.712890625 -and $toonBaseRuntimeDelta.red.maximum -ge 0.712890625) -Message 'Toon base selected-minus-module-free red range excludes the recorded BIRP delta.' + Assert-Harness -Condition ($toonBaseRuntimeSample.red.maximum -lt 4.2 -and $toonBaseRuntimeDelta.red.maximum -lt 1.3) -Message 'Toon base runtime contract regressed to the direct-add red expectation.' + foreach ($invalidAbiInput in @( + [ordered]@{ uniqueId = ''; propertyName = '_ProductPhaseValue' }, + [ordered]@{ uniqueId = 'jp..penguin'; propertyName = '_ProductPhaseValue' }, + [ordered]@{ uniqueId = 'jp.penguin'; propertyName = 'ProductPhaseValue' } + )) { + $invalidAbiFailure = $null + try { + Get-ShaderCoreNamespacedPropertyName -ModuleUniqueId $invalidAbiInput.uniqueId -RawPropertyName $invalidAbiInput.propertyName | Out-Null + } + catch { + $invalidAbiFailure = $_ + } + Assert-Harness -Condition ($null -ne $invalidAbiFailure) -Message 'Malformed Shader-Core property ABI input unexpectedly passed.' + } - $missingCanonicalConfig = Invoke-HarnessCase -Label 'missing-canonical-config' -ManifestHashes @('bootstrap') -OmitStagedCanonicalConfig - Assert-Harness -Condition ($null -ne $missingCanonicalConfig.failure -and $missingCanonicalConfig.failure.Exception.Message -match 'Staged consumer destination is missing') -Message 'Missing canonical Shader-Core config unexpectedly passed or reported the wrong failure.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $missingCanonicalConfig.bootstrapDirectory 'shader-core-state-initialization-config.json') -PathType Leaf) -Message 'Missing canonical Shader-Core config did not persist its config receipt artifact.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $missingCanonicalConfig.bootstrapDirectory 'failure-evidence/shader-core-state-initialization-config.json') -PathType Leaf) -Message 'Missing canonical Shader-Core config did not retain its config receipt in bootstrap failure evidence.' - - $fixturePreservation = Invoke-HarnessCase -Label 'fixture-preservation' -Selections @{ 'PureBase/Unlit' = @('jp.penguin.purebase.release.fixture.module') } -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row', 'row') - Assert-Harness -Condition ($null -eq $fixturePreservation.failure) -Message 'Fixture registration preservation case unexpectedly failed.' - $settingsText = Get-Content -LiteralPath $fixturePreservation.settingsPath -Raw - Assert-Harness -Condition ($settingsText -match '(?ms)^ - shadername: PureBase/Tests/ShaderCore/Phase/PostPixel\r?\n modules:\r?\n - jp\.penguin\.purebase\.tests\.shadercore\.phase\.postpixel\r?$') -Message 'Shader-Core fixture registration was not preserved.' - Assert-Harness -Condition ($settingsText -match '(?ms)^ - shadername: PureBase/Unlit\r?\n modules:\r?\n - jp\.penguin\.purebase\.release\.fixture\.module\r?$') -Message 'Shader-Core product module selection was not applied.' - foreach ($mutation in @('source-mutation', 'uri-escape', 'unknown-unity-manifest-dependency', 'wrong-revision', 'lock-mismatch', 'lock-version-mismatch', 'lock-source-mismatch', 'lock-newtonsoft-depth-mismatch', 'lock-shader-core-newtonsoft-edge-missing', 'lock-shader-core-newtonsoft-edge-mismatch', 'lock-added-entry', 'invalid-meta', 'orphan-meta', 'generated-meta-item-type-mismatch', 'duplicate-meta', 'receipt-meta-collision', 'unknown-add', 'invalid-billing-mode', 'invalid-project-settings', 'invalid-shader-core-settings', 'missing-fixed-shader-core-host', 'reversed-shader-core-module-order', 'unexpected-shader-core-host')) { - Assert-HarnessSemanticRejection -SuccessfulCase $fixturePreservation -Mutation $mutation - } - foreach ($projectSettingsPath in (Get-FirstBootstrapProjectSettingsProfile).Keys) { - Assert-HarnessSemanticRejection -SuccessfulCase $fixturePreservation -Mutation ('invalid-generated-project-settings:' + $projectSettingsPath) - } - $fixedPointFailure = $null - try { - Assert-ConsumerSecondBootstrapFixedPoint -FirstBootstrap ([ordered]@{ rootSha256 = 'fixed' }) -AfterLibraryReset ([ordered]@{ rootSha256 = 'fixed' }) -SecondBootstrap ([ordered]@{ rootSha256 = 'fixed' }) -Delta ([ordered]@{ added = @([ordered]@{ path = 'ProjectSettings/Unexpected.asset' }); changed = @(); removed = @() }) - } - catch { - $fixedPointFailure = $_ - } - Assert-Harness -Condition ($null -ne $fixedPointFailure -and $fixedPointFailure.Exception.Message -eq 'Second bootstrap did not reach a byte-exact immutable fixed point.') -Message 'A nonempty second-bootstrap delta unexpectedly passed.' - - $unityFailure = Invoke-HarnessCase -Label 'unity-exit-failure' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') -UnityExitCode 17 -NUnitResult 'Failed' -NUnitPassed 0 -NUnitFailed 1 - $nunitFailure = Invoke-HarnessCase -Label 'nunit-failure' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') -NUnitResult 'Failed' -NUnitPassed 0 -NUnitFailed 1 - foreach ($case in @($unityFailure, $nunitFailure)) { - Assert-Harness -Condition ($null -ne $case.failure) -Message "Failure row '$($case.runDirectory)' unexpectedly passed." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-after.json') -PathType Leaf) -Message "Failure row '$($case.runDirectory)' did not persist its after manifest." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'failure.json') -PathType Leaf) -Message "Failure row '$($case.runDirectory)' did not persist failure.json." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'failure-evidence/immutable-input-manifest-after.json') -PathType Leaf) -Message "Failure row '$($case.runDirectory)' did not preserve the after manifest as recoverable evidence." - } - Assert-Harness -Condition ($unityFailure.failure.Exception.Message -match 'NUnit failure: synthetic NUnit failure detail') -Message 'Nonzero Unity exit did not surface available NUnit failure detail.' + foreach ($completionCase in @( + [ordered]@{ validationScope = 'full-release-validation-matrix'; moduleFreeOnly = $false }, + [ordered]@{ validationScope = 'module-free-diagnostic-only'; moduleFreeOnly = $true }, + [ordered]@{ validationScope = 'progressive-cpu-bake-diagnostic-only'; moduleFreeOnly = $false } + )) { + $completion = Invoke-HarnessCompletionPath -ValidationScope $completionCase.validationScope -ModuleFreeOnly:$completionCase.moduleFreeOnly + Assert-Harness -Condition ($null -eq $completion.failure) -Message "Non-comparison completion '$($completionCase.validationScope)' unexpectedly failed under StrictMode." + Assert-Harness -Condition ($null -ne $completion.summary) -Message "Non-comparison completion '$($completionCase.validationScope)' did not persist run-summary.json." + Assert-Harness -Condition ($completion.summary.validationScope -eq $completionCase.validationScope) -Message "Non-comparison completion '$($completionCase.validationScope)' changed validationScope." + Assert-Harness -Condition ($completion.summary.outcomes[0].runDirectoryLabel -eq 'synthetic-success') -Message "Non-comparison completion '$($completionCase.validationScope)' did not preserve the run directory label." + Assert-Harness -Condition ($completion.summary.consumerDirectoryCreationCount -eq 1 -and $completion.summary.consumerDirectoryRemovalCount -eq 1) -Message "Non-comparison completion '$($completionCase.validationScope)' changed consumer lifecycle counts." + Assert-Harness -Condition (-not $completion.summary.comparisonMode -and ([bool]$completion.summary.moduleFreeOnly -eq [bool]$completionCase.moduleFreeOnly)) -Message "Non-comparison completion '$($completionCase.validationScope)' changed comparison flags." + Assert-Harness -Condition ($null -eq $completion.summary.comparisonVerdict) -Message "Non-comparison completion '$($completionCase.validationScope)' claimed a comparison verdict." + Assert-Harness -Condition ($completion.cleanup.consumerDirectoryCreationCount -eq 1 -and $completion.cleanup.consumerDirectoryRemovalCount -eq 1) -Message "Non-comparison cleanup '$($completionCase.validationScope)' changed consumer lifecycle counts." + Assert-Harness -Condition (-not $completion.cleanup.failed) -Message "Non-comparison completion '$($completionCase.validationScope)' wrote an incorrect cleanup failure state." + } - $editorGuardFailure = Invoke-HarnessCase -Label 'editor-guard-failure' -ManifestHashes @('bootstrap') -EditorGuardFailure - foreach ($case in @($editorGuardFailure)) { - Assert-Harness -Condition ($null -ne $case.failure) -Message "Preflight failure row '$($case.runDirectory)' unexpectedly passed." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'failure.json') -PathType Leaf) -Message "Preflight failure row '$($case.bootstrapDirectory)' did not persist bootstrap failure.json." - } + $runSummaryFailure = Invoke-HarnessCompletionPath -ValidationScope 'full-release-validation-matrix' -ForceRunSummaryFailure + Assert-Harness -Condition ($null -ne $runSummaryFailure.failure) -Message 'Synthetic run-summary failure unexpectedly passed.' + Assert-Harness -Condition ($runSummaryFailure.cleanup.failed) -Message 'Run-summary failure wrote cleanup-summary.json with failed=false.' + + $staleLockCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true + Assert-Harness -Condition ($null -eq $staleLockCleanup.failure -and -not (Test-Path -LiteralPath $staleLockCleanup.consumerRoot)) -Message 'Stale Unity lock state prevented consumer cleanup after a failed Unity execution without an active process.' + Assert-Harness -Condition ($staleLockCleanup.summary.consumerDirectoryCreationCount -eq 1 -and $staleLockCleanup.summary.consumerDirectoryRemovalCount -eq 1 -and $staleLockCleanup.summary.cleanupStatus -eq 'removed' -and -not $staleLockCleanup.summary.consumerDirectoryRemovalFailed -and $staleLockCleanup.summary.failed) -Message 'Stale-lock cleanup summary did not accurately record creation, removal, and execution failure.' + + $activeProcessCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true -ProcessDiscoveryScenario active + Assert-Harness -Condition ($activeProcessCleanup.discovery.status -eq 'active' -and $activeProcessCleanup.discovery.process.ProcessId -eq 4242 -and $null -ne $activeProcessCleanup.failure -and (Test-Path -LiteralPath $activeProcessCleanup.consumerRoot)) -Message 'Active Unity process did not block consumer cleanup.' + Assert-Harness -Condition ($activeProcessCleanup.summary.consumerDirectoryRemovalCount -eq 0 -and $activeProcessCleanup.summary.cleanupStatus -eq 'active' -and $activeProcessCleanup.summary.consumerDirectoryRemovalFailed -and $activeProcessCleanup.summary.failed) -Message 'Active-process cleanup summary did not record the refused deletion.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $activeProcessCleanup.root 'cleanup-failure.json') -PathType Leaf) -Message 'Active-process cleanup did not persist cleanup failure evidence.' + $activeCleanupEvidence = Get-Content -LiteralPath (Join-Path $activeProcessCleanup.root 'cleanup-failure.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition ($activeCleanupEvidence.executionFailure -eq 'Synthetic Unity failure.' -and $activeCleanupEvidence.cleanupFailure -match 'Unity process 4242') -Message 'Active-process cleanup evidence did not retain the execution and cleanup failures.' + + $cimFailureCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true -ProcessDiscoveryScenario query-error + Assert-Harness -Condition ($cimFailureCleanup.discovery.status -eq 'indeterminate' -and $cimFailureCleanup.discovery.reason -match 'Synthetic CIM access failure' -and $null -ne $cimFailureCleanup.failure -and (Test-Path -LiteralPath $cimFailureCleanup.consumerRoot)) -Message 'Get-CimInstance failure did not return indeterminate or block consumer cleanup.' + Assert-Harness -Condition ($cimFailureCleanup.summary.consumerDirectoryRemovalCount -eq 0 -and $cimFailureCleanup.summary.cleanupStatus -eq 'indeterminate' -and $cimFailureCleanup.summary.cleanupReason -match 'Synthetic CIM access failure' -and $cimFailureCleanup.summary.consumerDirectoryRemovalFailed -and $cimFailureCleanup.summary.failed) -Message 'Get-CimInstance failure cleanup summary did not record the refused deletion and reason.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $cimFailureCleanup.root 'cleanup-failure.json') -PathType Leaf) -Message 'Get-CimInstance failure did not persist cleanup failure evidence.' + $cimFailureCleanupEvidence = Get-Content -LiteralPath (Join-Path $cimFailureCleanup.root 'cleanup-failure.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition ($cimFailureCleanupEvidence.cleanupStatus -eq 'indeterminate' -and $cimFailureCleanupEvidence.cleanupReason -match 'Synthetic CIM access failure' -and $cimFailureCleanupEvidence.cleanupFailure -match 'Cannot verify whether a Unity process') -Message 'Get-CimInstance failure evidence did not retain the discovery failure.' + + $missingCommandLineCleanup = Invoke-HarnessCleanupPath -ExecutionFailed $true -ProcessDiscoveryScenario missing-command-line + Assert-Harness -Condition ($missingCommandLineCleanup.discovery.status -eq 'indeterminate' -and $missingCommandLineCleanup.discovery.reason -match 'Cannot inspect CommandLine.*4243.*4244' -and $null -ne $missingCommandLineCleanup.failure -and (Test-Path -LiteralPath $missingCommandLineCleanup.consumerRoot)) -Message 'Null or empty Unity CommandLine candidates did not return indeterminate or block consumer cleanup.' + Assert-Harness -Condition ($missingCommandLineCleanup.summary.consumerDirectoryRemovalCount -eq 0 -and $missingCommandLineCleanup.summary.cleanupStatus -eq 'indeterminate' -and $missingCommandLineCleanup.summary.cleanupReason -match 'Cannot inspect CommandLine.*4243.*4244' -and $missingCommandLineCleanup.summary.consumerDirectoryRemovalFailed -and $missingCommandLineCleanup.summary.failed) -Message 'Missing CommandLine cleanup summary did not record the refused deletion and reason.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $missingCommandLineCleanup.root 'cleanup-failure.json') -PathType Leaf) -Message 'Missing CommandLine cleanup did not persist cleanup failure evidence.' + $missingCommandLineCleanupEvidence = Get-Content -LiteralPath (Join-Path $missingCommandLineCleanup.root 'cleanup-failure.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition ($missingCommandLineCleanupEvidence.cleanupStatus -eq 'indeterminate' -and $missingCommandLineCleanupEvidence.cleanupReason -match 'Cannot inspect CommandLine.*4243.*4244' -and $missingCommandLineCleanupEvidence.cleanupFailure -match 'Cannot verify whether a Unity process') -Message 'Missing CommandLine cleanup evidence did not retain the discovery failure.' + + $expectedFirstBootstrapAddedCount = @(Get-ExpectedFirstBootstrapAddedPaths).Count + $expectedFirstBootstrapChangedCount = @(Get-ExpectedFirstBootstrapChangedPaths).Count + $expectedFirstBootstrapAcceptedCount = $expectedFirstBootstrapAddedCount + $expectedFirstBootstrapChangedCount + Assert-Harness -Condition ($expectedFirstBootstrapAddedCount -eq 25 -and $expectedFirstBootstrapChangedCount -eq 2 -and $expectedFirstBootstrapAcceptedCount -eq 27) -Message 'First-bootstrap expected transition counts do not match the hosted consumer contract.' + + foreach ($successfulLabel in @('module-free-clean-import', 'progressive-cpu-bake')) { + $case = Invoke-HarnessCase -Label $successfulLabel -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') + Assert-Harness -Condition ($null -eq $case.failure) -Message "Successful row '$successfulLabel' unexpectedly failed." + Assert-Harness -Condition ($case.resetCalls -eq 1) -Message "Successful row '$successfulLabel' did not reset only the bootstrap Library." + $resetEvidence = Get-Content -LiteralPath (Join-Path $case.runDirectory 'library-reset.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition (-not $resetEvidence.required -and -not $resetEvidence.attempted -and -not $resetEvidence.completed) -Message "Successful row '$successfulLabel' unexpectedly attempted a selected-module cold Library reset." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'staging-receipt.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its staging receipt." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-pre-bootstrap.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not collect the pre-bootstrap immutable manifest." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-after-scene-bootstrap.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not preserve the pre-initialization scene-bootstrap observation." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-quiescent.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not collect the canonical post-bootstrap manifest." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its bootstrap delta report." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'semantic-transition-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its first-bootstrap semantic report." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-command.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its Shader-Core initialization command." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its Shader-Core initialization report." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-config.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its Shader-Core initialization config receipt." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-test-hosts.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not preserve its staged Shader-Core config." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/shader-core-state-initialization-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its second Shader-Core initialization report." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-after-library-reset.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not compare immutable inputs after its bootstrap Library reset." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/fixed-point-report.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist second-bootstrap fixed-point evidence." + $preBootstrapManifest = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-pre-bootstrap.json') -Raw | ConvertFrom-Json + $bootstrapManifest = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-quiescent.json') -Raw | ConvertFrom-Json + $afterResetManifest = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-after-library-reset.json') -Raw | ConvertFrom-Json + $rowManifest = Get-Content -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-before.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition ($bootstrapManifest.rootSha256 -eq 'bootstrap' -and $rowManifest.rootSha256 -eq 'row') -Message "Successful row '$successfulLabel' did not collect its row baseline after bootstrap." + $bootstrapCommand = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'unity-command.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition ($bootstrapCommand.arguments -contains 'PureBase.Release.Consumer.Tests.PureBaseConsumerSceneTemplateBootstrapTests.DisposableSceneLifecycleMaterializesSceneTemplateSettings') -Message "Successful row '$successfulLabel' did not select the scene-template bootstrap test." + $bootstrapSceneTemplateEntry = @($bootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) + $preBootstrapSceneTemplateEntry = @($preBootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) + $afterResetSceneTemplateEntry = @($afterResetManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) + $bootstrapQualitySettingsEntry = @($bootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/QualitySettings.asset' }) + $preBootstrapQualitySettingsEntry = @($preBootstrapManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/QualitySettings.asset' }) + $bootstrapDelta = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -Raw | ConvertFrom-Json + $semanticTransition = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'semantic-transition-report.json') -Raw | ConvertFrom-Json + $initializationCommand = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-command.json') -Raw | ConvertFrom-Json + $initializationReport = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-report.json') -Raw | ConvertFrom-Json + $initializationConfig = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'shader-core-state-initialization-config.json') -Raw | ConvertFrom-Json + $stagingReceipt = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'staging-receipt.json') -Raw | ConvertFrom-Json + $secondInitializationReport = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/shader-core-state-initialization-report.json') -Raw | ConvertFrom-Json + $fixedPoint = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'second-bootstrap/fixed-point-report.json') -Raw | ConvertFrom-Json + Assert-Harness -Condition ($preBootstrapSceneTemplateEntry.Count -eq 0 -and $bootstrapSceneTemplateEntry.Count -eq 1 -and $afterResetSceneTemplateEntry.Count -eq 1 -and $afterResetSceneTemplateEntry[0].sha256 -eq $bootstrapSceneTemplateEntry[0].sha256) -Message "Successful row '$successfulLabel' did not preserve the materialized SceneTemplateSettings entry across the Library reset." + Assert-Harness -Condition ($preBootstrapQualitySettingsEntry.Count -eq 1 -and $bootstrapQualitySettingsEntry.Count -eq 1 -and $preBootstrapQualitySettingsEntry[0].sha256 -eq $bootstrapQualitySettingsEntry[0].sha256) -Message "Successful row '$successfulLabel' did not preserve the preexisting QualitySettings scaffold input." + Assert-Harness -Condition ($bootstrapDelta.classification -eq 'observed' -and @($bootstrapDelta.added).Count -eq $expectedFirstBootstrapAddedCount -and @($bootstrapDelta.changed).Count -eq $expectedFirstBootstrapChangedCount -and @($bootstrapDelta.removed).Count -eq 0) -Message "Successful row '$successfulLabel' did not report the observed first-bootstrap delta." + Assert-Harness -Condition ($semanticTransition.verdict -eq 'accepted' -and $semanticTransition.summary.accepted -eq $expectedFirstBootstrapAcceptedCount -and $semanticTransition.summary.rejected -eq 0 -and $semanticTransition.summary.unclassified -eq 0) -Message "Successful row '$successfulLabel' did not accept the exact first-bootstrap semantic transition." + $canonicalConfigDestination = Get-CanonicalShaderCoreConfigDestination + $canonicalReceiptEntry = @($stagingReceipt.entries | Where-Object { $_.destination -eq $canonicalConfigDestination }) + Assert-Harness -Condition ($initializationCommand.arguments -contains '-executeMethod' -and $initializationCommand.arguments -contains 'PureBase.Release.Consumer.Tests.PureBaseConsumerShaderCoreInitializer.InitializeForBatchMode') -Message "Successful row '$successfulLabel' did not execute the consumer-owned Shader-Core initializer." + Assert-Harness -Condition ($initializationConfig.destination -eq $canonicalConfigDestination -and $initializationConfig.expectedSourceKind -eq 'workspace-canonical-shader-core-config' -and $initializationConfig.receiptEntryCount -eq 1) -Message "Successful row '$successfulLabel' did not record its canonical Shader-Core config receipt." + Assert-Harness -Condition ($canonicalReceiptEntry.Count -eq 1 -and $canonicalReceiptEntry[0].sourceKind -eq 'workspace-canonical-shader-core-config' -and $canonicalReceiptEntry[0].sha256 -eq (Get-Sha256Hex -Path (Join-Path $case.consumerRoot $canonicalConfigDestination.Replace('/', '\')))) -Message "Successful row '$successfulLabel' did not stage the canonical Shader-Core config at its receipt-owned destination." + Assert-Harness -Condition ($initializationReport.schemaName -eq 'purebase-shader-core-bootstrap-initialization' -and $initializationReport.phase -eq 'first-bootstrap' -and $initializationReport.canonicalConfigDestination -eq $canonicalConfigDestination -and $initializationReport.rowCount -eq 15) -Message "Successful row '$successfulLabel' did not report the canonical first Shader-Core initialization mapping." + Assert-Harness -Condition (@($initializationReport.mapping.modules.'PureBase/Tests/ShaderCore/ModuleOrder').Count -eq 2 -and $initializationReport.mapping.modules.'PureBase/Tests/ShaderCore/ModuleOrder'[0] -eq 'jp.penguin.purebase.tests.shadercore.moduleorder.zeta' -and $initializationReport.mapping.modules.'PureBase/Tests/ShaderCore/ModuleOrder'[1] -eq 'jp.penguin.purebase.tests.shadercore.moduleorder.alpha') -Message "Successful row '$successfulLabel' did not preserve the canonical Shader-Core ModuleOrder mapping." + Assert-Harness -Condition ($secondInitializationReport.phase -eq 'second-bootstrap' -and $secondInitializationReport.rowCount -eq 15) -Message "Successful row '$successfulLabel' did not revalidate the canonical Shader-Core state after the Library reset." + Assert-Harness -Condition ($fixedPoint.rootsEqual -and @($fixedPoint.added).Count -eq 0 -and @($fixedPoint.changed).Count -eq 0 -and @($fixedPoint.removed).Count -eq 0) -Message "Successful row '$successfulLabel' did not reach the second-bootstrap immutable fixed point." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-before.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its before manifest." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-after.json') -PathType Leaf) -Message "Successful row '$successfulLabel' did not persist its after manifest." + } - $bootstrapProcessFailure = Invoke-HarnessCase -Label 'bootstrap-process-failure' -ManifestHashes @('bootstrap') -BootstrapExitCode 19 - $bootstrapNUnitFailure = Invoke-HarnessCase -Label 'bootstrap-nunit-failure' -ManifestHashes @('bootstrap') -BootstrapNUnitResult 'Failed' - $bootstrapBaselineFailure = Invoke-HarnessCase -Label 'bootstrap-baseline-failure' -ManifestHashes @('bootstrap', 'bootstrap') -BaselineMismatch - $initializerFailure = Invoke-HarnessCase -Label 'initializer-process-failure' -ManifestHashes @('bootstrap', 'bootstrap') -InitializerExitCode 23 - foreach ($case in @($bootstrapProcessFailure, $bootstrapNUnitFailure, $bootstrapBaselineFailure)) { - Assert-Harness -Condition ($null -ne $case.failure) -Message "Bootstrap failure '$($case.bootstrapDirectory)' unexpectedly passed." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-quiescent.json') -PathType Leaf) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not persist its post-bootstrap manifest." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -PathType Leaf) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not persist its observed delta." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'failure-evidence/immutable-input-manifest-bootstrap-delta.json') -PathType Leaf) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not preserve its actual delta artifact as failure evidence." - $bootstrapDelta = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -Raw | ConvertFrom-Json - $bootstrapFailureDelta = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'failure-evidence/immutable-input-manifest-bootstrap-delta.json') -Raw | ConvertFrom-Json - Assert-ExactJsonPropertyNames -Value $bootstrapDelta -ExpectedNames @('schemaName', 'schemaVersion', 'classification', 'pathOrdering', 'preBootstrapRootSha256', 'postBootstrapRootSha256', 'added', 'removed', 'changed') -Description 'Bootstrap failure delta report' - Assert-Harness -Condition ($bootstrapDelta.schemaName -eq 'purebase-immutable-manifest-bootstrap-delta' -and $bootstrapDelta.schemaVersion -eq 1 -and $bootstrapDelta.classification -eq 'observed' -and $bootstrapDelta.pathOrdering -eq 'System.StringComparer.Ordinal' -and @($bootstrapDelta.added).Count -eq $expectedFirstBootstrapAddedCount -and @($bootstrapDelta.changed).Count -eq $expectedFirstBootstrapChangedCount -and @($bootstrapDelta.removed).Count -eq 0) -Message "Bootstrap failure '$($case.bootstrapDirectory)' changed its deterministic observed delta schema or content." - Assert-Harness -Condition (($bootstrapDelta | ConvertTo-Json -Depth 8 -Compress) -eq ($bootstrapFailureDelta | ConvertTo-Json -Depth 8 -Compress)) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not preserve the actual delta artifact in failure evidence." - Assert-Harness -Condition (-not (Test-Path -LiteralPath $case.runDirectory)) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not fail closed before a matrix row." - } - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapProcessFailure.bootstrapDirectory 'failure-evidence/Process.log') -PathType Leaf) -Message 'Bootstrap process failure did not preserve its process evidence.' - Assert-Harness -Condition ($bootstrapNUnitFailure.failure.Exception.Message -match 'did not pass cleanly') -Message 'Bootstrap NUnit failure did not report the NUnit verdict.' - Assert-Harness -Condition ($bootstrapBaselineFailure.failure.Exception.Message -match 'semantic transition profile does not match') -Message 'Bootstrap identity failure did not reject the persisted semantic report.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapBaselineFailure.bootstrapDirectory 'semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap identity failure did not persist its semantic report before rejection.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapBaselineFailure.bootstrapDirectory 'failure-evidence/semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap identity failure did not preserve the semantic report as failure evidence.' - Assert-Harness -Condition ($null -ne $initializerFailure.failure -and $initializerFailure.failure.Exception.Message -match 'Shader-Core state initialization') -Message 'Consumer initializer process failure unexpectedly passed or reported the wrong failure.' - foreach ($initializerEvidenceFile in @('shader-core-state-initialization-command.json', 'shader-core-state-initialization-config.json', 'shader-core-test-hosts.json', 'shader-core-state-initialization-Process.log')) { - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $initializerFailure.bootstrapDirectory ('failure-evidence/' + $initializerEvidenceFile)) -PathType Leaf) -Message "Consumer initializer failure did not retain '$initializerEvidenceFile'." - } + $deltaPreBootstrap = [ordered]@{ + rootSha256 = 'pre-root' + entries = @( + [ordered]@{ path = 'Assets/Changed.asset'; sha256 = 'before-change' }, + [ordered]@{ path = 'Assets/Removed.asset'; sha256 = 'removed-hash' } + ) + } + $deltaPostBootstrap = [ordered]@{ + rootSha256 = 'post-root' + entries = @( + [ordered]@{ path = 'Assets/Added.asset'; sha256 = 'added-hash' }, + [ordered]@{ path = 'Assets/Changed.asset'; sha256 = 'after-change' } + ) + } + $deltaReport = Get-ConsumerImmutableManifestDeltaReport -PreBootstrap $deltaPreBootstrap -PostBootstrap $deltaPostBootstrap + $deltaReportRepeat = Get-ConsumerImmutableManifestDeltaReport -PreBootstrap $deltaPreBootstrap -PostBootstrap $deltaPostBootstrap + $deltaReportArtifact = $deltaReport | ConvertTo-Json -Depth 8 | ConvertFrom-Json + Assert-ExactJsonPropertyNames -Value $deltaReportArtifact -ExpectedNames @('schemaName', 'schemaVersion', 'classification', 'pathOrdering', 'preBootstrapRootSha256', 'postBootstrapRootSha256', 'added', 'removed', 'changed') -Description 'Bootstrap delta report' + Assert-Harness -Condition ($deltaReport.schemaName -eq 'purebase-immutable-manifest-bootstrap-delta' -and $deltaReport.schemaVersion -eq 1 -and $deltaReport.classification -eq 'observed' -and $deltaReport.pathOrdering -eq 'System.StringComparer.Ordinal' -and $deltaReport.preBootstrapRootSha256 -eq 'pre-root' -and $deltaReport.postBootstrapRootSha256 -eq 'post-root') -Message 'Bootstrap delta report schema changed.' + Assert-Harness -Condition (@($deltaReport.added).Count -eq 1 -and $deltaReport.added[0].path -eq 'Assets/Added.asset' -and $deltaReport.added[0].sha256 -eq 'added-hash') -Message 'Bootstrap delta report omitted the added path hash.' + Assert-Harness -Condition (@($deltaReport.removed).Count -eq 1 -and $deltaReport.removed[0].path -eq 'Assets/Removed.asset' -and $deltaReport.removed[0].sha256 -eq 'removed-hash') -Message 'Bootstrap delta report omitted the removed path hash.' + Assert-Harness -Condition (@($deltaReport.changed).Count -eq 1 -and $deltaReport.changed[0].path -eq 'Assets/Changed.asset' -and $deltaReport.changed[0].preBootstrapSha256 -eq 'before-change' -and $deltaReport.changed[0].postBootstrapSha256 -eq 'after-change') -Message 'Bootstrap delta report omitted the changed path hashes.' + Assert-Harness -Condition (($deltaReport | ConvertTo-Json -Depth 8 -Compress) -eq ($deltaReportRepeat | ConvertTo-Json -Depth 8 -Compress)) -Message 'Bootstrap delta report ordering is not deterministic.' + + $receiptSourceRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseReceiptSources-' + [guid]::NewGuid().ToString('N')) + try { + $receiptScaffoldRoot = Join-Path $receiptSourceRoot 'ConsumerProject' + $receiptShaderCoreRoot = Join-Path $receiptSourceRoot 'ShaderCore' + $receiptModulesRoot = Join-Path $receiptSourceRoot 'Modules' + $receiptFixturesRoot = Join-Path $receiptSourceRoot 'Fixtures' + $receiptZipSourceRoot = Join-Path $receiptSourceRoot 'ReleasePackage' + $receiptCanonicalConfigPath = Join-Path $receiptSourceRoot 'Canonical/shader-core-test-hosts.json' + foreach ($sourceFile in @( + [ordered]@{ root = $receiptScaffoldRoot; relativePath = 'ProjectSettings/ProjectVersion.txt'; content = 'scaffold' }, + [ordered]@{ root = $receiptShaderCoreRoot; relativePath = 'package.json'; content = 'shader-core' }, + [ordered]@{ root = $receiptModulesRoot; relativePath = 'RootModule/module.scmodule'; content = 'module' }, + [ordered]@{ root = $receiptFixturesRoot; relativePath = 'TestFixture.mat'; content = 'fixture' }, + [ordered]@{ root = $receiptZipSourceRoot; relativePath = 'package.json'; content = 'release-package' } + )) { + $sourcePath = Join-Path $sourceFile.root $sourceFile.relativePath + New-Item -ItemType Directory -Path (Split-Path -Parent $sourcePath) -Force | Out-Null + [System.IO.File]::WriteAllText($sourcePath, $sourceFile.content, (New-Object System.Text.UTF8Encoding($false))) + } + New-Item -ItemType Directory -Path (Split-Path -Parent $receiptCanonicalConfigPath) -Force | Out-Null + [System.IO.File]::WriteAllText($receiptCanonicalConfigPath, '{"schemaVersion":1,"hosts":[]}', (New-Object System.Text.UTF8Encoding($false))) + + $receiptZipPath = Join-Path $receiptSourceRoot 'release.zip' + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::CreateFromDirectory($receiptZipSourceRoot, $receiptZipPath) + $actualReceipt = Get-ConsumerStagingReceipt -ZipPath $receiptZipPath -ScaffoldRoot $receiptScaffoldRoot -ShaderCoreRoot $receiptShaderCoreRoot -ModulesRoot $receiptModulesRoot -FixturesRoot $receiptFixturesRoot -CanonicalShaderCoreConfigPath $receiptCanonicalConfigPath + $expectedReceiptEntries = @( + [ordered]@{ destination = 'ProjectSettings/ProjectVersion.txt'; sourceKind = 'consumer-scaffold' }, + [ordered]@{ destination = '_LocalPackages/jp.lilxyzw.shadercore/package.json'; sourceKind = 'shader-core-tree' }, + [ordered]@{ destination = 'Assets/ReleaseModules/RootModule/module.scmodule'; sourceKind = 'release-modules' }, + [ordered]@{ destination = 'Assets/ReleaseConsumer/Fixtures/TestFixture.mat'; sourceKind = 'release-fixtures' }, + [ordered]@{ destination = 'Assets/ReleaseConsumer/Fixtures/ShaderCore/shader-core-test-hosts.json'; sourceKind = 'workspace-canonical-shader-core-config' }, + [ordered]@{ destination = '_LocalPackages/jp.penguin.purebase/package.json'; sourceKind = 'release-zip-entry' } + ) + Assert-Harness -Condition (@($actualReceipt.entries).Count -eq $expectedReceiptEntries.Count) -Message 'Actual staging receipt did not contain every source tree entry.' + foreach ($expectedReceiptEntry in $expectedReceiptEntries) { + $actualReceiptEntry = @($actualReceipt.entries | Where-Object { $_.destination -eq $expectedReceiptEntry.destination }) + Assert-Harness -Condition ($actualReceiptEntry.Count -eq 1 -and $actualReceiptEntry[0].sourceKind -eq $expectedReceiptEntry.sourceKind) -Message "Actual staging receipt did not preserve '$($expectedReceiptEntry.destination)' from '$($expectedReceiptEntry.sourceKind)'." + } + $canonicalReceiptEntry = @($actualReceipt.entries | Where-Object { $_.destination -eq (Get-CanonicalShaderCoreConfigDestination) }) + Assert-Harness -Condition ($canonicalReceiptEntry.Count -eq 1 -and $canonicalReceiptEntry[0].source -eq $receiptCanonicalConfigPath -and $canonicalReceiptEntry[0].sha256 -eq (Get-Sha256Hex -Path $receiptCanonicalConfigPath)) -Message 'Actual staging receipt did not preserve the canonical config source and hash.' + Assert-Harness -Condition (@($actualReceipt.entries | Where-Object { $_.destination -like 'consumer-scaffold/*' }).Count -eq 0) -Message 'Actual staging receipt prefixed the consumer scaffold root unexpectedly.' - $bootstrapSemanticFailure = Invoke-HarnessCase -Label 'bootstrap-semantic-meta-collision' -ManifestHashes @('bootstrap', 'bootstrap') -BootstrapSemanticMutation 'receipt-meta-collision' - Assert-Harness -Condition ($null -ne $bootstrapSemanticFailure.failure -and $bootstrapSemanticFailure.failure.Exception.Message -match 'semantic transition rejected') -Message 'Bootstrap semantic mutation did not fail closed through Invoke-ConsumerBootstrapImport.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapSemanticFailure.bootstrapDirectory 'semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap semantic mutation did not persist its semantic report.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapSemanticFailure.bootstrapDirectory 'failure-evidence/semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap semantic mutation did not preserve the semantic report as failure evidence.' - Assert-Harness -Condition (-not (Test-Path -LiteralPath $bootstrapSemanticFailure.runDirectory)) -Message 'Bootstrap semantic mutation did not stop before the matrix row.' - - $resetMismatch = Invoke-HarnessCase -Label 'reset-mismatch' -Selections @{ 'PureBase/Unlit' = @('module') } -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'drift', 'drift') - Assert-Harness -Condition ($null -ne $resetMismatch.failure) -Message 'Cold reset manifest drift unexpectedly passed.' - Assert-Harness -Condition ($resetMismatch.resetCalls -eq 2) -Message 'Cold reset drift did not execute both bootstrap and selected-module Library resets.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $resetMismatch.runDirectory 'immutable-input-manifest-after-reset.json') -PathType Leaf) -Message 'Cold reset drift did not persist its post-reset manifest.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $resetMismatch.runDirectory 'failure-evidence/immutable-input-manifest-after.json') -PathType Leaf) -Message 'Cold reset drift did not preserve final immutable evidence.' - - $finalDrift = Invoke-HarnessCase -Label 'final-drift' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'drift') - Assert-Harness -Condition ($null -ne $finalDrift.failure) -Message 'Immutable drift unexpectedly passed.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $finalDrift.runDirectory 'failure.json') -PathType Leaf) -Message 'Immutable drift did not fail closed with failure.json.' - $finalDriftBeforeManifest = Get-Content -LiteralPath (Join-Path $finalDrift.runDirectory 'immutable-input-manifest-before.json') -Raw | ConvertFrom-Json - $finalDriftAfterManifest = Get-Content -LiteralPath (Join-Path $finalDrift.runDirectory 'immutable-input-manifest-after.json') -Raw | ConvertFrom-Json - $finalDriftBeforeSceneTemplateEntry = @($finalDriftBeforeManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) - $finalDriftAfterSceneTemplateEntry = @($finalDriftAfterManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) - Assert-Harness -Condition ($finalDriftBeforeManifest.rootSha256 -eq 'row' -and $finalDriftAfterManifest.rootSha256 -eq 'drift' -and $finalDriftBeforeSceneTemplateEntry.Count -eq 1 -and $finalDriftAfterSceneTemplateEntry.Count -eq 1 -and $finalDriftAfterSceneTemplateEntry[0].sha256 -eq $finalDriftBeforeSceneTemplateEntry[0].sha256) -Message 'Immutable drift did not preserve the materialized SceneTemplateSettings entry while rejecting unrelated immutable input drift.' - - $canonicalCounts = @{} - $duplicateCounts = @{} - $invalidCounts = @{} - foreach ($productName in $ProductNames) { - $canonicalCounts[$productName] = @(1, 1, 1, 0) - $duplicateCounts[$productName] = @(2, 2, 2, 0) - $invalidCounts[$productName] = @(1, 3, 1, 0) - } - $warmContract = New-HarnessStandardMorphContract -RunLabel 'standard-morph-warm-library-duplicate-evidence' - $coldContract = New-HarnessStandardMorphContract -RunLabel 'standard-morph-cold-library-legacy-counts' - - $canonicalRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonCanonical-' + [guid]::NewGuid().ToString('N')) - Write-HarnessComparisonEvidence -Root $canonicalRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $canonicalCounts -ColdCounts $canonicalCounts - $canonicalVerdict = Invoke-StandardMorphComparisonVerdict -RunRoot $canonicalRoot -WarmContract $warmContract -ColdContract $coldContract - Assert-Harness -Condition ($canonicalVerdict.status -eq 'passed') -Message 'Canonical warm/cold comparison did not pass.' - Assert-Harness -Condition (@($canonicalVerdict.products | Where-Object { $_.warmClassification -ne 'canonical' -or -not $_.coldCanonical }).Count -eq 0) -Message 'Canonical warm/cold comparison did not classify every product as canonical.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $canonicalRoot 'standard-morph-comparison-verdict.json') -PathType Leaf) -Message 'Canonical comparison did not persist its verdict.' - - $duplicateRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonDuplicate-' + [guid]::NewGuid().ToString('N')) - Write-HarnessComparisonEvidence -Root $duplicateRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $duplicateCounts -ColdCounts $canonicalCounts - $duplicateVerdict = Invoke-StandardMorphComparisonVerdict -RunRoot $duplicateRoot -WarmContract $warmContract -ColdContract $coldContract - Assert-Harness -Condition ($duplicateVerdict.status -eq 'passed') -Message 'Known-duplicate warm evidence with canonical cold evidence did not pass.' - Assert-Harness -Condition (@($duplicateVerdict.products | Where-Object { $_.warmClassification -ne 'known-duplicate' -or -not $_.coldCanonical }).Count -eq 0) -Message 'Known-duplicate warm evidence did not receive the expected classification.' - - $invalidRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonInvalid-' + [guid]::NewGuid().ToString('N')) - Write-HarnessComparisonEvidence -Root $invalidRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $invalidCounts -ColdCounts $canonicalCounts - $invalidFailure = $null - try { - Invoke-StandardMorphComparisonVerdict -RunRoot $invalidRoot -WarmContract $warmContract -ColdContract $coldContract | Out-Null - } - catch { - $invalidFailure = $_ - } - Assert-Harness -Condition ($null -ne $invalidFailure) -Message 'Invalid warm observation unexpectedly passed.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $invalidRoot 'standard-morph-comparison-verdict.json') -PathType Leaf) -Message 'Invalid warm observation did not persist a verdict.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $invalidRoot 'standard-morph-comparison-failure.json') -PathType Leaf) -Message 'Invalid warm observation did not persist an external failure artifact.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $invalidRoot ('runs/' + $warmContract.runLabel + '/consumer-evidence/standard-morph-observation.json')) -PathType Leaf) -Message 'Invalid warm observation did not retain row evidence.' - - foreach ($observationFailureKind in @('malformed', 'missing')) { - $observationFailureRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonObservation-' + $observationFailureKind + '-' + [guid]::NewGuid().ToString('N')) - Write-HarnessComparisonEvidence -Root $observationFailureRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $canonicalCounts -ColdCounts $canonicalCounts - $observationPath = Join-Path $observationFailureRoot ('runs/' + $warmContract.runLabel + '/consumer-evidence/standard-morph-observation.json') - if ($observationFailureKind -eq 'malformed') { - [System.IO.File]::WriteAllText($observationPath, '{not-json', (New-Object System.Text.UTF8Encoding($false))) - } - else { - Remove-Item -LiteralPath $observationPath -Force - } - $observationFailure = $null - try { - Invoke-StandardMorphComparisonVerdict -RunRoot $observationFailureRoot -WarmContract $warmContract -ColdContract $coldContract | Out-Null + $nullPrefixFailure = $null + try { + Add-ConsumerStagingReceiptTreeEntries -EntriesByDestination @{} -SourceRoot $receiptScaffoldRoot -DestinationPrefix $null -SourceKind 'consumer-scaffold' + } + catch { + $nullPrefixFailure = $_ + } + Assert-Harness -Condition ($null -ne $nullPrefixFailure) -Message 'Staging receipt destination prefix accepted null.' + } + finally { + Remove-Item -LiteralPath $receiptSourceRoot -Recurse -Force -ErrorAction SilentlyContinue + } + + foreach ($receiptCase in @( + [ordered]@{ label = 'missing'; expectedMessage = 'Staged consumer destination is missing' }, + [ordered]@{ label = 'extra'; expectedMessage = 'Staged consumer destination is extra' }, + [ordered]@{ label = 'hash-mismatch'; expectedMessage = 'Staged consumer destination content mismatches' } + )) { + $case = Invoke-HarnessCase -Label ('staging-receipt-' + $receiptCase.label) -ManifestHashes @('bootstrap') -StagingReceiptTransition $receiptCase.label + Assert-Harness -Condition ($null -ne $case.failure -and $case.failure.Exception.Message -match $receiptCase.expectedMessage) -Message "Staging receipt '$($receiptCase.label)' unexpectedly passed or reported the wrong failure." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'staging-receipt.json') -PathType Leaf) -Message "Staging receipt '$($receiptCase.label)' did not persist its receipt." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'failure-evidence/staging-receipt.json') -PathType Leaf) -Message "Staging receipt '$($receiptCase.label)' did not retain receipt failure evidence." + Assert-Harness -Condition (-not (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'unity-command.json') -PathType Leaf)) -Message "Staging receipt '$($receiptCase.label)' launched Unity before rejecting the staged consumer." + } + + $missingCanonicalConfig = Invoke-HarnessCase -Label 'missing-canonical-config' -ManifestHashes @('bootstrap') -OmitStagedCanonicalConfig + Assert-Harness -Condition ($null -ne $missingCanonicalConfig.failure -and $missingCanonicalConfig.failure.Exception.Message -match 'Staged consumer destination is missing') -Message 'Missing canonical Shader-Core config unexpectedly passed or reported the wrong failure.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $missingCanonicalConfig.bootstrapDirectory 'shader-core-state-initialization-config.json') -PathType Leaf) -Message 'Missing canonical Shader-Core config did not persist its config receipt artifact.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $missingCanonicalConfig.bootstrapDirectory 'failure-evidence/shader-core-state-initialization-config.json') -PathType Leaf) -Message 'Missing canonical Shader-Core config did not retain its config receipt in bootstrap failure evidence.' + + $fixturePreservation = Invoke-HarnessCase -Label 'fixture-preservation' -Selections @{ 'PureBase/Unlit' = @('jp.penguin.purebase.release.fixture.module') } -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row', 'row') + Assert-Harness -Condition ($null -eq $fixturePreservation.failure) -Message 'Fixture registration preservation case unexpectedly failed.' + $settingsText = Get-Content -LiteralPath $fixturePreservation.settingsPath -Raw + Assert-Harness -Condition ($settingsText -match '(?ms)^ - shadername: PureBase/Tests/ShaderCore/Phase/PostPixel\r?\n modules:\r?\n - jp\.penguin\.purebase\.tests\.shadercore\.phase\.postpixel\r?$') -Message 'Shader-Core fixture registration was not preserved.' + Assert-Harness -Condition ($settingsText -match '(?ms)^ - shadername: PureBase/Unlit\r?\n modules:\r?\n - jp\.penguin\.purebase\.release\.fixture\.module\r?$') -Message 'Shader-Core product module selection was not applied.' + foreach ($mutation in @('source-mutation', 'uri-escape', 'unknown-unity-manifest-dependency', 'wrong-revision', 'lock-mismatch', 'lock-version-mismatch', 'lock-source-mismatch', 'lock-newtonsoft-depth-mismatch', 'lock-shader-core-newtonsoft-edge-missing', 'lock-shader-core-newtonsoft-edge-mismatch', 'lock-added-entry', 'invalid-meta', 'orphan-meta', 'generated-meta-item-type-mismatch', 'duplicate-meta', 'receipt-meta-collision', 'unknown-add', 'invalid-billing-mode', 'invalid-project-settings', 'invalid-shader-core-settings', 'missing-fixed-shader-core-host', 'reversed-shader-core-module-order', 'unexpected-shader-core-host')) { + Assert-HarnessSemanticRejection -SuccessfulCase $fixturePreservation -Mutation $mutation + } + foreach ($projectSettingsPath in (Get-FirstBootstrapProjectSettingsProfile).Keys) { + Assert-HarnessSemanticRejection -SuccessfulCase $fixturePreservation -Mutation ('invalid-generated-project-settings:' + $projectSettingsPath) + } + $fixedPointFailure = $null + try { + Assert-ConsumerSecondBootstrapFixedPoint -FirstBootstrap ([ordered]@{ rootSha256 = 'fixed' }) -AfterLibraryReset ([ordered]@{ rootSha256 = 'fixed' }) -SecondBootstrap ([ordered]@{ rootSha256 = 'fixed' }) -Delta ([ordered]@{ added = @([ordered]@{ path = 'ProjectSettings/Unexpected.asset' }); changed = @(); removed = @() }) + } + catch { + $fixedPointFailure = $_ + } + Assert-Harness -Condition ($null -ne $fixedPointFailure -and $fixedPointFailure.Exception.Message -eq 'Second bootstrap did not reach a byte-exact immutable fixed point.') -Message 'A nonempty second-bootstrap delta unexpectedly passed.' + + $unityFailure = Invoke-HarnessCase -Label 'unity-exit-failure' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') -UnityExitCode 17 -NUnitResult 'Failed' -NUnitPassed 0 -NUnitFailed 1 + $nunitFailure = Invoke-HarnessCase -Label 'nunit-failure' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') -NUnitResult 'Failed' -NUnitPassed 0 -NUnitFailed 1 + foreach ($case in @($unityFailure, $nunitFailure)) { + Assert-Harness -Condition ($null -ne $case.failure) -Message "Failure row '$($case.runDirectory)' unexpectedly passed." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'immutable-input-manifest-after.json') -PathType Leaf) -Message "Failure row '$($case.runDirectory)' did not persist its after manifest." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'failure.json') -PathType Leaf) -Message "Failure row '$($case.runDirectory)' did not persist failure.json." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.runDirectory 'failure-evidence/immutable-input-manifest-after.json') -PathType Leaf) -Message "Failure row '$($case.runDirectory)' did not preserve the after manifest as recoverable evidence." + } + Assert-Harness -Condition ($unityFailure.failure.Exception.Message -match 'NUnit failure: synthetic NUnit failure detail') -Message 'Nonzero Unity exit did not surface available NUnit failure detail.' + + $editorGuardFailure = Invoke-HarnessCase -Label 'editor-guard-failure' -ManifestHashes @('bootstrap') -EditorGuardFailure + foreach ($case in @($editorGuardFailure)) { + Assert-Harness -Condition ($null -ne $case.failure) -Message "Preflight failure row '$($case.runDirectory)' unexpectedly passed." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'failure.json') -PathType Leaf) -Message "Preflight failure row '$($case.bootstrapDirectory)' did not persist bootstrap failure.json." + } + + $bootstrapProcessFailure = Invoke-HarnessCase -Label 'bootstrap-process-failure' -ManifestHashes @('bootstrap') -BootstrapExitCode 19 + $bootstrapNUnitFailure = Invoke-HarnessCase -Label 'bootstrap-nunit-failure' -ManifestHashes @('bootstrap') -BootstrapNUnitResult 'Failed' + $bootstrapBaselineFailure = Invoke-HarnessCase -Label 'bootstrap-baseline-failure' -ManifestHashes @('bootstrap', 'bootstrap') -BaselineMismatch + $initializerFailure = Invoke-HarnessCase -Label 'initializer-process-failure' -ManifestHashes @('bootstrap', 'bootstrap') -InitializerExitCode 23 + foreach ($case in @($bootstrapProcessFailure, $bootstrapNUnitFailure, $bootstrapBaselineFailure)) { + Assert-Harness -Condition ($null -ne $case.failure) -Message "Bootstrap failure '$($case.bootstrapDirectory)' unexpectedly passed." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-quiescent.json') -PathType Leaf) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not persist its post-bootstrap manifest." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -PathType Leaf) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not persist its observed delta." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $case.bootstrapDirectory 'failure-evidence/immutable-input-manifest-bootstrap-delta.json') -PathType Leaf) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not preserve its actual delta artifact as failure evidence." + $bootstrapDelta = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'immutable-input-manifest-bootstrap-delta.json') -Raw | ConvertFrom-Json + $bootstrapFailureDelta = Get-Content -LiteralPath (Join-Path $case.bootstrapDirectory 'failure-evidence/immutable-input-manifest-bootstrap-delta.json') -Raw | ConvertFrom-Json + Assert-ExactJsonPropertyNames -Value $bootstrapDelta -ExpectedNames @('schemaName', 'schemaVersion', 'classification', 'pathOrdering', 'preBootstrapRootSha256', 'postBootstrapRootSha256', 'added', 'removed', 'changed') -Description 'Bootstrap failure delta report' + Assert-Harness -Condition ($bootstrapDelta.schemaName -eq 'purebase-immutable-manifest-bootstrap-delta' -and $bootstrapDelta.schemaVersion -eq 1 -and $bootstrapDelta.classification -eq 'observed' -and $bootstrapDelta.pathOrdering -eq 'System.StringComparer.Ordinal' -and @($bootstrapDelta.added).Count -eq $expectedFirstBootstrapAddedCount -and @($bootstrapDelta.changed).Count -eq $expectedFirstBootstrapChangedCount -and @($bootstrapDelta.removed).Count -eq 0) -Message "Bootstrap failure '$($case.bootstrapDirectory)' changed its deterministic observed delta schema or content." + Assert-Harness -Condition (($bootstrapDelta | ConvertTo-Json -Depth 8 -Compress) -eq ($bootstrapFailureDelta | ConvertTo-Json -Depth 8 -Compress)) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not preserve the actual delta artifact in failure evidence." + Assert-Harness -Condition (-not (Test-Path -LiteralPath $case.runDirectory)) -Message "Bootstrap failure '$($case.bootstrapDirectory)' did not fail closed before a matrix row." + } + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapProcessFailure.bootstrapDirectory 'failure-evidence/Process.log') -PathType Leaf) -Message 'Bootstrap process failure did not preserve its process evidence.' + Assert-Harness -Condition ($bootstrapNUnitFailure.failure.Exception.Message -match 'did not pass cleanly') -Message 'Bootstrap NUnit failure did not report the NUnit verdict.' + Assert-Harness -Condition ($bootstrapBaselineFailure.failure.Exception.Message -match 'semantic transition profile does not match') -Message 'Bootstrap identity failure did not reject the persisted semantic report.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapBaselineFailure.bootstrapDirectory 'semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap identity failure did not persist its semantic report before rejection.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapBaselineFailure.bootstrapDirectory 'failure-evidence/semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap identity failure did not preserve the semantic report as failure evidence.' + Assert-Harness -Condition ($null -ne $initializerFailure.failure -and $initializerFailure.failure.Exception.Message -match 'Shader-Core state initialization') -Message 'Consumer initializer process failure unexpectedly passed or reported the wrong failure.' + foreach ($initializerEvidenceFile in @('shader-core-state-initialization-command.json', 'shader-core-state-initialization-config.json', 'shader-core-test-hosts.json', 'shader-core-state-initialization-Process.log')) { + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $initializerFailure.bootstrapDirectory ('failure-evidence/' + $initializerEvidenceFile)) -PathType Leaf) -Message "Consumer initializer failure did not retain '$initializerEvidenceFile'." + } + + $bootstrapSemanticFailure = Invoke-HarnessCase -Label 'bootstrap-semantic-meta-collision' -ManifestHashes @('bootstrap', 'bootstrap') -BootstrapSemanticMutation 'receipt-meta-collision' + Assert-Harness -Condition ($null -ne $bootstrapSemanticFailure.failure -and $bootstrapSemanticFailure.failure.Exception.Message -match 'semantic transition rejected') -Message 'Bootstrap semantic mutation did not fail closed through Invoke-ConsumerBootstrapImport.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapSemanticFailure.bootstrapDirectory 'semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap semantic mutation did not persist its semantic report.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $bootstrapSemanticFailure.bootstrapDirectory 'failure-evidence/semantic-transition-report.json') -PathType Leaf) -Message 'Bootstrap semantic mutation did not preserve the semantic report as failure evidence.' + Assert-Harness -Condition (-not (Test-Path -LiteralPath $bootstrapSemanticFailure.runDirectory)) -Message 'Bootstrap semantic mutation did not stop before the matrix row.' + + $resetMismatch = Invoke-HarnessCase -Label 'reset-mismatch' -Selections @{ 'PureBase/Unlit' = @('module') } -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'drift', 'drift') + Assert-Harness -Condition ($null -ne $resetMismatch.failure) -Message 'Cold reset manifest drift unexpectedly passed.' + Assert-Harness -Condition ($resetMismatch.resetCalls -eq 2) -Message 'Cold reset drift did not execute both bootstrap and selected-module Library resets.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $resetMismatch.runDirectory 'immutable-input-manifest-after-reset.json') -PathType Leaf) -Message 'Cold reset drift did not persist its post-reset manifest.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $resetMismatch.runDirectory 'failure-evidence/immutable-input-manifest-after.json') -PathType Leaf) -Message 'Cold reset drift did not preserve final immutable evidence.' + + $finalDrift = Invoke-HarnessCase -Label 'final-drift' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'drift') + Assert-Harness -Condition ($null -ne $finalDrift.failure) -Message 'Immutable drift unexpectedly passed.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $finalDrift.runDirectory 'failure.json') -PathType Leaf) -Message 'Immutable drift did not fail closed with failure.json.' + $finalDriftBeforeManifest = Get-Content -LiteralPath (Join-Path $finalDrift.runDirectory 'immutable-input-manifest-before.json') -Raw | ConvertFrom-Json + $finalDriftAfterManifest = Get-Content -LiteralPath (Join-Path $finalDrift.runDirectory 'immutable-input-manifest-after.json') -Raw | ConvertFrom-Json + $finalDriftBeforeSceneTemplateEntry = @($finalDriftBeforeManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) + $finalDriftAfterSceneTemplateEntry = @($finalDriftAfterManifest.entries | Where-Object { $_.path -eq 'ProjectSettings/SceneTemplateSettings.json' }) + Assert-Harness -Condition ($finalDriftBeforeManifest.rootSha256 -eq 'row' -and $finalDriftAfterManifest.rootSha256 -eq 'drift' -and $finalDriftBeforeSceneTemplateEntry.Count -eq 1 -and $finalDriftAfterSceneTemplateEntry.Count -eq 1 -and $finalDriftAfterSceneTemplateEntry[0].sha256 -eq $finalDriftBeforeSceneTemplateEntry[0].sha256) -Message 'Immutable drift did not preserve the materialized SceneTemplateSettings entry while rejecting unrelated immutable input drift.' + + $canonicalCounts = @{} + $duplicateCounts = @{} + $invalidCounts = @{} + foreach ($productName in $ProductNames) { + $canonicalCounts[$productName] = @(1, 1, 1, 0) + $duplicateCounts[$productName] = @(2, 2, 2, 0) + $invalidCounts[$productName] = @(1, 3, 1, 0) + } + $warmContract = New-HarnessStandardMorphContract -RunLabel 'standard-morph-warm-library-duplicate-evidence' + $coldContract = New-HarnessStandardMorphContract -RunLabel 'standard-morph-cold-library-legacy-counts' + + $canonicalRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonCanonical-' + [guid]::NewGuid().ToString('N')) + Write-HarnessComparisonEvidence -Root $canonicalRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $canonicalCounts -ColdCounts $canonicalCounts + $canonicalVerdict = Invoke-StandardMorphComparisonVerdict -RunRoot $canonicalRoot -WarmContract $warmContract -ColdContract $coldContract + Assert-Harness -Condition ($canonicalVerdict.status -eq 'passed') -Message 'Canonical warm/cold comparison did not pass.' + Assert-Harness -Condition (@($canonicalVerdict.products | Where-Object { $_.warmClassification -ne 'canonical' -or -not $_.coldCanonical }).Count -eq 0) -Message 'Canonical warm/cold comparison did not classify every product as canonical.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $canonicalRoot 'standard-morph-comparison-verdict.json') -PathType Leaf) -Message 'Canonical comparison did not persist its verdict.' + + $duplicateRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonDuplicate-' + [guid]::NewGuid().ToString('N')) + Write-HarnessComparisonEvidence -Root $duplicateRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $duplicateCounts -ColdCounts $canonicalCounts + $duplicateVerdict = Invoke-StandardMorphComparisonVerdict -RunRoot $duplicateRoot -WarmContract $warmContract -ColdContract $coldContract + Assert-Harness -Condition ($duplicateVerdict.status -eq 'passed') -Message 'Known-duplicate warm evidence with canonical cold evidence did not pass.' + Assert-Harness -Condition (@($duplicateVerdict.products | Where-Object { $_.warmClassification -ne 'known-duplicate' -or -not $_.coldCanonical }).Count -eq 0) -Message 'Known-duplicate warm evidence did not receive the expected classification.' + + $invalidRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonInvalid-' + [guid]::NewGuid().ToString('N')) + Write-HarnessComparisonEvidence -Root $invalidRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $invalidCounts -ColdCounts $canonicalCounts + $invalidFailure = $null + try { + Invoke-StandardMorphComparisonVerdict -RunRoot $invalidRoot -WarmContract $warmContract -ColdContract $coldContract | Out-Null + } + catch { + $invalidFailure = $_ + } + Assert-Harness -Condition ($null -ne $invalidFailure) -Message 'Invalid warm observation unexpectedly passed.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $invalidRoot 'standard-morph-comparison-verdict.json') -PathType Leaf) -Message 'Invalid warm observation did not persist a verdict.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $invalidRoot 'standard-morph-comparison-failure.json') -PathType Leaf) -Message 'Invalid warm observation did not persist an external failure artifact.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $invalidRoot ('runs/' + $warmContract.runLabel + '/consumer-evidence/standard-morph-observation.json')) -PathType Leaf) -Message 'Invalid warm observation did not retain row evidence.' + + foreach ($observationFailureKind in @('malformed', 'missing')) { + $observationFailureRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('PureBaseComparisonObservation-' + $observationFailureKind + '-' + [guid]::NewGuid().ToString('N')) + Write-HarnessComparisonEvidence -Root $observationFailureRoot -WarmContract $warmContract -ColdContract $coldContract -WarmCounts $canonicalCounts -ColdCounts $canonicalCounts + $observationPath = Join-Path $observationFailureRoot ('runs/' + $warmContract.runLabel + '/consumer-evidence/standard-morph-observation.json') + if ($observationFailureKind -eq 'malformed') { + [System.IO.File]::WriteAllText($observationPath, '{not-json', (New-Object System.Text.UTF8Encoding($false))) + } + else { + Remove-Item -LiteralPath $observationPath -Force + } + $observationFailure = $null + try { + Invoke-StandardMorphComparisonVerdict -RunRoot $observationFailureRoot -WarmContract $warmContract -ColdContract $coldContract | Out-Null + } + catch { + $observationFailure = $_ + } + Assert-Harness -Condition ($null -ne $observationFailure) -Message "${observationFailureKind} standard-morph observation unexpectedly passed." + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $observationFailureRoot 'standard-morph-comparison-failure.json') -PathType Leaf) -Message "${observationFailureKind} standard-morph observation did not persist a comparison failure artifact." + } + + $warmProcessFailure = Invoke-HarnessCase -Label 'standard-morph-warm-library-duplicate-evidence' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') -UnityExitCode 23 -TestFilter 'PureBase.Release.Consumer.Tests.PureBaseConsumerStandardMorphObservationTests.StandardMorphProductsRecordPassCountObservations' -AllowObservationEvidence + Assert-Harness -Condition ($null -ne $warmProcessFailure.failure) -Message 'Warm observation accepted a nonzero Unity process exit.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $warmProcessFailure.runDirectory 'failure.json') -PathType Leaf) -Message 'Warm observation process failure did not persist failure.json.' + Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $warmProcessFailure.runDirectory 'failure-evidence/Process.log') -PathType Leaf) -Message 'Warm observation process failure did not preserve Process.log.' + Write-Host 'Runner-only immutable manifest harness passed.' } - catch { - $observationFailure = $_ + finally { + Remove-Item -LiteralPath $fakeUnityPath -Force -ErrorAction SilentlyContinue } - Assert-Harness -Condition ($null -ne $observationFailure) -Message "${observationFailureKind} standard-morph observation unexpectedly passed." - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $observationFailureRoot 'standard-morph-comparison-failure.json') -PathType Leaf) -Message "${observationFailureKind} standard-morph observation did not persist a comparison failure artifact." - } - $warmProcessFailure = Invoke-HarnessCase -Label 'standard-morph-warm-library-duplicate-evidence' -ManifestHashes @('bootstrap', 'bootstrap', 'row', 'row') -UnityExitCode 23 -TestFilter 'PureBase.Release.Consumer.Tests.PureBaseConsumerStandardMorphObservationTests.StandardMorphProductsRecordPassCountObservations' -AllowObservationEvidence - Assert-Harness -Condition ($null -ne $warmProcessFailure.failure) -Message 'Warm observation accepted a nonzero Unity process exit.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $warmProcessFailure.runDirectory 'failure.json') -PathType Leaf) -Message 'Warm observation process failure did not persist failure.json.' - Assert-Harness -Condition (Test-Path -LiteralPath (Join-Path $warmProcessFailure.runDirectory 'failure-evidence/Process.log') -PathType Leaf) -Message 'Warm observation process failure did not preserve Process.log.' - Write-Host 'Runner-only immutable manifest harness passed.' -} -finally { - Remove-Item -LiteralPath $fakeUnityPath -Force -ErrorAction SilentlyContinue -} - -$runnerSource = Get-Content -LiteralPath $runnerPath -Raw -Assert-Harness -Condition ($runnerSource -match '\$packageJson\.version') -Message 'Release validation must derive the archive name from package.json.version.' -Assert-Harness -Condition ($runnerSource -notmatch 'jp\.penguin\.purebase-0\.1\.0\.zip') -Message 'Release validation must not use a fixed 0.1.0 archive name.' + $runnerSource = Get-Content -LiteralPath $runnerPath -Raw + Assert-Harness -Condition ($runnerSource -match '\$packageJson\.version') -Message 'Release validation must derive the archive name from package.json.version.' + Assert-Harness -Condition ($runnerSource -notmatch 'jp\.penguin\.purebase-0\.1\.0\.zip') -Message 'Release validation must not use a fixed 0.1.0 archive name.' } } + diff --git a/Tests/Release/Run-PureBaseReleaseValidation.ps1 b/Tests/Release/Run-PureBaseReleaseValidation.ps1 index f9928d1..e37989f 100644 --- a/Tests/Release/Run-PureBaseReleaseValidation.ps1 +++ b/Tests/Release/Run-PureBaseReleaseValidation.ps1 @@ -568,10 +568,10 @@ function Get-ExpectedFirstBootstrapChangedPaths { function Get-FirstBootstrapGeneratedMetaProfile { return [ordered]@{ - 'Assets/ReleaseConsumer/Fixtures.meta' = [ordered]@{ relatedPath = 'Assets/ReleaseConsumer/Fixtures'; itemType = 'Directory' } - 'Assets/ReleaseModules.meta' = [ordered]@{ relatedPath = 'Assets/ReleaseModules'; itemType = 'Directory' } - 'Assets/Resources.meta' = [ordered]@{ relatedPath = 'Assets/Resources'; itemType = 'Directory' } - 'Assets/Resources/BillingMode.json.meta' = [ordered]@{ relatedPath = 'Assets/Resources/BillingMode.json'; itemType = 'Leaf' } + 'Assets/ReleaseConsumer/Fixtures.meta' = [ordered]@{ relatedPath = 'Assets/ReleaseConsumer/Fixtures'; itemType = 'Directory' } + 'Assets/ReleaseModules.meta' = [ordered]@{ relatedPath = 'Assets/ReleaseModules'; itemType = 'Directory' } + 'Assets/Resources.meta' = [ordered]@{ relatedPath = 'Assets/Resources'; itemType = 'Directory' } + 'Assets/Resources/BillingMode.json.meta' = [ordered]@{ relatedPath = 'Assets/Resources/BillingMode.json'; itemType = 'Leaf' } } } @@ -903,8 +903,8 @@ function Get-FirstBootstrapMetaValidationFailures { $generatedMetaProfile = Get-FirstBootstrapGeneratedMetaProfile $allMetaPaths = @( Get-ChildItem -LiteralPath $ConsumerRoot -File -Recurse -Force | - ForEach-Object { Get-NormalizedRelativePath -Path $_.FullName.Substring($ConsumerRoot.Length).TrimStart('\', '/') } | - Where-Object { $_.EndsWith('.meta', [System.StringComparison]::Ordinal) -and ($_.StartsWith('Assets/', [System.StringComparison]::Ordinal) -or $_.StartsWith('Packages/', [System.StringComparison]::Ordinal) -or $_.StartsWith('ProjectSettings/', [System.StringComparison]::Ordinal) -or $_.StartsWith('_LocalPackages/', [System.StringComparison]::Ordinal)) } + ForEach-Object { Get-NormalizedRelativePath -Path $_.FullName.Substring($ConsumerRoot.Length).TrimStart('\', '/') } | + Where-Object { $_.EndsWith('.meta', [System.StringComparison]::Ordinal) -and ($_.StartsWith('Assets/', [System.StringComparison]::Ordinal) -or $_.StartsWith('Packages/', [System.StringComparison]::Ordinal) -or $_.StartsWith('ProjectSettings/', [System.StringComparison]::Ordinal) -or $_.StartsWith('_LocalPackages/', [System.StringComparison]::Ordinal)) } ) $allMetaPaths = Get-OrdinalSortedStrings -Values $allMetaPaths foreach ($path in $allMetaPaths) { @@ -2707,3 +2707,4 @@ if ($consumerCreated -ne 1 -or ((-not $KeepConsumer) -and $consumerRemoved -ne 1 } Write-Host "Pure-Base release consumer validation passed. Artifacts: '$runRoot'." + diff --git a/Tests/Run-PureBaseRegression.ps1 b/Tests/Run-PureBaseRegression.ps1 index 9b0c509..c5d63cf 100644 --- a/Tests/Run-PureBaseRegression.ps1 +++ b/Tests/Run-PureBaseRegression.ps1 @@ -430,8 +430,8 @@ function Assert-SmokeContract { } $mutatingCommands = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.CommandAst] }, $true) | - ForEach-Object { $_.GetCommandName() } | - Where-Object { $_ -in @('Copy-Item', 'Move-Item', 'Remove-Item') } + ForEach-Object { $_.GetCommandName() } | + Where-Object { $_ -in @('Copy-Item', 'Move-Item', 'Remove-Item') } if (@($mutatingCommands).Count -gt 0) { throw "Daily runner must not copy, move, or delete project content. Found: $($mutatingCommands -join ', ')." } @@ -496,4 +496,4 @@ if ($null -ne $dailyFailure) { throw $dailyFailure } -Write-Host "Pure-Base $Mode run passed. NUnit XML and Unity logs: '$runDirectory'." \ No newline at end of file +Write-Host "Pure-Base $Mode run passed. NUnit XML and Unity logs: '$runDirectory'." From 5457273c901d4e8f92131a2a24847c5b94ea89d1 Mon Sep 17 00:00:00 2001 From: PenguinDOOM Date: Tue, 11 Aug 2026 04:53:48 +0900 Subject: [PATCH 3/3] test: fix release authorization contracts - Allow horizontal alignment in release predicate source contracts and update the attestation SHA expectation. - Verified with Pester 5.9.0 targeted and clean-worktree full automation suites. --- .github/tests/ReleaseAuthorization.Tests.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/tests/ReleaseAuthorization.Tests.ps1 b/.github/tests/ReleaseAuthorization.Tests.ps1 index 56fa6f3..0bc36f1 100644 --- a/.github/tests/ReleaseAuthorization.Tests.ps1 +++ b/.github/tests/ReleaseAuthorization.Tests.ps1 @@ -82,9 +82,9 @@ Describe 'Release authorization workflow contract' { $predicateScript | Should -Match '(?m)^\[CmdletBinding\(\)\]$' $predicateScript | Should -Match '(?m)^\s*\[Parameter\(Mandatory\)\]\[string\]\$ReleaseArtifactRoot,$' $predicateScript | Should -Match 'Get-FileHash -LiteralPath \$subjectPath -Algorithm SHA256' - $predicateScript | Should -Match 'commitSha = \[string\]\$state\.commitSha' - $predicateScript | Should -Match 'sha = \$WorkflowSha' - $predicateScript | Should -Match 'runAttempt = \$RunAttempt' + $predicateScript | Should -Match '(?m)^ commitSha[ \t]*=[ \t]*\[string\]\$state\.commitSha$' + $predicateScript | Should -Match '(?m)^ sha[ \t]*=[ \t]*\$WorkflowSha$' + $predicateScript | Should -Match '(?m)^ runAttempt[ \t]*=[ \t]*\$RunAttempt$' } It 'rejects malformed release state with an explicit parse error' { @@ -133,7 +133,7 @@ Describe 'Release authorization workflow contract' { } It 'uses a pinned custom attestation and preserves its evidence bundle' { - $workflow | Should -Match 'uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4' + $workflow | Should -Match 'uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4' $workflow | Should -Match 'predicate-type: https://github\.com/Penguin-Repository/Pure-Base/attestations/release-authorization/v1' $workflow | Should -Match 'predicate-path: \$\{\{ steps\.release-authorization\.outputs\.predicate_path \}\}' $workflow | Should -Match 'release-authorization\.attestation\.json'