# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
# 
#   http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

# Downloads test binaries and executes tests using dotnet vstest,
# runs the tests for each project on a background job in parallel,
# then uploads the results to Azure DevOps pipelines

# NOTE: Depends on environment variables $(IsNightly) and $(IsWeekly)

parameters:
  osName: 'Windows' # The name of the operating system for display purposes.
  framework: '' # The target framework indicating which framework tests will be run on. See: https://docs.microsoft.com/en-us/dotnet/standard/frameworks.
  binaryArtifactName: 'testbinaries' # The name of the Azure DevOps build artifact where the test assemblies will be downloaded from. Default 'testbinaries'.
  testResultsArtifactName: 'testresults' # The name of the Azure DevOps build artifact where the test results will be published. Default 'testresults'.
  vsTestPlatform: 'x64' # Target platform architecture used for test execution. Valid values are x86, x64, and ARM.
  testBinaryFilesPattern: '\.*\.Tests\.?[^\\/]*?\.?[^\\/]*?.dll$' # The regex pattern (within $(System.DefaultWorkingDirectory)/**/<TargetFramework>/) where to look for test .dll files, so they can be distinguished from other .dll file types.
  testResultsFileName: 'TestResults.trx' # The name of the file (not path) of the test results. Default 'TestResults.trx'.
  maximumParallelJobs: 8
  maximumAllowedFailures: 0
  where: '' # A test filter expression, as defined by dotnet vstest
  
steps:
- checkout: none # self represents the repo where the initial Pipelines YAML file was found

- pwsh: |
    function EnsureNotNullOrEmpty([string]$param, [string]$nameOfParam) {
        if ([string]::IsNullOrEmpty($param)) {
            Write-Host "##vso[task.logissue type=error;]Missing template parameter \"$nameOfParam\""
            Write-Host "##vso[task.complete result=Failed;]"
        }
    }
    EnsureNotNullOrEmpty('${{ parameters.osName }}', 'osName')
    EnsureNotNullOrEmpty('${{ parameters.framework }}', 'framework')
    EnsureNotNullOrEmpty('${{ parameters.binaryArtifactName }}', 'binaryArtifactName')
    EnsureNotNullOrEmpty('${{ parameters.testResultsArtifactName }}', 'testResultsArtifactName')
    EnsureNotNullOrEmpty('${{ parameters.vsTestPlatform }}', 'vsTestPlatform')
    EnsureNotNullOrEmpty('${{ parameters.testBinaryFilesPattern }}', 'testBinaryFilesPattern')
    EnsureNotNullOrEmpty('${{ parameters.testResultsFileName }}', 'testResultsFileName')
    EnsureNotNullOrEmpty('${{ parameters.maximumParallelJobs }}', 'maximumParallelJobs')
    EnsureNotNullOrEmpty('${{ parameters.maximumAllowedFailures }}', 'maximumAllowedFailures')
  displayName: 'Validate Template Parameters'


- task:  DownloadPipelineArtifact@2
  displayName: 'Download Build Artifacts: ${{ parameters.binaryArtifactName }} to $(System.DefaultWorkingDirectory)/$(parameters.framework)'
  inputs:
    artifactName: '${{ parameters.binaryArtifactName }}_${{ parameters.framework }}'
    targetPath: '$(System.DefaultWorkingDirectory)/${{ parameters.framework }}'

#- pwsh: Get-ChildItem -Path $(System.DefaultWorkingDirectory) # Uncomment for debugging

- task: UseDotNet@2
  displayName: 'Use .NET Core sdk 2.1.807'
  inputs:
    version: 2.1.807
  condition: and(succeeded(), contains('${{ parameters.framework }}', 'netcoreapp2.'))

- task: UseDotNet@2
  displayName: 'Use .NET Core sdk 3.1.301'
  inputs:
    version: 3.1.301
  condition: and(succeeded(), contains('${{ parameters.framework }}', 'netcoreapp3.'))

- task: UseDotNet@2
  displayName: 'Use .NET sdk 5.0.100'
  inputs:
    version: 5.0.100

