diff --git a/lib/path/tests/generate.awk b/lib/path/tests/generate.awk deleted file mode 100644 index 811dd0c46d332..0000000000000 --- a/lib/path/tests/generate.awk +++ /dev/null @@ -1,64 +0,0 @@ -# Generate random path-like strings, separated by null characters. -# -# Invocation: -# -# awk -f ./generate.awk -v = | tr '\0' '\n' -# -# Customizable variables (all default to 0): -# - seed: Deterministic random seed to use for generation -# - count: Number of paths to generate -# - extradotweight: Give extra weight to dots being generated -# - extraslashweight: Give extra weight to slashes being generated -# - extranullweight: Give extra weight to null being generated, making paths shorter -BEGIN { - # Random seed, passed explicitly for reproducibility - srand(seed) - - # Don't include special characters below 32 - minascii = 32 - # Don't include DEL at 128 - maxascii = 127 - upperascii = maxascii - minascii - - # add extra weight for ., in addition to the one weight from the ascii range - upperdot = upperascii + extradotweight - - # add extra weight for /, in addition to the one weight from the ascii range - upperslash = upperdot + extraslashweight - - # add extra weight for null, indicating the end of the string - # Must be at least 1 to have strings end at all - total = upperslash + 1 + extranullweight - - # new=1 indicates that it's a new string - new=1 - while (count > 0) { - - # Random integer between [0, total) - value = int(rand() * total) - - if (value < upperascii) { - # Ascii range - printf("%c", value + minascii) - new=0 - - } else if (value < upperdot) { - # Dot range - printf "." - new=0 - - } else if (value < upperslash) { - # If it's the start of a new path, only generate a / in 10% of cases - # This is always an invalid subpath, which is not a very interesting case - if (new && rand() > 0.1) continue - printf "/" - - } else { - # Do not generate empty strings - if (new) continue - printf "\x00" - count-- - new=1 - } - } -} diff --git a/lib/path/tests/prop.nix b/lib/path/tests/prop.nix deleted file mode 100644 index 67e5c1e9d61c7..0000000000000 --- a/lib/path/tests/prop.nix +++ /dev/null @@ -1,60 +0,0 @@ -# Given a list of path-like strings, check some properties of the path library -# using those paths and return a list of attribute sets of the following form: -# -# { = ; } -# -# If `normalise` fails to evaluate, the attribute value is set to `""`. -# If not, the resulting value is normalised again and an appropriate attribute set added to the output list. -{ - # The path to the nixpkgs lib to use - libpath, - # A flat directory containing files with randomly-generated - # path-like values - dir, -}: -let - lib = import libpath; - - # read each file into a string - strings = map (name: - builtins.readFile (dir + "/${name}") - ) (builtins.attrNames (builtins.readDir dir)); - - inherit (lib.path.subpath) normalise isValid; - inherit (lib.asserts) assertMsg; - - normaliseAndCheck = str: - let - originalValid = isValid str; - - tryOnce = builtins.tryEval (normalise str); - tryTwice = builtins.tryEval (normalise tryOnce.value); - - absConcatOrig = /. + ("/" + str); - absConcatNormalised = /. + ("/" + tryOnce.value); - in - # Check the lib.path.subpath.normalise property to only error on invalid subpaths - assert assertMsg - (originalValid -> tryOnce.success) - "Even though string \"${str}\" is valid as a subpath, the normalisation for it failed"; - assert assertMsg - (! originalValid -> ! tryOnce.success) - "Even though string \"${str}\" is invalid as a subpath, the normalisation for it succeeded"; - - # Check normalisation idempotency - assert assertMsg - (originalValid -> tryTwice.success) - "For valid subpath \"${str}\", the normalisation \"${tryOnce.value}\" was not a valid subpath"; - assert assertMsg - (originalValid -> tryOnce.value == tryTwice.value) - "For valid subpath \"${str}\", normalising it once gives \"${tryOnce.value}\" but normalising it twice gives a different result: \"${tryTwice.value}\""; - - # Check that normalisation doesn't change a string when appended to an absolute Nix path value - assert assertMsg - (originalValid -> absConcatOrig == absConcatNormalised) - "For valid subpath \"${str}\", appending to an absolute Nix path value gives \"${absConcatOrig}\", but appending the normalised result \"${tryOnce.value}\" gives a different value \"${absConcatNormalised}\""; - - # Return an empty string when failed - if tryOnce.success then tryOnce.value else ""; - -in lib.genAttrs strings normaliseAndCheck diff --git a/lib/path/tests/prop.sh b/lib/path/tests/prop.sh deleted file mode 100755 index e48c6667fa08f..0000000000000 --- a/lib/path/tests/prop.sh +++ /dev/null @@ -1,179 +0,0 @@ -#!/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 diff --git a/lib/path/tests/props/normalise-append.sh b/lib/path/tests/props/normalise-append.sh new file mode 100644 index 0000000000000..6402a1f350e70 --- /dev/null +++ b/lib/path/tests/props/normalise-append.sh @@ -0,0 +1,5 @@ +stage_setup 1000 +stage_awk_expr subpath 'gen_valid_subpath()' +stage_nix_expr appended '/. + ("/" + subpath)' +stage_nix_expr normalised_appended '/. + ("/" + lib.path.subpath.normalise subpath)' +stage_check appended == normalised_appended diff --git a/lib/path/tests/props/normalise-error.sh b/lib/path/tests/props/normalise-error.sh new file mode 100644 index 0000000000000..6d41453066967 --- /dev/null +++ b/lib/path/tests/props/normalise-error.sh @@ -0,0 +1,9 @@ +stage_setup 1000 +stage_awk_expr \ + subpath 'gen_subpath()' \ + valid 'subpath_is_valid(subpath) ? "true" : "false"' + +stage_nix_expr normalise_success 'lib.boolToString (builtins.tryEval (lib.path.subpath.normalise subpath)).success' + +stage_check valid == normalise_success + diff --git a/lib/path/tests/props/normalise-idempotent.sh b/lib/path/tests/props/normalise-idempotent.sh new file mode 100644 index 0000000000000..a3bd45836a63b --- /dev/null +++ b/lib/path/tests/props/normalise-idempotent.sh @@ -0,0 +1,9 @@ +stage_setup 1000 +stage_awk_expr subpath 'gen_valid_subpath()' +stage_nix_expr normalised 'lib.path.subpath.normalise subpath' +stage_nix_expr normalised_valid 'lib.boolToString (lib.path.subpath.isValid normalised)' +stage_constant normalised_valid_expected true +stage_check normalised_valid == normalised_valid_expected + +stage_nix_expr normalised_twice 'lib.path.subpath.normalise normalised' +stage_check normalised == normalised_twice diff --git a/lib/path/tests/props/normalise-realpath.sh b/lib/path/tests/props/normalise-realpath.sh new file mode 100644 index 0000000000000..41bbc162a6e24 --- /dev/null +++ b/lib/path/tests/props/normalise-realpath.sh @@ -0,0 +1,7 @@ +stage_setup 300 +stage_awk_expr subpath 'gen_valid_subpath()' +stage_nix_expr subpath_normalised 'lib.path.subpath.normalise subpath' +stage_bash_expr subpath_realpath 'realpath -m --relative-to=$PWD -- "$subpath"' +stage_bash_expr subpath_normalised_realpath 'realpath -m --relative-to=$PWD -- "${subpath_normalised}"' + +stage_check subpath_realpath == subpath_normalised_realpath diff --git a/lib/path/tests/props/normalise-unique.sh b/lib/path/tests/props/normalise-unique.sh new file mode 100644 index 0000000000000..76300af3492a0 --- /dev/null +++ b/lib/path/tests/props/normalise-unique.sh @@ -0,0 +1,13 @@ +stage_setup 1000 +stage_awk_expr \ + a 'gen_subpath_with("valid", 5)' \ + b 'gen_subpath_with("valid", 5)' + +stage_nix_expr a_normalised 'lib.path.subpath.normalise a' +stage_nix_expr b_normalised 'lib.path.subpath.normalise b' +stage_condition a_normalised != b_normalised + +stage_bash_expr a_realpath 'realpath -m --relative-to=$PWD -- "$a"' +stage_bash_expr b_realpath 'realpath -m --relative-to=$PWD -- "$b"' + +stage_check a_realpath != b_realpath diff --git a/lib/path/tests/props/valid.sh b/lib/path/tests/props/valid.sh new file mode 100644 index 0000000000000..29f6e31449058 --- /dev/null +++ b/lib/path/tests/props/valid.sh @@ -0,0 +1,9 @@ +stage_setup 1000 + +stage_awk_expr \ + input 'gen_subpath()' \ + expected_valid 'subpath_is_valid(input) ? "true" : "false"' + +stage_nix_expr valid 'lib.boolToString (lib.path.subpath.isValid input)' + +stage_check valid == expected_valid diff --git a/lib/path/tests/run.sh b/lib/path/tests/run.sh new file mode 100755 index 0000000000000..5533c1eaab46d --- /dev/null +++ b/lib/path/tests/run.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +set -euo pipefail + +# https://stackoverflow.com/a/246128 +script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +script_name=$(basename ${BASH_SOURCE[0]}) + +nixpkgs_root=$(realpath "$script_dir"/../../..) +lib_path=${LIB_PATH:-$(realpath "$nixpkgs_root/lib")} +property_to_test=${1:-} +original_seed=${2:-$RANDOM} +seed=$original_seed + + +tmp=$(mktemp -d) +trap 'rm -r "$tmp"' exit + +stage_setup() { + #echo "stage_setup $*" + count=$1 + + steps_file="$tmp"/steps/"$property" + case_dir="$tmp"/cases/"$property" + if [[ -e "$case_dir" ]]; then + # Cleaning up any previous case + rm -r "$case_dir" + fi + mkdir -p "$(dirname "$steps_file")" + touch "$steps_file" + eval 'mkdir -p "$case_dir"/{0..'$((count - 1))'}' +} + +stage_condition_non_equal() { + for c in "$case_dir"/*; do + if ! diff "$c/$1" "$c/$2" >/dev/null; then + rm -r "$c" + fi + done +} + +stage_awk_expr() { + #echo "stage_awk_expr $*" + declare -a variable_ordering + declare -A variables + while [[ "$#" -gt 1 ]]; do + variable_ordering+=($1) + variables[$1]=$2 + echo "awk $1 $2" >> "$steps_file" + shift 2 + done + + echo "function stage() {" > "$tmp/expr.awk" + for variable in "${variable_ordering[@]}"; do + echo " $variable = ${variables[$variable]}" >> "$tmp/expr.awk" + echo " write(\"$variable\", $variable)" >> "$tmp/expr.awk" + done + echo "}" >> "$tmp/expr.awk" + + # echo "Running awk stage $1" + awk -f "$tmp/expr.awk" \ + -f "$script_dir"/stage/awk.awk \ + -v seed="$seed" \ + -v count="$count" \ + -v case_dir="$case_dir" + + + # Increase the seed such that each stage gets a different (but deterministic) seed + ((seed++)) || true +} + +stage_nix_expr() { + #echo "stage_nix_expr $*" + local variable=$1 + local expr=$2 + printf "%s" "$expr" > "$tmp/expr.nix" + nix-instantiate "$script_dir"/stage/nix-eval.nix \ + --argstr libPath "$lib_path" \ + --argstr caseDir "$case_dir" \ + --argstr variable "$variable" \ + --argstr exprFile "$tmp/expr.nix" \ + --eval --strict --json | jq -r > "$tmp/nix-script" + + source "$tmp/nix-script" + + echo "nix $variable $expr" >> "$steps_file" +} + +stage_bash_expr() { + #echo "stage_bash_expr $*" + local variable=$1 + local expr=$2 + + for c in "$case_dir"/*; do + ( + cd "$c" + for f in *; do + eval "$f=\$(<$f)" + done + eval "$expr" > $variable + ) + done + echo "bash $variable $expr" >> "$steps_file" +} + +stage_constant() { + #echo "stage_costant $*" + local variable=$1 + local string=$2 + + for c in "$case_dir"/*; do + printf "%s" "$string" > "$c"/"$variable" + done + echo "constant $variable $string" >> "$steps_file" +} + +stage_condition() { + _stage_check "$@" skip +} + +stage_check() { + _stage_check "$@" fail +} + +_stage_check() { + local var1=$1 + local check=$2 + local var2=$3 + local action=$4 + + #echo "stage_check $*" + # echo "Checking that for each test case, files $1 and $2 are the same" + for c in "$case_dir"/*; do + left_value=$(<"$c/$var1") + right_value=$(<"$c/$var2") + left_pretty="\e[32m$var1\e[0m" + right_pretty="\e[34m$var2\e[0m" + case "$check" in + "==") + if [[ "$left_value" == "$right_value" ]]; then + continue + elif [[ "$action" == "skip" ]]; then + rm -r "$c" + continue + else + echo -e "Property test $property failed: Expected variables $left_pretty and $right_pretty to be the same, but they're not" + fi + ;; + "!=") + if [[ "$left_value" != "$right_value" ]]; then + continue + elif [[ "$action" == "skip" ]]; then + rm -r "$c" + continue + else + echo -e "Property test $property failed: Expected variables $left_pretty and $right_pretty to be different, but they're not" + fi + ;; + *) + echo "stage_check: comparison $check not supported" + exit 1 + ;; + esac + + while read type variable expr; do + if [[ "$var1" == "$variable" ]]; then + variable_text="$left_pretty" + elif [[ "$var2" == "$variable" ]]; then + variable_text="$right_pretty" + else + variable_text="$variable" + fi + echo -e "[$type] $variable_text = $expr =\n $(<"$c/$variable")" + done <"$steps_file" + echo >&2 "To reproduce run: $(realpath --relative-to="$nixpkgs_root" "$script_dir/$script_name") $property $seed" + exit 1 + done +} + + + +for prop in "$script_dir"/props/*; do + property="$(basename "${prop%%.*}")" + if [[ -n "$property_to_test" && "$property" != "$property_to_test" ]]; then + continue + fi + echo >&2 "Running property test $property" + source "$prop" +done diff --git a/lib/path/tests/stage/awk.awk b/lib/path/tests/stage/awk.awk new file mode 100644 index 0000000000000..3d0b4bee86f1d --- /dev/null +++ b/lib/path/tests/stage/awk.awk @@ -0,0 +1,131 @@ +# Return a random array key according to the given weights array. +# The weights need to be normalised, you may use `normalise_weights` for this +# Example: +# weights["a"] = 3 +# weights["b"] = 1 +# normalise_weights(weights) +# gen_weighted(weights) +function gen_weighted(weights, random_value) { + random_value = rand(); + for (key in weights) { + if (random_value < weights[key]) { + return key + } else { + random_value -= weights[key] + } + } +} + +# Normalise an array of weights, scaling them such that they total 1 +# Example: +# weights["a"] = 3 +# weights["b"] = 1 +# normalise_weights(weights) +# print weights["a"] # 0.75 +# print weights["b"] # 0.25 +function normalise_weights(weights, total) { + total=0 + for (key in weights) { + total += weights[key] + } + for (key in weights) { + weights[key] /= total + } +} + +# Generates a random integer between the first (inclusive) and the second (exclusive) argument +# Example: +# gen_int_range(10, 20) # Lowest possible value is 10, highest possible value is 19 +function gen_int_range(a, b) { + return int(a + rand() * (b - a)) +} + +# Generates a random length >= 0 using a geometric distribution. The argument is the expected length +# Example: +# gen_length(10) # Lower numbers are more common, +# # but higher numbers are also possible, +# # the average is 10 +function gen_length(e, p, result) { + # Generates a geometric distribution with p = 1 / e from a uniform distribution + # See https://en.wikipedia.org/wiki/Geometric_distribution#Related_distributions + return int(log(rand()) / log(1 - 1 / e)) +} + +BEGIN { +} + +function gen_valid_subpath() { + return gen_subpath_with("valid", subpath_average_length) +} + +function gen_subpath( desired_type) { + desired_type = gen_weighted(subpath_type_weights) + return gen_subpath_with(desired_type, subpath_average_length) +} + +function gen_subpath_with(desired_type, average_length, left, result) { + + do { + left = gen_length(average_length) + result = "" + while (left > 0) { + switch (gen_weighted(subpath_char_weights)) { + case "ascii": + result = result sprintf("%c", gen_int_range(32, 128)) + break + case "dot": + result = result "." + break + case "slash": + result = result "/" + break + } + left = left - 1 + } + } while (subpath_type(result) != desired_type) + return result +} + +function subpath_type(subpath, components, i) { + if (subpath == "") { + return "invalid/empty" + } else if (substr(subpath, 0, 1) == "/") { + return "invalid/absolute" + } + split(subpath, parts, "/") + for (i in parts) { + if (parts[i] == "..") { + return "invalid/parent" + } + } + return "valid" +} + +function subpath_is_valid(subpath) { + return subpath_type(subpath) == "valid" +} + +function write(file, string) { + # print "Writing string " string " to " case_dir "/" i "/" file + printf "%s", string > case_dir "/" i "/" file +} + +BEGIN { + subpath_char_weights["dot"] = 1 + subpath_char_weights["slash"] = 1 + subpath_char_weights["ascii"] = 2 + normalise_weights(subpath_char_weights) + + subpath_type_weights["valid"] = 200 + subpath_type_weights["invalid/empty"] = 1 + subpath_type_weights["invalid/absolute"] = 5 + subpath_type_weights["invalid/parent"] = 30 + normalise_weights(subpath_type_weights) + + subpath_average_length = 8 + + srand(seed) + for (i = 0; i < count; i++) { + stage() + } +} diff --git a/lib/path/tests/stage/nix-eval.nix b/lib/path/tests/stage/nix-eval.nix new file mode 100644 index 0000000000000..20ec2e059bc33 --- /dev/null +++ b/lib/path/tests/stage/nix-eval.nix @@ -0,0 +1,24 @@ +{ + libPath, + caseDir, + variable, + exprFile, +}: +let + lib = import libPath; + + result = lib.concatMapStringsSep "\n" (case: + let + localCaseDir = caseDir + "/${case}"; + scope = lib.mapAttrs (name: type: + builtins.readFile (localCaseDir + "/${name}") + ) (builtins.readDir localCaseDir) // { + inherit lib; + }; + result = builtins.scopedImport scope exprFile; + in '' + printf "%s" ${lib.escapeShellArg result} > ${lib.escapeShellArg (localCaseDir + "/${variable}")} + '' + ) (lib.attrNames (builtins.readDir caseDir)); + +in result