aboutsummaryrefslogtreecommitdiff
path: root/path/tests/prop.sh
blob: e48c6667fa08f9e8ba635eea877c4afcf7baaaee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env bash

# Property tests for the `lib.path` library
#
# It generates random path-like strings and runs the functions on
# them, checking that the expected laws of the functions hold

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 \
    --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