diff --git a/.github/workflows/powershell-tests.yml b/.github/workflows/powershell-tests.yml new file mode 100644 index 000000000..3cf950d48 --- /dev/null +++ b/.github/workflows/powershell-tests.yml @@ -0,0 +1,25 @@ +# CI Validation + +name: PowerShell Tests + +on: + pull_request: + branches: [ main ] + paths: + - 'helpers/software-report-base/**' + +jobs: + powershell-tests: + name: PowerShell tests + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Run Software Report module tests + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + Invoke-Pester -Output Detailed "helpers/software-report-base/tests" + \ No newline at end of file diff --git a/helpers/software-report-base/Calculate-ImagesDiff.ps1 b/helpers/software-report-base/Calculate-ImagesDifference.ps1 similarity index 62% rename from helpers/software-report-base/Calculate-ImagesDiff.ps1 rename to helpers/software-report-base/Calculate-ImagesDifference.ps1 index 91a05c86b..153621709 100644 --- a/helpers/software-report-base/Calculate-ImagesDiff.ps1 +++ b/helpers/software-report-base/Calculate-ImagesDifference.ps1 @@ -1,5 +1,5 @@ using module ./SoftwareReport.psm1 -using module ./SoftwareReport.Comparer.psm1 +using module ./SoftwareReport.DifferenceCalculator.psm1 <# .SYNOPSIS @@ -10,6 +10,10 @@ using module ./SoftwareReport.Comparer.psm1 Path to the current software report. .PARAMETER OutputFile Path to the file where the difference will be saved. +.PARAMETER ReleaseBranchName + Name of the release branch to build image docs URL. +.PARAMETER ReadmePath + Path to the README file in repository to build image docs URL. #> Param ( @@ -20,7 +24,9 @@ Param ( [Parameter(Mandatory=$true)] [string] $OutputFile, [Parameter(Mandatory=$false)] - [string] $ImageDocsUrl + [string] $ReleaseBranchName, + [Parameter(Mandatory=$false)] + [string] $ReadmePath ) $ErrorActionPreference = "Stop" @@ -44,12 +50,14 @@ function Read-SoftwareReport { $previousReport = Read-SoftwareReport -JsonReportPath $PreviousJsonReportPath $currentReport = Read-SoftwareReport -JsonReportPath $CurrentJsonReportPath -$comparer = [SoftwareReportComparer]::new($previousReport, $currentReport) +$comparer = [SoftwareReportDifferenceCalculator]::new($previousReport, $currentReport) $comparer.CompareReports() $diff = $comparer.GetMarkdownReport() -if ($ImageDocsUrl) { - $diff += "`n`n`n For comprehensive list of software installed on this image please click [here]($ImageDocsUrl)." +if ($ReleaseBranch -and $ReadmePath) { + # https://github.com/actions/runner-images/blob/releases/macOS-12/20221215/images/macos/macos-12-Readme.md + $ImageDocsUrl = "https://github.com/actions/runner-images/blob/${ReleaseBranchName}/${ReadmePath}" + $diff += "`n`n`nFor comprehensive list of software installed on this image please click [here]($ImageDocsUrl)." } $diff | Out-File -Path $OutputFile -Encoding utf8NoBOM diff --git a/helpers/software-report-base/SoftwareReport.BaseNodes.psm1 b/helpers/software-report-base/SoftwareReport.BaseNodes.psm1 index db7916ae9..8bfb01c98 100644 --- a/helpers/software-report-base/SoftwareReport.BaseNodes.psm1 +++ b/helpers/software-report-base/SoftwareReport.BaseNodes.psm1 @@ -8,6 +8,14 @@ class BaseNode { return $false } + [String] ToMarkdown() { + return $this.ToMarkdown(1) + } + + [String] ToMarkdown([Int32] $Level) { + throw "Abtract method 'ToMarkdown(level)' is not implemented for '$($this.GetType().Name)'" + } + [Boolean] IsSimilarTo([BaseNode] $OtherNode) { throw "Abtract method 'IsSimilarTo' is not implemented for '$($this.GetType().Name)'" } @@ -19,6 +27,7 @@ class BaseNode { # Abstract base class for all nodes that describe a tool and should be rendered inside diff table class BaseToolNode: BaseNode { + [ValidateNotNullOrEmpty()] [String] $ToolName BaseToolNode([String] $ToolName) { diff --git a/helpers/software-report-base/SoftwareReport.Comparer.psm1 b/helpers/software-report-base/SoftwareReport.Comparer.psm1 deleted file mode 100644 index bcce515c3..000000000 --- a/helpers/software-report-base/SoftwareReport.Comparer.psm1 +++ /dev/null @@ -1,342 +0,0 @@ -using module ./SoftwareReport.psm1 -using module ./SoftwareReport.BaseNodes.psm1 -using module ./SoftwareReport.Nodes.psm1 - - -# SoftwareReportComparer is used to calculate differences between two SoftwareReport objects -class SoftwareReportComparer { - hidden [SoftwareReport] $PreviousReport - hidden [SoftwareReport] $CurrentReport - - hidden [Collections.Generic.List[ReportDifferenceItem]] $AddedItems - hidden [Collections.Generic.List[ReportDifferenceItem]] $ChangedItems - hidden [Collections.Generic.List[ReportDifferenceItem]] $DeletedItems - - SoftwareReportComparer([SoftwareReport] $PreviousReport, [SoftwareReport] $CurrentReport) { - $this.PreviousReport = $PreviousReport - $this.CurrentReport = $CurrentReport - } - - [void] CompareReports() { - $this.AddedItems = @() - $this.ChangedItems = @() - $this.DeletedItems = @() - - $this.CompareInternal($this.PreviousReport.Root, $this.CurrentReport.Root, @()) - } - - hidden [void] CompareInternal([HeaderNode] $previousReportPointer, [HeaderNode] $currentReportPointer, [Array] $Headers) { - $currentReportPointer.Children ?? @() | Where-Object { $_.ShouldBeIncludedToDiff() -and $this.FilterExcludedNodes($_) } | ForEach-Object { - $currentReportNode = $_ - $sameNodeInPreviousReport = $previousReportPointer ? $previousReportPointer.FindSimilarChildNode($currentReportNode) : $null - - if ($currentReportNode -is [HeaderNode]) { - $this.CompareInternal($sameNodeInPreviousReport, $currentReportNode, $Headers + $currentReportNode.Title) - } else { - if ($sameNodeInPreviousReport -and ($currentReportNode.IsIdenticalTo($sameNodeInPreviousReport))) { - # Nodes are identical, nothing changed, just ignore it - } elseif ($sameNodeInPreviousReport) { - # Nodes are equal but not identical, so something was changed - if ($currentReportNode -is [TableNode]) { - $this.CompareSimilarTableNodes($sameNodeInPreviousReport, $currentReportNode, $Headers) - } elseif ($currentReportNode -is [ToolVersionsListNode]) { - $this.CompareSimilarToolVersionsListNodes($sameNodeInPreviousReport, $currentReportNode, $Headers) - } else { - $this.ChangedItems.Add([ReportDifferenceItem]::new($sameNodeInPreviousReport, $currentReportNode, $Headers)) - } - } else { - # Node was not found in previous report, new node was added - $this.AddedItems.Add([ReportDifferenceItem]::new($null, $currentReportNode, $Headers)) - } - } - } - - # Detecting nodes that were removed - $previousReportPointer.Children ?? @() | Where-Object { $_.ShouldBeIncludedToDiff() -and $this.FilterExcludedNodes($_) } | ForEach-Object { - $previousReportNode = $_ - $sameNodeInCurrentReport = $currentReportPointer ? $currentReportPointer.FindSimilarChildNode($previousReportNode) : $null - - if (-not $sameNodeInCurrentReport) { - if ($previousReportNode -is [HeaderNode]) { - $this.CompareInternal($previousReportNode, $null, $Headers + $previousReportNode.Title) - } else { - $this.DeletedItems.Add([ReportDifferenceItem]::new($previousReportNode, $null, $Headers)) - } - } - } - } - - hidden [void] CompareSimilarTableNodes([TableNode] $PreviousReportNode, [TableNode] $CurrentReportNode, [Array] $Headers) { - $addedRows = $CurrentReportNode.Rows | Where-Object { $_ -notin $PreviousReportNode.Rows } - $deletedRows = $PreviousReportNode.Rows | Where-Object { $_ -notin $CurrentReportNode.Rows } - - if (($addedRows.Count -gt 0) -and ($deletedRows.Count -eq 0)) { - $this.AddedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers)) - } elseif (($deletedRows.Count -gt 0) -and ($addedRows.Count -eq 0)) { - $this.DeletedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers)) - } else { - $this.ChangedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers)) - } - } - - hidden [void] CompareSimilarToolVersionsListNodes([ToolVersionsListNode] $PreviousReportNode, [ToolVersionsListNode] $CurrentReportNode, [Array] $Headers) { - $previousReportMajorVersions = $PreviousReportNode.Versions | ForEach-Object { $PreviousReportNode.ExtractMajorVersion($_) } - $currentReportMajorVersion = $CurrentReportNode.Versions | ForEach-Object { $CurrentReportNode.ExtractMajorVersion($_) } - - $addedVersions = $CurrentReportNode.Versions | Where-Object { $CurrentReportNode.ExtractMajorVersion($_) -notin $previousReportMajorVersions } - $deletedVersions = $PreviousReportNode.Versions | Where-Object { $PreviousReportNode.ExtractMajorVersion($_) -notin $currentReportMajorVersion } - $changedPreviousVersions = $PreviousReportNode.Versions | Where-Object { ($PreviousReportNode.ExtractMajorVersion($_) -in $currentReportMajorVersion) -and ($_ -notin $CurrentReportNode.Versions) } - $changedCurrentVersions = $CurrentReportNode.Versions | Where-Object { ($CurrentReportNode.ExtractMajorVersion($_) -in $previousReportMajorVersions) -and ($_ -notin $PreviousReportNode.Versions) } - - if ($addedVersions.Count -gt 0) { - $this.AddedItems.Add([ReportDifferenceItem]::new($null, [ToolVersionsListNode]::new($CurrentReportNode.ToolName, $addedVersions, $CurrentReportNode.MajorVersionRegex, $true), $Headers)) - } - - if ($deletedVersions.Count -gt 0) { - $this.DeletedItems.Add([ReportDifferenceItem]::new([ToolVersionsListNode]::new($PreviousReportNode.ToolName, $deletedVersions, $PreviousReportNode.MajorVersionRegex, $true), $null, $Headers)) - } - - $previousChangedNode = ($changedPreviousVersions.Count -gt 0) ? [ToolVersionsListNode]::new($PreviousReportNode.ToolName, $changedPreviousVersions, $PreviousReportNode.MajorVersionRegex, $true) : $null - $currentChangedNode = ($changedCurrentVersions.Count -gt 0) ? [ToolVersionsListNode]::new($CurrentReportNode.ToolName, $changedCurrentVersions, $CurrentReportNode.MajorVersionRegex, $true) : $null - if ($previousChangedNode -and $currentChangedNode) { - $this.ChangedItems.Add([ReportDifferenceItem]::new($previousChangedNode, $currentChangedNode, $Headers)) - } - } - - [String] GetMarkdownReport() { - $reporter = [SoftwareReportComparerReport]::new() - $report = $reporter.GenerateMarkdownReport($this.CurrentReport, $this.PreviousReport, $this.AddedItems, $this.ChangedItems, $this.DeletedItems) - return $report - } - - hidden [Boolean] FilterExcludedNodes([BaseNode] $Node) { - # We shouldn't show "Image Version" diff because it is already shown in report header - if (($Node -is [ToolVersionNode]) -and ($Node.ToolName -eq "Image Version:")) { - return $false - } - - return $true - } -} - -# SoftwareReportComparerReport is used to render results of SoftwareReportComparer in markdown format -class SoftwareReportComparerReport { - [String] GenerateMarkdownReport([SoftwareReport] $CurrentReport, [SoftwareReport] $PreviousReport, [ReportDifferenceItem[]] $AddedItems, [ReportDifferenceItem[]] $ChangedItems, [ReportDifferenceItem[]] $DeletedItems) { - $sb = [System.Text.StringBuilder]::new() - - $rootNode = $CurrentReport.Root - $imageVersion = $this.GetImageVersion($CurrentReport) - $previousImageVersion = $this.GetImageVersion($PreviousReport) - - ############################# - ### Render report header #### - ############################# - - $sb.AppendLine("# :desktop_computer: Actions Runner Image: $($rootNode.Title)") - - # ToolNodes on root level contains main image description so just copy-paste them to final report - $rootNode.Children | Where-Object { $_ -is [BaseToolNode] } | ForEach-Object { - $sb.AppendLine($_.ToMarkdown(0)) - } - $sb.AppendLine() - - $sb.AppendLine("## :mega: What's changed?") - - ########################### - ### Render added items #### - ########################### - - [ReportDifferenceItem[]] $addedItemsExcludeTables = $AddedItems | Where-Object { $_.IsBaseToolNode() } - if ($addedItemsExcludeTables.Count -gt 0) { - $tableItems = $addedItemsExcludeTables | ForEach-Object { - [PSCustomObject]@{ - "Category" = $this.RenderCategory($_.Headers, $true); - "Tool name" = $this.RenderToolName($_.CurrentReportNode.ToolName); - "Current ($imageVersion)" = $_.CurrentReportNode.GetValue(); - } - } - - $sb.AppendLine("### Added :heavy_plus_sign:") - $sb.AppendLine($this.RenderHtmlTable($tableItems, "Category")) - } - - # Render added tables separately - $AddedItems | Where-Object { $_.IsTableNode() } | ForEach-Object { - $sb.AppendLine($this.RenderTableNodesDiff($_)) - } - - ############################# - ### Render deleted items #### - ############################# - - [ReportDifferenceItem[]] $deletedItemsExcludeTables = $DeletedItems | Where-Object { $_.IsBaseToolNode() } - if ($deletedItemsExcludeTables.Count -gt 0) { - $tableItems = $deletedItemsExcludeTables | ForEach-Object { - [PSCustomObject]@{ - "Category" = $this.RenderCategory($_.Headers, $true); - "Tool name" = $this.RenderToolName($_.PreviousReportNode.ToolName); - "Previous ($previousImageVersion)" = $_.PreviousReportNode.GetValue(); - } - } - - $sb.AppendLine("### Deleted :heavy_minus_sign:") - $sb.AppendLine($this.RenderHtmlTable($tableItems, "Category")) - } - - # Render deleted tables separately - $DeletedItems | Where-Object { $_.IsTableNode() } | ForEach-Object { - $sb.AppendLine($this.RenderTableNodesDiff($_)) - } - - ############################# - ### Render updated items #### - ############################# - - [ReportDifferenceItem[]] $changedItemsExcludeTables = $ChangedItems | Where-Object { $_.IsBaseToolNode() } - if ($changedItemsExcludeTables.Count -gt 0) { - $tableItems = $changedItemsExcludeTables | ForEach-Object { - [PSCustomObject]@{ - "Category" = $this.RenderCategory($_.Headers, $true); - "Tool name" = $this.RenderToolName($_.CurrentReportNode.ToolName); - "Previous ($previousImageVersion)" = $_.PreviousReportNode.GetValue(); - "Current ($imageVersion)" = $_.CurrentReportNode.GetValue(); - } - } - - $sb.AppendLine("### Updated") - $sb.AppendLine($this.RenderHtmlTable($tableItems, "Category")) - } - - # Render updated tables separately - $ChangedItems | Where-Object { $_.IsTableNode() } | ForEach-Object { - $sb.AppendLine($this.RenderTableNodesDiff($_)) - } - - return $sb.ToString() - } - - [String] RenderHtmlTable([PSCustomObject[]] $Table, $RowSpanColumnName) { - $headers = $Table[0].PSObject.Properties.Name - - $sb = [System.Text.StringBuilder]::new() - $sb.AppendLine("") - $sb.AppendLine(" ") - $headers | ForEach-Object { - $sb.AppendLine(" ") - } - $sb.AppendLine(" ") - $sb.AppendLine(" ") - - $tableRowSpans = $this.CalculateHtmlTableRowSpan($Table, $RowSpanColumnName) - for ($rowIndex = 0; $rowIndex -lt $Table.Count; $rowIndex++) { - $row = $Table[$rowIndex] - - $sb.AppendLine(" ") - $headers | ForEach-Object { - if ($_ -eq $RowSpanColumnName) { - if ($tableRowSpans[$rowIndex] -gt 0) { - $sb.AppendLine(" ") - } else { - # Skip rendering this cell at all - } - } else { - $sb.AppendLine(" ") - } - } - $sb.AppendLine(" ") - } - $sb.AppendLine(" ") - $sb.AppendLine("
$_
$($row.$_)$($row.$_)
") - - return $sb.ToString() - } - - [int[]] CalculateHtmlTableRowSpan([PSCustomObject[]] $Table, $keyColumn) { - $result = @(0) * $Table.Count - - for ($rowIndex = $Table.Count - 1; $rowIndex -ge 0; $rowIndex--) { - if (($rowIndex -lt ($Table.Count - 1)) -and ($Table[$rowIndex].$keyColumn -eq $Table[$rowIndex + 1].$keyColumn)) { - # If the current row is the same as the next row - # Then rowspan of current row should be equal to rowspan of the next row + 1 - # And rowspan of the next row should be 0 because it is already included in the rowspan of the current row - $result[$rowIndex] = $result[$rowIndex + 1] + 1 - $result[$rowIndex + 1] = 0 - } else { - $result[$rowIndex] = 1 - } - } - - return $result - } - - [String] RenderTableNodesDiff([ReportDifferenceItem] $DiffItem) { - # Use the simplest approach for now: first, print all removed lines. Then print added lines - # It will work well for most cases like changing existing rows, adding new rows and removing rows - # But can produce not so pretty results for cases when some rows are changed and some rows are added at the same time - # Let's see how it works in practice and improve it later if needed - - [String] $tableHeaders = ($DiffItem.CurrentReportNode ?? $DiffItem.PreviousReportNode).Headers - [System.Collections.ArrayList] $tableRows = @() - $DiffItem.PreviousReportNode.Rows ?? @() | Where-Object { $_ -notin $DiffItem.CurrentReportNode.Rows } | ForEach-Object { - $tableRows.Add($this.StrikeTableRow($_)) - } - $DiffItem.CurrentReportNode.Rows ?? @() | Where-Object { $_ -notin $DiffItem.PreviousReportNode.Rows } | ForEach-Object { - $tableRows.Add($_) - } - - $sb = [System.Text.StringBuilder]::new() - $sb.AppendLine("#### $($this.RenderCategory($DiffItem.Headers, $false))") - $sb.AppendLine([TableNode]::new($tableHeaders, $tableRows).ToMarkdown(0)) - return $sb.ToString() - } - - [String] RenderCategory([Array] $Headers, [Boolean] $AddLineSeparator) { - # Always skip the first header because it is "Installed Software" - [Array] $takeHeaders = $Headers | Select-Object -Skip 1 - if ($takeHeaders.Count -eq 0) { - return "" - } - - $lineSeparator = $AddLineSeparator ? "
": "" - return [String]::Join(" >$lineSeparator ", $takeHeaders) - } - - [String] RenderToolName([String] $ToolName) { - return $ToolName.TrimEnd(":") - } - - [String] StrikeTableRow([String] $Row) { - # Convert "a|b|c" to "~~a~~|~~b~~|~~c~~ - $cells = $Row.Split("|") - $strikedCells = $cells | ForEach-Object { "~~$($_)~~"} - return [String]::Join("|", $strikedCells) - } - - [String] GetImageVersion([SoftwareReport] $Report) { - $imageVersionNode = $Report.Root.Children ?? @() | Where-Object { ($_ -is [ToolVersionNode]) -and ($_.ToolName -eq "Image Version:") } | Select-Object -First 1 - return $imageVersionNode.Version ?? "Unknown version" - } -} - -# Temporary structure to store the single difference between two reports -class ReportDifferenceItem { - [BaseNode] $PreviousReportNode - [BaseNode] $CurrentReportNode - [Array] $Headers - - ReportDifferenceItem([BaseNode] $PreviousReportNode, [BaseNode] $CurrentReportNode, [Array] $Headers) { - $this.PreviousReportNode = $PreviousReportNode - $this.CurrentReportNode = $CurrentReportNode - $this.Headers = $Headers - } - - [Boolean] IsBaseToolNode() { - $node = $this.CurrentReportNode ?? $this.PreviousReportNode - return $node -is [BaseToolNode] - } - - [Boolean] IsTableNode() { - $node = $this.CurrentReportNode ?? $this.PreviousReportNode - return $node -is [TableNode] - } -} \ No newline at end of file diff --git a/helpers/software-report-base/SoftwareReport.DifferenceCalculator.psm1 b/helpers/software-report-base/SoftwareReport.DifferenceCalculator.psm1 new file mode 100644 index 000000000..38a1df6f8 --- /dev/null +++ b/helpers/software-report-base/SoftwareReport.DifferenceCalculator.psm1 @@ -0,0 +1,136 @@ +using module ./SoftwareReport.psm1 +using module ./SoftwareReport.BaseNodes.psm1 +using module ./SoftwareReport.Nodes.psm1 +using module ./SoftwareReport.DifferenceRender.psm1 + +class SoftwareReportDifferenceCalculator { + [ValidateNotNullOrEmpty()] + hidden [SoftwareReport] $PreviousReport + [ValidateNotNullOrEmpty()] + hidden [SoftwareReport] $CurrentReport + + hidden [Collections.Generic.List[ReportDifferenceItem]] $AddedItems + hidden [Collections.Generic.List[ReportDifferenceItem]] $ChangedItems + hidden [Collections.Generic.List[ReportDifferenceItem]] $DeletedItems + + SoftwareReportDifferenceCalculator([SoftwareReport] $PreviousReport, [SoftwareReport] $CurrentReport) { + $this.PreviousReport = $PreviousReport + $this.CurrentReport = $CurrentReport + } + + [void] CompareReports() { + $this.AddedItems = @() + $this.ChangedItems = @() + $this.DeletedItems = @() + + $this.CompareInternal($this.PreviousReport.Root, $this.CurrentReport.Root, @()) + } + + [String] GetMarkdownReport() { + $reporter = [SoftwareReportDifferenceRender]::new() + $report = $reporter.GenerateMarkdownReport($this.CurrentReport, $this.PreviousReport, $this.AddedItems, $this.ChangedItems, $this.DeletedItems) + return $report + } + + hidden [void] CompareInternal([HeaderNode] $previousReportPointer, [HeaderNode] $currentReportPointer, [String[]] $Headers) { + $currentReportPointer.Children ?? @() | Where-Object { $_.ShouldBeIncludedToDiff() -and $this.FilterExcludedNodes($_) } | ForEach-Object { + $currentReportNode = $_ + $sameNodeInPreviousReport = $previousReportPointer ? $previousReportPointer.FindSimilarChildNode($currentReportNode) : $null + + if ($currentReportNode -is [HeaderNode]) { + # Compare HeaderNode recursively + $this.CompareInternal($sameNodeInPreviousReport, $currentReportNode, $Headers + $currentReportNode.Title) + } else { + if ($sameNodeInPreviousReport -and ($currentReportNode.IsIdenticalTo($sameNodeInPreviousReport))) { + # Nodes are identical, nothing changed, just ignore it + } elseif ($sameNodeInPreviousReport) { + # Nodes are equal but not identical, something was changed + if ($currentReportNode -is [TableNode]) { + $this.CompareSimilarTableNodes($sameNodeInPreviousReport, $currentReportNode, $Headers) + } elseif ($currentReportNode -is [ToolVersionsListNode]) { + $this.CompareSimilarToolVersionsListNodes($sameNodeInPreviousReport, $currentReportNode, $Headers) + } else { + $this.ChangedItems.Add([ReportDifferenceItem]::new($sameNodeInPreviousReport, $currentReportNode, $Headers)) + } + } else { + # Node was not found in previous report, new node was added + $this.AddedItems.Add([ReportDifferenceItem]::new($null, $currentReportNode, $Headers)) + } + } + } + + # Detecting nodes that were removed + $previousReportPointer.Children ?? @() | Where-Object { $_.ShouldBeIncludedToDiff() -and $this.FilterExcludedNodes($_) } | ForEach-Object { + $previousReportNode = $_ + $sameNodeInCurrentReport = $currentReportPointer ? $currentReportPointer.FindSimilarChildNode($previousReportNode) : $null + + if (-not $sameNodeInCurrentReport) { + if ($previousReportNode -is [HeaderNode]) { + # Compare removed HeaderNode recursively + $this.CompareInternal($previousReportNode, $null, $Headers + $previousReportNode.Title) + } else { + # Node was not found in current report, node was removed + $this.DeletedItems.Add([ReportDifferenceItem]::new($previousReportNode, $null, $Headers)) + } + } + } + } + + hidden [void] CompareSimilarTableNodes([TableNode] $PreviousReportNode, [TableNode] $CurrentReportNode, [String[]] $Headers) { + $addedRows = $CurrentReportNode.Rows | Where-Object { $_ -notin $PreviousReportNode.Rows } + $deletedRows = $PreviousReportNode.Rows | Where-Object { $_ -notin $CurrentReportNode.Rows } + + if (($addedRows.Count -eq 0) -and ($deletedRows.Count -eq 0)) { + # Unexpected state: TableNodes are identical + return + } + + if ($PreviousReportNode.Headers -ne $CurrentReportNode.Headers) { + # If headers are changed and rows are changed at the same time, we should track it as removing table and adding new one + $this.DeletedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $null, $Headers)) + $this.AddedItems.Add([ReportDifferenceItem]::new($null, $CurrentReportNode, $Headers)) + } elseif (($addedRows.Count -gt 0) -and ($deletedRows.Count -eq 0)) { + # If new rows were added and no rows were deleted, then it is AddedItem + $this.AddedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers)) + } elseif (($deletedRows.Count -gt 0) -and ($addedRows.Count -eq 0)) { + # If no rows were added and some rows were deleted, then it is DeletedItem + $this.DeletedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers)) + } else { + # If some rows were added and some rows were removed, then it is UpdatedItem + $this.ChangedItems.Add([ReportDifferenceItem]::new($PreviousReportNode, $CurrentReportNode, $Headers)) + } + } + + hidden [void] CompareSimilarToolVersionsListNodes([ToolVersionsListNode] $PreviousReportNode, [ToolVersionsListNode] $CurrentReportNode, [String[]] $Headers) { + $previousReportMajorVersions = $PreviousReportNode.Versions | ForEach-Object { $PreviousReportNode.ExtractMajorVersion($_) } + $currentReportMajorVersion = $CurrentReportNode.Versions | ForEach-Object { $CurrentReportNode.ExtractMajorVersion($_) } + + $addedVersions = $CurrentReportNode.Versions | Where-Object { $CurrentReportNode.ExtractMajorVersion($_) -notin $previousReportMajorVersions } + $deletedVersions = $PreviousReportNode.Versions | Where-Object { $PreviousReportNode.ExtractMajorVersion($_) -notin $currentReportMajorVersion } + $changedPreviousVersions = $PreviousReportNode.Versions | Where-Object { ($PreviousReportNode.ExtractMajorVersion($_) -in $currentReportMajorVersion) -and ($_ -notin $CurrentReportNode.Versions) } + $changedCurrentVersions = $CurrentReportNode.Versions | Where-Object { ($CurrentReportNode.ExtractMajorVersion($_) -in $previousReportMajorVersions) -and ($_ -notin $PreviousReportNode.Versions) } + + if ($addedVersions.Count -gt 0) { + $this.AddedItems.Add([ReportDifferenceItem]::new($null, [ToolVersionsListNode]::new($CurrentReportNode.ToolName, $addedVersions, $CurrentReportNode.MajorVersionRegex, "List"), $Headers)) + } + + if ($deletedVersions.Count -gt 0) { + $this.DeletedItems.Add([ReportDifferenceItem]::new([ToolVersionsListNode]::new($PreviousReportNode.ToolName, $deletedVersions, $PreviousReportNode.MajorVersionRegex, "List"), $null, $Headers)) + } + + $previousChangedNode = ($changedPreviousVersions.Count -gt 0) ? [ToolVersionsListNode]::new($PreviousReportNode.ToolName, $changedPreviousVersions, $PreviousReportNode.MajorVersionRegex, "List") : $null + $currentChangedNode = ($changedCurrentVersions.Count -gt 0) ? [ToolVersionsListNode]::new($CurrentReportNode.ToolName, $changedCurrentVersions, $CurrentReportNode.MajorVersionRegex, "List") : $null + if ($previousChangedNode -and $currentChangedNode) { + $this.ChangedItems.Add([ReportDifferenceItem]::new($previousChangedNode, $currentChangedNode, $Headers)) + } + } + + hidden [Boolean] FilterExcludedNodes([BaseNode] $Node) { + # We shouldn't show "Image Version" diff because it is already shown in report header + if (($Node -is [ToolVersionNode]) -and ($Node.ToolName -eq "Image Version:")) { + return $false + } + + return $true + } +} \ No newline at end of file diff --git a/helpers/software-report-base/SoftwareReport.DifferenceRender.psm1 b/helpers/software-report-base/SoftwareReport.DifferenceRender.psm1 new file mode 100644 index 000000000..91d51e300 --- /dev/null +++ b/helpers/software-report-base/SoftwareReport.DifferenceRender.psm1 @@ -0,0 +1,225 @@ +using module ./SoftwareReport.psm1 +using module ./SoftwareReport.BaseNodes.psm1 +using module ./SoftwareReport.Nodes.psm1 + +class SoftwareReportDifferenceRender { + [String] GenerateMarkdownReport([SoftwareReport] $CurrentReport, [SoftwareReport] $PreviousReport, [ReportDifferenceItem[]] $AddedItems, [ReportDifferenceItem[]] $ChangedItems, [ReportDifferenceItem[]] $DeletedItems) { + $sb = [System.Text.StringBuilder]::new() + + $rootNode = $CurrentReport.Root + $imageVersion = $CurrentReport.GetImageVersion() + $previousImageVersion = $PreviousReport.GetImageVersion() + + ############################# + ### Render report header #### + ############################# + + $sb.AppendLine("# :desktop_computer: Actions Runner Image: $($rootNode.Title)") + + # ToolVersionNodes on root level contains main image description so just copy-paste them to final report + $rootNode.Children | Where-Object { $_ -is [ToolVersionNode] } | ForEach-Object { + $sb.AppendLine($_.ToMarkdown()) + } + $sb.AppendLine() + + $sb.AppendLine("## :mega: What's changed?").AppendLine() + + ########################### + ### Render added items #### + ########################### + + [ReportDifferenceItem[]] $addedItemsBaseTools = $AddedItems | Where-Object { $_.IsBaseToolNode() } + [ReportDifferenceItem[]] $addedItemsTables = $AddedItems | Where-Object { $_.IsTableNode() } + if ($addedItemsBaseTools.Count + $addedItemsTables.Count -gt 0) { + $sb.AppendLine("### Added :heavy_plus_sign:").AppendLine() + } + if ($addedItemsBaseTools.Count -gt 0) { + $tableItems = $addedItemsBaseTools | ForEach-Object { + [PSCustomObject]@{ + "Category" = $this.RenderCategory($_.Headers, $true); + "Tool name" = $this.RenderToolName($_.CurrentReportNode.ToolName); + "Current ($imageVersion)" = $_.CurrentReportNode.GetValue(); + } + } + $sb.AppendLine($this.RenderHtmlTable($tableItems, "Category")) + } + if ($addedItemsTables.Count -gt 0) { + $addedItemsTables | ForEach-Object { + $sb.AppendLine($this.RenderTableNodesDiff($_)) + } + } + + ############################# + ### Render deleted items #### + ############################# + + [ReportDifferenceItem[]] $deletedItemsBaseTools = $DeletedItems | Where-Object { $_.IsBaseToolNode() } + [ReportDifferenceItem[]] $deletedItemsTables = $DeletedItems | Where-Object { $_.IsTableNode() } + if ($deletedItemsBaseTools.Count + $deletedItemsTables.Count -gt 0) { + $sb.AppendLine("### Deleted :heavy_minus_sign:").AppendLine() + } + if ($deletedItemsBaseTools.Count -gt 0) { + $tableItems = $deletedItemsBaseTools | ForEach-Object { + [PSCustomObject]@{ + "Category" = $this.RenderCategory($_.Headers, $true); + "Tool name" = $this.RenderToolName($_.PreviousReportNode.ToolName); + "Previous ($previousImageVersion)" = $_.PreviousReportNode.GetValue(); + } + } + $sb.AppendLine($this.RenderHtmlTable($tableItems, "Category")) + } + if ($deletedItemsTables.Count -gt 0) { + $deletedItemsTables | ForEach-Object { + $sb.AppendLine($this.RenderTableNodesDiff($_)) + } + } + + ############################# + ### Render updated items #### + ############################# + + [ReportDifferenceItem[]] $changedItemsBaseTools = $ChangedItems | Where-Object { $_.IsBaseToolNode() } + [ReportDifferenceItem[]] $changedItemsTables = $ChangedItems | Where-Object { $_.IsTableNode() } + if ($changedItemsBaseTools.Count + $changedItemsTables.Count -gt 0) { + $sb.AppendLine("### Updated").AppendLine() + } + if ($changedItemsBaseTools.Count -gt 0) { + $tableItems = $changedItemsBaseTools | ForEach-Object { + [PSCustomObject]@{ + "Category" = $this.RenderCategory($_.Headers, $true); + "Tool name" = $this.RenderToolName($_.CurrentReportNode.ToolName); + "Previous ($previousImageVersion)" = $_.PreviousReportNode.GetValue(); + "Current ($imageVersion)" = $_.CurrentReportNode.GetValue(); + } + } + $sb.AppendLine($this.RenderHtmlTable($tableItems, "Category")) + } + if ($changedItemsTables.Count -gt 0) { + $changedItemsTables | ForEach-Object { + $sb.AppendLine($this.RenderTableNodesDiff($_)) + } + } + + return $sb.ToString() + } + + [String] RenderHtmlTable([PSCustomObject[]] $Table, [String] $RowSpanColumnName) { + $headers = $Table[0].PSObject.Properties.Name + + $sb = [System.Text.StringBuilder]::new() + $sb.AppendLine("") + $sb.AppendLine(" ") + $headers | ForEach-Object { + $sb.AppendLine(" ") + } + $sb.AppendLine(" ") + $sb.AppendLine(" ") + + $tableRowSpans = $this.CalculateHtmlTableRowSpan($Table, $RowSpanColumnName) + for ($rowIndex = 0; $rowIndex -lt $Table.Count; $rowIndex++) { + $row = $Table[$rowIndex] + + $sb.AppendLine(" ") + $headers | ForEach-Object { + if ($_ -eq $RowSpanColumnName) { + if ($tableRowSpans[$rowIndex] -gt 0) { + $sb.AppendLine(" ") + } else { + # Skip rendering this cell at all + } + } else { + $sb.AppendLine(" ") + } + } + $sb.AppendLine(" ") + } + $sb.AppendLine(" ") + $sb.AppendLine("
$_
$($row.$_)$($row.$_)
") + + return $sb.ToString() + } + + [int[]] CalculateHtmlTableRowSpan([PSCustomObject[]] $Table, [String] $keyColumn) { + $result = @(0) * $Table.Count + + for ($rowIndex = $Table.Count - 1; $rowIndex -ge 0; $rowIndex--) { + if (($rowIndex -lt ($Table.Count - 1)) -and ($Table[$rowIndex].$keyColumn -eq $Table[$rowIndex + 1].$keyColumn)) { + # If the current row is the same as the next row + # Then rowspan of current row should be equal to rowspan of the next row + 1 + # And rowspan of the next row should be 0 because it is already included in the rowspan of the current row + $result[$rowIndex] = $result[$rowIndex + 1] + 1 + $result[$rowIndex + 1] = 0 + } else { + $result[$rowIndex] = 1 + } + } + + return $result + } + + [String] RenderTableNodesDiff([ReportDifferenceItem] $DiffItem) { + # Use the simplest approach for now: first, print all removed lines. Then print added lines + # It will work well for most cases like changing existing rows, adding new rows and removing rows + # But can produce not so pretty results for cases when some rows are changed and some rows are added at the same time + # Let's see how it works in practice and improve it later if needed + + [String] $tableHeaders = ($DiffItem.CurrentReportNode ?? $DiffItem.PreviousReportNode).Headers + [Collections.Generic.List[String]] $tableRows = @() + $DiffItem.PreviousReportNode.Rows ?? @() | Where-Object { $_ -notin $DiffItem.CurrentReportNode.Rows } | ForEach-Object { + $tableRows.Add($this.StrikeTableRow($_)) + } + $DiffItem.CurrentReportNode.Rows ?? @() | Where-Object { $_ -notin $DiffItem.PreviousReportNode.Rows } | ForEach-Object { + $tableRows.Add($_) + } + + $sb = [System.Text.StringBuilder]::new() + $sb.AppendLine("#### $($this.RenderCategory($DiffItem.Headers, $false))") + $sb.AppendLine([TableNode]::new($tableHeaders, $tableRows).ToMarkdown()) + return $sb.ToString() + } + + [String] RenderCategory([String[]] $Headers, [Boolean] $AddLineSeparator) { + # Always skip the first header because it is "Installed Software" + [String[]] $takeHeaders = $Headers | Select-Object -Skip 1 + if ($takeHeaders.Count -eq 0) { + return "" + } + + $lineSeparator = $AddLineSeparator ? "
": "" + return [String]::Join(" >$lineSeparator ", $takeHeaders) + } + + [String] RenderToolName([String] $ToolName) { + return $ToolName.TrimEnd(":") + } + + [String] StrikeTableRow([String] $Row) { + # Convert "a|b|c" to "~~a~~|~~b~~|~~c~~ + $cells = $Row.Split("|") + $strikedCells = $cells | ForEach-Object { "~~$($_)~~"} + return [String]::Join("|", $strikedCells) + } +} + +# Temporary structure to store the single difference between two reports +class ReportDifferenceItem { + [BaseNode] $PreviousReportNode + [BaseNode] $CurrentReportNode + [String[]] $Headers + + ReportDifferenceItem([BaseNode] $PreviousReportNode, [BaseNode] $CurrentReportNode, [String[]] $Headers) { + $this.PreviousReportNode = $PreviousReportNode + $this.CurrentReportNode = $CurrentReportNode + $this.Headers = $Headers + } + + [Boolean] IsBaseToolNode() { + $node = $this.CurrentReportNode ?? $this.PreviousReportNode + return $node -is [BaseToolNode] + } + + [Boolean] IsTableNode() { + $node = $this.CurrentReportNode ?? $this.PreviousReportNode + return $node -is [TableNode] + } +} \ No newline at end of file diff --git a/helpers/software-report-base/SoftwareReport.Nodes.psm1 b/helpers/software-report-base/SoftwareReport.Nodes.psm1 index 5e820dc69..7fcb43ad4 100644 --- a/helpers/software-report-base/SoftwareReport.Nodes.psm1 +++ b/helpers/software-report-base/SoftwareReport.Nodes.psm1 @@ -7,27 +7,27 @@ using module ./SoftwareReport.BaseNodes.psm1 # NodesFactory is used to simplify parsing different types of notes # Every node has own logic of parsing and this method just invokes "FromJsonObject" of correct node type class NodesFactory { - static [BaseNode] ParseNodeFromObject($jsonObj) { - if ($jsonObj.NodeType -eq [HeaderNode].Name) { - return [HeaderNode]::FromJsonObject($jsonObj) - } elseif ($jsonObj.NodeType -eq [ToolVersionNode].Name) { - return [ToolVersionNode]::FromJsonObject($jsonObj) - } elseif ($jsonObj.NodeType -eq [ToolVersionsListNode].Name) { - return [ToolVersionsListNode]::FromJsonObject($jsonObj) - } elseif ($jsonObj.NodeType -eq [TableNode].Name) { - return [TableNode]::FromJsonObject($jsonObj) - } elseif ($jsonObj.NodeType -eq [NoteNode].Name) { - return [NoteNode]::FromJsonObject($jsonObj) + static [BaseNode] ParseNodeFromObject([object] $JsonObj) { + if ($JsonObj.NodeType -eq [HeaderNode].Name) { + return [HeaderNode]::FromJsonObject($JsonObj) + } elseif ($JsonObj.NodeType -eq [ToolVersionNode].Name) { + return [ToolVersionNode]::FromJsonObject($JsonObj) + } elseif ($JsonObj.NodeType -eq [ToolVersionsListNode].Name) { + return [ToolVersionsListNode]::FromJsonObject($JsonObj) + } elseif ($JsonObj.NodeType -eq [TableNode].Name) { + return [TableNode]::FromJsonObject($JsonObj) + } elseif ($JsonObj.NodeType -eq [NoteNode].Name) { + return [NoteNode]::FromJsonObject($JsonObj) } - throw "Unknown node type in ParseNodeFromObject '$($jsonObj.NodeType)'" + throw "Unknown node type in ParseNodeFromObject '$($JsonObj.NodeType)'" } } -# Node type to describe headers: "## Installed software" class HeaderNode: BaseNode { + [ValidateNotNullOrEmpty()] [String] $Title - [System.Collections.ArrayList] $Children + [Collections.Generic.List[BaseNode]] $Children HeaderNode([String] $Title) { $this.Title = $Title @@ -44,10 +44,15 @@ class HeaderNode: BaseNode { throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.`nFound node: $($similarNode.ToJsonObject() | ConvertTo-Json)`nNew node: $($node.ToJsonObject() | ConvertTo-Json)" } + [Array] $existingHeaderNodes = $this.Children | Where-Object { $_ -is [HeaderNode] } + if (($existingHeaderNodes.Count -gt 0) -and ($node -isnot [HeaderNode])) { + throw "It is not allowed to add the node of type '$($node.GetType().Name)' to the HeaderNode that already contains the HeaderNode children." + } + $this.Children.Add($node) } - [void] AddNodes([Array] $nodes) { + [void] AddNodes([BaseNode[]] $nodes) { $nodes | ForEach-Object { $this.AddNode($_) } @@ -63,11 +68,15 @@ class HeaderNode: BaseNode { $this.AddNode([ToolVersionNode]::new($ToolName, $Version)) } - [void] AddToolVersionsList([String] $ToolName, [Array] $Version, [String] $MajorVersionRegex, [Boolean] $InlineList) { - $this.AddNode([ToolVersionsListNode]::new($ToolName, $Version, $MajorVersionRegex, $InlineList)) + [void] AddToolVersionsList([String] $ToolName, [String[]] $Version, [String] $MajorVersionRegex) { + $this.AddNode([ToolVersionsListNode]::new($ToolName, $Version, $MajorVersionRegex, "List")) + } + + [void] AddToolVersionsListInline([String] $ToolName, [String[]] $Version, [String] $MajorVersionRegex) { + $this.AddNode([ToolVersionsListNode]::new($ToolName, $Version, $MajorVersionRegex, "Inline")) } - [void] AddTable([Array] $Table) { + [void] AddTable([PSCustomObject[]] $Table) { $this.AddNode([TableNode]::FromObjectsArray($Table)) } @@ -75,12 +84,12 @@ class HeaderNode: BaseNode { $this.AddNode([NoteNode]::new($Content)) } - [String] ToMarkdown($level) { + [String] ToMarkdown([Int32] $Level) { $sb = [System.Text.StringBuilder]::new() $sb.AppendLine() - $sb.AppendLine("$("#" * $level) $($this.Title)") + $sb.AppendLine("$("#" * $Level) $($this.Title)") $this.Children | ForEach-Object { - $sb.AppendLine($_.ToMarkdown($level + 1)) + $sb.AppendLine($_.ToMarkdown($Level + 1)) } return $sb.ToString().TrimEnd() @@ -94,9 +103,9 @@ class HeaderNode: BaseNode { } } - static [HeaderNode] FromJsonObject($jsonObj) { - $node = [HeaderNode]::new($jsonObj.Title) - $jsonObj.Children | Where-Object { $_ } | ForEach-Object { $node.AddNode([NodesFactory]::ParseNodeFromObject($_)) } + static [HeaderNode] FromJsonObject([Object] $JsonObj) { + $node = [HeaderNode]::new($JsonObj.Title) + $JsonObj.Children | Where-Object { $_ } | ForEach-Object { $node.AddNode([NodesFactory]::ParseNodeFromObject($_)) } return $node } @@ -123,15 +132,15 @@ class HeaderNode: BaseNode { } } -# Node type to describe the tool with single version: "Bash 5.1.16" class ToolVersionNode: BaseToolNode { + [ValidateNotNullOrEmpty()] [String] $Version ToolVersionNode([String] $ToolName, [String] $Version): base($ToolName) { $this.Version = $Version } - [String] ToMarkdown($level) { + [String] ToMarkdown([Int32] $Level) { return "- $($this.ToolName) $($this.Version)" } @@ -147,32 +156,35 @@ class ToolVersionNode: BaseToolNode { } } - static [BaseNode] FromJsonObject($jsonObj) { - return [ToolVersionNode]::new($jsonObj.ToolName, $jsonObj.Version) + static [BaseNode] FromJsonObject([Object] $JsonObj) { + return [ToolVersionNode]::new($JsonObj.ToolName, $JsonObj.Version) } } -# Node type to describe the tool with multiple versions "Toolcache Node.js 14.17.6 16.2.0 18.2.3" class ToolVersionsListNode: BaseToolNode { - [Array] $Versions + [ValidateNotNullOrEmpty()] + [String[]] $Versions + [Regex] $MajorVersionRegex + + [ValidateSet("List", "Inline")] [String] $ListType - ToolVersionsListNode([String] $ToolName, [Array] $Versions, [String] $MajorVersionRegex, [Boolean] $InlineList): base($ToolName) { + ToolVersionsListNode([String] $ToolName, [String[]] $Versions, [String] $MajorVersionRegex, [String] $ListType): base($ToolName) { $this.Versions = $Versions $this.MajorVersionRegex = [Regex]::new($MajorVersionRegex) - $this.ListType = $InlineList ? "Inline" : "List" + $this.ListType = $ListType $this.ValidateMajorVersionRegex() } - [String] ToMarkdown($level) { + [String] ToMarkdown([Int32] $Level) { if ($this.ListType -eq "Inline") { return "- $($this.ToolName): $($this.Versions -join ', ')" } $sb = [System.Text.StringBuilder]::new() $sb.AppendLine() - $sb.AppendLine("$("#" * $level) $($this.ToolName)") + $sb.AppendLine("$("#" * $Level) $($this.ToolName)") $this.Versions | ForEach-Object { $sb.AppendLine("- $_") } @@ -186,7 +198,7 @@ class ToolVersionsListNode: BaseToolNode { [String] ExtractMajorVersion([String] $Version) { $match = $this.MajorVersionRegex.Match($Version) - if ($match.Success -ne $true) { + if (($match.Success -ne $true) -or [String]::IsNullOrEmpty($match.Groups[0].Value)) { throw "Version '$Version' doesn't match regex '$($this.PrimaryVersionRegex)'" } @@ -203,57 +215,46 @@ class ToolVersionsListNode: BaseToolNode { } } - static [ToolVersionsListNode] FromJsonObject($jsonObj) { - return [ToolVersionsListNode]::new($jsonObj.ToolName, $jsonObj.Versions, $jsonObj.MajorVersionRegex, $jsonObj.ListType -eq "Inline") + static [ToolVersionsListNode] FromJsonObject([Object] $JsonObj) { + return [ToolVersionsListNode]::new($JsonObj.ToolName, $JsonObj.Versions, $JsonObj.MajorVersionRegex, $JsonObj.ListType) } hidden [void] ValidateMajorVersionRegex() { $this.Versions | Group-Object { $this.ExtractMajorVersion($_) } | ForEach-Object { if ($_.Count -gt 1) { - throw "Multiple versions from list $($this.GetValue()) return the same result from regex '$($this.MajorVersionRegex)': $($_.Name)" + throw "Multiple versions from list '$($this.GetValue())' return the same result from regex '$($this.MajorVersionRegex)': $($_.Name)" } } } } -# Node type to describe tables class TableNode: BaseNode { # It is easier to store the table as rendered lines because it will simplify finding differences in rows later + [ValidateNotNullOrEmpty()] [String] $Headers - [System.Collections.ArrayList] $Rows + [ValidateNotNullOrEmpty()] + [String[]] $Rows - TableNode($Headers, $Rows) { + TableNode([String] $Headers, [String[]] $Rows) { $this.Headers = $Headers $this.Rows = $Rows + + $columnsCount = $this.Headers.Split("|").Count + $this.Rows | ForEach-Object { + if ($_.Split("|").Count -ne $columnsCount) { + throw "Table has different number of columns in different rows" + } + } } [Boolean] ShouldBeIncludedToDiff() { return $true } - static [TableNode] FromObjectsArray([Array] $Table) { - # take column names from the first row in table because we expect all rows to have the same columns - [String] $tableHeaders = [TableNode]::ArrayToTableRow($Table[0].PSObject.Properties.Name) - [System.Collections.ArrayList] $tableRows = @() - - $Table | ForEach-Object { - $tableRows.Add([TableNode]::ArrayToTableRow($_.PSObject.Properties.Value)) - } - - return [TableNode]::new($tableHeaders, $tableRows) - } - - [String] ToMarkdown($level) { - $maxColumnWidths = $this.Headers.Split("|") | ForEach-Object { $_.Length } + [String] ToMarkdown([Int32] $Level) { + $maxColumnWidths = $this.CalculateColumnsWidth() $columnsCount = $maxColumnWidths.Count - $this.Rows | ForEach-Object { - $columnWidths = $_.Split("|") | ForEach-Object { $_.Length } - for ($colIndex = 0; $colIndex -lt $columnsCount; $colIndex++) { - $maxColumnWidths[$colIndex] = [Math]::Max($maxColumnWidths[$colIndex], $columnWidths[$colIndex]) - } - } - $delimeterLine = [String]::Join("|", @("-") * $columnsCount) $sb = [System.Text.StringBuilder]::new() @@ -273,6 +274,20 @@ class TableNode: BaseNode { return $sb.ToString().TrimEnd() } + hidden [Int32[]] CalculateColumnsWidth() { + $maxColumnWidths = $this.Headers.Split("|") | ForEach-Object { $_.Length } + $columnsCount = $maxColumnWidths.Count + + $this.Rows | ForEach-Object { + $columnWidths = $_.Split("|") | ForEach-Object { $_.Length } + for ($colIndex = 0; $colIndex -lt $columnsCount; $colIndex++) { + $maxColumnWidths[$colIndex] = [Math]::Max($maxColumnWidths[$colIndex], $columnWidths[$colIndex]) + } + } + + return $maxColumnWidths + } + [PSCustomObject] ToJsonObject() { return [PSCustomObject]@{ NodeType = $this.GetType().Name @@ -281,8 +296,8 @@ class TableNode: BaseNode { } } - static [TableNode] FromJsonObject($jsonObj) { - return [TableNode]::new($jsonObj.Headers, $jsonObj.Rows) + static [TableNode] FromJsonObject([Object] $JsonObj) { + return [TableNode]::new($JsonObj.Headers, $JsonObj.Rows) } [Boolean] IsSimilarTo([BaseNode] $OtherNode) { @@ -299,9 +314,8 @@ class TableNode: BaseNode { return $false } - if ($this.Headers -ne $OtherNode.Headers) { - return $false - } + # We don't compare $this.Headers intentionally + # It is fine to ignore the tables where headers are changed but rows are not changed if ($this.Rows.Count -ne $OtherNode.Rows.Count) { return $false @@ -316,20 +330,49 @@ class TableNode: BaseNode { return $true } - hidden static [String] ArrayToTableRow([Array] $Values) { - # TO-DO: Add validation for the case when $Values contains "|" + static [TableNode] FromObjectsArray([PSCustomObject[]] $Table) { + if ($Table.Count -eq 0) { + throw "Failed to create TableNode from empty objects array" + } + + [String] $tableHeaders = [TableNode]::ArrayToTableRow($Table[0].PSObject.Properties.Name) + [Collections.Generic.List[String]] $tableRows = @() + + $Table | ForEach-Object { + $rowHeaders = [TableNode]::ArrayToTableRow($_.PSObject.Properties.Name) + if (($rowHeaders -ne $tableHeaders)) { + throw "Failed to create TableNode from objects array because objects have different properties" + } + + $tableRows.Add([TableNode]::ArrayToTableRow($_.PSObject.Properties.Value)) + } + + return [TableNode]::new($tableHeaders, $tableRows) + } + + hidden static [String] ArrayToTableRow([String[]] $Values) { + if ($Values.Count -eq 0) { + throw "Failed to create TableNode because some objects are empty" + } + $Values | ForEach-Object { + if ($_.Contains("|")) { + throw "Failed to create TableNode because some cells '$_' contains forbidden symbol '|'" + } + } + return [String]::Join("|", $Values) } } class NoteNode: BaseNode { + [ValidateNotNullOrEmpty()] [String] $Content NoteNode([String] $Content) { $this.Content = $Content } - [String] ToMarkdown($level) { + [String] ToMarkdown([Int32] $Level) { return @( '```', $this.Content, @@ -344,8 +387,8 @@ class NoteNode: BaseNode { } } - static [NoteNode] FromJsonObject($jsonObj) { - return [NoteNode]::new($jsonObj.Content) + static [NoteNode] FromJsonObject([Object] $JsonObj) { + return [NoteNode]::new($JsonObj.Content) } [Boolean] IsSimilarTo([BaseNode] $OtherNode) { diff --git a/helpers/software-report-base/SoftwareReport.psm1 b/helpers/software-report-base/SoftwareReport.psm1 index 81f3cfd9c..c78757f25 100644 --- a/helpers/software-report-base/SoftwareReport.psm1 +++ b/helpers/software-report-base/SoftwareReport.psm1 @@ -2,6 +2,7 @@ using module ./SoftwareReport.BaseNodes.psm1 using module ./SoftwareReport.Nodes.psm1 class SoftwareReport { + [ValidateNotNullOrEmpty()] [HeaderNode] $Root SoftwareReport([String] $Title) { @@ -16,13 +17,18 @@ class SoftwareReport { return $this.Root.ToJsonObject() | ConvertTo-Json -Depth 10 } - static [SoftwareReport] FromJson($jsonString) { - $jsonObj = $jsonString | ConvertFrom-Json + static [SoftwareReport] FromJson([String] $JsonString) { + $jsonObj = $JsonString | ConvertFrom-Json $rootNode = [NodesFactory]::ParseNodeFromObject($jsonObj) return [SoftwareReport]::new($rootNode) } [String] ToMarkdown() { - return $this.Root.ToMarkdown(1).Trim() + return $this.Root.ToMarkdown().Trim() + } + + [String] GetImageVersion() { + $imageVersionNode = $this.Root.Children ?? @() | Where-Object { ($_ -is [ToolVersionNode]) -and ($_.ToolName -eq "Image Version:") } | Select-Object -First 1 + return $imageVersionNode.Version ?? "Unknown version" } } \ No newline at end of file diff --git a/helpers/software-report-base/tests/SoftwareReport.Difference.E2E.Tests.ps1 b/helpers/software-report-base/tests/SoftwareReport.Difference.E2E.Tests.ps1 new file mode 100644 index 000000000..8025e4736 --- /dev/null +++ b/helpers/software-report-base/tests/SoftwareReport.Difference.E2E.Tests.ps1 @@ -0,0 +1,525 @@ +using module ../SoftwareReport.psm1 +using module ../SoftwareReport.DifferenceCalculator.psm1 + +Describe "Comparer.E2E" { + It "Some tools are updated" { + # Previous report + $prevSoftwareReport = [SoftwareReport]::new("macOS 11") + $prevSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)") + $prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1") + $prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software") + $prevTools = $prevInstalledSoftware.AddHeader("Tools") + $prevTools.AddToolVersion("ToolWillBeUpdated1", "1.0.0") + $prevTools.AddToolVersion("ToolWillBeUpdated2", "3.0.1") + $prevTools.AddToolVersionsList("ToolWillBeUpdated3", @("14.0.0", "15.5.1"), "^\d+") + + # Next report + $nextSoftwareReport = [SoftwareReport]::new("macOS 11") + $nextSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)") + $nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.1") + $nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software") + $nextTools = $nextInstalledSoftware.AddHeader("Tools") + $nextTools.AddToolVersion("ToolWillBeUpdated1", "2.5.0") + $nextTools.AddToolVersion("ToolWillBeUpdated2", "3.0.2") + $nextTools.AddToolVersionsList("ToolWillBeUpdated3", @("14.2.0", "15.5.1"), "^\d+") + + # Compare reports + $comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport) + $comparer.CompareReports() + $comparer.GetMarkdownReport() | Should -BeExactly @' +# :desktop_computer: Actions Runner Image: macOS 11 +- OS Version: macOS 11.7.1 (20G817) +- Image Version: 20220922.1 + +## :mega: What's changed? + +### Updated + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryTool namePrevious (20220918.1)Current (20220922.1)
ToolsToolWillBeUpdated11.0.02.5.0
ToolWillBeUpdated23.0.13.0.2
ToolWillBeUpdated314.0.014.2.0
+ + +'@ + } + + It "Some tools are updated, added and removed" { + # Previous report + $prevSoftwareReport = [SoftwareReport]::new("macOS 11") + $prevSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.1 (20G817)") + $prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1") + $prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software") + + $prevLanguagesAndRuntimes = $prevInstalledSoftware.AddHeader("Language and Runtime") + $prevLanguagesAndRuntimes.AddToolVersion("ToolWillBeRemoved", "5.1.16(1)-release") + $prevLanguagesAndRuntimes.AddToolVersionsListInline("ToolWithMultipleVersions3", @("1.2.100", "1.2.200", "1.3.500", "1.4.100", "1.4.200"), "^\d+\.\d+\.\d") + $prevLanguagesAndRuntimes.AddToolVersion("ToolWithoutChanges", "5.34.0") + $prevLanguagesAndRuntimes.AddToolVersion("ToolWillBeUpdated", "8.1.0") + + $prevCachedTools = $prevInstalledSoftware.AddHeader("Cached Tools") + $prevCachedTools.AddToolVersionsList("ToolWithMultipleVersions1", @("2.7.3", "2.8.1", "3.1.2"), "^\d+\.\d+") + $prevCachedTools.AddToolVersionsList("ToolWithMultipleVersions2", @("14.8.0", "15.1.0", "16.4.2"), "^\d+") + + $prevSQLSection = $prevInstalledSoftware.AddHeader("Databases") + $prevSQLSection.AddToolVersion("MineSQL", "6.1.0") + $prevSQLSection.AddNote("First Note") + + # Next report + $nextSoftwareReport = [SoftwareReport]::new("macOS 11") + $nextSoftwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7.2 (20G922)") + $nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.0") + $nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software") + + $nextLanguagesAndRuntimes = $nextInstalledSoftware.AddHeader("Language and Runtime") + $nextLanguagesAndRuntimes.AddToolVersion("ToolWillBeAdded", "16.18.0") + $nextLanguagesAndRuntimes.AddToolVersionsListInline("ToolWithMultipleVersions3", @("1.2.200", "1.3.515", "1.4.100", "1.4.200", "1.5.800"), "^\d+\.\d+\.\d") + $nextLanguagesAndRuntimes.AddToolVersion("ToolWithoutChanges", "5.34.0") + $nextLanguagesAndRuntimes.AddToolVersion("ToolWillBeUpdated", "8.3.0") + + $nextCachedTools = $nextInstalledSoftware.AddHeader("Cached Tools") + $nextCachedTools.AddToolVersionsList("ToolWithMultipleVersions1", @("2.7.3", "2.8.1", "3.1.2"), "^\d+\.\d+") + $nextCachedTools.AddToolVersionsList("ToolWithMultipleVersions2", @("15.1.0", "16.4.2", "17.0.1"), "^\d+") + + $nextSQLSection = $nextInstalledSoftware.AddHeader("Databases") + $nextSQLSection.AddToolVersion("MineSQL", "6.1.1") + $nextSQLSection.AddNote("Second Note") + + # Compare reports + $comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport) + $comparer.CompareReports() + $comparer.GetMarkdownReport() | Should -BeExactly @' +# :desktop_computer: Actions Runner Image: macOS 11 +- OS Version: macOS 11.7.2 (20G922) +- Image Version: 20220922.0 + +## :mega: What's changed? + +### Added :heavy_plus_sign: + + + + + + + + + + + + + + + + + + + + + + + +
CategoryTool nameCurrent (20220922.0)
Language and RuntimeToolWillBeAdded16.18.0
ToolWithMultipleVersions31.5.800
Cached ToolsToolWithMultipleVersions217.0.1
+ +### Deleted :heavy_minus_sign: + + + + + + + + + + + + + + + + + + + + + + + +
CategoryTool namePrevious (20220918.1)
Language and RuntimeToolWithMultipleVersions31.2.100
ToolWillBeRemoved5.1.16(1)-release
Cached ToolsToolWithMultipleVersions214.8.0
+ +### Updated + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryTool namePrevious (20220918.1)Current (20220922.0)
OS VersionmacOS 11.7.1 (20G817)macOS 11.7.2 (20G922)
Language and RuntimeToolWithMultipleVersions31.3.5001.3.515
ToolWillBeUpdated8.1.08.3.0
DatabasesMineSQL6.1.06.1.1
+ + +'@ + } + + It "Header tree changes" { + # Previous report + $prevSoftwareReport = [SoftwareReport]::new("macOS 11") + $prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1") + $prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software") + $prevInstalledSoftware.AddToolVersion("ToolWithoutChanges", "5.34.0") + $prevInstalledSoftware.AddHeader("HeaderWillBeRemoved").AddHeader("SubheaderWillBeRemoved").AddToolVersion("ToolWillBeRemoved", "1.0.0") + $prevInstalledSoftware.AddHeader("Header1").AddToolVersion("ToolWillBeMovedToAnotherHeader", "3.0.0") + + # Next report + $nextSoftwareReport = [SoftwareReport]::new("macOS 11") + $nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.0") + $nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software") + $nextInstalledSoftware.AddToolVersion("ToolWithoutChanges", "5.34.0") + $nextInstalledSoftware.AddHeader("HeaderWillBeAdded").AddHeader("SubheaderWillBeAdded").AddToolVersion("ToolWillBeAdded", "5.0.0") + $nextInstalledSoftware.AddHeader("Header2").AddToolVersion("ToolWillBeMovedToAnotherHeader", "3.0.0") + + # Compare reports + $comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport) + $comparer.CompareReports() + $comparer.GetMarkdownReport() | Should -BeExactly @' +# :desktop_computer: Actions Runner Image: macOS 11 +- Image Version: 20220922.0 + +## :mega: What's changed? + +### Added :heavy_plus_sign: + + + + + + + + + + + + + + + + + + + +
CategoryTool nameCurrent (20220922.0)
HeaderWillBeAdded >
SubheaderWillBeAdded
ToolWillBeAdded5.0.0
Header2ToolWillBeMovedToAnotherHeader3.0.0
+ +### Deleted :heavy_minus_sign: + + + + + + + + + + + + + + + + + + + +
CategoryTool namePrevious (20220918.1)
HeaderWillBeRemoved >
SubheaderWillBeRemoved
ToolWillBeRemoved1.0.0
Header1ToolWillBeMovedToAnotherHeader3.0.0
+ + +'@ + } + + It "Tables are added and removed" { + # Previous report + $prevSoftwareReport = [SoftwareReport]::new("macOS 11") + $prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1") + $prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software") + $prevInstalledSoftware.AddHeader("HeaderWillExist").AddTable(@( + [PSCustomObject]@{TableInExistingHeaderWillBeRemoved = "Q"; Value = "25"}, + [PSCustomObject]@{TableInExistingHeaderWillBeRemoved = "O"; Value = "24"} + )) + + $prevTools = $prevInstalledSoftware.AddHeader("Tools") + $prevTools.AddHeader("HeaderWillBeRemoved").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "Z"; Value = "30"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "W"; Value = "29"} + )) + + # Next report + $nextSoftwareReport = [SoftwareReport]::new("macOS 11") + $nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.1") + $nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software") + $nextInstalledSoftware.AddHeader("HeaderWillExist") + $nextTools = $nextInstalledSoftware.AddHeader("Tools") + $nextTools.AddToolVersion("ToolWillBeAdded", "3.0.1") + $nextTools.AddTable(@( + [PSCustomObject]@{NewTableInExistingHeader = "A"; Value = "1"}, + [PSCustomObject]@{NewTableInExistingHeader = "B"; Value = "2"} + )) + $nextTools.AddHeader("NewHeaderWithTable").AddTable(@( + [PSCustomObject]@{NewTableInNewHeader = "C"; Value = "3"}, + [PSCustomObject]@{NewTableInNewHeader = "D"; Value = "4"} + )) + + # Compare reports + $comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport) + $comparer.CompareReports() + $comparer.GetMarkdownReport() | Should -BeExactly @' +# :desktop_computer: Actions Runner Image: macOS 11 +- Image Version: 20220922.1 + +## :mega: What's changed? + +### Added :heavy_plus_sign: + + + + + + + + + + + + + + +
CategoryTool nameCurrent (20220922.1)
ToolsToolWillBeAdded3.0.1
+ +#### Tools +| NewTableInExistingHeader | Value | +| ------------------------ | ----- | +| A | 1 | +| B | 2 | + +#### Tools > NewHeaderWithTable +| NewTableInNewHeader | Value | +| ------------------- | ----- | +| C | 3 | +| D | 4 | + +### Deleted :heavy_minus_sign: + +#### HeaderWillExist +| TableInExistingHeaderWillBeRemoved | Value | +| ---------------------------------- | ------ | +| ~~Q~~ | ~~25~~ | +| ~~O~~ | ~~24~~ | + +#### Tools > HeaderWillBeRemoved +| TableWillBeRemovedWithHeader | Value | +| ---------------------------- | ------ | +| ~~Z~~ | ~~30~~ | +| ~~W~~ | ~~29~~ | + + +'@ + } + + It "Tables are changed" { + # Previous report + $prevSoftwareReport = [SoftwareReport]::new("macOS 11") + $prevSoftwareReport.Root.AddToolVersion("Image Version:", "20220918.1") + $prevInstalledSoftware = $prevSoftwareReport.Root.AddHeader("Installed Software") + $prevTools = $prevInstalledSoftware.AddHeader("Tools") + $prevTools.AddHeader("TableWithAddedRows").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "AA"; Value = "10"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "AB"; Value = "11"} + )) + $prevTools.AddHeader("TableWithRemovedRows").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "BA"; Value = "32"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "BB"; Value = "33"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "BC"; Value = "34"} + )) + $prevTools.AddHeader("TableWithUpdatedRow").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "CA"; Value = "42"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "CB"; Value = "43"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "CC"; Value = "44"} + )) + $prevTools.AddHeader("TableWithUpdatedRows").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DA"; Value = "50"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DB"; Value = "51"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DC"; Value = "52"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DD"; Value = "53"} + )) + $prevTools.AddHeader("TableWithComplexChanges").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "EA"; Value = "62"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "EB"; Value = "63"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "EC"; Value = "64"} + [PSCustomObject]@{TableWillBeRemovedWithHeader = "ED"; Value = "65"} + )) + + $prevTools.AddHeader("TableWithOnlyHeaderChanged").AddTable(@( + [PSCustomObject]@{TableWithOnlyHeaderChanged = "FA"; Value = "72"}, + [PSCustomObject]@{TableWithOnlyHeaderChanged = "FB"; Value = "73"} + )) + + $prevTools.AddHeader("TableWithHeaderAndRowsChanges").AddTable(@( + [PSCustomObject]@{TableWithHeaderAndRowsChanges = "GA"; Value = "82"}, + [PSCustomObject]@{TableWithHeaderAndRowsChanges = "GB"; Value = "83"}, + [PSCustomObject]@{TableWithHeaderAndRowsChanges = "GC"; Value = "84"} + )) + + # Next report + $nextSoftwareReport = [SoftwareReport]::new("macOS 11") + $nextSoftwareReport.Root.AddToolVersion("Image Version:", "20220922.1") + $nextInstalledSoftware = $nextSoftwareReport.Root.AddHeader("Installed Software") + $nextTools = $nextInstalledSoftware.AddHeader("Tools") + $nextTools.AddHeader("TableWithAddedRows").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "AA"; Value = "10"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "AB"; Value = "11"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "AC"; Value = "12"} + )) + $nextTools.AddHeader("TableWithRemovedRows").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "BB"; Value = "33"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "BC"; Value = "34"} + )) + $nextTools.AddHeader("TableWithUpdatedRow").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "CA"; Value = "42"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "CB"; Value = "500"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "CC"; Value = "44"} + )) + $nextTools.AddHeader("TableWithUpdatedRows").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DA"; Value = "50"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DB"; Value = "5100"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DC"; Value = "5200"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "DD"; Value = "53"} + )) + $nextTools.AddHeader("TableWithComplexChanges").AddTable(@( + [PSCustomObject]@{TableWillBeRemovedWithHeader = "EB"; Value = "63"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "EC"; Value = "640"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "ED"; Value = "65"}, + [PSCustomObject]@{TableWillBeRemovedWithHeader = "EE"; Value = "66"} + )) + + $nextTools.AddHeader("TableWithOnlyHeaderChanged").AddTable(@( + [PSCustomObject]@{TableWithOnlyHeaderChanged2 = "FA"; Value = "72"}, + [PSCustomObject]@{TableWithOnlyHeaderChanged2 = "FB"; Value = "73"} + )) + + $nextTools.AddHeader("TableWithHeaderAndRowsChanges").AddTable(@( + [PSCustomObject]@{TableWithHeaderAndRowsChanges2 = "GA"; Value = "82"}, + [PSCustomObject]@{TableWithHeaderAndRowsChanges2 = "GE"; Value = "850"}, + [PSCustomObject]@{TableWithHeaderAndRowsChanges2 = "GC"; Value = "840"} + )) + + # Compare reports + $comparer = [SoftwareReportDifferenceCalculator]::new($prevSoftwareReport, $nextSoftwareReport) + $comparer.CompareReports() + $comparer.GetMarkdownReport() | Should -BeExactly @' +# :desktop_computer: Actions Runner Image: macOS 11 +- Image Version: 20220922.1 + +## :mega: What's changed? + +### Added :heavy_plus_sign: + +#### Tools > TableWithAddedRows +| TableWillBeRemovedWithHeader | Value | +| ---------------------------- | ----- | +| AC | 12 | + +#### Tools > TableWithHeaderAndRowsChanges +| TableWithHeaderAndRowsChanges2 | Value | +| ------------------------------ | ----- | +| GA | 82 | +| GE | 850 | +| GC | 840 | + +### Deleted :heavy_minus_sign: + +#### Tools > TableWithRemovedRows +| TableWillBeRemovedWithHeader | Value | +| ---------------------------- | ------ | +| ~~BA~~ | ~~32~~ | + +#### Tools > TableWithHeaderAndRowsChanges +| TableWithHeaderAndRowsChanges | Value | +| ----------------------------- | ------ | +| ~~GA~~ | ~~82~~ | +| ~~GB~~ | ~~83~~ | +| ~~GC~~ | ~~84~~ | + +### Updated + +#### Tools > TableWithUpdatedRow +| TableWillBeRemovedWithHeader | Value | +| ---------------------------- | ------ | +| ~~CB~~ | ~~43~~ | +| CB | 500 | + +#### Tools > TableWithUpdatedRows +| TableWillBeRemovedWithHeader | Value | +| ---------------------------- | ------ | +| ~~DB~~ | ~~51~~ | +| ~~DC~~ | ~~52~~ | +| DB | 5100 | +| DC | 5200 | + +#### Tools > TableWithComplexChanges +| TableWillBeRemovedWithHeader | Value | +| ---------------------------- | ------ | +| ~~EA~~ | ~~62~~ | +| ~~EC~~ | ~~64~~ | +| EC | 640 | +| EE | 66 | + + +'@ + } +} \ No newline at end of file diff --git a/helpers/software-report-base/tests/SoftwareReport.DifferenceCalculator.Unit.Tests.ps1 b/helpers/software-report-base/tests/SoftwareReport.DifferenceCalculator.Unit.Tests.ps1 new file mode 100644 index 000000000..31e690e27 --- /dev/null +++ b/helpers/software-report-base/tests/SoftwareReport.DifferenceCalculator.Unit.Tests.ps1 @@ -0,0 +1,603 @@ +using module ../SoftwareReport.Nodes.psm1 +using module ../SoftwareReport.DifferenceCalculator.psm1 + +BeforeDiscovery { + Import-Module $(Join-Path $PSScriptRoot "TestHelpers.psm1") -DisableNameChecking +} + +Describe "Comparer.UnitTests" { + Describe "Headers Tree" { + It "Add Node to existing header" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersion("MyTool1", "2.1.3") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.AddedItems[0].CurrentReportNode.Version | Should -Be "2.1.3" + $comparer.AddedItems[0].Headers | Should -BeArray @("MyHeader") + } + + It "Add new header with Node" { + $prevReport = [HeaderNode]::new("Version 1") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddHeader("MySubHeader").AddToolVersion("MyTool1", "2.1.3") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.AddedItems[0].CurrentReportNode.Version | Should -Be "2.1.3" + $comparer.AddedItems[0].Headers | Should -BeArray @("MyHeader", "MySubHeader") + } + + It "Remove Node from existing header" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersion("MyTool1", "2.1.3") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.DeletedItems[0].PreviousReportNode.Version | Should -Be "2.1.3" + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + $comparer.DeletedItems[0].Headers | Should -BeArray @("MyHeader") + } + + It "Remove header with Node" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3") + + $nextReport = [HeaderNode]::new("Version 2") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.DeletedItems[0].PreviousReportNode.Version | Should -Be "2.1.3" + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + $comparer.DeletedItems[0].Headers | Should -BeArray @("MyHeader", "MySubheader") + } + + It "Node with minor changes" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.4") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].PreviousReportNode.Version | Should -Be "2.1.3" + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].CurrentReportNode.Version | Should -Be "2.1.4" + $comparer.ChangedItems[0].Headers | Should -BeArray @("MyHeader", "MySubHeader") + } + + It "Node without changes" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + } + + It "Node is moved to different header" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddHeader("MySubheader").AddToolVersion("MyTool1", "2.1.3") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddHeader("MySubheader2").AddToolVersion("MyTool1", "2.1.3") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.AddedItems[0].CurrentReportNode.Version | Should -Be "2.1.3" + $comparer.AddedItems[0].Headers | Should -BeArray @("MyHeader", "MySubheader2") + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.DeletedItems[0].PreviousReportNode.Version | Should -Be "2.1.3" + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + $comparer.DeletedItems[0].Headers | Should -BeArray @("MyHeader", "MySubheader") + } + + It "Complex structure" { + $prevReport = [HeaderNode]::new("Version 1") + $prevSubHeader = $prevReport.AddHeader("MyHeader").AddHeader("MySubheader") + $prevSubHeader.AddToolVersion("MyTool1", "2.1.3") + $prevSubHeader.AddHeader("MySubSubheader").AddToolVersion("MyTool2", "2.9.1") + $prevReport.AddHeader("MyHeader2") + $prevReport.AddHeader("MyHeader3").AddHeader("MySubheader3").AddToolVersion("MyTool3", "14.2.1") + + $nextReport = [HeaderNode]::new("Version 2") + $nextSubHeader = $nextReport.AddHeader("MyHeader").AddHeader("MySubheader") + $nextSubHeader.AddToolVersion("MyTool1", "2.1.4") + $nextSubSubHeader = $nextSubHeader.AddHeader("MySubSubheader") + $nextSubSubHeader.AddToolVersion("MyTool2", "2.9.1") + $nextSubSubHeader.AddToolVersion("MyTool4", "2.7.6") + $nextReport.AddHeader("MyHeader2") + $nextReport.AddHeader("MyHeader3") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool4" + $comparer.AddedItems[0].CurrentReportNode.Version | Should -Be "2.7.6" + $comparer.AddedItems[0].Headers | Should -BeArray @("MyHeader", "MySubheader", "MySubSubheader") + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].PreviousReportNode.Version | Should -Be "2.1.3" + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].CurrentReportNode.Version | Should -Be "2.1.4" + $comparer.ChangedItems[0].Headers | Should -BeArray @("MyHeader", "MySubheader") + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool3" + $comparer.DeletedItems[0].PreviousReportNode.Version | Should -Be "14.2.1" + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + $comparer.DeletedItems[0].Headers | Should -BeArray @("MyHeader3", "MySubheader3") + } + } + + Describe "ToolVersionNode" { + It "ToolVersionNode is updated" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersion("MyTool1", "2.1.3") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersion("MyTool1", "2.1.4") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].PreviousReportNode.Version | Should -Be "2.1.3" + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionNode]) + $comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].CurrentReportNode.Version | Should -Be "2.1.4" + $comparer.ChangedItems[0].Headers | Should -BeArray @("MyHeader") + } + } + + Describe "ToolVersionsListNode" { + It "Single version is not changed" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3"), "^.+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3"), "^.+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + } + + It "Single version is changed" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3"), "^\d+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.4"), "^\d+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.1.3") + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.1.4") + } + + It "Major version is added" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3"), "^\d+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3", "3.1.4"), "^\d+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("3.1.4") + } + + It "Major version is removed" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3", "3.1.4"), "^\d+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("3.1.4"), "^\d+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.1.3") + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + } + + It "Major version is changed" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("3.1.4"), "^\d+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("3.2.0"), "^\d+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("3.1.4") + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("3.2.0") + } + + It "Major version is added, removed and updated at the same time" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("1.0.0", "2.1.3", "3.1.4", "4.0.2"), "^\d+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.1.3", "3.2.0", "4.0.2", "5.1.0"), "^\d+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("5.1.0") + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("3.1.4") + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("3.2.0") + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("1.0.0") + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + } + + It "Minor version is added, removed and updated at the same time" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.3.8", "2.4.9", "2.5.3", "2.6.0", "2.7.4", "2.8.0"), "^\d+\.\d+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.5.3", "2.6.2", "2.7.5", "2.8.0", "2.9.2", "2.10.3"), "^\d+\.\d+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.9.2", "2.10.3") + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.6.0", "2.7.4") + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.ChangedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.ChangedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.6.2", "2.7.5") + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.3.8", "2.4.9") + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + } + + It "Patch version is added, removed and updated at the same time" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.3.8", "2.4.9", "2.5.3", "2.6.0", "2.7.4"), "^\d+\.\d+\.\d+") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddToolVersionsList("MyTool1", @("2.4.9", "2.5.4", "2.6.0", "2.7.5", "2.8.2"), "^\d+\.\d+\.\d+") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.AddedItems[0].CurrentReportNode.ToolName | Should -Be "MyTool1" + $comparer.AddedItems[0].CurrentReportNode.Versions | Should -BeArray @("2.5.4", "2.7.5", "2.8.2") + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([ToolVersionsListNode]) + $comparer.DeletedItems[0].PreviousReportNode.ToolName | Should -Be "MyTool1" + $comparer.DeletedItems[0].PreviousReportNode.Versions | Should -BeArray @("2.3.8", "2.5.3", "2.7.4") + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + } + } + + Describe "TableNode" { + It "Rows are added" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2", "C1|C2", "D1|D2"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeOfType ([TableNode]) + $comparer.AddedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value" + $comparer.AddedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2") + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([TableNode]) + $comparer.AddedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value" + $comparer.AddedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2", "D1|D2") + } + + It "Rows are deleted" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2", "C1|C2", "D1|D2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("C1|C2", "D1|D2"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([TableNode]) + $comparer.DeletedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value" + $comparer.DeletedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2", "D1|D2") + $comparer.DeletedItems[0].CurrentReportNode | Should -BeOfType ([TableNode]) + $comparer.DeletedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value" + $comparer.DeletedItems[0].CurrentReportNode.Rows | Should -BeArray @("C1|C2", "D1|D2") + } + + It "Rows are changed" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B3|B4"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([TableNode]) + $comparer.ChangedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value" + $comparer.ChangedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2") + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([TableNode]) + $comparer.ChangedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value" + $comparer.ChangedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B3|B4") + } + + It "Rows are changed and updated at the same time" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B3|B4", "C1|C2"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([TableNode]) + $comparer.ChangedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value" + $comparer.ChangedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2") + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([TableNode]) + $comparer.ChangedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value" + $comparer.ChangedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B3|B4", "C1|C2") + } + + It "Rows are changed and removed at the same time" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2", "C1|C2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B3|B4"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 1 + $comparer.DeletedItems | Should -HaveCount 0 + + $comparer.ChangedItems[0].PreviousReportNode | Should -BeOfType ([TableNode]) + $comparer.ChangedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value" + $comparer.ChangedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2") + $comparer.ChangedItems[0].CurrentReportNode | Should -BeOfType ([TableNode]) + $comparer.ChangedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value" + $comparer.ChangedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B3|B4") + } + + It "Rows are not changed" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + } + + It "Rows are not changed but header is changed" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value2", @("A1|A2", "B1|B2"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + } + + It "Rows are changed and header is changed at the same time" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value", @("A1|A2", "B1|B2"))) + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddHeader("MyHeader").AddNode([TableNode]::new("Name|Value2", @("A1|A2", "B1|B2", "C1|C2"))) + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 1 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 1 + + $comparer.AddedItems[0].PreviousReportNode | Should -BeNullOrEmpty + $comparer.AddedItems[0].CurrentReportNode | Should -BeOfType ([TableNode]) + $comparer.AddedItems[0].CurrentReportNode.Headers | Should -Be "Name|Value2" + $comparer.AddedItems[0].CurrentReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2", "C1|C2") + + $comparer.DeletedItems[0].PreviousReportNode | Should -BeOfType ([TableNode]) + $comparer.DeletedItems[0].PreviousReportNode.Headers | Should -Be "Name|Value" + $comparer.DeletedItems[0].PreviousReportNode.Rows | Should -BeArray @("A1|A2", "B1|B2") + $comparer.DeletedItems[0].CurrentReportNode | Should -BeNullOrEmpty + } + } + + Describe "NoteNode" { + It "NoteNode is ignored from report" { + $prevReport = [HeaderNode]::new("Version 1") + $prevReport.AddNote("MyFirstNote") + $prevReport.AddHeader("MyFirstHeader").AddNote("MyFirstSubNote") + + $nextReport = [HeaderNode]::new("Version 2") + $nextReport.AddNote("MySecondNote") + $nextReport.AddHeader("MySecondHeader").AddNote("MySecondSubNote") + + $comparer = [SoftwareReportDifferenceCalculator]::new($prevReport, $nextReport) + $comparer.CompareReports() + + $comparer.AddedItems | Should -HaveCount 0 + $comparer.ChangedItems | Should -HaveCount 0 + $comparer.DeletedItems | Should -HaveCount 0 + } + } +} \ No newline at end of file diff --git a/helpers/software-report-base/tests/SoftwareReport.DifferenceRender.Unit.Tests.ps1 b/helpers/software-report-base/tests/SoftwareReport.DifferenceRender.Unit.Tests.ps1 new file mode 100644 index 000000000..92723f2c1 --- /dev/null +++ b/helpers/software-report-base/tests/SoftwareReport.DifferenceRender.Unit.Tests.ps1 @@ -0,0 +1,291 @@ +using module ../SoftwareReport.Nodes.psm1 +using module ../SoftwareReport.DifferenceRender.psm1 + +BeforeDiscovery { + Import-Module $(Join-Path $PSScriptRoot "TestHelpers.psm1") -DisableNameChecking +} + +Describe "ComparerReport.UnitTests" { + BeforeAll { + $script:DifferenceRender = [SoftwareReportDifferenceRender]::new() + } + + Context "CalculateHtmlTableRowSpan" { + It "Without the equal cells" { + $table = @( + [PSCustomObject]@{ Key = "A"; Value = "1" } + [PSCustomObject]@{ Key = "B"; Value = "2" } + [PSCustomObject]@{ Key = "C"; Value = "3" } + ) + + $actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Key") + $actual | Should -BeArray @(1, 1, 1) + } + + It "Only equal cells" { + $table = @( + [PSCustomObject]@{ Key = "A"; Value = "D" } + [PSCustomObject]@{ Key = "B"; Value = "D" } + [PSCustomObject]@{ Key = "C"; Value = "D" } + ) + + $actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Value") + $actual | Should -BeArray @(3, 0, 0) + } + + It "Single row" { + $table = @( + [PSCustomObject]@{ Key = "A"; Value = "1" } + ) + + $actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Key") + $actual | Should -BeArray @(1) + } + + It "Different cells" { + $table = @( + [PSCustomObject]@{ Key = "A"; Value = "1" } + [PSCustomObject]@{ Key = "B"; Value = "2" } + [PSCustomObject]@{ Key = "B"; Value = "3" } + [PSCustomObject]@{ Key = "C"; Value = "4" } + [PSCustomObject]@{ Key = "C"; Value = "5" } + [PSCustomObject]@{ Key = "C"; Value = "6" } + [PSCustomObject]@{ Key = "D"; Value = "7" } + [PSCustomObject]@{ Key = "E"; Value = "8" } + [PSCustomObject]@{ Key = "E"; Value = "9" } + [PSCustomObject]@{ Key = "F"; Value = "10" } + ) + + $actual = $DifferenceRender.CalculateHtmlTableRowSpan($table, "Key") + $actual | Should -BeArray @(1, 2, 0, 3, 0, 0, 1, 2, 0, 1) + } + } + + Context "RenderCategory" { + It "With line separator" { + $actual = $DifferenceRender.RenderCategory(@("Header 1", "Header 2", "Header 3"), $true) + $actual | Should -Be "Header 2 >
Header 3" + } + + It "Without line separator" { + $actual = $DifferenceRender.RenderCategory(@("Header 1", "Header 2", "Header 3"), $false) + $actual | Should -Be "Header 2 > Header 3" + } + + It "One header" { + $actual = $DifferenceRender.RenderCategory(@("Header 1"), $false) + $actual | Should -Be "" + } + + It "Empty headers" { + $actual = $DifferenceRender.RenderCategory(@(), $false) + $actual | Should -Be "" + } + } + + Context "RenderToolName" { + It "Clear tool name" { + $actual = $DifferenceRender.RenderToolName("My Tool 1") + $actual | Should -Be "My Tool 1" + } + + It "Name with colon symbol" { + $actual = $DifferenceRender.RenderToolName("My Tool 1:") + $actual | Should -Be "My Tool 1" + } + } + + Context "StrikeTableRow" { + It "Simple row" { + $actual = $DifferenceRender.StrikeTableRow("Test1|Test2|Test3") + $actual | Should -Be "~~Test1~~|~~Test2~~|~~Test3~~" + } + + It "Row with spaces" { + $actual = $DifferenceRender.StrikeTableRow("Test 1|Test 2|Test 3") + $actual | Should -Be "~~Test 1~~|~~Test 2~~|~~Test 3~~" + } + } + + Context "RenderHtmlTable" { + It "Simple table" { + $table = @( + [PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 1"; "Version" = "1.0" }, + [PSCustomObject]@{ "Category" = "B"; "Tool name" = "My Tool 2"; "Version" = "2.0" }, + [PSCustomObject]@{ "Category" = "C"; "Tool name" = "My Tool 3"; "Version" = "3.0" } + ) + + $renderedTable = $DifferenceRender.RenderHtmlTable($table, "Category") + $renderedTable | Should -Be @' + + + + + + + + + + + + + + + + + + + + + + + +
CategoryTool nameVersion
AMy Tool 11.0
BMy Tool 22.0
CMy Tool 33.0
+ +'@ + + } + + It "Table with the same category" { + $table = @( + [PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 1"; "Version" = "1.0" }, + [PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 2"; "Version" = "2.0" }, + [PSCustomObject]@{ "Category" = "A"; "Tool name" = "My Tool 3"; "Version" = "3.0" }, + [PSCustomObject]@{ "Category" = "B"; "Tool name" = "My Tool 4"; "Version" = "4.0" } + ) + + $renderedTable = $DifferenceRender.RenderHtmlTable($table, "Category") + $renderedTable | Should -Be @' + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryTool nameVersion
AMy Tool 11.0
My Tool 22.0
My Tool 33.0
BMy Tool 44.0
+ +'@ + + } + } + + Context "RenderTableNodesDiff" { + It "Add new table" { + $previousNode = $null + $currentNode = [TableNode]::new("Name|Value", @("A|1", "B|2")) + $reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3")) + + $actual = $DifferenceRender.RenderTableNodesDiff($reportItem) + $actual | Should -Be @' +#### Header 2 > Header 3 +| Name | Value | +| ---- | ----- | +| A | 1 | +| B | 2 | + +'@ + } + + It "Remove existing table" { + $previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2")) + $currentNode = $null + $reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3")) + + $actual = $DifferenceRender.RenderTableNodesDiff($reportItem) + $actual | Should -Be @' +#### Header 2 > Header 3 +| Name | Value | +| ----- | ----- | +| ~~A~~ | ~~1~~ | +| ~~B~~ | ~~2~~ | + +'@ + } + + It "Add new rows to existing table" { + $previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2")) + $currentNode = [TableNode]::new("Name|Value", @("A|1", "B|2", "C|3", "D|4")) + $reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3")) + + $actual = $DifferenceRender.RenderTableNodesDiff($reportItem) + $actual | Should -Be @' +#### Header 2 > Header 3 +| Name | Value | +| ---- | ----- | +| C | 3 | +| D | 4 | + +'@ + } + + It "Remove rows from existing table" { + $previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2", "C|3", "D|4")) + $currentNode = [TableNode]::new("Name|Value", @("C|3", "D|4")) + $reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3")) + + $actual = $DifferenceRender.RenderTableNodesDiff($reportItem) + $actual | Should -Be @' +#### Header 2 > Header 3 +| Name | Value | +| ----- | ----- | +| ~~A~~ | ~~1~~ | +| ~~B~~ | ~~2~~ | + +'@ + } + + It "Row is changed in existing table" { + $previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2")) + $currentNode = [TableNode]::new("Name|Value", @("A|1", "B|3")) + $reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3")) + + $actual = $DifferenceRender.RenderTableNodesDiff($reportItem) + $actual | Should -Be @' +#### Header 2 > Header 3 +| Name | Value | +| ----- | ----- | +| ~~B~~ | ~~2~~ | +| B | 3 | + +'@ + } + + It "Row is changed, added and removed at the same time in existing table" { + $previousNode = [TableNode]::new("Name|Value", @("A|1", "B|2", "C|3", "D|4")) + $currentNode = [TableNode]::new("Name|Value", @("B|2", "C|4", "D|4", "E|5")) + $reportItem = [ReportDifferenceItem]::new($previousNode, $currentNode, @("Header 1", "Header 2", "Header 3")) + + $actual = $DifferenceRender.RenderTableNodesDiff($reportItem) + $actual | Should -Be @' +#### Header 2 > Header 3 +| Name | Value | +| ----- | ----- | +| ~~A~~ | ~~1~~ | +| ~~C~~ | ~~3~~ | +| C | 4 | +| E | 5 | + +'@ + } + } +} \ No newline at end of file diff --git a/helpers/software-report-base/tests/SoftwareReport.E2E.Tests.ps1 b/helpers/software-report-base/tests/SoftwareReport.E2E.Tests.ps1 new file mode 100644 index 000000000..ca8b4626c --- /dev/null +++ b/helpers/software-report-base/tests/SoftwareReport.E2E.Tests.ps1 @@ -0,0 +1,93 @@ +using module ../SoftwareReport.psm1 +using module ../SoftwareReport.Nodes.psm1 + +Describe "SoftwareReport.E2E" { + Context "Report example 1" { + BeforeEach { + $softwareReport = [SoftwareReport]::new("macOS 11") + $softwareReport.Root.AddToolVersion("OS Version:", "macOS 11.7 (20G817)") + $softwareReport.Root.AddToolVersion("Image Version:", "20220918.1") + $installedSoftware = $softwareReport.Root.AddHeader("Installed Software") + + $languagesAndRuntimes = $installedSoftware.AddHeader("Language and Runtime") + $languagesAndRuntimes.AddToolVersion("Bash", "5.1.16(1)-release") + $languagesAndRuntimes.AddToolVersionsListInline(".NET Core SDK", @("1.2.100", "1.2.200", "3.1.414"), "^\d+\.\d+\.\d") + $languagesAndRuntimes.AddNode([ToolVersionNode]::new("Perl", "5.34.0")) + + $cachedTools = $installedSoftware.AddHeader("Cached Tools") + $cachedTools.AddToolVersionsList("Ruby", @("2.7.3", "2.8.1", "3.1.2"), "^\d+\.\d+") + $cachedTools.AddToolVersionsList("Node.js", @("14.8.0", "15.1.0", "16.4.2"), "^\d+") + + $javaSection = $installedSoftware.AddHeader("Java") + $javaSection.AddTable(@( + [PSCustomObject] @{ Version = "8.0.125"; Vendor = "My Vendor"; "Environment Variable" = "JAVA_HOME_8_X64" }, + [PSCustomObject] @{ Version = "11.3.103"; Vendor = "My Vendor"; "Environment Variable" = "JAVA_HOME_11_X64" } + )) + + $sqlSection = $installedSoftware.AddHeader("MySQL") + $sqlSection.AddToolVersion("MySQL", "6.1.0") + $sqlSection.AddNote("MySQL service is disabled by default.`nUse the following command as a part of your job to start the service: 'sudo systemctl start mysql.service'") + + $expectedMarkdown = @' +# macOS 11 +- OS Version: macOS 11.7 (20G817) +- Image Version: 20220918.1 + +## Installed Software + +### Language and Runtime +- Bash 5.1.16(1)-release +- .NET Core SDK: 1.2.100, 1.2.200, 3.1.414 +- Perl 5.34.0 + +### Cached Tools + +#### Ruby +- 2.7.3 +- 2.8.1 +- 3.1.2 + +#### Node.js +- 14.8.0 +- 15.1.0 +- 16.4.2 + +### Java +| Version | Vendor | Environment Variable | +| -------- | --------- | -------------------- | +| 8.0.125 | My Vendor | JAVA_HOME_8_X64 | +| 11.3.103 | My Vendor | JAVA_HOME_11_X64 | + +### MySQL +- MySQL 6.1.0 +``` +MySQL service is disabled by default. +Use the following command as a part of your job to start the service: 'sudo systemctl start mysql.service' +``` +'@ + } + + It "ToMarkdown" { + $softwareReport.ToMarkdown() | Should -Be $expectedMarkdown + } + + It "Serialization + Deserialization" { + $json = $softwareReport.ToJson() + $deserializedReport = [SoftwareReport]::FromJson($json) + $deserializedReport.ToMarkdown() | Should -Be $expectedMarkdown + } + } + + Context "GetImageVersion" { + It "Image version exists" { + $softwareReport = [SoftwareReport]::new("MyReport") + $softwareReport.Root.AddToolVersion("Image Version:", "123.4") + $softwareReport.GetImageVersion() | Should -Be "123.4" + } + + It "Empty report" { + $softwareReport = [SoftwareReport]::new("MyReport") + $softwareReport.GetImageVersion() | Should -Be "Unknown version" + } + } +} \ No newline at end of file diff --git a/helpers/software-report-base/tests/SoftwareReport.Nodes.Unit.Tests.ps1 b/helpers/software-report-base/tests/SoftwareReport.Nodes.Unit.Tests.ps1 new file mode 100644 index 000000000..58f57d397 --- /dev/null +++ b/helpers/software-report-base/tests/SoftwareReport.Nodes.Unit.Tests.ps1 @@ -0,0 +1,511 @@ +using module ../SoftwareReport.Nodes.psm1 + +BeforeDiscovery { + Import-Module $(Join-Path $PSScriptRoot "TestHelpers.psm1") -DisableNameChecking +} + +Describe "Nodes.UnitTests" { + Context "ToolVersionNode" { + It "ToMarkdown" { + $node = [ToolVersionNode]::new("MyTool", "2.1.3") + $node.ToMarkdown() | Should -Be "- MyTool 2.1.3" + } + + It "GetValue" { + $node = [ToolVersionNode]::new("MyTool", "2.1.3") + $node.GetValue() | Should -Be "2.1.3" + } + + It "Serialization" { + $node = [ToolVersionNode]::new("MyTool", "2.1.3") + $json = $node.ToJsonObject() + $json.NodeType | Should -Be "ToolVersionNode" + $json.ToolName | Should -Be "MyTool" + $json.Version | Should -Be "2.1.3" + } + + It "Deserialization" { + { [ToolVersionNode]::FromJsonObject(@{ NodeType = "ToolVersionNode"; ToolName = ""; Version = "2.1.3" }) } | Should -Throw '*Exception setting "ToolName": "The argument is null or empty.*' + { [ToolVersionNode]::FromJsonObject(@{ NodeType = "ToolVersionNode"; ToolName = "MyTool"; Version = "" }) } | Should -Throw '*Exception setting "Version": "The argument is null or empty.*' + { [ToolVersionNode]::FromJsonObject(@{ NodeType = "ToolVersionNode"; ToolName = "MyTool"; Version = "2.1.3" }) } | Should -Not -Throw + } + + It "Serialization + Deserialization" { + $node = [ToolVersionNode]::new("MyTool", "2.1.3") + $json = $node.ToJsonObject() + $node2 = [ToolVersionNode]::FromJsonObject($json) + $json2 = $node2.ToJsonObject() + $($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json) + } + + It "IsSimilarTo" { + [ToolVersionNode]::new("MyTool", "2.1.3").IsSimilarTo([ToolVersionNode]::new("MyTool", "2.1.3")) | Should -BeTrue + [ToolVersionNode]::new("MyTool", "2.1.3").IsSimilarTo([ToolVersionNode]::new("MyTool", "1.0.0")) | Should -BeTrue + [ToolVersionNode]::new("MyTool", "2.1.3").IsSimilarTo([ToolVersionNode]::new("MyTool2", "2.1.3")) | Should -BeFalse + } + + It "IsIdenticalTo" { + [ToolVersionNode]::new("MyTool", "2.1.3").IsIdenticalTo([ToolVersionNode]::new("MyTool", "2.1.3")) | Should -BeTrue + [ToolVersionNode]::new("MyTool", "2.1.3").IsIdenticalTo([ToolVersionNode]::new("MyTool", "1.0.0")) | Should -BeFalse + [ToolVersionNode]::new("MyTool", "2.1.3").IsIdenticalTo([ToolVersionNode]::new("MyTool2", "2.1.3")) | Should -BeFalse + } + } + + Context "ToolVersionsListNode" { + It "ToMarkdown - List" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List") + $expected = @( + "", + "# MyTool" + "- 2.7.7" + "- 3.0.5" + "- 3.1.3" + ) -join "`n" + $node.ToMarkdown() | Should -Be $expected + } + + It "ToMarkdown - Inline" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "Inline") + $node.ToMarkdown() | Should -Be "- MyTool: 2.7.7, 3.0.5, 3.1.3" + } + + It "GetValue" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List") + $node.GetValue() | Should -Be "2.7.7, 3.0.5, 3.1.3" + } + + It "Serialization - List" { + $node = [ToolVersionsListNode]::new("Ruby", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List") + $json = $node.ToJsonObject() + $json.NodeType | Should -Be "ToolVersionsListNode" + $json.ToolName | Should -Be "Ruby" + $json.Versions | Should -BeArray @("2.7.7", "3.0.5", "3.1.3") + $json.MajorVersionRegex | Should -Be "^.+" + $json.ListType | Should -Be "List" + } + + It "Serialization - Inline" { + $node = [ToolVersionsListNode]::new("Ruby", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "Inline") + $json = $node.ToJsonObject() + $json.NodeType | Should -Be "ToolVersionsListNode" + $json.ToolName | Should -Be "Ruby" + $json.Versions | Should -BeArray @("2.7.7", "3.0.5", "3.1.3") + $json.MajorVersionRegex | Should -Be "^.+" + $json.ListType | Should -Be "Inline" + } + + It "Deserialization" { + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = ""; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw '*Exception setting "ToolName": "The argument is null or empty.*' + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw '*Exception setting "Versions": "The argument is null or empty.*' + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @(); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw '*Exception setting "Versions": "The argument is null, empty,*' + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", '2.2.4'); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Throw 'Multiple versions from list * return the same result from regex *' + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = ""; ListType = "List" }) } | Should -Throw 'Version * doesn''t match regex *' + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "Fake" }) } | Should -Throw '*Exception setting "ListType": "The argument * does not belong to the set*' + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "List" }) } | Should -Not -Throw + { [ToolVersionsListNode]::FromJsonObject(@{ NodeType = "ToolVersionsListNode"; ToolName = "MyTool"; Versions = @("2.1.3", "3.1.4"); MajorVersionRegex = "^\d+"; ListType = "Inline" }) } | Should -Not -Throw + } + + It "Serialization + Deserialization" { + $node = [ToolVersionsListNode]::new("Ruby", @("2.7.7", "3.0.5", "3.1.3"), "^.+", "List") + $json = $node.ToJsonObject() + $node2 = [ToolVersionsListNode]::FromJsonObject($json) + $json2 = $node2.ToJsonObject() + $($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json) + } + + It "IsSimilarTo" { + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsSimilarTo( + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List") + ) | Should -BeTrue + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsSimilarTo( + [ToolVersionsListNode]::new("MyTool", @("2.1.5", "5.0.0"), "^.+", "List") + ) | Should -BeTrue + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsSimilarTo( + [ToolVersionsListNode]::new("MyTool2", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List") + ) | Should -BeFalse + } + + It "IsIdenticalTo" { + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsIdenticalTo( + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List") + ) | Should -BeTrue + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsIdenticalTo( + [ToolVersionsListNode]::new("MyTool", @("2.1.5", "5.0.0"), "^.+", "List") + ) | Should -BeFalse + [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List").IsIdenticalTo( + [ToolVersionsListNode]::new("MyTool2", @("2.1.3", "3.1.5", "4.0.0"), "^.+", "List") + ) | Should -BeFalse + } + + It "ExtractMajorVersion" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^\d+\.\d+", "List") + $node.ExtractMajorVersion("2.1.3") | Should -Be "2.1" + $node.ExtractMajorVersion("3.1.5") | Should -Be "3.1" + $node.ExtractMajorVersion("4.0.0") | Should -Be "4.0" + } + + Context "ValidateMajorVersionRegex" { + It "Major version regex - unique versions" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "4.0.0"), "^\d+", "List") + $node.Versions | Should -BeArray @("2.1.3", "3.1.5", "4.0.0") + } + + It "Major version regex - non-unique versions" { + { [ToolVersionsListNode]::new("MyTool", @("2.1.3", "3.1.5", "3.2.0", "4.0.0"), "^\d+", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *" + } + + It "Minor version regex - unique versions" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.4.0", "3.1.2"), "^\d+\.\d+", "List") + $node.Versions | Should -BeArray @("2.1.3", "2.4.0", "3.1.2") + } + + It "Minor version regex - non-unique versions" { + { [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.1.4", "3.1.2"), "^\d+\.\d+", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *" + } + + It "Patch version regex - unique versions" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.1.4", "2.1.5"), "^\d+\.\d+\.\d+", "List") + $node.Versions | Should -BeArray @("2.1.3", "2.1.4", "2.1.5") + } + + It "Patch version regex - non-unique versions" { + { [ToolVersionsListNode]::new("MyTool", @("2.1.3", "2.1.4", "2.1.4"), "^\d+\.\d+\.\d+", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *" + } + + It ".NET Core version regex - unique versions" { + $node = [ToolVersionsListNode]::new("MyTool", @("2.1.100", "2.1.205", "2.1.303"), "^\d+\.\d+\.\d", "List") + $node.Versions | Should -BeArray @("2.1.100", "2.1.205", "2.1.303") + } + + It ".NET Core version regex - non-unique versions" { + { [ToolVersionsListNode]::new("MyTool", @("2.1.100", "2.1.205", "2.1.230", "3.1.0"), "^\d+\.\d+\.\d", "List") } | Should -Throw "Multiple versions from list * return the same result from regex *" + } + } + } + + Context "TableNode" { + Context "ToMarkdown" { + It "Simple table" { + $node = [TableNode]::new("Name|Value", @("A|B", "C|D")) + $node.ToMarkdown() | Should -Be @' +| Name | Value | +| ---- | ----- | +| A | B | +| C | D | +'@ + } + + It "Wide cells" { + $node = [TableNode]::new("Name|Value", @("Very long value here|B", "C|And very long value here too")) + $node.ToMarkdown() | Should -Be @' +| Name | Value | +| -------------------- | ---------------------------- | +| Very long value here | B | +| C | And very long value here too | +'@ + } + } + + It "CalculateColumnsWidth" { + [TableNode]::new("Name|Value", @("A|B", "C|D")).CalculateColumnsWidth() | Should -BeArray @(4, 5) + [TableNode]::new("Name|Value", @("Very long value here|B", "C|And very long value here too")).CalculateColumnsWidth() | Should -BeArray @(20, 28) + } + + It "Serialization" { + $node = [TableNode]::new("Name|Value", @("A|B", "C|D")) + $json = $node.ToJsonObject() + $json.NodeType | Should -Be "TableNode" + $json.Headers | Should -Be "Name|Value" + $json.Rows | Should -BeArray @("A|B", "C|D") + } + + It "Deserialization" { + { [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = ""; Rows = @("A|1", "B|2") }) } | Should -Throw 'Exception setting "Headers": "The argument is null or empty. *' + { [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = "Name|Value"; Rows = @() }) } | Should -Throw 'Exception setting "Rows": "The argument is null, empty, *' + { [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = "Name|Value"; Rows = @("A|1", "B|2|T", "C|3") }) } | Should -Throw 'Table has different number of columns in different rows' + { [TableNode]::FromJsonObject(@{ NodeType = "TableNode"; Headers = "Name|Value"; Rows = @("A|1", "B|2") }) } | Should -Not -Throw + } + + It "Serialization + Deserialization" { + $node = [TableNode]::new("Name|Value", @("A|B", "C|D")) + $json = $node.ToJsonObject() + $node2 = [TableNode]::FromJsonObject($json) + $json2 = $node2.ToJsonObject() + $($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json) + } + + It "IsSimilarTo" { + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Value", @("A|B", "C|D"))) | Should -BeTrue + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Value", @("A|B", "C|D", "F|W"))) | Should -BeTrue + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Value", @("A|B", "C|E"))) | Should -BeTrue + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsSimilarTo([TableNode]::new("Name|Key", @("A|B", "C|D"))) | Should -BeTrue + } + + It "IsIdenticalTo" { + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Value", @("A|B", "C|D"))) | Should -BeTrue + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Key", @("A|B", "C|D"))) | Should -BeTrue + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Value", @("A|B", "C|D", "F|W"))) | Should -BeFalse + [TableNode]::new("Name|Value", @("A|B", "C|D")).IsIdenticalTo([TableNode]::new("Name|Value", @("A|B", "C|E"))) | Should -BeFalse + } + + Context "FromObjectsArray" { + It "Correct table" { + $table = @( + [PSCustomObject]@{Name = "A"; Value = "B"} + [PSCustomObject]@{Name = "C"; Value = "D"} + ) + + $tableNode = [TableNode]::FromObjectsArray($table) + $tableNode.Headers | Should -Be "Name|Value" + $tableNode.Rows | Should -BeArray @("A|B", "C|D") + } + + It "Correct table with spaces" { + $table = @( + [PSCustomObject]@{Name = "A B"; "My Value" = "1 2"} + [PSCustomObject]@{Name = "C D"; "My Value" = "3 4"} + ) + + $tableNode = [TableNode]::FromObjectsArray($table) + $tableNode.Headers | Should -Be "Name|My Value" + $tableNode.Rows | Should -BeArray @("A B|1 2", "C D|3 4") + } + + It "Throw on empty table" { + { [TableNode]::FromObjectsArray(@()) } | Should -Throw "Failed to create TableNode from empty objects array" + } + + It "Throw on table with different columns" { + $table = @( + [PSCustomObject]@{Name = "A"; Value = "B"} + [PSCustomObject]@{Name = "C"; Value2 = "D"} + ) + + { [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode from objects array because objects have different properties" + } + + It "Throw on empty row" { + $table = @( + [PSCustomObject]@{Name = "A"; Value = "B"}, + [PSCustomObject]@{}, + [PSCustomObject]@{Name = "C"; Value2 = "D"} + ) + + { [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode because some objects are empty" + } + + It "Throw on incorrect symbols in table column names" { + $table = @( + [PSCustomObject]@{"Name|War" = "A"; Value = "B"} + [PSCustomObject]@{"Name|War" = "C"; Value = "D"} + ) + + { [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode because some cells * contains forbidden symbol*" + } + + It "Throw on incorrect symbols in table rows" { + $table = @( + [PSCustomObject]@{Name = "A"; Value = "B|AA"} + [PSCustomObject]@{Name = "C"; Value = "D"} + ) + + { [TableNode]::FromObjectsArray($table) } | Should -Throw "Failed to create TableNode because some cells * contains forbidden symbol*" + } + } + } + + Context "NoteNode" { + It "ToMarkdown" { + $node = [NoteNode]::new("Hello world`nGood Bye world") + $node.ToMarkdown() | Should -Be @' +``` +hello world +Good Bye world +``` +'@ + } + + It "Serialization" { + $node = [NoteNode]::new("MyContent`nMyContent2") + $json = $node.ToJsonObject() + $json.NodeType | Should -Be "NoteNode" + $json.Content | Should -Be "MyContent`nMyContent2" + } + + It "Deserialization" { + { [NoteNode]::FromJsonObject(@{ NodeType = "NoteNode" }) } | Should -Throw '*Exception setting "Content": "The argument is null or empty.*' + { [NoteNode]::FromJsonObject(@{ NodeType = "NoteNode"; Content = "" }) } | Should -Throw '*Exception setting "Content": "The argument is null or empty.*' + { [NoteNode]::FromJsonObject(@{ NodeType = "NoteNode"; Content = "MyTool" }) } | Should -Not -Throw + } + + It "Serialization + Deserialization" { + $node = [NoteNode]::new("MyContent`nMyContent2") + $json = $node.ToJsonObject() + $node2 = [NoteNode]::FromJsonObject($json) + $json2 = $node2.ToJsonObject() + $($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json) + } + + It "IsSimilarTo" { + [NoteNode]::new("MyContent").IsSimilarTo([NoteNode]::new("MyContent")) | Should -BeTrue + [NoteNode]::new("MyContent").IsSimilarTo([NoteNode]::new("MyContent2")) | Should -BeFalse + } + + It "IsIdenticalTo" { + [NoteNode]::new("MyContent").IsIdenticalTo([NoteNode]::new("MyContent")) | Should -BeTrue + [NoteNode]::new("MyContent").IsIdenticalTo([NoteNode]::new("MyContent2")) | Should -BeFalse + } + } + + Context "HeaderNode" { + It "ToMarkdown" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersion("MyTool", "2.1.3") + $node.ToMarkdown(1) | Should -Be @' + +# MyHeader +- MyTool 2.1.3 +'@ + } + + It "ToMarkdown (level 3)" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersion("MyTool", "2.1.3") + $node.ToMarkdown(3) | Should -Be @' + +### MyHeader +- MyTool 2.1.3 +'@ + } + + It "ToMarkdown (multiple levels)" { + $node = [HeaderNode]::new("MyHeader") + $node.AddHeader("MyHeader 2").AddHeader("MyHeader 3").AddHeader("MyHeader 4").AddToolVersion("MyTool", "2.1.3") + $node.ToMarkdown(1) | Should -Be @' + +# MyHeader + +## MyHeader 2 + +### MyHeader 3 + +#### MyHeader 4 +- MyTool 2.1.3 +'@ + } + + It "Serialization" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersion("MyTool", "2.1.3") + $json = $node.ToJsonObject() + $json.NodeType | Should -Be "HeaderNode" + $json.Title | Should -Be "MyHeader" + $json.Children | Should -HaveCount 1 + } + + It "Deserialization" { + { [HeaderNode]::FromJsonObject(@{ NodeType = "HeaderNode" }) } | Should -Throw '*Exception setting "Title": "The argument is null or empty.*' + { [HeaderNode]::FromJsonObject(@{ NodeType = "HeaderNode"; Title = "" }) } | Should -Throw '*Exception setting "Title": "The argument is null or empty.*' + { [HeaderNode]::FromJsonObject(@{ NodeType = "HeaderNode"; Title = "MyHeader" }) } | Should -Not -Throw + } + + It "Serialization + Deserialization" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersion("MyTool", "2.1.3") + $json = $node.ToJsonObject() + $node2 = [HeaderNode]::FromJsonObject($json) + $json2 = $node2.ToJsonObject() + $($json | ConvertTo-Json) | Should -Be $($json2 | ConvertTo-Json) + } + + It "IsSimilarTo" { + [HeaderNode]::new("MyHeader").IsSimilarTo([HeaderNode]::new("MyHeader")) | Should -BeTrue + [HeaderNode]::new("MyHeader").IsSimilarTo([HeaderNode]::new("MyHeader2")) | Should -BeFalse + } + + It "IsIdenticalTo" { + [HeaderNode]::new("MyHeader").IsIdenticalTo([HeaderNode]::new("MyHeader")) | Should -BeTrue + [HeaderNode]::new("MyHeader").IsIdenticalTo([HeaderNode]::new("MyHeader2")) | Should -BeFalse + } + + It "FindSimilarChildNode" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersion("MyTool", "2.1.3") + + $node.FindSimilarChildNode([ToolVersionNode]::new("MyTool", "1.0.0")) | Should -Not -BeNullOrEmpty + $node.FindSimilarChildNode([ToolVersionNode]::New("MyTool2", "1.0.0")) | Should -BeNullOrEmpty + } + + Context "Detect node duplicates" { + It "Similar HeaderNode on the same header" { + $node = [HeaderNode]::new("MyHeader") + $node.AddHeader("MySubHeader1") + $node.AddHeader("MySubHeader2") + { $node.AddHeader("MySubHeader1") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*" + } + + It "Similar ToolVersionNode on the same header" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersion("MyTool", "2.1.3") + $node.AddToolVersion("MyTool2", "2.1.3") + { $node.AddToolVersion("MyTool", "2.1.3") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*" + } + + It "Similar ToolVersionsListNode on the same header" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersionsList("MyTool", @("2.1.3", "3.0.0"), "^\d+") + $node.AddToolVersionsListInline("MyTool2", @("2.1.3", "3.0.0"), "^\d+") + { $node.AddToolVersionsList("MyTool", @("2.1.3", "3.0.0"), "^\d+") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*" + } + + It "Similar TableNode on the same header" { + $node = [HeaderNode]::new("MyHeader") + $node.AddTable(@( + [PSCustomObject]@{Name = "Value1"}, + [PSCustomObject]@{Name = "Value2"} + )) + { + $node.AddTable(@( + [PSCustomObject]@{Name = "Value1"}, + [PSCustomObject]@{Name = "Value2"} + )) + } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*" + } + + It "Similar NoteNode on the same header" { + $node = [HeaderNode]::new("MyHeader") + $node.AddNote("MyContent") + $node.AddNote("MyContent2") + { $node.AddNote("MyContent") } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*" + } + + It "AddNode detects duplicates" { + $node = [HeaderNode]::new("MyHeader") + $node.AddNode([ToolVersionNode]::new("MyTool", "2.1.3")) + { $node.AddNode([ToolVersionNode]::new("MyTool", "2.1.3")) } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*" + } + + It "AddNodes detects duplicates" { + $node = [HeaderNode]::new("MyHeader") + $node.AddNodes(@( + [ToolVersionNode]::new("MyTool", "2.1.3"), + [ToolVersionNode]::new("MyTool2", "2.1.4") + )) + { + $node.AddNodes(@( + [ToolVersionNode]::new("MyTool3", "2.1.5"), + [ToolVersionNode]::new("MyTool", "2.1.3") + )) + } | Should -Throw "This HeaderNode already contains the similar child node. It is not allowed to add the same node twice.*" + } + + It "Doesn't allow adding non-header nodes after header node" { + $node = [HeaderNode]::new("MyHeader") + $node.AddToolVersion("MyTool", "2.1.3") + $node.AddHeader("MySubHeader") + { $node.AddToolVersion("MyTool2", "2.1.4") } | Should -Throw "It is not allowed to add the node of type * to the HeaderNode that already contains the HeaderNode children." + { $node.AddHeader("MySubHeader2") } | Should -Not -Throw + } + } + } +} \ No newline at end of file diff --git a/helpers/software-report-base/tests/TestHelpers.psm1 b/helpers/software-report-base/tests/TestHelpers.psm1 new file mode 100644 index 000000000..8918a2942 --- /dev/null +++ b/helpers/software-report-base/tests/TestHelpers.psm1 @@ -0,0 +1,34 @@ +function ShouldBeArray([Array] $ActualValue, [Array]$ExpectedValue, [Switch] $Negate, [String] $Because) { + if ($Negate) { + throw "Negation is not supported for Should-BeArray" + } + + if ($ExpectedValue.Count -eq 0) { + throw "Expected array cannot be empty. Use Should-BeNullOrEmpty instead." + } + + $ExpectedValue | ForEach-Object { + if ($_.GetType() -notin @([String], [Int32])) { + throw "Only string or int arrays are supported in Should-BeArray" + } + } + + $actualValueJson = $ActualValue | ConvertTo-Json + $expectedValueJson = $ExpectedValue | ConvertTo-Json + + $succeeded = ($ActualValue.Count -eq $ExpectedValue.Count) -and ($actualValueJson -eq $expectedValueJson) + + if (-not $succeeded) { + $failureMessage = "Expected array '$actualValueJson' to be equal to '$expectedValueJson'" + } + + return [PSCustomObject]@{ + Succeeded = $succeeded + FailureMessage = $failureMessage + } +} + +Add-ShouldOperator -Name BeArray ` + -InternalName 'ShouldBeArray' ` + -Test ${function:ShouldBeArray} ` + -SupportsArrayInput \ No newline at end of file diff --git a/images/macos/software-report/SoftwareReport.Generator.ps1 b/images/macos/software-report/SoftwareReport.Generator.ps1 index 9acb317f1..7065c84b5 100644 --- a/images/macos/software-report/SoftwareReport.Generator.ps1 +++ b/images/macos/software-report/SoftwareReport.Generator.ps1 @@ -33,7 +33,7 @@ $installedSoftware = $softwareReport.Root.AddHeader("Installed Software") # Language and Runtime $languageAndRuntime = $installedSoftware.AddHeader("Language and Runtime") -$languageAndRuntime.AddToolVersionsList(".NET Core SDK", $(Get-DotnetVersionList), '^\d+\.\d+\.\d', $true) +$languageAndRuntime.AddToolVersionsListInline(".NET Core SDK", $(Get-DotnetVersionList), '^\d+\.\d+\.\d') $languageAndRuntime.AddToolVersion("Bash", $(Get-BashVersion)) $languageAndRuntime.AddNodes($(Get-ClangLLVMVersions)) $languageAndRuntime.AddNodes($(Get-GccVersions)) @@ -45,7 +45,7 @@ $languageAndRuntime.AddToolVersion("Mono", $(Get-MonoVersion)) $languageAndRuntime.AddToolVersion("MSBuild", $(Get-MSBuildVersion)) $languageAndRuntime.AddToolVersion("Node.js", $(Get-NodeVersion)) $languageAndRuntime.AddToolVersion("NVM", $(Get-NVMVersion)) -$languageAndRuntime.AddToolVersionsList("NVM - Cached node versions", $(Get-NVMNodeVersionList), '^\d+', $true) +$languageAndRuntime.AddToolVersionsListInline("NVM - Cached node versions", $(Get-NVMNodeVersionList), '^\d+') $languageAndRuntime.AddToolVersion("Perl", $(Get-PerlVersion)) $languageAndRuntime.AddToolVersion("PHP", $(Get-PHPVersion)) $languageAndRuntime.AddToolVersion("Python", $(Get-PythonVersion)) diff --git a/images/macos/software-report/SoftwareReport.Toolcache.psm1 b/images/macos/software-report/SoftwareReport.Toolcache.psm1 index cad7873f2..a7296b203 100644 --- a/images/macos/software-report/SoftwareReport.Toolcache.psm1 +++ b/images/macos/software-report/SoftwareReport.Toolcache.psm1 @@ -35,11 +35,11 @@ function Get-ToolcacheGoVersions { function Build-ToolcacheSection { return @( - [ToolVersionsListNode]::new("Ruby", $(Get-ToolcacheRubyVersions), '^\d+\.\d+', $false), - [ToolVersionsListNode]::new("Python", $(Get-ToolcachePythonVersions), '^\d+\.\d+', $false), - [ToolVersionsListNode]::new("PyPy", $(Get-ToolcachePyPyVersions), '^\d+\.\d+', $false), - [ToolVersionsListNode]::new("Node.js", $(Get-ToolcacheNodeVersions), '^\d+', $false), - [ToolVersionsListNode]::new("Go", $(Get-ToolcacheGoVersions), '^\d+\.\d+', $false) + [ToolVersionsListNode]::new("Ruby", $(Get-ToolcacheRubyVersions), '^\d+\.\d+', "List"), + [ToolVersionsListNode]::new("Python", $(Get-ToolcachePythonVersions), '^\d+\.\d+', "List"), + [ToolVersionsListNode]::new("PyPy", $(Get-ToolcachePyPyVersions), '^\d+\.\d+', "List"), + [ToolVersionsListNode]::new("Node.js", $(Get-ToolcacheNodeVersions), '^\d+', "List"), + [ToolVersionsListNode]::new("Go", $(Get-ToolcacheGoVersions), '^\d+\.\d+', "List") ) } @@ -48,6 +48,6 @@ function Get-PowerShellModules { $modules | ForEach-Object { $moduleName = $_ $moduleVersions = Get-Module -Name $moduleName -ListAvailable | Select-Object -ExpandProperty Version | Sort-Object -Unique - return [ToolVersionsListNode]::new($moduleName, $moduleVersions, '^\d+', $true) + return [ToolVersionsListNode]::new($moduleName, $moduleVersions, '^\d+', "Inline") } } \ No newline at end of file