From c6d9276f39a5f08592573d86ab0c8584129a9e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Fri, 5 Jul 2024 23:10:18 +0300 Subject: [PATCH 1/9] Started work on generators --- docs/GENERATORS.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/GENERATORS.md diff --git a/docs/GENERATORS.md b/docs/GENERATORS.md new file mode 100644 index 00000000..72895bd2 --- /dev/null +++ b/docs/GENERATORS.md @@ -0,0 +1 @@ +# Test generators From 2b9f7a830c4bfbb2b2b1deb5b682480119aaf37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Sat, 6 Jul 2024 15:49:05 +0300 Subject: [PATCH 2/9] Implemented command-line interface --- generators/TestGeneratorTool/.gitignore | 42 ++++ generators/TestGeneratorTool/build.gradle | 20 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + generators/TestGeneratorTool/gradlew | 234 ++++++++++++++++++ generators/TestGeneratorTool/gradlew.bat | 89 +++++++ generators/TestGeneratorTool/settings.gradle | 2 + .../main/groovy/CommandLineInterface.groovy | 17 ++ .../main/groovy/ICommandLineInterface.groovy | 12 + .../src/main/groovy/Main.groovy | 23 ++ .../groovy/CommandLineInterfaceSpec.groovy | 51 ++++ 11 files changed, 496 insertions(+) create mode 100644 generators/TestGeneratorTool/.gitignore create mode 100644 generators/TestGeneratorTool/build.gradle create mode 100644 generators/TestGeneratorTool/gradle/wrapper/gradle-wrapper.jar create mode 100644 generators/TestGeneratorTool/gradle/wrapper/gradle-wrapper.properties create mode 100644 generators/TestGeneratorTool/gradlew create mode 100644 generators/TestGeneratorTool/gradlew.bat create mode 100644 generators/TestGeneratorTool/settings.gradle create mode 100644 generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy create mode 100644 generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy create mode 100644 generators/TestGeneratorTool/src/main/groovy/Main.groovy create mode 100644 generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy diff --git a/generators/TestGeneratorTool/.gitignore b/generators/TestGeneratorTool/.gitignore new file mode 100644 index 00000000..b63da455 --- /dev/null +++ b/generators/TestGeneratorTool/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/generators/TestGeneratorTool/build.gradle b/generators/TestGeneratorTool/build.gradle new file mode 100644 index 00000000..afaecb39 --- /dev/null +++ b/generators/TestGeneratorTool/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'groovy' +} + +group = 'org.example' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.codehaus.groovy:groovy-all:3.0.22" + implementation 'info.picocli:picocli:4.7.6' + testImplementation "org.spockframework:spock-core:2.1-groovy-3.0" +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/generators/TestGeneratorTool/gradle/wrapper/gradle-wrapper.jar b/generators/TestGeneratorTool/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/generators/TestGeneratorTool/gradlew.bat b/generators/TestGeneratorTool/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/generators/TestGeneratorTool/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/generators/TestGeneratorTool/settings.gradle b/generators/TestGeneratorTool/settings.gradle new file mode 100644 index 00000000..b5fcf3b1 --- /dev/null +++ b/generators/TestGeneratorTool/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'TestGeneratorTool' + diff --git a/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy b/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy new file mode 100644 index 00000000..abd72de0 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy @@ -0,0 +1,17 @@ +import groovy.cli.picocli.CliBuilder + +class CommandLineInterface { + CliBuilder builder + + CommandLineInterface() { + this.builder = new CliBuilder() + } + + ICommandLineInterface parse(String[] args) { + this.builder.parseFromSpec(ICommandLineInterface, args) + } + + void showUsage() { + this.builder.usage() + } +} \ No newline at end of file diff --git a/generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy b/generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy new file mode 100644 index 00000000..467d1900 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy @@ -0,0 +1,12 @@ +import groovy.cli.Option + +interface ICommandLineInterface { + @Option(shortName = 'h', description = 'Display usage.') + Boolean help() + + @Option(shortName = 'i', longName = 'canonical-data', description = 'Set path to canonical-data.json file.') + String canonical_data() + + @Option(shortName = 'd', longName = 'repository-directory', description = 'Set path to repository directory.') + String repository_directory() +} \ No newline at end of file diff --git a/generators/TestGeneratorTool/src/main/groovy/Main.groovy b/generators/TestGeneratorTool/src/main/groovy/Main.groovy new file mode 100644 index 00000000..12704051 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/Main.groovy @@ -0,0 +1,23 @@ +static void main(String[] args) { + CommandLineInterface commandLineInterface = new CommandLineInterface() + ICommandLineInterface options = commandLineInterface.parse args + + if (options.help()) { + commandLineInterface.showUsage() + return + } + + String canonical_data = options.canonical_data() + if (canonical_data == null) { + println 'Path to canonical-data.json not set.' + return + } + + String repository_directory = options.repository_directory() + if (repository_directory == null) { + println 'Path to repository directory not set.' + return + } + + println 'Success!' +} diff --git a/generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy new file mode 100644 index 00000000..78f47e7e --- /dev/null +++ b/generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy @@ -0,0 +1,51 @@ +import spock.lang.* + +class CommandLineInterfaceSpec extends Specification { + def "Can get help on CLI usage, using short notation."() { + given: + ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'-h'}) + + expect: + options.help() == true + } + + def "Can get help on CLI usage, using long notation."() { + given: + ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'--help'}) + + expect: + options.help() == true + } + + def "Can set path to canonical-data.json file, using short notation."() { + given: + ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'-i', 'C:\\Users\\Foo\\canonical-data.json'}) + + expect: + options.canonical_data() == 'C:\\Users\\Foo\\canonical-data.json' + } + + def "Can set path to canonical-data.json file, using long notation."() { + given: + ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'--canonical-data=C:\\Users\\Foo\\canonical-data.json'}) + + expect: + options.canonical_data() == 'C:\\Users\\Foo\\canonical-data.json' + } + + def "Can set repository directory, using short notation."() { + given: + ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'-d', 'C:\\Users\\Foo\\Groovy'}) + + expect: + options.repository_directory() == 'C:\\Users\\Foo\\Groovy' + } + + def "Can set repository directory, using long notation."() { + given: + ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'--repository-directory=C:\\Users\\Foo\\Groovy'}) + + expect: + options.repository_directory() == 'C:\\Users\\Foo\\Groovy' + } +} From 581fa6a98dca466cb013b0e6b338392269e57a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Sat, 6 Jul 2024 21:41:47 +0300 Subject: [PATCH 3/9] Created a parser for canonical data --- .../main/groovy/CanonicalDataParser.groovy | 35 +++++++ .../main/groovy/CommandLineInterface.groovy | 6 +- .../src/main/groovy/LabeledTestCase.groovy | 13 +++ .../src/main/groovy/Main.groovy | 9 +- .../groovy/CanonicalDataParserSpec.groovy | 94 +++++++++++++++++++ 5 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy create mode 100644 generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy create mode 100644 generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy diff --git a/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy b/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy new file mode 100644 index 00000000..aa609306 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy @@ -0,0 +1,35 @@ +import groovy.json.JsonSlurper + +class CanonicalDataParser { + LinkedHashMap specification + + CanonicalDataParser(String source) { + specification = new JsonSlurper().parseText(source) as LinkedHashMap + } + + String getExerciseSlug() { + specification["exercise"] + } + + ArrayList getTestCasesHelper(ArrayList testItems) { + ArrayList collectedItems = [] + for (LinkedHashMap labeledTestItem in testItems) { + if (labeledTestItem.containsKey 'cases') { + collectedItems.addAll(getTestCasesHelper(labeledTestItem['cases'] as ArrayList)) + } else { + boolean containsDescription = labeledTestItem.containsKey 'description' + boolean containsProperty = labeledTestItem.containsKey 'property' + boolean containsInput = labeledTestItem.containsKey 'input' + boolean containsExpected = labeledTestItem.containsKey 'expected' + if (containsDescription && containsProperty && containsInput && containsExpected) { + collectedItems.add(new LabeledTestCase(labeledTestItem['description'] as String, labeledTestItem['property'] as String, labeledTestItem['input'], labeledTestItem['expected'])) + } + } + } + collectedItems + } + + ArrayList getLabeledTestCases() { + getTestCasesHelper(specification['cases'] as ArrayList) + } +} diff --git a/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy b/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy index abd72de0..fa19d625 100644 --- a/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy @@ -4,14 +4,14 @@ class CommandLineInterface { CliBuilder builder CommandLineInterface() { - this.builder = new CliBuilder() + builder = new CliBuilder() } ICommandLineInterface parse(String[] args) { - this.builder.parseFromSpec(ICommandLineInterface, args) + builder.parseFromSpec ICommandLineInterface, args } void showUsage() { - this.builder.usage() + builder.usage() } } \ No newline at end of file diff --git a/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy b/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy new file mode 100644 index 00000000..46751eb2 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy @@ -0,0 +1,13 @@ +class LabeledTestCase { + String description + String property + Object input + Object expected + + LabeledTestCase(String description, String property, Object input, Object expected) { + this.description = description + this.property = property + this.input = input + this.expected = expected + } +} diff --git a/generators/TestGeneratorTool/src/main/groovy/Main.groovy b/generators/TestGeneratorTool/src/main/groovy/Main.groovy index 12704051..9049d39a 100644 --- a/generators/TestGeneratorTool/src/main/groovy/Main.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/Main.groovy @@ -1,3 +1,6 @@ +import java.nio.file.Files +import java.nio.file.Paths + static void main(String[] args) { CommandLineInterface commandLineInterface = new CommandLineInterface() ICommandLineInterface options = commandLineInterface.parse args @@ -19,5 +22,9 @@ static void main(String[] args) { return } - println 'Success!' + CanonicalDataParser specification = new CanonicalDataParser(Files.readString(Paths.get(canonical_data))) + String exerciseSlug = specification.exerciseSlug + ArrayList testCases = specification.labeledTestCases + int testCount = testCases.size() + println "We are going to implement $testCount tests for '$exerciseSlug' exercise." } diff --git a/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy new file mode 100644 index 00000000..bd0f3376 --- /dev/null +++ b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy @@ -0,0 +1,94 @@ +import spock.lang.* +import groovy.json.JsonBuilder + +class CanonicalDataParserSpec extends Specification { + + @Shared + String sample + + def setupSpec() { + JsonBuilder builder = new JsonBuilder() + builder { + exercise 'foobar' + comments 'blah', 'blah', 'blah' + cases( + { + uuid '31e9db74-86b9-4b14-a320-9ea910337289' + description 'Foo a word to reverse it' + property 'foo' + input { + word 'time' + } + expected 'emit' + }, + { + uuid '09113ce5-b008-45d0-98af-c0378b64966b' + description 'Bar a name to combine its parts' + property 'foo' + input { + firstName 'Alan' + lastName 'Smith' + } + expected 'Alan Smith' + }, + { + description 'Abnormal inputs: numbers' + cases( + { + uuid 'f22d7a03-e752-4f14-9231-4eae9f128cef' + description 'Foo of a number returns nothing' + property 'foo' + input { + word 42 + } + expected null + }, + { + uuid '8790a635-e8a8-4343-a29f-7da2929b9378' + description 'Foo of a very big number returns nothing' + comments 'Making this test case pass requires usage of big integer libraries.' + scenarios 'big-integers' + property 'foo' + input { + word 28948022309329048855892746252171976962977213799489202546401021394546514198529 + } + expected null + }, + { + uuid 'c7b6f24a-553f-475a-8a40-dba854fe1bff' + description 'Bar a name with numbers gives an error' + property 'bar' + input { + firstName 'Agent' + lastName 007 + } + expected { + error 'You should never bar a number' + } + } + ) + } + ) + } + sample = builder.toString() + } + + def "Can retrieve exercise slug."() { + expect: + new CanonicalDataParser(sample).exerciseSlug == 'foobar' + } + + def "Can retrieve data for each test case."() { + when: + CanonicalDataParser specification = new CanonicalDataParser(sample) + ArrayList testCases = specification.labeledTestCases + + then: + testCases.size() == 5 + testCases[0].description == 'Foo a word to reverse it' + testCases[1].property == 'foo' + testCases[2].input == [word: 42] + testCases[3].expected == null + testCases[4].expected == [error: 'You should never bar a number'] + } +} From 008eac1c9b641fc889e1d91b95efab1b8d3a601f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Sun, 7 Jul 2024 13:50:14 +0300 Subject: [PATCH 4/9] Created test cases renderer --- .../src/main/groovy/Casing.groovy | 4 + .../src/main/groovy/Identifier.groovy | 44 +++++++ .../src/main/groovy/Main.groovy | 14 ++- .../src/main/groovy/TestCasesRenderer.groovy | 32 +++++ .../groovy/CanonicalDataParserSpec.groovy | 10 +- .../groovy/CommandLineInterfaceSpec.groovy | 12 +- .../test/groovy/TestCasesRendererSpec.groovy | 115 ++++++++++++++++++ .../templates/TestClass.template | 9 ++ .../templates/TestMethod.template | 9 ++ 9 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 generators/TestGeneratorTool/src/main/groovy/Casing.groovy create mode 100644 generators/TestGeneratorTool/src/main/groovy/Identifier.groovy create mode 100644 generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy create mode 100644 generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy create mode 100644 generators/TestGeneratorTool/templates/TestClass.template create mode 100644 generators/TestGeneratorTool/templates/TestMethod.template diff --git a/generators/TestGeneratorTool/src/main/groovy/Casing.groovy b/generators/TestGeneratorTool/src/main/groovy/Casing.groovy new file mode 100644 index 00000000..6e50512b --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/Casing.groovy @@ -0,0 +1,4 @@ +enum Casing { + PascalCase, + KebabCase +} \ No newline at end of file diff --git a/generators/TestGeneratorTool/src/main/groovy/Identifier.groovy b/generators/TestGeneratorTool/src/main/groovy/Identifier.groovy new file mode 100644 index 00000000..d8788e30 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/Identifier.groovy @@ -0,0 +1,44 @@ +class Identifier { + String[] tokens + + private Identifier(String[] tokens) { + this.tokens = tokens + } + + static Identifier of(String identifier, Casing casing) { + String[] tokens + + switch(casing){ + case Casing.PascalCase: + tokens = (~/[A-Z][a-z]*/) + .matcher(identifier).results() + .map({ it.group() }) + .toArray(String[]::new) + break + case Casing.KebabCase: + tokens = identifier.split("-") + break + default: + throw new IllegalArgumentException("Suggested casing not implemented.") + } + + new Identifier(tokens) + } + + String toCase(Casing casing) { + String result + + switch(casing){ + case Casing.PascalCase: + result = tokens.collect({ it[0..<1].toUpperCase() + it[1..-1].toLowerCase() }).join('') + break + case Casing.KebabCase: + result = tokens.collect({ it.toLowerCase() }).join('-') + break + default: + throw new IllegalArgumentException("Suggested casing not implemented.") + } + + result + } +} diff --git a/generators/TestGeneratorTool/src/main/groovy/Main.groovy b/generators/TestGeneratorTool/src/main/groovy/Main.groovy index 9049d39a..f82b6ee4 100644 --- a/generators/TestGeneratorTool/src/main/groovy/Main.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/Main.groovy @@ -1,5 +1,5 @@ import java.nio.file.Files -import java.nio.file.Paths +import java.nio.file.Path static void main(String[] args) { CommandLineInterface commandLineInterface = new CommandLineInterface() @@ -22,9 +22,19 @@ static void main(String[] args) { return } - CanonicalDataParser specification = new CanonicalDataParser(Files.readString(Paths.get(canonical_data))) + CanonicalDataParser specification = new CanonicalDataParser(Files.readString(Path.of(canonical_data))) String exerciseSlug = specification.exerciseSlug + String exerciseName = Identifier.of(exerciseSlug, Casing.KebabCase).toCase(Casing.PascalCase) ArrayList testCases = specification.labeledTestCases + int testCount = testCases.size() println "We are going to implement $testCount tests for '$exerciseSlug' exercise." + + Path testFilePath = Path.of(repository_directory, 'exercises', 'practice', exerciseSlug, 'src', 'test', 'groovy', exerciseName + 'Spec.groovy') + String testFileFullName = testFilePath.toString() + println "Writing tests to $testFileFullName..." + + String renderedTests = TestCasesRenderer.render(specification) + Files.writeString(testFilePath, renderedTests) + println "Done." } diff --git a/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy new file mode 100644 index 00000000..90704929 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy @@ -0,0 +1,32 @@ +import groovy.json.JsonOutput +import groovy.text.SimpleTemplateEngine +import groovy.text.Template + +import java.nio.file.Files +import java.nio.file.Path + +class TestCasesRenderer { + static String render(CanonicalDataParser specification) { + SimpleTemplateEngine engine = new SimpleTemplateEngine() + String testClassPattern = Files.readString(Path.of('templates', 'TestClass.template')) + String testMethodPattern = Files.readString(Path.of('templates', 'TestMethod.template')) + Template templateMethod = engine.createTemplate(testMethodPattern) + + ArrayList testMethods = [] + for (labeledTestCase in specification.labeledTestCases) { + testMethods.add(templateMethod.make([ + description: labeledTestCase.description, + property: labeledTestCase.property, + input: JsonOutput.toJson(labeledTestCase.input), + expected: JsonOutput.toJson(labeledTestCase.expected) + ]).toString()) + } + + LinkedHashMap bindings = [ + exerciseName: Identifier.of(specification.exerciseSlug, Casing.KebabCase).toCase(Casing.PascalCase), + testMethods : testMethods + ] + + engine.createTemplate(testClassPattern).make(bindings).toString() + } +} diff --git a/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy index bd0f3376..0fbb5a68 100644 --- a/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy +++ b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy @@ -9,7 +9,7 @@ class CanonicalDataParserSpec extends Specification { def setupSpec() { JsonBuilder builder = new JsonBuilder() builder { - exercise 'foobar' + exercise 'foo-bar' comments 'blah', 'blah', 'blah' cases( { @@ -24,7 +24,7 @@ class CanonicalDataParserSpec extends Specification { { uuid '09113ce5-b008-45d0-98af-c0378b64966b' description 'Bar a name to combine its parts' - property 'foo' + property 'bar' input { firstName 'Alan' lastName 'Smith' @@ -73,12 +73,12 @@ class CanonicalDataParserSpec extends Specification { sample = builder.toString() } - def "Can retrieve exercise slug."() { + def "Can retrieve exercise slug"() { expect: - new CanonicalDataParser(sample).exerciseSlug == 'foobar' + new CanonicalDataParser(sample).exerciseSlug == 'foo-bar' } - def "Can retrieve data for each test case."() { + def "Can retrieve data for each test case"() { when: CanonicalDataParser specification = new CanonicalDataParser(sample) ArrayList testCases = specification.labeledTestCases diff --git a/generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy index 78f47e7e..d1460f99 100644 --- a/generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy +++ b/generators/TestGeneratorTool/src/test/groovy/CommandLineInterfaceSpec.groovy @@ -1,7 +1,7 @@ import spock.lang.* class CommandLineInterfaceSpec extends Specification { - def "Can get help on CLI usage, using short notation."() { + def "Can get help on CLI usage, using short notation"() { given: ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'-h'}) @@ -9,7 +9,7 @@ class CommandLineInterfaceSpec extends Specification { options.help() == true } - def "Can get help on CLI usage, using long notation."() { + def "Can get help on CLI usage, using long notation"() { given: ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'--help'}) @@ -17,7 +17,7 @@ class CommandLineInterfaceSpec extends Specification { options.help() == true } - def "Can set path to canonical-data.json file, using short notation."() { + def "Can set path to canonical-data.json file, using short notation"() { given: ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'-i', 'C:\\Users\\Foo\\canonical-data.json'}) @@ -25,7 +25,7 @@ class CommandLineInterfaceSpec extends Specification { options.canonical_data() == 'C:\\Users\\Foo\\canonical-data.json' } - def "Can set path to canonical-data.json file, using long notation."() { + def "Can set path to canonical-data.json file, using long notation"() { given: ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'--canonical-data=C:\\Users\\Foo\\canonical-data.json'}) @@ -33,7 +33,7 @@ class CommandLineInterfaceSpec extends Specification { options.canonical_data() == 'C:\\Users\\Foo\\canonical-data.json' } - def "Can set repository directory, using short notation."() { + def "Can set repository directory, using short notation"() { given: ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'-d', 'C:\\Users\\Foo\\Groovy'}) @@ -41,7 +41,7 @@ class CommandLineInterfaceSpec extends Specification { options.repository_directory() == 'C:\\Users\\Foo\\Groovy' } - def "Can set repository directory, using long notation."() { + def "Can set repository directory, using long notation"() { given: ICommandLineInterface options = new CommandLineInterface().parse(new String[]{'--repository-directory=C:\\Users\\Foo\\Groovy'}) diff --git a/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy new file mode 100644 index 00000000..a9ac1f2d --- /dev/null +++ b/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy @@ -0,0 +1,115 @@ +import groovy.json.JsonBuilder +import spock.lang.* + +class TestCasesRendererSpec extends Specification { + def "It can render an empty test class"() { + when: + JsonBuilder builder = new JsonBuilder() + builder { + exercise 'foo-bar' + cases() + } + String sample = builder.toString() + CanonicalDataParser specification = new CanonicalDataParser(sample) + String renderedTests = TestCasesRenderer.render(specification) + + then: + renderedTests == '''import spock.lang.* + +class FooBarSpec extends Specification { +}''' + } + + def "It can render test class with a single test method"() { + when: + JsonBuilder builder = new JsonBuilder() + builder { + exercise 'foo-bar' + cases([ + { + uuid '31e9db74-86b9-4b14-a320-9ea910337289' + description 'Foo a word to reverse it' + property 'foo' + input { + word 'time' + } + expected 'emit' + } + ]) + } + String sample = builder.toString() + CanonicalDataParser specification = new CanonicalDataParser(sample) + String renderedTests = TestCasesRenderer.render(specification) + + then: + renderedTests == '''import spock.lang.* + +class FooBarSpec extends Specification { + def "Foo a word to reverse it"() { + expect: + given == expected + + where: + // Please check the "foo" property + // Please use the following input: {"word":"time"} + // Please expect the following: "emit" + } +}''' + } + + def "It can render several test methods inside test class"() { + when: + JsonBuilder builder = new JsonBuilder() + builder { + exercise 'foo-bar' + cases( + { + uuid '31e9db74-86b9-4b14-a320-9ea910337289' + description 'Foo a word to reverse it' + property 'foo' + input { + word 'time' + } + expected 'emit' + }, + { + uuid '09113ce5-b008-45d0-98af-c0378b64966b' + description 'Bar a name to combine its parts' + property 'bar' + input { + firstName 'Alan' + lastName 'Smith' + } + expected 'Alan Smith' + } + ) + } + String sample = builder.toString() + CanonicalDataParser specification = new CanonicalDataParser(sample) + String renderedTests = TestCasesRenderer.render(specification) + + then: + renderedTests == '''import spock.lang.* + +class FooBarSpec extends Specification { + def "Foo a word to reverse it"() { + expect: + given == expected + + where: + // Please check the "foo" property + // Please use the following input: {"word":"time"} + // Please expect the following: "emit" + } + def "Bar a name to combine its parts"() { + expect: + given == expected + + where: + // Please check the "bar" property + // Please use the following input: {"firstName":"Alan","lastName":"Smith"} + // Please expect the following: "Alan Smith" + } +}''' + } +} diff --git a/generators/TestGeneratorTool/templates/TestClass.template b/generators/TestGeneratorTool/templates/TestClass.template new file mode 100644 index 00000000..b349cfe4 --- /dev/null +++ b/generators/TestGeneratorTool/templates/TestClass.template @@ -0,0 +1,9 @@ +import spock.lang.* + +class ${exerciseName}Spec extends Specification { +<% if (testMethods.size() != 0) { %>\ +<% for (testMethod in testMethods) { %>\ +<%= testMethod %>\ +<% } %>\ +<% } %>\ +} \ No newline at end of file diff --git a/generators/TestGeneratorTool/templates/TestMethod.template b/generators/TestGeneratorTool/templates/TestMethod.template new file mode 100644 index 00000000..62d449af --- /dev/null +++ b/generators/TestGeneratorTool/templates/TestMethod.template @@ -0,0 +1,9 @@ + def "${description}"() { + expect: + given == expected + + where: + // Please check the "${property}" property + // Please use the following input: ${input} + // Please expect the following: ${expected} + } From 1fd9ce02d0900276f5d135c6c3d7c69f206fd547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Wed, 10 Jul 2024 09:33:09 +0300 Subject: [PATCH 5/9] Replaced comment ceneration with actual code generation, added generation of @Ignore attributes. --- .../src/main/groovy/TestCasesRenderer.groovy | 55 ++++++++++-- .../groovy/CanonicalDataParserSpec.groovy | 4 +- .../test/groovy/TestCasesRendererSpec.groovy | 87 ++++++++++++++++--- .../templates/TestClass.template | 3 +- .../templates/TestMethod.template | 10 ++- 5 files changed, 132 insertions(+), 27 deletions(-) diff --git a/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy index 90704929..06b558c9 100644 --- a/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy @@ -10,20 +10,61 @@ class TestCasesRenderer { SimpleTemplateEngine engine = new SimpleTemplateEngine() String testClassPattern = Files.readString(Path.of('templates', 'TestClass.template')) String testMethodPattern = Files.readString(Path.of('templates', 'TestMethod.template')) + String testMethodErrorPattern = Files.readString(Path.of('templates', 'TestMethodError.template')) Template templateMethod = engine.createTemplate(testMethodPattern) + Template templateMethodError = engine.createTemplate(testMethodErrorPattern) + + String exerciseName = Identifier.of(specification.exerciseSlug, Casing.KebabCase).toCase(Casing.PascalCase) ArrayList testMethods = [] + boolean ignore = false for (labeledTestCase in specification.labeledTestCases) { - testMethods.add(templateMethod.make([ - description: labeledTestCase.description, - property: labeledTestCase.property, - input: JsonOutput.toJson(labeledTestCase.input), - expected: JsonOutput.toJson(labeledTestCase.expected) - ]).toString()) + LinkedHashMap input = labeledTestCase.input as LinkedHashMap + ArrayList argumentNames = new ArrayList<>(input.keySet()) + ArrayList argumentValues = input.values().collect({ JsonOutput.toJson(it) }) + ArrayList fieldWidths = [] + for (i in 0.. name.padRight(fieldWidths[index]) }) + .join(' | ') + String pipeSeparatedArgumentValues = + argumentValues + .withIndex() + .collect({ String value, Integer index -> value.padRight(fieldWidths[index]) }) + .join(' | ') + if (labeledTestCase.expected instanceof Map && (labeledTestCase.expected as LinkedHashMap).containsKey('error')) { + testMethods.add(templateMethodError.make([ + ignore: ignore, + description: labeledTestCase.description, + exerciseName: exerciseName, + property: labeledTestCase.property, + commaSeparatedArgumentNames: commaSeparatedArgumentNames, + pipeSeparatedArgumentNamesTrimmed: pipeSeparatedArgumentNames.trim(), + pipeSeparatedArgumentValuesTrimmed: pipeSeparatedArgumentValues.trim(), + errorMessage: JsonOutput.toJson(labeledTestCase.expected['error']) + ]).toString()) + } else { + testMethods.add(templateMethod.make([ + ignore: ignore, + description: labeledTestCase.description, + exerciseName: exerciseName, + property: labeledTestCase.property, + commaSeparatedArgumentNames: commaSeparatedArgumentNames, + pipeSeparatedArgumentNames: pipeSeparatedArgumentNames, + pipeSeparatedArgumentValues: pipeSeparatedArgumentValues, + expected: JsonOutput.toJson(labeledTestCase.expected) + ]).toString()) + } + ignore = true } LinkedHashMap bindings = [ - exerciseName: Identifier.of(specification.exerciseSlug, Casing.KebabCase).toCase(Casing.PascalCase), + exerciseName: exerciseName, testMethods : testMethods ] diff --git a/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy index 0fbb5a68..3523b94e 100644 --- a/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy +++ b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy @@ -59,8 +59,8 @@ class CanonicalDataParserSpec extends Specification { description 'Bar a name with numbers gives an error' property 'bar' input { - firstName 'Agent' - lastName 007 + firstName 'HAL' + lastName 900 } expected { error 'You should never bar a number' diff --git a/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy index a9ac1f2d..01b0fe77 100644 --- a/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy +++ b/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy @@ -47,12 +47,11 @@ class FooBarSpec extends Specification { class FooBarSpec extends Specification { def "Foo a word to reverse it"() { expect: - given == expected + FooBar.foo(word) == expected where: - // Please check the "foo" property - // Please use the following input: {"word":"time"} - // Please expect the following: "emit" + word || expected + "time" || "emit" } }''' } @@ -81,7 +80,16 @@ class FooBarSpec extends Specification { lastName 'Smith' } expected 'Alan Smith' - } + }, + { + uuid 'f22d7a03-e752-4f14-9231-4eae9f128cef' + description 'Foo of a number returns nothing' + property 'foo' + input { + word 42 + } + expected null + }, ) } String sample = builder.toString() @@ -94,21 +102,74 @@ class FooBarSpec extends Specification { class FooBarSpec extends Specification { def "Foo a word to reverse it"() { expect: - given == expected + FooBar.foo(word) == expected where: - // Please check the "foo" property - // Please use the following input: {"word":"time"} - // Please expect the following: "emit" + word || expected + "time" || "emit" } + + @Ignore def "Bar a name to combine its parts"() { expect: - given == expected + FooBar.bar(firstName, lastName) == expected + + where: + firstName | lastName || expected + "Alan" | "Smith" || "Alan Smith" + } + + @Ignore + def "Foo of a number returns nothing"() { + expect: + FooBar.foo(word) == expected + + where: + word || expected + 42 || null + } +}''' + } + + def "It can render test method with an error assertion"() { + when: + JsonBuilder builder = new JsonBuilder() + builder { + exercise 'foo-bar' + cases([ + { + uuid 'c7b6f24a-553f-475a-8a40-dba854fe1bff' + description 'Bar a name with numbers gives an error' + property 'bar' + input { + firstName 'HAL' + lastName 900 + } + expected { + error 'You should never bar a number' + } + } + ]) + } + String sample = builder.toString() + CanonicalDataParser specification = new CanonicalDataParser(sample) + String renderedTests = TestCasesRenderer.render(specification) + + then: + renderedTests == '''import spock.lang.* + +class FooBarSpec extends Specification { + def "Bar a name with numbers gives an error"() { + when: + FooBar.bar(firstName, lastName) + + then: + IllegalArgumentException exceptionThrown = thrown(IllegalArgumentException) + exceptionThrown.message == "You should never bar a number" where: - // Please check the "bar" property - // Please use the following input: {"firstName":"Alan","lastName":"Smith"} - // Please expect the following: "Alan Smith" + firstName | lastName + "HAL" | 900 } }''' } diff --git a/generators/TestGeneratorTool/templates/TestClass.template b/generators/TestGeneratorTool/templates/TestClass.template index b349cfe4..19e2de15 100644 --- a/generators/TestGeneratorTool/templates/TestClass.template +++ b/generators/TestGeneratorTool/templates/TestClass.template @@ -2,7 +2,8 @@ import spock.lang.* class ${exerciseName}Spec extends Specification { <% if (testMethods.size() != 0) { %>\ -<% for (testMethod in testMethods) { %>\ +<%= testMethods[0] %>\ +<% for (testMethod in testMethods.tail()) { %> <%= testMethod %>\ <% } %>\ <% } %>\ diff --git a/generators/TestGeneratorTool/templates/TestMethod.template b/generators/TestGeneratorTool/templates/TestMethod.template index 62d449af..92865f1c 100644 --- a/generators/TestGeneratorTool/templates/TestMethod.template +++ b/generators/TestGeneratorTool/templates/TestMethod.template @@ -1,9 +1,11 @@ +<% if (ignore) { %>\ + @Ignore +<% } %>\ def "${description}"() { expect: - given == expected + ${exerciseName}.${property}(${commaSeparatedArgumentNames}) == expected where: - // Please check the "${property}" property - // Please use the following input: ${input} - // Please expect the following: ${expected} + ${pipeSeparatedArgumentNames} || expected + ${pipeSeparatedArgumentValues} || ${expected} } From e03b44d92fb5ee7d36bb1956201c2bd58e3ecc38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Wed, 10 Jul 2024 12:03:39 +0300 Subject: [PATCH 6/9] Added automatic exclusion of tests based on the contents of tests.toml file --- .../main/groovy/CanonicalDataParser.groovy | 7 +- .../src/main/groovy/LabeledTestCase.groovy | 4 +- .../src/main/groovy/Main.groovy | 5 +- .../src/main/groovy/TestCasesRenderer.groovy | 20 +- .../groovy/CanonicalDataParserSpec.groovy | 156 ++++++++------ .../test/groovy/TestCasesRendererSpec.groovy | 198 ++++++++++-------- 6 files changed, 229 insertions(+), 161 deletions(-) diff --git a/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy b/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy index aa609306..1ef3ccd0 100644 --- a/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy @@ -22,7 +22,12 @@ class CanonicalDataParser { boolean containsInput = labeledTestItem.containsKey 'input' boolean containsExpected = labeledTestItem.containsKey 'expected' if (containsDescription && containsProperty && containsInput && containsExpected) { - collectedItems.add(new LabeledTestCase(labeledTestItem['description'] as String, labeledTestItem['property'] as String, labeledTestItem['input'], labeledTestItem['expected'])) + collectedItems.add(new LabeledTestCase( + UUID.fromString(labeledTestItem['uuid'] as String), + labeledTestItem['description'] as String, + labeledTestItem['property'] as String, + labeledTestItem['input'], + labeledTestItem['expected'])) } } } diff --git a/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy b/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy index 46751eb2..019d78e7 100644 --- a/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy @@ -1,10 +1,12 @@ class LabeledTestCase { + UUID identifier String description String property Object input Object expected - LabeledTestCase(String description, String property, Object input, Object expected) { + LabeledTestCase(UUID identifier, String description, String property, Object input, Object expected) { + this.identifier = identifier this.description = description this.property = property this.input = input diff --git a/generators/TestGeneratorTool/src/main/groovy/Main.groovy b/generators/TestGeneratorTool/src/main/groovy/Main.groovy index f82b6ee4..d87df625 100644 --- a/generators/TestGeneratorTool/src/main/groovy/Main.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/Main.groovy @@ -30,11 +30,14 @@ static void main(String[] args) { int testCount = testCases.size() println "We are going to implement $testCount tests for '$exerciseSlug' exercise." + Path testConfigurationPath = Path.of(repository_directory, 'exercises', 'practice', exerciseSlug, '.meta', 'tests.toml') + Set excludedTests = TestConfigurationParser.findExcludedTests(Files.readString(testConfigurationPath)) + Path testFilePath = Path.of(repository_directory, 'exercises', 'practice', exerciseSlug, 'src', 'test', 'groovy', exerciseName + 'Spec.groovy') String testFileFullName = testFilePath.toString() println "Writing tests to $testFileFullName..." - String renderedTests = TestCasesRenderer.render(specification) + String renderedTests = TestCasesRenderer.render(specification, excludedTests) Files.writeString(testFilePath, renderedTests) println "Done." } diff --git a/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy index 06b558c9..67e28358 100644 --- a/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy @@ -6,7 +6,7 @@ import java.nio.file.Files import java.nio.file.Path class TestCasesRenderer { - static String render(CanonicalDataParser specification) { + static String render(CanonicalDataParser specification, Set excludedTests) { SimpleTemplateEngine engine = new SimpleTemplateEngine() String testClassPattern = Files.readString(Path.of('templates', 'TestClass.template')) String testMethodPattern = Files.readString(Path.of('templates', 'TestMethod.template')) @@ -37,8 +37,9 @@ class TestCasesRenderer { .withIndex() .collect({ String value, Integer index -> value.padRight(fieldWidths[index]) }) .join(' | ') + String preparedMethod if (labeledTestCase.expected instanceof Map && (labeledTestCase.expected as LinkedHashMap).containsKey('error')) { - testMethods.add(templateMethodError.make([ + preparedMethod = templateMethodError.make([ ignore: ignore, description: labeledTestCase.description, exerciseName: exerciseName, @@ -47,9 +48,9 @@ class TestCasesRenderer { pipeSeparatedArgumentNamesTrimmed: pipeSeparatedArgumentNames.trim(), pipeSeparatedArgumentValuesTrimmed: pipeSeparatedArgumentValues.trim(), errorMessage: JsonOutput.toJson(labeledTestCase.expected['error']) - ]).toString()) + ]).toString() } else { - testMethods.add(templateMethod.make([ + preparedMethod = templateMethod.make([ ignore: ignore, description: labeledTestCase.description, exerciseName: exerciseName, @@ -58,8 +59,12 @@ class TestCasesRenderer { pipeSeparatedArgumentNames: pipeSeparatedArgumentNames, pipeSeparatedArgumentValues: pipeSeparatedArgumentValues, expected: JsonOutput.toJson(labeledTestCase.expected) - ]).toString()) + ]).toString() } + if (excludedTests.contains(labeledTestCase.identifier)) { + preparedMethod = commentOut(preparedMethod) + } + testMethods.add(preparedMethod) ignore = true } @@ -70,4 +75,9 @@ class TestCasesRenderer { engine.createTemplate(testClassPattern).make(bindings).toString() } + + static String commentOut(String source) { + String lineSeparator = source.contains('\r') ? '\r\n' : '\n' + source.split("\\r?\\n").collect({ '//' + it }).join(lineSeparator) + lineSeparator + } } diff --git a/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy index 3523b94e..95460f08 100644 --- a/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy +++ b/generators/TestGeneratorTool/src/test/groovy/CanonicalDataParserSpec.groovy @@ -1,5 +1,4 @@ import spock.lang.* -import groovy.json.JsonBuilder class CanonicalDataParserSpec extends Specification { @@ -7,70 +6,93 @@ class CanonicalDataParserSpec extends Specification { String sample def setupSpec() { - JsonBuilder builder = new JsonBuilder() - builder { - exercise 'foo-bar' - comments 'blah', 'blah', 'blah' - cases( - { - uuid '31e9db74-86b9-4b14-a320-9ea910337289' - description 'Foo a word to reverse it' - property 'foo' - input { - word 'time' - } - expected 'emit' - }, - { - uuid '09113ce5-b008-45d0-98af-c0378b64966b' - description 'Bar a name to combine its parts' - property 'bar' - input { - firstName 'Alan' - lastName 'Smith' - } - expected 'Alan Smith' - }, - { - description 'Abnormal inputs: numbers' - cases( - { - uuid 'f22d7a03-e752-4f14-9231-4eae9f128cef' - description 'Foo of a number returns nothing' - property 'foo' - input { - word 42 - } - expected null - }, - { - uuid '8790a635-e8a8-4343-a29f-7da2929b9378' - description 'Foo of a very big number returns nothing' - comments 'Making this test case pass requires usage of big integer libraries.' - scenarios 'big-integers' - property 'foo' - input { - word 28948022309329048855892746252171976962977213799489202546401021394546514198529 - } - expected null - }, - { - uuid 'c7b6f24a-553f-475a-8a40-dba854fe1bff' - description 'Bar a name with numbers gives an error' - property 'bar' - input { - firstName 'HAL' - lastName 900 - } - expected { - error 'You should never bar a number' - } - } - ) - } - ) + sample = '''{ + "exercise": "foo-bar", + "comments": [ + " Comments are always optional and can be used almost anywhere. ", + " ", + " They usually document how the exercise's readme ('description.md') ", + " is generally interpreted in test programs across different ", + " languages. ", + " ", + " In addition to a mainstream implementation path, this information ", + " can also document significant variations. " + ], + "cases": [ + { + "comments": [ + " A test case must have 'uuid', 'description', 'property', ", + " 'input' and 'expected' properties. The rest is optional. ", + " ", + " The 'property' is a string in lowerCamelCase identifying ", + " the type of test, but most of the times it is just the ", + " name of a function being tested. ", + " ", + " Test cases can have any number of additional keys, and ", + " most of them also have an 'expected' one, defining the ", + " value a test should return. " + ], + "uuid": "31e9db74-86b9-4b14-a320-9ea910337289", + "description": "Foo a word to reverse it", + "property": "foo", + "input": { + "word": "time" + }, + "expected": "emit" + }, + { + "uuid": "09113ce5-b008-45d0-98af-c0378b64966b", + "description": "Bar a name to combine its parts", + "property": "bar", + "input": { + "firstName": "Alan", + "lastName": "Smith" + }, + "expected": "Alan Smith" + }, + { + "comments": [ + " Test cases can be arbitrarily grouped with a description ", + " to make organization easier. " + ], + "description": "Abnormal inputs: numbers", + "cases": [ + { + "uuid": "f22d7a03-e752-4f14-9231-4eae9f128cef", + "description": "Foo of a number returns nothing", + "property": "foo", + "input": { + "word": 42 + }, + "expected": null + }, + { + "uuid": "8790a635-e8a8-4343-a29f-7da2929b9378", + "description": "Foo of a very big number returns nothing", + "comments": ["Making this test case pass requires using big integer library."], + "scenarios": ["big-integers"], + "property": "foo", + "input": { + "word": "28948022309329048855892746252171976962977213799489202546401021394546514198529" + }, + "expected": null + }, + { + "uuid": "c7b6f24a-553f-475a-8a40-dba854fe1bff", + "description": "Bar a name with numbers gives an error", + "property": "bar", + "input": { + "firstName": "HAL", + "lastName": 9000 + }, + "expected": { + "error": "You should never bar a number" + } } - sample = builder.toString() + ] + } + ] +}''' } def "Can retrieve exercise slug"() { @@ -85,10 +107,10 @@ class CanonicalDataParserSpec extends Specification { then: testCases.size() == 5 - testCases[0].description == 'Foo a word to reverse it' - testCases[1].property == 'foo' + testCases[0].identifier == UUID.fromString('31e9db74-86b9-4b14-a320-9ea910337289') + testCases[1].property == 'bar' testCases[2].input == [word: 42] - testCases[3].expected == null + testCases[3].description == 'Foo of a very big number returns nothing' testCases[4].expected == [error: 'You should never bar a number'] } } diff --git a/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy index 01b0fe77..90bbed55 100644 --- a/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy +++ b/generators/TestGeneratorTool/src/test/groovy/TestCasesRendererSpec.groovy @@ -1,17 +1,13 @@ -import groovy.json.JsonBuilder import spock.lang.* class TestCasesRendererSpec extends Specification { - def "It can render an empty test class"() { + def "Can render an empty test class"() { when: - JsonBuilder builder = new JsonBuilder() - builder { - exercise 'foo-bar' - cases() - } - String sample = builder.toString() + String sample = '''{ + "exercise": "foo-bar" +}''' CanonicalDataParser specification = new CanonicalDataParser(sample) - String renderedTests = TestCasesRenderer.render(specification) + String renderedTests = TestCasesRenderer.render(specification, [] as Set) then: renderedTests == '''import spock.lang.* @@ -20,26 +16,24 @@ class FooBarSpec extends Specification { }''' } - def "It can render test class with a single test method"() { + def "Can render test class with a single test method"() { when: - JsonBuilder builder = new JsonBuilder() - builder { - exercise 'foo-bar' - cases([ - { - uuid '31e9db74-86b9-4b14-a320-9ea910337289' - description 'Foo a word to reverse it' - property 'foo' - input { - word 'time' - } - expected 'emit' - } - ]) - } - String sample = builder.toString() + String sample = '''{ + "exercise": "foo-bar", + "cases": [ + { + "uuid": "31e9db74-86b9-4b14-a320-9ea910337289", + "description": "Foo a word to reverse it", + "property": "foo", + "input": { + "word": "time" + }, + "expected": "emit" + } + ] +}''' CanonicalDataParser specification = new CanonicalDataParser(sample) - String renderedTests = TestCasesRenderer.render(specification) + String renderedTests = TestCasesRenderer.render(specification, [] as Set) then: renderedTests == '''import spock.lang.* @@ -56,45 +50,43 @@ class FooBarSpec extends Specification { }''' } - def "It can render several test methods inside test class"() { + def "Can render several test methods inside test class"() { when: - JsonBuilder builder = new JsonBuilder() - builder { - exercise 'foo-bar' - cases( - { - uuid '31e9db74-86b9-4b14-a320-9ea910337289' - description 'Foo a word to reverse it' - property 'foo' - input { - word 'time' - } - expected 'emit' - }, - { - uuid '09113ce5-b008-45d0-98af-c0378b64966b' - description 'Bar a name to combine its parts' - property 'bar' - input { - firstName 'Alan' - lastName 'Smith' - } - expected 'Alan Smith' - }, - { - uuid 'f22d7a03-e752-4f14-9231-4eae9f128cef' - description 'Foo of a number returns nothing' - property 'foo' - input { - word 42 - } - expected null - }, - ) - } - String sample = builder.toString() + String sample = '''{ + "exercise": "foo-bar", + "cases": [ + { + "uuid": "31e9db74-86b9-4b14-a320-9ea910337289", + "description": "Foo a word to reverse it", + "property": "foo", + "input": { + "word": "time" + }, + "expected": "emit" + }, + { + "uuid": "09113ce5-b008-45d0-98af-c0378b64966b", + "description": "Bar a name to combine its parts", + "property": "bar", + "input": { + "firstName": "Alan", + "lastName": "Smith" + }, + "expected": "Alan Smith" + }, + { + "uuid": "f22d7a03-e752-4f14-9231-4eae9f128cef", + "description": "Foo of a number returns nothing", + "property": "foo", + "input": { + "word": 42 + }, + "expected": null + } + ] +}''' CanonicalDataParser specification = new CanonicalDataParser(sample) - String renderedTests = TestCasesRenderer.render(specification) + String renderedTests = TestCasesRenderer.render(specification, [] as Set) then: renderedTests == '''import spock.lang.* @@ -131,29 +123,27 @@ class FooBarSpec extends Specification { }''' } - def "It can render test method with an error assertion"() { + def "Can render test method with an error assertion"() { when: - JsonBuilder builder = new JsonBuilder() - builder { - exercise 'foo-bar' - cases([ - { - uuid 'c7b6f24a-553f-475a-8a40-dba854fe1bff' - description 'Bar a name with numbers gives an error' - property 'bar' - input { - firstName 'HAL' - lastName 900 - } - expected { - error 'You should never bar a number' - } - } - ]) - } - String sample = builder.toString() + String sample = '''{ + "exercise": "foo-bar", + "cases": [ + { + "uuid": "c7b6f24a-553f-475a-8a40-dba854fe1bff", + "description": "Bar a name with numbers gives an error", + "property": "bar", + "input": { + "firstName": "HAL", + "lastName": 9000 + }, + "expected": { + "error": "You should never bar a number" + } + } + ] +}''' CanonicalDataParser specification = new CanonicalDataParser(sample) - String renderedTests = TestCasesRenderer.render(specification) + String renderedTests = TestCasesRenderer.render(specification, [] as Set) then: renderedTests == '''import spock.lang.* @@ -169,8 +159,44 @@ class FooBarSpec extends Specification { where: firstName | lastName - "HAL" | 900 + "HAL" | 9000 } +}''' + } + + def "Can comment out excluded tests"() { + when: + String sample = '''{ + "exercise": "foo-bar", + "cases": [ + { + "uuid": "8790a635-e8a8-4343-a29f-7da2929b9378", + "description": "Foo of a very big number returns nothing", + "comments": ["Making this test case pass requires using big integer library."], + "scenarios": ["big-integers"], + "property": "foo", + "input": { + "word": "28948022309329048855892746252171976962977213799489202546401021394546514198529" + }, + "expected": null + } + ] +}''' + CanonicalDataParser specification = new CanonicalDataParser(sample) + String renderedTests = TestCasesRenderer.render(specification, [UUID.fromString('8790a635-e8a8-4343-a29f-7da2929b9378')] as Set) + + then: + renderedTests == '''import spock.lang.* + +class FooBarSpec extends Specification { +// def "Foo of a very big number returns nothing"() { +// expect: +// FooBar.foo(word) == expected +// +// where: +// word || expected +// "28948022309329048855892746252171976962977213799489202546401021394546514198529" || null +// } }''' } } From 408a09c233025beed006b5c666b63b1bae343c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Wed, 10 Jul 2024 12:04:50 +0300 Subject: [PATCH 7/9] Created parser of tests.toml file --- .../groovy/TestConfigurationParser.groovy | 14 +++++++ .../groovy/TestConfigurationParserSpec.groovy | 38 +++++++++++++++++++ .../templates/TestMethodError.template | 15 ++++++++ 3 files changed, 67 insertions(+) create mode 100644 generators/TestGeneratorTool/src/main/groovy/TestConfigurationParser.groovy create mode 100644 generators/TestGeneratorTool/src/test/groovy/TestConfigurationParserSpec.groovy create mode 100644 generators/TestGeneratorTool/templates/TestMethodError.template diff --git a/generators/TestGeneratorTool/src/main/groovy/TestConfigurationParser.groovy b/generators/TestGeneratorTool/src/main/groovy/TestConfigurationParser.groovy new file mode 100644 index 00000000..b1442ef5 --- /dev/null +++ b/generators/TestGeneratorTool/src/main/groovy/TestConfigurationParser.groovy @@ -0,0 +1,14 @@ +class TestConfigurationParser { + static Set findExcludedTests(String source) { + Set excludedTests = [] + UUID latestIdentifier = null + for (String line in source.split("\\r?\\n")) { + if (line == "include = false" && latestIdentifier != null) { + excludedTests.add(latestIdentifier) + } else if (line.startsWith('[') && line.endsWith(']')) { + latestIdentifier = UUID.fromString(line.substring(1, line.size() - 1)) + } + } + excludedTests + } +} diff --git a/generators/TestGeneratorTool/src/test/groovy/TestConfigurationParserSpec.groovy b/generators/TestGeneratorTool/src/test/groovy/TestConfigurationParserSpec.groovy new file mode 100644 index 00000000..c39793d8 --- /dev/null +++ b/generators/TestGeneratorTool/src/test/groovy/TestConfigurationParserSpec.groovy @@ -0,0 +1,38 @@ +import spock.lang.* + +class TestConfigurationParserSpec extends Specification { + def "Can find identifiers of tests with include = false"() { + given: + String sample = '''# 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. + +[3e5c30a8-87e2-4845-a815-a49671ade970] +description = "empty strand" + +[a0ea42a6-06d9-4ac6-828c-7ccaccf98fec] +description = "can count one nucleotide in single-character input" + +[eca0d565-ed8c-43e7-9033-6cefbf5115b5] +description = "strand with repeated nucleotide" + +[b4c47851-ee9e-4b0a-be70-a86e343bd851] +description = "strand with invalid nucleotides" +include = false +comment = "error handling omitted on purpose" + +[40a45eac-c83f-4740-901a-20b22d15a39f] +description = "strand with multiple nucleotides"''' + Set expected = [UUID.fromString('b4c47851-ee9e-4b0a-be70-a86e343bd851')] + + expect: + TestConfigurationParser.findExcludedTests(sample) == expected + } +} diff --git a/generators/TestGeneratorTool/templates/TestMethodError.template b/generators/TestGeneratorTool/templates/TestMethodError.template new file mode 100644 index 00000000..fdd45948 --- /dev/null +++ b/generators/TestGeneratorTool/templates/TestMethodError.template @@ -0,0 +1,15 @@ +<% if (ignore) { %>\ + @Ignore +<% } %>\ + def "${description}"() { + when: + ${exerciseName}.${property}(${commaSeparatedArgumentNames}) + + then: + IllegalArgumentException exceptionThrown = thrown(IllegalArgumentException) + exceptionThrown.message == ${errorMessage} + + where: + ${pipeSeparatedArgumentNamesTrimmed} + ${pipeSeparatedArgumentValuesTrimmed} + } From fb962912e797fa76c404ae3cb2622324b33d719f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=A2=D0=BE=D0=BB?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BA=D0=BE=D0=B2?= Date: Wed, 10 Jul 2024 15:36:45 +0300 Subject: [PATCH 8/9] Added documentation (GENERATORS.md and javadoc comments in source files) --- docs/GENERATORS.md | 116 +++++++++++ .../main/groovy/CanonicalDataParser.groovy | 44 ++-- .../src/main/groovy/Casing.groovy | 13 ++ .../main/groovy/CommandLineInterface.groovy | 14 ++ .../main/groovy/ICommandLineInterface.groovy | 3 + .../src/main/groovy/Identifier.groovy | 37 +++- .../src/main/groovy/LabeledTestCase.groovy | 22 ++ .../src/main/groovy/Main.groovy | 19 +- .../src/main/groovy/TestCasesRenderer.groovy | 188 +++++++++++++----- .../groovy/TestConfigurationParser.groovy | 8 + .../groovy/CommandLineInterfaceSpec.groovy | 16 +- ...ethodError.template => TestError.template} | 4 +- 12 files changed, 392 insertions(+), 92 deletions(-) rename generators/TestGeneratorTool/templates/{TestMethodError.template => TestError.template} (78%) diff --git a/docs/GENERATORS.md b/docs/GENERATORS.md index 72895bd2..e08da343 100644 --- a/docs/GENERATORS.md +++ b/docs/GENERATORS.md @@ -1 +1,117 @@ # Test generators + +Test generators allow tracks to generate tests automatically without having to write them ourselves. Each test generator reads from the exercise's `canonical data`, which defines the name of the test, its inputs, and outputs. You can read more about exercism's approach to test suites [here](https://github.com/exercism/problem-specifications#test-data-canonical-datajson). + +Generating tests automatically removes any sort of user error when creating tests. Furthermore, we want the tests to be accurate with respect to its canonical data. Test generation also makes it much easier to keep tests up to date. As the canonical data changes, the tests will be automatically updated when the generator for that test is run. + +An example of a canonical data file can be found [here](https://github.com/exercism/problem-specifications/blob/master/exercises/bob/canonical-data.json) + +## Deploying the generator + +### Creating a project + +1. Open IntelliJ Idea. +2. Click on "New Project". +3. Set name: "TestGeneratorTool". +4. Set desired test generator location +5. Set build system: "Gradle". +6. Set latest JDK and Groovy SDK. +7. Set Gradle DSL: "Groovy". +8. Click "Create". +9. Copy *TestGeneratorTool/src* folder to the project. +10. Copy *TestGeneratorTool/templates* folder to the project. +11. Copy *TestGeneratorTool/build.gradle* file to the project. + +### Adding necessary dependencies + +All necessary dependencies are already added to *TestGeneratorTool/build.gradle* + +Should you run into any problems, search for installation instructions of Spock testing framework on their [official site][spock-framework-official]. + +Search for installation instructions of Picocli framework [here][picocli]. + +### The structure of project + +- TestGeneratorTool/src - source files of test generator +- TestGeneratorTool/templates - templates used to render test class and test methods +- TestGeneratorTool/build.gradle - dependencies of project + +## Usage of test generator + +1. Open project in IntelliJ Idea. +2. Move to *TestGeneratorTool/src/main/groovy/Main.groovy* +3. Create a new run configuration by clicking on green arrow to the left of `static void main`. +4. Edit the configuration: + 1. Find the "Main" confugration on top panel, to the left of green arrow and green bug. + 2. Click on configuration and choose "Edit configurations..." + 3. In the "Program arguments" text box, set location of repository directory and the location on canonical-data.json to parse. + + Here is the example, please edit it to match your actual file system: + + ``` + --repository-directory=C:\Users\Aksima\Exercism\building-exercism\groovy --canonical-data=C:\Users\Aksima\Exercism\building-exercism\canonical-data\groovy\largest-series-product\canonical-data.json + ``` + 4. In the "Working directory" set the path to the project you created for test generator tool. For example, if you created project in + + ``` + C:\Users\Aksima\Exercism\building-exercism\tooling\groovy\generators\TestGeneratorTool + ``` + + ...then you must set the same text in the "Working directory" textbox. + + 5. Click "OK" +5. Everything should be set now! You can generate tests by simply clicking the green arrow on top menu. +6. Do not forget to change *--canonical-data* command line option when you need to generate tests from another *canonical-data.json* file. + +## Contibuting to test generator tool + +These resources may be useful for you if you plan to contibute to test generator tool: + +1. The [documentation][problem-specifications-readme] for problem specifiations. +2. The [schema][canonical-data-definition] for canonical data. +3. Groovy language [official documentation][groovy-docs]. +4. [Parsing and producing JSON][groovy-parsing-json] topic in Groovy language official documentation. +5. [Template engines][groovy-templating] in Groovy +6. An article on [command line builder][command-line-builder] at groovy-lang.org. +7. [Instructions][command-line-parser-annotations] on building command-line parsers using annotations and an interface. +8. [Official page][spock-framework-official] for Spock test framework. +9. [Examples][spock-test-primer] of test cases built on top of Spock framework. +10. [Picocli][picocli] official documentation. + +### Investigating canonical-data.json file structure + +The formal definition of canonical-data.json file can be found [here][canonical-data-definition]. According to that definition, the canonical-data.json file consists of following parts: + +- **exercise** - exercise's slug (kebab-case) - required. +- **comments** - optional. +- **cases** - an array of labeled test items - required. + - **labeledTestItem** - either **labeledTest** or group of description and **cases** defined above. + - **labeledTest** - a single test with following properties: + - **uuid** - a version 4 UUID - required. + - **reimplements** - optional. + - **description** - a short, clear, one-line description - required. + - **comments** - optional. + - **scenarios** - scenarios to include/exclude test cases - optional. + - **property** - letters-only, lowerCamelCase property name - required. + - **input** - the inputs to a test case - required. + - **expected** - the expected return value of a test case - required. + +[problem-specifications-readme]: "The documentation for problem specifiations" + +[canonical-data-definition]: "Formal definition of canonical data (schema)" + +[groovy-docs]: "Groovy language official documentation" + +[groovy-parsing-json]: "Parsing and producing JSON in Groovy" + +[groovy-templating]: "Template engines in Groovy" + +[command-line-builder]: "Builder for command-line interface" + +[command-line-parser-annotations]: "Describe command-line parsers using annotations and an interface" + +[spock-framework-official]: "Official page for Spock test framework" + +[spock-test-primer]: "Examples of test cases built on top of Spock framework" + +[picocli]: "Picocli framework official page" \ No newline at end of file diff --git a/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy b/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy index 1ef3ccd0..0e1701ad 100644 --- a/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/CanonicalDataParser.groovy @@ -1,39 +1,59 @@ import groovy.json.JsonSlurper +/** + * Canonical data parser. + */ class CanonicalDataParser { LinkedHashMap specification + /** + * Parse canonical-data.json. + * @param source The text content of canonical-data.json file. + */ CanonicalDataParser(String source) { specification = new JsonSlurper().parseText(source) as LinkedHashMap } + /** + * Get exercise slug from parsed data. + * @return Exercise slug. + */ String getExerciseSlug() { specification["exercise"] } + /** + * Helper function to flatten the nested structure of test cases. + * @param testItems A collection of test cases and test groups. + * @return A collection of test cases as LabeledTestCase objects (test groups are flattened). + */ ArrayList getTestCasesHelper(ArrayList testItems) { ArrayList collectedItems = [] + for (LinkedHashMap labeledTestItem in testItems) { + + // Only test groups contain the 'cases' property. if (labeledTestItem.containsKey 'cases') { + + // Test groups are handled by recursive call. collectedItems.addAll(getTestCasesHelper(labeledTestItem['cases'] as ArrayList)) } else { - boolean containsDescription = labeledTestItem.containsKey 'description' - boolean containsProperty = labeledTestItem.containsKey 'property' - boolean containsInput = labeledTestItem.containsKey 'input' - boolean containsExpected = labeledTestItem.containsKey 'expected' - if (containsDescription && containsProperty && containsInput && containsExpected) { - collectedItems.add(new LabeledTestCase( - UUID.fromString(labeledTestItem['uuid'] as String), - labeledTestItem['description'] as String, - labeledTestItem['property'] as String, - labeledTestItem['input'], - labeledTestItem['expected'])) - } + + // Standard test case (not a test group). + collectedItems.add(new LabeledTestCase(UUID.fromString(labeledTestItem['uuid'] as String), + labeledTestItem['description'] as String, + labeledTestItem['property'] as String, + labeledTestItem['input'], + labeledTestItem['expected'])) } } collectedItems } + /** + * Get collection of test cases as LabeledTestCase objects. + * @return A collection of test cases as LabeledTestCase objects. + */ ArrayList getLabeledTestCases() { getTestCasesHelper(specification['cases'] as ArrayList) } diff --git a/generators/TestGeneratorTool/src/main/groovy/Casing.groovy b/generators/TestGeneratorTool/src/main/groovy/Casing.groovy index 6e50512b..b1eccbb7 100644 --- a/generators/TestGeneratorTool/src/main/groovy/Casing.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/Casing.groovy @@ -1,4 +1,17 @@ +/** + * Variants of identifier casing. + */ enum Casing { + + UnknownCase, + + /** + * Each word start with capital letter and continue with lowercase letters. + */ PascalCase, + + /** + * Each word is delimited by dash (-). + */ KebabCase } \ No newline at end of file diff --git a/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy b/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy index fa19d625..6aa57a06 100644 --- a/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/CommandLineInterface.groovy @@ -1,16 +1,30 @@ import groovy.cli.picocli.CliBuilder +/** + * Command-line arguments parser. + */ class CommandLineInterface { CliBuilder builder + /** + * Initialize command-line arguments parser. + */ CommandLineInterface() { builder = new CliBuilder() } + /** + * Parse command-line arguments. + * @param args An array of command-line arguments to parse. + * @return Parsed arguments. + */ ICommandLineInterface parse(String[] args) { builder.parseFromSpec ICommandLineInterface, args } + /** + * Show program usage. + */ void showUsage() { builder.usage() } diff --git a/generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy b/generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy index 467d1900..9ebe88db 100644 --- a/generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/ICommandLineInterface.groovy @@ -1,5 +1,8 @@ import groovy.cli.Option +/** + * Command-line interface. + */ interface ICommandLineInterface { @Option(shortName = 'h', description = 'Display usage.') Boolean help() diff --git a/generators/TestGeneratorTool/src/main/groovy/Identifier.groovy b/generators/TestGeneratorTool/src/main/groovy/Identifier.groovy index d8788e30..f8fd94b4 100644 --- a/generators/TestGeneratorTool/src/main/groovy/Identifier.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/Identifier.groovy @@ -1,23 +1,38 @@ +/** + * An identifier. + */ class Identifier { - String[] tokens + String[] words - private Identifier(String[] tokens) { - this.tokens = tokens + /** + * Store identifier as array of each word in identifier. + * @param words The words which make up an identifier. + */ + private Identifier(String[] words) { + this.words = words } + /** + * Create identifier from its string representation, taking into account its casing. + * @param identifier Identifier as string. + * @param casing The casing of identifier. + * @return Parsed identifier. + */ static Identifier of(String identifier, Casing casing) { String[] tokens - switch(casing){ + switch (casing) { case Casing.PascalCase: tokens = (~/[A-Z][a-z]*/) .matcher(identifier).results() .map({ it.group() }) .toArray(String[]::new) break + case Casing.KebabCase: tokens = identifier.split("-") break + default: throw new IllegalArgumentException("Suggested casing not implemented.") } @@ -25,16 +40,24 @@ class Identifier { new Identifier(tokens) } + /** + * Convert identifier to the specified casing. + * @param casing Desired casing of identifier. + * @return Identifier in the specified casing. + */ String toCase(Casing casing) { String result - switch(casing){ + switch (casing) { + case Casing.PascalCase: - result = tokens.collect({ it[0..<1].toUpperCase() + it[1..-1].toLowerCase() }).join('') + result = words.collect({ it[0..<1].toUpperCase() + it[1..-1].toLowerCase() }).join('') break + case Casing.KebabCase: - result = tokens.collect({ it.toLowerCase() }).join('-') + result = words.collect({ it.toLowerCase() }).join('-') break + default: throw new IllegalArgumentException("Suggested casing not implemented.") } diff --git a/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy b/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy index 019d78e7..88fd3490 100644 --- a/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/LabeledTestCase.groovy @@ -1,8 +1,30 @@ +/** + * A single test with a description. + */ class LabeledTestCase { + /** + * A version 4 UUID (compliant with RFC 4122) in the canonical textual representation. + */ UUID identifier + + /** + * Description of test case. + */ String description + + /** + * Property to test. + */ String property + + /** + * Suggested inputs. + */ Object input + + /** + * Expected answer. + */ Object expected LabeledTestCase(UUID identifier, String description, String property, Object input, Object expected) { diff --git a/generators/TestGeneratorTool/src/main/groovy/Main.groovy b/generators/TestGeneratorTool/src/main/groovy/Main.groovy index d87df625..35b064a9 100644 --- a/generators/TestGeneratorTool/src/main/groovy/Main.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/Main.groovy @@ -5,39 +5,38 @@ static void main(String[] args) { CommandLineInterface commandLineInterface = new CommandLineInterface() ICommandLineInterface options = commandLineInterface.parse args + // User requested usage summary. if (options.help()) { commandLineInterface.showUsage() return } + // Getting path to canonical data and repository directory. String canonical_data = options.canonical_data() if (canonical_data == null) { println 'Path to canonical-data.json not set.' return } - String repository_directory = options.repository_directory() if (repository_directory == null) { println 'Path to repository directory not set.' return } + // Parsing canonical data. CanonicalDataParser specification = new CanonicalDataParser(Files.readString(Path.of(canonical_data))) String exerciseSlug = specification.exerciseSlug String exerciseName = Identifier.of(exerciseSlug, Casing.KebabCase).toCase(Casing.PascalCase) - ArrayList testCases = specification.labeledTestCases - - int testCount = testCases.size() - println "We are going to implement $testCount tests for '$exerciseSlug' exercise." + // Checking tests.toml for excluded tests. Path testConfigurationPath = Path.of(repository_directory, 'exercises', 'practice', exerciseSlug, '.meta', 'tests.toml') Set excludedTests = TestConfigurationParser.findExcludedTests(Files.readString(testConfigurationPath)) - Path testFilePath = Path.of(repository_directory, 'exercises', 'practice', exerciseSlug, 'src', 'test', 'groovy', exerciseName + 'Spec.groovy') - String testFileFullName = testFilePath.toString() - println "Writing tests to $testFileFullName..." - + // Rendering tests. String renderedTests = TestCasesRenderer.render(specification, excludedTests) + + // Writing rendered tests to file. + Path testFilePath = Path.of(repository_directory, 'exercises', 'practice', exerciseSlug, 'src', 'test', 'groovy', exerciseName + 'Spec.groovy') Files.writeString(testFilePath, renderedTests) - println "Done." + println "Tests are written to " + testFilePath.toString() } diff --git a/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy index 67e28358..072daa17 100644 --- a/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy +++ b/generators/TestGeneratorTool/src/main/groovy/TestCasesRenderer.groovy @@ -5,77 +5,159 @@ import groovy.text.Template import java.nio.file.Files import java.nio.file.Path +/** + * Renderer of test cases. + */ class TestCasesRenderer { - static String render(CanonicalDataParser specification, Set excludedTests) { - SimpleTemplateEngine engine = new SimpleTemplateEngine() - String testClassPattern = Files.readString(Path.of('templates', 'TestClass.template')) - String testMethodPattern = Files.readString(Path.of('templates', 'TestMethod.template')) - String testMethodErrorPattern = Files.readString(Path.of('templates', 'TestMethodError.template')) - Template templateMethod = engine.createTemplate(testMethodPattern) - Template templateMethodError = engine.createTemplate(testMethodErrorPattern) + /** + * Render all test cases inside a test class. + * @param specification A parsed specification of tests (from canonical-data.json). + * @param excludedTests A set of tests excluded in tests.toml file. + * @return Rendered test cases. + */ + static String render(CanonicalDataParser specification, Set excludedTests) { String exerciseName = Identifier.of(specification.exerciseSlug, Casing.KebabCase).toCase(Casing.PascalCase) - ArrayList testMethods = [] + Template testClassTemplate, testMethodTemplate, testErrorTemplate + (testClassTemplate, testMethodTemplate, testErrorTemplate) = loadTemplates() + + // Do not add @Ignore attribute to the first test. boolean ignore = false + + ArrayList testMethods = [] for (labeledTestCase in specification.labeledTestCases) { - LinkedHashMap input = labeledTestCase.input as LinkedHashMap - ArrayList argumentNames = new ArrayList<>(input.keySet()) - ArrayList argumentValues = input.values().collect({ JsonOutput.toJson(it) }) - ArrayList fieldWidths = [] - for (i in 0.. name.padRight(fieldWidths[index]) }) - .join(' | ') - String pipeSeparatedArgumentValues = - argumentValues - .withIndex() - .collect({ String value, Integer index -> value.padRight(fieldWidths[index]) }) - .join(' | ') - String preparedMethod - if (labeledTestCase.expected instanceof Map && (labeledTestCase.expected as LinkedHashMap).containsKey('error')) { - preparedMethod = templateMethodError.make([ - ignore: ignore, - description: labeledTestCase.description, - exerciseName: exerciseName, - property: labeledTestCase.property, - commaSeparatedArgumentNames: commaSeparatedArgumentNames, - pipeSeparatedArgumentNamesTrimmed: pipeSeparatedArgumentNames.trim(), - pipeSeparatedArgumentValuesTrimmed: pipeSeparatedArgumentValues.trim(), - errorMessage: JsonOutput.toJson(labeledTestCase.expected['error']) - ]).toString() - } else { - preparedMethod = templateMethod.make([ - ignore: ignore, - description: labeledTestCase.description, - exerciseName: exerciseName, - property: labeledTestCase.property, - commaSeparatedArgumentNames: commaSeparatedArgumentNames, - pipeSeparatedArgumentNames: pipeSeparatedArgumentNames, - pipeSeparatedArgumentValues: pipeSeparatedArgumentValues, - expected: JsonOutput.toJson(labeledTestCase.expected) - ]).toString() - } + + // Prepare the rendering of method. + String preparedMethod = renderMethod( + exerciseName, + labeledTestCase, + testMethodTemplate, + testErrorTemplate, + ignore) + + // Comment out the rendered method if it + // should not be included according to tests.toml file. + // + // As per https://github.com/exercism/problem-specifications/blob/main/README.md: + // Track generators should not automatically select the "latest" version + // of a test case by looking at the "reimplements" hierarchy. We recommend + // each track to make this a manual action, as the re-implemented test case + // might actually make less sense for a track. + // + // According to this, we cannot just delete the excluded exercise, + // all we can do is to comment it out, leaving the final decision + // to the exercise author and maintainers. if (excludedTests.contains(labeledTestCase.identifier)) { preparedMethod = commentOut(preparedMethod) } + + // Add rendered method to the list of methods + // which will be rendered inside test class. testMethods.add(preparedMethod) + + // Use @Ignore attribute for all tests after the first one. ignore = true } - LinkedHashMap bindings = [ + // Render the test class. + testClassTemplate.make([ exerciseName: exerciseName, testMethods : testMethods - ] + ]).toString() + } + + /** + * Render a single test method. + * @param exerciseName The name of exercise for which test method is rendered. + * @param labeledTestCase Test case to be rendered. + * @param testMethodTemplate Regular test method template. + * @param testErrorTemplate Template for checking the existence and contents of error. + * @param ignore Should the rendered method have the @Ignore attribute? + * @return Rendered test method. + */ + static String renderMethod( + String exerciseName, + LabeledTestCase labeledTestCase, + Template testMethodTemplate, + Template testErrorTemplate, + boolean ignore + ) { + // Convert the names of arguments and their values to string representation. + LinkedHashMap arguments = labeledTestCase.input as LinkedHashMap + ArrayList argumentNames = new ArrayList<>(arguments.keySet()) + ArrayList argumentValues = arguments.values().collect({ JsonOutput.toJson(it) }) + + // Prepare the list of argument names used in property call. + String commaSeparatedArgumentNames = argumentNames.join(', ') + + // Calculate column widths for data tables. + ArrayList columnWidths = [] + for (i in 0.. name.padRight(columnWidths[index]) }) + .join(' | ') + + // Pad values to column widths and join them using pipe. + String pipeSeparatedArgumentValues = + argumentValues + .withIndex() + .collect({ String value, Integer index -> value.padRight(columnWidths[index]) }) + .join(' | ') - engine.createTemplate(testClassPattern).make(bindings).toString() + // Check what template we should use: a regular test method template, + // or a template to test property call which should throw an error. + boolean useErrorCheckTemplate = + labeledTestCase.expected instanceof Map + && (labeledTestCase.expected as LinkedHashMap).containsKey('error') + + // Render the corresponding template. + (useErrorCheckTemplate ? + testErrorTemplate.make([ + ignore : ignore, + description : labeledTestCase.description, + exerciseName : exerciseName, + property : labeledTestCase.property, + commaSeparatedArgumentNames: commaSeparatedArgumentNames, + pipeSeparatedNamesTrimmed : pipeSeparatedArgumentNames.trim(), + pipeSeparatedValuesTrimmed : pipeSeparatedArgumentValues.trim(), + errorMessage : JsonOutput.toJson(labeledTestCase.expected['error']) + ]) : testMethodTemplate.make([ + ignore : ignore, + description : labeledTestCase.description, + exerciseName : exerciseName, + property : labeledTestCase.property, + commaSeparatedArgumentNames: commaSeparatedArgumentNames, + pipeSeparatedArgumentNames : pipeSeparatedArgumentNames, + pipeSeparatedArgumentValues: pipeSeparatedArgumentValues, + expected : JsonOutput.toJson(labeledTestCase.expected) + ])).toString() + } + + /** + * Load templates from filesystem. + * @return Retrieved templates. + */ + static ArrayList