diff --git a/eng/_util/buildutil/testjson.go b/eng/_util/buildutil/testjson.go index 214e70ef05..4f847c924c 100644 --- a/eng/_util/buildutil/testjson.go +++ b/eng/_util/buildutil/testjson.go @@ -21,6 +21,8 @@ import ( type TestJSONFlags struct { JUnitOutFile string RawTestOutFile string + Attempt string + JobAttempt string } func BindTestJSONFlags() *TestJSONFlags { @@ -31,6 +33,12 @@ func BindTestJSONFlags() *TestJSONFlags { flag.StringVar( &f.RawTestOutFile, "rawtestout", "", "Write raw test output to a new file at this path and summarize any test JSON before it reaches stdout.") + flag.StringVar( + &f.Attempt, "attempt", "", + "Label for the in-job test retry attempt, included in JUnit test suite names to distinguish retries.") + flag.StringVar( + &f.JobAttempt, "jobattempt", "", + "Label for the CI job-level rerun attempt (e.g. System.JobAttempt), included in JUnit test suite names.") return &f } @@ -42,6 +50,12 @@ func (f *TestJSONFlags) AppendToCmdline(cmdline []string) []string { if f.RawTestOutFile != "" { cmdline = append(cmdline, "-rawtestout", f.RawTestOutFile) } + if f.Attempt != "" { + cmdline = append(cmdline, "-attempt", f.Attempt) + } + if f.JobAttempt != "" { + cmdline = append(cmdline, "-jobattempt", f.JobAttempt) + } } return cmdline } @@ -63,7 +77,8 @@ func (f *TestJSONFlags) RunTestCmd(cmdline []string) (err error) { err = errors.Join(err, jf.Close()) }() c := json2junit.NewConverterWithOptions(jf, &json2junit.Options{ - IncludePackageInTestName: true, + Attempt: f.Attempt, + JobAttempt: f.JobAttempt, }) defer func() { err = errors.Join(err, c.Close()) diff --git a/eng/_util/go.mod b/eng/_util/go.mod index ba4459701a..eef94fc180 100644 --- a/eng/_util/go.mod +++ b/eng/_util/go.mod @@ -8,7 +8,7 @@ go 1.25.0 require ( github.com/golang-jwt/jwt/v5 v5.3.1 - github.com/microsoft/go-infra v0.0.10-0.20260322221929-fd732e56be96 + github.com/microsoft/go-infra v0.0.10-0.20260324205227-0b65a97ebded github.com/microsoft/go-infra/goinstallscript v1.2.0 golang.org/x/net v0.52.0 ) diff --git a/eng/_util/go.sum b/eng/_util/go.sum index 9932b4a72c..952b8c7ec4 100644 --- a/eng/_util/go.sum +++ b/eng/_util/go.sum @@ -31,8 +31,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= -github.com/microsoft/go-infra v0.0.10-0.20260322221929-fd732e56be96 h1:GK33KDwSMiBpGR82fU2MIdwk+febXI+d51uLQI5nmIE= -github.com/microsoft/go-infra v0.0.10-0.20260322221929-fd732e56be96/go.mod h1:HjCdUUo5fRDh0jFvKS2Ownh6RKKsWVJkXXuWS7FGKsY= +github.com/microsoft/go-infra v0.0.9 h1:emnuI8I9HGQESkwuTD2YAQFokgPKUzOjRlqpLp4+AfQ= +github.com/microsoft/go-infra v0.0.9/go.mod h1:HjCdUUo5fRDh0jFvKS2Ownh6RKKsWVJkXXuWS7FGKsY= +github.com/microsoft/go-infra v0.0.10-0.20260323223425-43f1982daeed h1:wKQu0mkxbENm6rX6rKBs+/HtbGKm6XRo8GuE6at6Q/A= +github.com/microsoft/go-infra v0.0.10-0.20260323223425-43f1982daeed/go.mod h1:HjCdUUo5fRDh0jFvKS2Ownh6RKKsWVJkXXuWS7FGKsY= +github.com/microsoft/go-infra v0.0.10-0.20260324205227-0b65a97ebded h1:n+CsgLFe7hoNT2vu8V2nqVrkmVM+V6D3bMYC/okVDYc= +github.com/microsoft/go-infra v0.0.10-0.20260324205227-0b65a97ebded/go.mod h1:HjCdUUo5fRDh0jFvKS2Ownh6RKKsWVJkXXuWS7FGKsY= github.com/microsoft/go-infra/goinstallscript v1.2.0 h1:ArYnZHsmv0jnpeDZdFACBUxSmhmYl+Vof8sfk19aYZI= github.com/microsoft/go-infra/goinstallscript v1.2.0/go.mod h1:SFsdKAEHdmGsGoh8FkksVaxoQ3rnnJ/TBqN09Ml/0Cw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= diff --git a/eng/pipeline/stages/run-stage.yml b/eng/pipeline/stages/run-stage.yml index 6d9e310173..ac659f77f7 100644 --- a/eng/pipeline/stages/run-stage.yml +++ b/eng/pipeline/stages/run-stage.yml @@ -265,8 +265,10 @@ stages: -builder '${{ parameters.builder.os }}-${{ parameters.builder.arch }}-${{ parameters.builder.config }}' ` $(if ('${{ parameters.builder.experiment }}') { '-experiment'; '${{ parameters.builder.experiment }}' }) ` $(if ('${{ parameters.builder.fips }}') { '-fipsmode' }) ` - -junitout '$(Build.SourcesDirectory)/eng/artifacts/RawTestOutput/TestResults.xml' ` - -rawtestout '$(Build.SourcesDirectory)/eng/artifacts/RawTestOutput/raw-json-attempt-${{ attempt }}.txt' + -junitout '$(Build.SourcesDirectory)/eng/artifacts/RawTestOutput/TestResults-attempt-${{ attempt }}.xml' ` + -rawtestout '$(Build.SourcesDirectory)/eng/artifacts/RawTestOutput/raw-json-attempt-${{ attempt }}.txt' ` + -attempt '${{ attempt }}' ` + -jobattempt '$(System.JobAttempt)' ${{ if eq(length(parameters.retryAttempts), 1) }}: displayName: Test ${{ else }}: @@ -281,17 +283,20 @@ stages: # Don't fail the job if we are at the last retry and the builder is marked as broken. continueOnError: true - # Publish test results. Use the last-overwritten result, if multiple attempts were made. - - task: PublishTestResults@2 - displayName: Publish test results - condition: succeededOrFailed() - inputs: - testResultsFormat: JUnit - testResultsFiles: $(Build.SourcesDirectory)/eng/artifacts/RawTestOutput/TestResults.xml - testRunTitle: $(System.JobDisplayName) - buildPlatform: ${{ parameters.builder.arch }} - buildConfiguration: ${{ parameters.builder.config }} - publishRunAttachments: true + # Publish this attempt's test results as its own test run. + - task: PublishTestResults@2 + ${{ if eq(length(parameters.retryAttempts), 1) }}: + displayName: Publish test results + ${{ else }}: + displayName: Publish test results (🔁 ${{ attempt }}) + condition: succeededOrFailed() + inputs: + testResultsFormat: JUnit + testResultsFiles: $(Build.SourcesDirectory)/eng/artifacts/RawTestOutput/TestResults-attempt-${{ attempt }}.xml + testRunTitle: $(System.JobDisplayName) (job attempt $(System.JobAttempt), retry ${{ attempt }}) + buildPlatform: ${{ parameters.builder.arch }} + buildConfiguration: ${{ parameters.builder.config }} + publishRunAttachments: true # Publish raw test JSON output and the published (or attempted to publish) JUnit XML. - task: ${{ iif(eq(variables['System.TeamProject'], 'public'), '', '1ES.') }}PublishPipelineArtifact@1