diff --git a/check b/check
index 607d2456e..4dc266dcf 100755
--- a/check
+++ b/check
@@ -15,19 +15,13 @@ notrun=()
 interrupt=true
 diff="diff -u"
 showme=false
-have_test_arg=false
-randomize=false
-exact_order=false
 export here=`pwd`
-xfile=""
-subdir_xfile=""
 brief_test_summary=false
 do_report=false
 DUMP_OUTPUT=false
 iterations=1
 istop=false
 loop_on_fail=0
-exclude_tests=()
 
 # This is a global variable used to pass test failure text to reporting gunk
 _err_msg=""
@@ -49,8 +43,9 @@ timestamp=${TIMESTAMP:=false}
 
 rm -f $tmp.list $tmp.tmp $tmp.grep $here/$iam.out $tmp.report.* $tmp.arglist
 
-SRC_GROUPS="generic"
-export SRC_DIR="tests"
+# We need to include the test list processing first as argument parsing
+# requires test list parsing and setup.
+. ./common/test_list
 
 usage()
 {
@@ -124,153 +119,12 @@ examples:
 	    exit 1
 }
 
-get_sub_group_list()
-{
-	local d=$1
-	local grp=$2
-
-	test -s "$SRC_DIR/$d/group.list" || return 1
-
-	local grpl=$(sed -n < $SRC_DIR/$d/group.list \
-		-e 's/#.*//' \
-		-e 's/$/ /' \
-		-e "s;^\($VALID_TEST_NAME\).* $grp .*;$SRC_DIR/$d/\1;p")
-	echo $grpl
-}
-
-get_group_list()
-{
-	local grp=$1
-	local grpl=""
-	local sub=$(dirname $grp)
-	local fsgroup="$FSTYP"
-
-	if [ -n "$sub" -a "$sub" != "." -a -d "$SRC_DIR/$sub" ]; then
-		# group is given as <subdir>/<group> (e.g. xfs/quick)
-		grp=$(basename $grp)
-		get_sub_group_list $sub $grp
-		return
-	fi
-
-	if [ "$FSTYP" = ext2 -o "$FSTYP" = ext3 ]; then
-	    fsgroup=ext4
-	fi
-	for d in $SRC_GROUPS $fsgroup; do
-		if ! test -d "$SRC_DIR/$d" ; then
-			continue
-		fi
-		grpl="$grpl $(get_sub_group_list $d $grp)"
-	done
-	echo $grpl
-}
-
-# Find all tests, excluding files that are test metadata such as group files.
-# It matches test names against $VALID_TEST_NAME defined in common/rc
-get_all_tests()
-{
-	touch $tmp.list
-	for d in $SRC_GROUPS $FSTYP; do
-		if ! test -d "$SRC_DIR/$d" ; then
-			continue
-		fi
-		ls $SRC_DIR/$d/* | \
-			grep -v "\..*" | \
-			grep "^$SRC_DIR/$d/$VALID_TEST_NAME"| \
-			grep -v "group\|Makefile" >> $tmp.list 2>/dev/null
-	done
-}
-
-# takes the list of tests to run in $tmp.list, and removes the tests passed to
-# the function from that list.
-trim_test_list()
-{
-	local test_list="$*"
-
-	rm -f $tmp.grep
-	local numsed=0
-	for t in $test_list
-	do
-	    if [ $numsed -gt 100 ]; then
-		grep -v -f $tmp.grep <$tmp.list >$tmp.tmp
-		mv $tmp.tmp $tmp.list
-		numsed=0
-		rm -f $tmp.grep
-	    fi
-	    echo "^$t\$" >>$tmp.grep
-	    numsed=`expr $numsed + 1`
-	done
-	grep -v -f $tmp.grep <$tmp.list >$tmp.tmp
-	mv $tmp.tmp $tmp.list
-	rm -f $tmp.grep
-}
-
 _timestamp()
 {
     local now=`date "+%T"`
     echo -n " [$now]"
 }
 
-_prepare_test_list()
-{
-	unset list
-	# Tests specified on the command line
-	if [ -s $tmp.arglist ]; then
-		cat $tmp.arglist > $tmp.list
-	else
-		touch $tmp.list
-	fi
-
-	# Specified groups to include
-	# Note that the CLI processing adds a leading space to the first group
-	# parameter, so we have to catch that here checking for "all"
-	if ! $have_test_arg && [ "$GROUP_LIST" == " all" ]; then
-		# no test numbers, do everything
-		get_all_tests
-	else
-		for group in $GROUP_LIST; do
-			list=$(get_group_list $group)
-			if [ -z "$list" ]; then
-				echo "Group \"$group\" is empty or not defined?"
-				exit 1
-			fi
-
-			for t in $list; do
-				grep -s "^$t\$" $tmp.list >/dev/null || \
-							echo "$t" >>$tmp.list
-			done
-		done
-	fi
-
-	# Specified groups to exclude
-	for xgroup in $XGROUP_LIST; do
-		list=$(get_group_list $xgroup)
-		if [ -z "$list" ]; then
-			echo "Group \"$xgroup\" is empty or not defined?"
-			continue
-		fi
-
-		trim_test_list $list
-	done
-
-	# sort the list of tests into numeric order unless we're running tests
-	# in the exact order specified
-	if ! $exact_order; then
-		if $randomize; then
-			if type shuf >& /dev/null; then
-				sorter="shuf"
-			else
-				sorter="awk -v seed=$RANDOM -f randomize.awk"
-			fi
-		else
-			sorter="cat"
-		fi
-		list=`sort -n $tmp.list | uniq | $sorter`
-	else
-		list=`cat $tmp.list`
-	fi
-	rm -f $tmp.list
-}
-
 # Process command arguments first.
 while [ $# -gt 0 ]; do
 	case "$1" in
@@ -285,48 +139,20 @@ while [ $# -gt 0 ]; do
 		export OVERLAY=true
 		;;
 
-	-g)	group=$2 ; shift ;
-		GROUP_LIST="$GROUP_LIST ${group//,/ }"
-		;;
+	-g)	_tl_setup_group $2 ; shift ;;
+	-e)	_tl_setup_exclude_tests $2 ; shift ;;
+	-E)	_tl_setup_exclude_file $2 ; shift ;;
+	-x)	_tl_setup_exclude_group $2; shift ;;
+	-X)	_tl_setup_exclude_subdir $2; shift ;;
+	-r)	_tl_setup_randomise ;;
+	--exact-order) _tl_setup_ordered ;;
 
-	-x)	xgroup=$2 ; shift ;
-		XGROUP_LIST="$XGROUP_LIST ${xgroup//,/ }"
-		;;
-
-	-X)	subdir_xfile=$2; shift ;
-		;;
-	-e)
-		xfile=$2; shift ;
-		readarray -t -O "${#exclude_tests[@]}" exclude_tests < \
-			<(echo "$xfile" | tr ', ' '\n\n')
-		;;
-
-	-E)	xfile=$2; shift ;
-		if [ -f $xfile ]; then
-			readarray -t -O ${#exclude_tests[@]} exclude_tests < \
-				<(sed "s/#.*$//" $xfile)
-		fi
-		;;
 	-s)	RUN_SECTION="$RUN_SECTION $2"; shift ;;
 	-S)	EXCLUDE_SECTION="$EXCLUDE_SECTION $2"; shift ;;
 	-l)	diff="diff" ;;
 	-udiff)	diff="$diff -u" ;;
 
 	-n)	showme=true ;;
-	-r)
-		if $exact_order; then
-			echo "Cannot specify -r and --exact-order."
-			exit 1
-		fi
-		randomize=true
-		;;
-	--exact-order)
-		if $randomize; then
-			echo "Cannnot specify --exact-order and -r."
-			exit 1
-		fi
-		exact_order=true
-		;;
 	-i)	iterations=$2; shift ;;
 	-I) 	iterations=$2; istop=true; shift ;;
 	-T)	timestamp=true ;;
@@ -344,13 +170,13 @@ while [ $# -gt 0 ]; do
 
 	-*)	usage ;;
 	*)	# not an argument, we've got tests now.
-		have_test_arg=true ;;
+		_tl_setup_cli $*
 	esac
 
 	# if we've found a test specification, the break out of the processing
 	# loop before we shift the arguments so that this is the first argument
 	# that we process in the test arg loop below.
-	if $have_test_arg; then
+	if $_tl_have_test_args; then
 		break;
 	fi
 
@@ -388,51 +214,6 @@ if [ -n "$FUZZ_REWRITE_DURATION" ]; then
 	fi
 fi
 
-if [ -n "$subdir_xfile" ]; then
-	for d in $SRC_GROUPS $FSTYP; do
-		[ -f $SRC_DIR/$d/$subdir_xfile ] || continue
-		for f in `sed "s/#.*$//" $SRC_DIR/$d/$subdir_xfile`; do
-			exclude_tests+=($d/$f)
-		done
-	done
-fi
-
-# Process tests from command line now.
-if $have_test_arg; then
-	while [ $# -gt 0 ]; do
-		case "$1" in
-		-*)	echo "Arguments before tests, please!"
-			status=1
-			exit $status
-			;;
-		*)	# Expand test pattern (e.g. xfs/???, *fs/001)
-			list=$(cd $SRC_DIR; echo $1)
-			for t in $list; do
-				t=${t#$SRC_DIR/}
-				test_dir=${t%%/*}
-				test_name=${t##*/}
-				group_file=$SRC_DIR/$test_dir/group.list
-
-				if grep -Eq "^$test_name" $group_file; then
-					# in group file ... OK
-					echo $SRC_DIR/$test_dir/$test_name \
-						>>$tmp.arglist
-				else
-					# oops
-					echo "$t - unknown test, ignored"
-				fi
-			done
-			;;
-		esac
-
-		shift
-	done
-elif [ -z "$GROUP_LIST" ]; then
-	# default group list is the auto group. If any other group or test is
-	# specified, we use that instead.
-	GROUP_LIST="auto"
-fi
-
 if [ `id -u` -ne 0 ]
 then
     echo "check: QA must be run as root"
