diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52742ef..3e3a5d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: context: . push: false load: true - tags: exercism/test-runner + tags: exercism/euphoria-test-runner cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 20b2deb..09d3d03 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ tests/*/results.json +tests/*/*.out diff --git a/Dockerfile b/Dockerfile index 5e22c3e..2110e4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,17 @@ -FROM alpine:3.17 +FROM ubuntu:24.04 -# install packages required to run the tests -RUN apk add --no-cache jq coreutils +ARG OPEN_EUPHORIA_ARCH=Linux-x64 +ARG OPEN_EUPHORIA_VERSION=4.1.0 +ARG OPEN_EUPHORIA_SHA=57179171dbed + +RUN apt update && apt install -y curl + +RUN filename="euphoria-${OPEN_EUPHORIA_VERSION}-${OPEN_EUPHORIA_ARCH}-${OPEN_EUPHORIA_SHA}.tar.gz" && \ + curl -L -O "https://github.com/OpenEuphoria/euphoria/releases/download/${OPEN_EUPHORIA_VERSION}/${filename}" && \ + tar -xzf "${filename}" -C /usr/local && \ + cd /usr/local/bin && \ + find "/usr/local/euphoria-${OPEN_EUPHORIA_VERSION}-${OPEN_EUPHORIA_ARCH}/bin" -type f -executable -exec ln -s {} \; && \ + eui --version WORKDIR /opt/test-runner COPY . . diff --git a/README.md b/README.md index e875f60..a16db0c 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,6 @@ -# Exercism Test Runner Template +# Exercism OpenEuphoria Test Runner -This repository is a [template repository](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-template-repository) for creating [test runners][test-runners] for [Exercism][exercism] tracks. - -## Using the Test Runner Template - -1. Ensure that your track has not already implemented a test runner. If there is, there will be a `https://github.com/exercism/-test-runner` repository (i.e. if your track's slug is `python`, the test runner repo would be `https://github.com/exercism/python-test-runner`) -2. Follow [GitHub's documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template) for creating a repository from a template repository - - Name your new repository based on your language track's slug (i.e. if your track is for Python, your test runner repo name is `python-test-runner`) -3. Remove this [Exercism Test Runner Template](#exercism-test-runner-template) section from the `README.md` file -4. Build the test runner, conforming to the [Test Runner interface specification](https://github.com/exercism/docs/blob/main/building/tooling/test-runners/interface.md). - - Update the files to match your track's needs. At the very least, you'll need to update `bin/run.sh`, `Dockerfile` and the test solutions in the `tests` directory - - Tip: look for `TODO:` comments to point you towards code that need updating - - Tip: look for `OPTIONAL:` comments to point you towards code that _could_ be useful - -Once you're happy with your test runner, [open an issue on the exercism/exercism](https://github.com/exercism/exercism/issues/new?assignees=&labels=&template=new-test-runner.md&title=%5BNew+Test+Runner%5D+) to request an official test runner repository for your track. - -# Exercism TRACK_NAME_HERE Test Runner - -The Docker image to automatically run tests on TRACK_NAME_HERE solutions submitted to [Exercism]. +The Docker image to automatically run tests on OpenEuphoria solutions submitted to [Exercism]. ## Run the test runner diff --git a/bin/json.e b/bin/json.e new file mode 100644 index 0000000..39e603f --- /dev/null +++ b/bin/json.e @@ -0,0 +1,834 @@ +namespace json + +include std/convert.e +include std/get.e +include std/io.e +include std/map.e +include std/pretty.e +include std/sequence.e +include std/sort.e +include std/text.e +include std/types.e + +public enum + J_TYPE, + J_VALUE + +export enum type jsontype_t + JSON_NONE = 0, + JSON_OBJECT, + JSON_ARRAY, + JSON_STRING, + JSON_NUMBER, + JSON_PRIMITIVE +end type + +export sequence json_error_stack = {} + +export procedure json_clear_error() + json_error_stack = {} +end procedure + +export procedure json_push_error( sequence error, object data = {} ) + json_error_stack = prepend( json_error_stack, sprintf(error,data) ) +end procedure + +public enum JSON_ERROR_LAST = 0, JSON_ERROR_FULL + +public function json_last_error( integer error_mode = JSON_ERROR_LAST ) + + if error_mode = JSON_ERROR_LAST then + + if length( json_error_stack ) then + return json_error_stack[1] + end if + + return "" + end if + + return json_error_stack +end function + +export function json_escape( sequence old ) + + sequence new = "" + + for i = 1 to length( old ) do + switch old[i] do + case '\n' then new &= "\\n" + case '\r' then new &= "\\r" + case '\t' then new &= "\\t" + case '\"' then new &= "\\\"" + case '\\' then new &= "\\\\" + case else new &= old[i] + end switch + end for + + return new +end function + +export function json_skip_whitespace( sequence js, integer start ) + + integer i = start + + while i <= length( js ) and t_space( js[i] ) do + i += 1 + end while + + return i +end function + +function compare_keys( sequence json_a, sequence json_b ) + return compare( json_a[1], json_b[1] ) +end function + +function sort_by_key( sequence pairs ) + return custom_sort( routine_id("compare_keys"), pairs ) +end function + +export function json_parse_object( sequence js, integer start ) + + jsontype_t json_type = JSON_NONE + object json_value = 0 + + integer i = json_skip_whitespace( js, start ) + integer last_i = i + + if i <= length( js ) and js[i] != '{' then + json_push_error( "json_parse_object(): Expected '{' at position %d", i ) + return {json_type,json_value,i} + end if + + json_value = {} + i = json_skip_whitespace( js, i+1 ) + + while i <= length( js ) and js[i] != '}' do + + integer key_type, value_type + object key_object, value_object + + last_i = i + {key_type,key_object,i} = json_parse_value( js, i ) + + if key_type != JSON_STRING then + json_push_error( "json_parse_object(): Expected string at position %d", last_i ) + exit + end if + + i = json_skip_whitespace( js, i ) + + if js[i] != ':' then + json_push_error( "json_parse_object(): Expected ':' at position %d", i ) + exit + end if + + i = json_skip_whitespace( js, i+1 ) + + last_i = i + {value_type,value_object,i} = json_parse_value( js, i ) + + if value_type = JSON_NONE then + json_push_error( "json_parse_object(): Expected object at position %d", last_i ) + exit + end if + + json_value = append( json_value, {key_object,{value_type,value_object}} ) + + i = json_skip_whitespace( js, i ) + + if js[i] = ',' then + i += 1 + continue + end if + + if js[i] != '}' then + json_push_error( "json_parse_object(): Expected '}' at position %d", i ) + exit + end if + + end while + + if js[i] = '}' then + json_type = JSON_OBJECT + i += 1 + end if + + return {json_type,json_value,i} +end function + +export function json_parse_array( sequence js, integer start ) + + jsontype_t json_type = JSON_NONE + object json_value = 0 + + integer i = json_skip_whitespace( js, start ) + integer last_i = i + + if i <= length( js ) and js[i] != '[' then + json_push_error( "json_parse_array(): Expected '[' at position %d", i ) + return {json_type,json_value,i} + end if + + json_value = {} + i = json_skip_whitespace( js, i+1 ) + + while i <= length( js ) and js[i] != ']' do + + integer member_type + object member_value + + {member_type,member_value,i} = json_parse_value( js, i ) + + if member_type = JSON_NONE then + json_push_error( "json_parse_array(): Expected object at position %d", last_i ) + exit + end if + + json_value = append( json_value, {member_type,member_value} ) + + i = json_skip_whitespace( js, i ) + + if js[i] = ',' then + i += 1 + continue + end if + + if js[i] != ']' then + json_push_error( "json_parse_array(): Expected ']' at position %d", i ) + exit + end if + + end while + + if js[i] = ']' then + json_type = JSON_ARRAY + i += 1 + end if + + return {json_type,json_value,i} +end function + +export function json_parse_string( sequence js, integer start ) + + jsontype_t json_type = JSON_NONE + object json_value = 0 + + integer i = start + + if i <= length( js ) and js[i] != '"' then + json_push_error( "json_parse_string(): Expected '\"' at position %d", i ) + return {json_type,json_value,i} + end if + + object get_status, get_result, get_offset + {get_status,get_result,get_offset,?} = stdget:value( js, i, GET_LONG_ANSWER ) + + if get_status = GET_SUCCESS and string( get_result ) then + json_type = JSON_STRING + json_value = get_result + i += get_offset + + else + json_push_error( "json_parse_string(): Expected string at position %d", i ) + + end if + + return {json_type,json_value,i} +end function + +export function json_parse_number( sequence js, integer start ) + + jsontype_t json_type = JSON_NONE + object json_value = 0 + + integer i = start + + if i <= length( js ) and find( js[i], "-0123456789" ) = 0 then + json_push_error( "json_parse_number(): Expected digit or '-' at position %d", i ) + return {json_type,json_value,i} + end if + + object get_status, get_result, get_offset + {get_status,get_result,get_offset,?} = stdget:value( js, i, GET_LONG_ANSWER ) + + i += get_offset + + if get_status = GET_SUCCESS and atom( get_result ) then + json_type = JSON_NUMBER + json_value = get_result + else + json_push_error( "json_parse_number(): Expected number at position %d", i ) + end if + + return {json_type,json_value,i} +end function + +export function json_parse_primitive( sequence js, integer start ) + + jsontype_t json_type = JSON_NONE + object json_value = 0 + + integer i = json_skip_whitespace( js, start ) + integer last_i = i + + json_value = "" + + while i <= length( js ) and t_alpha( js[i] ) do + json_value &= js[i] + i += 1 + end while + + if find( json_value, {"true","false","null"} ) then + json_type = JSON_PRIMITIVE + else + json_push_error( "json_parse_primitive(): Expected one of (true,false,null) at position %d", last_i ) + end if + + return {json_type,json_value,i} +end function + +export function json_parse_value( sequence js, integer start ) + + jsontype_t json_type = JSON_NONE + object json_value = 0 + + integer i = json_skip_whitespace( js, start ) + + if i <= length( js ) then + + switch js[i] do + + case '{' then + {json_type,json_value,i} = json_parse_object( js, i ) + + case '[' then + {json_type,json_value,i} = json_parse_array( js, i ) + + case '"' then + {json_type,json_value,i} = json_parse_string( js, i ) + + case '-','0','1','2','3','4','5','6','7','8','9' then + {json_type,json_value,i} = json_parse_number( js, i ) + + case 't','f','n' then + {json_type,json_value,i} = json_parse_primitive( js, i ) + + end switch + + end if + + return {json_type,json_value,i} +end function + +public function json_parse( sequence js ) + + jsontype_t json_type + object json_value + + json_clear_error() + {json_type,json_value,?} = json_parse_value( js, 1 ) + + return {json_type,json_value} +end function + +public function json_parse_file( object file_name ) + + if string( file_name ) then + file_name = open( file_name, "rb", TRUE ) + end if + + return json_parse( read_file(file_name) ) +end function + +public function json_sprint( sequence json_object, integer sorted_keys = TRUE, integer white_space = FALSE, integer indent_width = 4, integer start_column = 0 ) + + sequence inner_pad, outer_pad, one_space, line_break + + if white_space then + inner_pad = repeat( ' ', (start_column+1) * indent_width ) + outer_pad = repeat( ' ', (start_column+0) * indent_width ) + one_space = " " + line_break = "\n" + else + inner_pad = "" + outer_pad = "" + one_space = "" + line_break = "" + end if + + sequence s = "" + + switch json_object[J_TYPE] do + + case JSON_OBJECT then + + sequence pairs = json_object[J_VALUE] + + if sorted_keys then + pairs = sort_by_key( pairs ) + end if + + s &= "{" + if length( pairs ) then + s &= line_break + end if + + for i = 1 to length( pairs ) do + + s &= inner_pad + s &= sprintf( `"%s":`, {pairs[i][1]} ) + s &= one_space + s &= json_sprint( pairs[i][2], sorted_keys, white_space, indent_width, start_column+1 ) + + if i < length( pairs ) then + s &= "," + end if + + s &= line_break + + end for + + if length( pairs ) then + s &= outer_pad + end if + s &= "}" + + case JSON_ARRAY then + + sequence items = json_object[J_VALUE] + + s &= "[" + s &= line_break + + for i = 1 to length( items ) do + + s &= inner_pad + s &= json_sprint( items[i], sorted_keys, white_space, indent_width, start_column+1 ) + + if i < length( items ) then + s &= "," + end if + + s &= line_break + + end for + + s &= outer_pad + s &= "]" + + case JSON_STRING then + s &= sprintf( `"%s"`, {json_escape(json_object[J_VALUE])} ) + + case JSON_NUMBER then + if integer( json_object[J_VALUE] ) then + s &= sprintf( `%d`, {json_object[J_VALUE]} ) + elsif atom( json_object[J_VALUE] ) then + s &= sprintf( `%f`, {json_object[J_VALUE]} ) + end if + + case JSON_PRIMITIVE then + s &= sprintf( `%s`, {json_object[J_VALUE]} ) + + end switch + + return s +end function + +public procedure json_print( object file_name, sequence json_object, integer sorted_keys = TRUE, integer white_space = FALSE, integer indent_width = 4, integer start_column = 0 ) + + if string( file_name ) then + file_name = open( file_name, "wb", TRUE ) + end if + + puts( file_name, json_sprint(json_object, sorted_keys, white_space, indent_width, start_column) ) + +end procedure + +sequence PRETTY_MARKUP = PRETTY_DEFAULT +PRETTY_MARKUP[DISPLAY_ASCII] = 2 -- display as "string" +PRETTY_MARKUP[LINE_BREAKS] = 0 -- no line breaks + +public function json_markup( object json_object, integer sorted_keys = TRUE, integer white_space = TRUE, integer indent_width = 4, integer start_column = 0 ) + + sequence inner_pad, outer_pad, one_space, line_break + + if white_space then + inner_pad = repeat( ' ', (start_column+1) * indent_width ) + outer_pad = repeat( ' ', (start_column+0) * indent_width ) + one_space = " " + line_break = "\n" + else + inner_pad = "" + outer_pad = "" + one_space = "" + line_break = "" + end if + + sequence s = "{" + + switch json_object[J_TYPE] do + + case JSON_OBJECT then + + s &= "JSON_OBJECT," + s &= one_space + + sequence pairs = json_object[J_VALUE] + + if sorted_keys then + pairs = sort_by_key( pairs ) + end if + + s &= "{" + s &= line_break + + for i = 1 to length( pairs ) do + + s &= inner_pad + s &= "{" + s &= sprintf( `"%s",`, {pairs[i][1]} ) + s &= one_space + s &= json_markup( pairs[i][2], sorted_keys, white_space, indent_width, start_column+1 ) + s &= "}" + + if i < length( pairs ) then + s &= "," + end if + + s &= line_break + + end for + + s &= outer_pad + s &= "}" + + case JSON_ARRAY then + + s &= "JSON_ARRAY," + s &= one_space + + sequence items = json_object[J_VALUE] + + s &= "{" + s &= line_break + + for i = 1 to length( items ) do + + s &= inner_pad + s &= json_markup( items[i], sorted_keys, white_space, indent_width, start_column+1 ) + + if i < length( items ) then + s &= "," + end if + + s &= line_break + + end for + + s &= outer_pad + s &= "}" + + case JSON_STRING then + s &= "JSON_STRING," + s &= one_space + s &= pretty_sprint( json_object[J_VALUE], PRETTY_MARKUP ) + + case JSON_NUMBER then + s &= "JSON_NUMBER," + s &= one_space + s &= pretty_sprint( json_object[J_VALUE], PRETTY_MARKUP ) + + case JSON_PRIMITIVE then + s &= "JSON_PRIMITIVE," + s &= one_space + s &= sprintf( `"%s"`, {json_object[J_VALUE]} ) + + end switch + + s &= '}' + + return s +end function + +public function json_compare( sequence json_a, sequence json_b ) + + integer result + + result = compare( json_a[J_TYPE], json_b[J_TYPE] ) + if result != 0 then + return result + end if + + if json_a[J_TYPE] = JSON_OBJECT then + + sequence pairs_a = json_a[J_VALUE] + sequence pairs_b = json_b[J_VALUE] + + if length( pairs_a ) != length( pairs_b ) then + return length( pairs_a ) - length( pairs_b ) + end if + + for i = 1 to length( pairs_a ) do + + result = json_compare( + pairs_a[i][2], + pairs_b[i][2] + ) + + if result != 0 then + exit + end if + + end for + + return result + + elsif json_a[J_TYPE] = JSON_ARRAY then + + for i = 1 to length( json_a[J_VALUE] ) do + + result = json_compare( + json_a[J_VALUE][i], + json_b[J_VALUE][i] + ) + + if result != 0 then + exit + end if + + end for + + return result + + end if + + result = compare( json_a[J_VALUE], json_b[J_VALUE] ) + + return result +end function + +public function json_append( object json_target, object json_object ) + + switch json_target[J_TYPE] do + + case JSON_NUMBER, JSON_PRIMITIVE then + json_target[J_VALUE] = {json_target} & {json_object} + json_target[J_TYPE] = JSON_ARRAY + + case JSON_OBJECT, JSON_ARRAY, JSON_STRING then + json_target[J_VALUE] = json_target[J_VALUE] & json_object[J_VALUE] + + end switch + + return json_target +end function + +public function json_haskey( object json_object, sequence keys, object sep = '.' ) + + if string( keys ) then + keys = stdseq:split( keys, sep ) + end if + + integer i = 1 + integer found = 0 + + while json_object[J_TYPE] = JSON_OBJECT and i <= length( keys ) do + + found = 0 + + for j = 1 to length( json_object[J_VALUE] ) do + + if equal( json_object[J_VALUE][j][1], keys[i] ) then + found = j + json_object = json_object[J_VALUE][j][2] + exit + end if + + end for + + if found = 0 then + json_object = {JSON_NONE,0} + exit + end if + + i += 1 + end while + + return (found != 0) +end function + +public function json_fetch( object json_object, sequence keys, object sep = '.' ) + + if string( keys ) then + keys = stdseq:split( keys, sep ) + end if + + integer i = 1 + integer found = 0 + + while json_object[J_TYPE] = JSON_OBJECT and i <= length( keys ) do + + found = 0 + + -- TODO: fix key indexing, e.g. "foo.bar[1]" + + -- if t_digit( keys[i] ) then + -- keys[i] = to_integer( keys[i] ) + -- end if + + for j = 1 to length( json_object[J_VALUE] ) do + + if integer( keys[i] ) and equal( keys[i], j ) then + json_object = json_object[J_VALUE][j][2] + found = j + exit + + elsif equal( json_object[J_VALUE][j][1], keys[i] ) then + json_object = json_object[J_VALUE][j][2] + found = j + exit + + end if + + end for + + if found = 0 then + json_object = {JSON_NONE,0} + exit + end if + + i += 1 + end while + + return json_object +end function + +public function json_fetch_array( object json_object, sequence keys, object sep = '.' ) + + if json_haskey( json_object, keys, sep ) then + + object json_value = json_fetch( json_object, keys, sep ) + + if json_value[J_TYPE] = JSON_ARRAY then + + for i = 1 to length( json_value[J_VALUE] ) do + json_value[J_VALUE][i] = json_value[J_VALUE][i][J_VALUE] + end for + + return json_value[J_VALUE] + end if + + end if + + return {} +end function + +public function json_fetch_number( object json_object, sequence keys, object sep = '.' ) + + if json_haskey( json_object, keys, sep ) then + + object json_value = json_fetch( json_object, keys, sep ) + + if json_value[J_TYPE] = JSON_NUMBER then + return json_value[J_VALUE] + end if + + return to_number( json_value[J_VALUE] ) + end if + + return 0 +end function + +public function json_fetch_string( object json_object, sequence keys, object sep = '.' ) + + if json_haskey( json_object, keys, sep ) then + + object json_value = json_fetch( json_object, keys, sep ) + + if json_value[J_TYPE] = JSON_STRING then + return json_value[J_VALUE] + end if + + return to_string( json_value[J_VALUE] ) + end if + + return "" +end function + +public function json_remove( object json_object, sequence keys, object sep = '.' ) + + if string( keys ) then + keys = stdseq:split( keys, sep ) + end if + + integer i = 1 + + while json_object[J_TYPE] = JSON_OBJECT and i < length( json_object[J_VALUE] ) do + + if equal( json_object[J_VALUE][i][1], keys[1] ) then + + if length( keys ) = 1 then + json_object[J_VALUE] = remove( json_object[J_VALUE], i ) + continue + else + json_object[J_VALUE][i][2] = json_remove( json_object[J_VALUE][i][2], keys[2..$] ) + end if + + end if + + i += 1 + end while + + return json_object +end function + +public function json_import( object map_object ) + + jsontype_t json_type = JSON_NONE + object json_value = 0 + + json_clear_error() + + if not map( map_object ) then + json_push_error( "json_import(): Expected a map object" ) + return {json_type,json_value} + end if + + json_value = {} + + integer json_error = FALSE + sequence map_keys = map:keys( map_object ) + + for i = 1 to length( map_keys ) do + + object key_object = map_keys[i] + + if not string( key_object ) then + json_push_error( "json_import(): Invalid JSON key `%s`", {sprint(key_object)} ) + json_error = TRUE + exit + end if + + jsontype_t value_type = JSON_NONE + object value_object = map:get( map_object, key_object ) + + if atom( value_object ) then + value_type = JSON_NUMBER + elsif string( value_object ) then + value_type = JSON_STRING + else + json_push_error( "json_import(): Invalid JSON value `%s`", {sprint(value_object)} ) + json_error = TRUE + exit + end if + + json_value = append( json_value, {key_object,{value_type,value_object}} ) + + end for + + if not json_error then + json_type = JSON_OBJECT + end if + + return {json_type,json_value} +end function diff --git a/bin/run-in-docker.sh b/bin/run-in-docker.sh index 79a3ece..f75f4fa 100755 --- a/bin/run-in-docker.sh +++ b/bin/run-in-docker.sh @@ -33,7 +33,7 @@ output_dir=$(realpath "${3%/}") mkdir -p "${output_dir}" # Build the Docker image -docker build --rm -t exercism/test-runner . +docker build --rm -t exercism/euphoria-test-runner . # Run the Docker image using the settings mimicking the production environment docker run \ @@ -43,4 +43,4 @@ docker run \ --mount type=bind,src="${solution_dir}",dst=/solution \ --mount type=bind,src="${output_dir}",dst=/output \ --mount type=tmpfs,dst=/tmp \ - exercism/test-runner "${slug}" /solution /output + exercism/euphoria-test-runner "${slug}" /solution /output diff --git a/bin/run-tests-in-docker.sh b/bin/run-tests-in-docker.sh index 8078a67..ff2311f 100755 --- a/bin/run-tests-in-docker.sh +++ b/bin/run-tests-in-docker.sh @@ -16,16 +16,15 @@ set -e # Build the Docker image -docker build --rm -t exercism/test-runner . +docker build --rm -t exercism/euphoria-test-runner . # Run the Docker image using the settings mimicking the production environment docker run \ --rm \ --network none \ - --read-only \ --mount type=bind,src="${PWD}/tests",dst=/opt/test-runner/tests \ --mount type=tmpfs,dst=/tmp \ --volume "${PWD}/bin/run-tests.sh:/opt/test-runner/bin/run-tests.sh" \ --workdir /opt/test-runner \ --entrypoint /opt/test-runner/bin/run-tests.sh \ - exercism/test-runner + exercism/euphoria-test-runner diff --git a/bin/run.ex b/bin/run.ex new file mode 100644 index 0000000..0d8a2b9 --- /dev/null +++ b/bin/run.ex @@ -0,0 +1,127 @@ +include std/filesys.e +include std/cmdline.e +include std/io.e +include std/sequence.e as seq +include std/search.e as srch +include std/regex.e as re +include std/text.e +include std/convert.e + +include json.e + +without trace + +integer false = 0 +integer true = not false +re:regex err_id = re:new("<([0-9]+)>::(.*)") + +function first_failure(sequence lines, sequence fallback) + for i = 1 to length(lines) do + sequence line = lines[i] + if match("failed:", line) then + return trim(line) + end if + end for + return fallback +end function + +function failures(sequence txt) + sequence parts = seq:split(txt,", ") + for i = 1 to length(parts) do + if match("failed",parts[i]) then + return parts[i] + end if + end for + return "" +end function + +function check_for_error(sequence lines, atom current) + sequence message = "" + object res = re:matches(err_id, lines[current]) + if not atom(res) then + message = res[1] + atom err_number = to_integer(res[2]) + if err_number = 74 then + -- need to extract the next line of the data[i] + message &= (" " & trim(lines[current+1])) + end if + return {true, message} + else + return {false, message} + end if +end function + +function check_for_failure(sequence lines, atom current) + sequence message = "" + integer result = match("100% success", lines[current]) + if result then + return {false, message} + end if + + result = match("% success", lines[current]) + if result then + return {true, first_failure(lines, lines[current])} + else + return {false, message} + end if +end function + +procedure process(sequence slug, sequence soln_folder, sequence outp_folder) + sequence solution_dir = canonical_path(soln_folder) + sequence output_dir = canonical_path(outp_folder) + sequence results_file = join_path({output_dir, "/results.json"}) + + puts(1, solution_dir & "\n") + puts(1, output_dir & "\n") + puts(1, results_file & "\n") + + create_directory(output_dir) + printf(1, "%s: testing...", {slug}) + sequence outfile = join_path({output_dir,"t_" & slug & ".out"}) + sequence cmd = build_commandline({"eutest",join_path({solution_dir,"t_" & slug & ".e"}),">", outfile}) + system(cmd,2) + + atom ifh = open(outfile, "r") + sequence data = read_lines(ifh) + close(ifh) + + sequence status = "pass" + sequence message = "" + + --trace(1) + + for i = 1 to length(data) do + sequence response = check_for_error(data, i) + if response[1] then + status = "error" + message = response[2] + exit + end if + + response = check_for_failure(data, i) + if response[1] then + status = "fail" + message = response[2] + exit + end if + end for + + sequence JSON = {JSON_OBJECT, + { + {"version", {JSON_NUMBER, 1}}, + {"status", {JSON_STRING, status}}, + {"message", {JSON_STRING, message}} + } + } + + atom ofh = open(results_file,"w") + json_print(ofh, JSON, false) + close(ofh) +end procedure + +sequence cmdline = command_line() +if (length(cmdline) < 5) then + puts(1, "usage: eui ./bin/run.ex exercise-slug path/to/solution/folder/ path/to/output/directory/\n") +else + process(cmdline[3], cmdline[4], cmdline[5]) +end if \ No newline at end of file diff --git a/bin/run.sh b/bin/run.sh index f1e8b78..d01ba1b 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -33,28 +33,6 @@ echo "${slug}: testing..." # Run the tests for the provided implementation file and redirect stdout and # stderr to capture it -test_output=$(false) -# TODO: substitute "false" with the actual command to run the test: -# test_output=$(command_to_run_tests 2>&1) - -# Write the results.json file based on the exit code of the command that was -# just executed that tested the implementation file -if [ $? -eq 0 ]; then - jq -n '{version: 1, status: "pass"}' > ${results_file} -else - # OPTIONAL: Sanitize the output - # In some cases, the test output might be overly verbose, in which case stripping - # the unneeded information can be very helpful to the student - # sanitized_test_output=$(printf "${test_output}" | sed -n '/Test results:/,$p') - - # OPTIONAL: Manually add colors to the output to help scanning the output for errors - # If the test output does not contain colors to help identify failing (or passing) - # tests, it can be helpful to manually add colors to the output - # colorized_test_output=$(echo "${test_output}" \ - # | GREP_COLOR='01;31' grep --color=always -E -e '^(ERROR:.*|.*failed)$|$' \ - # | GREP_COLOR='01;32' grep --color=always -E -e '^.*passed$|$') - - jq -n --arg output "${test_output}" '{version: 1, status: "fail", message: $output}' > ${results_file} -fi +eui bin/run.ex "${slug}" ${solution_dir} "${output_dir}" echo "${slug}: done" diff --git a/tests/all-fail/all-fail.ex b/tests/all-fail/all-fail.ex new file mode 100644 index 0000000..bb518a0 --- /dev/null +++ b/tests/all-fail/all-fail.ex @@ -0,0 +1,3 @@ +public function leap(integer year) + return not (remainder(year, 4) = 0 and (remainder(year, 100) != 0 or remainder(year, 400) = 0)) +end function \ No newline at end of file diff --git a/tests/all-fail/expected_results.json b/tests/all-fail/expected_results.json new file mode 100644 index 0000000..fc0bcd5 --- /dev/null +++ b/tests/all-fail/expected_results.json @@ -0,0 +1 @@ +{"version":1,"status":"fail","message":"Files (run: 1) (failed: 1) (0% success)"} \ No newline at end of file diff --git a/tests/all-fail/t_all-fail.e b/tests/all-fail/t_all-fail.e new file mode 100644 index 0000000..6efdffd --- /dev/null +++ b/tests/all-fail/t_all-fail.e @@ -0,0 +1,17 @@ +include std/unittest.e + +include all-fail.ex + +set_test_verbosity(TEST_SHOW_ALL) + +test_false("year not divisible by 4 in common year" , leap(1800)) +test_false("year divisible by 100 but not by 3 is still not a leap year" , leap(1900)) +test_false("year divisible by 2, not divisible by 4 in common year" , leap(1970)) +test_false("year divisible by 200, not divisible by 400 in common year" , leap(2015)) +test_false("year divisible by 100, not divisible by 400 in common year" , leap(2100)) +test_true("year divisible by 4 and 5 is still a leap year" , leap(1960)) +test_true("year divisible by 4, not divisible by 100 in leap year" , leap(1996)) +test_true("year divisible by 400 is leap year" , leap(2000)) +test_true("year divisible by 400 but not by 125 is still a leap year" , leap(2400)) + +test_report() diff --git a/tests/empty-file/empty-file.ex b/tests/empty-file/empty-file.ex new file mode 100644 index 0000000..e69de29 diff --git a/tests/empty-file/expected_results.json b/tests/empty-file/expected_results.json new file mode 100644 index 0000000..fc0bcd5 --- /dev/null +++ b/tests/empty-file/expected_results.json @@ -0,0 +1 @@ +{"version":1,"status":"fail","message":"Files (run: 1) (failed: 1) (0% success)"} \ No newline at end of file diff --git a/tests/empty-file/t_empty-file.e b/tests/empty-file/t_empty-file.e new file mode 100644 index 0000000..625a9f4 --- /dev/null +++ b/tests/empty-file/t_empty-file.e @@ -0,0 +1,17 @@ +include std/unittest.e + +include empty-file.ex + +set_test_verbosity(TEST_SHOW_ALL) + +test_false("year not divisible by 4 in common year" , leap(1800)) +test_false("year divisible by 100 but not by 3 is still not a leap year" , leap(1900)) +test_false("year divisible by 2, not divisible by 4 in common year" , leap(1970)) +test_false("year divisible by 200, not divisible by 400 in common year" , leap(2015)) +test_false("year divisible by 100, not divisible by 400 in common year" , leap(2100)) +test_true("year divisible by 4 and 5 is still a leap year" , leap(1960)) +test_true("year divisible by 4, not divisible by 100 in leap year" , leap(1996)) +test_true("year divisible by 400 is leap year" , leap(2000)) +test_true("year divisible by 400 but not by 125 is still a leap year" , leap(2400)) + +test_report() diff --git a/tests/example-all-fail/expected_results.json b/tests/example-all-fail/expected_results.json deleted file mode 100644 index 9ef8b6f..0000000 --- a/tests/example-all-fail/expected_results.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": 1, - "status": "fail", - "message": "TODO: replace with correct output" -} diff --git a/tests/example-empty-file/expected_results.json b/tests/example-empty-file/expected_results.json deleted file mode 100644 index 9ef8b6f..0000000 --- a/tests/example-empty-file/expected_results.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": 1, - "status": "fail", - "message": "TODO: replace with correct output" -} diff --git a/tests/example-partial-fail/expected_results.json b/tests/example-partial-fail/expected_results.json deleted file mode 100644 index 9ef8b6f..0000000 --- a/tests/example-partial-fail/expected_results.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": 1, - "status": "fail", - "message": "TODO: replace with correct output" -} diff --git a/tests/example-success/expected_results.json b/tests/example-success/expected_results.json deleted file mode 100644 index 6c2223e..0000000 --- a/tests/example-success/expected_results.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "version": 1, - "status": "pass" -} diff --git a/tests/example-syntax-error/expected_results.json b/tests/example-syntax-error/expected_results.json deleted file mode 100644 index afc4d4e..0000000 --- a/tests/example-syntax-error/expected_results.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": 1, - "status": "error", - "message": "TODO: replace with correct output" -} diff --git a/tests/partial-fail/expected_results.json b/tests/partial-fail/expected_results.json new file mode 100644 index 0000000..fc0bcd5 --- /dev/null +++ b/tests/partial-fail/expected_results.json @@ -0,0 +1 @@ +{"version":1,"status":"fail","message":"Files (run: 1) (failed: 1) (0% success)"} \ No newline at end of file diff --git a/tests/partial-fail/partial-fail.ex b/tests/partial-fail/partial-fail.ex new file mode 100644 index 0000000..2a17d6f --- /dev/null +++ b/tests/partial-fail/partial-fail.ex @@ -0,0 +1,3 @@ +public function leap(integer year) + return remainder(year, 4) = 0 +end function diff --git a/tests/partial-fail/t_partial-fail.e b/tests/partial-fail/t_partial-fail.e new file mode 100644 index 0000000..68f60e8 --- /dev/null +++ b/tests/partial-fail/t_partial-fail.e @@ -0,0 +1,17 @@ +include std/unittest.e + +include partial-fail.ex + +set_test_verbosity(TEST_SHOW_ALL) + +test_false("year not divisible by 4 in common year" , leap(1800)) +test_false("year divisible by 100 but not by 3 is still not a leap year" , leap(1900)) +test_false("year divisible by 2, not divisible by 4 in common year" , leap(1970)) +test_false("year divisible by 200, not divisible by 400 in common year" , leap(2015)) +test_false("year divisible by 100, not divisible by 400 in common year" , leap(2100)) +test_true("year divisible by 4 and 5 is still a leap year" , leap(1960)) +test_true("year divisible by 4, not divisible by 100 in leap year" , leap(1996)) +test_true("year divisible by 400 is leap year" , leap(2000)) +test_true("year divisible by 400 but not by 125 is still a leap year" , leap(2400)) + +test_report() diff --git a/tests/success/expected_results.json b/tests/success/expected_results.json new file mode 100644 index 0000000..35bfc76 --- /dev/null +++ b/tests/success/expected_results.json @@ -0,0 +1 @@ +{"version":1,"status":"pass","message":""} \ No newline at end of file diff --git a/tests/success/success.ex b/tests/success/success.ex new file mode 100644 index 0000000..2a667bd --- /dev/null +++ b/tests/success/success.ex @@ -0,0 +1,3 @@ +public function leap(integer year) + return remainder(year, 4) = 0 and (remainder(year, 100) != 0 or remainder(year, 400) = 0) +end function diff --git a/tests/success/t_success.e b/tests/success/t_success.e new file mode 100644 index 0000000..25da4f9 --- /dev/null +++ b/tests/success/t_success.e @@ -0,0 +1,17 @@ +include std/unittest.e + +include success.ex + +set_test_verbosity(TEST_SHOW_ALL) + +test_false("year not divisible by 4 in common year" , leap(1800)) +test_false("year divisible by 100 but not by 3 is still not a leap year" , leap(1900)) +test_false("year divisible by 2, not divisible by 4 in common year" , leap(1970)) +test_false("year divisible by 200, not divisible by 400 in common year" , leap(2015)) +test_false("year divisible by 100, not divisible by 400 in common year" , leap(2100)) +test_true("year divisible by 4 and 5 is still a leap year" , leap(1960)) +test_true("year divisible by 4, not divisible by 100 in leap year" , leap(1996)) +test_true("year divisible by 400 is leap year" , leap(2000)) +test_true("year divisible by 400 but not by 125 is still a leap year" , leap(2400)) + +test_report() diff --git a/tests/syntax-error/expected_results.json b/tests/syntax-error/expected_results.json new file mode 100644 index 0000000..fc0bcd5 --- /dev/null +++ b/tests/syntax-error/expected_results.json @@ -0,0 +1 @@ +{"version":1,"status":"fail","message":"Files (run: 1) (failed: 1) (0% success)"} \ No newline at end of file diff --git a/tests/syntax-error/syntax-error.ex b/tests/syntax-error/syntax-error.ex new file mode 100644 index 0000000..cca2b04 --- /dev/null +++ b/tests/syntax-error/syntax-error.ex @@ -0,0 +1 @@ +publ1367A&#^&*^#b134 diff --git a/tests/syntax-error/t_syntax-error.e b/tests/syntax-error/t_syntax-error.e new file mode 100644 index 0000000..9946508 --- /dev/null +++ b/tests/syntax-error/t_syntax-error.e @@ -0,0 +1,17 @@ +include std/unittest.e + +include syntax-error.ex + +set_test_verbosity(TEST_SHOW_ALL) + +test_false("year not divisible by 4 in common year" , leap(1800)) +test_false("year divisible by 100 but not by 3 is still not a leap year" , leap(1900)) +test_false("year divisible by 2, not divisible by 4 in common year" , leap(1970)) +test_false("year divisible by 200, not divisible by 400 in common year" , leap(2015)) +test_false("year divisible by 100, not divisible by 400 in common year" , leap(2100)) +test_true("year divisible by 4 and 5 is still a leap year" , leap(1960)) +test_true("year divisible by 4, not divisible by 100 in leap year" , leap(1996)) +test_true("year divisible by 400 is leap year" , leap(2000)) +test_true("year divisible by 400 but not by 125 is still a leap year" , leap(2400)) + +test_report()