diff options
Diffstat (limited to 'eclass/junit5.eclass')
| -rwxr-xr-x | eclass/junit5.eclass | 500 |
1 files changed, 500 insertions, 0 deletions
diff --git a/eclass/junit5.eclass b/eclass/junit5.eclass new file mode 100755 index 000000000000..4fb04894e963 --- /dev/null +++ b/eclass/junit5.eclass @@ -0,0 +1,500 @@ +# 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 --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 |
