Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ These are not renames: the name is unchanged and no notice is printed, so review
| Surface | Before | Now |
| --- | --- | --- |
| `assert_dir_contains_string`, `assert_dir_not_contains_string` | Case-sensitive; the string was read as a `grep` basic regular expression; the matched file list was printed to STDOUT | Case-insensitive and literal, with `_case` twins; a regular expression search is `assert_dir_matches` and friends, reading extended regular expressions; matched files are reported inside the failure report only |
| `mock_set_output`, `mock_set_status`, `mock_set_side_effect`, `mock_spec_set_output`, `mock_spec_set_status`, `mock_spec_set_side_effect` | The value was written with `echo -e`, so a value of `-n`, `-e` or `-E` was read as a flag and swallowed, and a backslash escape such as `\n` was expanded | The value is written literally and reaches the caller unchanged. Write a multi-line response with ANSI-C quoting: `mock_set_output "${mock}" $'AccessDenied\n403'` |
| `mock_create`, `mock_command` | A call that a mock's expectations did not cover was answered with the default status and output | Mocks are strict by default, and such a call fails with `Mock 'git' received a call that no expectation covers: git 'log'`. Declare the uncovered calls, or set `BATS_HELPERS_MOCK_STRICT=0` before the first mock is created |
| `steps_run` | A `@` step carrying a newline was truncated at it, silently dropping the status, the output and the side effect | Such a step is rejected. Keep it on one line, continuing a long one with a trailing `\`, and write a newline in its output as `\n`, which the output field expands |
8 changes: 7 additions & 1 deletion docs/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,13 @@ mock_spec_set_output "${push}" "rejected"

Both forms coexist. A call is answered by the first specification that accepts it, in the order the specifications were added; a call that none of them accepts falls back to the response for that call index, and then to the response set without one.

A response is written literally, whichever form sets it, so a value such as `-n`, or one holding a backslash, reaches the caller unchanged.
A response is written literally, whichever form sets it, so a value such as `-n`, or one holding a backslash, reaches the caller unchanged. A multi-line response is written with ANSI-C quoting:

```bash
mock_set_output "${mock_curl}" $'AccessDenied\n403'
```

The [step runner](steps.md#command) expands the escapes in its output field instead, since a step is a single line and cannot carry a newline.

The first argument of `mock_spec_arg` after the specification is the position: a one-based argument index, or `*` for "some argument". The second is the matcher:

Expand Down
13 changes: 12 additions & 1 deletion docs/steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ Mock a command with the given status, optional output, and optional side effect.
- `<mock_status>` - exit status to return (optional):
- If not specified, `0` exit code will be used
- Can be omitted and the output given in its place, unless that output is all digits, which parses as a status
- `<mock_output>` - output to return (optional)
- `<mock_output>` - output to return (optional):
- Backslash escapes are expanded, so `\n` gives a multi-line response, `\t` a tab and `\\` a literal backslash
- `<mock_side_effect>` - Bash code executed when mock is called (optional):
- Useful for creating files/directories, setting env vars, logging, simulating complex behaviors
- Executed in the context of the mock, not the test
Expand All @@ -93,6 +94,16 @@ Mock a command with the given status, optional output, and optional side effect.

A step may contain at most three `#` characters and no consecutive `##`. Escape a literal `#` in the arguments as `\#`.

