183 lines
5.8 KiB
Bash
183 lines
5.8 KiB
Bash
|
#!/usr/bin/env bash
|
||
|
|
||
|
# Property tests for lib/path/default.nix
|
||
|
# It generates random path-like strings and runs the functions on
|
||
|
# them, checking that the expected laws of the functions hold
|
||
|
# Run:
|
||
|
# [nixpkgs]$ lib/path/tests/prop.sh
|
||
|
# or:
|
||
|
# [nixpkgs]$ nix-build lib/tests/release.nix
|
||
|
|
||
|
set -euo pipefail
|
||
|
shopt -s inherit_errexit
|
||
|
|
||
|
# https://stackoverflow.com/a/246128
|
||
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||
|
|
||
|
if test -z "${TEST_LIB:-}"; then
|
||
|
TEST_LIB=$SCRIPT_DIR/../..
|
||
|
fi
|
||
|
|
||
|
tmp="$(mktemp -d)"
|
||
|
clean_up() {
|
||
|
rm -rf "$tmp"
|
||
|
}
|
||
|
trap clean_up EXIT
|
||
|
mkdir -p "$tmp/work"
|
||
|
cd "$tmp/work"
|
||
|
|
||
|
# Defaulting to a random seed but the first argument can override this
|
||
|
seed=${1:-$RANDOM}
|
||
|
echo >&2 "Using seed $seed, use \`lib/path/tests/prop.sh $seed\` to reproduce this result"
|
||
|
|
||
|
# The number of random paths to generate. This specific number was chosen to
|
||
|
# be fast enough while still generating enough variety to detect bugs.
|
||
|
count=500
|
||
|
|
||
|
debug=0
|
||
|
# debug=1 # print some extra info
|
||
|
# debug=2 # print generated values
|
||
|
|
||
|
# Fine tuning parameters to balance the number of generated invalid paths
|
||
|
# to the variance in generated paths.
|
||
|
extradotweight=64 # Larger value: more dots
|
||
|
extraslashweight=64 # Larger value: more slashes
|
||
|
extranullweight=16 # Larger value: shorter strings
|
||
|
|
||
|
die() {
|
||
|
echo >&2 "test case failed: " "$@"
|
||
|
exit 1
|
||
|
}
|
||
|
|
||
|
if [[ "$debug" -ge 1 ]]; then
|
||
|
echo >&2 "Generating $count random path-like strings"
|
||
|
fi
|
||
|
|
||
|
# Read stream of null-terminated strings entry-by-entry into bash,
|
||
|
# write it to a file and the `strings` array.
|
||
|
declare -a strings=()
|
||
|
mkdir -p "$tmp/strings"
|
||
|
while IFS= read -r -d $'\0' str; do
|
||
|
printf "%s" "$str" > "$tmp/strings/${#strings[@]}"
|
||
|
strings+=("$str")
|
||
|
done < <(awk \
|
||
|
-f "$SCRIPT_DIR"/generate.awk \
|
||
|
-v seed="$seed" \
|
||
|
-v count="$count" \
|
||
|
-v extradotweight="$extradotweight" \
|
||
|
-v extraslashweight="$extraslashweight" \
|
||
|
-v extranullweight="$extranullweight")
|
||
|
|
||
|
if [[ "$debug" -ge 1 ]]; then
|
||
|
echo >&2 "Trying to normalise the generated path-like strings with Nix"
|
||
|
fi
|
||
|
|
||
|
# Precalculate all normalisations with a single Nix call. Calling Nix for each
|
||
|
# string individually would take way too long
|
||
|
nix-instantiate --eval --strict --json --show-trace \
|
||
|
--argstr libpath "$TEST_LIB" \
|
||
|
--argstr dir "$tmp/strings" \
|
||
|
"$SCRIPT_DIR"/prop.nix \
|
||
|
>"$tmp/result.json"
|
||
|
|
||
|
# Uses some jq magic to turn the resulting attribute set into an associative
|
||
|
# bash array assignment
|
||
|
declare -A normalised_result="($(jq '
|
||
|
to_entries
|
||
|
| map("[\(.key | @sh)]=\(.value | @sh)")
|
||
|
| join(" \n")' -r < "$tmp/result.json"))"
|
||
|
|
||
|
# Looks up a normalisation result for a string
|
||
|
# Checks that the normalisation is only failing iff it's an invalid subpath
|
||
|
# For valid subpaths, returns 0 and prints the normalisation result
|
||
|
# For invalid subpaths, returns 1
|
||
|
normalise() {
|
||
|
local str=$1
|
||
|
# Uses the same check for validity as in the library implementation
|
||
|
if [[ "$str" == "" || "$str" == /* || "$str" =~ ^(.*/)?\.\.(/.*)?$ ]]; then
|
||
|
valid=
|
||
|
else
|
||
|
valid=1
|
||
|
fi
|
||
|
|
||
|
normalised=${normalised_result[$str]}
|
||
|
# An empty string indicates failure, this is encoded in ./prop.nix
|
||
|
if [[ -n "$normalised" ]]; then
|
||
|
if [[ -n "$valid" ]]; then
|
||
|
echo "$normalised"
|
||
|
else
|
||
|
die "For invalid subpath \"$str\", lib.path.subpath.normalise returned this result: \"$normalised\""
|
||
|
fi
|
||
|
else
|
||
|
if [[ -n "$valid" ]]; then
|
||
|
die "For valid subpath \"$str\", lib.path.subpath.normalise failed"
|
||
|
else
|
||
|
if [[ "$debug" -ge 2 ]]; then
|
||
|
echo >&2 "String \"$str\" is not a valid subpath"
|
||
|
fi
|
||
|
# Invalid and it correctly failed, we let the caller continue if they catch the exit code
|
||
|
return 1
|
||
|
fi
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
# Intermediate result populated by test_idempotency_realpath
|
||
|
# and used in test_normalise_uniqueness
|
||
|
#
|
||
|
# Contains a mapping from a normalised subpath to the realpath result it represents
|
||
|
declare -A norm_to_real
|
||
|
|
||
|
test_idempotency_realpath() {
|
||
|
if [[ "$debug" -ge 1 ]]; then
|
||
|
echo >&2 "Checking idempotency of each result and making sure the realpath result isn't changed"
|
||
|
fi
|
||
|
|
||
|
# Count invalid subpaths to display stats
|
||
|
invalid=0
|
||
|
for str in "${strings[@]}"; do
|
||
|
if ! result=$(normalise "$str"); then
|
||
|
((invalid++)) || true
|
||
|
continue
|
||
|
fi
|
||
|
|
||
|
# Check the law that it doesn't change the result of a realpath
|
||
|
mkdir -p -- "$str" "$result"
|
||
|
real_orig=$(realpath -- "$str")
|
||
|
real_norm=$(realpath -- "$result")
|
||
|
|
||
|
if [[ "$real_orig" != "$real_norm" ]]; then
|
||
|
die "realpath of the original string \"$str\" (\"$real_orig\") is not the same as realpath of the normalisation \"$result\" (\"$real_norm\")"
|
||
|
fi
|
||
|
|
||
|
if [[ "$debug" -ge 2 ]]; then
|
||
|
echo >&2 "String \"$str\" gets normalised to \"$result\" and file path \"$real_orig\""
|
||
|
fi
|
||
|
norm_to_real["$result"]="$real_orig"
|
||
|
done
|
||
|
if [[ "$debug" -ge 1 ]]; then
|
||
|
echo >&2 "$(bc <<< "scale=1; 100 / $count * $invalid")% of the total $count generated strings were invalid subpath strings, and were therefore ignored"
|
||
|
fi
|
||
|
}
|
||
|
|
||
|
test_normalise_uniqueness() {
|
||
|
if [[ "$debug" -ge 1 ]]; then
|
||
|
echo >&2 "Checking for the uniqueness law"
|
||
|
fi
|
||
|
|
||
|
for norm_p in "${!norm_to_real[@]}"; do
|
||
|
real_p=${norm_to_real["$norm_p"]}
|
||
|
for norm_q in "${!norm_to_real[@]}"; do
|
||
|
real_q=${norm_to_real["$norm_q"]}
|
||
|
# Checks normalisation uniqueness law for each pair of values
|
||
|
if [[ "$norm_p" != "$norm_q" && "$real_p" == "$real_q" ]]; then
|
||
|
die "Normalisations \"$norm_p\" and \"$norm_q\" are different, but the realpath of them is the same: \"$real_p\""
|
||
|
fi
|
||
|
done
|
||
|
done
|
||
|
}
|
||
|
|
||
|
test_idempotency_realpath
|
||
|
test_normalise_uniqueness
|
||
|
|
||
|
echo >&2 tests ok
|