Bug: https://bugs.gentoo.org/482170 --- bin/eapi.sh | 4 + bin/eapi7-ver-funcs.sh | 191 ++++++++++++++++++++ bin/phase-helpers.sh | 4 + pym/portage/tests/bin/test_eapi7_ver_funcs.py | 240 ++++++++++++++++++++++++++ 4 files changed, 439 insertions(+) create mode 100644 bin/eapi7-ver-funcs.sh create mode 100644 pym/portage/tests/bin/test_eapi7_ver_funcs.py
diff --git a/bin/eapi.sh b/bin/eapi.sh index 5d77c8daf..cd2acad80 100644 --- a/bin/eapi.sh +++ b/bin/eapi.sh @@ -108,6 +108,10 @@ ___eapi_has_sandbox_rm_functions() { [[ ! ${1-${EAPI-0}} =~ ^(0|1|2|3|4|4-python|4-slot-abi|5|5-progress|6)$ ]] } +___eapi_has_version_functions() { + [[ ! ${1-${EAPI-0}} =~ ^(0|1|2|3|4|4-python|4-slot-abi|5|5-progress|6)$ ]] +} + ___eapi_has_master_repositories() { [[ ${1-${EAPI-0}} =~ ^(5-progress)$ ]] } diff --git a/bin/eapi7-ver-funcs.sh b/bin/eapi7-ver-funcs.sh new file mode 100644 index 000000000..b4e98f4e7 --- /dev/null +++ b/bin/eapi7-ver-funcs.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# Copyright 1999-2018 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +__eapi7_ver_parse_range() { + local range=${1} + local max=${2} + + [[ ${range} == [0-9]* ]] \ + || die "${FUNCNAME}: range must start with a number" + start=${range%-*} + [[ ${range} == *-* ]] && end=${range#*-} || end=${start} + if [[ ${end} ]]; then + [[ ${start} -le ${end} ]] \ + || die "${FUNCNAME}: end of range must be >= start" + [[ ${end} -le ${max} ]] || end=${max} + else + end=${max} + fi +} + +__eapi7_ver_split() { + local v=${1} LC_ALL=C + + comp=() + + # get separators and components + local s c + while [[ ${v} ]]; do + # cut the separator + s=${v%%[a-zA-Z0-9]*} + v=${v:${#s}} + # cut the next component; it can be either digits or letters + [[ ${v} == [0-9]* ]] && c=${v%%[^0-9]*} || c=${v%%[^a-zA-Z]*} + v=${v:${#c}} + + comp+=( "${s}" "${c}" ) + done +} + +ver_cut() { + local range=${1} + local v=${2:-${PV}} + local start end + local -a comp + + __eapi7_ver_split "${v}" + local max=$((${#comp[@]}/2)) + __eapi7_ver_parse_range "${range}" "${max}" + + local IFS= + if [[ ${start} -gt 0 ]]; then + start=$(( start*2 - 1 )) + fi + echo "${comp[*]:start:end*2-start}" +} + +ver_rs() { + local v + (( ${#} & 1 )) && v=${@: -1} || v=${PV} + local start end i + local -a comp + + __eapi7_ver_split "${v}" + local max=$((${#comp[@]}/2 - 1)) + + while [[ ${#} -ge 2 ]]; do + __eapi7_ver_parse_range "${1}" "${max}" + for (( i = start*2; i <= end*2; i+=2 )); do + [[ ${i} -eq 0 && -z ${comp[i]} ]] && continue + comp[i]=${2} + done + shift 2 + done + + local IFS= + echo "${comp[*]}" +} + +__eapi7_ver_compare_int() { + local a=$1 b=$2 d=$(( ${#1}-${#2} )) + + # Zero-pad to equal length if necessary. + if [[ ${d} -gt 0 ]]; then + printf -v b "%0${d}d%s" 0 "${b}" + elif [[ ${d} -lt 0 ]]; then + printf -v a "%0$(( -d ))d%s" 0 "${a}" + fi + + [[ ${a} > ${b} ]] && return 3 + [[ ${a} == "${b}" ]] +} + +__eapi7_ver_compare() { + local va=${1} vb=${2} a an al as ar b bn bl bs br re LC_ALL=C + + re="^([0-9]+(\.[0-9]+)*)([a-z]?)((_(alpha|beta|pre|rc|p)[0-9]*)*)(-r[0-9]+)?$" + + [[ ${va} =~ ${re} ]] || die "${FUNCNAME}: invalid version: ${va}" + an=${BASH_REMATCH[1]} + al=${BASH_REMATCH[3]} + as=${BASH_REMATCH[4]} + ar=${BASH_REMATCH[7]} + + [[ ${vb} =~ ${re} ]] || die "${FUNCNAME}: invalid version: ${vb}" + bn=${BASH_REMATCH[1]} + bl=${BASH_REMATCH[3]} + bs=${BASH_REMATCH[4]} + br=${BASH_REMATCH[7]} + + # Compare numeric components (PMS algorithm 3.2) + # First component + __eapi7_ver_compare_int "${an%%.*}" "${bn%%.*}" || return + + while [[ ${an} == *.* && ${bn} == *.* ]]; do + # Other components (PMS algorithm 3.3) + an=${an#*.} + bn=${bn#*.} + a=${an%%.*} + b=${bn%%.*} + if [[ ${a} == 0* || ${b} == 0* ]]; then + # Remove any trailing zeros + [[ ${a} =~ 0+$ ]] && a=${a%"${BASH_REMATCH[0]}"} + [[ ${b} =~ 0+$ ]] && b=${b%"${BASH_REMATCH[0]}"} + [[ ${a} > ${b} ]] && return 3 + [[ ${a} < ${b} ]] && return 1 + else + __eapi7_ver_compare_int "${a}" "${b}" || return + fi + done + [[ ${an} == *.* ]] && return 3 + [[ ${bn} == *.* ]] && return 1 + + # Compare letter components (PMS algorithm 3.4) + [[ ${al} > ${bl} ]] && return 3 + [[ ${al} < ${bl} ]] && return 1 + + # Compare suffixes (PMS algorithm 3.5) + as=${as#_}${as:+_} + bs=${bs#_}${bs:+_} + while [[ -n ${as} && -n ${bs} ]]; do + # Compare each suffix (PMS algorithm 3.6) + a=${as%%_*} + b=${bs%%_*} + if [[ ${a%%[0-9]*} == "${b%%[0-9]*}" ]]; then + __eapi7_ver_compare_int "${a##*[a-z]}" "${b##*[a-z]}" || return + else + # Check for p first + [[ ${a%%[0-9]*} == p ]] && return 3 + [[ ${b%%[0-9]*} == p ]] && return 1 + # Hack: Use that alpha < beta < pre < rc alphabetically + [[ ${a} > ${b} ]] && return 3 || return 1 + fi + as=${as#*_} + bs=${bs#*_} + done + if [[ -n ${as} ]]; then + [[ ${as} == p[_0-9]* ]] && return 3 || return 1 + elif [[ -n ${bs} ]]; then + [[ ${bs} == p[_0-9]* ]] && return 1 || return 3 + fi + + # Compare revision components (PMS algorithm 3.7) + __eapi7_ver_compare_int "${ar#-r}" "${br#-r}" || return + + return 2 +} + +ver_test() { + local va op vb + + if [[ $# -eq 3 ]]; then + va=${1} + shift + else + va=${PVR} + fi + + [[ $# -eq 2 ]] || die "${FUNCNAME}: bad number of arguments" + + op=${1} + vb=${2} + + case ${op} in + -eq|-ne|-lt|-le|-gt|-ge) ;; + *) die "${FUNCNAME}: invalid operator: ${op}" ;; + esac + + __eapi7_ver_compare "${va}" "${vb}" + test $? "${op}" 2 +} diff --git a/bin/phase-helpers.sh b/bin/phase-helpers.sh index 3a2138636..8bb110082 100644 --- a/bin/phase-helpers.sh +++ b/bin/phase-helpers.sh @@ -1172,6 +1172,10 @@ if ___eapi_has_in_iuse; then } fi +if ___eapi_has_version_functions; then + source "${PORTAGE_BIN_PATH}/eapi7-ver-funcs.sh" || exit 1 +fi + if ___eapi_has_master_repositories; then master_repositories() { local output repository=$1 retval diff --git a/pym/portage/tests/bin/test_eapi7_ver_funcs.py b/pym/portage/tests/bin/test_eapi7_ver_funcs.py new file mode 100644 index 000000000..408975298 --- /dev/null +++ b/pym/portage/tests/bin/test_eapi7_ver_funcs.py @@ -0,0 +1,240 @@ +# Copyright 2018 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import subprocess +import tempfile + +from portage.const import PORTAGE_BIN_PATH +from portage.tests import TestCase + + +class TestEAPI7VerFuncs(TestCase): + def _test_output(self, test_cases): + """ + Test that commands in test_cases produce expected output. + """ + with tempfile.NamedTemporaryFile('w') as test_script: + test_script.write('source "%s"/eapi7-ver-funcs.sh\n' + % (PORTAGE_BIN_PATH,)) + for cmd, exp in test_cases: + test_script.write('%s\n' % (cmd,)) + test_script.flush() + + s = subprocess.Popen(['bash', test_script.name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + sout, serr = s.communicate() + self.assertEqual(s.returncode, 0) + + for test_case, result in zip(test_cases, sout.decode().splitlines()): + cmd, exp = test_case + self.assertEqual(result, exp, + '%s -> %s; expected: %s' % (cmd, result, exp)) + + def _test_return(self, test_cases): + """ + Test that commands in test_cases give appropriate exit codes. + """ + with tempfile.NamedTemporaryFile('w+') as test_script: + test_script.write('source "%s"/eapi7-ver-funcs.sh\n' + % (PORTAGE_BIN_PATH,)) + for cmd, exp in test_cases: + test_script.write('%s; echo $?\n' % (cmd,)) + test_script.flush() + + s = subprocess.Popen(['bash', test_script.name], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + sout, serr = s.communicate() + self.assertEqual(s.returncode, 0) + + for test_case, result in zip(test_cases, sout.decode().splitlines()): + cmd, exp = test_case + self.assertEqual(result, exp, + '%s -> %s; expected: %s' % (cmd, result, exp)) + + def _test_fail(self, test_cases): + """ + Test that commands in test_cases fail. + """ + + for cmd in test_cases: + test = ''' +source "%s"/eapi7-ver-funcs.sh +die() { exit 1; } +%s''' % (PORTAGE_BIN_PATH, cmd) + + s = subprocess.Popen(['bash', '-c', test], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + sout, serr = s.communicate() + self.assertEqual(s.returncode, 1, + '"%s" did not fail; output: %s; %s)' + % (cmd, sout.decode(), serr.decode())) + + def test_ver_cut(self): + test_cases = [ + # (command, output) + ('ver_cut 1 1.2.3', '1'), + ('ver_cut 1-1 1.2.3', '1'), + ('ver_cut 1-2 1.2.3', '1.2'), + ('ver_cut 2- 1.2.3', '2.3'), + ('ver_cut 1- 1.2.3', '1.2.3'), + ('ver_cut 3-4 1.2.3b_alpha4', '3b'), + ('ver_cut 5 1.2.3b_alpha4', 'alpha'), + ('ver_cut 1-2 .1.2.3', '1.2'), + ('ver_cut 0-2 .1.2.3', '.1.2'), + ('ver_cut 2-3 1.2.3.', '2.3'), + ('ver_cut 2- 1.2.3.', '2.3.'), + ('ver_cut 2-4 1.2.3.', '2.3.'), + ] + self._test_output(test_cases) + + def test_ver_rs(self): + test_cases = [ + # (command, output) + ('ver_rs 1 - 1.2.3', '1-2.3'), + ('ver_rs 2 - 1.2.3', '1.2-3'), + ('ver_rs 1-2 - 1.2.3.4', '1-2-3.4'), + ('ver_rs 2- - 1.2.3.4', '1.2-3-4'), + ('ver_rs 2 . 1.2-3', '1.2.3'), + ('ver_rs 3 . 1.2.3a', '1.2.3.a'), + ('ver_rs 2-3 - 1.2_alpha4', '1.2-alpha-4'), + ('ver_rs 3 - 2 "" 1.2.3b_alpha4', '1.23-b_alpha4'), + ('ver_rs 3-5 _ 4-6 - a1b2c3d4e5', 'a1b_2-c-3-d4e5'), + ('ver_rs 1 - .1.2.3', '.1-2.3'), + ('ver_rs 0 - .1.2.3', '-1.2.3'), + ] + self._test_output(test_cases) + + def test_truncated_range(self): + test_cases = [ + # (command, output) + ('ver_cut 0-2 1.2.3', '1.2'), + ('ver_cut 2-5 1.2.3', '2.3'), + ('ver_cut 4 1.2.3', ''), + ('ver_cut 0 1.2.3', ''), + ('ver_cut 4- 1.2.3', ''), + ('ver_rs 0 - 1.2.3', '1.2.3'), + ('ver_rs 3 . 1.2.3', '1.2.3'), + ('ver_rs 3- . 1.2.3', '1.2.3'), + ('ver_rs 3-5 . 1.2.3', '1.2.3'), + ] + self._test_output(test_cases) + + def test_invalid_range(self): + test_cases = [ + 'ver_cut foo 1.2.3', + 'ver_rs -3 _ a1b2c3d4e5', + 'ver_rs 5-3 _ a1b2c3d4e5', + ] + self._test_fail(test_cases) + + def test_ver_test(self): + test_cases = [ + # Tests from Portage's test_vercmp.py + ('ver_test 6.0 -gt 5.0', '0'), + ('ver_test 5.0 -gt 5', '0'), + ('ver_test 1.0-r1 -gt 1.0-r0', '0'), + ('ver_test 999999999999999999 -gt 999999999999999998', '0'), # 18 digits + ('ver_test 1.0.0 -gt 1.0', '0'), + ('ver_test 1.0.0 -gt 1.0b', '0'), + ('ver_test 1b -gt 1', '0'), + ('ver_test 1b_p1 -gt 1_p1', '0'), + ('ver_test 1.1b -gt 1.1', '0'), + ('ver_test 12.2.5 -gt 12.2b', '0'), + ('ver_test 4.0 -lt 5.0', '0'), + ('ver_test 5 -lt 5.0', '0'), + ('ver_test 1.0_pre2 -lt 1.0_p2', '0'), + ('ver_test 1.0_alpha2 -lt 1.0_p2', '0'), + ('ver_test 1.0_alpha1 -lt 1.0_beta1', '0'), + ('ver_test 1.0_beta3 -lt 1.0_rc3', '0'), + ('ver_test 1.001000000000000001 -lt 1.001000000000000002', '0'), + ('ver_test 1.00100000000 -lt 1.001000000000000001', '0'), + ('ver_test 999999999999999998 -lt 999999999999999999', '0'), + ('ver_test 1.01 -lt 1.1', '0'), + ('ver_test 1.0-r0 -lt 1.0-r1', '0'), + ('ver_test 1.0 -lt 1.0-r1', '0'), + ('ver_test 1.0 -lt 1.0.0', '0'), + ('ver_test 1.0b -lt 1.0.0', '0'), + ('ver_test 1_p1 -lt 1b_p1', '0'), + ('ver_test 1 -lt 1b', '0'), + ('ver_test 1.1 -lt 1.1b', '0'), + ('ver_test 12.2b -lt 12.2.5', '0'), + ('ver_test 4.0 -eq 4.0', '0'), + ('ver_test 1.0 -eq 1.0', '0'), + ('ver_test 1.0-r0 -eq 1.0', '0'), + ('ver_test 1.0 -eq 1.0-r0', '0'), + ('ver_test 1.0-r0 -eq 1.0-r0', '0'), + ('ver_test 1.0-r1 -eq 1.0-r1', '0'), + ('ver_test 1 -eq 2', '1'), + ('ver_test 1.0_alpha -eq 1.0_pre', '1'), + ('ver_test 1.0_beta -eq 1.0_alpha', '1'), + ('ver_test 1 -eq 0.0', '1'), + ('ver_test 1.0-r0 -eq 1.0-r1', '1'), + ('ver_test 1.0-r1 -eq 1.0-r0', '1'), + ('ver_test 1.0 -eq 1.0-r1', '1'), + ('ver_test 1.0-r1 -eq 1.0', '1'), + ('ver_test 1.0 -eq 1.0.0', '1'), + ('ver_test 1_p1 -eq 1b_p1', '1'), + ('ver_test 1b -eq 1', '1'), + ('ver_test 1.1b -eq 1.1', '1'), + ('ver_test 12.2b -eq 12.2', '1'), + + # A subset of tests from Paludis + ('ver_test 1.0_alpha -gt 1_alpha', '0'), + ('ver_test 1.0_alpha -gt 1', '0'), + ('ver_test 1.0_alpha -lt 1.0', '0'), + ('ver_test 1.2.0.0_alpha7-r4 -gt 1.2_alpha7-r4', '0'), + ('ver_test 0001 -eq 1', '0'), + ('ver_test 01 -eq 001', '0'), + ('ver_test 0001.1 -eq 1.1', '0'), + ('ver_test 01.01 -eq 1.01', '0'), + ('ver_test 1.010 -eq 1.01', '0'), + ('ver_test 1.00 -eq 1.0', '0'), + ('ver_test 1.0100 -eq 1.010', '0'), + ('ver_test 1-r00 -eq 1-r0', '0'), + + # Additional tests + ('ver_test 0_rc99 -lt 0', '0'), + ('ver_test 011 -eq 11', '0'), + ('ver_test 019 -eq 19', '0'), + ('ver_test 1.2 -eq 001.2', '0'), + ('ver_test 1.2 -gt 1.02', '0'), + ('ver_test 1.2a -lt 1.2b', '0'), + ('ver_test 1.2_pre1 -gt 1.2_pre1_beta2', '0'), + ('ver_test 1.2_pre1 -lt 1.2_pre1_p2', '0'), + ('ver_test 1.00 -lt 1.0.0', '0'), + ('ver_test 1.010 -eq 1.01', '0'), + ('ver_test 1.01 -lt 1.1', '0'), + ('ver_test 1.2_pre08-r09 -eq 1.2_pre8-r9', '0'), + ('ver_test 0 -lt 576460752303423488', '0'), # 2**59 + ('ver_test 0 -lt 9223372036854775808', '0'), # 2**63 + ] + self._test_return(test_cases) + + def test_invalid_test(self): + test_cases = [ + # Bad number or ordering of arguments + 'ver_test 1', + 'ver_test 1 -lt 2 3', + 'ver_test -lt 1 2', + + # Bad operators + 'ver_test 1 "<" 2', + 'ver_test 1 lt 2', + 'ver_test 1 -foo 2', + + # Malformed versions + 'ver_test "" -ne 1', + 'ver_test 1. -ne 1', + 'ver_test 1ab -ne 1', + 'ver_test b -ne 1', + 'ver_test 1-r1_pre -ne 1', + 'ver_test 1-pre1 -ne 1', + 'ver_test 1_foo -ne 1', + 'ver_test 1_pre1.1 -ne 1', + 'ver_test 1-r1.0 -ne 1', + 'ver_test cvs.9999 -ne 9999', + ] + self._test_fail(test_cases) -- 2.16.2