A step is a single line. The parser splits it at the first newline, so a step carrying one is rejected rather than losing everything after it; continue a long step with a trailing `\` instead. The output is the one field that expands backslash escapes, which is what gives a mock a multi-line response:

```bash
declare -a STEPS=(
"@curl * # 0 # AccessDenied\n403"
)
```

The arguments and the side effect are used as written, so a backslash in either reaches the mock unchanged. Setting a response through [`mock_set_output`](mocking.md) directly expands nothing either - there, ANSI-C quoting writes the newline.

### Substring presence

`<substring>`
Expand Down
16 changes: 14 additions & 2 deletions src/steps.bash
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
# Mocks <command>. The status may be omitted and the output given in its
# place, unless that output is all digits, which parses as a status. The
# side effect is Bash code run when the mock is called. A command may be
# mocked by several steps; every call goes through the same mock. Literal
# '#' characters in <args> are escaped as '\#'.
# mocked by several steps; every call goes through the same mock. The step
# is one line: literal '#' characters in <args> are escaped as '\#', and
# <output> expands backslash escapes, so '\n' gives a multi-line response.
#
# <substring>
# Asserts that the output contains <substring>.
Expand Down Expand Up @@ -108,6 +109,13 @@ steps_run() {

_steps_debug_sub "PARSE: STARTED"

# A step is split with 'read', which stops at the first newline, so a step
# holding one would lose its status, output and side effect in silence.
if [[ ${item} == *$'\n'* ]]; then
flunk "A command step must be a single line. Continue a long step with '\\' and write a newline in the output as '\\n'."
return 1
fi

local ESCAPED_HASH_PLACEHOLDER="__ESCAPED_HASH__"
local item_with_placeholders="${item//\\#/${ESCAPED_HASH_PLACEHOLDER}}"

Expand Down Expand Up @@ -142,6 +150,10 @@ steps_run() {
mock_status=0
fi

# A step is one line, so an escape is the only way to write a newline in
# the output. The arguments and the side effect stay as written.
printf -v mock_output '%b' "${mock_output}"

_steps_debug_sub "PARSE: FINISHED"
_steps_debug_sub " command : '${command_binary}'"
_steps_debug_sub " args : '${command_args}'"
Expand Down
123 changes: 122 additions & 1 deletion tests/steps.bats
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ load _test_helper
--opt3 \
{ \
test: 1 \
}
} \
# 0 # multi-line arg"
)

Expand All @@ -467,6 +467,9 @@ load _test_helper
"{" \
"test": 1 \
"}"
assert_success
assert_output "multi-line arg"

steps_run "assert" "${mocks[@]}"
}

Expand Down Expand Up @@ -651,6 +654,44 @@ load _test_helper
assert_output_contains "The string should not contain consecutive '##' and should have a maximum of three '#' characters in total."
}

@test "steps_run with a newline in a command step" {
declare -a STEPS=(
"@somebin --opt1
# 0 # someval"
)

run steps_run "setup"
assert_failure
assert_output_contains "A command step must be a single line."
}

@test "steps_run with a newline in a command step and the caller recovers" {
declare -a STEPS=(
"@somebin --opt1
# 0 # someval"
)

recovered=0
steps_run "setup" 2>/dev/null || recovered=1

assert_equal 1 "${recovered}"
}

@test "steps_run with a newline in a substring presence step" {
declare -a STEPS=(
"first
second"
)

run printf '%s\n' "first" "second"
steps_run "assert"

# Negative.
run printf '%s\n' "first" "third"
run steps_run "assert"
assert_failure
}

@test "steps_run with a wildcard command" {
declare -a STEPS=(
"@somebin * # 0 # wildcard output 1"
Expand Down Expand Up @@ -817,6 +858,86 @@ load _test_helper
steps_run "assert" "${mocks[@]}"
}

@test "steps_run with an escaped newline in the output" {
declare -a STEPS=(
'@somebin # 0 # first\nsecond'
)

mocks="$(steps_run "setup")"
run somebin
assert_success
assert_output $'first\nsecond'

steps_run "assert" "${mocks[@]}"
}

@test "steps_run with an escaped newline in the output in shorthand" {
declare -a STEPS=(
'@somebin # first\nsecond'
)

mocks="$(steps_run "setup")"
run somebin
assert_success
assert_output $'first\nsecond'

steps_run "assert" "${mocks[@]}"
}

@test "steps_run with an escaped tab in the output" {
declare -a STEPS=(
'@somebin # 0 # first\tsecond'
)

mocks="$(steps_run "setup")"
run somebin
assert_success
assert_output $'first\tsecond'

steps_run "assert" "${mocks[@]}"
}

@test "steps_run with an escaped backslash in the output" {
declare -a STEPS=(
'@somebin # 0 # first\\nsecond'
)

mocks="$(steps_run "setup")"
run somebin
assert_success
assert_output 'first\nsecond'

steps_run "assert" "${mocks[@]}"
}

@test "steps_run with an escaped newline in the arguments" {
declare -a STEPS=(
'@somebin --opt1=first\nsecond # 0 # someval'
)

mocks="$(steps_run "setup")"
run somebin '--opt1=first\nsecond'
assert_success
assert_output "someval"

steps_run "assert" "${mocks[@]}"
}

@test "steps_run with an escaped newline in the side effect" {
declare -a STEPS=(
'@somebin # 0 # someval # printf "%s" "first\nsecond" > ${BATS_TEST_TMPDIR}/escaped_newline'
)

mocks="$(steps_run "setup")"
run somebin
assert_success
assert_output "someval"

steps_run "assert" "${mocks[@]}"

assert_file_contains "${BATS_TEST_TMPDIR}/escaped_newline" 'first\nsecond'
}

@test "steps_run with an escaped hash in the side effect" {
declare -a STEPS=(
'@somebin # 0 # success # echo "Comment \# starts here" > ${BATS_TEST_TMPDIR}/escaped_hash'
Expand Down
Loading