1#!/usr/bin/env bash
2
3# Property tests for lib/path/default.nix
4# It generates random path-like strings and runs the functions on
5# them, checking that the expected laws of the functions hold
6# Run:
7# [nixpkgs]$ lib/path/tests/prop.sh
8# or:
9# [nixpkgs]$ nix-build lib/tests/release.nix
10
11set -euo pipefail
12shopt -s inherit_errexit
13
14# https://stackoverflow.com/a/246128
15SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
16
17if test -z "${TEST_LIB:-}"; then
18 TEST_LIB=$SCRIPT_DIR/../..
19fi
20
21tmp="$(mktemp -d)"
22clean_up() {
23 rm -rf "$tmp"
24}
25trap clean_up EXIT
26mkdir -p "$tmp/work"
27cd "$tmp/work"
28
29# Defaulting to a random seed but the first argument can override this
30seed=${1:-$RANDOM}
31echo >&2 "Using seed $seed, use \`lib/path/tests/prop.sh $seed\` to reproduce this result"
32
33# The number of random paths to generate. This specific number was chosen to
34# be fast enough while still generating enough variety to detect bugs.
35count=500
36
37debug=0
38# debug=1 # print some extra info
39# debug=2 # print generated values
40
41# Fine tuning parameters to balance the number of generated invalid paths
42# to the variance in generated paths.
43extradotweight=64 # Larger value: more dots
44extraslashweight=64 # Larger value: more slashes
45extranullweight=16 # Larger value: shorter strings
46
47die() {
48 echo >&2 "test case failed: " "$@"
49 exit 1
50}
51
52if [[ "$debug" -ge 1 ]]; then
53 echo >&2 "Generating $count random path-like strings"
54fi
55
56# Read stream of null-terminated strings entry-by-entry into bash,
57# write it to a file and the `strings` array.
58declare -a strings=()
59mkdir -p "$tmp/strings"
60while IFS= read -r -d $'\0' str; do
61 printf "%s" "$str" > "$tmp/strings/${#strings[@]}"
62 strings+=("$str")
63done < <(awk \
64 -f "$SCRIPT_DIR"/generate.awk \
65 -v seed="$seed" \
66 -v count="$count" \
67 -v extradotweight="$extradotweight" \
68 -v extraslashweight="$extraslashweight" \
69 -v extranullweight="$extranullweight")
70
71if [[ "$debug" -ge 1 ]]; then
72 echo >&2 "Trying to normalise the generated path-like strings with Nix"
73fi
74
75# Precalculate all normalisations with a single Nix call. Calling Nix for each
76# string individually would take way too long
77nix-instantiate --eval --strict --json --show-trace \
78 --argstr libpath "$TEST_LIB" \
79 --argstr dir "$tmp/strings" \
80 "$SCRIPT_DIR"/prop.nix \
81 >"$tmp/result.json"
82
83# Uses some jq magic to turn the resulting attribute set into an associative
84# bash array assignment
85declare -A normalised_result="($(jq '
86 to_entries
87 | map("[\(.key | @sh)]=\(.value | @sh)")
88 | join(" \n")' -r < "$tmp/result.json"))"
89
90# Looks up a normalisation result for a string
91# Checks that the normalisation is only failing iff it's an invalid subpath
92# For valid subpaths, returns 0 and prints the normalisation result
93# For invalid subpaths, returns 1
94normalise() {
95 local str=$1
96 # Uses the same check for validity as in the library implementation
97 if [[ "$str" == "" || "$str" == /* || "$str" =~ ^(.*/)?\.\.(/.*)?$ ]]; then
98 valid=
99 else
100 valid=1
101 fi
102
103 normalised=${normalised_result[$str]}
104 # An empty string indicates failure, this is encoded in ./prop.nix
105 if [[ -n "$normalised" ]]; then
106 if [[ -n "$valid" ]]; then
107 echo "$normalised"
108 else
109 die "For invalid subpath \"$str\", lib.path.subpath.normalise returned this result: \"$normalised\""
110 fi
111 else
112 if [[ -n "$valid" ]]; then
113 die "For valid subpath \"$str\", lib.path.subpath.normalise failed"
114 else
115 if [[ "$debug" -ge 2 ]]; then
116 echo >&2 "String \"$str\" is not a valid subpath"
117 fi
118 # Invalid and it correctly failed, we let the caller continue if they catch the exit code
119 return 1
120 fi
121 fi
122}
123
124# Intermediate result populated by test_idempotency_realpath
125# and used in test_normalise_uniqueness
126#
127# Contains a mapping from a normalised subpath to the realpath result it represents
128declare -A norm_to_real
129
130test_idempotency_realpath() {
131 if [[ "$debug" -ge 1 ]]; then
132 echo >&2 "Checking idempotency of each result and making sure the realpath result isn't changed"
133 fi
134
135 # Count invalid subpaths to display stats
136 invalid=0
137 for str in "${strings[@]}"; do
138 if ! result=$(normalise "$str"); then
139 ((invalid++)) || true
140 continue
141 fi
142
143 # Check the law that it doesn't change the result of a realpath
144 mkdir -p -- "$str" "$result"
145 real_orig=$(realpath -- "$str")
146 real_norm=$(realpath -- "$result")
147
148 if [[ "$real_orig" != "$real_norm" ]]; then
149 die "realpath of the original string \"$str\" (\"$real_orig\") is not the same as realpath of the normalisation \"$result\" (\"$real_norm\")"
150 fi
151
152 if [[ "$debug" -ge 2 ]]; then
153 echo >&2 "String \"$str\" gets normalised to \"$result\" and file path \"$real_orig\""
154 fi
155 norm_to_real["$result"]="$real_orig"
156 done
157 if [[ "$debug" -ge 1 ]]; then
158 echo >&2 "$(bc <<< "scale=1; 100 / $count * $invalid")% of the total $count generated strings were invalid subpath strings, and were therefore ignored"
159 fi
160}
161
162test_normalise_uniqueness() {
163 if [[ "$debug" -ge 1 ]]; then
164 echo >&2 "Checking for the uniqueness law"
165 fi
166
167 for norm_p in "${!norm_to_real[@]}"; do
168 real_p=${norm_to_real["$norm_p"]}
169 for norm_q in "${!norm_to_real[@]}"; do
170 real_q=${norm_to_real["$norm_q"]}
171 # Checks normalisation uniqueness law for each pair of values
172 if [[ "$norm_p" != "$norm_q" && "$real_p" == "$real_q" ]]; then
173 die "Normalisations \"$norm_p\" and \"$norm_q\" are different, but the realpath of them is the same: \"$real_p\""
174 fi
175 done
176 done
177}
178
179test_idempotency_realpath
180test_normalise_uniqueness
181
182echo >&2 tests ok