busser-bats 0.6.0 → 0.7.0

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/publish.yml +22 -0
  3. data/.gitignore +0 -1
  4. data/.release-please-manifest.json +1 -1
  5. data/.rubocop.yml +1 -4
  6. data/CHANGELOG.md +11 -0
  7. data/CONTRIBUTING.md +86 -0
  8. data/Gemfile +7 -5
  9. data/README.md +100 -28
  10. data/Rakefile +42 -15
  11. data/config/features/support/env.rb +0 -5
  12. data/features/support/env.rb +0 -12
  13. data/features/test_command.feature +31 -31
  14. data/lib/busser/bats/version.rb +3 -1
  15. data/lib/busser/runner_plugin/bats.rb +27 -3
  16. data/spec/busser/runner_plugin/bats_spec.rb +41 -0
  17. data/spec/spec_helper.rb +2 -0
  18. data/vendor/bats/LICENSE.md +53 -0
  19. data/vendor/bats/README.md +84 -242
  20. data/vendor/bats/VERSION.txt +1 -1
  21. data/vendor/bats/bin/bats +75 -0
  22. data/vendor/bats/install.sh +20 -28
  23. data/vendor/bats/lib/bats-core/common.bash +278 -0
  24. data/vendor/bats/lib/bats-core/formatter.bash +151 -0
  25. data/vendor/bats/lib/bats-core/preprocessing.bash +19 -0
  26. data/vendor/bats/lib/bats-core/semaphore.bash +113 -0
  27. data/vendor/bats/lib/bats-core/test_functions.bash +539 -0
  28. data/vendor/bats/lib/bats-core/tracing.bash +425 -0
  29. data/vendor/bats/lib/bats-core/validator.bash +37 -0
  30. data/vendor/bats/lib/bats-core/warnings.bash +44 -0
  31. data/vendor/bats/libexec/bats-core/bats +549 -0
  32. data/vendor/bats/libexec/bats-core/bats-exec-file +402 -0
  33. data/vendor/bats/libexec/bats-core/bats-exec-suite +342 -0
  34. data/vendor/bats/libexec/bats-core/bats-exec-test +383 -0
  35. data/vendor/bats/libexec/bats-core/bats-format-cat +6 -0
  36. data/vendor/bats/libexec/bats-core/bats-format-junit +293 -0
  37. data/vendor/bats/libexec/bats-core/bats-format-pretty +360 -0
  38. data/vendor/bats/libexec/bats-core/bats-format-tap +55 -0
  39. data/vendor/bats/libexec/bats-core/bats-format-tap13 +89 -0
  40. data/vendor/bats/libexec/bats-core/bats-gather-tests +402 -0
  41. data/vendor/bats/libexec/bats-core/bats-preprocess +118 -0
  42. data/vendor/bats/man/Makefile +16 -3
  43. data/vendor/bats/man/bats.1 +100 -58
  44. data/vendor/bats/man/bats.1.ronn +102 -12
  45. data/vendor/bats/man/bats.7 +205 -83
  46. data/vendor/bats/man/bats.7.ronn +262 -14
  47. data/vendor/bats/uninstall.sh +63 -0
  48. metadata +25 -6
  49. data/.tool-versions +0 -1
  50. data/vendor/bats/.gitattributes +0 -3
  51. data/vendor/bats/.travis.yml +0 -5
  52. data/vendor/bats/LICENSE +0 -20
  53. data/vendor/bats/bin/bats +0 -1
  54. data/vendor/bats/package.json +0 -9
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env bash
2
+
3
+ bats_prefix_lines_for_tap_output() {
4
+ while IFS= read -r line; do
5
+ printf '# %s\n' "$line" || break # avoid feedback loop when errors are redirected into BATS_OUT (see #353)
6
+ done
7
+ if [[ -n "$line" ]]; then
8
+ printf '# %s\n' "$line"
9
+ fi
10
+ }
11
+
12
+ function bats_replace_filename() {
13
+ local line
14
+ while read -r line; do
15
+ printf "%s\n" "${line//$BATS_TEST_SOURCE/$BATS_TEST_FILENAME}"
16
+ done
17
+ if [[ -n "$line" ]]; then
18
+ printf "%s\n" "${line//$BATS_TEST_SOURCE/$BATS_TEST_FILENAME}"
19
+ fi
20
+ }
21
+
22
+ bats_quote_code() { # <var> <code>
23
+ printf -v "$1" -- "%s%s%s" "$BATS_BEGIN_CODE_QUOTE" "$2" "$BATS_END_CODE_QUOTE"
24
+ }
25
+
26
+ bats_check_valid_version() {
27
+ if [[ ! $1 =~ [0-9]+.[0-9]+.[0-9]+ ]]; then
28
+ printf "ERROR: version '%s' must be of format <major>.<minor>.<patch>!\n" "$1" >&2
29
+ exit 1
30
+ fi
31
+ }
32
+
33
+ # compares two versions. Return 0 when version1 < version2
34
+ bats_version_lt() { # <version1> <version2>
35
+ bats_check_valid_version "$1"
36
+ bats_check_valid_version "$2"
37
+
38
+ local -a version1_parts version2_parts
39
+ IFS=. read -ra version1_parts <<<"$1"
40
+ IFS=. read -ra version2_parts <<<"$2"
41
+
42
+ local -i i
43
+ for i in {0..2}; do
44
+ if ((version1_parts[i] < version2_parts[i])); then
45
+ return 0
46
+ elif ((version1_parts[i] > version2_parts[i])); then
47
+ return 1
48
+ fi
49
+ done
50
+ # if we made it this far, they are equal -> also not less then
51
+ return 2 # use other failing return code to distinguish equal from gt
52
+ }
53
+
54
+ # ensure a minimum version of bats is running or exit with failure
55
+ bats_require_minimum_version() { # <required version>
56
+ local required_minimum_version=$1
57
+
58
+ if bats_version_lt "$BATS_VERSION" "$required_minimum_version"; then
59
+ printf "BATS_VERSION=%s does not meet required minimum %s\n" "$BATS_VERSION" "$required_minimum_version"
60
+ exit 1
61
+ fi
62
+
63
+ if bats_version_lt "$BATS_GUARANTEED_MINIMUM_VERSION" "$required_minimum_version"; then
64
+ BATS_GUARANTEED_MINIMUM_VERSION="$required_minimum_version"
65
+ fi
66
+ }
67
+
68
+ # returns 0 when search-value is found in array
69
+ bats_linear_reverse_search() { # <search-value> <array-name>
70
+ local -r search_value=$1 array_name=$2
71
+ eval "local -ri array_length=\${#${array_name}[@]}"
72
+ # shellcheck disable=SC2154
73
+ for ((i=array_length - 1; i >=0; --i)); do
74
+ eval "local value=\"\${${array_name}[i]}\""
75
+ # shellcheck disable=SC2154
76
+ if [[ $value == "$search_value" ]]; then
77
+ return 0
78
+ fi
79
+ done
80
+ return 1
81
+ }
82
+
83
+ # returns 0 when search-value is found in array
84
+ bats_binary_search() { # <search-value> <array-name>
85
+ if [[ $# -ne 2 ]]; then
86
+ printf "ERROR: bats_binary_search requires exactly 2 arguments: <search value> <array name>\n" >&2
87
+ return 2
88
+ fi
89
+
90
+ local -r search_value=$1 array_name=$2
91
+
92
+ # we'd like to test if array is set but we cannot distinguish unset from empty arrays, so we need to skip that
93
+
94
+ local start=0 mid end mid_value
95
+ # start is inclusive, end is exclusive ...
96
+ eval "end=\${#${array_name}[@]}"
97
+
98
+ # so start == end means empty search space
99
+ while ((start < end)); do
100
+ mid=$(((start + end) / 2))
101
+ eval "mid_value=\${${array_name}[$mid]}"
102
+ if [[ "$mid_value" == "$search_value" ]]; then
103
+ return 0
104
+ elif [[ "$mid_value" < "$search_value" ]]; then
105
+ # This branch excludes equality -> +1 to skip the mid element.
106
+ # This +1 also avoids endless recursion on odd sized search ranges.
107
+ start=$((mid + 1))
108
+ else
109
+ end=$mid
110
+ fi
111
+ done
112
+
113
+ # did not find it -> its not there
114
+ return 1
115
+ }
116
+
117
+ # store the values in ascending (string!) order in result array
118
+ # Intended for short lists! (uses insertion sort)
119
+ bats_sort() { # <result-array-name> <values to sort...>
120
+ local -r result_name=$1
121
+ shift
122
+
123
+ if (($# == 1)) && [[ $1 == "" ]]; then
124
+ shift # remove leading "" to deal with bash 3 expanding "${empty_array[@]}" to one ""
125
+ fi
126
+
127
+ if (($# == 0)); then
128
+ eval "$result_name=()"
129
+ return 0
130
+ fi
131
+
132
+ local -a sorted_array=()
133
+ local -i i
134
+ while (( $# > 0 )); do # loop over input values
135
+ local current_value="$1"
136
+ shift
137
+ for ((i = ${#sorted_array[@]}; i >= 0; --i)); do # loop over output array from end
138
+ if (( i == 0 )) || [[ ${sorted_array[i - 1]} < $current_value ]]; then
139
+ # insert new element at (freed) desired location
140
+ sorted_array[i]=$current_value
141
+ break
142
+ else
143
+ # shift bigger elements one position to the end
144
+ sorted_array[i]=${sorted_array[i - 1]}
145
+ fi
146
+ done
147
+ done
148
+
149
+ eval "$result_name=(\"\${sorted_array[@]}\")"
150
+ }
151
+
152
+ # check if all search values (must be sorted!) are in the (sorted!) array
153
+ # Intended for short lists/arrays!
154
+ bats_all_in() { # <sorted-array> <sorted search values...>
155
+ local -r haystack_array=$1
156
+ shift
157
+
158
+ local -i haystack_length # just to appease shellcheck
159
+ eval "local -r haystack_length=\${#${haystack_array}[@]}"
160
+
161
+ local -i haystack_index=0 # initialize only here to continue from last search position
162
+ local search_value haystack_value # just to appease shellcheck
163
+ local -i i
164
+ for ((i = 1; i <= $#; ++i)); do
165
+ eval "local search_value=${!i}"
166
+ for (( ; haystack_index < haystack_length; ++haystack_index)); do
167
+ eval "local haystack_value=\${${haystack_array}[$haystack_index]}"
168
+ if [[ $haystack_value > "$search_value" ]]; then
169
+ # we passed the location this value would have been at -> not found
170
+ return 1
171
+ elif [[ $haystack_value == "$search_value" ]]; then
172
+ continue 2 # search value found -> try the next one
173
+ fi
174
+ done
175
+ return 1 # we ran of the end of the haystack without finding the value!
176
+ done
177
+
178
+ # did not return from loop above -> all search values were found
179
+ return 0
180
+ }
181
+
182
+ # check if any search value (must be sorted!) is in the (sorted!) array
183
+ # intended for short lists/arrays
184
+ bats_any_in() { # <sorted-array> <sorted search values>
185
+ local -r haystack_array=$1
186
+ shift
187
+
188
+ local -i haystack_length # just to appease shellcheck
189
+ eval "local -r haystack_length=\${#${haystack_array}[@]}"
190
+
191
+ local -i haystack_index=0 # initialize only here to continue from last search position
192
+ local search_value haystack_value # just to appease shellcheck
193
+ local -i i
194
+ for ((i = 1; i <= $#; ++i)); do
195
+ eval "local search_value=${!i}"
196
+ for (( ; haystack_index < haystack_length; ++haystack_index)); do
197
+ eval "local haystack_value=\${${haystack_array}[$haystack_index]}"
198
+ if [[ $haystack_value > "$search_value" ]]; then
199
+ continue 2 # search value not in array! -> try next
200
+ elif [[ $haystack_value == "$search_value" ]]; then
201
+ return 0 # search value found
202
+ fi
203
+ done
204
+ done
205
+
206
+ # did not return from loop above -> no search value was found
207
+ return 1
208
+ }
209
+
210
+ bats_trim() { # <output-variable> <string>
211
+ local -r bats_trim_ltrimmed=${2#"${2%%[![:space:]]*}"} # cut off leading whitespace
212
+ # shellcheck disable=SC2034 # used in eval!
213
+ local -r bats_trim_trimmed=${bats_trim_ltrimmed%"${bats_trim_ltrimmed##*[![:space:]]}"} # cut off trailing whitespace
214
+ eval "$1=\$bats_trim_trimmed"
215
+ }
216
+
217
+ # a helper function to work around unbound variable errors with ${arr[@]} on Bash 3
218
+ bats_append_arrays_as_args() { # <array...> -- <command ...>
219
+ local -a trailing_args=()
220
+ while (($# > 0)) && [[ $1 != -- ]]; do
221
+ local array=$1
222
+ shift
223
+
224
+ if eval "(( \${#${array}[@]} > 0 ))"; then
225
+ eval "trailing_args+=(\"\${${array}[@]}\")"
226
+ fi
227
+ done
228
+ shift # remove -- separator
229
+
230
+ if (($# == 0)); then
231
+ printf "Error: append_arrays_as_args is missing a command or -- separator\n" >&2
232
+ return 1
233
+ fi
234
+
235
+ if ((${#trailing_args[@]} > 0)); then
236
+ "$@" "${trailing_args[@]}"
237
+ else
238
+ "$@"
239
+ fi
240
+ }
241
+
242
+ bats_format_file_line_reference() { # <output> <file> <line>
243
+ # shellcheck disable=SC2034 # will be used in subimplementation
244
+ local output="${1?}"
245
+ shift
246
+ "bats_format_file_line_reference_${BATS_LINE_REFERENCE_FORMAT?}" "$@"
247
+ }
248
+
249
+ bats_format_file_line_reference_comma_line() {
250
+ printf -v "$output" "%s, line %d" "$@"
251
+ }
252
+
253
+ bats_format_file_line_reference_colon() {
254
+ printf -v "$output" "%s:%d" "$@"
255
+ }
256
+
257
+ # approximate realpath without subshell
258
+ bats_approx_realpath() { # <output-variable> <path>
259
+ local output=$1 path=$2
260
+ if [[ $path != /* ]]; then
261
+ path="$PWD/$path"
262
+ fi
263
+ # x/./y -> x/y
264
+ path=${path//\/.\//\/}
265
+ printf -v "$output" "%s" "$path"
266
+ }
267
+
268
+ bats_format_file_line_reference_uri() {
269
+ local filename=${1?} line=${2?}
270
+ bats_approx_realpath filename "$filename"
271
+ printf -v "$output" "file://%s:%d" "$filename" "$line"
272
+ }
273
+
274
+ # execute command with backed up path
275
+ # to prevent path mocks from interfering with our internals
276
+ bats_execute() { # <command...>
277
+ PATH="${BATS_SAVED_PATH?}" "$@"
278
+ }
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # reads (extended) bats tap streams from stdin and calls callback functions for each line
4
+ #
5
+ # Segmenting functions
6
+ # ====================
7
+ # bats_tap_stream_plan <number of tests> -> when the test plan is encountered
8
+ # bats_tap_stream_suite <file name> -> when a new file is begun WARNING: extended only
9
+ # bats_tap_stream_begin <test index> <test name> -> when a new test is begun WARNING: extended only
10
+ #
11
+ # Test result functions
12
+ # =====================
13
+ # If timing was enabled, BATS_FORMATTER_TEST_DURATION will be set to their duration in milliseconds
14
+ # bats_tap_stream_ok <test index> <test name> -> when a test was successful
15
+ # bats_tap_stream_not_ok <test index> <test name> -> when a test has failed. If the failure was due to a timeout,
16
+ # BATS_FORMATTER_TEST_TIMEOUT is set to the timeout duration in seconds
17
+ # bats_tap_stream_skipped <test index> <test name> <skip reason> -> when a test was skipped
18
+ #
19
+ # Context functions
20
+ # =================
21
+ # bats_tap_stream_comment <comment text without leading '# '> <scope> -> when a comment line was encountered,
22
+ # scope tells the last encountered of plan, begin, ok, not_ok, skipped, suite
23
+ # bats_tap_stream_unknown <full line> <scope> -> when a line is encountered that does not match the previous entries,
24
+ # scope @see bats_tap_stream_comment
25
+ # forwards all input as is, when there is no TAP test plan header
26
+ function bats_parse_internal_extended_tap() {
27
+ local header_pattern='[0-9]+\.\.[0-9]+'
28
+ IFS= read -r header
29
+
30
+ if [[ "$header" =~ $header_pattern ]]; then
31
+ bats_tap_stream_plan "${header:3}"
32
+ else
33
+ # If the first line isn't a TAP plan, print it and pass the rest through
34
+ printf '%s\n' "$header"
35
+ exec cat
36
+ fi
37
+
38
+ ok_line_regexpr="ok ([0-9]+) (.*)"
39
+ skip_line_regexpr="ok ([0-9]+) (.*) # skip( (.*))?$"
40
+ timeout_line_regexpr="not ok ([0-9]+) (.*) # timeout after ([0-9]+)s$"
41
+ not_ok_line_regexpr="not ok ([0-9]+) (.*)"
42
+
43
+ timing_expr="in ([0-9]+)ms$"
44
+ local test_name begin_index last_begin_index try_index ok_index not_ok_index index scope
45
+ begin_index=0
46
+ last_begin_index=-1
47
+ try_index=0
48
+ index=0
49
+ scope=plan
50
+ while IFS= read -r line; do
51
+ unset BATS_FORMATTER_TEST_DURATION BATS_FORMATTER_TEST_TIMEOUT
52
+ case "$line" in
53
+ 'begin '*) # this might only be called in extended tap output
54
+ scope=begin
55
+ begin_index=${line#begin }
56
+ begin_index=${begin_index%% *}
57
+ if [[ $begin_index == "$last_begin_index" ]]; then
58
+ (( ++try_index ))
59
+ else
60
+ try_index=0
61
+ fi
62
+ test_name="${line#begin "$begin_index" }"
63
+ bats_tap_stream_begin "$begin_index" "$test_name"
64
+ ;;
65
+ 'ok '*)
66
+ ((++index))
67
+ if [[ "$line" =~ $ok_line_regexpr ]]; then
68
+ ok_index="${BASH_REMATCH[1]}"
69
+ test_name="${BASH_REMATCH[2]}"
70
+ if [[ "$line" =~ $skip_line_regexpr ]]; then
71
+ scope=skipped
72
+ test_name="${BASH_REMATCH[2]}" # cut off name before "# skip"
73
+ local skip_reason="${BASH_REMATCH[4]}"
74
+ if [[ "$test_name" =~ $timing_expr ]]; then
75
+ local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}"
76
+ test_name="${test_name% in "${BATS_FORMATTER_TEST_DURATION}"ms}"
77
+ bats_tap_stream_skipped "$ok_index" "$test_name" "$skip_reason"
78
+ else
79
+ bats_tap_stream_skipped "$ok_index" "$test_name" "$skip_reason"
80
+ fi
81
+ else
82
+ scope=ok
83
+ if [[ "$line" =~ $timing_expr ]]; then
84
+ local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}"
85
+ bats_tap_stream_ok "$ok_index" "${test_name% in "${BASH_REMATCH[1]}"ms}"
86
+ else
87
+ bats_tap_stream_ok "$ok_index" "$test_name"
88
+ fi
89
+ fi
90
+ else
91
+ printf "ERROR: could not match ok line: %s" "$line" >&2
92
+ exit 1
93
+ fi
94
+ ;;
95
+ 'not ok '*)
96
+ ((++index))
97
+ scope=not_ok
98
+ if [[ "$line" =~ $not_ok_line_regexpr ]]; then
99
+ not_ok_index="${BASH_REMATCH[1]}"
100
+ test_name="${BASH_REMATCH[2]}"
101
+ if [[ "$line" =~ $timeout_line_regexpr ]]; then
102
+ not_ok_index="${BASH_REMATCH[1]}"
103
+ test_name="${BASH_REMATCH[2]}"
104
+ # shellcheck disable=SC2034 # used in bats_tap_stream_ok
105
+ local BATS_FORMATTER_TEST_TIMEOUT="${BASH_REMATCH[3]}"
106
+ fi
107
+ if [[ "$test_name" =~ $timing_expr ]]; then
108
+ # shellcheck disable=SC2034 # used in bats_tap_stream_ok
109
+ local BATS_FORMATTER_TEST_DURATION="${BASH_REMATCH[1]}"
110
+ test_name="${test_name% in "${BASH_REMATCH[1]}"ms}"
111
+ fi
112
+ bats_tap_stream_not_ok "$not_ok_index" "$test_name"
113
+ else
114
+ printf "ERROR: could not match not ok line: %s" "$line" >&2
115
+ exit 1
116
+ fi
117
+ ;;
118
+ '# '*)
119
+ bats_tap_stream_comment "${line:2}" "$scope"
120
+ ;;
121
+ '#')
122
+ bats_tap_stream_comment "" "$scope"
123
+ ;;
124
+ 'suite '*)
125
+ scope=suite
126
+ # pass on the
127
+ bats_tap_stream_suite "${line:6}"
128
+ ;;
129
+ *)
130
+ bats_tap_stream_unknown "$line" "$scope"
131
+ ;;
132
+ esac
133
+ done
134
+ }
135
+
136
+ normalize_base_path() { # <target variable> <base path>
137
+ # the relative path root to use for reporting filenames
138
+ # this is mainly intended for suite mode, where this will be the suite root folder
139
+ local base_path="$2"
140
+ # use the containing directory when --base-path is a file
141
+ if [[ ! -d "$base_path" ]]; then
142
+ base_path="$(dirname "$base_path")"
143
+ fi
144
+ # get the absolute path
145
+ base_path="$(cd "$base_path" && pwd)"
146
+ # ensure the path ends with / to strip that later on
147
+ if [[ "${base_path}" != *"/" ]]; then
148
+ base_path="$base_path/"
149
+ fi
150
+ printf -v "$1" "%s" "$base_path"
151
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+
3
+ bats_export_preprocess_source_BATS_TEST_SOURCE() {
4
+ # export to make it visible to bats_evaluate_preprocessed_source
5
+ # since the latter runs in bats-exec-test's bash while this runs in bats-exec-file's
6
+ export BATS_TEST_SOURCE="$BATS_RUN_TMPDIR/${BATS_TEST_FILE_NUMBER?}-${BATS_TEST_FILENAME##*/}.src"
7
+ }
8
+
9
+ bats_preprocess_source() { # index
10
+ bats_export_preprocess_source_BATS_TEST_SOURCE
11
+ # shellcheck disable=SC2153
12
+ CHECK_BATS_COMMENT_COMMANDS=1 "$BATS_ROOT/libexec/bats-core/bats-preprocess" "$BATS_TEST_FILENAME" >"$BATS_TEST_SOURCE"
13
+ }
14
+
15
+ bats_evaluate_preprocessed_source() {
16
+ # Dynamically loaded user files provided outside of Bats.
17
+ # shellcheck disable=SC1090
18
+ source "${BATS_TEST_SOURCE?}"
19
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+
3
+ bats_run_under_flock() {
4
+ flock "$BATS_SEMAPHORE_DIR" "$@"
5
+ }
6
+
7
+ bats_run_under_shlock() {
8
+ local lockfile="$BATS_SEMAPHORE_DIR/shlock.lock"
9
+ while ! shlock -p $$ -f "$lockfile"; do
10
+ sleep 1
11
+ done
12
+ # we got the lock now, execute the command
13
+ "$@"
14
+ local status=$?
15
+ # free the lock
16
+ rm -f "$lockfile"
17
+ return $status
18
+ }
19
+
20
+ # setup the semaphore environment for the loading file
21
+ bats_semaphore_setup() {
22
+ export -f bats_semaphore_get_free_slot_count
23
+ export -f bats_semaphore_acquire_while_locked
24
+ export BATS_SEMAPHORE_DIR="$BATS_RUN_TMPDIR/semaphores"
25
+
26
+ if command -v flock >/dev/null; then
27
+ BATS_LOCKING_IMPLEMENTATION=flock
28
+ elif command -v shlock >/dev/null; then
29
+ BATS_LOCKING_IMPLEMENTATION=shlock
30
+ else
31
+ printf "ERROR: flock/shlock is required for parallelization within files!\n" >&2
32
+ exit 1
33
+ fi
34
+ }
35
+
36
+ # $1 - output directory for stdout/stderr
37
+ # $@ - command to run
38
+ # run the given command in a semaphore
39
+ # block when there is no free slot for the semaphore
40
+ # when there is a free slot, run the command in background
41
+ # gather the output of the command in files in the given directory
42
+ bats_semaphore_run() {
43
+ local output_dir=$1
44
+ shift
45
+ local semaphore_slot
46
+ semaphore_slot=$(bats_semaphore_acquire_slot)
47
+ bats_semaphore_release_wrapper "$output_dir" "$semaphore_slot" "$@" &
48
+ printf "%d\n" "$!"
49
+ }
50
+
51
+ # $1 - output directory for stdout/stderr
52
+ # $@ - command to run
53
+ # this wraps the actual function call to install some traps on exiting
54
+ bats_semaphore_release_wrapper() {
55
+ local output_dir="$1"
56
+ local semaphore_name="$2"
57
+ shift 2 # all other parameters will be use for the command to execute
58
+
59
+ # shellcheck disable=SC2064 # we want to expand the semaphore_name right now!
60
+ trap "status=$?; bats_semaphore_release_slot '$semaphore_name'; exit $status" EXIT
61
+
62
+ mkdir -p "$output_dir"
63
+ "$@" 2>"$output_dir/stderr" >"$output_dir/stdout"
64
+ local status=$?
65
+
66
+ # bash bug: the exit trap is not called for the background process
67
+ bats_semaphore_release_slot "$semaphore_name"
68
+ trap - EXIT # avoid calling release twice
69
+ return $status
70
+ }
71
+
72
+ bats_semaphore_acquire_while_locked() {
73
+ if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then
74
+ local slot=0
75
+ while [[ -e "$BATS_SEMAPHORE_DIR/slot-$slot" ]]; do
76
+ ((++slot))
77
+ done
78
+ if [[ $slot -lt $BATS_SEMAPHORE_NUMBER_OF_SLOTS ]]; then
79
+ touch "$BATS_SEMAPHORE_DIR/slot-$slot" && printf "%d\n" "$slot" && return 0
80
+ fi
81
+ fi
82
+ return 1
83
+ }
84
+
85
+ # block until a semaphore slot becomes free
86
+ # prints the number of the slot that it received
87
+ bats_semaphore_acquire_slot() {
88
+ mkdir -p "$BATS_SEMAPHORE_DIR"
89
+ # wait for a slot to become free
90
+ # TODO: avoid busy waiting by using signals -> this opens op prioritizing possibilities as well
91
+ while true; do
92
+ # don't lock for reading, we are fine with spuriously getting no free slot
93
+ if [[ $(bats_semaphore_get_free_slot_count) -gt 0 ]]; then
94
+ bats_run_under_"$BATS_LOCKING_IMPLEMENTATION" \
95
+ bash -c bats_semaphore_acquire_while_locked \
96
+ && break
97
+ fi
98
+ sleep 1
99
+ done
100
+ }
101
+
102
+ bats_semaphore_release_slot() {
103
+ # we don't need to lock this, since only our process owns this file
104
+ # and freeing a semaphore cannot lead to conflicts with others
105
+ rm "$BATS_SEMAPHORE_DIR/slot-$1" # this will fail if we had not acquired a semaphore!
106
+ }
107
+
108
+ bats_semaphore_get_free_slot_count() {
109
+ # find might error out without returning something useful when a file is deleted,
110
+ # while the directory is traversed -> only continue when there was no error
111
+ until used_slots=$(find "$BATS_SEMAPHORE_DIR" -name 'slot-*' 2>/dev/null | wc -l); do :; done
112
+ echo $((BATS_SEMAPHORE_NUMBER_OF_SLOTS - used_slots))
113
+ }