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..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' {
@@ -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'),
@@ -133,10 +133,11 @@ 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'
([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/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/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/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."
+ );
}
}
}
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'."