@bats-hardened/bats 1.14.1-alpha02

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @bats-hardened/bats might be problematic. Click here for more details.

@@ -0,0 +1,555 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ export BATS_VERSION='1.14.1-alpha02'
5
+ VALID_FORMATTERS="pretty, junit, tap, tap13"
6
+
7
+ version() {
8
+ printf 'Bats %s\n' "$BATS_VERSION"
9
+ }
10
+
11
+ abort() {
12
+ local print_usage=1
13
+ if [[ ${1:-} == --no-print-usage ]]; then
14
+ print_usage=
15
+ shift
16
+ fi
17
+ printf 'Error: %s\n' "$1" >&2
18
+ if [[ -n $print_usage ]]; then
19
+ usage >&2
20
+ fi
21
+ exit 1
22
+ }
23
+
24
+ usage() {
25
+ local cmd="${0##*/}"
26
+
27
+ cat <<HELP_TEXT_HEADER
28
+ Usage: ${cmd} [OPTIONS] <tests>
29
+ ${cmd} [-h | -v]
30
+
31
+ HELP_TEXT_HEADER
32
+
33
+ cat <<'HELP_TEXT_BODY'
34
+ <tests> is the path to a Bats test file, or the path to a directory
35
+ containing Bats test files (ending with ".bats")
36
+
37
+ --abort Stop execution of suite on first failed test
38
+ --allow-empty-suite Exit with code 0 (instead of the default code 1) when no tests are found.
39
+ Can also be set via $BATS_ALLOW_EMPTY_SUITE
40
+ -c, --count Count test cases without running any tests
41
+ --code-quote-style <style>
42
+ A two character string of code quote delimiters
43
+ or 'custom' which requires setting $BATS_BEGIN_CODE_QUOTE and
44
+ $BATS_END_CODE_QUOTE. Can also be set via $BATS_CODE_QUOTE_STYLE
45
+ --line-reference-format Controls how file/line references e.g. in stack traces are printed:
46
+ - comma_line (default): a.bats, line 1
47
+ - colon: a.bats:1
48
+ - uri: file:///tests/a.bats:1
49
+ - custom: provide your own via defining bats_format_file_line_reference_custom
50
+ with parameters <filename> <line>, store via `printf -v "$output"`
51
+ -f, --filter <regex> Only run tests that match the regular expression
52
+ --negative-filter <regex> Only run tests that do not match the regular expression
53
+ --filter-status <status> Only run tests with the given status in the last completed (no CTRL+C/SIGINT) run.
54
+ Valid <status> values are:
55
+ failed - runs tests that failed or were not present in the last run
56
+ missed - runs tests that were not present in the last run
57
+ --filter-tags <comma-separated-tag-list>
58
+ Only run tests that match all the tags in the list (&&).
59
+ You can negate a tag via prepending '!'.
60
+ Specifying this flag multiple times allows for logical or (||):
61
+ `--filter-tags A,B --filter-tags A,!C` matches tags (A && B) || (A && !C)
62
+ -F, --formatter <type> Switch between formatters: pretty (default),
63
+ tap (default w/o term), tap13, junit, /<absolute path to formatter>
64
+ --gather-test-outputs-in <directory>
65
+ Gather the output of failing *and* passing tests
66
+ as files in directory (if existing, must be empty)
67
+ -h, --help Display this help message
68
+ -j, --jobs <jobs> Number of parallel jobs (requires GNU parallel or shenwei356/rush)
69
+ --parallel-binary-name Name of parallel binary
70
+ --no-tempdir-cleanup Preserve test output temporary directory
71
+ --errexit Enable errexit (set -e) for commands run in `run`
72
+ --no-parallelize-across-files
73
+ Serialize test file execution instead of running
74
+ them in parallel (requires --jobs >1)
75
+ --no-parallelize-within-files
76
+ Serialize test execution within files instead of
77
+ running them in parallel (requires --jobs >1)
78
+ --report-formatter <type> Switch between reporters (same options as --formatter)
79
+ -o, --output <dir> Directory to write report files (must exist)
80
+ -p, --pretty Shorthand for "--formatter pretty"
81
+ --print-output-on-failure Automatically print the value of `$output` on failed tests
82
+ -r, --recursive Include tests in subdirectories
83
+ --show-output-of-passing-tests
84
+ Print output of passing tests
85
+ -t, --tap Shorthand for "--formatter tap"
86
+ -T, --timing Add timing information to tests
87
+ -x, --trace Print test commands as they are executed (like `set -x`)
88
+ --verbose-run Make `run` print `$output` by default
89
+ -v, --version Display the version number
90
+
91
+ For more information, see https://github.com/bats-core/bats-core
92
+ HELP_TEXT_BODY
93
+ }
94
+
95
+ expand_path() {
96
+ if [[ -z "$1" ]]; then
97
+ abort --no-print-usage "Path is empty string."
98
+ fi
99
+
100
+ local path="${1%/}"
101
+ local dirname="${path%/*}"
102
+ if [[ -z "$dirname" ]]; then
103
+ dirname=/
104
+ fi
105
+ local result="$2"
106
+ local OLDPWD="$PWD"
107
+
108
+ if [[ "$dirname" == "$path" ]]; then
109
+ dirname="$PWD"
110
+ else
111
+ cd "$dirname"
112
+ dirname="$PWD"
113
+ cd "$OLDPWD"
114
+ fi
115
+ printf -v "$result" '%s/%s' "$dirname" "${path##*/}"
116
+ }
117
+
118
+ BATS_LIBEXEC="$(
119
+ cd "$(dirname "$(bats_readlinkf "${BASH_SOURCE[0]}")")"
120
+ pwd
121
+ )"
122
+ export BATS_LIBEXEC
123
+ export BATS_CWD="$PWD"
124
+ export PATH="$BATS_LIBEXEC:$PATH"
125
+ export BATS_ROOT_PID=$$
126
+ export BATS_TMPDIR="${TMPDIR:-/tmp}"
127
+ BATS_TMPDIR=${BATS_TMPDIR%/} # chop off trailing / to avoid duplication
128
+ export BATS_RUN_TMPDIR=
129
+ export BATS_GUARANTEED_MINIMUM_VERSION=0.0.0
130
+ export BATS_LIB_PATH=${BATS_LIB_PATH-/usr/lib/bats}
131
+ BATS_REPORT_OUTPUT_DIR=${BATS_REPORT_OUTPUT_DIR-.}
132
+ export BATS_LINE_REFERENCE_FORMAT=${BATS_LINE_REFERENCE_FORMAT-comma_line}
133
+
134
+ if [[ ! -d "${BATS_TMPDIR}" ]]; then
135
+ printf "Error: BATS_TMPDIR (%s) does not exist or is not a directory" "${BATS_TMPDIR}" >&2
136
+ exit 1
137
+ elif [[ ! -w "${BATS_TMPDIR}" ]]; then
138
+ printf "Error: BATS_TMPDIR (%s) is not writable" "${BATS_TMPDIR}" >&2
139
+ exit 1
140
+ fi
141
+
142
+ arguments=()
143
+
144
+ # list of single char options that don't expect a value
145
+ single_char_flags="hvcprtTx"
146
+
147
+ # Unpack single-character options bundled together, e.g. -cr, -pr.
148
+ for arg in "$@"; do
149
+ if [[ "$arg" =~ ^-[^-]. ]]; then
150
+ index=1
151
+ while option="${arg:$((index++)):1}"; do
152
+ if [[ -z "$option" ]]; then
153
+ break
154
+ fi
155
+ if [[ "$single_char_flags" != *$option* && -n "${arg:index}" ]]; then
156
+ printf "Error: -%s is not allowed within pack of flags.\n" "$option"
157
+ printf " Please put it last (e.g. \`-rF junit\` instead of \`-Fr junit\`), or on its own (\`-r -F junit\`)!\n"
158
+ exit 1
159
+ fi
160
+ arguments+=("-$option")
161
+ done
162
+ else
163
+ arguments+=("$arg")
164
+ fi
165
+ shift
166
+ done
167
+
168
+ set -- "${arguments[@]}"
169
+ arguments=()
170
+
171
+ unset flags recursive formatter_flags
172
+ flags=('--dummy-flag') # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
173
+ formatter_flags=('--dummy-flag') # add a dummy flag to prevent unset variable errors on empty array expansion in old bash versions
174
+ if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then
175
+ default_formatter=pretty
176
+ else
177
+ default_formatter='tap'
178
+ fi
179
+ formatter=${BATS_FORMATTER:-$default_formatter}
180
+ report_formatter=''
181
+ recursive=
182
+ setup_suite_file=''
183
+ export BATS_TEMPDIR_CLEANUP=1
184
+
185
+ while [[ "$#" -ne 0 ]]; do
186
+ case "$1" in
187
+ -h | --help)
188
+ version
189
+ usage
190
+ exit 0
191
+ ;;
192
+ -v | --version)
193
+ version
194
+ exit 0
195
+ ;;
196
+ -c | --count)
197
+ flags+=('-c')
198
+ ;;
199
+ --allow-empty-suite)
200
+ flags+=(--allow-empty-suite)
201
+ ;;
202
+ -f | --filter)
203
+ shift
204
+ flags+=('-f' "$1")
205
+ ;;
206
+ --negative-filter)
207
+ shift
208
+ flags+=('--negative-filter' "$1")
209
+ ;;
210
+ -F | --formatter)
211
+ shift
212
+ # allow cat formatter to see extended output but don't advertise to users
213
+ if [[ $1 =~ ^(pretty|junit|tap|tap13|cat|/.*)$ ]]; then
214
+ formatter="$1"
215
+ else
216
+ printf "Unknown formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
217
+ exit 1
218
+ fi
219
+ ;;
220
+ --report-formatter)
221
+ shift
222
+ if [[ $1 =~ ^(cat|pretty|junit|tap|tap13|/.*)$ ]]; then
223
+ report_formatter="$1"
224
+ else
225
+ printf "Unknown report formatter '%s', valid options are %s\n" "$1" "${VALID_FORMATTERS}"
226
+ exit 1
227
+ fi
228
+ ;;
229
+ -o | --output)
230
+ shift
231
+ BATS_REPORT_OUTPUT_DIR="$1"
232
+ ;;
233
+ -p | --pretty)
234
+ formatter='pretty'
235
+ ;;
236
+ -j | --jobs)
237
+ shift
238
+ flags+=('-j' "$1")
239
+ ;;
240
+ --parallel-binary-name)
241
+ shift
242
+ flags+=('--parallel-binary-name' "$1")
243
+ ;;
244
+ -r | --recursive)
245
+ recursive=1
246
+ ;;
247
+ -t | --tap)
248
+ formatter='tap'
249
+ ;;
250
+ -T | --timing)
251
+ flags+=('-T')
252
+ formatter_flags+=('-T')
253
+ ;;
254
+ --abort)
255
+ flags+=('--abort')
256
+ ;;
257
+ # this flag is now a no-op, as it is the parallel default
258
+ --parallel-preserve-environment) ;;
259
+
260
+ --no-parallelize-across-files)
261
+ flags+=("--no-parallelize-across-files")
262
+ ;;
263
+ --no-parallelize-within-files)
264
+ flags+=("--no-parallelize-within-files")
265
+ ;;
266
+ --no-tempdir-cleanup)
267
+ BATS_TEMPDIR_CLEANUP=''
268
+ ;;
269
+ --errexit)
270
+ BATS_RUN_ERREXIT=1
271
+ ;;
272
+ --tempdir) # for internal test consumption only!
273
+ BATS_RUN_TMPDIR="$2"
274
+ shift
275
+ ;;
276
+ -x | --trace)
277
+ flags+=(--trace)
278
+ ;;
279
+ --print-output-on-failure)
280
+ flags+=(--print-output-on-failure)
281
+ ;;
282
+ --show-output-of-passing-tests)
283
+ flags+=(--show-output-of-passing-tests)
284
+ ;;
285
+ --verbose-run)
286
+ flags+=(--verbose-run)
287
+ ;;
288
+ --gather-test-outputs-in)
289
+ shift
290
+ output_dir="$1"
291
+ # Convert relative path to absolute to handle tests that change directory
292
+ # This ensures the path remains valid even if tests use 'cd'
293
+ if [[ $output_dir != /* ]]; then
294
+ output_dir="$PWD/$output_dir"
295
+ fi
296
+ if [ -d "$output_dir" ]; then
297
+ if ! find "$output_dir" -mindepth 1 -exec false {} + 2>/dev/null; then
298
+ abort --no-print-usage "Directory '$output_dir' must be empty for --gather-test-outputs-in"
299
+ fi
300
+ elif ! mkdir "$output_dir" 2>/dev/null; then
301
+ abort --no-print-usage "Could not create '$output_dir' for --gather-test-outputs-in"
302
+ fi
303
+ flags+=(--gather-test-outputs-in "$output_dir")
304
+ ;;
305
+ --setup-suite-file)
306
+ shift
307
+ setup_suite_file="$1"
308
+ ;;
309
+ --code-quote-style)
310
+ shift
311
+ BATS_CODE_QUOTE_STYLE="$1"
312
+ ;;
313
+ --filter-status)
314
+ shift
315
+ flags+=('--filter-status' "$1")
316
+ ;;
317
+ --filter-tags)
318
+ shift
319
+ flags+=('--filter-tags' "$1")
320
+ ;;
321
+ --line-reference-format)
322
+ shift
323
+ BATS_LINE_REFERENCE_FORMAT=$1
324
+ ;;
325
+ -*)
326
+ abort "Bad command line option '$1'"
327
+ ;;
328
+ *)
329
+ arguments+=("$1")
330
+ ;;
331
+ esac
332
+ shift
333
+ done
334
+
335
+ if [[ ! $BATS_LINE_REFERENCE_FORMAT =~ (custom|comma_line|colon|uri) ]]; then
336
+ abort "Invalid BATS_LINE_REFERENCE_FORMAT '$BATS_LINE_REFERENCE_FORMAT' (e.g. via --line-reference-format)"
337
+ fi
338
+
339
+ if [[ -n "${BATS_RUN_TMPDIR:-}" ]]; then
340
+ if [[ -d "$BATS_RUN_TMPDIR" ]]; then
341
+ printf "Error: BATS_RUN_TMPDIR (%s) already exists\n" "$BATS_RUN_TMPDIR" >&2
342
+ printf "Reusing old run directories can lead to unexpected results ... aborting!\n" >&2
343
+ exit 1
344
+ elif ! mkdir -p "$BATS_RUN_TMPDIR"; then
345
+ printf "Error: Failed to create BATS_RUN_TMPDIR (%s)\n" "$BATS_RUN_TMPDIR" >&2
346
+ exit 1
347
+ fi
348
+ elif ! BATS_RUN_TMPDIR=$(mktemp -d "${BATS_TMPDIR}/bats-run-XXXXXX"); then
349
+ printf "Error: Failed to create BATS_RUN_TMPDIR (%s) with mktemp\n" "${BATS_TMPDIR}/bats-run-XXXXXX" >&2
350
+ exit 1
351
+ fi
352
+
353
+ export BATS_WARNING_FILE="${BATS_RUN_TMPDIR}/warnings.log"
354
+ export BATS_RUN_ERREXIT=${BATS_RUN_ERREXIT:-}
355
+
356
+ bats_exit_trap() {
357
+ if [[ -s "$BATS_WARNING_FILE" ]]; then
358
+ local pre_cat='' post_cat=''
359
+ if [[ $formatter == pretty ]]; then
360
+ pre_cat=$'\x1B[31m'
361
+ post_cat=$'\x1B[0m'
362
+ fi
363
+ printf "\nThe following warnings were encountered during tests:\n%s" "$pre_cat"
364
+ cat "$BATS_WARNING_FILE"
365
+ printf "%s" "$post_cat"
366
+ fi >&2
367
+
368
+ if [[ -n "$BATS_TEMPDIR_CLEANUP" ]]; then
369
+ rm -rf "$BATS_RUN_TMPDIR"
370
+ else
371
+ printf "BATS_RUN_TMPDIR: %s\n" "$BATS_RUN_TMPDIR" >&2
372
+ fi
373
+ }
374
+
375
+ trap bats_exit_trap EXIT
376
+
377
+ if [[ "$formatter" != "tap" ]]; then
378
+ flags+=('-x')
379
+ fi
380
+
381
+ if [[ -n "$report_formatter" && "$report_formatter" != "tap" ]]; then
382
+ flags+=('-x')
383
+ fi
384
+
385
+ if [[ "$formatter" == "junit" ]]; then
386
+ flags+=('-T')
387
+ formatter_flags+=('--base-path' "${arguments[0]}")
388
+ fi
389
+ if [[ "$report_formatter" == "junit" ]]; then
390
+ flags+=('-T')
391
+ report_formatter_flags+=('--base-path' "${arguments[0]}")
392
+ fi
393
+
394
+ if [[ "$formatter" == "pretty" ]]; then
395
+ formatter_flags+=('--base-path' "${arguments[0]}")
396
+ fi
397
+
398
+ # if we don't need to filter extended syntax, use the faster formatter
399
+ if [[ "$formatter" == tap && -z "$report_formatter" ]]; then
400
+ formatter="cat"
401
+ fi
402
+
403
+ if [[ "$formatter" == pretty ]]; then
404
+ if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then
405
+ formatter_flags+=(--interactive)
406
+ fi
407
+ fi
408
+
409
+ bats_check_formatter() { # <formatter-path>
410
+ local -r formatter="$1"
411
+ if [[ ! -f "$formatter" ]]; then
412
+ printf "ERROR: Formatter '%s' is not readable!\n" "$formatter"
413
+ exit 1
414
+ elif [[ ! -x "$formatter" ]]; then
415
+ printf "ERROR: Formatter '%s' is not executable!\n" "$formatter"
416
+ exit 1
417
+ fi
418
+ }
419
+
420
+ if [[ $formatter == /* ]]; then # absolute paths are direct references to formatters
421
+ bats_check_formatter "$formatter"
422
+ interpolated_formatter="$formatter"
423
+ else
424
+ interpolated_formatter="bats-format-${formatter}"
425
+ fi
426
+
427
+ if [[ "${#arguments[@]}" -eq 0 ]]; then
428
+ abort 'Must specify at least one <test>'
429
+ fi
430
+
431
+ if [[ -n "$report_formatter" ]]; then
432
+ if [[ ! -w "${BATS_REPORT_OUTPUT_DIR}" ]]; then
433
+ abort "Output path ${BATS_REPORT_OUTPUT_DIR} is not writeable"
434
+ fi
435
+ # only set BATS_REPORT_FILENAME if none was given
436
+ if [[ -z "${BATS_REPORT_FILENAME:-}" ]]; then
437
+ case "$report_formatter" in
438
+ tap | tap13)
439
+ BATS_REPORT_FILENAME="report.tap"
440
+ ;;
441
+ junit)
442
+ BATS_REPORT_FILENAME="report.xml"
443
+ ;;
444
+ *)
445
+ BATS_REPORT_FILENAME="report.log"
446
+ ;;
447
+ esac
448
+ fi
449
+ fi
450
+
451
+ if [[ $report_formatter == /* ]]; then # absolute paths are direct references to formatters
452
+ bats_check_formatter "$report_formatter"
453
+ interpolated_report_formatter="${report_formatter}"
454
+ else
455
+ interpolated_report_formatter="bats-format-${report_formatter}"
456
+ fi
457
+
458
+ if [[ "${BATS_CODE_QUOTE_STYLE-BATS_CODE_QUOTE_STYLE_UNSET}" == BATS_CODE_QUOTE_STYLE_UNSET ]]; then
459
+ BATS_CODE_QUOTE_STYLE="\`'"
460
+ fi
461
+
462
+ case "${BATS_CODE_QUOTE_STYLE}" in
463
+ ??)
464
+ BATS_BEGIN_CODE_QUOTE="${BATS_CODE_QUOTE_STYLE::1}"
465
+ BATS_END_CODE_QUOTE="${BATS_CODE_QUOTE_STYLE:1:1}"
466
+ export BATS_BEGIN_CODE_QUOTE BATS_END_CODE_QUOTE
467
+ ;;
468
+ custom)
469
+ if [[ ${BATS_BEGIN_CODE_QUOTE-BATS_BEGIN_CODE_QUOTE_UNSET} == BATS_BEGIN_CODE_QUOTE_UNSET ||
470
+ ${BATS_END_CODE_QUOTE-BATS_BEGIN_CODE_QUOTE_UNSET} == BATS_BEGIN_CODE_QUOTE_UNSET ]]; then
471
+ printf "ERROR: BATS_CODE_QUOTE_STYLE=custom requires BATS_BEGIN_CODE_QUOTE and BATS_END_CODE_QUOTE to be set\n" >&2
472
+ exit 1
473
+ fi
474
+ ;;
475
+ *)
476
+ printf "ERROR: Unknown BATS_CODE_QUOTE_STYLE: %s\n" "$BATS_CODE_QUOTE_STYLE" >&2
477
+ exit 1
478
+ ;;
479
+ esac
480
+
481
+ if [[ -n "$setup_suite_file" && ! -f "$setup_suite_file" ]]; then
482
+ abort "--setup-suite-file $setup_suite_file does not exist!"
483
+ fi
484
+
485
+ filenames=()
486
+ for ((i=0; i < ${#arguments[@]}; ++i)); do
487
+ filename="${arguments[i]}"
488
+ if [[ -z "$filename" ]]; then
489
+ abort --no-print-usage "File path must not be empty! (Path number $((i + 1)))"
490
+ fi
491
+ expand_path "$filename" 'filename'
492
+
493
+ if [[ -z "$setup_suite_file" ]]; then
494
+ if [[ -d "$filename" ]]; then
495
+ dirname="$filename"
496
+ else
497
+ dirname="${filename%/*}"
498
+ fi
499
+ potential_setup_suite_file="$dirname/setup_suite.bash"
500
+ if [[ -e "$potential_setup_suite_file" ]]; then
501
+ setup_suite_file="$potential_setup_suite_file"
502
+ fi
503
+ fi
504
+
505
+ if [[ -d "$filename" ]]; then
506
+ shopt -s nullglob
507
+ if [[ "$recursive" -eq 1 ]]; then
508
+ while IFS= read -r -d $'\0' file; do
509
+ filenames+=("$file")
510
+ done < <(find -L "$filename" -type f -name "*.${BATS_FILE_EXTENSION:-bats}" -print0 | sort -z)
511
+ else
512
+ for suite_filename in "$filename"/*."${BATS_FILE_EXTENSION:-bats}"; do
513
+ filenames+=("$suite_filename")
514
+ done
515
+ fi
516
+ shopt -u nullglob
517
+ else
518
+ filenames+=("$filename")
519
+ fi
520
+ done
521
+
522
+ if [[ -n "$setup_suite_file" ]]; then
523
+ flags+=("--setup-suite-file" "$setup_suite_file")
524
+ fi
525
+
526
+ # shellcheck source=lib/bats-core/validator.bash
527
+ source "$BATS_ROOT/$BATS_LIBDIR/bats-core/validator.bash"
528
+
529
+ trap 'BATS_INTERRUPTED=true' INT # let the lower levels handle the interruption
530
+
531
+ set -o pipefail execfail
532
+
533
+ # pipe stdin into command and to stdout
534
+ # pipe command stdout to file
535
+ bats_tee() { # <output-file> <command...>
536
+ local output_file=$1 status=0
537
+ shift
538
+ exec 3<&1 # use FD3 to get around pipe
539
+ tee >(cat >&3) | "$@" >"$output_file" || status=$?
540
+ if (( status != 0 )); then
541
+ printf "ERROR: command \`%s\` failed with status %d\n" "$*" "$status" >&2
542
+ fi
543
+ return $status
544
+ }
545
+
546
+ if [[ -n "$report_formatter" ]]; then
547
+ exec bats-exec-suite "${flags[@]}" "${filenames[@]}" |
548
+ bats_tee "${BATS_REPORT_OUTPUT_DIR}/${BATS_REPORT_FILENAME}" "$interpolated_report_formatter" "${report_formatter_flags[@]}" |
549
+ bats_test_count_validator |
550
+ "$interpolated_formatter" "${formatter_flags[@]}"
551
+ else
552
+ exec bats-exec-suite "${flags[@]}" "${filenames[@]}" |
553
+ bats_test_count_validator |
554
+ "$interpolated_formatter" "${formatter_flags[@]}"
555
+ fi