411 lines
19 KiB
PowerShell
411 lines
19 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SessionId,
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$StartedAtUtc,
|
|
[int]$ProbeExitCode = 0
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
function Get-FailureCategory {
|
|
param([object]$Case)
|
|
if (-not $Case.evidenceComplete) { return 'evidence_missing' }
|
|
if (-not $Case.targetChanged) { return 'surface_unreachable' }
|
|
if (-not $Case.nonTargetUnchanged) { return 'surface_cross_write' }
|
|
if (-not $Case.gdiStateRestored) { return 'gdi_state_not_restored' }
|
|
if (-not $Case.wideContentPreservedAfterEasyXFollowUp) {
|
|
return 'measurement_relation_invalid'
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function New-WideTextCases {
|
|
param([object]$Raw, [object[]]$EvidenceFiles)
|
|
$items = New-Object System.Collections.Generic.List[object]
|
|
foreach ($case in @($Raw.result.cases)) {
|
|
$caseFailure = Get-FailureCategory -Case $case
|
|
$caseEvidence = @($EvidenceFiles | Where-Object {
|
|
$_.path -like "evidence/$($case.testCaseId)_*"
|
|
})
|
|
$phases = @('after_draw')
|
|
if ($case.batchState -eq 'inside') { $phases += @('after_flush', 'after_end') }
|
|
foreach ($text in @($case.textTests)) {
|
|
$textFailure = $caseFailure
|
|
if (-not $textFailure -and (-not $text.isValidUtf16 -or
|
|
-not $text.getTextExtentPoint32W -or -not $text.textOutW -or
|
|
[int]$text.drawTextWHeight -le 0)) {
|
|
$textFailure = 'measurement_relation_invalid'
|
|
}
|
|
foreach ($phase in $phases) {
|
|
$fingerprint = switch ($phase) {
|
|
'after_draw' { $case.observations.afterDraw; break }
|
|
'after_flush' { $case.observations.afterFlush; break }
|
|
'after_end' { $case.observations.afterEnd; break }
|
|
}
|
|
$status = if ($textFailure) { 'fail' } else { 'pass' }
|
|
$items.Add([pscustomobject][ordered]@{
|
|
testCaseId = "$($case.testCaseId)/$($text.testStringId)/$phase"
|
|
testStringId = $text.testStringId
|
|
codePoints = @($text.codePoints)
|
|
utf16Units = @($text.utf16Units)
|
|
surfaceId = $case.surfaceId
|
|
batchState = $case.batchState
|
|
observationPhase = $phase
|
|
hdc = $fingerprint.hdc
|
|
hbitmap = $fingerprint.hbitmap
|
|
hfont = $fingerprint.hfont
|
|
bitmapWidth = $fingerprint.bitmapWidth
|
|
bitmapHeight = $fingerprint.bitmapHeight
|
|
planes = $fingerprint.planes
|
|
bitsPerPixel = $fingerprint.bitsPerPixel
|
|
stride = $fingerprint.stride
|
|
dibSection = $fingerprint.dibSection
|
|
logFont = $fingerprint.logFont
|
|
gdiStateBefore = $case.before.gdiState
|
|
gdiStateAfter = $fingerprint.gdiState
|
|
apiResults = [pscustomobject][ordered]@{
|
|
getTextExtentPoint32W = $text.getTextExtentPoint32W
|
|
textOutW = $text.textOutW
|
|
drawTextWHeight = $text.drawTextWHeight
|
|
}
|
|
logicalExtent = $text.logicalExtent
|
|
textOutInkBounds = $text.textOutInkBounds
|
|
drawTextInkBounds = $text.drawTextInkBounds
|
|
targetPixelsBefore = $case.before.pixelDigest
|
|
targetPixelsAfter = $fingerprint.pixelDigest
|
|
controlPixelsBefore = $case.nonTargetBefore.pixelDigest
|
|
controlPixelsAfter = $case.nonTargetAfterDraw.pixelDigest
|
|
easyXFollowUp = [pscustomobject][ordered]@{
|
|
wideContentPreserved = $case.wideContentPreservedAfterEasyXFollowUp
|
|
surfaceFingerprint = $case.observations.afterEasyXFollowUp
|
|
}
|
|
automatedStatus = $status
|
|
manualStatus = 'not_reviewed'
|
|
failureCategory = $textFailure
|
|
evidenceFiles = $caseEvidence
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return $items.ToArray()
|
|
}
|
|
|
|
function New-DpiCoordinateCases {
|
|
param([object]$Raw, [object[]]$EvidenceFiles)
|
|
$failure = $null
|
|
if ($EvidenceFiles.Count -lt @($Raw.evidenceFiles).Count) { $failure = 'evidence_missing' }
|
|
elseif (-not $Raw.result.gdiStateRestored) { $failure = 'gdi_state_not_restored' }
|
|
elseif (-not $Raw.result.roundTripsExact) { $failure = 'coordinate_transform_invalid' }
|
|
elseif (-not $Raw.result.gdiExtentSuccess -or -not $Raw.result.gdiTextSuccess) {
|
|
$failure = 'measurement_relation_invalid'
|
|
}
|
|
$status = if ($failure) { 'fail' } else { 'pass' }
|
|
$decision = if ($Raw.result.implicitScalingObserved) {
|
|
'requires_explicit_transform_validation'
|
|
}
|
|
else {
|
|
'direct_hwnd_relative_coordinates_supported_at_observed_dpi'
|
|
}
|
|
return @([pscustomobject][ordered]@{
|
|
testCaseId = 'dpi-coordinate-current-environment'
|
|
surfaceId = 'main'
|
|
batchState = $Raw.result.batchState
|
|
observationPhase = 'after_end'
|
|
dpiAwareness = [pscustomobject][ordered]@{
|
|
process = $Raw.environment.processAwareness
|
|
thread = $Raw.environment.threadAwareness
|
|
window = $Raw.environment.windowAwareness
|
|
}
|
|
windowDpi = $Raw.environment.dpiForWindow
|
|
clientRect = $Raw.environment.clientRect
|
|
windowScreenRect = $Raw.environment.windowRect
|
|
surfaceSize = [pscustomobject]@{ width = 960; height = 720 }
|
|
surfaceFingerprint = $Raw.result.surfaceFingerprintAfterDraw
|
|
surfaceBounds = $Raw.result.surfaceBounds
|
|
clientBounds = $Raw.result.clientBounds
|
|
coordinateRoundTrips = @($Raw.result.coordinateRoundTrips)
|
|
surfaceMarkerSizesAgree = $Raw.result.surfaceMarkerSizesAgree
|
|
clientMatchesSurface = $Raw.result.clientMatchesSurface
|
|
implicitScalingObserved = $Raw.result.implicitScalingObserved
|
|
coordinateDecision = $decision
|
|
automatedStatus = $status
|
|
manualStatus = 'not_reviewed'
|
|
failureCategory = $failure
|
|
evidenceFiles = $EvidenceFiles
|
|
})
|
|
}
|
|
|
|
function New-ImePositionCases {
|
|
param([object]$Raw, [object[]]$EvidenceFiles)
|
|
$items = New-Object System.Collections.Generic.List[object]
|
|
foreach ($anchor in @($Raw.result.anchors)) {
|
|
$events = @($Raw.result.imeEvents | Where-Object {
|
|
$_.syntheticAnchorId -eq $anchor.syntheticAnchorId -and $_.apiAttempted
|
|
})
|
|
$successful = @($events | Where-Object {
|
|
$_.contextAvailable -and $_.compositionForm.apiResult -and $_.candidateForm.apiResult
|
|
})
|
|
$preconfigureEvents = @($events | Where-Object {
|
|
$_.positionTrigger -eq 'anchor_change' -or $_.stage -eq 'anchor_preconfigure'
|
|
})
|
|
$successfulPreconfigure = @($preconfigureEvents | Where-Object {
|
|
$_.contextAvailable -and $_.compositionForm.apiResult -and $_.candidateForm.apiResult
|
|
})
|
|
$lifecycleEvents = @($events | Where-Object {
|
|
$_.positionTrigger -eq 'ime_lifecycle' -or
|
|
$_.stage -eq 'ime_start' -or $_.stage -eq 'ime_composition'
|
|
})
|
|
$successfulLifecycle = @($lifecycleEvents | Where-Object {
|
|
$_.contextAvailable -and $_.compositionForm.apiResult -and $_.candidateForm.apiResult
|
|
})
|
|
$latest = $events | Select-Object -Last 1
|
|
$failure = if ($successful.Count -gt 0) { $null } else { 'ime_position_invalid' }
|
|
$status = if ($failure) { 'fail' } else { 'pass' }
|
|
$anchorEvidence = @($EvidenceFiles | Where-Object {
|
|
$_.path -like "evidence/$($anchor.syntheticAnchorId)_*" -or
|
|
$_.path -like "manual-evidence/$($anchor.syntheticAnchorId)_*"
|
|
})
|
|
$items.Add([pscustomobject][ordered]@{
|
|
testCaseId = "ime-position/$($anchor.syntheticAnchorId)"
|
|
syntheticAnchorId = $anchor.syntheticAnchorId
|
|
anchorCoordinates = [pscustomobject][ordered]@{
|
|
controlOrigin = $anchor.controlOrigin
|
|
controlLocalCaretTopLeft = $anchor.controlLocalCaretTopLeft
|
|
controlLocalCaretBottom = $anchor.controlLocalCaretBottom
|
|
controlLocalLineRect = $anchor.controlLocalLineRect
|
|
easyxSurfaceCaretTopLeft = $anchor.easyxSurfaceCaretTopLeft
|
|
easyxSurfaceCaretBottom = $anchor.easyxSurfaceCaretBottom
|
|
easyxSurfaceLineRect = $anchor.easyxSurfaceLineRect
|
|
targetHwndRelativePoint = $anchor.targetHwndRelativePoint
|
|
diagnosticScreenPoint = if ($latest) { $latest.diagnosticScreenPoint } else { $null }
|
|
}
|
|
compositionForm = if ($latest) { $latest.compositionForm } else { $null }
|
|
candidateForm = if ($latest) { $latest.candidateForm } else { $null }
|
|
imeId = if ($latest) { $latest.imeDescriptionUtf16 } else { @() }
|
|
imeFileName = if ($latest) { $latest.imeFileNameUtf16 } else { @() }
|
|
hkl = if ($latest) { $latest.hkl } else { $null }
|
|
apiAttemptCount = $events.Count
|
|
successfulApiAttemptCount = $successful.Count
|
|
preconfigureApiAttemptCount = $preconfigureEvents.Count
|
|
successfulPreconfigureApiAttemptCount = $successfulPreconfigure.Count
|
|
lifecycleApiAttemptCount = $lifecycleEvents.Count
|
|
successfulLifecycleApiAttemptCount = $successfulLifecycle.Count
|
|
positionDisposition = 'not_observable'
|
|
automatedStatus = $status
|
|
manualStatus = 'not_reviewed'
|
|
failureCategory = $failure
|
|
evidenceFiles = $anchorEvidence
|
|
})
|
|
}
|
|
return $items.ToArray()
|
|
}
|
|
|
|
$resolved = Resolve-Path -LiteralPath $Path
|
|
$item = Get-Item -LiteralPath $resolved
|
|
$directory = if ($item.PSIsContainer) { $item.FullName } else { $item.DirectoryName }
|
|
$rawPath = if ($item.PSIsContainer) { Join-Path $directory 'raw-result.json' } else { $item.FullName }
|
|
if (-not (Test-Path -LiteralPath $rawPath)) { throw "Raw result not found: $rawPath" }
|
|
|
|
$raw = Get-Content -Raw -LiteralPath $rawPath | ConvertFrom-Json
|
|
if ([int]$raw.rawSchemaVersion -ne 1) { throw "Unsupported raw schema: $($raw.rawSchemaVersion)" }
|
|
if ($raw.mode -notin @('wide-text', 'dpi-coordinate', 'ime-position')) {
|
|
throw "Unsupported probe mode: $($raw.mode)"
|
|
}
|
|
|
|
$evidence = New-Object System.Collections.Generic.List[object]
|
|
foreach ($entry in @($raw.evidenceFiles)) {
|
|
$relativePath = [string]$entry.path
|
|
$nativeRelative = $relativePath.Replace('/', [IO.Path]::DirectorySeparatorChar)
|
|
$fullPath = Join-Path $directory $nativeRelative
|
|
if (-not (Test-Path -LiteralPath $fullPath)) { continue }
|
|
$file = Get-Item -LiteralPath $fullPath
|
|
$evidence.Add([pscustomobject][ordered]@{
|
|
path = $relativePath
|
|
purpose = $entry.purpose
|
|
captureBackend = $entry.captureBackend
|
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $file.FullName).Hash
|
|
byteLength = $file.Length
|
|
crop = $null
|
|
})
|
|
}
|
|
$rawEvidencePresent = $evidence.Count
|
|
$manualEvidenceDirectory = Join-Path $directory 'manual-evidence'
|
|
if (Test-Path -LiteralPath $manualEvidenceDirectory) {
|
|
Get-ChildItem -LiteralPath $manualEvidenceDirectory -Filter '*.png' -File | ForEach-Object {
|
|
$evidence.Add([pscustomobject][ordered]@{
|
|
path = 'manual-evidence/' + $_.Name
|
|
purpose = 'manually cropped IME position evidence'
|
|
captureBackend = 'manual_crop'
|
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash
|
|
byteLength = $_.Length
|
|
crop = 'user_supplied'
|
|
})
|
|
}
|
|
}
|
|
|
|
$evidenceArray = $evidence.ToArray()
|
|
$testCases = if ($raw.mode -eq 'wide-text') {
|
|
New-WideTextCases -Raw $raw -EvidenceFiles $evidenceArray
|
|
}
|
|
elseif ($raw.mode -eq 'dpi-coordinate') {
|
|
New-DpiCoordinateCases -Raw $raw -EvidenceFiles $evidenceArray
|
|
}
|
|
else {
|
|
New-ImePositionCases -Raw $raw -EvidenceFiles $evidenceArray
|
|
}
|
|
|
|
$overallManualStatus = 'not_reviewed'
|
|
$sourceManualReview = $null
|
|
$manualReviewSummary = $null
|
|
$manualReviewPath = Join-Path $directory 'manual-review-v1.json'
|
|
if (Test-Path -LiteralPath $manualReviewPath) {
|
|
$manualReview = Get-Content -Raw -LiteralPath $manualReviewPath | ConvertFrom-Json
|
|
if ([int]$manualReview.manualReviewSchemaVersion -ne 1) {
|
|
throw "Unsupported manual review schema: $($manualReview.manualReviewSchemaVersion)"
|
|
}
|
|
if ([string]$manualReview.mode -ne [string]$raw.mode) {
|
|
throw "Manual review mode does not match raw result: $($manualReview.mode)"
|
|
}
|
|
$allowedManualStatuses = @('pass', 'fail', 'inconclusive', 'not_reviewed', 'not_available')
|
|
$allowedPositionDispositions = @('as_requested', 'system_adjusted', 'not_observable')
|
|
$reviewedIds = New-Object System.Collections.Generic.List[string]
|
|
foreach ($reviewCase in @($manualReview.testCases)) {
|
|
$testCaseId = [string]$reviewCase.testCaseId
|
|
if ($reviewedIds.Contains($testCaseId)) { throw "Duplicate manual review case: $testCaseId" }
|
|
$reviewedIds.Add($testCaseId)
|
|
$matchingCases = @($testCases | Where-Object testCaseId -eq $testCaseId)
|
|
if ($matchingCases.Count -ne 1) { throw "Unknown manual review case: $testCaseId" }
|
|
$manualStatus = [string]$reviewCase.manualStatus
|
|
$positionDisposition = [string]$reviewCase.positionDisposition
|
|
if ($manualStatus -notin $allowedManualStatuses) {
|
|
throw "Unsupported manual status for ${testCaseId}: $manualStatus"
|
|
}
|
|
if ($positionDisposition -notin $allowedPositionDispositions) {
|
|
throw "Unsupported position disposition for ${testCaseId}: $positionDisposition"
|
|
}
|
|
$targetCase = $matchingCases[0]
|
|
if ($manualStatus -eq 'pass' -and $targetCase.automatedStatus -ne 'pass') {
|
|
throw "Manual pass cannot override automated failure: $testCaseId"
|
|
}
|
|
$reviewEvidence = @($reviewCase.evidenceFiles)
|
|
if ($manualStatus -eq 'pass' -and $reviewEvidence.Count -eq 0) {
|
|
throw "Manual pass requires explicit evidence files: $testCaseId"
|
|
}
|
|
foreach ($relativePath in $reviewEvidence) {
|
|
$relative = [string]$relativePath
|
|
if ($relative -notlike 'manual-evidence/*') {
|
|
throw "Manual review evidence must be under manual-evidence: $relative"
|
|
}
|
|
$matchingEvidence = @($targetCase.evidenceFiles | Where-Object path -eq $relative)
|
|
if ($matchingEvidence.Count -ne 1) {
|
|
throw "Manual review evidence is missing or belongs to another case: $relative"
|
|
}
|
|
}
|
|
if ($manualStatus -eq 'pass' -and $positionDisposition -eq 'not_observable') {
|
|
throw "Manual pass requires an observable position disposition: $testCaseId"
|
|
}
|
|
$targetCase.manualStatus = $manualStatus
|
|
$targetCase.positionDisposition = $positionDisposition
|
|
$targetCase | Add-Member -NotePropertyName manualReviewNotes `
|
|
-NotePropertyValue ([string]$reviewCase.notes) -Force
|
|
}
|
|
if ($reviewedIds.Count -ne @($testCases).Count) {
|
|
throw 'Manual review must cover every manifest test case'
|
|
}
|
|
$caseManualStatuses = @($testCases | ForEach-Object { $_.manualStatus })
|
|
$derivedManualStatus = if ($caseManualStatuses -contains 'fail') {
|
|
'fail'
|
|
}
|
|
elseif ($caseManualStatuses -contains 'inconclusive') {
|
|
'inconclusive'
|
|
}
|
|
elseif ($caseManualStatuses -contains 'not_reviewed') {
|
|
'not_reviewed'
|
|
}
|
|
elseif ($caseManualStatuses -contains 'not_available') {
|
|
if (@($caseManualStatuses | Where-Object { $_ -ne 'not_available' }).Count -eq 0) {
|
|
'not_available'
|
|
}
|
|
else {
|
|
'inconclusive'
|
|
}
|
|
}
|
|
else {
|
|
'pass'
|
|
}
|
|
if ([string]$manualReview.overallStatus -ne $derivedManualStatus) {
|
|
throw "Manual review overall status mismatch: expected $derivedManualStatus"
|
|
}
|
|
$overallManualStatus = $derivedManualStatus
|
|
$sourceManualReview = [pscustomobject][ordered]@{
|
|
path = 'manual-review-v1.json'
|
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $manualReviewPath).Hash
|
|
}
|
|
$manualReviewSummary = [pscustomobject][ordered]@{
|
|
reviewedAtUtc = $manualReview.reviewedAtUtc
|
|
reviewer = $manualReview.reviewer
|
|
expectedImeName = $manualReview.expectedImeName
|
|
actualImeName = $manualReview.actualImeName
|
|
identitySource = $manualReview.identitySource
|
|
notes = $manualReview.notes
|
|
}
|
|
}
|
|
|
|
$failedCases = @($testCases | Where-Object automatedStatus -eq 'fail')
|
|
$allEvidencePresent = $rawEvidencePresent -eq @($raw.evidenceFiles).Count
|
|
$overallFailure = $null
|
|
if (-not $allEvidencePresent) { $overallFailure = 'evidence_missing' }
|
|
elseif ($failedCases.Count -gt 0) { $overallFailure = $failedCases[0].failureCategory }
|
|
elseif (-not $raw.completed -or $ProbeExitCode -ne 0) { $overallFailure = 'inconclusive_probe_exit' }
|
|
$overallStatus = if ($overallFailure) { 'fail' } else { 'pass' }
|
|
|
|
$manifest = [pscustomobject][ordered]@{
|
|
visualProbeSchemaVersion = 1
|
|
sessionId = $SessionId
|
|
startedAtUtc = $StartedAtUtc
|
|
completedAtUtc = [DateTime]::UtcNow.ToString('o')
|
|
mode = $raw.mode
|
|
processId = $raw.environment.processId
|
|
threadId = $raw.environment.threadId
|
|
osBuild = $raw.environment.osBuild
|
|
easyxVersion = $raw.environment.easyxVersion
|
|
probeExecutableExitCode = $ProbeExitCode
|
|
dpiConfiguration = $raw.dpiConfiguration
|
|
environment = $raw.environment
|
|
automatedStatus = $overallStatus
|
|
manualStatus = $overallManualStatus
|
|
failureCategory = $overallFailure
|
|
sourceRawResult = [pscustomobject][ordered]@{
|
|
path = 'raw-result.json'
|
|
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $rawPath).Hash
|
|
}
|
|
sourceManualReview = $sourceManualReview
|
|
manualReview = $manualReviewSummary
|
|
evidenceFiles = $evidenceArray
|
|
testCases = @($testCases)
|
|
}
|
|
|
|
$manifestPath = Join-Path $directory 'manifest-v1.json'
|
|
$manifestJson = $manifest | ConvertTo-Json -Depth 100
|
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
[IO.File]::WriteAllText($manifestPath, $manifestJson + [Environment]::NewLine, $utf8Bom)
|
|
$manifestHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $manifestPath).Hash
|
|
$sidecarPath = $manifestPath + '.sha256'
|
|
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
[IO.File]::WriteAllText($sidecarPath, "$manifestHash *manifest-v1.json`r`n", $utf8NoBom)
|
|
|
|
[pscustomobject][ordered]@{
|
|
mode = $raw.mode
|
|
manifestPath = $manifestPath
|
|
manifestSha256 = $manifestHash
|
|
automatedStatus = $overallStatus
|
|
manualStatus = $overallManualStatus
|
|
testCaseCount = @($testCases).Count
|
|
evidenceCount = $evidence.Count
|
|
dpiForWindow = $raw.environment.dpiForWindow
|
|
windowAwareness = $raw.environment.windowAwareness
|
|
}
|