Public/ReleaseNotes/PrepareExportData/ConvertTo-ExportDataWorkItemsProcessTestsRelations.ps1

function ConvertTo-ExportDataWorkItemsProcessTestsRelations {

    <#
        .SYNOPSIS
            Converts set of ReleaseNotesDataItems to ExportData - WorkItems subset.

            Evaluates the relations of the given work item and returns distinct
            work item states from all work items that are tested by the given work item.

        .PARAMETER Items
            Hashtable of ReleaseNotesDataItems.

        .PARAMETER Item
            The current Item being processed.
    #>


    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseSingularNouns', '',
        Justification = ''
    )]
    [OutputType([string])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 1)]
        [hashtable] $Items,

        [PSTypeNameAttribute('PSTypeNames.AzureDevOpsApi.ReleaseNotesDataItem')]
        [Parameter(Mandatory, ValueFromPipeline)]
        $Item
    )

    process {

        # determine, whether given item is a test item
        # if not, return N/A equivalent
        if (-not (Test-TestWorkItem -WorkItem $Item.WorkItem)) {
            return $null
        }

        # otherwise try to inspect the Tests relations

        $collectedStates = [System.Collections.Generic.HashSet[string]]::new()
        $stack = [System.Collections.Generic.Stack[object]]::new()
        $visited = [System.Collections.Generic.HashSet[object]]::new()
        $null = $stack.Push($Item)
        $null = $visited.Add($Item)

        while ($stack.Count -gt 0) {

            $current = $stack.Pop()

            # determine, whether current item is a test item
            $isTestWorkItem = Test-TestWorkItem -WorkItem $current.WorkItem

            # if not a Test work item, we followed the Tests relations to actual work item
            # to be tested, just return the item state
            if (-not $isTestWorkItem) {
                $null = $collectedStates.Add($current.WorkItem.fields.'System.State')
                continue;
            }

            # otherwise inspect the Tests relations

            # retrieve the target urls of Tests relations;
            $testsTargetsUrls = $current.RelationsList `
            | Where-Object { $_.Name -ieq 'Tests' } `
            | Select-Object -ExpandProperty 'Relations'

            # check whether there are any
            if (!$testsTargetsUrls) {
                # if not, nothing to do
                continue
            }

            # iterate over all related tested items,
            # following the relations recursively to the real tested items
            # (i.e. transparently interpret test requirements as test cases for this purpose)
            foreach ($testsTargetUrl in $testsTargetsUrls) {
                # 'recursivelly' call on all related items
                $testsTargetItem = $Items[[string] $testsTargetUrl]
                # only push the item, if it is not already visited;
                # protection against infinite loops
                if ($testsTargetItem -and -not $visited.Contains($testsTargetItem)) {
                    $null = $stack.Push($testsTargetItem)
                    $null = $visited.Add($testsTargetItem)
                }
            }
        }

        # evaluate and prepare result
        $result = @()
        if ($collectedStates.Contains('Proposed')) {
            $result += 'Proposed'
        }
        if ($collectedStates.Contains('Active')) {
            $result += 'Active'
        }
        if ($collectedStates.Contains('Resolved')) {
            $result += 'Resolved'
        }
        if ($collectedStates.Contains('Closed')) {
            $result += 'Closed'
        }

        # return the result
        $result -join ', '
    }
}