diff --git a/bin/verify-exercises-in-docker b/bin/verify-exercises-in-docker index 5d77d882..4b42632d 100755 --- a/bin/verify-exercises-in-docker +++ b/bin/verify-exercises-in-docker @@ -64,7 +64,7 @@ verify_exercise() { trap 'rm -rf "${tmpdir}"' EXIT # remove tempdir when subshell ends cp -r "${dir}/." "${tmpdir}" || exit copy_example_or_exemplar_to_solution "${tmpdir}" - run_tests "${slug}" "${tmpdir}" || { cat "${tmpdir}/results.json"; exit 1; } + run_tests "${slug}" "${tmpdir}" || { jq . "${tmpdir}/results.json"; exit 1; } ) } diff --git a/config.json b/config.json index a5863665..17d0ffb3 100644 --- a/config.json +++ b/config.json @@ -569,6 +569,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "food-chain", + "name": "Food Chain", + "uuid": "4f7eea63-2cda-401c-af8c-b5007e5baffe", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "list-ops", "name": "List Ops", diff --git a/exercises/practice/food-chain/.docs/instructions.md b/exercises/practice/food-chain/.docs/instructions.md new file mode 100644 index 00000000..125820e3 --- /dev/null +++ b/exercises/practice/food-chain/.docs/instructions.md @@ -0,0 +1,64 @@ +# Instructions + +Generate the lyrics of the song 'I Know an Old Lady Who Swallowed a Fly'. + +While you could copy/paste the lyrics, or read them from a file, this problem is much more interesting if you approach it algorithmically. + +This is a [cumulative song][cumulative-song] of unknown origin. + +This is one of many common variants. + +```text +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! +``` + +[cumulative-song]: https://en.wikipedia.org/wiki/Cumulative_song diff --git a/exercises/practice/food-chain/.meta/config.json b/exercises/practice/food-chain/.meta/config.json new file mode 100644 index 00000000..366fa5ae --- /dev/null +++ b/exercises/practice/food-chain/.meta/config.json @@ -0,0 +1,20 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "food-chain.scm" + ], + "test": [ + "test.scm", + "test-util.ss" + ], + "example": [ + ".meta/example.scm" + ] + }, + "blurb": "Generate the lyrics of the song 'I Know an Old Lady Who Swallowed a Fly'.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/There_Was_an_Old_Lady_Who_Swallowed_a_Fly" +} diff --git a/exercises/practice/food-chain/.meta/example.scm b/exercises/practice/food-chain/.meta/example.scm new file mode 100644 index 00000000..ad8e49aa --- /dev/null +++ b/exercises/practice/food-chain/.meta/example.scm @@ -0,0 +1,53 @@ +(import (rnrs)) + +(define (recite-verse verse) + (define animals + '#("" + "fly" + "spider" + "bird" + "cat" + "dog" + "goat" + "cow" + "horse")) + + (define exclamations + '#("" + "I don't know why she swallowed the fly. Perhaps she'll die." + "It wriggled and jiggled and tickled inside her.\n" + "How absurd to swallow a bird!\n" + "Imagine that, to swallow a cat!\n" + "What a hog, to swallow a dog!\n" + "Just opened her throat and swallowed a goat!\n" + "I don't know how she swallowed a cow!\n" + "She's dead, of course!")) + + (define first-line + (string-append "I know an old lady who swallowed a " (vector-ref animals verse) ".\n")) + + (define exclamation (vector-ref exclamations verse)) + + (define (suffix index) + (if (= index 2) + " that wriggled and jiggled and tickled inside her" + "")) + + (if (< 1 verse 8) + (let loop ((index 1) + (result (vector-ref exclamations 1))) + (if (= index verse) + (string-append first-line exclamation result) + (loop (+ index 1) + (string-append "She swallowed the " (vector-ref animals (+ index 1)) + " to catch the " (vector-ref animals index) (suffix index) ".\n" + result)))) + (string-append first-line exclamation))) + +(define (recite start-verse end-verse) + (let loop ((verse end-verse) + (result '())) + (let ((result (cons (recite-verse verse) result))) + (if (= verse start-verse) + (apply string-append result) + (loop (- verse 1) (cons "\n\n" result)))))) diff --git a/exercises/practice/food-chain/.meta/template.j2 b/exercises/practice/food-chain/.meta/template.j2 new file mode 100644 index 00000000..d0fa5371 --- /dev/null +++ b/exercises/practice/food-chain/.meta/template.j2 @@ -0,0 +1,13 @@ +{{ header }} + +(load "test-util.ss") + +(define test-cases + `( +{%- for case in cases %} + (test-success "{{ loop.index }}. {{ case["description"] }}" + equal? recite '({{ case["input"]["startVerse"] }} {{ case["input"]["endVerse"] }}) + "{{ case["expected"] | join("\n") }}"){% if not loop.last %} +{% endif %}{% endfor %})) + +(run-with-cli "{{ name }}.scm" (list test-cases)) diff --git a/exercises/practice/food-chain/.meta/tests.toml b/exercises/practice/food-chain/.meta/tests.toml new file mode 100644 index 00000000..30c5b980 --- /dev/null +++ b/exercises/practice/food-chain/.meta/tests.toml @@ -0,0 +1,40 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[751dce68-9412-496e-b6e8-855998c56166] +description = "fly" + +[6c56f861-0c5e-4907-9a9d-b2efae389379] +description = "spider" + +[3edf5f33-bef1-4e39-ae67-ca5eb79203fa] +description = "bird" + +[e866a758-e1ff-400e-9f35-f27f28cc288f] +description = "cat" + +[3f02c30e-496b-4b2a-8491-bc7e2953cafb] +description = "dog" + +[4b3fd221-01ea-46e0-825b-5734634fbc59] +description = "goat" + +[1b707da9-7001-4fac-941f-22ad9c7a65d4] +description = "cow" + +[3cb10d46-ae4e-4d2c-9296-83c9ffc04cdc] +description = "horse" + +[22b863d5-17e4-4d1e-93e4-617329a5c050] +description = "multiple verses" + +[e626b32b-745c-4101-bcbd-3b13456893db] +description = "full song" diff --git a/exercises/practice/food-chain/Makefile b/exercises/practice/food-chain/Makefile new file mode 100644 index 00000000..ea4c6c16 --- /dev/null +++ b/exercises/practice/food-chain/Makefile @@ -0,0 +1,17 @@ +solution := + +chez := scheme +guile := guile + +help : + echo 'Run make chez or make guile' + +check-all : chez guile + +chez : + $(chez) --script test.scm $(solution) + +guile : + $(guile) test.scm $(solution) + +.PHONY : help check-all chez guile diff --git a/exercises/practice/food-chain/food-chain.scm b/exercises/practice/food-chain/food-chain.scm new file mode 100644 index 00000000..381b1b03 --- /dev/null +++ b/exercises/practice/food-chain/food-chain.scm @@ -0,0 +1,4 @@ +(import (rnrs)) + +(define (recite start-verse end-verse) + 'implement-me!) diff --git a/exercises/practice/food-chain/test-util.ss b/exercises/practice/food-chain/test-util.ss new file mode 100644 index 00000000..0d8b1c52 --- /dev/null +++ b/exercises/practice/food-chain/test-util.ss @@ -0,0 +1,163 @@ +(import (except (rnrs) current-output-port)) + +(define test-fields '(input expected actual)) + +(define (test-run-solution solution input) + (if (procedure? solution) (apply solution input) solution)) + +(define (scheme->string o) + (with-output-to-string + (lambda () + (write o)))) + +(define (process-condition e) + (if (not (condition? e)) e + `(error + ,(if (who-condition? e) (condition-who e) + 'unknown) + ,(condition-message e) + ,@(map scheme->string + (if (not (irritants-condition? e)) '() + (condition-irritants e)))))) + +(define (test-success description success-predicate + procedure input expected code) + (call/cc + (lambda (k) + (let ([out (open-output-string)]) + (dynamic-wind + (lambda () (set! out (open-output-string))) + (lambda () + (with-exception-handler + (lambda (e) + (k `(fail + (description . ,description) + (code . ,code) + (input . ,input) + (expected . ,expected) + (actual . ,(process-condition e)) + (stdout . ,(get-output-string out))))) + (lambda () + (let ([result (parameterize ([current-output-port out]) + (test-run-solution procedure input))]) + (unless (success-predicate result expected) + (raise result)) + `(pass + (description . ,description) + (code . ,code) + (stdout . ,(get-output-string out))))))) + (lambda () (close-output-port out))))))) + +(define (test-error description procedure input code) + (call/cc + (lambda (k) + (let ([out '()]) + (dynamic-wind + (lambda () (set! out (open-output-string))) + (lambda () + (with-exception-handler + (lambda (e) + (k `(pass + (description . ,description) + (code . ,code) + (stdout . ,(get-output-string out))))) + (lambda () + (let ((result (parameterize ([current-output-port out]) + (test-run-solution procedure input)))) + `(fail + (description . ,description) + (code . ,code) + (input . ,input) + (expected . error) + (actual . ,result) + (stdout . ,(get-output-string out))))))) + (lambda () (close-output-port out))))))) + +(define (run-test test) + (eval (append test `((quote ,test))) (interaction-environment))) + +(define (run-test-suite tests . query) + (for-each + (lambda (field) + (unless (and (symbol? field) (memq field test-fields)) + (error 'run-test-suite + (format #t "~a not in ~a" field test-fields)))) + query) + (let-values ([(passes failures) + (partition + (lambda (result) (eq? 'pass (car result))) + (map run-test tests))]) + (cond + [(null? failures) (format #t "~%Well done!~%~%")] + [else + (format + #t + "~%Passed ~a/~a tests.~%~%The following test cases failed:~%~%" + (length passes) + (length tests)) + (for-each + (lambda (failure) + (format + #t + "* ~a~%" + (cond + [(assoc 'description (cdr failure)) => cdr] + [else (cdr failure)])) + (for-each + (lambda (field) + (let ([info (assoc field (cdr failure))]) + (display " - ") + (write (car info)) + (display ": ") + (write (cdr info)) + (newline))) + query)) + failures) + (error 'test "incorrect solution")]))) + + +(define (run-docker suite) + (write (map run-test suite))) + +(define (test suite . query) + (apply run-test-suite suite query)) + +(define (tests suites . query) + (for-each (lambda (suite) (apply test suite query)) suites)) + +(define (run-with-cli solution suites) + (let ((args (command-line))) + (cond + ;; Normal execution. This is the default behavior used by students + ;; running their tests locally. + [(null? (cdr args)) + (load solution) + (tests suites 'input 'expected 'actual)] + ;; Scheme programs ingesting this output can expect an alist with + ;; the keys 'test-lib-version and 'status. No test-lib version + ;; means an older version of these test utilities is in use, so there + ;; will only be pass/fail lists in the output. When status is 'error, + ;; A message is provided for explanation. It is usually a stringified + ;; condition. When status is 'completed everything is normal, and the + ;; rest of the list comsists of pass/fail lists. + [(string=? (cadr args) "--docker") + (write + `((test-lib-version . 1) + ,@(call/cc + (lambda (k) + (with-exception-handler + ;; Catch failures while loading/compiling the solution. + (lambda (e) + (k `((status . error) + (message + . ,(string-append + "Failed with value: " + (scheme->string (process-condition e))))))) + (lambda () + (load solution) + `((status . ok) + ,@(fold-left (lambda (results suite) + (append results (map run-test suite))) + '() suites))))))))] + ;; You can pass the name of a file to load instead of the "expected" solution filename. + [else (load (cadr args)) (tests suites 'input 'expected 'actual)]))) diff --git a/exercises/practice/food-chain/test.scm b/exercises/practice/food-chain/test.scm new file mode 100644 index 00000000..473a50e7 --- /dev/null +++ b/exercises/practice/food-chain/test.scm @@ -0,0 +1,145 @@ +; These tests are auto-generated with test data from: +; https://github.com/exercism/problem-specifications/blob/main/exercises/food-chain/canonical-data.json +; File last updated on 2026-10-28T02:27:59+00:00 + +(load "test-util.ss") + +(define test-cases + `( + (test-success "1. fly" + equal? recite '(1 1) + "I know an old lady who swallowed a fly. +I don't know why she swallowed the fly. Perhaps she'll die.") + + (test-success "2. spider" + equal? recite '(2 2) + "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.") + + (test-success "3. bird" + equal? recite '(3 3) + "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.") + + (test-success "4. cat" + equal? recite '(4 4) + "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.") + + (test-success "5. dog" + equal? recite '(5 5) + "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.") + + (test-success "6. goat" + equal? recite '(6 6) + "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.") + + (test-success "7. cow" + equal? recite '(7 7) + "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.") + + (test-success "8. horse" + equal? recite '(8 8) + "I know an old lady who swallowed a horse. +She's dead, of course!") + + (test-success "9. multiple verses" + equal? recite '(1 3) + "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.") + + (test-success "10. full song" + equal? recite '(1 8) + "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!"))) + +(run-with-cli "food-chain.scm" (list test-cases))