mirror of
https://github.com/actions/runner-images-sangeeth.git
synced 2025-12-10 11:41:32 +00:00
439 lines
14 KiB
PowerShell
439 lines
14 KiB
PowerShell
using module ./SoftwareReport.BaseNodes.psm1
|
|
|
|
#########################################
|
|
### Nodes to describe image software ####
|
|
#########################################
|
|
|
|
# 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([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)'"
|
|
}
|
|
}
|
|
|
|
class HeaderNode: BaseNode {
|
|
[ValidateNotNullOrEmpty()]
|
|
[String] $Title
|
|
[Collections.Generic.List[BaseNode]] $Children
|
|
|
|
HeaderNode([String] $Title) {
|
|
$this.Title = $Title
|
|
$this.Children = @()
|
|
}
|
|
|
|
[Boolean] ShouldBeIncludedToDiff() {
|
|
return $true
|
|
}
|
|
|
|
[void] AddNode([BaseNode] $node) {
|
|
$similarNode = $this.FindSimilarChildNode($node)
|
|
if ($similarNode) {
|
|
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)"
|
|
}
|
|
|
|
if (-not $this.IsNodeHasMarkdownHeader($node)) {
|
|
# If the node doesn't print own header to markdown, we should check that there is no other nodes that print header to markdown before it.
|
|
# It is done to avoid unexpected situation like this:
|
|
#
|
|
# HeaderNode A -> # A
|
|
# HeaderNode B -> ## B
|
|
# ToolVersionNode C -> - C
|
|
# ToolVersionNode D -> - D
|
|
#
|
|
# In this example, we add 'HeaderNode B" to 'HeaderNode A' and add 'ToolVersionNode C' to 'HeaderNode B'.
|
|
# Then we add 'ToolVersionNode D' to 'HeaderNode A'.
|
|
# But the result markdown will look like 'ToolVersionNode D' belongs to 'HeaderNode B' instead of 'HeaderNode A'.
|
|
$this.Children | Where-Object { $this.IsNodeHasMarkdownHeader($_) } | ForEach-Object {
|
|
throw "It is not allowed to add the non-header node after the header node. Consider adding the separate HeaderNode for this node"
|
|
}
|
|
}
|
|
|
|
$this.Children.Add($node)
|
|
}
|
|
|
|
[void] AddNodes([BaseNode[]] $nodes) {
|
|
$nodes | ForEach-Object {
|
|
$this.AddNode($_)
|
|
}
|
|
}
|
|
|
|
[HeaderNode] AddHeader([String] $Title) {
|
|
$node = [HeaderNode]::new($Title)
|
|
$this.AddNode($node)
|
|
return $node
|
|
}
|
|
|
|
[void] AddToolVersion([String] $ToolName, [String] $Version) {
|
|
$this.AddNode([ToolVersionNode]::new($ToolName, $Version))
|
|
}
|
|
|
|
[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([PSCustomObject[]] $Table) {
|
|
$this.AddNode([TableNode]::FromObjectsArray($Table))
|
|
}
|
|
|
|
[void] AddNote([String] $Content) {
|
|
$this.AddNode([NoteNode]::new($Content))
|
|
}
|
|
|
|
[String] ToMarkdown([Int32] $Level) {
|
|
$sb = [System.Text.StringBuilder]::new()
|
|
$sb.AppendLine()
|
|
$sb.AppendLine("$("#" * $Level) $($this.Title)")
|
|
$this.Children | ForEach-Object {
|
|
$sb.AppendLine($_.ToMarkdown($Level + 1))
|
|
}
|
|
|
|
return $sb.ToString().TrimEnd()
|
|
}
|
|
|
|
[PSCustomObject] ToJsonObject() {
|
|
return [PSCustomObject]@{
|
|
NodeType = $this.GetType().Name
|
|
Title = $this.Title
|
|
Children = $this.Children | ForEach-Object { $_.ToJsonObject() }
|
|
}
|
|
}
|
|
|
|
static [HeaderNode] FromJsonObject([Object] $JsonObj) {
|
|
$node = [HeaderNode]::new($JsonObj.Title)
|
|
$JsonObj.Children | Where-Object { $_ } | ForEach-Object { $node.AddNode([NodesFactory]::ParseNodeFromObject($_)) }
|
|
return $node
|
|
}
|
|
|
|
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
|
|
if ($OtherNode.GetType() -ne [HeaderNode]) {
|
|
return $false
|
|
}
|
|
|
|
return $this.Title -eq $OtherNode.Title
|
|
}
|
|
|
|
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
|
|
return $this.IsSimilarTo($OtherNode)
|
|
}
|
|
|
|
[BaseNode] FindSimilarChildNode([BaseNode] $Find) {
|
|
foreach ($childNode in $this.Children) {
|
|
if ($childNode.IsSimilarTo($Find)) {
|
|
return $childNode
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
hidden [Boolean] IsNodeHasMarkdownHeader([BaseNode] $node) {
|
|
if ($node -is [HeaderNode]) {
|
|
return $true
|
|
}
|
|
|
|
if (($node -is [ToolVersionsListNode]) -and ($node.ListType -eq "List")) {
|
|
return $true
|
|
}
|
|
|
|
return $false
|
|
}
|
|
}
|
|
|
|
class ToolVersionNode: BaseToolNode {
|
|
[ValidateNotNullOrEmpty()]
|
|
[String] $Version
|
|
|
|
ToolVersionNode([String] $ToolName, [String] $Version): base($ToolName) {
|
|
|
|
if ([String]::IsNullOrEmpty($Version)) {
|
|
throw "ToolVersionNode '$($this.ToolName)' has empty version"
|
|
}
|
|
|
|
$this.Version = $Version
|
|
}
|
|
|
|
[String] ToMarkdown([Int32] $Level) {
|
|
return "- $($this.ToolName) $($this.Version)"
|
|
}
|
|
|
|
[String] GetValue() {
|
|
return $this.Version
|
|
}
|
|
|
|
[PSCustomObject] ToJsonObject() {
|
|
return [PSCustomObject]@{
|
|
NodeType = $this.GetType().Name
|
|
ToolName = $this.ToolName
|
|
Version = $this.Version
|
|
}
|
|
}
|
|
|
|
static [BaseNode] FromJsonObject([Object] $JsonObj) {
|
|
return [ToolVersionNode]::new($JsonObj.ToolName, $JsonObj.Version)
|
|
}
|
|
}
|
|
|
|
class ToolVersionsListNode: BaseToolNode {
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]] $Versions
|
|
|
|
[Regex] $MajorVersionRegex
|
|
|
|
[ValidateSet("List", "Inline")]
|
|
[String] $ListType
|
|
|
|
ToolVersionsListNode([String] $ToolName, [String[]] $Versions, [String] $MajorVersionRegex, [String] $ListType): base($ToolName) {
|
|
$this.Versions = $Versions
|
|
|
|
if ([String]::IsNullOrEmpty($Versions)) {
|
|
throw "ToolVersionsListNode '$($this.ToolName)' has empty versions list"
|
|
}
|
|
|
|
$this.MajorVersionRegex = [Regex]::new($MajorVersionRegex)
|
|
$this.ListType = $ListType
|
|
$this.ValidateMajorVersionRegex()
|
|
}
|
|
|
|
[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)")
|
|
$this.Versions | ForEach-Object {
|
|
$sb.AppendLine("- $_")
|
|
}
|
|
|
|
return $sb.ToString().TrimEnd()
|
|
}
|
|
|
|
[String] GetValue() {
|
|
return $this.Versions -join ', '
|
|
}
|
|
|
|
[String] ExtractMajorVersion([String] $Version) {
|
|
$match = $this.MajorVersionRegex.Match($Version)
|
|
if (($match.Success -ne $true) -or [String]::IsNullOrEmpty($match.Groups[0].Value)) {
|
|
throw "Version '$Version' doesn't match regex '$($this.PrimaryVersionRegex)'"
|
|
}
|
|
|
|
return $match.Groups[0].Value
|
|
}
|
|
|
|
[PSCustomObject] ToJsonObject() {
|
|
return [PSCustomObject]@{
|
|
NodeType = $this.GetType().Name
|
|
ToolName = $this.ToolName
|
|
Versions = $this.Versions
|
|
MajorVersionRegex = $this.MajorVersionRegex.ToString()
|
|
ListType = $this.ListType
|
|
}
|
|
}
|
|
|
|
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)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
[ValidateNotNullOrEmpty()]
|
|
[String[]] $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
|
|
}
|
|
|
|
[String] ToMarkdown([Int32] $Level) {
|
|
$maxColumnWidths = $this.CalculateColumnsWidth()
|
|
$columnsCount = $maxColumnWidths.Count
|
|
|
|
$delimiterLine = [String]::Join("|", @("-") * $columnsCount)
|
|
|
|
$sb = [System.Text.StringBuilder]::new()
|
|
@($this.Headers) + @($delimiterLine) + $this.Rows | ForEach-Object {
|
|
$sb.Append("|")
|
|
$row = $_.Split("|")
|
|
|
|
for ($colIndex = 0; $colIndex -lt $columnsCount; $colIndex++) {
|
|
$padSymbol = $row[$colIndex] -eq "-" ? "-" : " "
|
|
$cellContent = $row[$colIndex].PadRight($maxColumnWidths[$colIndex], $padSymbol)
|
|
$sb.Append(" $($cellContent) |")
|
|
}
|
|
|
|
$sb.AppendLine()
|
|
}
|
|
|
|
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
|
|
Headers = $this.Headers
|
|
Rows = $this.Rows
|
|
}
|
|
}
|
|
|
|
static [TableNode] FromJsonObject([Object] $JsonObj) {
|
|
return [TableNode]::new($JsonObj.Headers, $JsonObj.Rows)
|
|
}
|
|
|
|
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
|
|
if ($OtherNode.GetType() -ne [TableNode]) {
|
|
return $false
|
|
}
|
|
|
|
# We don't support having multiple TableNode instances on the same header level so such check is fine
|
|
return $true
|
|
}
|
|
|
|
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
|
|
if (-not $this.IsSimilarTo($OtherNode)) {
|
|
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
|
|
}
|
|
|
|
for ($rowIndex = 0; $rowIndex -lt $this.Rows.Count; $rowIndex++) {
|
|
if ($this.Rows[$rowIndex] -ne $OtherNode.Rows[$rowIndex]) {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
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([Int32] $Level) {
|
|
return @(
|
|
'```',
|
|
$this.Content,
|
|
'```'
|
|
) -join "`n"
|
|
}
|
|
|
|
[PSCustomObject] ToJsonObject() {
|
|
return [PSCustomObject]@{
|
|
NodeType = $this.GetType().Name
|
|
Content = $this.Content
|
|
}
|
|
}
|
|
|
|
static [NoteNode] FromJsonObject([Object] $JsonObj) {
|
|
return [NoteNode]::new($JsonObj.Content)
|
|
}
|
|
|
|
[Boolean] IsSimilarTo([BaseNode] $OtherNode) {
|
|
if ($OtherNode.GetType() -ne [NoteNode]) {
|
|
return $false
|
|
}
|
|
|
|
return $this.Content -eq $OtherNode.Content
|
|
}
|
|
|
|
[Boolean] IsIdenticalTo([BaseNode] $OtherNode) {
|
|
return $this.IsSimilarTo($OtherNode)
|
|
}
|
|
} |