#- template: 'show-all-files.yml' # Uncomment for debugging
- pwsh: |
    $framework = '${{ parameters.framework }}'
    $testBinaryRootDirectory = "$(System.DefaultWorkingDirectory)"
    $testResultsArtifactDirectory = "${{ format('$(Build.ArtifactStagingDirectory)/{0}',parameters.testResultsArtifactName) }}"
    $testPlatform = '${{ parameters.vsTestPlatform }}'
    $testOSName = '${{ parameters.osName }}'
    $testBinaryFilesPattern = '${{ parameters.testBinaryFilesPattern }}'
    $testResultsFileName = '${{ parameters.testResultsFileName }}'
    $maximumParalellJobs = '${{ parameters.maximumParallelJobs }}'
    $where = '${{ parameters.where }}'
    $tempDirectory = "$(Agent.TempDirectory)"
    $isNightly = if ($env:ISNIGHTLY -eq 'true') { 'true' } else { 'false' }
    $isWeekly = if ($env:ISWEEKLY -eq 'true') { 'true' } else { 'false' }
    
    function IsSupportedFramework([string]$framework) {
        if ($IsWindows -eq $null) {
            $IsWindows = $env:OS.StartsWith('Win')
        }
        if (!$IsWindows -and $framework.StartsWith('net4')) {
            return $false
        }
        return $true
    }
    
    function RunTests([string]$framework, [string]$fileRegexPattern) {
        if (!(IsSupportedFramework($framework))) { continue }
    
        $testBinaries = Get-ChildItem -Path "$testBinaryRootDirectory" -File -Recurse | Where-Object {$_.FullName -match "$framework" -and $_.FullName -match "$fileRegexPattern" -and !$_.Name.EndsWith('.resources.dll') } | Sort-Object -Property FullName
        Write-Host $testBinaries
        foreach ($testBinary in $testBinaries) {
            $testName = [System.IO.Path]::GetFileNameWithoutExtension($testBinary.FullName)
    
            if ($maximumParalellJobs -gt 1) {
                # Pause if we have queued too many parallel jobs
                $running = @(Get-Job | Where-Object { $_.State -eq 'Running' })
                if ($running.Count -ge $maximumParalellJobs) {
                    Write-Host ""
                    Write-Host "  Running tests in parallel on $($running.Count) projects." -ForegroundColor Cyan
                    Write-Host "  Next in queue is $testName on $framework. This will take a bit, please wait..." -ForegroundColor Cyan
                    $running | Wait-Job -Any | Out-Null
                }
            }
    
            $testResultDirectory = "$testResultsArtifactDirectory/$testOSName/$framework/$testPlatform/$testName"
            if (!(Test-Path "$testResultDirectory")) {
                New-Item "$testResultDirectory" -ItemType Directory -Force
            }
            # Test binaries are copied to a temp directory so locking behavior of other processes doesn't interfere with this test run
            $tempTestDirectory = "$tempDirectory/$framework/$testName"
            if (!(Test-Path "$tempTestDirectory")) {
                New-Item "$tempTestDirectory" -ItemType Directory -Force
            }
            $testTarget = "$tempTestDirectory/$($testBinary.Name)"
            $sourceDirectory = $testBinary.Directory.FullName

            Copy-Item -Path "$sourceDirectory/*" -Destination "$tempTestDirectory" -Recurse -Force

            if ($isNightly -ne 'true' -and $isWeekly -ne 'true') {
                $blameHangTimeout = "--blame-hang-timeout 15minutes"
            } else {
                $blameHangTimeout = "--blame-hang-timeout 40minutes"
            }

            Write-Host "Running with $blameHangTimeout"

            $testExpression = "dotnet test ""$testTarget"" --framework ""$framework"" --blame --no-build --no-restore" + `
                " --logger:""console;verbosity=normal"" --logger:""trx;LogFileName=$testResultsFileName""" + `
                " --results-directory:""$testResultDirectory""" + `
                " --blame-hang --blame-hang-dump-type mini $blameHangTimeout"

            if (![string]::IsNullOrEmpty($where)) {
                $testExpression = "$testExpression --filter ""$where"""
            }
    
            $testExpression = "$testExpression -- RunConfiguration.TargetPlatform=$testPlatform"
    
            Write-Host "Testing '$($testBinary.FullName)' on framework '$framework' and outputting test results to '$testResultDirectory/$testResultsFileName'..."
            Write-Host $testExpression -ForegroundColor Magenta
            if ($maximumParalellJobs -le 1) {
                Invoke-Expression $testExpression # For running in the foreground
            } else {
    
                $testExpression += " > ""$testResultDirectory/dotnet-vstest.log"" 2> ""$testResultDirectory/dotnet-vstest-error.log"""
                $scriptBlock = {
                    param([string]$testExpression)
                    Invoke-Expression $testExpression
                }
    
                # Avoid dotnet vstest collisions by delaying for 500ms
                Start-Sleep -Milliseconds 500
    
                # Execute the jobs in parallel
                Start-Job -Name "$testName,$framework,$testPlatform" -ScriptBlock $scriptBlock -ArgumentList $testExpression
            }
        }
    }
    
    RunTests -Framework "$framework" -FileRegexPattern "$testBinaryFilesPattern"
    
    if ($maximumParalellJobs -gt 1) {
        # Wait for it all to complete
        do {
            $running = @(Get-Job | Where-Object { $_.State -eq 'Running' })
            if ($running.Count -gt 0) {
                Write-Host ""
                Write-Host "  Almost finished, only $($running.Count) projects left..." -ForegroundColor Cyan
                [int]$number = 0
                foreach ($runningJob in $running) {
                    $number++
                    $jobName = $runningJob | Select -ExpandProperty Name
                    Write-Host "$number. $jobName"
                }
                $running | Wait-Job -Any
            }
        } until ($running.Count -eq 0)
    }
    
    $global:LASTEXITCODE = 0 # Force the script to continue on error
  displayName: 'dotnet vstest ${{ parameters.framework }},${{ parameters.vsTestPlatform }}'
  ignoreLASTEXITCODE: true

#- template: 'show-all-files.yml' # Uncomment for debugging

- task: PublishPipelineArtifact@1
  displayName: 'Publish Artifact: ${{ parameters.testResultsArtifactName }}'
  inputs:
    targetPath: '$(Build.ArtifactStagingDirectory)/${{ parameters.testResultsArtifactName }}'
    ArtifactName: '${{ parameters.testResultsArtifactName }}_${{ parameters.osName }}_${{ parameters.framework }}_${{ parameters.vsTestPlatform }}'
  condition: succeededOrFailed()

# Due to the fact that it is not possible to loop a task and
# it would be a ton of work to make a replacement for the
# Publish Test Results task or the (deprecated) TfsPublisher
# our only other option is to make a task for every supported
# platform and project and update it whenever a new platform 
# is targeted or test project is created in Lucene.Net.

- template: 'publish-test-results-for-test-projects.yml'
  parameters:
    osName: '${{ parameters.osName }}'
    framework: '${{ parameters.framework }}'
    vsTestPlatform: '${{ parameters.vsTestPlatform }}'

- pwsh: |
    $failed = $false
    if ($env:HOSTCRASHED -eq 'true') {
        Write-Host "##vso[task.logissue type=error;]Test host process(es) crashed:  $($env:CRASHEDRUNS)."
        $failed = $true
    }
    $maximumAllowedFailures = '${{ parameters.maximumAllowedFailures }}'
    if ([int]$env:TOTALFAILURES -gt [int]$maximumAllowedFailures) {
        Write-Host "##vso[task.logissue type=error;]Test run failed due to too many failed tests. Maximum failures allowed: $maximumAllowedFailures, total failures: $($env:TOTALFAILURES)."
        $failed = $true
    }
    if ($failed) {
        Write-Host "##vso[task.complete result=Failed;]"
    }