···212- > The file set library is currently somewhat limited but is being expanded to include more functions over time.
213214 in [the manual](../../doc/functions/fileset.section.md)
215-- Once a tracing function exists, `__noEval` in [internal.nix](./internal.nix) should mention it
216- If/Once a function to convert `lib.sources` values into file sets exists, the `_coerce` and `toSource` functions should be updated to mention that function in the error when such a value is passed
217- If/Once a function exists that can optionally include a path depending on whether it exists, the error message for the path not existing in `_coerce` should mention the new function
···212- > The file set library is currently somewhat limited but is being expanded to include more functions over time.
213214 in [the manual](../../doc/functions/fileset.section.md)
0215- If/Once a function to convert `lib.sources` values into file sets exists, the `_coerce` and `toSource` functions should be updated to mention that function in the error when such a value is passed
216- If/Once a function exists that can optionally include a path depending on whether it exists, the error message for the path not existing in `_coerce` should mention the new function
···6 _coerceMany
7 _toSourceFilter
8 _unionMany
9+ _printFileset
10 ;
1112 inherit (builtins)
13 isList
14 isPath
15 pathExists
16+ seq
17 typeOf
18 ;
19···276 _unionMany
277 ];
278279+ /*
280+ Incrementally evaluate and trace a file set in a pretty way.
281+ This function is only intended for debugging purposes.
282+ The exact tracing format is unspecified and may change.
283+284+ This function takes a final argument to return.
285+ In comparison, [`traceVal`](#function-library-lib.fileset.traceVal) returns
286+ the given file set argument.
287+288+ This variant is useful for tracing file sets in the Nix repl.
289+290+ Type:
291+ trace :: FileSet -> Any -> Any
292+293+ Example:
294+ trace (unions [ ./Makefile ./src ./tests/run.sh ]) null
295+ =>
296+ trace: /home/user/src/myProject
297+ trace: - Makefile (regular)
298+ trace: - src (all files in directory)
299+ trace: - tests
300+ trace: - run.sh (regular)
301+ null
302+ */
303+ trace =
304+ /*
305+ The file set to trace.
306+307+ This argument can also be a path,
308+ which gets [implicitly coerced to a file set](#sec-fileset-path-coercion).
309+ */
310+ fileset:
311+ let
312+ # "fileset" would be a better name, but that would clash with the argument name,
313+ # and we cannot change that because of https://github.com/nix-community/nixdoc/issues/76
314+ actualFileset = _coerce "lib.fileset.trace: argument" fileset;
315+ in
316+ seq
317+ (_printFileset actualFileset)
318+ (x: x);
319+320+ /*
321+ Incrementally evaluate and trace a file set in a pretty way.
322+ This function is only intended for debugging purposes.
323+ The exact tracing format is unspecified and may change.
324+325+ This function returns the given file set.
326+ In comparison, [`trace`](#function-library-lib.fileset.trace) takes another argument to return.
327+328+ This variant is useful for tracing file sets passed as arguments to other functions.
329+330+ Type:
331+ traceVal :: FileSet -> FileSet
332+333+ Example:
334+ toSource {
335+ root = ./.;
336+ fileset = traceVal (unions [
337+ ./Makefile
338+ ./src
339+ ./tests/run.sh
340+ ]);
341+ }
342+ =>
343+ trace: /home/user/src/myProject
344+ trace: - Makefile (regular)
345+ trace: - src (all files in directory)
346+ trace: - tests
347+ trace: - run.sh (regular)
348+ "/nix/store/...-source"
349+ */
350+ traceVal =
351+ /*
352+ The file set to trace and return.
353+354+ This argument can also be a path,
355+ which gets [implicitly coerced to a file set](#sec-fileset-path-coercion).
356+ */
357+ fileset:
358+ let
359+ # "fileset" would be a better name, but that would clash with the argument name,
360+ # and we cannot change that because of https://github.com/nix-community/nixdoc/issues/76
361+ actualFileset = _coerce "lib.fileset.traceVal: argument" fileset;
362+ in
363+ seq
364+ (_printFileset actualFileset)
365+ # We could also return the original fileset argument here,
366+ # but that would then duplicate work for consumers of the fileset, because then they have to coerce it again
367+ actualFileset;
368}
+125-14
lib/fileset/internal.nix
···7 isString
8 pathExists
9 readDir
00010 typeOf
11- split
12 ;
1314 inherit (lib.attrsets)
015 attrValues
16 mapAttrs
17 setAttrByPath
···103 ];
104105 _noEvalMessage = ''
106- lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'';
00107108 # The empty file set without a base path
109 _emptyWithoutBase = {
···114 # The one and only!
115 _internalIsEmptyWithoutBase = true;
116117- # Double __ to make it be evaluated and ordered first
118- __noEval = throw _noEvalMessage;
00119 };
120121 # Create a fileset, see ./README.md#fileset
···137 _internalBaseComponents = components parts.subpath;
138 _internalTree = tree;
139140- # Double __ to make it be evaluated and ordered first
141- __noEval = throw _noEvalMessage;
00142 };
143144 # Coerce a value to a fileset, erroring when the value cannot be coerced.
···237 // value;
238239 /*
240- Simplify a filesetTree recursively:
241- - Replace all directories that have no files with `null`
242 This removes directories that would be empty
243- - Replace all directories with all files with `"directory"`
244 This speeds up the source filter function
245246 Note that this function is strict, it evaluates the entire tree
247248 Type: Path -> filesetTree -> filesetTree
249 */
250- _simplifyTree = path: tree:
251 if tree == "directory" || isAttrs tree then
252 let
253 entries = _directoryEntries path tree;
254- simpleSubtrees = mapAttrs (name: _simplifyTree (path + "/${name}")) entries;
255- subtreeValues = attrValues simpleSubtrees;
256 in
257 # This triggers either when all files in a directory are filtered out
258 # Or when the directory doesn't contain any files at all
···262 else if all isString subtreeValues then
263 "directory"
264 else
265- simpleSubtrees
00000000000000000000000000000000000000000266 else
267 tree;
2680000000000000000000000000000000000000000000000000000000000000269 # Turn a fileset into a source filter function suitable for `builtins.path`
270 # Only directories recursively containing at least one files are recursed into
271 # Type: Path -> fileset -> (String -> String -> Bool)
···273 let
274 # Simplify the tree, necessary to make sure all empty directories are null
275 # which has the effect that they aren't included in the result
276- tree = _simplifyTree fileset._internalBase fileset._internalTree;
277278 # The base path as a string with a single trailing slash
279 baseString =
···7 isString
8 pathExists
9 readDir
10+ seq
11+ split
12+ trace
13 typeOf
014 ;
1516 inherit (lib.attrsets)
17+ attrNames
18 attrValues
19 mapAttrs
20 setAttrByPath
···106 ];
107108 _noEvalMessage = ''
109+ lib.fileset: Directly evaluating a file set is not supported.
110+ To turn it into a usable source, use `lib.fileset.toSource`.
111+ To pretty-print the contents, use `lib.fileset.trace` or `lib.fileset.traceVal`.'';
112113 # The empty file set without a base path
114 _emptyWithoutBase = {
···119 # The one and only!
120 _internalIsEmptyWithoutBase = true;
121122+ # Due to alphabetical ordering, this is evaluated last,
123+ # which makes the nix repl output nicer than if it would be ordered first.
124+ # It also allows evaluating it strictly up to this error, which could be useful
125+ _noEval = throw _noEvalMessage;
126 };
127128 # Create a fileset, see ./README.md#fileset
···144 _internalBaseComponents = components parts.subpath;
145 _internalTree = tree;
146147+ # Due to alphabetical ordering, this is evaluated last,
148+ # which makes the nix repl output nicer than if it would be ordered first.
149+ # It also allows evaluating it strictly up to this error, which could be useful
150+ _noEval = throw _noEvalMessage;
151 };
152153 # Coerce a value to a fileset, erroring when the value cannot be coerced.
···246 // value;
247248 /*
249+ A normalisation of a filesetTree suitable filtering with `builtins.path`:
250+ - Replace all directories that have no files with `null`.
251 This removes directories that would be empty
252+ - Replace all directories with all files with `"directory"`.
253 This speeds up the source filter function
254255 Note that this function is strict, it evaluates the entire tree
256257 Type: Path -> filesetTree -> filesetTree
258 */
259+ _normaliseTreeFilter = path: tree:
260 if tree == "directory" || isAttrs tree then
261 let
262 entries = _directoryEntries path tree;
263+ normalisedSubtrees = mapAttrs (name: _normaliseTreeFilter (path + "/${name}")) entries;
264+ subtreeValues = attrValues normalisedSubtrees;
265 in
266 # This triggers either when all files in a directory are filtered out
267 # Or when the directory doesn't contain any files at all
···271 else if all isString subtreeValues then
272 "directory"
273 else
274+ normalisedSubtrees
275+ else
276+ tree;
277+278+ /*
279+ A minimal normalisation of a filesetTree, intended for pretty-printing:
280+ - If all children of a path are recursively included or empty directories, the path itself is also recursively included
281+ - If all children of a path are fully excluded or empty directories, the path itself is an empty directory
282+ - Other empty directories are represented with the special "emptyDir" string
283+ While these could be replaced with `null`, that would take another mapAttrs
284+285+ Note that this function is partially lazy.
286+287+ Type: Path -> filesetTree -> filesetTree (with "emptyDir"'s)
288+ */
289+ _normaliseTreeMinimal = path: tree:
290+ if tree == "directory" || isAttrs tree then
291+ let
292+ entries = _directoryEntries path tree;
293+ normalisedSubtrees = mapAttrs (name: _normaliseTreeMinimal (path + "/${name}")) entries;
294+ subtreeValues = attrValues normalisedSubtrees;
295+ in
296+ # If there are no entries, or all entries are empty directories, return "emptyDir".
297+ # After this branch we know that there's at least one file
298+ if all (value: value == "emptyDir") subtreeValues then
299+ "emptyDir"
300+301+ # If all subtrees are fully included or empty directories
302+ # (both of which are coincidentally represented as strings), return "directory".
303+ # This takes advantage of the fact that empty directories can be represented as included directories.
304+ # Note that the tree == "directory" check allows avoiding recursion
305+ else if tree == "directory" || all (value: isString value) subtreeValues then
306+ "directory"
307+308+ # If all subtrees are fully excluded or empty directories, return null.
309+ # This takes advantage of the fact that empty directories can be represented as excluded directories
310+ else if all (value: isNull value || value == "emptyDir") subtreeValues then
311+ null
312+313+ # Mix of included and excluded entries
314+ else
315+ normalisedSubtrees
316 else
317 tree;
318319+ # Trace a filesetTree in a pretty way when the resulting value is evaluated.
320+ # This can handle both normal filesetTree's, and ones returned from _normaliseTreeMinimal
321+ # Type: Path -> filesetTree (with "emptyDir"'s) -> Null
322+ _printMinimalTree = base: tree:
323+ let
324+ treeSuffix = tree:
325+ if isAttrs tree then
326+ ""
327+ else if tree == "directory" then
328+ " (all files in directory)"
329+ else
330+ # This does "leak" the file type strings of the internal representation,
331+ # but this is the main reason these file type strings even are in the representation!
332+ # TODO: Consider removing that information from the internal representation for performance.
333+ # The file types can still be printed by querying them only during tracing
334+ " (${tree})";
335+336+ # Only for attribute set trees
337+ traceTreeAttrs = prevLine: indent: tree:
338+ foldl' (prevLine: name:
339+ let
340+ subtree = tree.${name};
341+342+ # Evaluating this prints the line for this subtree
343+ thisLine =
344+ trace "${indent}- ${name}${treeSuffix subtree}" prevLine;
345+ in
346+ if subtree == null || subtree == "emptyDir" then
347+ # Don't print anything at all if this subtree is empty
348+ prevLine
349+ else if isAttrs subtree then
350+ # A directory with explicit entries
351+ # Do print this node, but also recurse
352+ traceTreeAttrs thisLine "${indent} " subtree
353+ else
354+ # Either a file, or a recursively included directory
355+ # Do print this node but no further recursion needed
356+ thisLine
357+ ) prevLine (attrNames tree);
358+359+ # Evaluating this will print the first line
360+ firstLine =
361+ if tree == null || tree == "emptyDir" then
362+ trace "(empty)" null
363+ else
364+ trace "${toString base}${treeSuffix tree}" null;
365+ in
366+ if isAttrs tree then
367+ traceTreeAttrs firstLine "" tree
368+ else
369+ firstLine;
370+371+ # Pretty-print a file set in a pretty way when the resulting value is evaluated
372+ # Type: fileset -> Null
373+ _printFileset = fileset:
374+ if fileset._internalIsEmptyWithoutBase then
375+ trace "(empty)" null
376+ else
377+ _printMinimalTree fileset._internalBase
378+ (_normaliseTreeMinimal fileset._internalBase fileset._internalTree);
379+380 # Turn a fileset into a source filter function suitable for `builtins.path`
381 # Only directories recursively containing at least one files are recursed into
382 # Type: Path -> fileset -> (String -> String -> Bool)
···384 let
385 # Simplify the tree, necessary to make sure all empty directories are null
386 # which has the effect that they aren't included in the result
387+ tree = _normaliseTreeFilter fileset._internalBase fileset._internalTree;
388389 # The base path as a string with a single trailing slash
390 baseString =
+286-59
lib/fileset/tests.sh
···57expectEqual() {
58 local actualExpr=$1
59 local expectedExpr=$2
60- if ! actualResult=$(nix-instantiate --eval --strict --show-trace \
61 --expr "$prefixExpression ($actualExpr)"); then
62- die "$actualExpr failed to evaluate, but it was expected to succeed"
0063 fi
64- if ! expectedResult=$(nix-instantiate --eval --strict --show-trace \
0065 --expr "$prefixExpression ($expectedExpr)"); then
66- die "$expectedExpr failed to evaluate, but it was expected to succeed"
00000000067 fi
6869 if [[ "$actualResult" != "$expectedResult" ]]; then
70 die "$actualExpr should have evaluated to $expectedExpr:\n$expectedResult\n\nbut it evaluated to\n$actualResult"
71 fi
000072}
7374# Check that a nix expression evaluates successfully to a store path and returns it (without quotes).
···84 crudeUnquoteJSON <<< "$result"
85}
8687-# Check that a nix expression fails to evaluate (strictly, coercing to json, read-write-mode).
88# And check the received stderr against a regex
89# The expression has `lib.fileset` in scope.
90# Usage: expectFailure NIX REGEX
91expectFailure() {
92 local expr=$1
93 local expectedErrorRegex=$2
94- if result=$(nix-instantiate --eval --strict --json --read-write-mode --show-trace 2>"$tmp/stderr" \
95 --expr "$prefixExpression $expr"); then
96 die "$expr evaluated successfully to $result, but it was expected to fail"
97 fi
···101 fi
102}
103104-# We conditionally use inotifywait in checkFileset.
00000000000000000000000000000105# Check early whether it's available
106# TODO: Darwin support, though not crucial since we have Linux CI
107if type inotifywait 2>/dev/null >/dev/null; then
108- canMonitorFiles=1
109else
110- echo "Warning: Not checking that excluded files don't get accessed since inotifywait is not available" >&2
111- canMonitorFiles=
112fi
1130000000000000000000000000000000000000000000000000000000000000000000114# Check whether a file set includes/excludes declared paths as expected, usage:
115#
116# tree=(
···120# )
121# checkFileset './a' # Pass the fileset as the argument
122declare -A tree
123-checkFileset() (
124 # New subshell so that we can have a separate trap handler, see `trap` below
125 local fileset=$1
126···168 touch "${filesToCreate[@]}"
169 fi
170171- # Start inotifywait in the background to monitor all excluded files (if any)
172- if [[ -n "$canMonitorFiles" ]] && (( "${#excludedFiles[@]}" != 0 )); then
173- coproc watcher {
174- # inotifywait outputs a string on stderr when ready
175- # Redirect it to stdout so we can access it from the coproc's stdout fd
176- # exec so that the coprocess is inotify itself, making the kill below work correctly
177- # See below why we listen to both open and delete_self events
178- exec inotifywait --format='%e %w' --event open,delete_self --monitor "${excludedFiles[@]}" 2>&1
179- }
180- # This will trigger when this subshell exits, no matter if successful or not
181- # After exiting the subshell, the parent shell will continue executing
182- # shellcheck disable=SC2154
183- trap 'kill "${watcher_PID}"' exit
184-185- # Synchronously wait until inotifywait is ready
186- while read -r -u "${watcher[0]}" line && [[ "$line" != "Watches established." ]]; do
187- :
188- done
189- fi
190-191- # Call toSource with the fileset, triggering open events for all files that are added to the store
192 expression="toSource { root = ./.; fileset = $fileset; }"
193- storePath=$(expectStorePath "$expression")
194195- # Remove all files immediately after, triggering delete_self events for all of them
196- rm -rf -- *
00000000197198- # Only check for the inotify events if we actually started inotify earlier
199- if [[ -v watcher ]]; then
200- # Get the first event
201- read -r -u "${watcher[0]}" event file
202-203- # There's only these two possible event timelines:
204- # - open, ..., open, delete_self, ..., delete_self: If some excluded files were read
205- # - delete_self, ..., delete_self: If no excluded files were read
206- # So by looking at the first event we can figure out which one it is!
207- case "$event" in
208- OPEN)
209- die "$expression opened excluded file $file when it shouldn't have"
210- ;;
211- DELETE_SELF)
212- # Expected events
213- ;;
214- *)
215- die "Unexpected event type '$event' on file $file that should be excluded"
216- ;;
217- esac
218- fi
219220 # For each path that should be included, make sure it does occur in the resulting store path
221 for p in "${included[@]}"; do
···230 die "$expression included path $p when it shouldn't have"
231 fi
232 done
233-)
00234235236#### Error messages #####
···281expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` \('"$work"'/a\) does not exist.'
282283# File sets cannot be evaluated directly
284-expectFailure 'union ./. ./.' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
285-expectFailure '_emptyWithoutBase' 'lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.'
0000286287# Past versions of the internal representation are supported
288expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 0; _internalBase = ./.; }' \
···500# Meanwhile, the test infra here is not the fastest, creating 10000 would be too slow.
501# So, just using 1000 files for now.
502checkFileset 'unions (mapAttrsToList (name: _: ./. + "/${name}/a") (builtins.readDir ./.))'
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000503504# TODO: Once we have combinators and a property testing library, derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets
505
···57expectEqual() {
58 local actualExpr=$1
59 local expectedExpr=$2
60+ if actualResult=$(nix-instantiate --eval --strict --show-trace 2>"$tmp"/actualStderr \
61 --expr "$prefixExpression ($actualExpr)"); then
62+ actualExitCode=$?
63+ else
64+ actualExitCode=$?
65 fi
66+ actualStderr=$(< "$tmp"/actualStderr)
67+68+ if expectedResult=$(nix-instantiate --eval --strict --show-trace 2>"$tmp"/expectedStderr \
69 --expr "$prefixExpression ($expectedExpr)"); then
70+ expectedExitCode=$?
71+ else
72+ expectedExitCode=$?
73+ fi
74+ expectedStderr=$(< "$tmp"/expectedStderr)
75+76+ if [[ "$actualExitCode" != "$expectedExitCode" ]]; then
77+ echo "$actualStderr" >&2
78+ echo "$actualResult" >&2
79+ die "$actualExpr should have exited with $expectedExitCode, but it exited with $actualExitCode"
80 fi
8182 if [[ "$actualResult" != "$expectedResult" ]]; then
83 die "$actualExpr should have evaluated to $expectedExpr:\n$expectedResult\n\nbut it evaluated to\n$actualResult"
84 fi
85+86+ if [[ "$actualStderr" != "$expectedStderr" ]]; then
87+ die "$actualExpr should have had this on stderr:\n$expectedStderr\n\nbut it was\n$actualStderr"
88+ fi
89}
9091# Check that a nix expression evaluates successfully to a store path and returns it (without quotes).
···101 crudeUnquoteJSON <<< "$result"
102}
103104+# Check that a nix expression fails to evaluate (strictly, read-write-mode).
105# And check the received stderr against a regex
106# The expression has `lib.fileset` in scope.
107# Usage: expectFailure NIX REGEX
108expectFailure() {
109 local expr=$1
110 local expectedErrorRegex=$2
111+ if result=$(nix-instantiate --eval --strict --read-write-mode --show-trace 2>"$tmp/stderr" \
112 --expr "$prefixExpression $expr"); then
113 die "$expr evaluated successfully to $result, but it was expected to fail"
114 fi
···118 fi
119}
120121+# Check that the traces of a Nix expression are as expected when evaluated.
122+# The expression has `lib.fileset` in scope.
123+# Usage: expectTrace NIX STR
124+expectTrace() {
125+ local expr=$1
126+ local expectedTrace=$2
127+128+ nix-instantiate --eval --show-trace >/dev/null 2>"$tmp"/stderrTrace \
129+ --expr "$prefixExpression trace ($expr)" || true
130+131+ actualTrace=$(sed -n 's/^trace: //p' "$tmp/stderrTrace")
132+133+ nix-instantiate --eval --show-trace >/dev/null 2>"$tmp"/stderrTraceVal \
134+ --expr "$prefixExpression traceVal ($expr)" || true
135+136+ actualTraceVal=$(sed -n 's/^trace: //p' "$tmp/stderrTraceVal")
137+138+ # Test that traceVal returns the same trace as trace
139+ if [[ "$actualTrace" != "$actualTraceVal" ]]; then
140+ cat "$tmp"/stderrTrace >&2
141+ die "$expr traced this for lib.fileset.trace:\n\n$actualTrace\n\nand something different for lib.fileset.traceVal:\n\n$actualTraceVal"
142+ fi
143+144+ if [[ "$actualTrace" != "$expectedTrace" ]]; then
145+ cat "$tmp"/stderrTrace >&2
146+ die "$expr should have traced this:\n\n$expectedTrace\n\nbut this was actually traced:\n\n$actualTrace"
147+ fi
148+}
149+150+# We conditionally use inotifywait in withFileMonitor.
151# Check early whether it's available
152# TODO: Darwin support, though not crucial since we have Linux CI
153if type inotifywait 2>/dev/null >/dev/null; then
154+ canMonitor=1
155else
156+ echo "Warning: Cannot check for paths not getting read since the inotifywait command (from the inotify-tools package) is not available" >&2
157+ canMonitor=
158fi
159160+# Run a function while monitoring that it doesn't read certain paths
161+# Usage: withFileMonitor FUNNAME PATH...
162+# - FUNNAME should be a bash function that:
163+# - Performs some operation that should not read some paths
164+# - Delete the paths it shouldn't read without triggering any open events
165+# - PATH... are the paths that should not get read
166+#
167+# This function outputs the same as FUNNAME
168+withFileMonitor() {
169+ local funName=$1
170+ shift
171+172+ # If we can't monitor files or have none to monitor, just run the function directly
173+ if [[ -z "$canMonitor" ]] || (( "$#" == 0 )); then
174+ "$funName"
175+ else
176+177+ # Use a subshell to start the coprocess in and use a trap to kill it when exiting the subshell
178+ (
179+ # Assigned by coproc, makes shellcheck happy
180+ local watcher watcher_PID
181+182+ # Start inotifywait in the background to monitor all excluded paths
183+ coproc watcher {
184+ # inotifywait outputs a string on stderr when ready
185+ # Redirect it to stdout so we can access it from the coproc's stdout fd
186+ # exec so that the coprocess is inotify itself, making the kill below work correctly
187+ # See below why we listen to both open and delete_self events
188+ exec inotifywait --format='%e %w' --event open,delete_self --monitor "$@" 2>&1
189+ }
190+191+ # This will trigger when this subshell exits, no matter if successful or not
192+ # After exiting the subshell, the parent shell will continue executing
193+ trap 'kill "${watcher_PID}"' exit
194+195+ # Synchronously wait until inotifywait is ready
196+ while read -r -u "${watcher[0]}" line && [[ "$line" != "Watches established." ]]; do
197+ :
198+ done
199+200+ # Call the function that should not read the given paths and delete them afterwards
201+ "$funName"
202+203+ # Get the first event
204+ read -r -u "${watcher[0]}" event file
205+206+ # With funName potentially reading files first before deleting them,
207+ # there's only these two possible event timelines:
208+ # - open*, ..., open*, delete_self, ..., delete_self: If some excluded paths were read
209+ # - delete_self, ..., delete_self: If no excluded paths were read
210+ # So by looking at the first event we can figure out which one it is!
211+ # This also means we don't have to wait to collect all events.
212+ case "$event" in
213+ OPEN*)
214+ die "$funName opened excluded file $file when it shouldn't have"
215+ ;;
216+ DELETE_SELF)
217+ # Expected events
218+ ;;
219+ *)
220+ die "During $funName, Unexpected event type '$event' on file $file that should be excluded"
221+ ;;
222+ esac
223+ )
224+ fi
225+}
226+227# Check whether a file set includes/excludes declared paths as expected, usage:
228#
229# tree=(
···233# )
234# checkFileset './a' # Pass the fileset as the argument
235declare -A tree
236+checkFileset() {
237 # New subshell so that we can have a separate trap handler, see `trap` below
238 local fileset=$1
239···281 touch "${filesToCreate[@]}"
282 fi
283000000000000000000000284 expression="toSource { root = ./.; fileset = $fileset; }"
0285286+ # We don't have lambda's in bash unfortunately,
287+ # so we just define a function instead and then pass its name
288+ # shellcheck disable=SC2317
289+ run() {
290+ # Call toSource with the fileset, triggering open events for all files that are added to the store
291+ expectStorePath "$expression"
292+ if (( ${#excludedFiles[@]} != 0 )); then
293+ rm "${excludedFiles[@]}"
294+ fi
295+ }
296297+ # Runs the function while checking that the given excluded files aren't read
298+ storePath=$(withFileMonitor run "${excludedFiles[@]}")
0000000000000000000299300 # For each path that should be included, make sure it does occur in the resulting store path
301 for p in "${included[@]}"; do
···310 die "$expression included path $p when it shouldn't have"
311 fi
312 done
313+314+ rm -rf -- *
315+}
316317318#### Error messages #####
···363expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` \('"$work"'/a\) does not exist.'
364365# File sets cannot be evaluated directly
366+expectFailure 'union ./. ./.' 'lib.fileset: Directly evaluating a file set is not supported.
367+\s*To turn it into a usable source, use `lib.fileset.toSource`.
368+\s*To pretty-print the contents, use `lib.fileset.trace` or `lib.fileset.traceVal`.'
369+expectFailure '_emptyWithoutBase' 'lib.fileset: Directly evaluating a file set is not supported.
370+\s*To turn it into a usable source, use `lib.fileset.toSource`.
371+\s*To pretty-print the contents, use `lib.fileset.trace` or `lib.fileset.traceVal`.'
372373# Past versions of the internal representation are supported
374expectEqual '_coerce "<tests>: value" { _type = "fileset"; _internalVersion = 0; _internalBase = ./.; }' \
···586# Meanwhile, the test infra here is not the fastest, creating 10000 would be too slow.
587# So, just using 1000 files for now.
588checkFileset 'unions (mapAttrsToList (name: _: ./. + "/${name}/a") (builtins.readDir ./.))'
589+590+## Tracing
591+592+# The second trace argument is returned
593+expectEqual 'trace ./. "some value"' 'builtins.trace "(empty)" "some value"'
594+595+# The fileset traceVal argument is returned
596+expectEqual 'traceVal ./.' 'builtins.trace "(empty)" (_create ./. "directory")'
597+598+# The tracing happens before the final argument is needed
599+expectEqual 'trace ./.' 'builtins.trace "(empty)" (x: x)'
600+601+# Tracing an empty directory shows it as such
602+expectTrace './.' '(empty)'
603+604+# This also works if there are directories, but all recursively without files
605+mkdir -p a/b/c
606+expectTrace './.' '(empty)'
607+rm -rf -- *
608+609+# The empty file set without a base also prints as empty
610+expectTrace '_emptyWithoutBase' '(empty)'
611+expectTrace 'unions [ ]' '(empty)'
612+613+# If a directory is fully included, print it as such
614+touch a
615+expectTrace './.' "$work"' (all files in directory)'
616+rm -rf -- *
617+618+# If a directory is not fully included, recurse
619+mkdir a b
620+touch a/{x,y} b/{x,y}
621+expectTrace 'union ./a/x ./b' "$work"'
622+- a
623+ - x (regular)
624+- b (all files in directory)'
625+rm -rf -- *
626+627+# If an included path is a file, print its type
628+touch a x
629+ln -s a b
630+mkfifo c
631+expectTrace 'unions [ ./a ./b ./c ]' "$work"'
632+- a (regular)
633+- b (symlink)
634+- c (unknown)'
635+rm -rf -- *
636+637+# Do not print directories without any files recursively
638+mkdir -p a/b/c
639+touch b x
640+expectTrace 'unions [ ./a ./b ]' "$work"'
641+- b (regular)'
642+rm -rf -- *
643+644+# If all children are either fully included or empty directories,
645+# the parent should be printed as fully included
646+touch a
647+mkdir b
648+expectTrace 'union ./a ./b' "$work"' (all files in directory)'
649+rm -rf -- *
650+651+mkdir -p x/b x/c
652+touch x/a
653+touch a
654+# If all children are either fully excluded or empty directories,
655+# the parent should be shown (or rather not shown) as fully excluded
656+expectTrace 'unions [ ./a ./x/b ./x/c ]' "$work"'
657+- a (regular)'
658+rm -rf -- *
659+660+# Completely filtered out directories also print as empty
661+touch a
662+expectTrace '_create ./. {}' '(empty)'
663+rm -rf -- *
664+665+# A general test to make sure the resulting format makes sense
666+# Such as indentation and ordering
667+mkdir -p bar/{qux,someDir}
668+touch bar/{baz,qux,someDir/a} foo
669+touch bar/qux/x
670+ln -s x bar/qux/a
671+mkfifo bar/qux/b
672+expectTrace 'unions [
673+ ./bar/baz
674+ ./bar/qux/a
675+ ./bar/qux/b
676+ ./bar/someDir/a
677+ ./foo
678+]' "$work"'
679+- bar
680+ - baz (regular)
681+ - qux
682+ - a (symlink)
683+ - b (unknown)
684+ - someDir (all files in directory)
685+- foo (regular)'
686+rm -rf -- *
687+688+# For recursively included directories,
689+# `(all files in directory)` should only be used if there's at least one file (otherwise it would be `(empty)`)
690+# and this should be determined without doing a full search
691+#
692+# a is intentionally ordered first here in order to allow triggering the short-circuit behavior
693+# We then check that b is not read
694+# In a more realistic scenario, some directories might need to be recursed into,
695+# but a file would be quickly found to trigger the short-circuit.
696+touch a
697+mkdir b
698+# We don't have lambda's in bash unfortunately,
699+# so we just define a function instead and then pass its name
700+# shellcheck disable=SC2317
701+run() {
702+ # This shouldn't read b/
703+ expectTrace './.' "$work"' (all files in directory)'
704+ # Remove all files immediately after, triggering delete_self events for all of them
705+ rmdir b
706+}
707+# Runs the function while checking that b isn't read
708+withFileMonitor run b
709+rm -rf -- *
710+711+# Partially included directories trace entries as they are evaluated
712+touch a b c
713+expectTrace '_create ./. { a = null; b = "regular"; c = throw "b"; }' "$work"'
714+- b (regular)'
715+716+# Except entries that need to be evaluated to even figure out if it's only partially included:
717+# Here the directory could be fully excluded or included just from seeing a and b,
718+# so c needs to be evaluated before anything can be traced
719+expectTrace '_create ./. { a = null; b = null; c = throw "c"; }' ''
720+expectTrace '_create ./. { a = "regular"; b = "regular"; c = throw "c"; }' ''
721+rm -rf -- *
722+723+# We can trace large directories (10000 here) without any problems
724+filesToCreate=({0..9}{0..9}{0..9}{0..9})
725+expectedTrace=$work$'\n'$(printf -- '- %s (regular)\n' "${filesToCreate[@]}")
726+# We need an excluded file so it doesn't print as `(all files in directory)`
727+touch 0 "${filesToCreate[@]}"
728+expectTrace 'unions (mapAttrsToList (n: _: ./. + "/${n}") (removeAttrs (builtins.readDir ./.) [ "0" ]))' "$expectedTrace"
729+rm -rf -- *
730731# TODO: Once we have combinators and a property testing library, derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets
732