Skip to content

Multi-line array assignments are reported as uncovered #1338

Description

@cursor
Q A
OS Linux
Shell & version bash 5.2.21(1)-release
bashunit version 0.50.1

Summary

A multi-line array assignment inside a function that a test executes is reported as uncovered on
every line it spans. #722 fixed this for statements split with a backslash, but a statement that
spans lines because its parentheses are still open is not covered by that propagation, so an array
literal costs one uncovered line per element.

Current behavior

Bash's DEBUG trap reports the array assignment on the line of its closing ), not on the line
where the statement starts:

#!/usr/bin/env bash
set -T
trap 'printf "DEBUG trap fired with LINENO=%s\n" "$LINENO"' DEBUG

demo() {          # line 5
  local commands=(  # line 6
    "start"       # line 7
    "stop"        # line 8
  )               # line 9
  printf 'after array\n'  # line 10
}

demo              # line 13
DEBUG trap fired with LINENO=13
DEBUG trap fired with LINENO=5
DEBUG trap fired with LINENO=9
DEBUG trap fired with LINENO=10
after array

The only hit for the assignment is on line 9, a )-only line, which is classified as non-executable
and so is discarded when hits are matched against executable lines. The result is that the hit
disappears completely: the opening line and every element line are reported as uncovered, even
though the statement ran.

A multi-line quoted string is affected by the same gap but degrades differently — the hit does land
on the first line, so the first line is reported as covered and only the interior lines are
reported as uncovered.

How to reproduce

src/example.sh:

#!/usr/bin/env bash

multi_line_array() {
  local commands=(
    "start"
    "stop"
    "status"
  )

  printf '%s\n' "${commands[@]}"
}

backslash_continuation() {
  printf '%s\n' \
    "start" \
    "stop" \
    "status"
}

multi_line_string() {
  printf '%s' '{
    "index": "spend",
    "alias": "filters"
  }'
}

tests/example_test.sh:

#!/usr/bin/env bash

source "$(dirname "${BASH_SOURCE[0]}")/../src/example.sh"

function test_multi_line_array() {
  assert_contains "start" "$(multi_line_array)"
}

function test_backslash_continuation() {
  assert_contains "start" "$(backslash_continuation)"
}

function test_multi_line_string() {
  assert_contains "spend" "$(multi_line_string)"
}
BASHUNIT_COVERAGE_SHOW_UNCOVERED=true BASHUNIT_COVERAGE_SHOW_LINE_HITS=true \
  bashunit --coverage --coverage-paths src --no-coverage-report tests

All three tests pass, so all three functions ran, and the report is:

Coverage Report
---------------
src/example.sh                             6/ 13 lines ( 46%)
---------------
Total: 6/13 (46%)

Uncovered Lines
---------------
src/example.sh:4-7,22-24

Line Hits
---------
src/example.sh: 10:1 14:1 15:1 16:1 17:1 21:1
  • Lines 4-7, the whole local commands=(...) statement, are reported uncovered.
  • Lines 14-17, the backslash-continued statement, are all covered, which is the Line continuations not counted as covered in report #722 propagation
    working as intended.
  • Lines 22-24, the interior of the multi-line string, are reported uncovered while its first line
    (21) is covered.

Expected behavior

A statement that ran should count as covered on every physical line it spans, however it happens to
span them — the same guarantee #722 established for a backslash chain. Alternatively the lines that
carry no statement of their own (array elements, the interior of a multi-line string) could be
treated as non-executable so they leave the denominator. Either way the array in the repro should
not contribute four uncovered lines after its function has run.

The scale of this in real code: in a repository of 13 shell scripts with 548 executable lines,
83 of the 188 lines reported as uncovered are array elements or interior lines of a multi-line
string, so roughly 15% of the denominator can never be covered by any test. Command allowlists such
as available_commands=("start" "stop" ...) written one entry per line make it worse the more
commands a script grows.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions