Private/Collect/Invoke-TcCollector.ps1

function Invoke-TcCollector {
    <#
    .SYNOPSIS
    Runs one collector, in isolation, and records its coverage.
 
    .DESCRIPTION
    A missing module or a refused access ends up in the coverage with what to do about it, and the
    collection continues. A collector that did not collect leaves no empty table behind: an empty table
    means the service is not used, so it must never stand for a table that could not be read. A collector
    cut by a row limit ends up Partial and says so.
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    param(
        [Parameter(Mandatory)][hashtable]$Collector,
        [Parameter(Mandatory)][string]$Role,
        [int]$Rank = 1,
        [int]$Total = 1
    )

    $id = $Collector['Id']
    $service = $Collector['Service']
    $label = (Get-TcServiceInfo $service)['Label']
    Write-TcStepStart -Rank $Rank -Total $Total -Id $id -Title $Collector['Title'] -Purpose "$($Collector['Workload']) - $label"

    $state = [ordered]@{
        id       = $id
        tables   = [System.Collections.Generic.List[string]]::new()
        warnings = [System.Collections.Generic.List[string]]::new()
        classes  = [System.Collections.Generic.List[string]]::new()
        notes    = [System.Collections.Generic.List[string]]::new()
        limits   = [System.Collections.Generic.List[string]]::new()
    }
    $script:TcCurrentCollector = $state
    $status = 'OK'
    $message = ''
    $remedy = ''
    $connected = $false
    $timer = [System.Diagnostics.Stopwatch]::StartNew()

    try {
        $module = Test-TcServiceModule $service
        $blocked = Get-TcBlockedRequirement -Collector $Collector
        if (-not $module['ok']) {
            $status = $module['status']
            $message = $module['message']
            $remedy = $module['remedy']
        }
        elseif ($blocked) {
            $status = $blocked['status']
            $message = $blocked['message']
            $remedy = $blocked['remedy']
        }
        else {
            Connect-TcService -Service $service -Role $Role
            $connected = $true
            & $Collector['Collect'] $Role

            if ($state['warnings'].Count) {
                $rows = (@($state['tables'] | ForEach-Object { @(Get-TcTable $_).Count }) | Measure-Object -Sum).Sum
                $first = $state['classes'][0]
                $status = if ($rows -gt 0) { 'Partial' } elseif ($first -eq 'Not configured') { 'Insufficient access' } else { $first }
                $message = (@($state['warnings']) | Select-Object -First 4) -join ' - '
                $remedy = Get-TcRemedy -Service $service -Class ($state['classes'] | Where-Object { $_ -ne 'Failed' } | Select-Object -First 1) -Role $Role
            }

            if ($state['limits'].Count) {
                $message = (@($message) + @($state['limits']) | Where-Object { $_ }) -join ' - '
                if ($status -eq 'OK') {
                    $status = 'Partial'
                    $remedy = 'The limit protects the run on a large tenant. Review by hand what is beyond it.'
                }
            }
        }
    }
    catch {
        $text = Get-TcErrorText -ErrorInput $_
        $class = Get-TcErrorClass -Text $text
        # A service that could not be reached was not read at all: that is an access gap, not an empty result.
        if (-not $connected -and $class -eq 'Failed') { $class = 'Insufficient access' }
        $status = if ($class -eq 'Not configured') { 'Insufficient access' } else { $class }
        $message = $text -replace '^NOTCONFIGURED\s*:\s*', ''
        $remedy = Get-TcRemedy -Service $service -Class $class -Role $Role
        Write-TcLog -Message "Collector ${id}: $status - $text" -Level WARN -Step $id
    }

    $timer.Stop()
    $seconds = [math]::Round($timer.Elapsed.TotalSeconds, 1)

    if ($status -notin 'OK', 'Partial') {
        foreach ($table in @($state['tables'])) {
            if (@(Get-TcTable $table).Count -eq 0) {
                $script:TcCurrentTenant['tables'].Remove($table)
                [void]$state['tables'].Remove($table)
            }
        }
    }

    $rows = [int](@($state['tables'] | ForEach-Object { @(Get-TcTable $_).Count }) | Measure-Object -Sum).Sum
    $notes = @($state['notes']) -join ' - '
    if ($notes) { $message = if ($message) { "$message - $notes" } else { $notes } }
    $message = Protect-TcSecret -InputObject $message

    switch ($status) {
        'OK' {
            Write-TcStepLine -Status OK -Text ('{0} tables, {1} rows' -f $state['tables'].Count, $rows) -Step $id
            Write-TcStepEnd -Status Done -Seconds $seconds
        }
        'Partial' {
            Write-TcStepLine -Status Warning -Text ('Partial collection: {0} rows - {1}' -f $rows, $message) -Step $id
            Write-TcStepLine -Status Info -Text $remedy -Step $id
            Write-TcStepEnd -Status Warning -Seconds $seconds
        }
        default {
            Write-TcStepLine -Status Warning -Text ('{0}: {1}' -f $status, $message) -Step $id
            Write-TcStepLine -Status Info -Text $remedy -Step $id
            Write-TcStepEnd -Status Skipped -Seconds $seconds -Detail $status
        }
    }

    $line = [ordered]@{
        role     = $Role
        tenant   = $script:TcCurrentTenant['tenant']
        id       = $id
        workload = $Collector['Workload']
        title    = $Collector['Title']
        service  = $label
        status   = $status
        message  = $message
        remedy   = $remedy
        duration = $seconds
        rows     = $rows
        tables   = @($state['tables'])
    }
    $script:TcCurrentTenant['coverage'].Add($line)
    $script:TcCurrentCollector = $null
    $line
}