From 9231c48c01c3c27bc10d1c8eb816c899d1a8b02a Mon Sep 17 00:00:00 2001 From: Derk-Jan Karrenbeld Date: Thu, 1 Aug 2024 19:14:25 +0200 Subject: [PATCH] Add concept exercise test with type tests (#74) --- .vscode/settings.json | 8 +- bin/run.sh | 85 ++++++++++++++++--- test/dev.test.mjs | 16 ++++ test/fixtures/lasagna/pass/.docs/hints.md | 30 +++++++ .../lasagna/pass/.docs/instructions.md | 38 +++++++++ .../lasagna/pass/.docs/introduction.md | 71 ++++++++++++++++ test/fixtures/lasagna/pass/.meta/config.json | 27 ++++++ test/fixtures/lasagna/pass/.meta/design.md | 38 +++++++++ test/fixtures/lasagna/pass/.meta/exemplar.ts | 45 ++++++++++ .../lasagna/pass/.meta/test-runner.mjs | 54 ++++++++++++ .../lasagna/pass/.vscode/extensions.json | 7 ++ .../lasagna/pass/.vscode/settings.json | 7 ++ test/fixtures/lasagna/pass/.yarnrc.yml | 3 + .../lasagna/pass/__typetests__/lasagna.tst.ts | 45 ++++++++++ test/fixtures/lasagna/pass/babel.config.cjs | 4 + test/fixtures/lasagna/pass/eslint.config.mjs | 26 ++++++ .../lasagna/pass/expected_results.json | 46 ++++++++++ test/fixtures/lasagna/pass/jest.config.cjs | 22 +++++ test/fixtures/lasagna/pass/lasagna.test.ts | 43 ++++++++++ test/fixtures/lasagna/pass/lasagna.ts | 45 ++++++++++ test/fixtures/lasagna/pass/package.json | 39 +++++++++ test/fixtures/lasagna/pass/tsconfig.json | 31 +++++++ test/fixtures/lasagna/pass/tstyche.stderr.txt | 0 test/fixtures/lasagna/pass/tstyche.stdout.txt | 22 +++++ test/fixtures/lasagna/pass/yarn.lock | 0 test/smoke.test.mjs | 9 ++ 26 files changed, 746 insertions(+), 15 deletions(-) create mode 100644 test/dev.test.mjs create mode 100644 test/fixtures/lasagna/pass/.docs/hints.md create mode 100644 test/fixtures/lasagna/pass/.docs/instructions.md create mode 100644 test/fixtures/lasagna/pass/.docs/introduction.md create mode 100644 test/fixtures/lasagna/pass/.meta/config.json create mode 100644 test/fixtures/lasagna/pass/.meta/design.md create mode 100644 test/fixtures/lasagna/pass/.meta/exemplar.ts create mode 100644 test/fixtures/lasagna/pass/.meta/test-runner.mjs create mode 100644 test/fixtures/lasagna/pass/.vscode/extensions.json create mode 100644 test/fixtures/lasagna/pass/.vscode/settings.json create mode 100644 test/fixtures/lasagna/pass/.yarnrc.yml create mode 100644 test/fixtures/lasagna/pass/__typetests__/lasagna.tst.ts create mode 100644 test/fixtures/lasagna/pass/babel.config.cjs create mode 100644 test/fixtures/lasagna/pass/eslint.config.mjs create mode 100644 test/fixtures/lasagna/pass/expected_results.json create mode 100644 test/fixtures/lasagna/pass/jest.config.cjs create mode 100644 test/fixtures/lasagna/pass/lasagna.test.ts create mode 100644 test/fixtures/lasagna/pass/lasagna.ts create mode 100644 test/fixtures/lasagna/pass/package.json create mode 100644 test/fixtures/lasagna/pass/tsconfig.json create mode 100644 test/fixtures/lasagna/pass/tstyche.stderr.txt create mode 100644 test/fixtures/lasagna/pass/tstyche.stdout.txt create mode 100644 test/fixtures/lasagna/pass/yarn.lock diff --git a/.vscode/settings.json b/.vscode/settings.json index b7e02b5..374bf2c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,11 @@ "eslint.nodePath": ".yarn/sdks", "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", "typescript.tsdk": ".yarn/sdks/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "cSpell.words": [ + "corepack", + "estree", + "exercism", + "tstyche" + ] } diff --git a/bin/run.sh b/bin/run.sh index 8e7dddb..067a0e3 100755 --- a/bin/run.sh +++ b/bin/run.sh @@ -166,6 +166,13 @@ if [[ "${OUTPUT}" =~ "$ROOT" ]]; then echo "✔️ tsconfig.json from root to output" cp "${ROOT}/tsconfig.solutions.json" "${OUTPUT}tsconfig.json" + + if test -f "${OUTPUT}yarn.lock"; then + echo "✔️ renaming yarn.lock in output to prevent yarn from " + echo " interpreting this directory as a standalone package." + mv "${OUTPUT}yarn.lock" "${OUTPUT}yarn.lock.💥.bak" || true + fi; + echo "" else echo "" @@ -256,7 +263,9 @@ else if test -f "${OUTPUT}package.json.💥.bak"; then echo "✔️ restoring package.json in output" - unlink "${OUTPUT}package.json" + if test -f "${OUTPUT}package.json"; then + unlink "${OUTPUT}package.json" + fi mv "${OUTPUT}package.json.💥.bak" "${OUTPUT}package.json" || true fi; @@ -265,8 +274,16 @@ else mv "${OUTPUT}tsconfig.json.💥.bak" "${OUTPUT}tsconfig.json" || true fi; + if test -f "${OUTPUT}yarn.lock.💥.bak"; then + echo "✔️ restoring yarn.lock in output" + if test -f "${OUTPUT}yarn.lock"; then + unlink "${OUTPUT}yarn.lock" + fi + mv "${OUTPUT}yarn.lock.💥.bak" "${OUTPUT}yarn.lock" || true + fi; + result="The submitted code cannot be ran by the test-runner. There is no configuration file inside the .meta (or .exercism) directory, and the fallback test file '${test_file}' does not exist. Please fix these issues and resubmit." - echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"$result\" }" > $result_file + echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"${result}\" }" > $result_file sed -Ei ':a;N;$!ba;s/\r{0,1}\n/\\n/g' $result_file echo "❌ could not run the test suite(s). A valid output exists:" @@ -366,7 +383,9 @@ if [ $test_exit -eq 2 ]; then if test -f "${OUTPUT}package.json.💥.bak"; then echo "✔️ restoring package.json in output" - unlink "${OUTPUT}package.json" + if test -f "${OUTPUT}package.json"; then + unlink "${OUTPUT}package.json" + fi mv "${OUTPUT}package.json.💥.bak" "${OUTPUT}package.json" || true fi; @@ -376,14 +395,22 @@ if [ $test_exit -eq 2 ]; then mv "${OUTPUT}tsconfig.json.💥.bak" "${OUTPUT}tsconfig.json" || true fi; + if test -f "${OUTPUT}yarn.lock.💥.bak"; then + echo "✔️ restoring yarn.lock in output" + if test -f "${OUTPUT}yarn.lock"; then + unlink "${OUTPUT}yarn.lock" + fi + mv "${OUTPUT}yarn.lock.💥.bak" "${OUTPUT}yarn.lock" || true + fi; + # Compose the message to show to the student # # TODO: interpret the tsc_result lines and pull out the source. # We actually already have code to do this, given the cursor position # - tsc_result=$(cat $result_file | jq -Rsa . | sed -e 's/^"//' -e 's/"$//') - tsc_result="The submitted code didn't compile. We have collected the errors encountered during compilation. At this moment the error messages are not very read-friendly, but it's a start. We are working on a more helpful output.\n-------------------------------\n$tsc_result" - echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"$tsc_result\" }" > $result_file + tsc_result="$(cat $result_file | jq -Rsa . | sed -e 's/^"//' -e 's/"$//')" + tsc_result="The submitted code didn't compile. We have collected the errors encountered during compilation. At this moment the error messages are not very read-friendly, but it's a start. We are working on a more helpful output.\n-------------------------------\n${tsc_result}" + echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"${tsc_result}\" }" > $result_file sed -Ei ':a;N;$!ba;s/\r{0,1}\n/\\n/g' $result_file echo "❌ tsc compilation failed with a valid output:" @@ -424,14 +451,14 @@ if test -d "${OUTPUT}__typetests__/"; then echo "" cd "${OUTPUT}" && corepack yarn tstyche --failFast 2> "${OUTPUT}tstyche.stderr.txt" 1> "${OUTPUT}tstyche.stdout.txt" - tstyche_error_output=$(cat "${OUTPUT}tstyche.stderr.txt") + tstyche_error_output="$(cat "${OUTPUT}tstyche.stderr.txt")" if [ -z "${tstyche_error_output}" ]; then echo "✅ all tests (*.tst.ts) passed." else - tstyche_result=$(echo $tstyche_error_output | jq -Rsa . | sed -e 's/^"//' -e 's/"$//') + tstyche_result=$(echo "${tstyche_error_output}" | jq -Rsa . | sed -e 's/^"//' -e 's/"$//' | sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g') tstyche_result="The submitted code did compile but at least one of the type-tests failed. We have collected the failing test encountered. At this moment the error messages are not very read-friendly, but it's a start. We are working on a more helpful output.\n-------------------------------\n${tstyche_result}" - echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"$tstyche_result\" }" > $result_file + echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"${tstyche_result}\" }" > $result_file sed -Ei ':a;N;$!ba;s/\r{0,1}\n/\\n/g' $result_file echo "❌ not all tests (*.tst.ts) passed." @@ -450,7 +477,9 @@ if test -d "${OUTPUT}__typetests__/"; then if test -f "${OUTPUT}package.json.💥.bak"; then echo "✔️ restoring package.json in output" - unlink "${OUTPUT}package.json" + if test -f "${OUTPUT}package.json"; then + unlink "${OUTPUT}package.json" + fi mv "${OUTPUT}package.json.💥.bak" "${OUTPUT}package.json" || true fi; @@ -459,6 +488,14 @@ if test -d "${OUTPUT}__typetests__/"; then mv "${OUTPUT}tsconfig.json.💥.bak" "${OUTPUT}tsconfig.json" || true fi; + if test -f "${OUTPUT}yarn.lock.💥.bak"; then + echo "✔️ restoring yarn.lock in output" + if test -f "${OUTPUT}yarn.lock"; then + unlink "${OUTPUT}yarn.lock" + fi + mv "${OUTPUT}yarn.lock.💥.bak" "${OUTPUT}yarn.lock" || true + fi; + echo "" echo "---------------------------------------------------------------" echo "The results of this run have been written to 'results.json'." @@ -496,11 +533,11 @@ if [ -z "${jest_tests}" ]; then # TODO: use results from tstyche runner_result="The type tests ran correctly. We are working on showing the individual tests results but for now, everything is fine!" - echo "{ \"version\": 1, \"status\": \"pass\", \"message\": \"$runner_result\" }" > $result_file + echo "{ \"version\": 1, \"status\": \"pass\", \"message\": \"${runner_result}\" }" > $result_file else echo "❌ neither type tests, nor execution tests ran" runner_result="The submitted code was not subjected to any type or execution tests. It did compile correctly, but something is wrong because at least one test was expected." - echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"$runner_result\" }" > $result_file + echo "{ \"version\": 1, \"status\": \"error\", \"message\": \"${runner_result}\" }" > $result_file sed -Ei ':a;N;$!ba;s/\r{0,1}\n/\\n/g' $result_file fi @@ -518,7 +555,9 @@ if [ -z "${jest_tests}" ]; then if test -f "${OUTPUT}package.json.💥.bak"; then echo "✔️ restoring package.json in output" - unlink "${OUTPUT}package.json" + if test -f "${OUTPUT}package.json"; then + unlink "${OUTPUT}package.json" + fi mv "${OUTPUT}package.json.💥.bak" "${OUTPUT}package.json" || true fi; @@ -527,6 +566,14 @@ if [ -z "${jest_tests}" ]; then mv "${OUTPUT}tsconfig.json.💥.bak" "${OUTPUT}tsconfig.json" || true fi; + if test -f "${OUTPUT}yarn.lock.💥.bak"; then + echo "✔️ restoring yarn.lock in output" + if test -f "${OUTPUT}yarn.lock"; then + unlink "${OUTPUT}yarn.lock" + fi + mv "${OUTPUT}yarn.lock.💥.bak" "${OUTPUT}yarn.lock" || true + fi; + echo "" echo "---------------------------------------------------------------" echo "The results of this run have been written to 'results.json'." @@ -581,7 +628,9 @@ fi; if test -f "${OUTPUT}package.json.💥.bak"; then echo "✔️ restoring package.json in output" - unlink "${OUTPUT}package.json" + if test -f "${OUTPUT}package.json"; then + unlink "${OUTPUT}package.json" + fi mv "${OUTPUT}package.json.💥.bak" "${OUTPUT}package.json" || true fi; @@ -590,6 +639,14 @@ if test -f "${OUTPUT}tsconfig.json.💥.bak"; then mv "${OUTPUT}tsconfig.json.💥.bak" "${OUTPUT}tsconfig.json" || true fi; +if test -f "${OUTPUT}yarn.lock.💥.bak"; then + echo "✔️ restoring yarn.lock in output" + if test -f "${OUTPUT}yarn.lock"; then + unlink "${OUTPUT}yarn.lock" + fi + mv "${OUTPUT}yarn.lock.💥.bak" "${OUTPUT}yarn.lock" || true +fi; + echo "" echo "---------------------------------------------------------------" echo "The results of this run have been written to 'results.json'." diff --git a/test/dev.test.mjs b/test/dev.test.mjs new file mode 100644 index 0000000..6bafdf0 --- /dev/null +++ b/test/dev.test.mjs @@ -0,0 +1,16 @@ +import { join } from 'node:path' +import shelljs from 'shelljs' +import { assertPass } from './asserts.mjs' +import { fixtures } from './paths.mjs' + +// run this file like: +// corepack yarn dlx cross-env SILENT=0 corepack yarn node test/dev.test.mjs + +shelljs.echo( + 'typescript-test-runner > passing solution (jest + tstyche) > no output directory' +) +assertPass( + 'lasagna', + join(fixtures, 'lasagna', 'pass'), + join(fixtures, 'lasagna', 'pass') +) diff --git a/test/fixtures/lasagna/pass/.docs/hints.md b/test/fixtures/lasagna/pass/.docs/hints.md new file mode 100644 index 0000000..e79f38c --- /dev/null +++ b/test/fixtures/lasagna/pass/.docs/hints.md @@ -0,0 +1,30 @@ +# Hints + +## 1. Define the expected oven time in minutes + +- Define a [constant][constants] which should contain the [`number`][numbers] value specified in the recipe. +- [`export`][export] the constant. + +## 2. Calculate the remaining oven time in minutes + +- [Explicitly return a number][return] from the function. +- Use the [mathematical operator for subtraction][operators] to subtract values. + +## 3. Calculate the preparation time in minutes + +- [Explicitly return a number][return] from the function. +- Use the [mathematical operator for multiplication][operators] to multiply values. +- Use the extra constant for the time in minutes per layer. + +## 4. Calculate the total working time in minutes + +- [Explicitly return a number][return] from the function. +- [Invoke][invocation] one of the other methods implemented previously. +- Use the [mathematical operator for addition][operators] to add values. + +[return]: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Return_values +[export]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export +[operators]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Arithmetic_Operators +[constants]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const +[invocation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#Calling_functions +[numbers]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type diff --git a/test/fixtures/lasagna/pass/.docs/instructions.md b/test/fixtures/lasagna/pass/.docs/instructions.md new file mode 100644 index 0000000..ee39b4c --- /dev/null +++ b/test/fixtures/lasagna/pass/.docs/instructions.md @@ -0,0 +1,38 @@ +# Instructions + +Lucian's girlfriend is on her way home, and he hasn't cooked their anniversary dinner! + +In this exercise, you're going to write some code to help Lucian cook an exquisite lasagna from his favorite cookbook. + +You have four tasks related to the time spent cooking the lasagna. + +## 1. Define the expected oven time in minutes + +Define the `EXPECTED_MINUTES_IN_OVEN` constant that represents how many minutes the lasagna should be in the oven. It must be exported. According to the cooking book, the expected oven time in minutes is `40`. + +## 2. Calculate the remaining oven time in minutes + +Implement the `remainingMinutesInOven` function that takes the actual minutes the lasagna has been in the oven as a _parameter_ and _returns_ how many minutes the lasagna still has to remain in the oven, based on the **expected oven time in minutes** from the previous task. + +```javascript +remainingMinutesInOven(30) +// => 10 +``` + +## 3. Calculate the preparation time in minutes + +Implement the `preparationTimeInMinutes` function that takes the number of layers you added to the lasagna as a _parameter_ and _returns_ how many minutes you spent preparing the lasagna, assuming each layer takes you 2 minutes to prepare. + +```javascript +preparationTimeInMinutes(2) +// => 4 +``` + +## 4. Calculate the total working time in minutes + +Implement the `totalTimeInMinutes` function that takes _two parameters_: the `numberOfLayers` parameter is the number of layers you added to the lasagna, and the `actualMinutesInOven` parameter is the number of minutes the lasagna has been in the oven. The function should _return_ how many minutes in total you've worked on cooking the lasagna, which is the sum of the preparation time in minutes, and the time in minutes the lasagna has spent in the oven at the moment. + +```javascript +totalTimeInMinutes(3, 20) +// => 26 +``` diff --git a/test/fixtures/lasagna/pass/.docs/introduction.md b/test/fixtures/lasagna/pass/.docs/introduction.md new file mode 100644 index 0000000..7679aab --- /dev/null +++ b/test/fixtures/lasagna/pass/.docs/introduction.md @@ -0,0 +1,71 @@ +# Introduction + +JavaScript is a dynamic language, supporting object-oriented, imperative, and declarative (e.g. functional programming) styles. + +## (Re-)Assignment + +There are a few primary ways to assign values to names in JavaScript - using variables or constants. On Exercism, variables are always written in [camelCase][wiki-camel-case]; constants are written in [SCREAMING_SNAKE_CASE][wiki-snake-case]. There is no official guide to follow, and various companies and organizations have various style guides. _Feel free to write variables any way you like_. The upside from writing them the way the exercises are prepared is that they'll be highlighted differently in the web interface and most IDEs. + +Variables in JavaScript can be defined using the [`const`][mdn-const], [`let`][mdn-let] or [`var`][mdn-var] keyword. + +A variable can reference different values over its lifetime when using `let` or `var`. For example, `myFirstVariable` can be defined and redefined many times using the assignment operator `=`: + +```javascript +let myFirstVariable = 1 +myFirstVariable = 'Some string' +myFirstVariable = new SomeComplexClass() +``` + +In contrast to `let` and `var`, variables that are defined with `const` can only be assigned once. This is used to define constants in JavaScript. + +```javascript +const MY_FIRST_CONSTANT = 10 + +// Can not be re-assigned. +MY_FIRST_CONSTANT = 20 +// => TypeError: Assignment to constant variable. +``` + +> 💡 In a later Concept Exercise the difference between _constant_ assignment / binding and _constant_ value is explored and explained. + +## Function Declarations + +In JavaScript, units of functionality are encapsulated in _functions_, usually grouping functions together in the same file if they belong together. These functions can take parameters (arguments), and can _return_ a value using the `return` keyword. Functions are invoked using `()` syntax. + +```javascript +function add(num1, num2) { + return num1 + num2 +} + +add(1, 3) +// => 4 +``` + +> 💡 In JavaScript there are _many_ different ways to declare a function. These other ways look different than using the `function` keyword. The track tries to gradually introduce them, but if you already know about them, feel free to use any of them. In most cases, using one or the other isn't better or worse. + +## Exposing to Other Files + +To make a `function`, a constant, or a variable available in _other files_, they need to be [exported][mdn-export] using the `export` keyword. Another file may then [import][mdn-import] these using the `import` keyword. This is also known as the module system. A great example is how all the tests work. Each exercise has at least one file, for example `lasagna.js`, which contains the _implementation_. Additionally there is at least one other file, for example `lasagna.spec.js`, that contains the _tests_. This file _imports_ the public (i.e. exported) entities in order to test the implementation: + +```javascript +// file.js +export const MY_VALUE = 10 + +export function add(num1, num2) { + return num1 + num2 +} + +// file.spec.js +import { MY_VALUE, add } from './file' + +add(MY_VALUE, 5) +// => 15 +``` + +[mdn-const]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const +[mdn-export]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export +[mdn-import]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import +[mdn-let]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let +[mdn-var]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var +[wiki-camel-case]: https://en.wikipedia.org/wiki/Camel_case +[wiki-snake-case]: https://en.wikipedia.org/wiki/Snake_case diff --git a/test/fixtures/lasagna/pass/.meta/config.json b/test/fixtures/lasagna/pass/.meta/config.json new file mode 100644 index 0000000..2a67f5b --- /dev/null +++ b/test/fixtures/lasagna/pass/.meta/config.json @@ -0,0 +1,27 @@ +{ + "authors": ["SleeplessByte"], + "files": { + "solution": [ + "lasagna.ts" + ], + "test": [ + "__typetests__/lasagna.tst.ts", + "lasagna.test.ts" + ], + "exemplar": [ + ".meta/exemplar.ts" + ] + }, + "forked_from": [ + "javascript/lasagna" + ], + "blurb": "Learn the basics of TypeScript cooking a brilliant lasagna from your favorite cooking book.", + "custom": { + "version.tests.compatibility": "jest-29", + "flag.tests.task-per-describe": true, + "flag.tests.may-run-long": false, + "flag.tests.includes-optional": false, + "flag.tests.jest": true, + "flag.tests.tstyche": true + } +} diff --git a/test/fixtures/lasagna/pass/.meta/design.md b/test/fixtures/lasagna/pass/.meta/design.md new file mode 100644 index 0000000..a2a6da3 --- /dev/null +++ b/test/fixtures/lasagna/pass/.meta/design.md @@ -0,0 +1,38 @@ +# Design + +## Learning objectives + +- Know what a variable is. +- Know what a constant variable is. +- Know how to define a variable. +- Know how to export a variable +- Know how to return a value from a function (explicit return). +- Know how to annotate a function parameter +- Know how to annotate a function return type + +## Out of scope + +This exercise is really just to introduce the bare minimum a student needs to know to solve a very basic exercise on Exercism. +Details about the primitive data types, different ways to define functions etc. will all be properly introduced in the later concept exercises. + +We don't even explicitly teach the basics of numbers and arithmetic operators in the introduction. +Given the general code examples that are provided and some "I will just try that", the student should be fine solving the exercise nevertheless. + +## Concepts + +- `basics` + +## Prerequisites + +There are no prerequisites. + +## Analyzer + +This exercise could benefit from the following rules added to the the [analyzer][analyzer]: + +- Verify that the `remainingMinutesInOven` function uses the `EXPECTED_MINUTES_IN_OVEN` constant. +- Verify that the `preparationTimeInMinutes` function uses the `PREPARATION_MINUTES_PER_LAYER` constant. +- Verify that the `totalTimeInMinutes` function calls the `preparationTimeInMinutes` function. +- Verify that no extra _bookkeeping_ or _intermediate_ variables are declared + +[analyzer]: https://github.com/exercism/typescript-analyzer diff --git a/test/fixtures/lasagna/pass/.meta/exemplar.ts b/test/fixtures/lasagna/pass/.meta/exemplar.ts new file mode 100644 index 0000000..debccc3 --- /dev/null +++ b/test/fixtures/lasagna/pass/.meta/exemplar.ts @@ -0,0 +1,45 @@ +/** + * The amount of minutes the lasagna should be in the oven. + */ +export const EXPECTED_MINUTES_IN_OVEN = 40 + +/** + * The amount of minutes it takes to prepare a single layer. + */ +const PREPARATION_MINUTES_PER_LAYER = 2 + +/** + * Determines the amount of minutes the lasagna still needs to remain in the + * oven to be properly prepared. + * + * @param actualMinutesInOven + * @returns the number of minutes remaining + */ +export function remainingMinutesInOven(actualMinutesInOven: number): number { + return EXPECTED_MINUTES_IN_OVEN - actualMinutesInOven +} + +/** + * Given a number of layers, determines the total preparation time. + * + * @param numberOfLayers + * @returns the total preparation time + */ +export function preparationTimeInMinutes(numberOfLayers: number): number { + return numberOfLayers * PREPARATION_MINUTES_PER_LAYER +} + +/** + * Calculates the total working time. That is, the time to prepare all the layers + * of lasagna, and the time already spent in the oven. + * + * @param numberOfLayers + * @param actualMinutesInOven + * @returns the total working time + */ +export function totalTimeInMinutes( + numberOfLayers: number, + actualMinutesInOven: number +): number { + return preparationTimeInMinutes(numberOfLayers) + actualMinutesInOven +} diff --git a/test/fixtures/lasagna/pass/.meta/test-runner.mjs b/test/fixtures/lasagna/pass/.meta/test-runner.mjs new file mode 100644 index 0000000..1f49865 --- /dev/null +++ b/test/fixtures/lasagna/pass/.meta/test-runner.mjs @@ -0,0 +1,54 @@ +import { execSync } from 'node:child_process' +// Experimental: import config from './config.json' with { type: 'json' } + +import { readFileSync } from 'node:fs' +import { exit } from 'node:process' + +/** @type {import('./config.json') } */ +const config = JSON.parse( + readFileSync(new URL('./config.json', import.meta.url)) +) + +const jest = !config.custom || config.custom['flag.tests.jest'] +const tstyche = config.custom?.['flag.tests.tstyche'] + +console.log( + `[tests] tsc: ✅, tstyche: ${tstyche ? '✅' : '❌'}, jest: ${jest ? '✅' : '❌'}, ` +) + +console.log('[tests] tsc (compile)') + +try { + execSync('corepack yarn lint:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) +} catch { + exit(-1) +} + +if (tstyche) { + console.log('[tests] tstyche (type tests)') + + try { + execSync('corepack yarn test:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-2) + } +} + +if (jest) { + console.log('[tests] tstyche (implementation tests)') + + try { + execSync('corepack yarn test:implementation', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-3) + } +} diff --git a/test/fixtures/lasagna/pass/.vscode/extensions.json b/test/fixtures/lasagna/pass/.vscode/extensions.json new file mode 100644 index 0000000..daaa5ee --- /dev/null +++ b/test/fixtures/lasagna/pass/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "arcanis.vscode-zipfs", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/test/fixtures/lasagna/pass/.vscode/settings.json b/test/fixtures/lasagna/pass/.vscode/settings.json new file mode 100644 index 0000000..761fb42 --- /dev/null +++ b/test/fixtures/lasagna/pass/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": ["exercism"], + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + } +} diff --git a/test/fixtures/lasagna/pass/.yarnrc.yml b/test/fixtures/lasagna/pass/.yarnrc.yml new file mode 100644 index 0000000..23e4a6d --- /dev/null +++ b/test/fixtures/lasagna/pass/.yarnrc.yml @@ -0,0 +1,3 @@ +compressionLevel: mixed + +enableGlobalCache: true diff --git a/test/fixtures/lasagna/pass/__typetests__/lasagna.tst.ts b/test/fixtures/lasagna/pass/__typetests__/lasagna.tst.ts new file mode 100644 index 0000000..2db7d78 --- /dev/null +++ b/test/fixtures/lasagna/pass/__typetests__/lasagna.tst.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from 'tstyche' +import { + EXPECTED_MINUTES_IN_OVEN, + remainingMinutesInOven, + preparationTimeInMinutes, + totalTimeInMinutes, +} from '../lasagna.ts' + +describe('EXPECTED_MINUTES_IN_OVEN', () => { + test('constant is defined as a number or a constant number', () => { + expect(EXPECTED_MINUTES_IN_OVEN).type.toBeAssignableTo() + }) +}) + +describe('remainingMinutesInOven', () => { + test('takes one number parameter', () => { + expect>().type.toBe<[number]>() + }) + + test('returns a number', () => { + expect>().type.toBe() + }) +}) + +describe('preparationTimeInMinutes', () => { + test('takes one number parameter', () => { + expect>().type.toBe<[number]>() + }) + + test('returns a number', () => { + expect>().type.toBe() + }) +}) + +describe('totalTimeInMinutes', () => { + test('takes two number parameters', () => { + expect>().type.toBe< + [number, number] + >() + }) + + test('returns a number', () => { + expect>().type.toBe() + }) +}) diff --git a/test/fixtures/lasagna/pass/babel.config.cjs b/test/fixtures/lasagna/pass/babel.config.cjs new file mode 100644 index 0000000..ae4e66a --- /dev/null +++ b/test/fixtures/lasagna/pass/babel.config.cjs @@ -0,0 +1,4 @@ +module.exports = { + presets: [[require('@exercism/babel-preset-typescript'), { corejs: '3.37' }]], + plugins: [], +} diff --git a/test/fixtures/lasagna/pass/eslint.config.mjs b/test/fixtures/lasagna/pass/eslint.config.mjs new file mode 100644 index 0000000..1be39c5 --- /dev/null +++ b/test/fixtures/lasagna/pass/eslint.config.mjs @@ -0,0 +1,26 @@ +// @ts-check + +import tsEslint from 'typescript-eslint' +import config from '@exercism/eslint-config-typescript' +import maintainersConfig from '@exercism/eslint-config-typescript/maintainers.mjs' + +export default [ + ...tsEslint.config(...config, { + files: ['.meta/proof.ci.ts', '.meta/exemplar.ts', '*.test.ts'], + extends: maintainersConfig, + }), + { + ignores: [ + // # Protected or generated + '.git/**/*', + '.vscode/**/*', + + //# When using npm + 'node_modules/**/*', + + // # Configuration files + 'babel.config.cjs', + 'jest.config.cjs', + ], + }, +] diff --git a/test/fixtures/lasagna/pass/expected_results.json b/test/fixtures/lasagna/pass/expected_results.json new file mode 100644 index 0000000..ca2bb5d --- /dev/null +++ b/test/fixtures/lasagna/pass/expected_results.json @@ -0,0 +1,46 @@ +{ + "status": "pass", + "tests": [ + { + "name": "EXPECTED_MINUTES_IN_OVEN > constant is defined correctly", + "status": "pass", + "message": "", + "output": null, + "test_code": "expect(EXPECTED_MINUTES_IN_OVEN).toBe(40)", + "task_id": 1 + }, + { + "name": "remainingMinutesInOven > calculates the remaining time", + "status": "pass", + "message": "", + "output": null, + "test_code": "expect(remainingMinutesInOven(25)).toBe(15)\nexpect(remainingMinutesInOven(5)).toBe(35)\nexpect(remainingMinutesInOven(39)).toBe(1)", + "task_id": 2 + }, + { + "name": "remainingMinutesInOven > works correctly for the edge cases", + "status": "pass", + "message": "", + "output": null, + "test_code": "expect(remainingMinutesInOven(40)).toBe(0)\nexpect(remainingMinutesInOven(0)).toBe(40)", + "task_id": 2 + }, + { + "name": "preparationTimeInMinutes > calculates the preparation time", + "status": "pass", + "message": "", + "output": null, + "test_code": "expect(preparationTimeInMinutes(1)).toBe(2)\nexpect(preparationTimeInMinutes(2)).toBe(4)\nexpect(preparationTimeInMinutes(8)).toBe(16)", + "task_id": 3 + }, + { + "name": "totalTimeInMinutes > calculates the total cooking time", + "status": "pass", + "message": "", + "output": null, + "test_code": "expect(totalTimeInMinutes(1, 5)).toBe(7)\nexpect(totalTimeInMinutes(4, 15)).toBe(23)\nexpect(totalTimeInMinutes(1, 30)).toBe(32)", + "task_id": 4 + } + ], + "version": 3 +} \ No newline at end of file diff --git a/test/fixtures/lasagna/pass/jest.config.cjs b/test/fixtures/lasagna/pass/jest.config.cjs new file mode 100644 index 0000000..0aba1a5 --- /dev/null +++ b/test/fixtures/lasagna/pass/jest.config.cjs @@ -0,0 +1,22 @@ +module.exports = { + verbose: true, + projects: [''], + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/test/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + testPathIgnorePatterns: [ + '/(?:production_)?node_modules/', + '.d.ts$', + '/test/fixtures', + '/test/helpers', + '__mocks__', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + moduleNameMapper: { + '^(\\.\\/.+)\\.js$': '$1', + }, +} diff --git a/test/fixtures/lasagna/pass/lasagna.test.ts b/test/fixtures/lasagna/pass/lasagna.test.ts new file mode 100644 index 0000000..72e6272 --- /dev/null +++ b/test/fixtures/lasagna/pass/lasagna.test.ts @@ -0,0 +1,43 @@ +import { describe, test, expect } from '@jest/globals' + +import { + EXPECTED_MINUTES_IN_OVEN, + remainingMinutesInOven, + preparationTimeInMinutes, + totalTimeInMinutes, +} from './lasagna.ts' + +describe('EXPECTED_MINUTES_IN_OVEN', () => { + test('constant is defined correctly', () => { + expect(EXPECTED_MINUTES_IN_OVEN).toBe(40) + }) +}) + +describe('remainingMinutesInOven', () => { + test('calculates the remaining time', () => { + expect(remainingMinutesInOven(25)).toBe(15) + expect(remainingMinutesInOven(5)).toBe(35) + expect(remainingMinutesInOven(39)).toBe(1) + }) + + test('works correctly for the edge cases', () => { + expect(remainingMinutesInOven(40)).toBe(0) + expect(remainingMinutesInOven(0)).toBe(40) + }) +}) + +describe('preparationTimeInMinutes', () => { + test('calculates the preparation time', () => { + expect(preparationTimeInMinutes(1)).toBe(2) + expect(preparationTimeInMinutes(2)).toBe(4) + expect(preparationTimeInMinutes(8)).toBe(16) + }) +}) + +describe('totalTimeInMinutes', () => { + test('calculates the total cooking time', () => { + expect(totalTimeInMinutes(1, 5)).toBe(7) + expect(totalTimeInMinutes(4, 15)).toBe(23) + expect(totalTimeInMinutes(1, 30)).toBe(32) + }) +}) diff --git a/test/fixtures/lasagna/pass/lasagna.ts b/test/fixtures/lasagna/pass/lasagna.ts new file mode 100644 index 0000000..debccc3 --- /dev/null +++ b/test/fixtures/lasagna/pass/lasagna.ts @@ -0,0 +1,45 @@ +/** + * The amount of minutes the lasagna should be in the oven. + */ +export const EXPECTED_MINUTES_IN_OVEN = 40 + +/** + * The amount of minutes it takes to prepare a single layer. + */ +const PREPARATION_MINUTES_PER_LAYER = 2 + +/** + * Determines the amount of minutes the lasagna still needs to remain in the + * oven to be properly prepared. + * + * @param actualMinutesInOven + * @returns the number of minutes remaining + */ +export function remainingMinutesInOven(actualMinutesInOven: number): number { + return EXPECTED_MINUTES_IN_OVEN - actualMinutesInOven +} + +/** + * Given a number of layers, determines the total preparation time. + * + * @param numberOfLayers + * @returns the total preparation time + */ +export function preparationTimeInMinutes(numberOfLayers: number): number { + return numberOfLayers * PREPARATION_MINUTES_PER_LAYER +} + +/** + * Calculates the total working time. That is, the time to prepare all the layers + * of lasagna, and the time already spent in the oven. + * + * @param numberOfLayers + * @param actualMinutesInOven + * @returns the total working time + */ +export function totalTimeInMinutes( + numberOfLayers: number, + actualMinutesInOven: number +): number { + return preparationTimeInMinutes(numberOfLayers) + actualMinutesInOven +} diff --git a/test/fixtures/lasagna/pass/package.json b/test/fixtures/lasagna/pass/package.json new file mode 100644 index 0000000..c895d38 --- /dev/null +++ b/test/fixtures/lasagna/pass/package.json @@ -0,0 +1,39 @@ +{ + "name": "@exercism/typescript-lasagna", + "version": "1.0.0", + "description": "Exercism concept exercise on lasagna", + "author": "Derk-Jan Karrenbeld (https://derk-jan.com)", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/typescript" + }, + "type": "module", + "engines": { + "node": "^18.16.0 || >=20.0.0" + }, + "devDependencies": { + "@exercism/babel-preset-typescript": "^0.5.0", + "@exercism/eslint-config-typescript": "^0.7.1", + "@jest/globals": "^29.7.0", + "@types/node": "~22.0.2", + "babel-jest": "^29.7.0", + "core-js": "~3.37.1", + "eslint": "^9.8.0", + "expect": "^29.7.0", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "tstyche": "^2.1.1", + "typescript": "~5.5.4", + "typescript-eslint": "^7.18.0" + }, + "scripts": { + "test": "corepack yarn node .meta/test-runner.mjs", + "test:types": "corepack yarn tstyche", + "test:implementation": "corepack yarn jest --no-cache --passWithNoTests", + "lint": "corepack yarn lint:types && corepack yarn lint:ci", + "lint:types": "corepack yarn tsc --noEmit -p .", + "lint:ci": "corepack yarn eslint . --ext .tsx,.ts" + }, + "packageManager": "yarn@4.3.1" +} diff --git a/test/fixtures/lasagna/pass/tsconfig.json b/test/fixtures/lasagna/pass/tsconfig.json new file mode 100644 index 0000000..23fc994 --- /dev/null +++ b/test/fixtures/lasagna/pass/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + // Allows you to use the newest syntax, and have access to console.log + // https://www.typescriptlang.org/tsconfig#lib + "lib": ["ES2020", "dom"], + // Make sure typescript is configured to output ESM + // https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm + "module": "Node16", + // Since this project is using babel, TypeScript may target something very + // high, and babel will make sure it runs on your local Node version. + // https://babeljs.io/docs/en/ + "target": "ES2020", // ESLint doesn't support this yet: "es2022", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // Because jest-resolve isn't like node resolve, the absolute path must be .ts + "allowImportingTsExtensions": true, + "noEmit": true, + + // Because we'll be using babel: ensure that Babel can safely transpile + // files in the TypeScript project. + // + // https://babeljs.io/docs/en/babel-plugin-transform-typescript/#caveats + "isolatedModules": true + }, + "exclude": [".meta/*", "__typetests__/*", "*.test.ts", "*.tst.ts"] +} diff --git a/test/fixtures/lasagna/pass/tstyche.stderr.txt b/test/fixtures/lasagna/pass/tstyche.stderr.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/lasagna/pass/tstyche.stdout.txt b/test/fixtures/lasagna/pass/tstyche.stdout.txt new file mode 100644 index 0000000..0b520e1 --- /dev/null +++ b/test/fixtures/lasagna/pass/tstyche.stdout.txt @@ -0,0 +1,22 @@ +uses TypeScript 5.5.4 + +pass ./__typetests__/lasagna.tst.ts + EXPECTED_MINUTES_IN_OVEN + + constant is defined as a number or a constant number + remainingMinutesInOven + + takes one number parameter + + returns a number + preparationTimeInMinutes + + takes one number parameter + + returns a number + totalTimeInMinutes + + takes two number parameters + + returns a number + +Targets: 1 passed, 1 total +Test files: 1 passed, 1 total +Tests: 7 passed, 7 total +Assertions: 7 passed, 7 total +Duration: 0.7s + +Ran all test files. diff --git a/test/fixtures/lasagna/pass/yarn.lock b/test/fixtures/lasagna/pass/yarn.lock new file mode 100644 index 0000000..e69de29 diff --git a/test/smoke.test.mjs b/test/smoke.test.mjs index 7ab986b..31d7bc1 100644 --- a/test/smoke.test.mjs +++ b/test/smoke.test.mjs @@ -16,6 +16,15 @@ shelljs.echo( ) assertError('clock', join(fixtures, 'clock', 'fail')) +shelljs.echo( + 'typescript-test-runner > passing solution (jest + tstyche) > no output directory' +) +assertPass( + 'lasagna', + join(fixtures, 'lasagna', 'pass'), + join(fixtures, 'lasagna', 'pass') +) + /** Test failures */ const failures = ['tests', 'empty']