@@ -593,21 +374,6 @@ _check_filesystems()
 	return $ret
 }
 
-_expunge_test()
-{
-	local TEST_ID="$1"
-
-	for f in "${exclude_tests[@]}"; do
-		# $f may contain traling spaces and comments
-		local id_regex="^${TEST_ID}\b"
-		if [[ "$f" =~ ${id_regex} ]]; then
-			echo "       [expunged]"
-			return 0
-		fi
-	done
-	return 1
-}
-
 # retain files which would be overwritten in subsequent reruns of the same test
 _stash_fail_loop_files() {
 	local seq_prefix="${REPORT_DIR}/${1}"
@@ -714,7 +480,7 @@ _run_seq() {
 }
 
 _detect_kmemleak
-_prepare_test_list
+_tl_prepare_test_list
 fstests_start_time="$(date +"%F %T")"
 
 if $OPTIONS_HAVE_SECTIONS; then
@@ -794,7 +560,7 @@ function run_section()
 		# TEST_DEV could be changed, source common/rc again with
 		# correct FSTYP to get FSTYP specific configs, e.g. common/xfs
 		. common/rc
-		_prepare_test_list
+		_tl_prepare_test_list
 	elif [ "$OLD_TEST_FS_MOUNT_OPTS" != "$TEST_FS_MOUNT_OPTS" ]; then
 		_test_unmount 2> /dev/null
 		if ! _test_mount
@@ -859,7 +625,7 @@ function run_section()
 
 	loop_status=()	# track rerun-on-failure state
 	local tc_status ix
-	local -a _list=( $list )
+	local -a _list=( $_tl_tests )
 	for ((ix = 0; ix < ${#_list[*]}; !${#loop_status[*]} && ix++)); do
 		seq="${_list[$ix]}"
 
@@ -880,7 +646,7 @@ function run_section()
 
 		# the filename for the test and the name output are different.
 		# we don't include the tests/ directory in the name output.
-		export seqnum=${seq#$SRC_DIR/}
+		export seqnum=$(_tl_strip_src_dir $seq)
 		group=${seqnum%%/*}
 		if $OPTIONS_HAVE_SECTIONS; then
 			REPORT_DIR="$RESULT_BASE/$section"
@@ -902,7 +668,7 @@ function run_section()
 		echo -n "$seqnum"
 
 		if $showme; then
-			if _expunge_test $seqnum; then
+			if _tl_expunge_test $seqnum; then
 				tc_status="expunge"
 			else
 				echo
@@ -928,7 +694,7 @@ function run_section()
 		rm -f $seqres.out.bad $seqres.hints
 
 		# check if we really should run it
-		if _expunge_test $seqnum; then
+		if _tl_expunge_test $seqnum; then
 			tc_status="expunge"
 			_stash_test_status "$seqnum" "$tc_status"
 			continue
diff --git a/common/report b/common/report
index 7128bbeba..5697d2540 100644
--- a/common/report
+++ b/common/report
@@ -196,7 +196,7 @@ _xunit_make_testcase_report()
 		echo -e "\t\t<skipped/>" >> $report
 		;;
 	"fail")
-		local out_src="${SRC_DIR}/${test_name}.out"
+		local out_src="${_tl_src_dir}/${test_name}.out"
 		local full_file="${REPORT_DIR}/${test_name}.full"
 		local dmesg_file="${REPORT_DIR}/${test_name}.dmesg"
 		local outbad_file="${REPORT_DIR}/${test_name}.out.bad"
diff --git a/common/test_list b/common/test_list
new file mode 100644
index 000000000..2432be6f7
--- /dev/null
+++ b/common/test_list
@@ -0,0 +1,295 @@
+##/bin/bash
+# SPDX-License-Identifier: GPL-2.0+
+# Copyright (c) 2000-2002,2006 Silicon Graphics, Inc.  All Rights Reserved.
+# Copyright (c) 2024 Red Hat, Inc.  All Rights Reserved.
+#
+# Test list parsing and building functions
+#
+# Note: this file must stand alone and not be dependent on any other includes,
+# most especially common/rc and common/config. This is because we have to
+# include this file before option parsing, whilst the rc/config includes need to
+# be included -after- option parsing.
+#
+# Any function or variable that is public should have a "_tl_" prefix.
+
+export _tl_src_dir="tests"
+
+_SRC_GROUPS="generic"
+_GROUP_LIST=
+_XGROUP_LIST=
+_tl_exact_order=false
+_tl_randomise=false
+_tl_have_test_args=false
+_tl_file="$tmp.test_list"
+_tl_exclude_tests=()
+_tl_tests=
+
+_tl_strip_src_dir()
+{
+	local test="$1"
+
+	echo ${test#$_tl_src_dir/}
+}
+
+get_sub_group_list()
+{
+	local d=$1
+	local grp=$2
+
+	test -s "$_tl_src_dir/$d/group.list" || return 1
+
+	local grpl=$(sed -n < $_tl_src_dir/$d/group.list \
+		-e 's/#.*//' \
+		-e 's/$/ /' \
+		-e "s;^\($VALID_TEST_NAME\).* $grp .*;$_tl_src_dir/$d/\1;p")
+	echo $grpl
+}
+
+get_group_list()
+{
+	local grp=$1
+	local grpl=""
+	local sub=$(dirname $grp)
+	local fsgroup="$FSTYP"
+
+	if [ -n "$sub" -a "$sub" != "." -a -d "$_tl_src_dir/$sub" ]; then
+		# group is given as <subdir>/<group> (e.g. xfs/quick)
+		grp=$(basename $grp)
+		get_sub_group_list $sub $grp
+		return
+	fi
+
+	if [ "$FSTYP" = ext2 -o "$FSTYP" = ext3 ]; then
+	    fsgroup=ext4
+	fi
+	for d in $_SRC_GROUPS $fsgroup; do
+		if ! test -d "$_tl_src_dir/$d" ; then
+			continue
+		fi
+		grpl="$grpl $(get_sub_group_list $d $grp)"
+	done
+	echo $grpl
+}
+
+# Find all tests, excluding files that are test metadata such as group files.
+# It matches test names against $VALID_TEST_NAME defined in common/rc
+get_all_tests()
+{
+	touch $tmp.list
+	for d in $_SRC_GROUPS $FSTYP; do
+		if ! test -d "$_tl_src_dir/$d" ; then
+			continue
+		fi
+		ls $_tl_src_dir/$d/* | \
+			grep -v "\..*" | \
+			grep "^$_tl_src_dir/$d/$VALID_TEST_NAME"| \
+			grep -v "group\|Makefile" >> $tmp.list 2>/dev/null
+	done
+}
+
+# takes the list of tests to run in $tmp.list, and removes the tests passed to
+# the function from that list.
+trim_test_list()
+{
+	local test_list="$*"
+
+	rm -f $tmp.grep
+	local numsed=0
+	for t in $test_list
+	do
+	    if [ $numsed -gt 100 ]; then
+		grep -v -f $tmp.grep <$tmp.list >$tmp.tmp
+		mv $tmp.tmp $tmp.list
+		numsed=0
+		rm -f $tmp.grep
+	    fi
+	    echo "^$t\$" >>$tmp.grep
+	    numsed=`expr $numsed + 1`
+	done
+	grep -v -f $tmp.grep <$tmp.list >$tmp.tmp
+	mv $tmp.tmp $tmp.list
+	rm -f $tmp.grep
+}
+
+_tl_prepare_test_list()
+{
+	unset _tl_tests
+	# Tests specified on the command line
+	if [ -s $_tl_file ]; then
+		cat $_tl_file > $tmp.list
+	else
+		touch $tmp.list
+	fi
+
+	# Specified groups to include
+	# Note that the CLI processing adds a leading space to the first group
+	# parameter, so we have to catch that here checking for "all"
+	if ! $_tl_have_test_args && [ "$_GROUP_LIST" == " all" ]; then
+		# no test numbers, do everything
+		get_all_tests
+	else
+		for group in $_GROUP_LIST; do
+			list=$(get_group_list $group)
+			if [ -z "$list" ]; then
+				echo "Group \"$group\" is empty or not defined?"
+				exit 1
+			fi
+
+			for t in $list; do
+				grep -s "^$t\$" $tmp.list >/dev/null || \
+							echo "$t" >>$tmp.list
+			done
+		done
+	fi
+
+	# Specified groups to exclude
+	for xgroup in $_XGROUP_LIST; do
+		list=$(get_group_list $xgroup)
+		if [ -z "$list" ]; then
+			echo "Group \"$xgroup\" is empty or not defined?"
+			continue
+		fi
+
+		trim_test_list $list
+	done
+
+	# sort the list of tests into numeric order unless we're running tests
+	# in the exact order specified
+	if ! $_tl_exact_order; then
+		if $_tl_randomise; then
+			if type shuf >& /dev/null; then
+				sorter="shuf"
+			else
+				sorter="awk -v seed=$RANDOM -f randomize.awk"
+			fi
+		else
+			sorter="cat"
+		fi
+		_tl_tests=`sort -n $tmp.list | uniq | $sorter`
+	else
+		_tl_tests=`cat $tmp.list`
+	fi
+	rm -f $tmp.list
+}
+
+_tl_expunge_test()
+{
+	local TEST_ID="$1"
+
+	for f in "${_tl_exclude_tests[@]}"; do
+		# $f may contain traling spaces and comments
+		local id_regex="^${TEST_ID}\b"
+		if [[ "$f" =~ ${id_regex} ]]; then
+			echo "       [expunged]"
+			return 0
+		fi
+	done
+	return 1
+}
+
+_tl_setup_exclude_tests()
+{
+	local list="$1"
+
+	readarray -t -O "${#_tl_exclude_tests[@]}" _tl_exclude_tests < \
+		<(echo "$list" | tr ', ' '\n\n')
+}
+
+_tl_setup_exclude_file()
+{
+	local xfile="$1"
+
+	if [ -f $xfile ]; then
+		readarray -t -O ${#_tl_exclude_tests[@]} _tl_exclude_tests < \
+			<(sed "s/#.*$//" $xfile)
+	fi
+}
+
+_tl_setup_exclude_subdir()
+{
+	local xfile="$1"
+	local d
+	local f
+
+	[ -z "$xfile" ] && return
+
+	for d in $_SRC_GROUPS $FSTYP; do
+		[ -f $_tl_src_dir/$d/$xfile ] || continue
+		for f in `sed "s/#.*$//" $_tl_src_dir/$d/$xfile`; do
+			_tl_exclude_tests+=($d/$f)
+		done
+	done
+}
+
+_tl_setup_exclude_group()
+{
+	local xgroup="$1"
+
+	_XGROUP_LIST="$_XGROUP_LIST ${xgroup//,/ }"
+}
+
+_tl_setup_group()
+{
+	local group="$1"
+
+	_GROUP_LIST="$_GROUP_LIST ${group//,/ }"
+}
+
+_tl_setup_randomise()
+{
+	if $_tl_exact_order; then
+		echo "Cannot specify -r and --exact-order."
+		exit 1
+	fi
+	_tl_randomise=true
+}
+
+_tl_setup_ordered()
+{
+	if $_tl_randomise; then
+		echo "Cannnot specify --exact-order and -r."
+		exit 1
+	fi
+	_tl_exact_order=true
+}
+
+_tl_setup_cli()
+{
+	while [ $# -gt 0 ]; do
+		case "$1" in
+		-*)	echo "Arguments before tests, please!"
+			status=1
+			exit $status
+			;;
+		*)	# Expand test pattern (e.g. xfs/???, *fs/001)
+			local list=$(cd $_tl_src_dir; echo $1)
+			local t
+
+			for t in $list; do
+				t=${t#$_tl_src_dir/}
+				local test_dir=${t%%/*}
+				local test_name=${t##*/}
+				local group_file=$_tl_src_dir/$test_dir/group.list
+
+				if grep -Eq "^$test_name" $group_file; then
+					# in group file ... OK
+					echo $_tl_src_dir/$test_dir/$test_name \
+						>> $_tl_file
+					_tl_have_test_args=true
+				else
+					# oops
+					echo "$t - unknown test, ignored"
+				fi
+			done
+			;;
+		esac
+
+		shift
+	done
+
+	if ! $_tl_have_test_args && [ -z "$_GROUP_LIST" ]; then
+		# default group list is the auto group. If any other group or
+		# test is specified, we use that instead.
+		_GROUP_LIST="auto"
+	fi
+}
