[arch-projects] [mkinitcpio][PATCH 1/2] add test harness for parse_cmdline
Some of these tests currently fail including hard failures where the shell quits entirely (this would lead to a kernel panic). A followup commit will rewrite the parse_cmdline function to improve it and fix these deficiencies. --- Makefile | 3 + test/test_parse_cmdline | 233 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100755 test/test_parse_cmdline diff --git a/Makefile b/Makefile index 5af0eb2..6c70718 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,9 @@ man/%: man/%.txt Makefile -a manversion=$(VERSION) \ -a manmanual="mkinitcpio manual" $< +check: + @r=0; for t in test/test_*; do $$t || { echo $$t fail; r=1; }; done; exit $$r + clean: $(RM) mkinitcpio-${VERSION}.tar.gz $(MANPAGES) diff --git a/test/test_parse_cmdline b/test/test_parse_cmdline new file mode 100755 index 0000000..9afd9cb --- /dev/null +++ b/test/test_parse_cmdline @@ -0,0 +1,233 @@ +#!/lib/initcpio/busybox ash + +. ./init_functions + +failed=0 +tests=0 + +e_ok=0 +e_parser_failure=2 +e_assertion_failure=130 + +assert() { + local expect_fail= key= expected_value= actual_value + + if [ "$1" = '--expect-fail' ]; then + expect_fail=y + shift + fi + + key=$1 expected_value=$2 + eval actual_value=\$"$1" + + case $actual_value in + $expected_value) + if [ -n "$expect_fail" ]; then + echo "EXPECTED FAIL: $key: expected='$expected_value', got='$actual_value'" + return 1 + fi + ;; + *) + if [ -z "$expect_fail" ]; then + echo "FAIL: $key: expected='$expected_value', got='$actual_value'" + return 1 + fi + ;; + esac + + return 0 +} + +test_parse() { + local flag= cmdline= expect_fail= expect_parse_fail= + + tests=$(( tests + 1 )) + + for flag; do + case $flag in + --expect-fail) + expect_fail='--expect-fail' + shift + ;; + --expect-parse-fail) + expect_parse_fail=y + shift + ;; + *) + break + ;; + esac + done + + cmdline=$1; shift + [ -n "$V" ] && echo "testing cmdline: $cmdline" + + echo "$cmdline" | { + parse_cmdline + + result=0 + while [ "$#" -gt 0 ]; do + key=$1 expected_value=$2 + shift 2 + + assert $expect_fail "$key" "$expected_value" || result=$e_assertion_failure + done + + exit "$result" + } 2>/dev/null + + case $? in + $e_parser_failure) + # parser failure + if [ -z "$expect_parse_fail" ]; then + echo "FAIL: parse_cmdline failed" + failed=$(( failed + 1 )) + fi + ;; + $e_assertion_failure) + # test assertion failure + failed=$(( failed + 1 )) + ;; + $e_ok) + if [ -n "$expect_parse_fail" ]; then + echo "EXPECTED_FAIL: parse_cmdline succeeded" + failed=$(( failed + 1 )) + fi + ;; + esac +} + +# bare words +test_parse 'foo' \ + 'foo' 'y' +test_parse 'foo bar' \ + 'foo' 'y' \ + 'bar' 'y' + +# overwriting +test_parse 'foo=bar bar=baz foo bar="no pe"' \ + 'bar' 'no pe' \ + 'foo' 'y' + +# simple key=value assignment +test_parse 'foo=bar' \ + 'foo' 'bar' +test_parse 'foo=bar bar=baz' \ + 'foo' 'bar' \ + 'bar' 'baz' +test_parse '_derpy=hooves' \ + '_derpy' 'hooves' +test_parse 'f5=abc f_5_=abc' \ + 'f5' 'abc' \ + 'f_5_' 'abc' +test_parse 'v="foo bar=baz"' \ + 'v' 'foo bar=baz' + +# double quoting +test_parse 'foo="bar"' \ + 'foo' 'bar' +test_parse 'foo="bar baz"' \ + 'foo' 'bar baz' + +# single quoting +test_parse --expect-fail "foo='bar'" \ + 'foo' 'bar' +test_parse --expect-parse-fail "foo='bar baz'" \ + 'foo' 'bar baz' + +# dangling quotes +test_parse --expect-fail 'foo="bar' \ + 'foo' '"bar' +test_parse 'foo=bar"' \ + 'foo' 'bar"' + +# nested quotes +test_parse --expect-parse-fail "foo='\"bar baz\"' herp='\"de\"rp'" \ + 'foo' '"bar baz"' \ + 'herp' '"de"rp' + +# escaped quotes +test_parse 'foo=bar"baz' \ + 'foo' 'bar"baz' + +# neighboring quoted regions +test_parse --expect-fail 'foo="bar""baz"' \ + 'foo' 'barbaz' +test_parse --expect-fail "foo=\"bar\"'baz'" \ + 'foo' "barbaz" +test_parse --expect-fail "foo='bar'\"baz\"" \ + 'foo' "barbaz" + +# comments +test_parse 'foo=bar # ignored content' \ + 'foo' 'bar' \ + 'ignored' '' \ + 'content' '' +test_parse 'foo=bar #ignored content' \ + 'foo' 'bar' \ + 'ignored' '' \ + 'content' '' +test_parse 'foo="bar #baz" parse=this' \ + 'foo' 'bar #baz' \ + 'parse' 'this' + +# shell metachars +test_parse 'foo=*' \ + 'foo' '\*' +test_parse --expect-fail 'Make*' \ + 'Makefile' '' +test_parse --expect-fail '[Makefile]*' \ + 'Makefile' '' \ + 'init' '' \ + 'functions' '' + +# invalid names +test_parse 'in-valid=name' +test_parse '6foo=bar' +test_parse --expect-parse-fail '"gar bage"' \ + 'gar' '' \ + 'bage' '' + +# special handling +test_parse 'rw' \ + 'ro' '' \ + 'rw' '' \ + 'rwopt' 'rw' +test_parse 'ro' \ + 'ro' '' \ + 'rw' '' \ + 'rwopt' 'ro' +test_parse --expect-fail 'fstype=btrfs' \ + 'rootfstype' 'btrfs' +test_parse 'fsck.mode=force' \ + 'forcefsck' 'y' \ + 'fastboot' '' +test_parse 'fsck.mode=skip' \ + 'forcefsck' '' \ + 'fastboot' 'y' +test_parse 'rd.debug' \ + 'rd_debug' 'y' +test_parse 'rd.log' \ + 'rd_logmask' '6' +test_parse 'rd.log=all' \ + 'rd_logmask' '7' +test_parse 'rd.log=console' \ + 'rd_logmask' '4' +test_parse 'rd.log=kmsg' \ + 'rd_logmask' '2' +test_parse 'rd.log=file' \ + 'rd_logmask' '1' + +# a mix of stuff +test_parse 'foo=bar bareword bar="ba az"' \ + 'foo' 'bar' \ + 'bareword' 'y' \ + 'bar' 'ba az' + +if [ "$failed" -eq 0 ]; then + echo "PASS: ${0##*/test_} ($tests tests)" + exit 0 +else + echo "FAIL: $failed tests failed" + exit 1 +fi -- 2.8.3
This refactors parse_cmdline into a few chunks of code, mainly to separate the work of parsing from the handling of the actual parsed parameters. By default, parse_cmdline_item is used as the callback for handling the parameters, but this could be overriden by other code sourcing init_functions if desirable. Our test harness passes more tests, but we leave behind some expected failures to describe the cases where it still fails. Fortunately I've not yet been able to find any cases which warrant --expect-parse-fail. --- init_functions | 175 ++++++++++++++++++++++++++++++++---------------- test/test_parse_cmdline | 16 ++--- 2 files changed, 125 insertions(+), 66 deletions(-) diff --git a/init_functions b/init_functions index 05f4ab0..d5a584e 100644 --- a/init_functions +++ b/init_functions @@ -114,74 +114,133 @@ set_log_option() { done } -parse_cmdline() { - local _w _quoted _lhs _rhs _cmdline - read -r _cmdline - for _w in $_cmdline; do - if [ -z "$_quoted" ]; then - case $_w in - # ignore everything after a # in the commandline - \#*) break ;; - # special cases - rw|ro) rwopt=$_w ;; - fstype) rootfstype=$_w ;; - fsck.mode=*) - case ${_w#*=} in - force) - forcefsck=y - ;; - skip) - fastboot=y - ;; - *) - err "unknown fsck.mode parameter: '${_w#*=}'" - ;; - esac +startswith() { + local word=$1 prefix=$2 + + case $word in + $prefix*) + return 0 + ;; + esac + + return 1 +} + +endswith() { + local word=$1 suffix=$2 + + case $word in + *$suffix) + return 0 + ;; + esac + + return 1 +} + +parse_cmdline_item() { + local key=$1 value=$2 + + case $key in + rw|ro) + rwopt=$key + ;; + fstype) + # The kernel understands 'rootfstype', but mkinitcpio has (without + # documentation) supported 'fstype' instead. Ensure we support both + # for backwards compat, but make fstype legacy. + rootfstype=$value + ;; + fsck.mode) + case $value in + force) + forcefsck=y ;; - rd.*) - case ${_w#rd.} in - debug) - rd_debug=y - ;; - log) - rd_logmask=$(( _rdlog_kmsg | _rdlog_cons )) - ;; - log=*) - set_log_option "${_w#rd.log=}" - ;; - esac + skip) + fastboot=y ;; - # abide by shell variable naming rules - [[:alpha:]_]*=*) - _rhs=${_w#*=} - _lhs=${_w%%=*} - _lhs=${_lhs//[-.]/_} - if [ '"' = "${_rhs:0:1}" ]; then - if [ '"' = "${_rhs:$((${#_rhs}-1))}" ]; then - _rhs="${_rhs:1:$((${#_rhs}-2))}" - else - _rhs=${_rhs:1} - _quoted=1 - continue - fi + *) + err "unknown fsck.mode parameter: '$value'" + ;; + esac + ;; + rd.debug) + rd_debug=y + ;; + rd.log) + if [ -n "$value" ]; then + set_log_option "$value" + else + rd_logmask=$(( _rdlog_kmsg | _rdlog_cons )) + fi + ;; + [![:alpha:]_]*|[[:alpha:]_]*[![:alnum:]_]*) + # invalid shell variable, ignore it + ;; + *) + # valid shell variable + eval "$key"='${value:-y}' + ;; + esac +} + +process_cmdline_param() { + local item_callback=$1 key=$2 value=$3 + + # maybe unquote the value + if startswith "$value" "[\"']" && endswith "$value" "${value:0:1}"; then + value=${value#?} value=${value%?} + fi + + "$item_callback" "$key" "$value" +} + +parse_cmdline() { + local item_callback=${1:-parse_cmdline_item} + local cmdline word quoted key value + + set -f + read -r cmdline + set -- $cmdline + set +f + + for word; do + if [ -n "$quoted" ]; then + value="$value $word" + else + case $word in + *=*) + key=${word%%=*} + value=${word#*=} + + if startswith "$value" "[\"']"; then + quoted=${value:0:1} fi - eval $_lhs=\$_rhs ;; - [[:alpha:]_]*) - _lhs=${_w//[-.]/_} - eval $_lhs=y + '#'*) + break + ;; + *) + key=$word ;; esac - else - if [ '"' = "${_w:$((${#_w}-1))}" ]; then - _rhs="$_rhs ${_w%\"}" - unset _quoted - eval $_lhs=\$_rhs + fi + + if [ -n "$quoted" ]; then + if endswith "$value" "$quoted"; then + unset quoted else - _rhs="$_rhs $_w" + continue fi fi + + process_cmdline_param "$item_callback" "$key" "$value" + unset key value done + + if [ -n "$key" ]; then + process_cmdline_param "$item_callback" "$key" "$value" + fi } fsck_device() { diff --git a/test/test_parse_cmdline b/test/test_parse_cmdline index 9afd9cb..ff855e4 100755 --- a/test/test_parse_cmdline +++ b/test/test_parse_cmdline @@ -130,19 +130,19 @@ test_parse 'foo="bar baz"' \ 'foo' 'bar baz' # single quoting -test_parse --expect-fail "foo='bar'" \ +test_parse "foo='bar'" \ 'foo' 'bar' -test_parse --expect-parse-fail "foo='bar baz'" \ +test_parse "foo='bar baz'" \ 'foo' 'bar baz' # dangling quotes -test_parse --expect-fail 'foo="bar' \ +test_parse 'foo="bar' \ 'foo' '"bar' test_parse 'foo=bar"' \ 'foo' 'bar"' # nested quotes -test_parse --expect-parse-fail "foo='\"bar baz\"' herp='\"de\"rp'" \ +test_parse "foo='\"bar baz\"' herp='\"de\"rp'" \ 'foo' '"bar baz"' \ 'herp' '"de"rp' @@ -174,9 +174,9 @@ test_parse 'foo="bar #baz" parse=this' \ # shell metachars test_parse 'foo=*' \ 'foo' '\*' -test_parse --expect-fail 'Make*' \ +test_parse 'Make*' \ 'Makefile' '' -test_parse --expect-fail '[Makefile]*' \ +test_parse '[Makefile]*' \ 'Makefile' '' \ 'init' '' \ 'functions' '' @@ -184,7 +184,7 @@ test_parse --expect-fail '[Makefile]*' \ # invalid names test_parse 'in-valid=name' test_parse '6foo=bar' -test_parse --expect-parse-fail '"gar bage"' \ +test_parse '"gar bage"' \ 'gar' '' \ 'bage' '' @@ -197,7 +197,7 @@ test_parse 'ro' \ 'ro' '' \ 'rw' '' \ 'rwopt' 'ro' -test_parse --expect-fail 'fstype=btrfs' \ +test_parse 'fstype=btrfs' \ 'rootfstype' 'btrfs' test_parse 'fsck.mode=force' \ 'forcefsck' 'y' \ -- 2.8.3
participants (1)
-
Dave Reisner