Implement tests for software-report-module (#6815)

* Minor improvements

* fix typos

* fix brew rendering

* add temp test

* Implement tests

* Add arguments validation

* ToMarkdown()

* Use before-All and helpers

* Get rid of arrays

* Add validation, no new nodes after header

* Fix naming

* add workflow

* Revisit comments + tiny improvements

* Fix tables

* Fix html table indent

* remove comment

* attemp to break test - testing CI

* revert breaking test

* fix nitpicks
This commit is contained in:
Maxim Lobanov
2022-12-21 10:58:27 +01:00
committed by GitHub
parent bc38aa4173
commit 4aeccc7b5b
16 changed files with 2597 additions and 430 deletions

View File

@@ -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) {