Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5db34af
DAOS-19247 test: Support scripted Test stages
phender Jul 1, 2026
ef5d151
Restore getFunctionalTestStage.groovy
phender Jul 2, 2026
09d02ef
Updates.
phender Jul 2, 2026
0f14bd5
Debug
phender Jul 2, 2026
d677368
Debug
phender Jul 2, 2026
82667f5
Updates
phender Jul 2, 2026
ffc879a
Updates
phender Jul 2, 2026
5afdaea
Updates
phender Jul 2, 2026
b22f783
Fixes.
phender Jul 2, 2026
89f8bc1
Support passing in the distro arg to provisionNodes
phender Jul 2, 2026
acce7e9
Merge branch 'master' into hendersp/DAOS-19247-fix
phender Jul 7, 2026
efb52c0
Remove unused code.
phender Jul 7, 2026
ab0e990
Merge branch 'master' into hendersp/DAOS-19247-fix
phender Jul 9, 2026
633079a
Cleanup defaults.
phender Jul 9, 2026
304ab81
Groovylint fixes
phender Jul 9, 2026
8cf3e29
Applying feedback.
phender Jul 9, 2026
61a43af
Fix skipping functional test stage due to no tag match
phender Jul 9, 2026
1a3cdef
Update tag fit check for functional test stages
phender Jul 9, 2026
1950b3a
Debug
phender Jul 10, 2026
3e2f4fc
Debug to figure out why testsInStage isn't working.
phender Jul 13, 2026
037d0dd
Fix debug
phender Jul 14, 2026
8d53241
Cleanup debug and apply feedback
phender Jul 14, 2026
9deead5
Cleanup docstrings
phender Jul 14, 2026
0162051
Applying feedback
phender Jul 14, 2026
6b92bcf
Ensure methods relying on the stage name run in the stage
phender Jul 14, 2026
3c8c2dc
Adding info message to testsInStage().
phender Jul 14, 2026
8130ede
Adding clarifications to docstrings.
phender Jul 15, 2026
495d93a
Collect return status from alwaysScript.
phender Jul 15, 2026
8e3ed9d
Allow distro to be passed to daosRepos()
phender Jul 15, 2026
9b496bb
Debug
phender Jul 16, 2026
0c45b16
More debug
phender Jul 16, 2026
ffae8de
Remove debug - problem was passing in NODELIST
phender Jul 16, 2026
ad7d418
Attempt to fix running testsInStage()
phender Jul 16, 2026
8fa926a
Updates to testsInStage()
phender Jul 16, 2026
021404b
Debug.
phender Jul 16, 2026
a5583e1
Removing env.UNIT_TEST handling in testsInStage
phender Jul 17, 2026
e38a6a5
Revert most of the testsInStage() changes
phender Jul 17, 2026
db18bdc
Add self-reference to verify unit tests
phender Jul 17, 2026
0de6c07
Remove self-reference.
phender Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 46 additions & 27 deletions vars/getFunctionalTestStage.groovy
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* groovylint-disable VariableName */
/* groovylint-disable NestedBlockDepth, VariableName, DuplicateStringLiteral */
// vars/getFunctionalTestStage.groovy

