about summary refs log tree commit diff
path: root/nixpkgs/lib/fileset/tests.sh
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/lib/fileset/tests.sh')
-rwxr-xr-xnixpkgs/lib/fileset/tests.sh350
1 files changed, 350 insertions, 0 deletions
diff --git a/nixpkgs/lib/fileset/tests.sh b/nixpkgs/lib/fileset/tests.sh
new file mode 100755
index 000000000000..9492edf4f55e
--- /dev/null
+++ b/nixpkgs/lib/fileset/tests.sh
@@ -0,0 +1,350 @@
+#!/usr/bin/env bash
+
+# Tests lib.fileset
+# Run:
+# [nixpkgs]$ lib/fileset/tests.sh
+# or:
+# [nixpkgs]$ nix-build lib/tests/release.nix
+
+set -euo pipefail
+shopt -s inherit_errexit dotglob
+
+die() {
+    # The second to last entry contains the line number of the top-level caller
+    lineIndex=$(( ${#BASH_LINENO[@]} - 2 ))
+    echo >&2 -e "test case at ${BASH_SOURCE[0]}:${BASH_LINENO[$lineIndex]} failed:" "$@"
+    exit 1
+}
+
+if test -n "${TEST_LIB:-}"; then
+  NIX_PATH=nixpkgs="$(dirname "$TEST_LIB")"
+else
+  NIX_PATH=nixpkgs="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.."; pwd)"
+fi
+export NIX_PATH
+
+tmp="$(mktemp -d)"
+clean_up() {
+    rm -rf "$tmp"
+}
+trap clean_up EXIT SIGINT SIGTERM
+work="$tmp/work"
+mkdir "$work"
+cd "$work"
+
+# Crudely unquotes a JSON string by just taking everything between the first and the second quote.
+# We're only using this for resulting /nix/store paths, which can't contain " anyways,
+# nor can they contain any other characters that would need to be escaped specially in JSON
+# This way we don't need to add a dependency on e.g. jq
+crudeUnquoteJSON() {
+    cut -d \" -f2
+}
+
+prefixExpression='let
+  lib = import <nixpkgs/lib>;
+  internal = import <nixpkgs/lib/fileset/internal.nix> {
+    inherit lib;
+  };
+in
+with lib;
+with internal;
+with lib.fileset;'
+
+# Check that a nix expression evaluates successfully (strictly, coercing to json, read-write-mode).
+# The expression has `lib.fileset` in scope.
+# If a second argument is provided, the result is checked against it as a regex.
+# Otherwise, the result is output.
+# Usage: expectSuccess NIX [REGEX]
+expectSuccess() {
+    local expr=$1
+    if [[ "$#" -gt 1 ]]; then
+        local expectedResultRegex=$2
+    fi
+    if ! result=$(nix-instantiate --eval --strict --json --read-write-mode --show-trace \
+        --expr "$prefixExpression $expr"); then
+        die "$expr failed to evaluate, but it was expected to succeed"
+    fi
+    if [[ -v expectedResultRegex ]]; then
+        if [[ ! "$result" =~ $expectedResultRegex ]]; then
+            die "$expr should have evaluated to this regex pattern:\n\n$expectedResultRegex\n\nbut this was the actual result:\n\n$result"
+        fi
+    else
+        echo "$result"
+    fi
+}
+
+# Check that a nix expression fails to evaluate (strictly, coercing to json, read-write-mode).
+# And check the received stderr against a regex
+# The expression has `lib.fileset` in scope.
+# Usage: expectFailure NIX REGEX
+expectFailure() {
+    local expr=$1
+    local expectedErrorRegex=$2
+    if result=$(nix-instantiate --eval --strict --json --read-write-mode --show-trace 2>"$tmp/stderr" \
+        --expr "$prefixExpression $expr"); then
+        die "$expr evaluated successfully to $result, but it was expected to fail"
+    fi
+    stderr=$(<"$tmp/stderr")
+    if [[ ! "$stderr" =~ $expectedErrorRegex ]]; then
+        die "$expr should have errored with this regex pattern:\n\n$expectedErrorRegex\n\nbut this was the actual error:\n\n$stderr"
+    fi
+}
+
+# We conditionally use inotifywait in checkFileset.
+# Check early whether it's available
+# TODO: Darwin support, though not crucial since we have Linux CI
+if type inotifywait 2>/dev/null >/dev/null; then
+    canMonitorFiles=1
+else
+    echo "Warning: Not checking that excluded files don't get accessed since inotifywait is not available" >&2
+    canMonitorFiles=
+fi
+
+# Check whether a file set includes/excludes declared paths as expected, usage:
+#
+# tree=(
+#   [a/b] =1  # Declare that file       a/b should exist and expect it to be included in the store path
+#   [c/a] =   # Declare that file       c/a should exist and expect it to be excluded in the store path
+#   [c/d/]=   # Declare that directory c/d/ should exist and expect it to be excluded in the store path
+# )
+# checkFileset './a' # Pass the fileset as the argument
+declare -A tree
+checkFileset() (
+    # New subshell so that we can have a separate trap handler, see `trap` below
+    local fileset=$1
+
+    # Process the tree into separate arrays for included paths, excluded paths and excluded files.
+    # Also create all the paths in the local directory
+    local -a included=()
+    local -a excluded=()
+    local -a excludedFiles=()
+    for p in "${!tree[@]}"; do
+        # If keys end with a `/` we treat them as directories, otherwise files
+        if [[ "$p" =~ /$ ]]; then
+            mkdir -p "$p"
+            isFile=
+        else
+            mkdir -p "$(dirname "$p")"
+            touch "$p"
+            isFile=1
+        fi
+        case "${tree[$p]}" in
+            1)
+                included+=("$p")
+                ;;
+            0)
+                excluded+=("$p")
+                if [[ -n "$isFile" ]]; then
+                    excludedFiles+=("$p")
+                fi
+                ;;
+            *)
+                die "Unsupported tree value: ${tree[$p]}"
+        esac
+    done
+
+    # Start inotifywait in the background to monitor all excluded files (if any)
+    if [[ -n "$canMonitorFiles" ]] && (( "${#excludedFiles[@]}" != 0 )); then
+        coproc watcher {
+            # inotifywait outputs a string on stderr when ready
+            # Redirect it to stdout so we can access it from the coproc's stdout fd
+            # exec so that the coprocess is inotify itself, making the kill below work correctly
+            # See below why we listen to both open and delete_self events
+            exec inotifywait --format='%e %w' --event open,delete_self --monitor "${excludedFiles[@]}" 2>&1
+        }
+        # This will trigger when this subshell exits, no matter if successful or not
+        # After exiting the subshell, the parent shell will continue executing
+        trap 'kill "${watcher_PID}"' exit
+
+        # Synchronously wait until inotifywait is ready
+        while read -r -u "${watcher[0]}" line && [[ "$line" != "Watches established." ]]; do
+            :
+        done
+    fi
+
+    # Call toSource with the fileset, triggering open events for all files that are added to the store
+    expression="toSource { root = ./.; fileset = $fileset; }"
+    # crudeUnquoteJSON is safe because we get back a store path in a string
+    storePath=$(expectSuccess "$expression" | crudeUnquoteJSON)
+
+    # Remove all files immediately after, triggering delete_self events for all of them
+    rm -rf -- *
+
+    # Only check for the inotify events if we actually started inotify earlier
+    if [[ -v watcher ]]; then
+        # Get the first event
+        read -r -u "${watcher[0]}" event file
+
+        # There's only these two possible event timelines:
+        # - open, ..., open, delete_self, ..., delete_self: If some excluded files were read
+        # - delete_self, ..., delete_self: If no excluded files were read
+        # So by looking at the first event we can figure out which one it is!
+        case "$event" in
+            OPEN)
+                die "$expression opened excluded file $file when it shouldn't have"
+                ;;
+            DELETE_SELF)
+                # Expected events
+                ;;
+            *)
+                die "Unexpected event type '$event' on file $file that should be excluded"
+                ;;
+        esac
+    fi
+
+    # For each path that should be included, make sure it does occur in the resulting store path
+    for p in "${included[@]}"; do
+        if [[ ! -e "$storePath/$p" ]]; then
+            die "$expression doesn't include path $p when it should have"
+        fi
+    done
+
+    # For each path that should be excluded, make sure it doesn't occur in the resulting store path
+    for p in "${excluded[@]}"; do
+        if [[ -e "$storePath/$p" ]]; then
+            die "$expression included path $p when it shouldn't have"
+        fi
+    done
+)
+
+
+#### Error messages #####
+
+# Absolute paths in strings cannot be passed as `root`
+expectFailure 'toSource { root = "/nix/store/foobar"; fileset = ./.; }' 'lib.fileset.toSource: `root` "/nix/store/foobar" is a string-like value, but it should be a path instead.
+\s*Paths in strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'
+
+# Only paths are accepted as `root`
+expectFailure 'toSource { root = 10; fileset = ./.; }' 'lib.fileset.toSource: `root` is of type int, but it should be a path instead.'
+
+# Different filesystem roots in root and fileset are not supported
+mkdir -p {foo,bar}/mock-root
+expectFailure 'with ((import <nixpkgs/lib>).extend (import <nixpkgs/lib/fileset/mock-splitRoot.nix>)).fileset;
+  toSource { root = ./foo/mock-root; fileset = ./bar/mock-root; }
+' 'lib.fileset.toSource: Filesystem roots are not the same for `fileset` and `root` "'"$work"'/foo/mock-root":
+\s*`root`: root "'"$work"'/foo/mock-root"
+\s*`fileset`: root "'"$work"'/bar/mock-root"
+\s*Different roots are not supported.'
+rm -rf *
+
+# `root` needs to exist
+expectFailure 'toSource { root = ./a; fileset = ./.; }' 'lib.fileset.toSource: `root` '"$work"'/a does not exist.'
+
+# `root` needs to be a file
+touch a
+expectFailure 'toSource { root = ./a; fileset = ./a; }' 'lib.fileset.toSource: `root` '"$work"'/a is a file, but it should be a directory instead. Potential solutions:
+\s*- If you want to import the file into the store _without_ a containing directory, use string interpolation or `builtins.path` instead of this function.
+\s*- If you want to import the file into the store _with_ a containing directory, set `root` to the containing directory, such as '"$work"', and set `fileset` to the file path.'
+rm -rf *
+
+# Only paths under `root` should be able to influence the result
+mkdir a
+expectFailure 'toSource { root = ./a; fileset = ./.; }' 'lib.fileset.toSource: `fileset` could contain files in '"$work"', which is not under the `root` '"$work"'/a. Potential solutions:
+\s*- Set `root` to '"$work"' or any directory higher up. This changes the layout of the resulting store path.
+\s*- Set `fileset` to a file set that cannot contain files outside the `root` '"$work"'/a. This could change the files included in the result.'
+rm -rf *
+
+# Path coercion only works for paths
+expectFailure 'toSource { root = ./.; fileset = 10; }' 'lib.fileset.toSource: `fileset` is of type int, but it should be a path instead.'
+expectFailure 'toSource { root = ./.; fileset = "/some/path"; }' 'lib.fileset.toSource: `fileset` "/some/path" is a string-like value, but it should be a path instead.
+\s*Paths represented as strings are not supported by `lib.fileset`, use `lib.sources` or derivations instead.'
+
+# Path coercion errors for non-existent paths
+expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` '"$work"'/a does not exist.'
+
+# File sets cannot be evaluated directly
+expectFailure '_create ./. null' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
+
+# Future versions of the internal representation are unsupported
+expectFailure '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 1; }' '<tests>: value is a file set created from a future version of the file set library with a different internal representation:
+\s*- Internal version of the file set: 1
+\s*- Internal version of the library: 0
+\s*Make sure to update your Nixpkgs to have a newer version of `lib.fileset`.'
+
+# _create followed by _coerce should give the inputs back without any validation
+expectSuccess '{
+  inherit (_coerce "<test>" (_create "base" "tree"))
+    _internalVersion _internalBase _internalTree;
+}' '\{"_internalBase":"base","_internalTree":"tree","_internalVersion":0\}'
+
+#### Resulting store path ####
+
+# The store path name should be "source"
+expectSuccess 'toSource { root = ./.; fileset = ./.; }' '"'"${NIX_STORE_DIR:-/nix/store}"'/.*-source"'
+
+# We should be able to import an empty directory and end up with an empty result
+tree=(
+)
+checkFileset './.'
+
+# Directories recursively containing no files are not included
+tree=(
+    [e/]=0
+    [d/e/]=0
+    [d/d/e/]=0
+    [d/d/f]=1
+    [d/f]=1
+    [f]=1
+)
+checkFileset './.'
+
+# Check trees that could cause a naïve string prefix checking implementation to fail
+tree=(
+    [a]=0
+    [ab/x]=0
+    [ab/xy]=1
+    [ab/xyz]=0
+    [abc]=0
+)
+checkFileset './ab/xy'
+
+# Check path coercion examples in ../../doc/functions/fileset.section.md
+tree=(
+    [a/x]=1
+    [a/b/y]=1
+    [c/]=0
+    [c/d/]=0
+)
+checkFileset './.'
+
+tree=(
+    [a/x]=1
+    [a/b/y]=1
+    [c/]=0
+    [c/d/]=0
+)
+checkFileset './a'
+
+tree=(
+    [a/x]=1
+    [a/b/y]=0
+    [c/]=0
+    [c/d/]=0
+)
+checkFileset './a/x'
+
+tree=(
+    [a/x]=0
+    [a/b/y]=1
+    [c/]=0
+    [c/d/]=0
+)
+checkFileset './a/b'
+
+tree=(
+    [a/x]=0
+    [a/b/y]=0
+    [c/]=0
+    [c/d/]=0
+)
+checkFileset './c'
+
+# Test the source filter for the somewhat special case of files in the filesystem root
+# We can't easily test this with the above functions because we can't write to the filesystem root and we don't want to make any assumptions which files are there in the sandbox
+expectSuccess '_toSourceFilter (_create /. null) "/foo" ""' 'false'
+expectSuccess '_toSourceFilter (_create /. { foo = "regular"; }) "/foo" ""' 'true'
+expectSuccess '_toSourceFilter (_create /. { foo = null; }) "/foo" ""' 'false'
+
+# TODO: Once we have combinators and a property testing library, derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets
+
+echo >&2 tests ok