1
1
Fork 0

feat(jq): remote-control-car

Signed-off-by: Christina Sørensen <christina@cafkafk.com>
This commit is contained in:
Christina Sørensen 2024-12-09 19:05:58 +01:00
parent 425aeafedf
commit 215e3d084b
Signed by: cafkafk
GPG key ID: 26C542FD97F965CE
9 changed files with 1241 additions and 0 deletions

View file

@ -0,0 +1,21 @@
{
"authors": [
"glennj"
],
"files": {
"solution": [
"remote-control-car.jq"
],
"test": [
"test-remote-control-car.bats"
],
"exemplar": [
".meta/exemplar.jq"
]
},
"forked_from": [
"elixir/elons-toys"
],
"icon": "elons-toys",
"blurb": "Learn about functions by playing around with a remote controlled car."
}

View file

@ -0,0 +1 @@
{"track":"jq","exercise":"remote-control-car","id":"cb6ee3adc237438989481b7d2f706d82","url":"https://exercism.org/tracks/jq/exercises/remote-control-car","handle":"cafkafk","is_requester":true,"auto_approve":false}

View file

@ -0,0 +1,114 @@
# Help
## Running the tests
Each exercise contains a test file.
Run the tests using the `bats` program.
```bash
bats test-hello-world.bats
```
`bats` will need to be installed.
See the [Testing on the Bash track][bash] page for instructions to install `bats` for your system.
### bats is implemented in bash
The bats file is a bash script, with some special functions recognized by the `bats` command.
You'll see some tests that look like
```sh
jq -f some-exercise.jq <<< "{some,json,here}"
```
That `<<<` syntax is a bash [Here String][here-string].
It sends the string on the right-hand side into the standard input of the program on the left-hand side.
It is ([approximately][so]) the same as
```sh
echo "{some,json,here}" | jq -f some-exercise.jq
```
## Help for assert functions
The tests use functions from the [bats-assert][bats-assert] library.
Help for the various `assert*` functions can be found there.
## Skipped tests
Solving an exercise means making all its tests pass.
By default, only one test (the first one) is executed when you run the tests.
This is intentional, as it allows you to focus on just making that one test pass.
Once it passes, you can enable the next test by commenting out or removing the
[[ $BATS_RUN_SKIPPED == true ]] || skip
annotations prepending other tests.
## Overriding skips
To run all tests, including the ones with `skip` annotations, you can run:
```bash
BATS_RUN_SKIPPED=true bats test-some-exercise.bats
```
It can be convenient to use a wrapper function to save on typing: in `bash` you can do:
```bash
bats() {
BATS_RUN_SKIPPED=true command bats *.bats
}
```
Then run tests with just:
```bash
bats
```
## Debugging in `jq`
`jq` comes with a handy [`debug`][debug] filter.
Use it while you are developing your exercise solutions to inspect the data that is currently in the jq pipline.
See the [debugging doc][debugging] for more details.
[bash]: https://exercism.org/docs/tracks/bash/tests
[bats-assert]: https://github.com/bats-core/bats-assert
[here-string]: https://www.gnu.org/software/bash/manual/bash.html#Here-Strings
[so]: https://unix.stackexchange.com/a/80372/4667
[debug]: https://jqlang.github.io/jq/manual/v1.7/#debug
[debugging]: https://exercism.org/docs/tracks/jq/debugging
## Submitting your solution
You can submit your solution using the `exercism submit remote-control-car.jq` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [jq track's documentation](https://exercism.org/docs/tracks/jq)
- The [jq track's programming category on the forum](https://forum.exercism.org/c/programming/jq)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
## Need more help?
- Go to the [Exercism Community forum](https://forum.exercism.org) to get support and ask questions (or just chat!)
- Use the [Exercism Support](https://forum.exercism.org/c/support/8) category if you face any issues with working in the web editor, or downloading or submitting your exercises locally.
- Use the [Programming:jq](https://forum.exercism.org/c/programming/jq/133) category for jq-specific topics.
- Join the community on [Exercism's Discord server](https://exercism.org/r/discord).
- [StackOverflow](https://stackoverflow.com/questions/tagged/jq) can be used to search for your problem and see if it has been answered already.
You can also ask and answer questions.
- [Github issue tracker](https://github.com/exercism/jq/issues) is where we track our development and maintainance of `jq` exercises in exercism.
If none of the above links help you, feel free to post an issue here.

View file

@ -0,0 +1,29 @@
# Hints
## 1. Create a brand-new remote controlled car
- The result can use a constant object.
## 2. Create a brand-new remote controlled car with a nickname
- Consider calling the 0-arity function and piping the result to assign the nickname.
## 3. Display the distance
- [String interpolation][string-interpolation] would be useful here.
## 4. Display the battery percentage
- The [if][if-then-else] expression is needed.
## 5. Driving changes the battery and distance driven
- [Arithmetic update-assignments][update-assn] will be useful here.
## 6. Account for driving with a dead battery
- If the battery is dead, it should return the object unchanged.
[string-interpolation]: https://jqlang.github.io/jq/manual/v1.7/#string-interpolation
[if-then-else]: https://jqlang.github.io/jq/manual/v1.7/#if-then-else-end
[update-assn]: https://jqlang.github.io/jq/manual/v1.7/#arithmetic-update-assignment

View file

@ -0,0 +1,268 @@
# Remote Control Car
Welcome to Remote Control Car on Exercism's jq Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Functions
You can define your own **custom functions** in `jq` to encapsulate whatever logic you need.
_Functions_ act just like builtins: they take an input and emit zero, one or more outputs.
### Defining a function
You can define a `jq` _function_ using the following syntax:
```jq
# no arguments
def funcname: expression;
# or with arguments
def funcname(args): expression;
```
- starts with `def` keyword,
- a colon before the function body,
- the body consists of a single expression,
- ends with a semicolon,
- like the rest of `jq` syntax, you can use arbitrary whitespace for readability.
### Where to put functions
_Functions_ must be defined before they are used: this is an error:
```jq
def A: B(10);
def B(n): n + 1;
A
# => error: B/1 is not defined
```
This implies you have to place _functions_ at the top of your `jq` code, prior to the "main" expression.
#### Nested functions
_Functions_ can be nested:
```jq
def A:
def B(n): n + 1;
B(10)
;
A
# => 11
```
Here, the `B` _function_ is only visible in the body of `A`.
### Scope
A _function_ introduces a new **scope** for variables and nested functions.
### Arguments
_Function_ **arguments** are separated by _semi-colons_ not commas.
For example, a _function_ that takes a number, and then adds a number and multiplies by a number:
```jq
def add_mul(adder; multiplier): (. + adder) * multiplier;
10 | add_mul(5; 4) # => 60
```
<!-- prettier-ignore -->
~~~~exercism/note
Semi-colons are needed because comma already has a purpose in `jq`: an operator that joins streams.
Using a comma instead of a semi-colon will attempt to make two calls to a _1-argument_ `add_mul` function, which doesn't exist and therefore will fail on the first attempted call:
```jq
10 | add_mul(5, 4)
# error: add_mul/1 is not defined
```
~~~~
<!-- prettier-ignore-end -->
#### Arguments are _expressions_
Function _arguments_ are filters, not values.
In this sense, they act like what other languages describe as callbacks:
Using the `add_mul` function as an example:
```jq
10 | add_mul(. + 5; . - 2) # => 200
```
What's happening here?
- the `adder` argument gets the _expression_ `. + 5`
- when the function does `. + adder`, that becomes `. + . + 5`
- that evaluates to 25 since `. == 10`
- similarly, the `multiplier` argument is the expression `. - 2`
- that evaluates to 8
- then the result is `25 * 8 == 200`
#### Arguments as values
Sometimes you'll want to "materialize" an _argument_ into a variable:
```jq
def my_func(arg):
arg as $arg
| other stuff ...
;
```
There's a shorthand for this:
```jq
def my_func($arg):
other stuff ...
;
```
Take note that this is just "syntactic sugar": the name `arg` with no `$` is still in scope in the _function_.
### Arity
_Functions_ have an **arity** -- the number of _arguments_ they take.
_Functions_ can use the same name with different _arities_.
The builtin [`range`][man-range] function demonstrates this: `range/1`, `range/2` and `range/3` all co-exist.
This can be useful for defining recursive functions that carry state via arguments.
For example `map` _could_ be implemented like:
```jq
def my_map($accumulator; func):
if length == 0
then $accumulator
else first as $elem | .[1:] | my_map($accumulator + [$elem | func]; func)
end
;
def my_map(func):
my_map([]; func)
;
[1, 2, 3, 4] | my_map(. * 10) # => [10, 20, 30, 40]
```
### Recursion
`jq` will perform tailcall optimization, but for 0-arity functions only.
### Modules
A `jq` module is a file containing only functions.
Modules are included into a jq program with the [`include`][man-include] or [`import`][man-import] commands.
[man-range]: https://jqlang.github.io/jq/manual/v1.7/#range
[man-import]: https://jqlang.github.io/jq/manual/v1.7/#import-relativepathstring-as-name
[man-include]: https://jqlang.github.io/jq/manual/v1.7/#include-relativepathstring
## Instructions
In this exercise you'll be playing around with a remote controlled car, which you've finally saved enough money for to buy.
Cars start with full (100%) batteries.
Each time you drive the car using the remote control, it covers 20 meters and drains one percent of the battery.
The car's nickname is not known until it is created.
The remote controlled car has a fancy LED display that shows two bits of information:
- The total distance it has driven, displayed as: `"<METERS> meters"`.
- The remaining battery charge, displayed as: `"Battery at <PERCENTAGE>%"`.
If the battery is at 0%, you can't drive the car anymore and the battery display will show `"Battery empty"`.
## 1. Create a brand-new remote controlled car
Implement the `new_remote_control_car/0` function to return a brand-new remote controlled car object:
```jq
new_remote_control_car
# => {
# "battery_percentage": 100,
# "distance_driven_in_meters": 0,
# "nickname": null
# }
```
## 2. Create a brand-new remote controlled car with a nickname
Implement the `new_remote_control_car/1` function to return a brand-new remote controlled car object with a provided nickname:
```jq
new_remote_control_car("Blue")
# => {
# "battery_percentage": 100,
# "distance_driven_in_meters": 0,
# "nickname": "Blue"
# }
```
## 3. Display the distance
Implement the `display_distance/0` function that takes a car object as input and outputs the distance string as displayed on the LED display:
```jq
new_remote_control_car | display_distance
# => "0 meters"
```
## 4. Display the battery percentage
Implement the `display_battery/0` function that takes a car object as input and outputs the battery percentage string as displayed on the LED display:
```jq
new_remote_control_car | display_battery
# => "Battery at 100%"
```
If the battery is at 0%, the battery display will show "Battery empty".
## 5. Driving changes the battery and distance driven
Implement the `drive/0` function that:
- takes a car object as input
- updates the number of meters driven by 20
- drains 1% of the battery
- outputs the modified car object
```jq
new_remote_control_car("Red") | drive
# => {
# "battery_percentage": 99,
# "distance_driven_in_meters": 20,
# "nickname": "Red"
# }
```
## 6. Account for driving with a dead battery
Update the `drive/0` function to not increase the distance driven nor decrease the battery percentage when the battery is drained (at 0%):
```jq
{
battery_percentage: 0,
distance_driven_in_meters: 2000,
nickname: "Red"
} | drive
# => {
# "battery_percentage": 0,
# "distance_driven_in_meters": 2000,
# "nickname": "Red"
# }
```
## Source
### Created by
- @glennj

View file

@ -0,0 +1,637 @@
# This is the source code for bats-support and bats-assert, concatenated
# * https://github.com/bats-core/bats-support
# * https://github.com/bats-core/bats-assert
#
# Comments have been removed to save space. See the git repos for full source code.
############################################################
#
# bats-support - Supporting library for Bats test helpers
#
# Written in 2016 by Zoltan Tombol <zoltan dot tombol at gmail dot com>
#
# To the extent possible under law, the author(s) have dedicated all
# copyright and related and neighboring rights to this software to the
# public domain worldwide. This software is distributed without any
# warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication
# along with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
#
fail() {
(( $# == 0 )) && batslib_err || batslib_err "$@"
return 1
}
batslib_is_caller() {
local -i is_mode_direct=1
# Handle options.
while (( $# > 0 )); do
case "$1" in
-i|--indirect) is_mode_direct=0; shift ;;
--) shift; break ;;
*) break ;;
esac
done
# Arguments.
local -r func="$1"
# Check call stack.
if (( is_mode_direct )); then
[[ $func == "${FUNCNAME[2]}" ]] && return 0
else
local -i depth
for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do
[[ $func == "${FUNCNAME[$depth]}" ]] && return 0
done
fi
return 1
}
batslib_err() {
{ if (( $# > 0 )); then
echo "$@"
else
cat -
fi
} >&2
}
batslib_count_lines() {
local -i n_lines=0
local line
while IFS='' read -r line || [[ -n $line ]]; do
(( ++n_lines ))
done < <(printf '%s' "$1")
echo "$n_lines"
}
batslib_is_single_line() {
for string in "$@"; do
(( $(batslib_count_lines "$string") > 1 )) && return 1
done
return 0
}
batslib_get_max_single_line_key_width() {
local -i max_len=-1
while (( $# != 0 )); do
local -i key_len="${#1}"
batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len"
shift 2
done
echo "$max_len"
}
batslib_print_kv_single() {
local -ir col_width="$1"; shift
while (( $# != 0 )); do
printf '%-*s : %s\n' "$col_width" "$1" "$2"
shift 2
done
}
batslib_print_kv_multi() {
while (( $# != 0 )); do
printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )"
printf '%s\n' "$2"
shift 2
done
}
batslib_print_kv_single_or_multi() {
local -ir width="$1"; shift
local -a pairs=( "$@" )
local -a values=()
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
values+=( "${pairs[$i]}" )
done
if batslib_is_single_line "${values[@]}"; then
batslib_print_kv_single "$width" "${pairs[@]}"
else
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )"
done
batslib_print_kv_multi "${pairs[@]}"
fi
}
batslib_prefix() {
local -r prefix="${1:- }"
local line
while IFS='' read -r line || [[ -n $line ]]; do
printf '%s%s\n' "$prefix" "$line"
done
}
batslib_mark() {
local -r symbol="$1"; shift
# Sort line numbers.
set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" )
local line
local -i idx=0
while IFS='' read -r line || [[ -n $line ]]; do
if (( ${1:--1} == idx )); then
printf '%s\n' "${symbol}${line:${#symbol}}"
shift
else
printf '%s\n' "$line"
fi
(( ++idx ))
done
}
batslib_decorate() {
echo
echo "-- $1 --"
cat -
echo '--'
echo
}
############################################################
assert() {
if ! "$@"; then
batslib_print_kv_single 10 'expression' "$*" \
| batslib_decorate 'assertion failed' \
| fail
fi
}
assert_equal() {
if [[ $1 != "$2" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$2" \
'actual' "$1" \
| batslib_decorate 'values do not equal' \
| fail
fi
}
assert_failure() {
: "${output?}"
: "${status?}"
(( $# > 0 )) && local -r expected="$1"
if (( status == 0 )); then
batslib_print_kv_single_or_multi 6 'output' "$output" \
| batslib_decorate 'command succeeded, but it was expected to fail' \
| fail
elif (( $# > 0 )) && (( status != expected )); then
{ local -ir width=8
batslib_print_kv_single "$width" \
'expected' "$expected" \
'actual' "$status"
batslib_print_kv_single_or_multi "$width" \
'output' "$output"
} \
| batslib_decorate 'command failed as expected, but status differs' \
| fail
fi
}
assert_line() {
local -i is_match_line=0
local -i is_mode_partial=0
local -i is_mode_regexp=0
: "${lines?}"
# Handle options.
while (( $# > 0 )); do
case "$1" in
-n|--index)
if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
echo "\`--index' requires an integer argument: \`$2'" \
| batslib_decorate 'ERROR: assert_line' \
| fail
return $?
fi
is_match_line=1
local -ri idx="$2"
shift 2
;;
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_line' \
| fail
return $?
fi
# Arguments.
local -r expected="$1"
if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$expected'" \
| batslib_decorate 'ERROR: assert_line' \
| fail
return $?
fi
# Matching.
if (( is_match_line )); then
# Specific line.
if (( is_mode_regexp )); then
if ! [[ ${lines[$idx]} =~ $expected ]]; then
batslib_print_kv_single 6 \
'index' "$idx" \
'regexp' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression does not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} != *"$expected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line does not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} != "$expected" ]]; then
batslib_print_kv_single 8 \
'index' "$idx" \
'expected' "$expected" \
'actual' "${lines[$idx]}" \
| batslib_decorate 'line differs' \
| fail
fi
fi
else
# Contained in output.
if (( is_mode_regexp )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} =~ $expected ]] && return 0
done
{ local -ar single=( 'regexp' "$expected" )
local -ar may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} \
| batslib_decorate 'no output line matches regular expression' \
| fail
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == *"$expected"* ]] && return 0
done
{ local -ar single=( 'substring' "$expected" )
local -ar may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} \
| batslib_decorate 'no output line contains substring' \
| fail
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == "$expected" ]] && return 0
done
{ local -ar single=( 'line' "$expected" )
local -ar may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} \
| batslib_decorate 'output does not contain line' \
| fail
fi
fi
}
assert_output() {
local -i is_mode_partial=0
local -i is_mode_regexp=0
local -i is_mode_nonempty=0
local -i use_stdin=0
: "${output?}"
# Handle options.
if (( $# == 0 )); then
is_mode_nonempty=1
fi
while (( $# > 0 )); do
case "$1" in
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
-|--stdin) use_stdin=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
# Arguments.
local expected
if (( use_stdin )); then
expected="$(cat -)"
else
expected="${1-}"
fi
# Matching.
if (( is_mode_nonempty )); then
if [ -z "$output" ]; then
echo 'expected non-empty output, but output was empty' \
| batslib_decorate 'no output' \
| fail
fi
elif (( is_mode_regexp )); then
if [[ '' =~ $expected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$expected'" \
| batslib_decorate 'ERROR: assert_output' \
| fail
elif ! [[ $output =~ $expected ]]; then
batslib_print_kv_single_or_multi 6 \
'regexp' "$expected" \
'output' "$output" \
| batslib_decorate 'regular expression does not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output != *"$expected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$expected" \
'output' "$output" \
| batslib_decorate 'output does not contain substring' \
| fail
fi
else
if [[ $output != "$expected" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$expected" \
'actual' "$output" \
| batslib_decorate 'output differs' \
| fail
fi
fi
}
assert_success() {
: "${output?}"
: "${status?}"
if (( status != 0 )); then
{ local -ir width=6
batslib_print_kv_single "$width" 'status' "$status"
batslib_print_kv_single_or_multi "$width" 'output' "$output"
} \
| batslib_decorate 'command failed' \
| fail
fi
}
refute() {
if "$@"; then
batslib_print_kv_single 10 'expression' "$*" \
| batslib_decorate 'assertion succeeded, but it was expected to fail' \
| fail
fi
}
refute_line() {
local -i is_match_line=0
local -i is_mode_partial=0
local -i is_mode_regexp=0
: "${lines?}"
# Handle options.
while (( $# > 0 )); do
case "$1" in
-n|--index)
if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
echo "\`--index' requires an integer argument: \`$2'" \
| batslib_decorate 'ERROR: refute_line' \
| fail
return $?
fi
is_match_line=1
local -ri idx="$2"
shift 2
;;
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_line' \
| fail
return $?
fi
# Arguments.
local -r unexpected="$1"
if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$unexpected'" \
| batslib_decorate 'ERROR: refute_line' \
| fail
return $?
fi
# Matching.
if (( is_match_line )); then
# Specific line.
if (( is_mode_regexp )); then
if [[ ${lines[$idx]} =~ $unexpected ]]; then
batslib_print_kv_single 6 \
'index' "$idx" \
'regexp' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression should not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} == "$unexpected" ]]; then
batslib_print_kv_single 5 \
'index' "$idx" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should differ' \
| fail
fi
fi
else
# Line contained in output.
if (( is_mode_regexp )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} =~ $unexpected ]]; then
{ local -ar single=( 'regexp' "$unexpected" 'index' "$idx" )
local -a may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} \
| batslib_decorate 'no line should match the regular expression' \
| fail
return $?
fi
done
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
{ local -ar single=( 'substring' "$unexpected" 'index' "$idx" )
local -a may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} \
| batslib_decorate 'no line should contain substring' \
| fail
return $?
fi
done
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == "$unexpected" ]]; then
{ local -ar single=( 'line' "$unexpected" 'index' "$idx" )
local -a may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} \
| batslib_decorate 'line should not be in output' \
| fail
return $?
fi
done
fi
fi
}
refute_output() {
local -i is_mode_partial=0
local -i is_mode_regexp=0
local -i is_mode_empty=0
local -i use_stdin=0
: "${output?}"
# Handle options.
if (( $# == 0 )); then
is_mode_empty=1
fi
while (( $# > 0 )); do
case "$1" in
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
-|--stdin) use_stdin=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Arguments.
local unexpected
if (( use_stdin )); then
unexpected="$(cat -)"
else
unexpected="${1-}"
fi
if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$unexpected'" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Matching.
if (( is_mode_empty )); then
if [ -n "$output" ]; then
batslib_print_kv_single_or_multi 6 \
'output' "$output" \
| batslib_decorate 'output non-empty, but expected no output' \
| fail
fi
elif (( is_mode_regexp )); then
if [[ $output =~ $unexpected ]]; then
batslib_print_kv_single_or_multi 6 \
'regexp' "$unexpected" \
'output' "$output" \
| batslib_decorate 'regular expression should not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output == *"$unexpected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$unexpected" \
'output' "$output" \
| batslib_decorate 'output should not contain substring' \
| fail
fi
else
if [[ $output == "$unexpected" ]]; then
batslib_print_kv_single_or_multi 6 \
'output' "$output" \
| batslib_decorate 'output equals, but it was expected to differ' \
| fail
fi
fi
}

View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
#
# `bats-core` will consume both stdout and stderr for the `run` command's output.
# However `jq` prints its DEBUG output on stderr.
#
# Lines starting with `["DEBUG:",` will be prefixed with a hash and printed on file descriptor 3.
# Other lines on stderr will remain on stderr for bats to consume.
#
# See `bats-core` docs:
# - "Printing to the terminal", https://bats-core.readthedocs.io/en/stable/writing-tests.html#printing-to-the-terminal
# - "File descriptor 3", https://bats-core.readthedocs.io/en/stable/writing-tests.html#file-descriptor-3-read-this-if-bats-hangs
jq() {
local output stderr rc line
stderr=$(mktemp)
output=$(command jq "$@" 2> "$stderr")
rc=$?
while IFS= read -r line || [[ -n $line ]]; do
if [[ $line == '["DEBUG:",'* ]]; then
echo "# $line" >&3
else
echo "$line" >&2
fi
done < "$stderr"
rm -f "$stderr"
echo "$output"
return "$rc"
}

View file

@ -0,0 +1,24 @@
def new_remote_control_car:
{
"battery_percentage": 100,
"distance_driven_in_meters": 0,
"nickname": null,
}
;
def new_remote_control_car(nickname):
{
"battery_percentage": 100,
"distance_driven_in_meters": 0,
"nickname": nickname,
}
;
def display_distance: "\(.["distance_driven_in_meters"]) meters";
def display_battery: .["battery_percentage"]| if . != 0 then "Battery at \(.)%" else "Battery empty" end;
def drive:
if .["battery_percentage"] == 0 then . else
.|.["battery_percentage"] -= 1|.["distance_driven_in_meters"] += 20
end;

View file

@ -0,0 +1,118 @@
#!/usr/bin/env bats
load bats-extra
load bats-jq
assert_key_value() {
local expected=$1 key=$2
local result
result=$(echo "$output" | jq -r --arg key "$key" --argjson val "$expected" '.[$key] == $val')
[[ $result == "true" ]]
}
@test "new car" {
## task 1
run jq -n '
include "remote-control-car";
new_remote_control_car
'
assert_success
assert_key_value 100 "battery_percentage"
assert_key_value 0 "distance_driven_in_meters"
assert_key_value "null" "nickname"
}
@test "new car with nickname" {
## task 2
run jq -n '
include "remote-control-car";
new_remote_control_car("Red")
'
assert_success
assert_key_value 100 "battery_percentage"
assert_key_value 0 "distance_driven_in_meters"
assert_key_value '"Red"' "nickname"
}
@test "display distance for new car is zero" {
## task 3
run jq -r -n '
include "remote-control-car";
new_remote_control_car | display_distance
'
assert_success
assert_output '0 meters'
}
@test "display distance for car with some distance" {
## task 3
run jq -r -n '
include "remote-control-car";
new_remote_control_car
| .distance_driven_in_meters += 20
| display_distance
'
assert_success
assert_output '20 meters'
}
@test "display battery for new car is 100%" {
## task 4
run jq -r -n '
include "remote-control-car";
new_remote_control_car | display_battery
'
assert_success
assert_output 'Battery at 100%'
}
@test "display battery for car with some usage" {
## task 4
run jq -r -n '
include "remote-control-car";
new_remote_control_car
| .battery_percentage -= 40
| display_battery
'
assert_success
assert_output 'Battery at 60%'
}
@test "display battery for car with empty battery" {
## task 4
run jq -r -n '
include "remote-control-car";
new_remote_control_car
| .battery_percentage = 0
| display_battery
'
assert_success
assert_output 'Battery empty'
}
@test "drive a new car" {
## task 5
run jq -n '
include "remote-control-car";
new_remote_control_car | drive
'
assert_success
assert_key_value 99 "battery_percentage"
assert_key_value 20 "distance_driven_in_meters"
}
@test "drive a car with a dead battery" {
## task 6
run jq -n '
include "remote-control-car";
new_remote_control_car
| drive
| .battery_percentage = 0
| drive
'
assert_success
assert_key_value 0 "battery_percentage"
assert_key_value 20 "distance_driven_in_meters"
}
# vim: sw=4 ts=8