import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
Expand Down Expand Up @@ -33,8 +33,9 @@ import org.jenkinsci.plugins.pipeline.modeldefinition.Utils
* job_status Map of status for each stage in the job/build
* @return a scripted stage to run in a pipeline
*/
/* groovylint-disable-next-line MethodSize */
Map call(Map kwargs = [:]) {
String name = kwargs.get('name', 'Unknown Functional Test Stage')
String name = kwargs.get('name', '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why empty string instead of null?

String pragma_suffix = kwargs.get('pragma_suffix')
String label = kwargs.get('label')
String next_version = kwargs.get('next_version', null)
Expand All @@ -57,37 +58,38 @@ Map call(Map kwargs = [:]) {
// to be skipped by the existing skipFunctionalTestStage logic, while new stages can use
// runStage directly. Once all stages have been converted to use runStage, this should be
// changed to a Boolean.
String runStage = kwargs.get('runStage', 'undefined').toString()
String runStage = kwargs.get('runStage', 'undefined')

Map job_status = kwargs.get('job_status', [:])

if (!name) {
error("getFunctionalTestStage() requires a stage 'name' argument")
}

return {
stage("${name}") {
if (runStage == 'false') {
println("[${name}] Stage skipped by runStage=false")
Utils.markStageSkippedForConditional("${name}")
return
}

// Get the tags for the stage. Use either the build parameter, commit pragma, or
// default tags. All tags are combined with the stage tags to ensure only tests that
// 'fit' the cluster will be run.
String tags = getFunctionalTags(
pragma_suffix: pragma_suffix, stage_tags: stage_tags, default_tags: default_tags)

if (runStage == 'false') {
println("[${name}] Stage skipped by runStage=false")
Utils.markStageSkippedForConditional("${name}")
return
} else if (runStage == 'undefined') {
// To be removed once all stages have been converted to use runStage.
Map skip_kwargs = [
'tags': tags,
'pragma_suffix': pragma_suffix,
'distro': distro,
'run_if_pr': run_if_pr,
'run_if_landing': run_if_landing]
if (skipFunctionalTestStage(skip_kwargs)) {
println("[${name}] Stage skipped by skipFunctionalTestStage()")
Utils.markStageSkippedForConditional("${name}")
return
}
} else if (!testsInStage(tags)) {
println("[${name}] Stage skipped by no tests matching the '${tags}' tags")
Map skipKwargs = ['tags': tags, 'basicCheck': false]
if (runStage == 'true') {
skipKwargs['basicCheck'] = true
} else {
skipKwargs['pragma_suffix'] = pragma_suffix
skipKwargs['distro'] = distro
skipKwargs['run_if_pr'] = run_if_pr
skipKwargs['run_if_landing'] = run_if_landing
}
if (skipFunctionalTestStage(skipKwargs)) {
println("[${name}] Stage skipped by skipFunctionalTestStage()")
Utils.markStageSkippedForConditional("${name}")
return
}
Expand All @@ -105,6 +107,7 @@ Map call(Map kwargs = [:]) {
checkoutScm(pruneStaleBranch: true)
}

Throwable tryError = null
try {
println("[${name}] Running functionalTest() on ${label} with tags=${tags}")
Map ftestConfig = [
Expand All @@ -129,13 +132,29 @@ Map call(Map kwargs = [:]) {
job_status,
name,
functionalTest(ftestConfig))
/* groovylint-disable-next-line CatchException */
} catch (Exception e) {
tryError = e
println("[${name}] Caught exception in try: ${tryError}")
jobStatusUpdate(job_status, name, 'FAILURE')
throw tryError
} finally {
println("[${name}] Running functionalTestPostV2()")
functionalTestPostV2()
jobStatusUpdate(job_status, name)
try {
println("[${name}] Running functionalTestPostV2()")
functionalTestPostV2()
jobStatusUpdate(job_status, name)
/* groovylint-disable-next-line CatchException */
} catch (Exception finallyError) {
println("[${name}] Caught exception in finally: ${finallyError}")
jobStatusUpdate(job_status, name, 'FAILURE')
if (tryError == null) {
/* groovylint-disable-next-line ThrowExceptionFromFinallyBlock */
throw finallyError
}
}
}
}
println("[${name}] Finished with ${job_status}")
}
println("[${name}] Finished with ${job_status}")
}
}
113 changes: 113 additions & 0 deletions vars/scriptedTestRpmStage.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/* groovylint-disable NestedBlockDepth */
// vars/scriptedTestRpmStage.groovy

import org.jenkinsci.plugins.pipeline.modeldefinition.Utils

/**
* scriptedTestRpmStage.groovy
*
* Get a test stage in scripted syntax.
*
* @param kwargs Map containing the following optional arguments (empty strings yield defaults):
* name test stage name
* runStage whether or not to run the test stage; defaults to true
* label test stage default cluster label
* testBranch if specified, checkout sources from this branch before running tests;
* defaults to ''
* jobStatus Map of status for each stage in the job/build
* testRpmArgs Map of arguments to pass to testRpm() for the stage
* alwaysScript script to always run after the test stage, e.g.
* 'ci/rpm/test_daos_post.sh'; defaults to ''
* archiveArtifactsArgs Map of arguments to pass to archiveArtifacts() for the stage
* distro the distro to pass to daosRepos() if testRpmArgs does not specify a
* inst_repos, e.g. 'el9'; defaults to null
* next_version next daos package version to pass to daosPackagesVersion() if
* testRpmArgs does not specify a daos_pkg_version; defaults to null
* @return a scripted stage to run in a pipeline
*/
Map call(Map kwargs = [:]) {
// General parameters
String name = kwargs.get('name', '')
Boolean runStage = kwargs.get('runStage', true) as Boolean
String label = kwargs.get('label')
String testBranch = kwargs.get('testBranch', '')
Map jobStatus = kwargs.get('jobStatus', null) ?: [:]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not the same?

Suggested change
Map jobStatus = kwargs.get('jobStatus', null) ?: [:]
Map jobStatus = kwargs.get('jobStatus', [:])

@phender phender Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly:

def kwargs = [jobStatus: false]

// #1 Returns [:]    (same result for null, 0, or '' jobStatus value or an missing jobStatus key)
kwargs.get('jobStatus', null) ?: [:]

// #2 Returns false  ([:] is only returned if the 'jobStatus' key is not assigned in kwargs, regardless of its value)
kwargs.get('jobStatus', [:])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we have [jobStatus: false] if it is supposed to be a map? This seems like we are masking other issues?

Map testRpmArgs = kwargs.get('testRpmArgs', null) ?: [:]
String alwaysScript = kwargs.get('alwaysScript', '')
Map archiveArtifactsArgs = kwargs.get('archiveArtifactsArgs', null) ?: [:]

if (!name) {
error("scriptedTestRpmStage() requires a stage 'name' argument")
}

return {
stage("${name}") {
if (!runStage) {
println("[${name}] Stage skipped by runStage=false")
Utils.markStageSkippedForConditional("${name}")
return
}

// Add defaults for any missing testRpm() arguments
if (!testRpmArgs.containsKey('inst_repos')) {
/* groovylint-disable-next-line DuplicateStringLiteral */
testRpmArgs['inst_repos'] = daosRepos(kwargs.get('distro', null))
}
if (!testRpmArgs.containsKey('daos_pkg_version')) {
/* groovylint-disable-next-line DuplicateStringLiteral */
testRpmArgs['daos_pkg_version'] = daosPackagesVersion(
kwargs.get('next_version', null))
}

node(label) {
// Ensure access to any branch provisioning scripts exist
if (testBranch) {
println("[${name}] Check out '${testBranch}' from version control")
checkoutScm(
url: 'https://github.com/daos-stack/daos.git',
branch: testBranch,
withSubmodules: false,
pruneStaleBranch: true)
} else {
println("[${name}] Check out branch from version control")
checkoutScm(pruneStaleBranch: true)
}

Throwable tryError = null
try {
println("[${name}] Running testRpm() on ${label}")
jobStatusUpdate(jobStatus, name, testRpm(testRpmArgs))
/* groovylint-disable-next-line CatchException */
} catch (Exception e) {
tryError = e
println("[${name}] Caught exception in try: ${tryError}")
jobStatusUpdate(jobStatus, name, 'FAILURE')
throw tryError
} finally {
try {
if (alwaysScript) {
sh(script: alwaysScript,
label: "Running alwaysScript: ${alwaysScript}",
returnStatus: true)
}
if (archiveArtifactsArgs) {
println("[${name}] Running archiveArtifacts()")
archiveArtifacts(archiveArtifactsArgs)
}
jobStatusUpdate(jobStatus, name)
/* groovylint-disable-next-line CatchException */
} catch (Exception finallyError) {
println("[${name}] Caught exception in finally: ${finallyError}")
/* groovylint-disable-next-line DuplicateStringLiteral */
jobStatusUpdate(jobStatus, name, 'FAILURE')
if (tryError == null) {
/* groovylint-disable-next-line ThrowExceptionFromFinallyBlock */
throw finallyError
}
}
}
}
println("[${name}] Finished with ${jobStatus}")
}
}
}
112 changes: 112 additions & 0 deletions vars/scriptedUnitTestStage.groovy
Comment thread
grom72 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/* groovylint-disable NestedBlockDepth */
// vars/scriptedUnitTestStage.groovy

import org.jenkinsci.plugins.pipeline.modeldefinition.Utils

/**
* scriptedUnitTestStage.groovy
*
* Get a unit test stage in scripted syntax.
*
* @param kwargs Map containing the following optional arguments (empty strings yield defaults):
* name test stage name
* runStage whether or not to run the test stage
* label test stage default cluster label
* testBranch if specified, checkout sources from this branch before running tests
* jobStatus Map of status for each stage in the job/build
* distro the distro to use for daosRepos() and unitPackages() when providing
* default arguments in unitTestArgs, e.g. 'el9'; defaults to ''
* unitTestArgs Map of arguments to pass to unitTest; defaults to an empty Map.
* unitTestPostArgs Map of arguments to pass to unitTestPost() for the stage; defaults to
* an empty Map.
* archiveArtifactsArgs Map of arguments to pass to archiveArtifacts() for the stage; defaults
* to an empty Map.
* @return a scripted stage to run in a pipeline
Comment thread
grom72 marked this conversation as resolved.
*/
Map call(Map kwargs = [:]) {
// General parameters
String name = kwargs.get('name', '')
Boolean runStage = kwargs.get('runStage', true) as Boolean
String label = kwargs.get('label')
String testBranch = kwargs.get('testBranch', '')
Map jobStatus = kwargs.get('jobStatus', null) ?: [:]
String distro = kwargs.get('distro', '')

// Unit Test stage parameters
Map unitTestArgs = kwargs.get('unitTestArgs', null) ?: [:]
Map unitTestPostArgs = kwargs.get('unitTestPostArgs', null) ?: [:]
Map archiveArtifactsArgs = kwargs.get('archiveArtifactsArgs', null) ?: [:]

if (!name) {
error("scriptedUnitTestStage() requires a stage 'name' argument")
}

return {
stage("${name}") {
if (!runStage) {
println("[${name}] Stage skipped by runStage=false")
Utils.markStageSkippedForConditional("${name}")
return
}

// Add defaults for any missing unitTest() arguments
if (!unitTestArgs.containsKey('inst_repos')) {
/* groovylint-disable-next-line DuplicateStringLiteral */
unitTestArgs['inst_repos'] = daosRepos(distro)
Comment thread
grom72 marked this conversation as resolved.
}
if (!unitTestArgs.containsKey('inst_rpms')) {
/* groovylint-disable-next-line DuplicateStringLiteral */
unitTestArgs['inst_rpms'] = unitPackages(target: distro) + ' daos-client-tests'
}
Comment thread
grom72 marked this conversation as resolved.
Comment thread
grom72 marked this conversation as resolved.
Comment on lines +57 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we add ' daos-client-test'?
The actual Jenkins file does not require it:

                stage('Unit Test') {
...
                    steps {
                            job_step_update(
                                unitTest(timeout_time: 60,
                                        unstash_opt: true,
                                        inst_repos: daosRepos(),
                                        inst_rpms: unitPackages(target: 'el9'),
                                        image_version: 'el9.7',
                                        )
                            )
                    }
...
                }
Suggested change
if (!unitTestArgs.containsKey('inst_rpms')) {
/* groovylint-disable-next-line DuplicateStringLiteral */
unitTestArgs['inst_rpms'] = unitPackages(target: distro) + ' daos-client-tests'
}
if (!unitTestArgs.containsKey('inst_rpms')) {
/* groovylint-disable-next-line DuplicateStringLiteral */
unitTestArgs['inst_rpms'] = unitPackages(target: distro)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed for the Fault Injection Testing stage, where scriptedUnitTestStage() is being used.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem I see here (and in previous comments) is that we start using the scriptedUnitTestStage() method not for default unit test behavior but for an exceptional.
If Fault Injection requires some parameters different than default ones (e.g. additional daos-client-tests) they should be provided in Jenkinsfile (or via different wrapper function).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree with @grom72 here. IMO it's better to not hardcode this kind of thing into pipeline-lib

@phender phender Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tricky part is that methods like unitPackages() rely on parseStageInfo() and parseStageInfo relies on the environment variables like the stage name being set. Since this method returns the stage definition to be run, the stage name is not yet set to the stage we're going to run, so the return value of unitPackages() cannot be passed as an input - in its current form. We would essentially need to replace all the parseStageInfo() outputs with required inputs into a bunch of groovy methods. Then we could call:

scriptedUnitTestStage(
   ...
   inst_rpms: unitPackages(target: distro, script: 'ci/unit/required_packages.sh') + ' daos-client-tests',
   ....
)

This is something I think is better tackled in another PR - one where we could also consider using scriptedUnitTestStage() for the other stages that use unitTest().


node(label) {
// Ensure access to any branch provisioning scripts exist
if (testBranch) {
println("[${name}] Check out '${testBranch}' from version control")
checkoutScm(
url: 'https://github.com/daos-stack/daos.git',
branch: testBranch,
withSubmodules: false,
pruneStaleBranch: true)
} else {
println("[${name}] Check out branch from version control")
checkoutScm(pruneStaleBranch: true)
}

Throwable tryError = null
try {
println("[${name}] Running unitTest() on ${label}")
jobStatusUpdate(jobStatus, name, unitTest(unitTestArgs))
/* groovylint-disable-next-line CatchException */
} catch (Exception e) {
tryError = e
println("[${name}] Caught exception in try: ${tryError}")
jobStatusUpdate(jobStatus, name, 'FAILURE')
throw tryError
} finally {
try {
if (unitTestPostArgs) {
println("[${name}] Running unitTestPost()")
unitTestPost(unitTestPostArgs)
}
if (archiveArtifactsArgs) {
println("[${name}] Running archiveArtifacts()")
archiveArtifacts(archiveArtifactsArgs)
}
jobStatusUpdate(jobStatus, name)
/* groovylint-disable-next-line CatchException */
} catch (Exception finallyError) {
println("[${name}] Caught exception in finally: ${finallyError}")
/* groovylint-disable-next-line DuplicateStringLiteral */
jobStatusUpdate(jobStatus, name, 'FAILURE')
if (tryError == null) {
/* groovylint-disable-next-line ThrowExceptionFromFinallyBlock */
throw finallyError
}
}
}
}
println("[${name}] Finished with ${jobStatus}")
}
}
}
24 changes: 18 additions & 6 deletions vars/skipFunctionalTestStage.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* distro functional test stage distro (required for VM)
* run_if_pr whether or not the stage should run for PR builds
* run_if_landing whether or not the stage should run for landing builds
* basicCheck only verify if the stage has altready passed and has tests matching the tags
* @return a String reason why the stage should be skipped; empty if the stage should run
*/
/* groovylint-disable-next-line MethodSize */
Expand All @@ -30,14 +31,19 @@ Map call(Map kwargs = [:]) {
String build_param_value = paramsValue(build_param, '').toString()
Boolean run_if_landing = kwargs['run_if_landing'] ?: false
Boolean run_if_pr = kwargs['run_if_pr'] ?: false
Boolean basicCheck = kwargs.get('basicCheck', false)
String target_branch = env.CHANGE_TARGET ? env.CHANGE_TARGET : env.BRANCH_NAME

println("[${env.STAGE_NAME}] Running skipFunctionalTestStage: " +
"tags=${tags}, pragma_suffix=${pragma_suffix}, size=${size}, distro=${distro}, " +
"build_param=${build_param}, build_param_value=${build_param_value}, " +
"run_if_landing=${run_if_landing}, run_if_pr=${run_if_pr}, " +
"startedByUser()=${startedByUser()}, startedByTimer()=${startedByTimer()}, " +
"startedByUpstream()=${startedByUpstream()}, target_branch=${target_branch}")
if (basicCheck) {
println("[${env.STAGE_NAME}] Running skipFunctionalTestStage: tags=${tags}")
} else {
println("[${env.STAGE_NAME}] Running skipFunctionalTestStage: " +
"tags=${tags}, pragma_suffix=${pragma_suffix}, size=${size}, distro=${distro}, " +
"build_param=${build_param}, build_param_value=${build_param_value}, " +
"run_if_landing=${run_if_landing}, run_if_pr=${run_if_pr}, " +
"startedByUser()=${startedByUser()}, startedByTimer()=${startedByTimer()}, " +
"startedByUpstream()=${startedByUpstream()}, target_branch=${target_branch}")
}

// Regardless of how the stage has been started always skip a stage that has either already
// passed or does not contain any tests match the tags.
Expand All @@ -50,6 +56,12 @@ Map call(Map kwargs = [:]) {
return true
}

// With basicCheck set other stage skip criteria has been already confirmed externally
if (basicCheck) {
println("[${env.STAGE_NAME}] Stage needs to run and has tests matching the tags.")
return false
}

// If the stage has been started by the user, e.g. Build with Parameters, or a timer, or an
// upstream build then use the stage's build parameter (check box) to determine if the stage
// should be run or skipped.
Expand Down
Loading