Public/Test-HDTApplicationDetection.ps1

function Test-HDTApplicationDetection {
    <#
        .SYNOPSIS
            Answers whether an application's detection rule says it is already
            installed.
 
        .DESCRIPTION
            DESIGN 8's detection, and the reason an HDT sequence can be re-run
            over a machine without reinstalling everything on it - MDT has no
            first-class detection, so its reruns do.
 
            It runs entirely against injected services, so every rule type is
            provable under Pester with no registry, no disk and no installed
            software.
 
            NO RULE MEANS NOT DETECTED, WHICH MEANS INSTALL. A $null rule returns
            $false without asking any service anything. That is not a degenerate
            case: DESIGN 8 makes detect: optional, and an application that
            declares none installs every time the step reaches it. The engine
            never infers a rule, because a guessed rule that reports an
            application installed when it is not silently skips work the sequence
            asked for - which is worse than installing something twice.
 
            THE FOUR RULES
 
              msiProduct the product code is a key under
                          CurrentVersion\Uninstall, in the native view OR in
                          WOW6432Node. Both, because a 32-bit MSI on a 64-bit
                          machine registers only under the second one and an
                          engine that looked at one view would reinstall it every
                          deployment. NOT Win32_Product: querying that class
                          triggers a consistency check that can silently
                          reconfigure every MSI on the machine, which is a real
                          way to break a build.
 
              file the file is there, and - if the rule states a version -
                          the file's version is at least that one. THE VERSION IS
                          THE HALF THAT MATTERS: a path test alone reports
                          "installed" for a stale build the sequence exists to
                          replace. A rule stating no version does not read one.
 
              registry the key exists; if the rule names a value, that value
                          is present; if it also states data, the data matches.
                          Each level is a narrowing, so "the key exists" is a
                          legitimate whole rule.
 
              script the script's output is truthy. A SCRIPT THAT THROWS
                          REPORTS NOT INSTALLED rather than failing the
                          deployment: it was asked "is this here?" and could not
                          answer, which is the same outcome for the sequence as
                          answering no, and is not a reason to scrap an otherwise
                          good build.
 
            A RULE WHOSE SERVICE WAS NOT SUPPLIED THROWS. That is a wiring
            mistake in the engine rather than an application that is absent, and
            reporting it as "not installed" would reinstall the application on
            every single deployment while looking like it worked.
 
        .PARAMETER Detect
            The projected detection rule, as Get-HDTApplication returns it on an
            application's Detect property. $null - the application declares none -
            returns $false.
 
        .PARAMETER FileSystem
            An IFileSystem. Required by the file rule.
 
        .PARAMETER Registry
            An IRegistryService. Required by the msiProduct and registry rules.
 
        .PARAMETER ScriptInvoker
            An IScriptInvoker. Required by the script rule.
 
        .PARAMETER Variable
            The variable scope. Tokens in a path or a key are expanded through it,
            and it is what a detection script is handed.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Boolean. $true when the application is already installed.
 
        .EXAMPLE
            Test-HDTApplicationDetection -Detect $app.Detect -Registry $registry
 
        .EXAMPLE
            if (-not (Test-HDTApplicationDetection -Detect $app.Detect -FileSystem $fs -Variable $context.Variable)) {
                # install it
            }
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowNull()]
        [object] $Detect,

        [Parameter()]
        [AllowNull()]
        [object] $FileSystem = $null,

        [Parameter()]
        [AllowNull()]
        [object] $Registry = $null,

        [Parameter()]
        [AllowNull()]
        [object] $ScriptInvoker = $null,

        [Parameter()]
        [AllowNull()]
        [System.Collections.IDictionary] $Variable = $null
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if ($null -eq $Detect) { return $false }

    # A property the rule did not declare reads as empty rather than throwing
    # under Set-StrictMode, so a hand-built rule object behaves like a projected
    # one.
    $read = {
        param($Name)

        if ($null -eq $Detect.PSObject.Properties[$Name]) { return '' }
        return [string] $Detect.$Name
    }

    $expand = {
        param([string] $Text)

        if ($null -eq $Variable) { return $Text }
        return [string] (Expand-HDTVariableToken -Value $Text -Scope $Variable)
    }

    $require = {
        param($Service, [string] $ServiceName, [string] $RuleType)

        if ($null -eq $Service) {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord `
                        -Message ("a {0} detection rule needs the {1} service, and none was supplied. This is a wiring mistake in the engine: reporting it as 'not installed' would reinstall the application on every deployment while looking like it worked." -f $RuleType, $ServiceName)))
        }
    }

    $type = & $read 'Type'

    switch ($type) {

        'msiProduct' {
            & $require $Registry 'Registry' 'msiProduct'

            $productCode = & $read 'ProductCode'

            # Native view first, then the 32-bit one. A 32-bit MSI on a 64-bit
            # machine appears only in the second.
            $uninstallRoot = @(
                'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
                'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
            )

            foreach ($root in $uninstallRoot) {
                if ($Registry.TestPath(('{0}\{1}' -f $root, $productCode))) { return $true }
            }

            return $false
        }

        'file' {
            & $require $FileSystem 'FileSystem' 'file'

            $path = & $expand (& $read 'Path')

            if (-not $FileSystem.TestPath($path)) { return $false }

            $required = & $read 'Version'
            if ([string]::IsNullOrWhiteSpace($required)) { return $true }

            return ([version] $FileSystem.GetVersion($path) -ge [version] $required)
        }

        'registry' {
            & $require $Registry 'Registry' 'registry'

            $key = & $expand (& $read 'Key')

            if (-not $Registry.TestPath($key)) { return $false }

            $valueName = & $read 'Value'
            if ([string]::IsNullOrWhiteSpace($valueName)) { return $true }

            $data = $Registry.GetValue($key, $valueName)
            if ($null -eq $data) { return $false }

            $expected = & $read 'Data'
            if ([string]::IsNullOrWhiteSpace($expected)) { return $true }

            return ([string] $data -eq $expected)
        }

        'script' {
            & $require $ScriptInvoker 'ScriptInvoker' 'script'

            $scriptPath = & $read 'Path'

            $scope = $Variable
            if ($null -eq $scope) { $scope = @{} }

            try {
                $result = $ScriptInvoker.Invoke($scriptPath, $scope)
            } catch {
                # It was asked whether the application is here and could not
                # answer. That is not a reason to scrap the build.
                return $false
            }

            return [bool] $result
        }

        default {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord `
                        -Message ("'{0}' is not a detection rule HDT can run. The types are msiProduct, file, registry, script." -f $type)))
        }
    }
}