Trouble with local food chain testing

When I test my food chain solution on my own machine, I get a weird error:

BATS_RUN_SKIPPED=true bats test-food-chain.bats
test-food-chain.bats
 ✓ fly
 ✓ spider
 ✓ bird
 ✓ cat
 ✓ dog
 ✓ goat
 bats: unknown test name `test_horse'                                          7/10
 ✗ cow
   (in test file test-food-chain.bats, line 180)
     `)' failed
   /Users/bhl/Exercism/jq/food-chain/test-food-chain.bats: command substitution: line 191: syntax error near unexpected token `)'
   /Users/bhl/Exercism/jq/food-chain/test-food-chain.bats: command substitution: line 191: `)'
 ✓ multiple verses
 ✓ full song
   bats warning: Executed 9 instead of expected 10 tests

10 tests, 1 failure, 1 not run

I don’t know anything about bats, but staring at the code, I don’t see any obvious syntax error. The cow and horse tests look just the same as the rest. When I commented out both of these tests, the remaining ones ran without any problem, but if I commented out just one (either one), I would get another strange error:

 ✗ setup_file failed
   (in test file test-food-chain.bats, line 180)
     `)' failed
   bats warning: Executed 1 instead of expected 9 tests

9 tests, 1 failure, 8 not run

I pasted my solution into the browser and there all of the tests ran successfully.

I guess that my local installation of bats is somehow out of whack? Any ideas?

Please paste the test file in its entirety as it exists on your system.

It seems that messages are limited to 7000 characters and the test file is about 8000, so I will post it in two pieces.

Part 1 (first 184 lines, single-verse tests):

#!/usr/bin/env bats
# generated on 2024-07-17T15:57:54Z
load bats-extra
load bats-jq

@test 'fly' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 1,
          "endVerse": 1
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'spider' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 2,
          "endVerse": 2
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a spider.
It wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'bird' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 3,
          "endVerse": 3
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a bird.
How absurd to swallow a bird!
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'cat' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 4,
          "endVerse": 4
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a cat.
Imagine that, to swallow a cat!
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'dog' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 5,
          "endVerse": 5
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a dog.
What a hog, to swallow a dog!
She swallowed the dog to catch the cat.
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'goat' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 6,
          "endVerse": 6
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a goat.
Just opened her throat and swallowed a goat!
She swallowed the goat to catch the dog.
She swallowed the dog to catch the cat.
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'cow' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 7,
          "endVerse": 7
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a cow.
I don't know how she swallowed a cow!
She swallowed the cow to catch the goat.
She swallowed the goat to catch the dog.
She swallowed the dog to catch the cat.
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'horse' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 8,
          "endVerse": 8
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a horse.
She's dead, of course!
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

Part 2 (multiverse tests):

@test 'multiple verses' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 1,
          "endVerse": 3
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a spider.
It wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a bird.
How absurd to swallow a bird!
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

@test 'full song' {
    [[ $BATS_RUN_SKIPPED == "true" ]] || skip

    run jq -r -f food-chain.jq << 'END_INPUT'
        {
          "startVerse": 1,
          "endVerse": 8
        }
END_INPUT

    expected=$(cat <<END_EXPECTED
I know an old lady who swallowed a fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a spider.
It wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a bird.
How absurd to swallow a bird!
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a cat.
Imagine that, to swallow a cat!
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a dog.
What a hog, to swallow a dog!
She swallowed the dog to catch the cat.
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a goat.
Just opened her throat and swallowed a goat!
She swallowed the goat to catch the dog.
She swallowed the dog to catch the cat.
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a cow.
I don't know how she swallowed a cow!
She swallowed the cow to catch the goat.
She swallowed the goat to catch the dog.
She swallowed the dog to catch the cat.
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die.

I know an old lady who swallowed a horse.
She's dead, of course!
END_EXPECTED
)
    assert_success
    assert_equal "$output" "$expected"
}

And that exact test file is throwing the error? Hmm.

Two more things:

bash --version
bats --version

Yes, it throws the error. FWIW, I have done several jq exercises in the last few days and none of them had a similar problem. Nothing has changed in my environment as far as I know.

bhl % bash --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)
Copyright (C) 2007 Free Software Foundation, Inc.
bhl % bats --version
Bats 1.10.0

Ah. I can reproduce.

I strongly recommend you use Homebrew to install bash v5.2

1 Like

Just to confirm, I upgraded bash and now it runs successfully.

By the way, bash 3.2.57 seems to be what comes with Macs, so I likely won’t be the only one who sees this.

Can confirm. IIRC, it’s fairly easy to upgrade using Homebrew, but it is not quite as straightforward to change the default shell (which is now zsh), or point the bash shortcut to the updated bash version.

I found this and this SO post helpful reminders to get things running nicely again.

I think the above is more or less covered in the Exercism bash docs, but it might be worth a couple more links/reminders.

I’m doing some debugging, and bash 3.2 is having a problem with the END_EXPECTED heredoc in the cow test: it’s not recognizing the end marker of the document, even if I change it to something unique like END_COW. Additionally if I comment-out the horse test, the error message becomes line 150: unexpected EOF while looking for matching )'`

There are no control chars around the cow ending heredoc marker, nor around the end parenthesis of the command substitution.

bash v3.2 is the only version with this problem (that I’ve tested). I’m about to dig into the bash change notes for v4.0.

Well, I haven’t figured out what’s special about the cow test, it looks just like the tests that precede it.

I’ll add a check in the test file to abort for bash 3.2 with a suggestion to upgrade with Homebrew

Fortunately, food-chain is the only jq exercise that suffers from this bash 3.2 bug.

Could it be that the prior test has an open-single-quote in the heredoc that isn’t closed?

Dammit ancient bash!

That’s it. An odd number of single quotes in the heredoc.

If I escape the single quote, the test merely fails because the expected value contains a backslash. I’ll have to awkwardly quote the expected value like

    expected="I know an old lady who swallowed a cow.
I don't know how she swallowed a cow!
She swallowed the cow to catch the goat.
She swallowed the goat to catch the dog.
She swallowed the dog to catch the cat.
She swallowed the cat to catch the bird.
She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.
She swallowed the spider to catch the fly.
I don't know why she swallowed the fly. Perhaps she'll die."

or perhaps this is less worse?

    expected="I know an old lady who swallowed a cow."$'\n'
    expected+="I don't know how she swallowed a cow!"$'\n'
    expected+="She swallowed the cow to catch the goat."$'\n'
    expected+="She swallowed the goat to catch the dog."$'\n'
    expected+="She swallowed the dog to catch the cat."$'\n'
    expected+="She swallowed the cat to catch the bird."$'\n'
    expected+="She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her."$'\n'
    expected+="She swallowed the spider to catch the fly."$'\n'
    expected+="I don't know why she swallowed the fly. Perhaps she'll die."

Or change the lyrics?

I have no idea how she swallowed a cow!
...
She died of course!

I think either is fine. I would avoid changing the lyrics as that breaks existing solutions and deviates from the problem specs.

The bash track went with the first style of quoting. sigh

OK, although my sense of style is offended, the problem should be resolved:

1 Like

I would never have guessed! Thanks for working on it, Glenn.

It is a bash bug. Here’s a demo

$ a=$(cat <<END
> line'1
> line'2
> END
> )

After hitting enter, you return to your PS1 prompt.

But

$ a=$(cat <<END
> line'1
> line'2
> line'3
> END
> )
> 

After the close parenthesis, we’re still at the PS2 prompt. However bash has parsed what we’ve entered so far, it has not recognized the end of the command substitution. You can take the cat ... END out of the command substitution and bash handles it as a valid heredoc.