Refactor macOS CI so that tests run in parallel (#9056)

Refactor macOS CI so that tests run in parallel 
  - Also:
    - Fix bootstrap issue where curl was failing for a month (so obviously not needed)
    - Change Windows test stages to use PowerShell Core (where we can) to avoid compat issues

## PR Context

This allows running all tests in about the same about of clock time.
This commit is contained in:
Travis Plunk 2019-03-11 13:00:22 -07:00 committed by GitHub
parent 0ebbdc1cc4
commit 058a19028d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 423 additions and 253 deletions

View File

@ -81,7 +81,7 @@ jobs:
- powershell: |
Import-Module ./tools/ci.psm1
Invoke-Bootstrap-Stage
Invoke-BootstrapStage
displayName: Bootstrap
condition: succeeded()

View File

@ -35,43 +35,64 @@ resources:
- repo: self
clean: true
jobs:
- job: macOS_CI
- template: templates/ci-build.yml
parameters:
pool: Hosted macOS
jobName: mac_build
displayName: macOS Build
- template: templates/nix-test.yml
parameters:
purpose: UnelevatedPesterTests
tagSet: CI
parentJobs:
- mac_build
- template: templates/nix-test.yml
parameters:
purpose: ElevatedPesterTests
tagSet: CI
parentJobs:
- mac_build
- template: templates/nix-test.yml
parameters:
purpose: UnelevatedPesterTests
tagSet: Others
parentJobs:
- mac_build
- template: templates/nix-test.yml
parameters:
purpose: ElevatedPesterTests
tagSet: Others
parentJobs:
- mac_build
- job: verify_xunit
displayName: Verify xUnit Results
pool:
name: Hosted macOS
timeoutInMinutes: 120
name: 'Hosted macOS'
dependsOn:
- mac_build
steps:
- powershell: |
Get-ChildItem -Path env:
displayName: Capture environment
condition: succeededOrFailed()
- task: DownloadBuildArtifacts@0
displayName: 'Download build artifacts'
inputs:
downloadType: specific
itemPattern: |
xunit/**/*
downloadPath: '$(System.ArtifactsDirectory)'
- powershell: Write-Host "##vso[build.updatebuildnumber]$env:BUILD_SOURCEBRANCHNAME-$env:BUILD_SOURCEVERSION-$((get-date).ToString("yyyyMMddhhmmss"))"
displayName: Set Build Name for Non-PR
condition: ne(variables['Build.Reason'], 'PullRequest')
- template: ../tools/releaseBuild/azureDevOps/templates/insert-nuget-config-azfeed.yml
- powershell: |
Import-Module ./tools/ci.psm1
Invoke-Bootstrap-Stage
displayName: Bootstrap
condition: succeeded()
- powershell: |
Import-Module ./tools/ci.psm1
$env:NugetKey = '$(NUGET_KEY)'
Invoke-LinuxTests
displayName: Build and Test
condition: succeeded()
# Uploads any packages as an artifact
- powershell: |
Get-ChildItem -Path ${env:BUILD_ARTIFACTSTAGINGDIRECTORY} -Include *.nupkg, *.pkg, *.tar.gz, TestPackage.zip -Recurse | Select-Object -ExpandProperty FullName | ForEach-Object {
Write-Host "##vso[artifact.upload containerfolder=artifacts;artifactname=artifacts]$_"
}
displayName: Publish Artifacts
condition: succeededOrFailed()
- pwsh: |
Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse
displayName: 'Capture artifacts directory'
continueOnError: true
- pwsh: |
Import-Module .\tools\ci.psm1
$xUnitTestResultsFile = "$(System.ArtifactsDirectory)\xunit\xUnitTestResults.xml"
Test-XUnitTestResults -TestResultsFile $xUnitTestResultsFile
displayName: Test
condition: succeeded()

View File

@ -1,13 +1,14 @@
parameters:
pool: 'Hosted VS2017'
jobName: 'win_build'
displayName: Windows Build
jobs:
- job: ${{ parameters.jobName }}
pool:
name: ${{ parameters.pool }}
displayName: Windows Build
displayName: ${{ parameters.displayName }}
steps:
- powershell: |
@ -24,7 +25,7 @@ jobs:
- powershell: |
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Import-Module .\tools\ci.psm1
Invoke-CIInstall
Invoke-CIInstall -SkipUser
displayName: Bootstrap
condition: succeededOrFailed()

View File

@ -0,0 +1,72 @@
parameters:
pool: 'Hosted macOS'
parentJobs: []
purpose: ''
tagSet: 'CI'
name: 'mac'
jobs:
- job: ${{ parameters.name }}_test_${{ parameters.purpose }}_${{ parameters.tagSet }}
dependsOn:
${{ parameters.parentJobs }}
pool:
name: ${{ parameters.pool }}
displayName: ${{ parameters.name }} Test - ${{ parameters.purpose }} - ${{ parameters.tagSet }}
steps:
- pwsh: |
Get-ChildItem -Path env:
displayName: Capture environment
condition: succeededOrFailed()
- task: DownloadBuildArtifacts@0
displayName: 'Download build artifacts'
inputs:
downloadType: specific
itemPattern: |
build/**/*
downloadPath: '$(System.ArtifactsDirectory)'
- pwsh: |
Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse
displayName: 'Capture artifacts directory'
continueOnError: true
- pwsh: |
Import-Module .\tools\ci.psm1
Invoke-CIInstall -SkipUser
displayName: Bootstrap
condition: succeededOrFailed()
- task: ExtractFiles@1
displayName: 'Extract build zip'
inputs:
archiveFilePatterns: '$(System.ArtifactsDirectory)/build/build.zip'
destinationFolder: '$(System.ArtifactsDirectory)/bins'
- bash: |
find "$(System.ArtifactsDirectory)/bins" -type d -exec chmod +rwx {} \;
find "$(System.ArtifactsDirectory)/bins" -type f -exec chmod +rw {} \;
displayName: 'Fix permissions'
continueOnError: true
- pwsh: |
Get-ChildItem "$(System.ArtifactsDirectory)\bins\*" -Recurse -ErrorAction SilentlyContinue
displayName: 'Capture extracted build zip'
continueOnError: true
- pwsh: |
Import-Module .\tools\ci.psm1
Restore-PSOptions -PSOptionsPath '$(System.ArtifactsDirectory)\build\psoptions.json'
$options = (Get-PSOptions)
$rootPath = '$(System.ArtifactsDirectory)\bins'
$originalRootPath = Split-Path -path $options.Output
$path = Join-Path -path $rootPath -ChildPath (split-path -leaf -path $originalRootPath)
$pwshPath = Join-Path -path $path -ChildPath 'pwsh'
chmod a+x $pwshPath
$options.Output = $pwshPath
Set-PSOptions $options
Invoke-CITest -Purpose '${{ parameters.purpose }}' -TagSet '${{ parameters.tagSet }}'
displayName: Test
condition: succeeded()

View File

@ -14,7 +14,7 @@ jobs:
displayName: Windows Test - ${{ parameters.purpose }} - ${{ parameters.tagSet }}
steps:
- powershell: |
- pwsh: |
Get-ChildItem -Path env:
displayName: Capture environment
condition: succeededOrFailed()
@ -27,19 +27,19 @@ jobs:
build/**/*
downloadPath: '$(System.ArtifactsDirectory)'
- powershell: |
dir "$(System.ArtifactsDirectory)\*" -Recurse
- pwsh: |
Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse
displayName: 'Capture artifacts directory'
continueOnError: true
# must be run frow Windows PowerShell
- powershell: |
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Import-Module .\tools\ci.psm1
Invoke-CIInstall
displayName: Bootstrap
condition: succeededOrFailed()
- powershell: |
- pwsh: |
Import-Module .\tools\ci.psm1
Restore-PSOptions -PSOptionsPath '$(System.ArtifactsDirectory)\build\psoptions.json'
$options = (Get-PSOptions)

View File

@ -36,7 +36,7 @@ resources:
clean: true
jobs:
- template: templates/windows-build.yml
- template: templates/ci-build.yml
- template: templates/windows-test.yml
parameters:

View File

@ -1636,9 +1636,6 @@ function Start-PSBootstrap {
# Install dependencies
# ignore exitcode, because they may be already installed
Start-NativeExecution { brew install $Deps } -IgnoreExitcode
# Install patched version of curl
Start-NativeExecution { brew install curl --with-openssl --with-gssapi } -IgnoreExitcode
} elseif ($Environment.IsAlpine) {
$Deps += 'libunwind', 'libcurl', 'bash', 'cmake', 'clang', 'build-base', 'git', 'curl'

77
tools/WindowsCI.psm1 Normal file
View File

@ -0,0 +1,77 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
if($PSVersionTable.PSEdition -ne 'Desktop')
{
throw 'Must be run from Windows PowerShell'
}
function New-LocalUser
{
<#
.SYNOPSIS
Creates a local user with the specified username and password
.DESCRIPTION
.EXAMPLE
.PARAMETER
username Username of the user which will be created
.PARAMETER
password Password of the user which will be created
.OUTPUTS
.NOTES
#>
param(
[Parameter(Mandatory=$true)]
[string] $username,
[Parameter(Mandatory=$true)]
[string] $password
)
$LocalComputer = [ADSI] "WinNT://$env:computername";
$user = $LocalComputer.Create('user', $username);
$user.SetPassword($password) | out-null;
$user.SetInfo() | out-null;
}
<#
Converts SID to NT Account Name
#>
function ConvertTo-NtAccount
{
param(
[Parameter(Mandatory=$true)]
[string] $sid
)
(new-object System.Security.Principal.SecurityIdentifier($sid)).translate([System.Security.Principal.NTAccount]).Value
}
<#
Add a user to a local security group
Requires Windows PowerShell
#>
function Add-UserToGroup
{
param(
[Parameter(Mandatory=$true)]
[string] $username,
[Parameter(Mandatory=$true, ParameterSetName = "SID")]
[string] $groupSid,
[Parameter(Mandatory=$true, ParameterSetName = "Name")]
[string] $group
)
$userAD = [ADSI] "WinNT://$env:computername/${username},user"
if($PsCmdlet.ParameterSetName -eq "SID")
{
$ntAccount=ConvertTo-NtAccount $groupSid
$group =$ntAccount.Split("\\")[1]
}
$groupAD = [ADSI] "WinNT://$env:computername/${group},group"
$groupAD.Add($userAD.AdsPath);
}

View File

@ -15,75 +15,12 @@ if(Test-Path $dotNetPath)
# import build into the global scope so it can be used by packaging
Import-Module (Join-Path $repoRoot 'build.psm1') -Scope Global
Import-Module (Join-Path $repoRoot 'tools\packaging')
Import-Module (Join-Path $repoRoot 'tools\packaging') -scope Global
function New-LocalUser
# import the windows specific functcion only in Windows PowerShell or on Windows
if($PSVersionTable.PSEdition -eq 'Desktop' -or $isWindows)
{
<#
.SYNOPSIS
Creates a local user with the specified username and password
.DESCRIPTION
.EXAMPLE
.PARAMETER
username Username of the user which will be created
.PARAMETER
password Password of the user which will be created
.OUTPUTS
.NOTES
#>
param(
[Parameter(Mandatory=$true)]
[string] $username,
[Parameter(Mandatory=$true)]
[string] $password
)
$LocalComputer = [ADSI] "WinNT://$env:computername";
$user = $LocalComputer.Create('user', $username);
$user.SetPassword($password) | out-null;
$user.SetInfo() | out-null;
}
<#
Converts SID to NT Account Name
#>
function ConvertTo-NtAccount
{
param(
[Parameter(Mandatory=$true)]
[string] $sid
)
(new-object System.Security.Principal.SecurityIdentifier($sid)).translate([System.Security.Principal.NTAccount]).Value
}
<#
Add a user to a local security group
#>
function Add-UserToGroup
{
param(
[Parameter(Mandatory=$true)]
[string] $username,
[Parameter(Mandatory=$true, ParameterSetName = "SID")]
[string] $groupSid,
[Parameter(Mandatory=$true, ParameterSetName = "Name")]
[string] $group
)
$userAD = [ADSI] "WinNT://$env:computername/${username},user"
if($PsCmdlet.ParameterSetName -eq "SID")
{
$ntAccount=ConvertTo-NtAccount $groupSid
$group =$ntAccount.Split("\\")[1]
}
$groupAD = [ADSI] "WinNT://$env:computername/${group},group"
$groupAD.Add($userAD.AdsPath);
Import-Module (Join-Path $PSScriptRoot 'WindowsCI.psm1') -scope Global
}
# tests if we should run a daily build
@ -209,6 +146,10 @@ function Invoke-CIBuild
# Implements the CI 'install' step
function Invoke-CIInstall
{
param(
[switch]
$SkipUser
)
# Make sure we have all the tags
Sync-PSTags -AddRemoteIfMissing
$releaseTag = Get-ReleaseTag
@ -221,7 +162,7 @@ function Invoke-CIInstall
}
}
if ($env:TF_BUILD)
if ($env:TF_BUILD -and !$SkipUser.IsPresent)
{
# Generate new credential for CI (only) remoting tests.
Write-Log -Message "Creating account for remoting tests in CI."
@ -269,12 +210,18 @@ function Invoke-CIxUnit
$SkipFailing
)
$env:CoreOutput = Split-Path -Parent (Get-PSOutput -Options (Get-PSOptions))
if(!(Test-Path "$env:CoreOutput\pwsh.exe"))
$path = "$env:CoreOutput\pwsh.exe"
if($IsMacOS -or $IsLinux)
{
$path = "$env:CoreOutput\pwsh"
}
if(!(Test-Path $path))
{
throw "CoreCLR pwsh.exe was not built"
}
$xUnitTestResultsFile = "$pwd\xUnitTestResults.xml"
$xUnitTestResultsFile = Join-Path -Path $pwd -childpath "xUnitTestResults.xml"
Start-PSxUnit -xUnitTestResultsFile $xUnitTestResultsFile
Push-Artifact -Path $xUnitTestResultsFile -name xunit
@ -296,16 +243,6 @@ function Invoke-CITest
[ValidateSet('CI', 'Others')]
[string] $TagSet
)
# CoreCLR
$env:CoreOutput = Split-Path -Parent (Get-PSOutput -Options (Get-PSOptions))
Write-Host -Foreground Green 'Run CoreCLR tests'
$testResultsNonAdminFile = "$pwd\TestsResultsNonAdmin-$TagSet.xml"
$testResultsAdminFile = "$pwd\TestsResultsAdmin-$TagSet.xml"
if(!(Test-Path "$env:CoreOutput\pwsh.exe"))
{
throw "CoreCLR pwsh.exe was not built"
}
# Pester doesn't allow Invoke-Pester -TagAll@('CI', 'RequireAdminOnWindows') currently
# https://github.com/pester/Pester/issues/608
@ -324,6 +261,22 @@ function Invoke-CITest
}
}
if($IsLinux -or $IsMacOS)
{
return Invoke-LinuxTestsCore -Purpose $Purpose -ExcludeTag $ExcludeTag -TagSet $TagSet
}
# CoreCLR
$env:CoreOutput = Split-Path -Parent (Get-PSOutput -Options (Get-PSOptions))
Write-Host -Foreground Green 'Run CoreCLR tests'
$testResultsNonAdminFile = "$pwd\TestsResultsNonAdmin-$TagSet.xml"
$testResultsAdminFile = "$pwd\TestsResultsAdmin-$TagSet.xml"
if(!(Test-Path "$env:CoreOutput\pwsh.exe"))
{
throw "CoreCLR pwsh.exe was not built"
}
# Get the experimental feature names and the tests associated with them
$ExperimentalFeatureTests = Get-ExperimentalFeatureTests
@ -336,6 +289,7 @@ function Invoke-CITest
Tag = @()
ExcludeTag = $ExcludeTag + 'RequireAdminOnWindows'
}
Start-PSPester @arguments -Title "Pester Unelevated - $TagSet"
# Fail the build, if tests failed
Test-PSPesterResults -TestResultsFile $testResultsNonAdminFile
@ -357,6 +311,7 @@ function Invoke-CITest
# If a non-empty string or array is specified for the feature name, we only run those test files.
$arguments['Path'] = $testFiles
}
Start-PSPester @arguments -Title "Pester Experimental Unelevated - $featureName"
# Fail the build, if tests failed
@ -372,6 +327,7 @@ function Invoke-CITest
Tag = @('RequireAdminOnWindows')
ExcludeTag = $ExcludeTag
}
Start-PSPester @arguments -Title "Pester Elevated - $TagSet"
# Fail the build, if tests failed
@ -499,6 +455,12 @@ function Invoke-CIFinish
param(
[string] $NuGetKey
)
if($IsLinux -or $IsMacOS)
{
return New-LinuxPackage -NugetKey $NugetKey
}
try {
$releaseTag = Get-ReleaseTag
@ -613,7 +575,7 @@ function Invoke-CIFinish
}
# Bootstrap script for Linux and macOS
function Invoke-Bootstrap-Stage
function Invoke-BootstrapStage
{
$createPackages = Test-DailyBuild
Write-Log -Message "Executing ci.psm1 Bootstrap Stage"
@ -622,42 +584,33 @@ function Invoke-Bootstrap-Stage
Start-PSBootstrap -Package:$createPackages
}
# Build and test script for Linux and macOS:
function Invoke-LinuxTests
# Run pester tests for Linux and macOS
function Invoke-LinuxTestsCore
{
$releaseTag = Get-ReleaseTag
Write-Log -Message "Executing ci.psm1 build and test on a Linux based operating system."
$originalProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
# We use CrossGen build to run tests only if it's the daily build.
Start-PSBuild -CrossGen -PSModuleRestore -CI -ReleaseTag $releaseTag -Configuration 'Release'
}
finally
{
$ProgressPreference = $originalProgressPreference
}
[CmdletBinding()]
param(
[ValidateSet('UnelevatedPesterTests', 'ElevatedPesterTests', 'All')]
[string] $Purpose = 'All',
[string[]] $ExcludeTag = @('Slow', 'Feature', 'Scenario'),
[string] $TagSet = 'CI'
)
$output = Split-Path -Parent (Get-PSOutput -Options (Get-PSOptions))
$testResultsNoSudo = "$pwd/TestResultsNoSudo.xml"
$testResultsSudo = "$pwd/TestResultsSudo.xml"
$excludeTag = @('RequireSudoOnUnix')
$testExcludeTag = $ExcludeTag + 'RequireSudoOnUnix'
$noSudoPesterParam = @{
'BinDir' = $output
'PassThru' = $true
'Terse' = $true
'Tag' = @()
'ExcludeTag' = $excludeTag
'ExcludeTag' = $testExcludeTag
'OutputFile' = $testResultsNoSudo
}
# create packages if it is a full build
$isFullBuild = Test-DailyBuild
$createPackages = $isFullBuild
if ($isFullBuild) {
$noSudoPesterParam['Tag'] = @('CI','Feature','Scenario')
} else {
$noSudoPesterParam['Tag'] = @('CI')
if (!$isFullBuild) {
$noSudoPesterParam['ThrowOnFailure'] = $true
}
if ($hasRunFailingTestTag) {
@ -668,7 +621,9 @@ function Invoke-LinuxTests
$ExperimentalFeatureTests = Get-ExperimentalFeatureTests
# Running tests which do not require sudo.
$pesterPassThruNoSudoObject = Start-PSPester @noSudoPesterParam -Title 'Pester No Sudo'
if($Purpose -eq 'UnelevatedPesterTests' -or $Purpose -eq 'All')
{
$pesterPassThruNoSudoObject = Start-PSPester @noSudoPesterParam -Title "Pester No Sudo - $TagSet"
# Running tests that do not require sudo, with specified experimental features enabled
$noSudoResultsWithExpFeatures = @()
@ -689,17 +644,22 @@ function Invoke-LinuxTests
# If a non-empty string or array is specified for the feature name, we only run those test files.
$noSudoPesterParam['Path'] = $testFiles
}
$passThruResult = Start-PSPester @noSudoPesterParam -Title "Pester Experimental No Sudo - $featureName"
$passThruResult = Start-PSPester @noSudoPesterParam -Title "Pester Experimental No Sudo - $featureName - $TagSet"
$noSudoResultsWithExpFeatures += $passThruResult
}
}
# Running tests, which require sudo.
if($Purpose -eq 'ElevatedPesterTests' -or $Purpose -eq 'All')
{
$sudoPesterParam = $noSudoPesterParam.Clone()
$sudoPesterParam.Remove('Path')
$sudoPesterParam['Tag'] = @('RequireSudoOnUnix')
$sudoPesterParam['ExcludeTag'] = @()
$sudoPesterParam['ExcludeTag'] = $ExcludeTag
$sudoPesterParam['Sudo'] = $true
$sudoPesterParam['OutputFile'] = $testResultsSudo
$pesterPassThruSudoObject = Start-PSPester @sudoPesterParam -Title 'Pester Sudo'
$pesterPassThruSudoObject = Start-PSPester @sudoPesterParam -Title "Pester Sudo - $TagSet"
# Running tests that require sudo, with specified experimental features enabled
$sudoResultsWithExpFeatures = @()
foreach ($entry in $ExperimentalFeatureTests.GetEnumerator()) {
@ -720,9 +680,10 @@ function Invoke-LinuxTests
# If a non-empty string or array is specified for the feature name, we only run those test files.
$sudoPesterParam['Path'] = $testFiles
}
$passThruResult = Start-PSPester @sudoPesterParam -Title "Pester Experimental Sudo - $featureName"
$passThruResult = Start-PSPester @sudoPesterParam -Title "Pester Experimental Sudo - $featureName - $TagSet"
$sudoResultsWithExpFeatures += $passThruResult
}
}
# Determine whether the build passed
try {
@ -737,6 +698,33 @@ function Invoke-LinuxTests
$resultError = $_
$result = "FAIL"
}
}
# Build and test script for Linux and macOS:
function Invoke-LinuxTests
{
param(
[switch]
$SkipBuild
)
if(!$SkipBuild.IsPresent)
{
$releaseTag = Get-ReleaseTag
Write-Log -Message "Executing ci.psm1 build and test on a Linux based operating system."
$originalProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
# We use CrossGen build to run tests only if it's the daily build.
Start-PSBuild -CrossGen -PSModuleRestore -CI -ReleaseTag $releaseTag -Configuration 'Release'
}
finally
{
$ProgressPreference = $originalProgressPreference
}
}
Invoke-LinuxTestsCore
try {
$xUnitTestResultsFile = "$pwd/xUnitTestResults.xml"
@ -751,8 +739,34 @@ function Invoke-LinuxTests
}
}
$createPackages = $isFullBuild
if ($createPackages)
{
New-LinuxPackage -NugetKey $env:NugetKey
}
# If the tests did not pass, throw the reason why
if ( $result -eq "FAIL" )
{
Write-Warning "Tests failed. See the issue below."
Throw $resultError
}
else
{
Write-Verbose "Tests did not fail! Nice job!"
}
}
function New-LinuxPackage
{
param(
[string]
$NugetKey
)
$isFullBuild = Test-DailyBuild
$releaseTag = Get-ReleaseTag
$packageParams = @{}
$packageParams += @{ReleaseTag=$releaseTag}
@ -810,15 +824,3 @@ function Invoke-LinuxTests
New-TestPackage -Destination "${env:SYSTEM_ARTIFACTSDIRECTORY}"
}
}
# If the tests did not pass, throw the reason why
if ( $result -eq "FAIL" )
{
Write-Warning "Tests failed. See the issue below."
Throw $resultError
}
else
{
Write-Verbose "Tests did not fail! Nice job!"
}
}