Merge pull request #256417 from tweag/fileset.trace

`lib.fileset.trace`, `lib.fileset.traceVal`: init

authored by Silvan Mosberger and committed by GitHub 5db719f6 fc66242d

+502 -74
-1
lib/fileset/README.md
··· 212 - > The file set library is currently somewhat limited but is being expanded to include more functions over time. 213 214 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. 213 214 in [the manual](../../doc/functions/fileset.section.md) 215 - 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
+91
lib/fileset/default.nix
··· 6 _coerceMany 7 _toSourceFilter 8 _unionMany 9 ; 10 11 inherit (builtins) 12 isList 13 isPath 14 pathExists 15 typeOf 16 ; 17 ··· 274 _unionMany 275 ]; 276 277 }
··· 6 _coerceMany 7 _toSourceFilter 8 _unionMany 9 + _printFileset 10 ; 11 12 inherit (builtins) 13 isList 14 isPath 15 pathExists 16 + seq 17 typeOf 18 ; 19 ··· 276 _unionMany 277 ]; 278 279 + /* 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 10 typeOf 11 - split 12 ; 13 14 inherit (lib.attrsets) 15 attrValues 16 mapAttrs 17 setAttrByPath ··· 103 ]; 104 105 _noEvalMessage = '' 106 - lib.fileset: Directly evaluating a file set is not supported. Use `lib.fileset.toSource` to turn it into a usable source instead.''; 107 108 # The empty file set without a base path 109 _emptyWithoutBase = { ··· 114 # The one and only! 115 _internalIsEmptyWithoutBase = true; 116 117 - # Double __ to make it be evaluated and ordered first 118 - __noEval = throw _noEvalMessage; 119 }; 120 121 # Create a fileset, see ./README.md#fileset ··· 137 _internalBaseComponents = components parts.subpath; 138 _internalTree = tree; 139 140 - # Double __ to make it be evaluated and ordered first 141 - __noEval = throw _noEvalMessage; 142 }; 143 144 # Coerce a value to a fileset, erroring when the value cannot be coerced. ··· 237 // value; 238 239 /* 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 245 246 Note that this function is strict, it evaluates the entire tree 247 248 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 266 else 267 tree; 268 269 # 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; 277 278 # 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 14 ; 15 16 inherit (lib.attrsets) 17 + attrNames 18 attrValues 19 mapAttrs 20 setAttrByPath ··· 106 ]; 107 108 _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`.''; 112 113 # The empty file set without a base path 114 _emptyWithoutBase = { ··· 119 # The one and only! 120 _internalIsEmptyWithoutBase = true; 121 122 + # 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 }; 127 128 # Create a fileset, see ./README.md#fileset ··· 144 _internalBaseComponents = components parts.subpath; 145 _internalTree = tree; 146 147 + # 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 }; 152 153 # Coerce a value to a fileset, erroring when the value cannot be coerced. ··· 246 // value; 247 248 /* 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 254 255 Note that this function is strict, it evaluates the entire tree 256 257 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; 318 319 + # 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; 388 389 # The base path as a string with a single trailing slash 390 baseString =
+286 -59
lib/fileset/tests.sh
··· 57 expectEqual() { 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" 63 fi 64 - if ! expectedResult=$(nix-instantiate --eval --strict --show-trace \ 65 --expr "$prefixExpression ($expectedExpr)"); then 66 - die "$expectedExpr failed to evaluate, but it was expected to succeed" 67 fi 68 69 if [[ "$actualResult" != "$expectedResult" ]]; then 70 die "$actualExpr should have evaluated to $expectedExpr:\n$expectedResult\n\nbut it evaluated to\n$actualResult" 71 fi 72 } 73 74 # Check that a nix expression evaluates successfully to a store path and returns it (without quotes). ··· 84 crudeUnquoteJSON <<< "$result" 85 } 86 87 - # 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 91 expectFailure() { 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 } 103 104 - # We conditionally use inotifywait in checkFileset. 105 # Check early whether it's available 106 # TODO: Darwin support, though not crucial since we have Linux CI 107 if type inotifywait 2>/dev/null >/dev/null; then 108 - canMonitorFiles=1 109 else 110 - echo "Warning: Not checking that excluded files don't get accessed since inotifywait is not available" >&2 111 - canMonitorFiles= 112 fi 113 114 # 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 122 declare -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 170 171 - # 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") 194 195 - # Remove all files immediately after, triggering delete_self events for all of them 196 - rm -rf -- * 197 198 - # 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 219 220 # 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 - ) 234 235 236 #### Error messages ##### ··· 281 expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` \('"$work"'/a\) does not exist.' 282 283 # 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.' 286 287 # Past versions of the internal representation are supported 288 expectEqual '_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. 502 checkFileset 'unions (mapAttrsToList (name: _: ./. + "/${name}/a") (builtins.readDir ./.))' 503 504 # TODO: Once we have combinators and a property testing library, derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets 505
··· 57 expectEqual() { 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 81 82 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 } 90 91 # Check that a nix expression evaluates successfully to a store path and returns it (without quotes). ··· 101 crudeUnquoteJSON <<< "$result" 102 } 103 104 + # 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 108 expectFailure() { 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 } 120 121 + # 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 153 if type inotifywait 2>/dev/null >/dev/null; then 154 + canMonitor=1 155 else 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= 158 fi 159 160 + # 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 235 declare -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 283 284 expression="toSource { root = ./.; fileset = $fileset; }" 285 286 + # 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 + } 296 297 + # Runs the function while checking that the given excluded files aren't read 298 + storePath=$(withFileMonitor run "${excludedFiles[@]}") 299 300 # 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 + } 316 317 318 #### Error messages ##### ··· 363 expectFailure 'toSource { root = ./.; fileset = ./a; }' 'lib.fileset.toSource: `fileset` \('"$work"'/a\) does not exist.' 364 365 # 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`.' 372 373 # Past versions of the internal representation are supported 374 expectEqual '_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. 588 checkFileset '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 -- * 730 731 # TODO: Once we have combinators and a property testing library, derive property tests from https://en.wikipedia.org/wiki/Algebra_of_sets 732