diff --git a/README.rst b/README.rst
index 443ea81c..88f621df 100644
--- a/README.rst
+++ b/README.rst
@@ -184,6 +184,7 @@ These perl packages include:
 - Devel::StackTrace
 - Digest::MD5
 - File::Spec
+- GD
 - at least one flavor of JSON module. In order of performance/preference:
 
   - JSON::XS
@@ -213,13 +214,23 @@ To execute ``make install ...`` or ``make doc``, you will need:
 Some of the applications provided with the lcov module are written
 in Python - and may require additional Python packages.
 In particular, 'xlsxwriter' is required in order to generate any
-of the spreadsheet reports, and 'xml' and 'zipfile' are required in
+of the spreadsheet reports, and Python packages 'xml' and 'zipfile' are
+required in
 order to be able to run regression tests.
 
 To measure Python code coverage, users will need Python packages:
 
 - Coverage.py
-  
+
+Certain tests use additional tools:
+
+- ``jq`` - the command-line json parser - is required by some tests.
+  ``make check`` fail if it is not found.
+- ``py2lcov`` tests use Coverage.py; these tests will be skipped if
+  the tool is not found in your environment.
+- some LLVM-related tests (*e.g.*, the ``llvm2lcov`` tests) require
+  ``clang`` and/or ``clang++``.  If these are not present in your
+  environment, then the LLVM-related tests are skipped.
 
 In addition, contributors will need:
 
diff --git a/bin/lcov b/bin/lcov
index 4b0fe9c6..c8c53a1a 100755
--- a/bin/lcov
+++ b/bin/lcov
@@ -136,6 +136,7 @@ sub setup_gkv();
 sub lcov_geninfo(@);
 sub create_package($$$;$);
 sub summary();
+sub count_data_cb($$$);
 
 # Global variables & initialization
 
@@ -1039,6 +1040,22 @@ sub read_file($)
     return $content;
 }
 
+#
+# count_data_cb(dir, relative_name, count_ref)
+#
+# Count the data files found while searching an unpacked package.  Returns
+# undefined:  a defined value would stop 'lcov_find' at the first match.
+#
+
+sub count_data_cb($$$)
+{
+    my ($dir, $rel, $count_ref) = @_;
+
+    $$count_ref++;
+
+    return;
+}
+
 #
 # get_package(package_file)
 #
@@ -1060,16 +1077,16 @@ sub get_package($)
     info("Reading package $file:\n");
     $file = abs_path($file);
     chdir($dir);
-    open(HANDLE, "-|", "tar xvfz " . quotemeta($file) . " 2>$lcovutil::devnull")
+    # Extract quietly:  the format of the verbose listing depends on which
+    #   tar implementation is installed (GNU tar prints just the path, while
+    #   bsdtar appends the file size to each line), so the data files are
+    #   counted in the expanded directory rather than from the tar listing.
+    open(HANDLE, "-|", "tar xfz " . quotemeta($file) . " 2>$lcovutil::devnull")
         or
         die("could not process package $file: $!\n");
-    $count = 0;
-    while (<HANDLE>) {
-        if (/\.da$/ || /\.gcda$/) {
-            $count++;
-        }
-    }
     close(HANDLE);
+    $count = 0;
+    lcov_find($dir, \&count_data_cb, \$count, '\.gcda$', '\.da$');
     if ($count == 0) {
         die("no data file found in package $file\n");
     }
