From 92d6304aa7c078dfd5368666d7f962fbeae7a021 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Wed, 14 Oct 2020 16:00:01 -0400 Subject: [PATCH 1/3] Script to validate test cases against the problem specifications. Identifies test cases from the problem spec canonical data that are unimplemented. --- bin/check_test_cases | 192 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100755 bin/check_test_cases diff --git a/bin/check_test_cases b/bin/check_test_cases new file mode 100755 index 00000000..f2748210 --- /dev/null +++ b/bin/check_test_cases @@ -0,0 +1,192 @@ +#!/usr/bin/env bash + +die() { echo "$*" >&2; exit 1; } + +if [[ "$(IFS="."; echo "${BASH_VERSINFO[*]:0:2}")" < "4.3" ]]; then + die "This script requires bash v4.3 or greater" +fi + +usage() { + cat <&2 +Validate the test cases specified in the canonical data +versus the test implemented as listed in .meta/tests.toml + +usage: $0 [-avh] [exercise ...] + +where: + -a validate all exercises, default only those specified + -v show the resusts for each test case, default is to + show only the unimplemted ones + +Exit status is non-zero if there are any unimplemented test cases. +END_USAGE +} + +dependencies() { + command -v gron >/dev/null || + die "Install gron: go get -u github.com/tomnomnom/gron" + + command -v yj >/dev/null || + die "Install yj: go get -u github.com/sclevine/yj" + + [[ -d ../problem-specifications ]] || + die "Clone the exercism/problem-specifications repo." +} + +parseOptions() { + declare -ga exercises + declare -g verbose=false + local all=false + local OPTIND OPTARG + + while getopts ":avh" opt; do + case $opt in + h) usage; exit ;; + v) verbose=true ;; + a) all=true ;; + *) die "invalid option $OPTARG" ;; + esac + done + shift $((OPTIND - 1)) + + if $all; then + mapfile -t exercises < <( + find ./exercises -mindepth 1 -maxdepth 1 -type d -printf "%f\n" + ) + else + exercises=( "$@" ) + fi +} + +# Parse a canonical-data.json file for an exercise. +# Sets 2 shell variables, whose varnames are passed in as params: +# uuids: array of test case UUIDs, in the order they apprear +# map: associative array of UUID => description +# +testSpecs() { + local exercise=$1 + local -n map=$2 + local -n uuids=$3 + local json=../problem-specifications/exercises/"$exercise"/canonical-data.json + if ! [[ -f "$json" ]]; then + if $verbose; then + printf "No canonical data for %s\n\n" "$exercise" >&2 + fi + return + fi + source <( + gron "$json" | awk -F' = ' ' + function join(start, end, separator, result,i) { + if (separator == "") separator = FS + result = $start + for (i = start+1; i <= end; i++) + result = result separator $i + return result + } + function trim(s) { + return gensub("\"(.+)\";", "\\1", 1, s) + } + $1 ~ /\.uuid$/ { + id = trim(join(2, NF)) + uuids = uuids id " " + uuid[substr($1, 1, length($1)-5)] = id + } + $1 ~ /\.description$/ { + desc[substr($1, 1, length($1)-12)] = trim(join(2, NF)) + } + END { + print "uuids=(", uuids, ")" + print "map=(" + for (k in uuid) { + printf "[\"%s\"]=\"%s\"\n", uuid[k], desc[k] + } + print ")" + } + ' + ) +} + +# Parse a ./meta/tests.toml file for an exercise. +# Sets a shell variables, whose varname is passed in as a param: +# map: associative array of UUID => isImplemented status +# +testsImpl() { + local exercise=$1 + local -n map=$2 + local toml="./exercises/$exercise/.meta/tests.toml" + if ! [[ -f "$toml" ]]; then + echo "MISSING: $toml" >&2 + return + fi + source <( + yj -tj < "$toml" | gron | awk -F '[][ ]' ' + NF == 7 {printf "map[%s]=%s\n", $4, $7} + ' + ) +} + +validate() { + local exercise=$1 + local -a testUuidsOrdered descriptionsOrdered + local -A testsSpecified testsImplemented results + + testSpecs "$exercise" testsSpecified testUuidsOrdered + (( ${#testsSpecified[@]} == 0 )) && return + + testsImpl "$exercise" testsImplemented + + local desc status=0 + for uuid in "${testUuidsOrdered[@]}"; do + desc=${testsSpecified[$uuid]} + if ! [[ -v testsImplemented[$uuid] ]]; then + results[$desc]="not implemented" + descriptionsOrdered+=("$desc") + status=1 + elif ! ${testsImplemented[$uuid]}; then + results[$desc]="excluded" + descriptionsOrdered+=("$desc") + elif $verbose; then + results[$desc]="implemented" + descriptionsOrdered+=("$desc") + fi + done + + report + return $status +} + +report() { + local width + if (( ${#results[@]} > 0 )); then + echo "Exercise $exercise" + width=$( + for desc in "${descriptionsOrdered[@]}"; do + echo "${#desc}" + done | sort -n | tail -n 1 + ) + for desc in "${descriptionsOrdered[@]}"; do + printf " %-*s = %s\n" $width "$desc" "${results[$desc]}" + done + echo + fi +} + +main() { + cd "$(dirname -- "$0")/.." >/dev/null || die + dependencies + parseOptions "$@" # sets global vars "exercises" and "verbose" + + if (( ${#exercises[@]} == 0 )); then + echo "Nothing to do" + exit + fi + + local exitStatus=0 + while IFS= read -r exercise; do + validate "$exercise" + (( exitStatus |= $? )) + done < <( printf '%s\n' "${exercises[@]}" | sort ) + return $exitStatus +} + +main "$@" From 6df23110129837b165434f3506d66b4ec90772bd Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Fri, 16 Oct 2020 14:55:24 -0400 Subject: [PATCH 2/3] No need to reimplement what the canonical-data-syncer does --- .gitignore | 1 + bin/check_test_cases | 194 +------------------------------- bin/fetch-canonical_data_syncer | 52 +++++++++ 3 files changed, 56 insertions(+), 191 deletions(-) create mode 100755 bin/fetch-canonical_data_syncer diff --git a/.gitignore b/.gitignore index 3987e01c..439864c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ bin/configlet bin/configlet.exe +bin/canonical_data_syncer diff --git a/bin/check_test_cases b/bin/check_test_cases index f2748210..5391692e 100755 --- a/bin/check_test_cases +++ b/bin/check_test_cases @@ -1,192 +1,4 @@ #!/usr/bin/env bash - -die() { echo "$*" >&2; exit 1; } - -if [[ "$(IFS="."; echo "${BASH_VERSINFO[*]:0:2}")" < "4.3" ]]; then - die "This script requires bash v4.3 or greater" -fi - -usage() { - cat <&2 -Validate the test cases specified in the canonical data -versus the test implemented as listed in .meta/tests.toml - -usage: $0 [-avh] [exercise ...] - -where: - -a validate all exercises, default only those specified - -v show the resusts for each test case, default is to - show only the unimplemted ones - -Exit status is non-zero if there are any unimplemented test cases. -END_USAGE -} - -dependencies() { - command -v gron >/dev/null || - die "Install gron: go get -u github.com/tomnomnom/gron" - - command -v yj >/dev/null || - die "Install yj: go get -u github.com/sclevine/yj" - - [[ -d ../problem-specifications ]] || - die "Clone the exercism/problem-specifications repo." -} - -parseOptions() { - declare -ga exercises - declare -g verbose=false - local all=false - local OPTIND OPTARG - - while getopts ":avh" opt; do - case $opt in - h) usage; exit ;; - v) verbose=true ;; - a) all=true ;; - *) die "invalid option $OPTARG" ;; - esac - done - shift $((OPTIND - 1)) - - if $all; then - mapfile -t exercises < <( - find ./exercises -mindepth 1 -maxdepth 1 -type d -printf "%f\n" - ) - else - exercises=( "$@" ) - fi -} - -# Parse a canonical-data.json file for an exercise. -# Sets 2 shell variables, whose varnames are passed in as params: -# uuids: array of test case UUIDs, in the order they apprear -# map: associative array of UUID => description -# -testSpecs() { - local exercise=$1 - local -n map=$2 - local -n uuids=$3 - local json=../problem-specifications/exercises/"$exercise"/canonical-data.json - if ! [[ -f "$json" ]]; then - if $verbose; then - printf "No canonical data for %s\n\n" "$exercise" >&2 - fi - return - fi - source <( - gron "$json" | awk -F' = ' ' - function join(start, end, separator, result,i) { - if (separator == "") separator = FS - result = $start - for (i = start+1; i <= end; i++) - result = result separator $i - return result - } - function trim(s) { - return gensub("\"(.+)\";", "\\1", 1, s) - } - $1 ~ /\.uuid$/ { - id = trim(join(2, NF)) - uuids = uuids id " " - uuid[substr($1, 1, length($1)-5)] = id - } - $1 ~ /\.description$/ { - desc[substr($1, 1, length($1)-12)] = trim(join(2, NF)) - } - END { - print "uuids=(", uuids, ")" - print "map=(" - for (k in uuid) { - printf "[\"%s\"]=\"%s\"\n", uuid[k], desc[k] - } - print ")" - } - ' - ) -} - -# Parse a ./meta/tests.toml file for an exercise. -# Sets a shell variables, whose varname is passed in as a param: -# map: associative array of UUID => isImplemented status -# -testsImpl() { - local exercise=$1 - local -n map=$2 - local toml="./exercises/$exercise/.meta/tests.toml" - if ! [[ -f "$toml" ]]; then - echo "MISSING: $toml" >&2 - return - fi - source <( - yj -tj < "$toml" | gron | awk -F '[][ ]' ' - NF == 7 {printf "map[%s]=%s\n", $4, $7} - ' - ) -} - -validate() { - local exercise=$1 - local -a testUuidsOrdered descriptionsOrdered - local -A testsSpecified testsImplemented results - - testSpecs "$exercise" testsSpecified testUuidsOrdered - (( ${#testsSpecified[@]} == 0 )) && return - - testsImpl "$exercise" testsImplemented - - local desc status=0 - for uuid in "${testUuidsOrdered[@]}"; do - desc=${testsSpecified[$uuid]} - if ! [[ -v testsImplemented[$uuid] ]]; then - results[$desc]="not implemented" - descriptionsOrdered+=("$desc") - status=1 - elif ! ${testsImplemented[$uuid]}; then - results[$desc]="excluded" - descriptionsOrdered+=("$desc") - elif $verbose; then - results[$desc]="implemented" - descriptionsOrdered+=("$desc") - fi - done - - report - return $status -} - -report() { - local width - if (( ${#results[@]} > 0 )); then - echo "Exercise $exercise" - width=$( - for desc in "${descriptionsOrdered[@]}"; do - echo "${#desc}" - done | sort -n | tail -n 1 - ) - for desc in "${descriptionsOrdered[@]}"; do - printf " %-*s = %s\n" $width "$desc" "${results[$desc]}" - done - echo - fi -} - -main() { - cd "$(dirname -- "$0")/.." >/dev/null || die - dependencies - parseOptions "$@" # sets global vars "exercises" and "verbose" - - if (( ${#exercises[@]} == 0 )); then - echo "Nothing to do" - exit - fi - - local exitStatus=0 - while IFS= read -r exercise; do - validate "$exercise" - (( exitStatus |= $? )) - done < <( printf '%s\n' "${exercises[@]}" | sort ) - return $exitStatus -} - -main "$@" +cd "$(dirname "$0")"/.. && +bin/fetch-canonical_data_syncer && +bin/canonical_data_syncer --check "$@" diff --git a/bin/fetch-canonical_data_syncer b/bin/fetch-canonical_data_syncer new file mode 100755 index 00000000..ecb4e1cf --- /dev/null +++ b/bin/fetch-canonical_data_syncer @@ -0,0 +1,52 @@ +#!/bin/bash + +set -eo pipefail + +readonly LATEST='https://api.github.com/repos/exercism/canonical-data-syncer/releases/latest' + +case "$(uname)" in + (Darwin*) OS='mac' ;; + (Linux*) OS='linux' ;; + (Windows*) OS='windows' ;; + (MINGW*) OS='windows' ;; + (MSYS_NT-*) OS='windows' ;; + (*) OS='linux' ;; +esac + +case "$OS" in + (windows*) EXT='zip' ;; + (*) EXT='tgz' ;; +esac + +case "$(uname -m)" in + (*64*) ARCH='64bit' ;; + (*686*) ARCH='32bit' ;; + (*386*) ARCH='32bit' ;; + (*) ARCH='64bit' ;; +esac + +if [ -z "${GITHUB_TOKEN}" ] +then + HEADER='' +else + HEADER="authorization: Bearer ${GITHUB_TOKEN}" +fi + +FILENAME="canonical_data_syncer-${OS}-${ARCH}.${EXT}" + +get_url () { + curl --header "$HEADER" -s "$LATEST" | + awk -v filename=$FILENAME '$1 ~ /browser_download_url/ && $2 ~ filename { print $2 }' | + tr -d '"' +} + +URL=$(get_url) + +case "$EXT" in + (*zip) + curl --header "$HEADER" -s --location "$URL" -o bin/latest-canonical_data_syncer.zip + unzip bin/latest-canonical_data_syncer.zip -d bin/ + rm bin/latest-canonical_data_syncer.zip + ;; + (*) curl --header "$HEADER" -s --location "$URL" | tar xz -C bin/ ;; +esac From 6b9adb797e3ddae0c934501dc4f2581f5fc19c0d Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Sun, 18 Oct 2020 11:10:22 -0400 Subject: [PATCH 3/3] canonical-data-syncer requires config.json to be in good shape --- bin/check_test_cases | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/check_test_cases b/bin/check_test_cases index 5391692e..2d1cb48d 100755 --- a/bin/check_test_cases +++ b/bin/check_test_cases @@ -1,4 +1,10 @@ #!/usr/bin/env bash -cd "$(dirname "$0")"/.. && -bin/fetch-canonical_data_syncer && +die() { echo "$*" >&2; exit 1; } + +cd "$(dirname "$0")"/.. || die "cannot cd" + +bin/fetch_configlet || die "cannot fetch configlet" +bin/configlet lint . || die "resolve config.json problems first" + +bin/fetch-canonical_data_syncer || die "cannot fetch canonical_data_syncer" bin/canonical_data_syncer --check "$@"