Assert-SameFile.ps1

<#PSScriptInfo
.VERSION 1.2.1
.AUTHOR Roman Kuzmin
.COPYRIGHT (c) Roman Kuzmin
.TAGS Test
.GUID 1707aec3-6f77-41bd-8df4-953c7704f4a6
.LICENSEURI http://www.apache.org/licenses/LICENSE-2.0
.PROJECTURI https://github.com/nightroman/PowerShelf
#>


<#
.Synopsis
    Compares the sample and result files or texts.
 
.Description
    This script automates one typical test scenario, it compares the sample and
    result files and performs copy and view operations if the sample is missing
    (nor yet created) or the result is different (potentially valid but changed
    so that the sample may have to be updated after review).
 
    If the result is missing then the test fails. If the sample is missing then
    a warning is written and the sample is created as a copy of the result. The
    target directory is also created if it does not exist.
 
    If files are different then the test either fails or, if View is specified
    and Fail is not, invokes View, writes a warning, and then prompts to update
    the sample file.
 
    File comparison is done via MD5 hashes, it is fast and suitable for large
    files. But there is a tiny chance that file differences are not detected.
 
.Parameter Sample
        Specifies the sample file path. If it does not exist then it is
        created as a copy of the result.
 
.Parameter Result
        Specifies the result file path. The file must exist.
 
.Parameter View
        Specifies a command invoked when the files are different. It is an
        application name or a script block. The arguments are file paths.
 
.Parameter Fail
        Tells to fail on differences even when View is specified.
 
.Parameter Text
        Tells that the sample and result are strings to compare as text
        ignoring line endings. If they differ and View is set then View
        uses temp files Sample.txt and Result.txt with saved texts.
 
.Example
    Assert-SameFile Sample.log Result.log Merge.exe
 
    This command compares Sample.log and Result.log at the current location and
    uses Merge.exe for viewing differences (Merge.exe and file paths are passed
    in Start-Process).
 
.Example
    Assert-SameFile Sample.log Result.log {git diff --no-index $args[0] $args[1]}
 
    This command uses git in order to view changes. git requires more arguments
    than Merge.exe above, so that the proper script block is used as a command.
 
.Link
    https://github.com/nightroman/PowerShelf
#>


[CmdletBinding()]
param(
    [string]$Sample
    ,
    [string]$Result
    ,
    [object]$View
    ,
    [switch]$Fail
    ,
    [switch]$Text
)

$ErrorActionPreference = 1

if ($View -and !($View -is [string] -or $View -is [scriptblock])) {
    Write-Error "Invalid view command: '$View'."
}

function Start-View([string]$A, [string]$B) {
    if ($View -is [string]) {
        Start-Process $View ('"{0}" "{1}"' -f $A, $B)
    }
    else {
        & $View $A $B
    }
}

# compare text lines
if ($Text) {
    # make and compare strings
    $regex = [regex]'\r\n?'
    $text1 = ($Sample.TrimEnd() -replace $regex, "`n") + "`n"
    $text2 = ($Result.TrimEnd() -replace $regex, "`n") + "`n"
    if ([string]::Equals($text1, $text2)) {
        return
    }

    if (!$View) {
        Write-Error "Different sample and result text."
    }

    # use cyclic suffix 0-9 to reduce collisions
    $n = $env:AssertSameFile
    $n = if ($n -match '^\d$') {([int]$n + 1) % 10} else {0}
    $env:AssertSameFile = $n

    # write temp files
    $Sample = Join-Path $env:TEMP "Sample-$n.txt"
    $Result = Join-Path $env:TEMP "Result-$n.txt"
    [System.IO.File]::WriteAllText($Sample, $text1)
    [System.IO.File]::WriteAllText($Result, $text2)

    # show
    Start-View $Sample $Result

    if ($Fail) {
        Write-Error "Different sample and result text."
    }

    Write-Warning "Different sample and result text."
    return
}

# result must exist
$fileResult = [System.IO.FileInfo]$PSCmdlet.GetUnresolvedProviderPathFromPSPath($Result)
if (!$fileResult.Exists) {
    Write-Error "Missing result file '$Result'."
}

# make missing sample
$fileSample = [System.IO.FileInfo]$PSCmdlet.GetUnresolvedProviderPathFromPSPath($Sample)
if (!$fileSample.Exists) {
    $null = [System.IO.Directory]::CreateDirectory($fileSample.DirectoryName)
    Copy-Item -LiteralPath $fileResult.FullName -Destination $fileSample.FullName -Force
    Write-Warning "Created missing sample file '$Sample'."
    return
}

# compare
$same = $fileResult.Length -eq $fileSample.Length
if ($same) {
    $md5 = [System.Security.Cryptography.MD5]::Create()
    $reader = $fileSample.OpenRead()
    try {
        $1 = [Guid]$md5.ComputeHash($reader)
        $reader.Close()
        $2 = [Guid]$md5.ComputeHash(($reader = $fileResult.OpenRead()))
        $same = $1 -eq $2
    }
    finally {
        $reader.Close()
    }
}

# pass
if ($same) {
    return
}

# fail
if (!$View) {
    Write-Error "Different sample '$Sample' and result '$Result'."
}

# show
Start-View $fileSample.FullName $fileResult.FullName

if ($Fail) {
    Write-Error "Different sample '$Sample' and result '$Result'."
}

Write-Warning "Different sample '$Sample' and result '$Result'."

# choice, cast is for v2.0
function Get-Choice($Caption, $Message, $Choices) {
    $Host.UI.PromptForChoice($Caption, $Message, [System.Management.Automation.Host.ChoiceDescription[]]$Choices, 0)
}
function New-Choice {
    New-Object System.Management.Automation.Host.ChoiceDescription $args
}

# prompt
switch(Get-Choice 'Different result' 'How would you like to proceed?' @(
    New-Choice '&0. Ignore' 'Do nothing.'
    New-Choice '&1. Update' 'Copy result to sample.'
    New-Choice '&2. Abort' 'Write terminating error.'
))
{
    1 {
        Copy-Item -LiteralPath $fileResult.FullName -Destination $fileSample.FullName -Force
    }
    2 {
        Write-Error "Different sample '$Sample' and result '$Result'."
    }
}