@@ -1126,7 +1143,8 @@ sub count_package_data($)
 
     open(HANDLE, "-|", "tar tfz " . quotemeta($filename)) or return undef;
     while (<HANDLE>) {
-        if (/\.da$/ || /\.gcda$/) {
+        # tolerate 'bsdtar', which appends the file size to each listing line
+        if (/\.(?:gcda|da)(?:\s|\z)/) {
             $count++;
         }
     }
diff --git a/tests/common.tst b/tests/common.tst
index 53501dc5..40546745 100644
--- a/tests/common.tst
+++ b/tests/common.tst
@@ -8,6 +8,46 @@ PROFILE="--profile"
 LOCAL_COVERAGE=1
 KEEP_GOING=0
 
+# ---------------------------------------------------------------------------
+# Portable helpers
+# ---------------------------------------------------------------------------
+
+# BSD 'wc -l' left-pads its output, so a literal string comparison against an
+# expected count fails even when the counts are numerically equal.  Always
+# compare '$(count_lines FILE)' - awk prints a clean number everywhere.
+# BSD/macOS has 'md5' (stdout) but not the GNU 'md5sum' that was forked in
+# legacy scripts; compute the identical lowercase hex digest in-process using
+# perl Digest::MD5 so tests agree with the tools on every platform.
+compute_md5_hex()
+{
+    perl -MDigest::MD5 -e '
+        $SIG{PIPE} = q{IGNORE};
+        my $name = @ARGV && $ARGV[0] ne q{-} ? $ARGV[0] : q{-};
+        binmode STDIN;
+        my $self = Digest::MD5->new;
+        $self->add(do { local $/; my $fh = $name eq q{-} ? *STDIN : do { open my $h, q{<}, $name or die qq{md5: $name: $!}; $h }; <$fh> });
+        print $self->hexdigest, chr(10);
+    ' "$@"
+}
+
+count_lines()
+{
+    awk 'END { print NR }' "$@"
+}
+
+# Prints the best way to invoke the python Coverage.py module - empty when
+# the package isn't installed (callers should then SKIP, not fail).
+find_python_coverage()
+{
+    if command -v coverage >/dev/null 2> /dev/null ; then
+        echo "coverage"
+    elif command -v python3-coverage >/dev/null 2> /dev/null ; then
+        echo "python3-coverage"
+    elif python3 -m coverage --version >/dev/null 2> /dev/null ; then
+        echo "python3 -m coverage"
+    fi
+}
+
 #echo "CMD:  $0 $@"
 
 while [ $# -gt 0 ] ; do
@@ -68,14 +108,20 @@ while [ $# -gt 0 ] ; do
                     CMD='python3-coverage' # ubuntu?
                 fi
             fi
-            which $CMD
-            if [ 0 != $? ] ; then
-                echo "cannot find 'coverage' or 'python3-coverage'"
-                echo "unable to run py2lcov - please install python Coverage.py package"
-                exit 1
+            if ! command -v "$CMD" > /dev/null 2> /dev/null ; then
+                if [ "python3 -m coverage" != "$CMD" ] &&
+                   ! python3 -m coverage --version >/dev/null 2> /dev/null ; then
+                    # Not a bug in the tools - run without coverage
+                    #   instrumentation (see runtests.py: a missing Coverage.py
+                    #   also counts as a platform skip there).
+                    echo "python Coverage.py package not installed - running without runtime instrumentation"
+                    CMD=''
+                fi
             fi
 
-            PYCOVER="COVERAGE_FILE=$PYCOV_DB $CMD run --branch --append"
+            if [ '' != "$CMD" ] ; then
+                PYCOVER="COVERAGE_FILE=$PYCOV_DB $CMD run --branch --append"
+            fi
             ;;
 
         --home | -home )
@@ -266,7 +312,7 @@ function check_tla_css()
     local rc=0
     local class
     for class in `find $dir -name '*.html' -print0 |
-                  xargs -0 --no-run-if-empty grep -h -o -E 'class="[^"]+"' |
+                  xargs -0 -r grep -h -o -E 'class="[^"]+"' |
                   sed -e 's/class="//' -e 's/"$//' | tr ' ' '\n' |
                   grep -E '^tla(Bg)?[A-Z]' | sort -u` ; do
         if ! grep -E -q "(^|[[:space:]])(td|span|a)\.$class([^A-Za-z0-9]|\$)" \
diff --git a/tests/genhtml/htmlgen/htmlgen.sh b/tests/genhtml/htmlgen/htmlgen.sh
index f7ca4557..6804fb5b 100755
--- a/tests/genhtml/htmlgen/htmlgen.sh
+++ b/tests/genhtml/htmlgen/htmlgen.sh
@@ -430,8 +430,8 @@ for mode in plain nav ; do
     done
     # the source text has no brackets in it, so every '[' in the page opened a
     #   coverpoint group and every ']' closed one
-    OPEN=`grep -o '\[' $PAGE | wc -l`
-    CLOSE=`grep -o '\]' $PAGE | wc -l`
+    OPEN=`grep -o '\[' $PAGE | grep -c ''`
+    CLOSE=`grep -o '\]' $PAGE | grep -c ''`
     if [ "$OPEN" != "$CLOSE" ] ; then
         fail unreach_$mode "$OPEN '[' but $CLOSE ']' in the source page"
     fi
@@ -448,7 +448,7 @@ for mode in plain nav ; do
     #   matches and not lines - and the page is found first, because backticks
     #   inside backticks do not nest
     PAGE=`find html_$mode -name 't.c.gcov.html'`
-    COUNT=`grep -o -F -e 'tlaEUC' $PAGE | wc -l`
+    COUNT=`grep -o -F -e 'tlaEUC' $PAGE | grep -c ''`
     eval "COUNT_$mode=$COUNT"
 done
 if [ "$COUNT_plain" != "$COUNT_nav" ] ; then
@@ -817,7 +817,7 @@ for mode in nfb nfb_off nfb_noann nfb_olderbase ; do
     # the MC/DC coverpoints are on the page at all:  four markers on line 5,
     #   which are dropped entirely if the categorizer walks only the baseline.
     #   All four are on one line, so this counts matches and not lines
-    COUNT=`grep -o -F -e 'class="mcdcTla"' $PAGE | wc -l`
+    COUNT=`grep -o -F -e 'class="mcdcTla"' $PAGE | grep -c ''`
     if [ 4 != "$COUNT" ] ; then
         fail $mode "expected 4 MC/DC markers in the $mode page, found $COUNT"
     fi
diff --git a/tests/genpng/genpng.sh b/tests/genpng/genpng.sh
index 13a7fd2f..ecc73351 100755
--- a/tests/genpng/genpng.sh
+++ b/tests/genpng/genpng.sh
@@ -183,7 +183,7 @@ elif [ -s genpng2.log ] ; then
 else
     dump hand.png > hand.dump
     # nine source lines, and no row for either of the two metadata lines
-    ROWS=`wc -l < hand.dump`
+    ROWS=`count_lines hand.dump`
     if [ "$ROWS" != 9 ] ; then
         fail "Test 2 layout: expected 9 rows, got $ROWS: `cat hand.dump`"
     else
@@ -230,7 +230,7 @@ if [ 0 != ${PIPESTATUS[0]} ] ; then
     fail "Test 3 plain text: genpng failed"
 else
     dump plain.png > plain.dump
-    if [ "`wc -l < plain.dump`" != 3 ] ; then
+    if [ "`count_lines plain.dump`" != 3 ] ; then
         fail "Test 3 plain text: expected 3 rows: `cat plain.dump`"
     elif [ "`grep -c \" $PLAIN \" plain.dump`" != 3 ] ; then
         fail "Test 3 plain text: not every row is plain: `cat plain.dump`"
diff --git a/tests/lcov/extract/extract1.sh b/tests/lcov/extract/extract1.sh
index c7f2881c..55591436 100755
--- a/tests/lcov/extract/extract1.sh
+++ b/tests/lcov/extract/extract1.sh
@@ -181,7 +181,13 @@ if [ 0 != $? ] ; then
 fi
 
 # exclude some un-covered standard includes
-EXCLUDE='--exclude */include/c++/* --exclude */include/g++-v* --ignore unused'
+# Depending on platform, the compiler's private headers live under
+# '/usr/include/c++/N' (Linux), '/usr/include/g++/bits' or
+# '/usr/pkg/include/gcc/N' - exclude every spelling, tolerating the unused
+# ones.
+EXCLUDE='--exclude */include/c++/* --exclude */include/g++-v*
+        --exclude */include/g++/* --exclude */include/gcc/*
+        --exclude */include/gcc[0-9]*/* --ignore unused'
 
 # callback tests
 echo $COVER $CAPTURE . $LCOV_OPTS -o callback.info $FILTER $IGNORE --criteria $SCRIPT_DIR/threshold.pm,--line,90,--branch,65,--function,100 $EXCLUDE
diff --git a/tests/lcov/extract/extract2.sh b/tests/lcov/extract/extract2.sh
index a9aac16b..9ff05813 100755
--- a/tests/lcov/extract/extract2.sh
+++ b/tests/lcov/extract/extract2.sh
@@ -761,7 +761,7 @@ fi
 
 # test filename containing spaces
 rm -rf ./mytest
-mkdir -pv ./mytest
+mkdir -p ./mytest
 echo "int main(){}" > './mytest/main space.cpp'
 ( cd ./mytest ; ${CXX} -c  'main space.cpp' --coverage )
 
diff --git a/tests/lcov/fork_throttle/fork_throttle.pl b/tests/lcov/fork_throttle/fork_throttle.pl
index bf5a2d8c..969ac82c 100755
--- a/tests/lcov/fork_throttle/fork_throttle.pl
+++ b/tests/lcov/fork_throttle/fork_throttle.pl
@@ -28,6 +28,22 @@
 
 lcovutil::parseOptions({}, {});
 
+# The throttle's whole subject is a memory governor:  it learns a byte-per-
+#   unit rate from a child's peak RSS, which is only measurable where the OS
+#   reports one - the same fact which makes lcovutil emit no per-job
+#   'memory' profile data at all on platforms which don't expose it (their
+#   read_proc_peak_memory returns zeroes).  Without that signal every check
+#   below is moot - and worse, the 'learnedBytes / learnedWeight' rate
+#   divides by a never-learned weight, which is the crash this guard exists
+#   to prevent - so skip rather than fail.
+{
+    my ($rss, $vsize) = lcovutil::read_proc_peak_memory();
+    if (!$rss && !$vsize) {
+        print("skipped: platform does not report peak memory\n");
+        exit(0);
+    }
+}
+
 my $tempdir = 'fork_throttle.tmp';
 rmtree($tempdir) if -d $tempdir;
 mkdir($tempdir) or die("cannot create $tempdir: $!");
diff --git a/tests/lcov/parallel_parse/parallel_parse.sh b/tests/lcov/parallel_parse/parallel_parse.sh
index 0193dcba..7edb2a65 100755
--- a/tests/lcov/parallel_parse/parallel_parse.sh
+++ b/tests/lcov/parallel_parse/parallel_parse.sh
@@ -372,7 +372,7 @@ fi
 # 9. truncated final section:  the same format error as a serial read, with
 #    the same line numbers
 #-----------------------------------------------------------------------
-head -n -1 multi.info > trunc.info
+sed '$ d' multi.info > trunc.info
 run_pair trunc trunc.info --ignore-errors corrupt,format
 for f in trunc_serial.log trunc_split.log ; do
     if ! grep -E "unexpected end of file: missing 'end_of_record'" $f \
diff --git a/tests/lcov/scheduling/scheduling.sh b/tests/lcov/scheduling/scheduling.sh
index 18b8ff2a..393dad22 100755
--- a/tests/lcov/scheduling/scheduling.sh
+++ b/tests/lcov/scheduling/scheduling.sh
@@ -323,16 +323,22 @@ check_memory agg_prof.json filter filt_child .filt_child
 check_memory agg_prof.json aggregate 'group total' \
     '(with_entries(select(.key | test("^[0-9]+$"))))'
 
-# Both phases must be present simultaneously in that one profile:  under the
-# earlier pid keying they were indistinguishable, and under a flat numeric
-# keying they would have overwritten each other.
-for phase in filter aggregate ; do
-    n=`jq -r --arg p "$phase" '[.memory | keys[]
-           | select(startswith($p + "_"))] | length' agg_prof.json`
-    if [ "${n:-0}" -lt 1 ] ; then
-        fail_memory $phase "no $phase worker in agg_prof.json"
-    fi
-done
+if [ "`jq -r 'has("memory")' agg_prof.json`" != "true" ] ; then
+    echo "memory data absent in agg_prof.json (platform does not expose peak memory)"
+else
+    {
+    # Both phases must be present simultaneously in that one profile:  under the
+    # earlier pid keying they were indistinguishable, and under a flat numeric
+    # keying they would have overwritten each other.
+    for phase in filter aggregate ; do
+        n=`jq -r --arg p "$phase" '[.memory | keys[]
+               | select(startswith($p + "_"))] | length' agg_prof.json`
+        if [ "${n:-0}" -lt 1 ] ; then
+            fail_memory $phase "no $phase worker in agg_prof.json"
+        fi
+    done
+    }
+fi
 # ...and a collision would have been reported by merge_child_profile
 if grep -i 'unexpected duplicate key' agg.log geninfo_hist.log genhtml_hist.log ; then
     fail_memory collision \
@@ -364,13 +370,19 @@ if grep -i 'unexpected duplicate key' nested.log ; then
 fi
 # the qualified filter workers, and the check that memory{filter_<id>} still
 # lines up with filt_child{<id>} for them
-nested=`jq -r '[.memory | keys[] | select(test("^filter_aggregate_"))]
-               | sort | join(" ")' nested_prof.json`
-if [ "`echo $nested | wc -w`" -lt 2 ] ; then
-    jq -r '.memory | keys' nested_prof.json
-    fail_memory nested "no nested filter worker in nested_prof.json"
+if [ "`jq -r 'has("memory")' nested_prof.json`" != "true" ] ; then
+    echo "memory data absent in nested_prof.json (platform does not expose peak memory)"
 else
-    echo "OK (memory nested): [$nested]"
+    {
+    nested=`jq -r '[.memory | keys[] | select(test("^filter_aggregate_"))]
+                   | sort | join(" ")' nested_prof.json`
+    if [ "`echo $nested | wc -w`" -lt 2 ] ; then
+        jq -r '.memory | keys' nested_prof.json
+        fail_memory nested "no nested filter worker in nested_prof.json"
+    else
+        echo "OK (memory nested): [$nested]"
+    fi
+    }
 fi
 check_memory nested_prof.json filter filt_child .filt_child
 
diff --git a/tests/py2lcov/py2lcov.sh b/tests/py2lcov/py2lcov.sh
index 9628b903..d36e887d 100755
--- a/tests/py2lcov/py2lcov.sh
+++ b/tests/py2lcov/py2lcov.sh
@@ -51,17 +51,14 @@ LCOV_OPTS="--branch-coverage $PARALLEL $PROFILE"
 if [ '' != "${COVERAGE_COMMAND}" ] ; then
     CMD=${COVERAGE_COMMAND}
 else
-    CMD='coverage'
-    which $CMD
-    if [ 0 != $? ] ; then
-        CMD='python3-coverage' # ubuntu?
-    fi
+    CMD=$(find_python_coverage)
 fi
-which $CMD
-if [ 0 != $? ] ; then
-    echo "cannot find 'coverage' or 'python3-coverage'"
-    echo "unable to run py2lcov - please install python Coverage.py package"
-    exit 1
+if [ '' == "$CMD" ] ; then
+    # The python Coverage.py package is not installed on this platform:
+    #   py2lcov itself is not implicated - skip instead of failing
+    #   (same treatment as other platform-constraint skips in this suite).
+    echo "python Coverage.py package not installed - skipping py2lcov test"
+    exit 0
 fi
 
 # some corner cases:
diff --git a/tests/scripts/batchgitversion_test.sh b/tests/scripts/batchgitversion_test.sh
index 0111c999..8f5c31ff 100755
--- a/tests/scripts/batchgitversion_test.sh
+++ b/tests/scripts/batchgitversion_test.sh
@@ -55,6 +55,8 @@ if [[ "x" == "${LCOV_HOME}x" ]] ; then
 fi
 source ../common.tst
 
+rm -f batch_git_*.pl
+
 if [ -z "$SCRIPT_DIR" ] ; then
     echo "SCRIPT_DIR not set" >&2
     exit 1
@@ -107,6 +109,10 @@ run_pl() {
     local pl_file="$1"; shift
     OUTPUT=$($PERL -I"$SCRIPT_DIR" "$pl_file" "$@" 2>&1)
     RC=$?
+    # Devel::Cover status lines can leak into the captured output even with
+    # -silent (its 'Deleting old coverage for changed file' message is
+    # printed unconditionally), so they must not reach the comparisons.
+    OUTPUT=$(sed '/^Devel::Cover:/d' <<<"$OUTPUT")
 }
 
 # ---------------------------------------------------------------------------
@@ -136,7 +142,7 @@ ENDSCRIPT
 # ===========================================================================
 read -r REPODIR FOO_SHA <<< "$(make_repo)"
 MD5_FOO=$(md5sum "$REPODIR/foo.c" | awk '{print $1}')
-trap 'rm -rf "$REPODIR"' EXIT
+trap 'rm -rf "$REPODIR"; rm -f batch_git_*.pl' EXIT
 
 # ===========================================================================
 # Tests 1-2: standalone invocation via call_get_version
@@ -179,7 +185,8 @@ fi
 # ===========================================================================
 
 # Test 4: bad option to new() returns undef
-PL=$(mktemp --suffix=.pl)
+TESTIDX=4
+PL="batch_git_${TESTIDX}.pl"
 cat > "$PL" << 'PLEOF'
 use batchGitVersion;
 my $obj = batchGitVersion->new('/fake/script', '--bad-option');
@@ -194,7 +201,7 @@ fi
 rm -f "$PL"
 
 # Test 5: --help to new() returns undef
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << 'PLEOF'
 use batchGitVersion;
 my $obj = batchGitVersion->new('/fake/script', '--help');
@@ -213,7 +220,7 @@ rm -f "$PL"
 # ===========================================================================
 
 # Test 6: file found via prefix stripping -> "BLOB <sha>"
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$sha) = @ARGV;
@@ -232,7 +239,7 @@ fi
 rm -f "$PL"
 
 # Test 7: file found directly in DB (no prefix needed) -> "BLOB <sha>"
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$sha) = @ARGV;
@@ -251,7 +258,7 @@ fi
 rm -f "$PL"
 
 # Test 8: file not in DB, exists on disk -> mtime (ISO8601)
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -273,7 +280,7 @@ fi
 rm -f "$PL"
 
 # Test 9: file not in DB, exists on disk, --md5 -> mtime + " md5:<hash>"
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -295,7 +302,7 @@ fi
 rm -f "$PL"
 
 # Test 10: file not in DB, not on disk, --allow-missing -> empty string
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -314,7 +321,7 @@ fi
 rm -f "$PL"
 
 # Test 11: file not in DB, not on disk, no --allow-missing -> dies
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -337,7 +344,7 @@ rm -f "$PL"
 # ===========================================================================
 
 # Test 12: --prepend path prepends to all DB keys; lookup via prepend/file
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$sha) = @ARGV;
@@ -360,7 +367,7 @@ rm -f "$PL"
 # ===========================================================================
 
 # Test 13: --token SHA uses SHA instead of BLOB as the token prefix
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$sha) = @ARGV;
@@ -383,7 +390,7 @@ rm -f "$PL"
 # ===========================================================================
 
 # Test 14: without --repo, new() falls back to getcwd(); chdir to repo first
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 use Cwd qw(chdir);
@@ -408,7 +415,7 @@ rm -f "$PL"
 # ===========================================================================
 
 # Test 15: compare_version same BLOB strings -> 0 (false)
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -425,7 +432,7 @@ fi
 rm -f "$PL"
 
 # Test 16: compare_version different BLOB strings -> 1 (true)
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -442,7 +449,7 @@ fi
 rm -f "$PL"
 
 # Test 17: compare_version --md5, old has md5 (no BLOB prefix), both same -> 0
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$md5) = @ARGV;
@@ -460,7 +467,7 @@ fi
 rm -f "$PL"
 
 # Test 18: compare_version --md5, old has md5, new has md5, differ -> 1
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$md5) = @ARGV;
@@ -479,7 +486,7 @@ fi
 rm -f "$PL"
 
 # Test 19: compare_version --md5, old starts BLOB -> bypass md5 branch, exact match
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -497,7 +504,7 @@ fi
 rm -f "$PL"
 
 # Test 20: compare_version --md5, old has md5 but new has none -> fall through, 1
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$md5) = @ARGV;
@@ -520,7 +527,7 @@ rm -f "$PL"
 # ===========================================================================
 
 # Test 21: -v prints extract_version trace (prefix check / match / found)
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my (\$repo, \$sha) = @ARGV;
@@ -561,7 +568,7 @@ chmod +x "$MOCKDIR/git"
 FAKEREPO=$(mktemp -d)
 export PATH="$MOCKDIR:$PATH"
 
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -600,7 +607,7 @@ chmod +x "$MOCKDIR/git"
 FAKEREPO=$(mktemp -d)
 export PATH="$MOCKDIR:$PATH"
 
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -642,7 +649,7 @@ chmod +x "$MOCKDIR/git"
 FAKEREPO=$(mktemp -d)
 export PATH="$MOCKDIR:$PATH"
 
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -679,7 +686,7 @@ chmod +x "$MOCKDIR/git"
 FAKEREPO=$(mktemp -d)
 export PATH="$MOCKDIR:$PATH"
 
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
@@ -721,7 +728,7 @@ chmod +x "$MOCKDIR/git"
 FAKEREPO=$(mktemp -d)
 export PATH="$MOCKDIR:$PATH"
 
-PL=$(mktemp --suffix=.pl)
+PL="batch_git_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use batchGitVersion;
 my \$repo = \$ARGV[0];
diff --git a/tests/scripts/gitblame_test.sh b/tests/scripts/gitblame_test.sh
index 53949c7c..2b3496d1 100755
--- a/tests/scripts/gitblame_test.sh
+++ b/tests/scripts/gitblame_test.sh
@@ -42,6 +42,9 @@ if [[ "x" == "${LCOV_HOME}x" ]] ; then
 fi
 source ../common.tst
 
+# cleanup: fixed-name inputs from previous runs
+rm -f gitblame_*.c gitblame_*.pl
+
 if [ -z "$SCRIPT_DIR" ] ; then
     echo "SCRIPT_DIR not set" >&2
     exit 1
@@ -517,7 +520,7 @@ rm -rf "$REPO17"
 #      with annotation data which differs in a token instead.  Three lines, two
 #      of which differ only in whitespace and must stay silent, so the count is
 #      asserted and not just the presence of the message.
-TMP19=$(mktemp --suffix=.c)
+TMP19="gitblame_19.c"
 printf 'int  main( )  {\n\treturn 0;   \n}\n' > "$TMP19"
 OUTPUT=$($PERL -I"$LCOV_LIB" -I"$SCRIPT_DIR" -e \
     'use lcovutil; use annotateutil;
@@ -679,7 +682,7 @@ fi
 #   file is not in a git repo at all - so it is annotated from the filesystem,
 #   and the pattern would never have been reached.
 # -----------------------------------------------------------------------
-TMP23=$(mktemp --suffix=.c)
+TMP23="gitblame_23.c"
 printf 'no repo here\n' > "$TMP23"
 OUTPUT=$($GITBLAME --abbrev 's/[/' "$TMP23" 2>&1)
 RC=$?
diff --git a/tests/scripts/gitversion_test.sh b/tests/scripts/gitversion_test.sh
index eae7b9e5..5447f312 100755
--- a/tests/scripts/gitversion_test.sh
+++ b/tests/scripts/gitversion_test.sh
@@ -352,7 +352,8 @@ rm -f "$TF15"
 #   Branch: MD5 set, old !~ /^SHA/, !P4, old =~ / md5:/, new =~ / md5:/ -> compare md5s
 # ---------------------------------------------------------------------------
 TF16=$(mktemp) ; echo "x" > "$TF16"
-MD5_16=$(md5sum "$TF16" | awk '{print $1}')
+
+MD5_16=$(compute_md5_hex "$TF16" | awk '{print $1}')
 $GITVERSION --md5 --compare \
     "2024-01-01T00:00:00+00:00 md5:${MD5_16}" \
     "2024-01-01T00:00:00+00:00 md5:${MD5_16}" \
@@ -366,7 +367,7 @@ rm -f "$TF16"
 # Test 17: --compare --md5, both versions carry different md5 -> exit 1
 # ---------------------------------------------------------------------------
 TF17=$(mktemp) ; echo "x" > "$TF17"
-MD5_17=$(md5sum "$TF17" | awk '{print $1}')
+MD5_17=$(compute_md5_hex "$TF17" | awk '{print $1}')
 $GITVERSION --md5 --compare \
     "2024-01-01T00:00:00+00:00 md5:${MD5_17}" \
     "2024-01-01T00:00:00+00:00 md5:DIFFERENTHASH" \
@@ -381,7 +382,7 @@ rm -f "$TF17"
 #   exact string match -> exit 1 (strings differ)
 # ---------------------------------------------------------------------------
 TF18=$(mktemp) ; echo "x" > "$TF18"
-MD5_18=$(md5sum "$TF18" | awk '{print $1}')
+MD5_18=$(compute_md5_hex "$TF18" | awk '{print $1}')
 $GITVERSION --md5 --compare \
     "2024-01-01T00:00:00+00:00 md5:${MD5_18}" \
     "2024-01-01T00:00:00+00:00" \
@@ -397,7 +398,7 @@ rm -f "$TF18"
 #   -> overall (old !~ /^CL/) part causes the whole MD5 block to be skipped.
 # ---------------------------------------------------------------------------
 TF19=$(mktemp) ; echo "x" > "$TF19"
-MD5_19=$(md5sum "$TF19" | awk '{print $1}')
+MD5_19=$(compute_md5_hex "$TF19" | awk '{print $1}')
 # Same CL number -> exact match -> exit 0
 $GITVERSION --md5 --p4 --compare \
     "CL 12345 md5:${MD5_19}" \
diff --git a/tests/scripts/p4annotate_test.sh b/tests/scripts/p4annotate_test.sh
index 1250d101..afbbd860 100755
--- a/tests/scripts/p4annotate_test.sh
+++ b/tests/scripts/p4annotate_test.sh
@@ -33,6 +33,8 @@ if [[ "x" == "${LCOV_HOME}x" ]] ; then
 fi
 source ../common.tst
 
+rm -f p4annotate_*.c p4annotate_*.pl
+
 if [ -z "$SCRIPT_DIR" ] ; then
     echo "SCRIPT_DIR not set" >&2
     exit 1
@@ -119,8 +121,16 @@ export PATH="$MOCKDIR:$PATH"
 
 # Create a temp source file; printf-style content string is the first argument
 mk_target() {
-    local f
-    f=$(mktemp --suffix=.c)
+    local i=1 f
+    # This function is always called via $(mk_target ...), so an index
+    # variable incremented here would die with the subshell and every
+    # caller would keep getting the same name (which let Devel::Cover's
+    # changed-file check fire on the reused name).  Pick the first free
+    # name by scanning what is already on disk instead.
+    while [ -e "p4annotate_${i}.c" ] ; do
+        i=$(( i + 1 ))
+    done
+    f="p4annotate_${i}.c"
     printf "${1:-line one\nline two\nline three\n}" > "$f"
     echo "$f"
 }
@@ -133,6 +143,8 @@ mk_diff()     { local f ; f=$(mktemp) ; cat > "$f" ; echo "$f" ; }
 run_p4annotate() {
     OUTPUT=$($P4ANNOTATE "$@" 2>&1)
     RC=$?
+    # Strip possible Devel::Cover status lines (not all are gated by -silent).
+    OUTPUT=$(sed '/^Devel::Cover:/d' <<<"$OUTPUT")
 }
 
 # ---------------------------------------------------------------------------
@@ -401,7 +413,7 @@ fi
 # ---------------------------------------------------------------------------
 # Test 15: Path normalisation logic from annotate_callback resolves symlink
 # ---------------------------------------------------------------------------
-NORM_SCRIPT=$(mktemp --suffix=.pl)
+NORM_SCRIPT="p4annotate_15.pl"
 cat > "$NORM_SCRIPT" << 'NORMPL'
 use File::Spec;
 use File::Basename qw(dirname);
@@ -433,7 +445,7 @@ fi
 # ---------------------------------------------------------------------------
 # Test 16: Path normalisation handles .. components in symlink target
 # ---------------------------------------------------------------------------
-NORM_SCRIPT=$(mktemp --suffix=.pl)
+NORM_SCRIPT="p4annotate_16.pl"
 cat > "$NORM_SCRIPT" << 'NORMPL'
 use File::Spec;
 use File::Basename qw(dirname);
@@ -487,7 +499,8 @@ SYMLINK_TESTDIR=""
 # ===========================================================================
 # Tests 18-34: End-to-end annotation output  (from annotate_check.sh)
 # ===========================================================================
-
+# mk_target() numbers its files by scanning the directory, starting again
+# at p4annotate_1.c since the startup cleanup removed any leftovers.
 # ---------------------------------------------------------------------------
 # Test 18: File not in p4 -> not_in_repo fallback -> NONE annotation
 # ---------------------------------------------------------------------------
@@ -1100,7 +1113,7 @@ fi
 # Test 35: normalize_path returns original for non-existent path (no symlink
 #          resolution attempted when -e is false)
 # ---------------------------------------------------------------------------
-NORM_SCRIPT=$(mktemp --suffix=.pl)
+NORM_SCRIPT="p4annotate_35.pl"
 cat > "$NORM_SCRIPT" << 'NORMPL'
 use File::Spec;
 use File::Basename qw(dirname);
diff --git a/tests/scripts/p4version_test.sh b/tests/scripts/p4version_test.sh
index eea6223c..2c0ec971 100755
--- a/tests/scripts/p4version_test.sh
+++ b/tests/scripts/p4version_test.sh
@@ -70,6 +70,16 @@ if [[ "x" == "${LCOV_HOME}x" ]] ; then
 fi
 source ../common.tst
 
+# Remove any helper scripts a killed or failing run left behind.  The
+# p4v_test_*.pl files are written into this directory; a run which dies
+# (fail(), a timeout, Ctrl-C) exits before its 'rm -f "$PL"', and a stale
+# copy under a name the Devel::Cover database still knows re-hashes to a
+# different digest, which makes Devel::Cover print
+#   'Devel::Cover: Deleting old coverage for changed file ...'
+# on stderr - not suppressed by -silent, and it would pollute the captured
+# output of the exact-match tests.
+rm -f p4v_test_*.sh p4v_test_*.pl
+
 if [ -z "$SCRIPT_DIR" ] ; then
     echo "SCRIPT_DIR not set" >&2
     exit 1
@@ -100,7 +110,7 @@ fail() { echo "FAIL: $1" ; ((FAIL++)) ; if [ "$KEEP_GOING" != 1 ] ; then exit 1
 # ---------------------------------------------------------------------------
 MOCKDIR=$(mktemp -d)
 WORKSPACE=$(mktemp -d)
-trap 'rm -rf "$MOCKDIR" "$WORKSPACE"' EXIT
+trap 'rm -rf "$MOCKDIR" "$WORKSPACE"; rm -f p4v_test_*.pl' EXIT
 
 # The fake p4 script handles both getp4version and P4version.pm invocations.
 # For getp4version: uses P4V_NO_SUCH, P4V_HAVE_OUT, P4V_OPENED_OUT
@@ -153,16 +163,20 @@ TF1="$WORKSPACE/foo.c"
 TF2="$WORKSPACE/bar.c"
 echo "int main(){}" > "$TF1"
 echo "// bar" > "$TF2"
-MD5_TF1=$(md5sum "$TF1" | awk '{print $1}')
+MD5_TF1=$(compute_md5_hex "$TF1" | awk '{print $1}')
 
 export P4V_WORKSPACE="$WORKSPACE"
 
 # ---------------------------------------------------------------------------
 # Helper: run getp4version, capture OUTPUT and RC
 # ---------------------------------------------------------------------------
+# Devel::Cover status lines can leak into the captured output even with
+# -silent (its 'Deleting old coverage for changed file' message is printed
+# unconditionally), so they must not reach the exact-match comparisons.
 run_getp4version() {
     OUTPUT=$($GETP4VERSION "$@" 2>&1)
     RC=$?
+    OUTPUT=$(sed '/^Devel::Cover:/d' <<<"$OUTPUT")
 }
 
 # ---------------------------------------------------------------------------
@@ -172,6 +186,7 @@ run_p4version_pl() {
     local pl_file="$1"; shift
     OUTPUT=$($PERL -I"$SCRIPT_DIR" -I"$LCOV_LIB" "$pl_file" "$@" 2>&1)
     RC=$?
+    OUTPUT=$(sed '/^Devel::Cover:/d' <<<"$OUTPUT")
 }
 
 # ===========================================================================
@@ -391,7 +406,8 @@ fi
 # ---------------------------------------------------------------------------
 # Test 20: new() bad option -> undef (script ne $0 so returns undef not exit)
 # ---------------------------------------------------------------------------
-PL=$(mktemp --suffix=.pl)
+TESTIDX=20
+PL="p4v_test_${TESTIDX}.pl"
 cat > "$PL" << 'PLEOF'
 use P4version;
 # Pass a fake script name different from $0 so new() returns undef instead of exiting
@@ -409,7 +425,7 @@ rm -f "$PL"
 # ---------------------------------------------------------------------------
 # Test 21: new() --help -> undef
 # ---------------------------------------------------------------------------
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << 'PLEOF'
 use P4version;
 my $obj = P4version->new('/fake/caller', '--help');
@@ -426,7 +442,7 @@ rm -f "$PL"
 # ---------------------------------------------------------------------------
 # Test 22: new() depot arg not a directory -> dies
 # ---------------------------------------------------------------------------
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << 'PLEOF'
 use P4version;
 eval { P4version->new('/fake/caller', '/nonexistent/depot/path'); };
@@ -447,7 +463,7 @@ unset P4V_HAVE_LINES P4V_WHERE_LINE P4V_OPENED_LINES P4V_NO_SUCH
 export P4V_HAVE_LINES=""   # empty -> no filehash entries
 export P4V_SUBCOMMAND_EXIT=0
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my \$file = \$ARGV[0];
@@ -473,7 +489,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES=""
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -497,7 +513,7 @@ export P4V_HAVE_LINES="//depot/bar.c#5 - $TF2"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES=""
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -523,7 +539,7 @@ export P4V_HAVE_LINES="//depot/bar.c#5 - $TF2"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES=""
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file, \$md5) = @ARGV;
@@ -549,7 +565,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES="//depot/foo.c#3 - edit default change (text)"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot) = @ARGV;
@@ -571,7 +587,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES="//depot/foo.c#3 - edit default change (text)"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -597,7 +613,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES="//depot/foo.c#3 - integrate change 42 (text)"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -624,7 +640,7 @@ export P4V_HAVE_LINES="//depot/bar.c#5 - $TF2"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES="//depot/foo.c#1 - add default change (text)"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -652,7 +668,7 @@ export P4V_HAVE_LINES="//depot/bar.c#5 - $TF2"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES="//depot/gone.c#3 - delete default change (text)"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -678,7 +694,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES="//depot/foo.c#3 - edit default change (text)"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file, \$md5) = @ARGV;
@@ -704,7 +720,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES=""
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$missing) = @ARGV;
@@ -726,7 +742,7 @@ rm -f "$PL"
 # ---------------------------------------------------------------------------
 # Test 34: extract_version without --allow-missing, missing file -> dies
 # ---------------------------------------------------------------------------
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$missing) = @ARGV;
@@ -752,7 +768,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES=""
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$prefix) = @ARGV;
@@ -773,7 +789,7 @@ rm -f "$PL"
 # ---------------------------------------------------------------------------
 # Test 36: compare_version same strings -> 0 (false)
 # ---------------------------------------------------------------------------
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -795,7 +811,7 @@ rm -f "$PL"
 # ---------------------------------------------------------------------------
 # Test 37: compare_version different strings -> 1 (true)
 # ---------------------------------------------------------------------------
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -818,7 +834,7 @@ export P4V_SUBCOMMAND_EXIT=1   # make all p4 sub-commands exit 1 (have fails)
 unset P4V_HAVE_LINES
 export P4V_HAVE_LINES=""       # empty output + bad exit -> close fails, empty hash
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use lcovutil;
 lcovutil::parse_ignore_errors("usage");
@@ -860,7 +876,7 @@ export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES="//depot/foo.c#3 - delete default change (text)"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << PLEOF
 use P4version;
 my (\$depot, \$file) = @ARGV;
@@ -885,6 +901,7 @@ rm -f "$PL"
 #   reached p4 as two arguments;  p4 answered 'no such file' and the file was
 #   reported by modification time as if it were not in perforce at all.
 # ===========================================================================
+TESTIDX=$(( TESTIDX + 1 ))
 TF40="$WORKSPACE/has space.c"
 echo "int spaced(){}" > "$TF40"
 unset P4V_NO_SUCH P4V_OPENED_OUT P4V_SUBCOMMAND_EXIT
@@ -909,7 +926,7 @@ export P4V_HAVE_LINES="//depot/foo.c - no revision here"
 export P4V_WHERE_LINE="//depot/... //ws/... $WORKSPACE/..."
 export P4V_OPENED_LINES=""
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << 'PLEOF'
 use P4version;
 my ($depot) = @ARGV;
@@ -930,7 +947,7 @@ rm -f "$PL"
 export P4V_HAVE_LINES="//depot/foo.c#3 - $TF1"
 export P4V_OPENED_LINES="//depot/foo.c - opened somehow"
 
-PL=$(mktemp --suffix=.pl)
+PL="p4v_test_$(( ++TESTIDX )).pl"
 cat > "$PL" << 'PLEOF'
 use P4version;
 my ($depot) = @ARGV;
