gentoo/eclass/junit5.eclass
Sam James 4f66d70cda
junit5.eclass: drop -x
Reported by selckin.

Signed-off-by: Sam James <sam@gentoo.org>
2025-08-14 09:08:04 +01:00

501 lines
16 KiB
Bash

# Copyright 2022-2025 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
# @ECLASS: junit5.eclass
# @MAINTAINER:
# java@gentoo.org
# @AUTHOR:
# Yuan Liao <liaoyuan@gmail.com>
# @SUPPORTED_EAPIS: 8
# @BLURB: Experimental eclass to add support for testing on the JUnit Platform
# @DESCRIPTION:
# This eclass runs tests on the JUnit Platform (which is a JUnit 5 sub-project)
# during the src_test phase. It is an experimental eclass whose code should
# eventually be merged into java-utils-2.eclass and/or java-pkg-simple.eclass
# when it is mature.
if [[ ! ${_JUNIT5_ECLASS} ]]; then
case ${EAPI} in
8) ;;
*) die "${ECLASS}: EAPI ${EAPI} unsupported." ;;
esac
inherit java-pkg-simple
# @ECLASS_VARIABLE: JAVA_TEST_SELECTION_METHOD
# @DESCRIPTION:
# A string that represents the method to discover and select test classes to
# run on the JUnit Platform. These values are accepted:
#
# - "traditional" (default): Use the same method as java-pkg-simple.eclass.
#
# - "scan-classpath": Rely on the JUnit Platform's ConsoleLauncher's
# '--scan-classpath' option to discover tests, and run these discovered
# tests. JAVA_TEST_RUN_ONLY and JAVA_TEST_EXCLUDES are both honored.
#
# - "scan-classpath+pattern": Rely on the JUnit Platform's ConsoleLauncher's
# '--scan-classpath' option to discover tests, but also select the same tests
# that java-pkg-simple.eclass would select from the discovered tests.
# JAVA_TEST_RUN_ONLY and JAVA_TEST_EXCLUDES are both honored.
#
# - "console-args": Do not perform any test discovery or test selection;
# instead, pass the JAVA_JUNIT_CONSOLE_ARGS variable's value to the JUnit
# Platform's ConsoleLauncher. In this case, JAVA_JUNIT_CONSOLE_ARGS should
# contain arguments to ConsoleLauncher that select tests to run. Neither
# JAVA_TEST_RUN_ONLY nor JAVA_TEST_EXCLUDES is honored.
#
# If multiple values separated by white-space characters are included, then
# this eclass will use every method to run tests once and print a comparison of
# the number of tests each method ran at the end. However, this should only be
# used in development for comparing and evaluating the methods.
#
# Example values:
# @CODE
# JAVA_TEST_SELECTION_METHOD="scan-classpath+pattern"
# JAVA_TEST_SELECTION_METHOD="traditional scan-classpath"
# @CODE
: ${JAVA_TEST_SELECTION_METHOD:=traditional}
# @ECLASS_VARIABLE: JAVA_JUNIT_CONSOLE_ARGS
# @DEFAULT_UNSET
# @DESCRIPTION:
# Extra arguments to pass to JUnit Platform's ConsoleLauncher only when
# JAVA_TEST_SELECTION_METHOD contains "console-args". Any white-space
# character in this variable's value will separate tokens into different
# arguments.
# @ECLASS_VARIABLE: JAVA_JUNIT_CONSOLE_COLOR
# @USER_VARIABLE
# @DEFAULT_UNSET
# @DESCRIPTION:
# If this variable's value is not empty, enable color in the JUnit Platform's
# ConsoleLauncher's output.
# @ECLASS_VARIABLE: _JAVA_JUNIT_REPORTS_DIR
# @INTERNAL
# @DESCRIPTION:
# The output path of JUnit Platform test reports. The reports contain
# information about test executions that are useful to QA checks and analysis.
_JAVA_JUNIT_REPORTS_DIR="${T}/junit-5-reports"
if has test ${JAVA_PKG_IUSE}; then
DEPEND="test? (
dev-java/junit:5
)"
fi
junit5_pkg_setup() {
java-pkg-2_pkg_setup
[[ ${MERGE_TYPE} == binary ]] && return
# Note: Each method must have a "_junit5_src_test_${method}"
# function in this eclass
local accepted_methods="
traditional
scan-classpath
scan-classpath+pattern
console-args
"
show_accepted_methods_and_die() {
eerror "Accepted methods are:"
local m
for m in ${accepted_methods}; do
eerror "- ${m}"
done
die "Invalid JAVA_TEST_SELECTION_METHOD value: ${JAVA_TEST_SELECTION_METHOD}"
}
local methods=()
local method
for method in ${JAVA_TEST_SELECTION_METHOD}; do
if has ${method} ${accepted_methods}; then
methods+=( ${method} )
else
eerror "Unknown test selection method: ${method}"
show_accepted_methods_and_die
fi
done
if [[ ${#methods[@]} -eq 1 ]]; then
einfo "Using JUnit Platform test selection method: ${methods[@]}"
elif [[ ${#methods[@]} -gt 1 ]]; then
einfo "Using multiple JUnit Platform test selection methods,"
einfo "which should only be used for development purposes:"
for method in "${methods[@]}"; do
einfo "- ${method}"
done
else
eerror "No valid JUnit Platform test selection method specified"
show_accepted_methods_and_die
fi
_JUNIT5_PKG_SETUP=1
}
# @FUNCTION: ejunit5
# @USAGE: [-cp <classpath>|-classpath <classpath>] <classes>
# @DESCRIPTION:
# Using the specified classpath, launches a JVM instance to run the specified
# test classes by invoking the JUnit Platform's ConsoleLauncher.
#
# This function's interface is consistent with the existing 'ejunit' and
# 'ejunit4' functions in java-utils-2.eclass.
ejunit5() {
debug-print-function ${FUNCNAME} $*
local pkgs
if [[ -f ${JAVA_PKG_DEPEND_FILE} ]]; then
for atom in $(cat ${JAVA_PKG_DEPEND_FILE} | tr : ' '); do
pkgs=${pkgs},$(echo ${atom} | sed -re "s/^.*@//")
done
fi
local junit="junit-5"
local cp=$(java-pkg_getjars --build-only --with-dependencies ${junit}${pkgs})
if [[ ${1} = -cp || ${1} = -classpath ]]; then
cp="${2}:${cp}"
shift 2
else
cp=".:${cp}"
fi
_junit5_ConsoleLauncher "${cp}"$(printf -- ' -c=%q' "${@}")
}
# @FUNCTION: _junit5_ConsoleLauncher
# @INTERNAL
# @USAGE: <classpath> [args]
# @DESCRIPTION:
# Invokes the JUnit Platform's ConsoleLauncher on the specified classpath,
# using the specified arguments.
_junit5_ConsoleLauncher() {
debug-print-function ${FUNCNAME} $*
local cp=${1}
shift 1
# Save test reports, which contain information about
# the test execution that can be useful to QA checks
mkdir -p "${_JAVA_JUNIT_REPORTS_DIR}" ||
die "Failed to create JUnit report directory"
local runner=org.junit.platform.console.ConsoleLauncher
local runner_args=(
--reports-dir="${_JAVA_JUNIT_REPORTS_DIR}"
--fail-if-no-tests
# By default, remove ANSI escape code for coloring
# to make log files more readable
$([[ ${JAVA_JUNIT_CONSOLE_COLOR} ]] || echo --disable-ansi-colors)
${JAVA_PKG_DEBUG:+--details=verbose}
)
local args=(
-cp "${cp}"
-Djava.io.tmpdir="${T}"
-Djava.awt.headless=true
"${JAVA_TEST_EXTRA_ARGS[@]}"
${runner}
"${runner_args[@]}"
"${JAVA_TEST_RUNNER_EXTRA_ARGS[@]}"
"${@}"
)
set -- java "${args[@]}"
debug-print "Calling: ${*}"
echo "${@}" >&2
"${@}"
local ret=${?}
[[ ${ret} -eq 2 ]] && die "No JUnit tests found"
[[ ${ret} -eq 0 ]] || die "ConsoleLauncher failed"
}
junit5_src_test() {
if ! has test ${JAVA_PKG_IUSE}; then
return
elif ! use test; then
return
fi
if [[ ! ${_JUNIT5_PKG_SETUP} ]]; then
eqawarn "junit5.eclass is inherited, but the"
eqawarn "junit5_pkg_setup function has not been called."
eqawarn "Please add the function call to pkg_setup."
fi
local junit_5_classpath="junit-5"
JAVA_TEST_GENTOO_CLASSPATH+=" ${junit_5_classpath}"
java-pkg-simple_src_test
elog "java-pkg-simple.eclass might have printed a \"No suitable function found\""
elog "message. This is OK, as junit5.eclass will handle JUnit 5..."
local classes="target/test-classes"
local classpath="${classes}:${JAVA_JAR_FILENAME}"
java-pkg-simple_getclasspath
java-pkg-simple_prepend_resources ${classes} "${JAVA_TEST_RESOURCE_DIRS[@]}"
local method
declare -A num_tests
for method in ${JAVA_TEST_SELECTION_METHOD}; do
local method_func="_junit5_src_test_${method}"
declare -F ${method_func} > /dev/null ||
die "Function for \"${method}\" method not found: ${method_func}"
${method_func}
num_tests[${method}]="$(\
cat "${_JAVA_JUNIT_REPORTS_DIR}"/TEST-*.xml |
grep -c '</testcase>')"
done
_junit5_post_test_qa_check_use_dep
if [[ ${#num_tests[@]} -gt 1 ]]; then
einfo "Number of tests each test selection method selected:"
for method in "${!num_tests[@]}"; do
einfo "- ${method}: ${num_tests[${method}]}"
done
fi
}
# @FUNCTION: _junit5_post_test_qa_check_use_dep
# @INTERNAL
# @DESCRIPTION:
# Checks whether the dev-java/junit:5 atom's USE dependency in DEPEND includes
# all USE flags that are required by the tests. This function should only be
# called after the tests on the JUnit Platform have run.
#
# This function helps ebuild authors determine the correct USE dependency for
# dev-java/junit:5. Consider the following situation:
#
# Suppose an ebuild author has already installed dev-java/junit:5 with the
# 'suite' USE flag enabled, and they are creating a new ebuild that has tests
# to run on the junit-platform-suite test engine. If the author had disabled
# the 'suite' USE flag, some tests might fail due to the missing JUnit 5
# modules, so the author could realize that the ebuild needs to depend on
# dev-java/junit:5[suite]. However, the USE flag is enabled, so it is possible
# that all tests pass in the author's environment, thus the author thinks the
# ebuild does not have issues and publishes it.
#
# When another person gets the ebuild and tries to run the tests in an
# environment where dev-java/junit:5's 'suite' USE flag is _not_ enabled, the
# tests _will_ launch and then fail. The dev-java/junit:5[suite] dependency is
# not declared, so the package manager will not enforce it.
_junit5_post_test_qa_check_use_dep() {
local flag
# If a test engine ran any tests, its report will contain a
# '<testcase ...>...</testcase>' XML entry for each test it ran
local engines_with_tests=$(grep -l '</testcase>' \
"${_JAVA_JUNIT_REPORTS_DIR}"/TEST-junit-*.xml)
# A test engine's report filename format is "TEST-${engine_id}.xml"
engines_with_tests="${engines_with_tests//"${_JAVA_JUNIT_REPORTS_DIR}/TEST-"}"
engines_with_tests="${engines_with_tests//.xml}"
local engine
local unexpected_engines=()
for engine in ${engines_with_tests}; do
case ${engine} in
junit-jupiter)
# Built unconditionally in dev-java/junit:5; no USE flag needed
;;
junit-platform-suite)
flag=suite
;;
junit-vintage)
flag=vintage
;;
esac
[[ -z ${flag} ]] || _junit5_dep_has_use "${flag}" ||
unexpected_engines+=( "${engine}: dev-java/junit:5[${flag}]" )
done
if [[ -n ${unexpected_engines[@]} ]]; then
eqawarn "Some tests ran on a JUnit Platform test engine whose USE flag"
eqawarn "is not enabled by the dev-java/junit:5 atom in DEPEND."
eqawarn "Please check the following test engine list and add the"
eqawarn "mentioned USE dependencies into DEPEND=\"test? ( ... )\":"
for engine in "${unexpected_engines[@]}"; do
eqawarn "- ${engine}"
done
fi
einfo "Verifying test classes' dependencies"
local jdeps_output="${T}/test-classes-jdeps.txt"
find "${classes}" -type f -name '*.class' -exec \
"$(java-config --jdk-home)/bin/jdeps" {} + > "${jdeps_output}" ||
die "jdeps failed"
declare -A junit_5_flag_to_package=(
[migration-support]=org.junit.jupiter.migrationsupport
[test-kit]=org.junit.platform.testkit.engine
)
local package
local unexpected_packages=()
for flag in "${!junit_5_flag_to_package[@]}"; do
package="${junit_5_flag_to_package[${flag}]}"
_junit5_dep_has_use "${flag}" ||
! grep -q -F "${package}" "${jdeps_output}" ||
unexpected_packages+=( "${package}: dev-java/junit:5[${flag}]" )
done
if [[ -n ${unexpected_packages[@]} ]]; then
eqawarn "Some tests used an optional JUnit 5 module whose USE flag"
eqawarn "is not enabled by the dev-java/junit:5 atom in DEPEND."
eqawarn "Please check the following Java package list and add the"
eqawarn "mentioned USE dependencies into DEPEND=\"test? ( ... )\":"
for package in "${unexpected_packages[@]}"; do
eqawarn "- ${package}"
done
fi
}
# @FUNCTION: _junit5_dep_has_use
# @INTERNAL
# @USAGE: <flag>
# @DESCRIPTION:
# Checks whether dev-java/junit:5 is declared with USE dependency on the
# specified USE flag (i.e. dev-java/junit:5[<flag>]) in DEPEND.
# @RETURN: Shell true if the check passed, shell false otherwise
_junit5_dep_has_use() {
debug-print-function ${FUNCNAME} $*
local flag=${1}
local re="\bdev-java/junit(-[0-9].*)?:5\[[^]]*\b${flag}\b[^]]*\]"
# Do not match "dev-java/junit:5[-${flag}]"
local n_re1="\bdev-java/junit(-[0-9].*)?:5\[[^]]*-\b${flag}\b[^]]*\]"
[[ ${DEPEND} =~ ${re} && ! ${DEPEND} =~ ${n_re1} ]]
}
# @FUNCTION: _junit5_src_test_traditional
# @INTERNAL
# @DESCRIPTION:
# Finds tests to run using the traditional method that java-pkg-simple.eclass
# utilizes, then runs these tests on the JUnit Platform.
#
# The method to find tests is:
# 1. If JAVA_TEST_RUN_ONLY is defined, run only the tests listed in it, and
# skip the rest steps.
# 2. Use the 'find' command to gather a list of Java source files whose
# filename matches a preset pattern.
# 3. Remove any tests in JAVA_TEST_EXCLUDES from the list. Run tests that are
# still in the list after the removal.
_junit5_src_test_traditional() {
debug-print-function ${FUNCNAME} $*
local tests_to_run
# grab a set of tests that testing framework will run
if [[ -n ${JAVA_TEST_RUN_ONLY} ]]; then
tests_to_run="${JAVA_TEST_RUN_ONLY[@]}"
else
pushd "${JAVA_TEST_SRC_DIR}" > /dev/null || die
tests_to_run=$(find * -type f\
\( -name "*Test.java"\
-o -name "Test*.java"\
-o -name "*Tests.java"\
-o -name "*TestCase.java" \)\
! -name "*Abstract*"\
! -name "*BaseTest*"\
! -name "*TestTypes*"\
! -name "*TestUtils*"\
! -name "*\$*")
tests_to_run=${tests_to_run//"${classes}"\/}
tests_to_run=${tests_to_run//.java}
tests_to_run=${tests_to_run//\//.}
popd > /dev/null || die
# exclude extra test classes, usually corner cases
# that the code above cannot handle
local class
for class in "${JAVA_TEST_EXCLUDES[@]}"; do
tests_to_run=${tests_to_run//${class}}
done
fi
ejunit5 -classpath "${classpath}" ${tests_to_run}
}
# @FUNCTION: _junit5_src_test_scan-classpath
# @INTERNAL
# @DESCRIPTION:
# If JAVA_TEST_RUN_ONLY is defined, runs only the tests listed in it on the
# JUnit Platform.
# Otherwise, runs the JUnit Platform's ConsoleLauncher with the
# '--scan-classpath' to let the JUnit Platform automatically detect, select,
# and run tests. JAVA_TEST_EXCLUDES is still honored in this case.
_junit5_src_test_scan-classpath() {
debug-print-function ${FUNCNAME} $*
if [[ -n ${JAVA_TEST_RUN_ONLY} ]]; then
ejunit5 -classpath "${classpath}" ${JAVA_TEST_RUN_ONLY[@]}
else
local args=(
--scan-classpath
)
# 'includes' and 'excludes' may be set by another function.
#
# The 'classname' options take a regular expression for a class's
# fully qualified name, which contains the class's package.
# '^(.*\.)*' matches the package part in the class name;
# '[^.]*$' prevents the pattern for a class name to match any part of
# the package name.
local pattern
for pattern in "${includes[@]}"; do
args+=( --include-classname="^(.*\\.)*${pattern}[^.]*\$" )
done
for pattern in "${excludes[@]}"; do
args+=( --exclude-classname="^(.*\\.)*${pattern}[^.]*\$" )
done
local class
for class in "${JAVA_TEST_EXCLUDES[@]}"; do
args+=( --exclude-classname="^${class//./\\.}\$" )
done
_junit5_ConsoleLauncher "${classpath}" "${args[@]}"
fi
}
# @FUNCTION: _junit5_src_test_scan-classpath+pattern
# @INTERNAL
# @DESCRIPTION:
# If JAVA_TEST_RUN_ONLY is defined, runs only the tests listed in it on the
# JUnit Platform.
# Otherwise, finds tests to run using the JUnit Platform's ConsoleLauncher's
# '--scan-classpath' option, and also includes and excludes class names based
# on the test class name patterns that java-pkg-simple.eclass uses. Then, runs
# the found tests on the JUnit Platform.
_junit5_src_test_scan-classpath+pattern() {
debug-print-function ${FUNCNAME} $*
local includes=(
'.*Test'
'Test.*'
'.*Tests'
'.*TestCase'
)
local excludes=(
'.*Abstract.*'
'.*BaseTest.*'
'.*TestTypes.*'
'.*TestUtils.*'
'.*\$.*'
)
_junit5_src_test_scan-classpath
}
# @FUNCTION: _junit5_src_test_console-args
# @INTERNAL
# @DESCRIPTION:
# Does not do anything with regards to test selection at all; instead, passes
# JAVA_JUNIT_CONSOLE_ARGS to JUnit Platform's ConsoleLauncher, and lets the
# arguments in JAVA_JUNIT_CONSOLE_ARGS control test selection.
_junit5_src_test_console-args() {
_junit5_ConsoleLauncher "${classpath}" ${JAVA_JUNIT_CONSOLE_ARGS}
}
_JUNIT5_ECLASS=1
fi
EXPORT_FUNCTIONS pkg_setup src_test