diff --git "a/AideM\303\251moireJava.pdf" "b/AideM\303\251moireJava.pdf" deleted file mode 100644 index 66dbcaf..0000000 Binary files "a/AideM\303\251moireJava.pdf" and /dev/null differ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 986cd74..fd4980e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,8 @@ In particular: kids don't need to know about functionnal interfaces, arrays, or You might ask why not start right away with programming the robot then? The students are way too lost in the midst of pages of arcane syntax, and we observed anecdotally it is slower to teach that way. In particular, it is requiring a lot more repetition, because they jump too fast from one concept they don't master to another, and don't register them the first time they hear about it. +That being said, the bonus koans are providing additional material. We found good success using them only after students have started programming actual robots, so as to keep the kids excited. + ### Enough exercises Most of the resources have too few exercices. Or exercises not really relevant to the kind of thinking we need for programming robots with WPILib. On the other hand, (most) kids like to learn by practicing. So this curiculum is focused on having a good amount exercices. Of course "a good amount" is not an exact science, so we expect to tweak this as our experience teaching various profiles of rookies is growing. @@ -62,7 +64,7 @@ We expressed above a concern for saving student's attention / motivation / time. ### Design goals - Strive to work on a bare WPILib installation: on VSCode with no need for a plugin. -- Simple start: no dependency other than the Java standard library, so as to avoid a build step with a dependency management tool. +- Simple start: no dependency other than the Java standard library, so as to avoid a build step with a dependency management tool. This has consequences: the project has to includes a mini test framework for example. - Java 17, because as of 2024, this is the version used by default in WPILib's VSCode. ### Compromises and limitations @@ -77,3 +79,36 @@ We expressed above a concern for saving student's attention / motivation / time. Pull requests for translations, curiculum tweaks or new capabilities are welcome. When submitting bugs, please submit a zip file of the koans in a state exhibiting the issue. + +## I want to contribute. What can I do? + +Here are suggestions, in ascending order of involvement: + +* Submit [issues](https://github.com/jletroui/FrcJavaKoans/issues/new) for text issues: typos, awkward, ambiguous, etc... +* Submit pull requests for text issues: typos, awkward, ambiguous, etc... +* Report [bugs](https://github.com/jletroui/FrcJavaKoans/issues/new). Please submit a zip file of the koans in a state exhibiting the issue. +* Submit [issues](https://github.com/jletroui/FrcJavaKoans/issues/new) or pull requests for better koans replacing existing ones. In particular, one improvements we are trying to converge to are koans exhibiting the same pedagogic targets and quality, but FRC and robot themed. +* Submit [issues](https://github.com/jletroui/FrcJavaKoans/issues/new) or pull requests for new koans in existing series plugging a hole in the learning journey. +* Better engine code comments or test coverage. +* Submit a new bonus koan series. Current potentially beneficial areas not covered: sugar syntax (var, ternary operator, etc...), inheritance (and its fallbacks), generics, etc... +* A new language localization. This involves: + 1. Adding the new [Locale](https://github.com/jletroui/FrcJavaKoans/blob/master/src/main/java/engine/Locale.java) and [Localizable helper](https://github.com/jletroui/FrcJavaKoans/blob/master/src/main/java/engine/Localizable.java#L20). + 2. Translating all [sensei and assertion messages](https://github.com/jletroui/FrcJavaKoans/blob/master/src/main/java/engine/Texts.java). + 3. Translating all [koans title and texts](https://github.com/jletroui/FrcJavaKoans/blob/master/src/main/java/sensei/Texts.java). + 4. Adding a [`*PathToEnlightment.java`](https://github.com/jletroui/FrcJavaKoans/tree/master/src/main/java). + 5. Copying the [main series of koans](https://github.com/jletroui/FrcJavaKoans/tree/master/src/main/java/koans/english) in a package for the new language. Translate all the comments in there. + 6. Repeat 5. for the [bonus koans](https://github.com/jletroui/FrcJavaKoans/tree/master/src/main/java/bonuses/english). + 7. Add koans solutions for the new language in [the testing companion project](https://github.com/jletroui/FrcJavaKoansTests), to make sure koans work in the new language as well. + +## Testing + +Automated testing of this project was challenging for a few reasons, main ones being: + +1. By design, we don't have a build tool (Gradle or Maven for example), nor access to any libraries. So no JUnit on hand to test the koans engine. +2. We would not want to include solutions to the koans within the project, because the students might stumble on them, which would affect their learning. + +Here are the compromises we came up with to solve these challenges: + +For 1., we have created a mini test framework in `engine.test.runner` in order to run unit and integration tests of the koans engine. Tests are located in `engine.test`. To run those tests, simply run the `engine.test.runner.TestRunner.main` method. + +For 2., we have created a [brother project](https://github.com/jletroui/FrcJavaKoansTests) testing the koans themselves. diff --git a/README.md b/README.md index a01d8ad..f6fa62d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ -**[![Drapeau Français](images/french_flag.png) Français ](#french) [![English Flag](images/english_flag.png) English ](#english)** ---- - -# ![English Flag](images/english_flag.png) Java Koans for the FRC +# Java Koans for the FRC ## Table of content @@ -28,7 +25,7 @@ To get started you will need to either install VS Code or use GitHub Codespaces, To install VS Code, you will need to install [WPILib](https://docs.wpilib.org/en/stable/docs/zero-to-robot/step-2/wpilib-setup.html) first to run the Java Koans for the FRC. -Once installed, download the [latest release](https://github.com/jletroui/FrcJavaKoans/releases/download/v1.1/FrcJavaKoans.zip) of the Java Koans. +Once installed, download the [latest release](https://github.com/jletroui/FrcJavaKoans/releases/download/v2.0/FrcJavaKoans.zip) of the Java Koans. Then, extract it somewhere on your computer. Go to the folder where you have downloaded the koans, righ-click on the koans zip file, and choose 'Extract All'. Choose your destination folder, for example, a `/src` folder within your `Documents` folder. @@ -255,6 +252,10 @@ To help you remember the bits of Java syntax you are learning while completing y ## And then what? After completing the koans, you are ready to learn how to program an actual robot. Mykah, from [team 9153 - Bearcat Robotics](https://sites.google.com/lincolnschoolscb.org/9153-bearcat-robotics/home), is maintaining a [wonderful compendium of FRC programming resources](https://docs.google.com/document/d/1jcBLAyJ3iTbsYSnWMVWqHaK8uywGTaTjF98eY_xxpl0/edit#heading=h.21bclvyus8vm) from which you can dig for your next steps. + +Once you understand how to program a simple `TimedRobot`, you can come back here and practice with bonus koans you will find in `src/main/java/bonuses/english`. These koan series are independant of each other and can be followed in any order. +To run them, right click on the one you are interested, for example `src/main/java/bonuses/english/AboutArrays.java` and choose `Run Java` directely on the koans file itself. +
> Experience is the name everyone gives to their mistakes. @@ -267,6 +268,19 @@ This course intent to come batteries included, with 100% of the information need We have found that students learn faster if mentors are not giving solutions to students' issues, but instead ask them open questions about what they don't understand about the exercise instructions or displayed error. Programming is, most of the time, about figuring out what little detail have been overlooked. Thus, helping students to look for answers by themselves in the koans' text will help them become more autonomous faster when it will be time to program and debug a robot. +### Full curiculum suggestion + +1) Start with the students completing all the initial koan series ("EnglishPathToEnlightment"). +2) Not part of the FRC Java Koans: make them program their first robot with a simple `TimedRobot`. It is suggested to have a simple differential drive robot. + * Super simple auto mode making the robot go forward at 20% speed for 1 second. + * Simple teleop mode making the robot move with a joystick. +3) Students can now follow the `src/main/java/bonuses/english/AboutInterfaces.java` bonus koans. +4) Not part of the FRC Java Koans: make them program the simple robot again, but using [commands based programming](https://docs.wpilib.org/en/stable/docs/software/commandbased/index.html). + +Optional: + +Before teaching them to deal with a Swerve Drive, the students could follow `src/main/java/bonuses/english/AboutArrays.java` to learn arrays and `for` loops. + ### Topics included * Printing to and reading from the console @@ -274,10 +288,16 @@ We have found that students learn faster if mentors are not giving solutions to * `int`, `boolean`, `String`, `double` basic types * `if`, `else`, `if else` construct * Methods -* `while` loops +* `for` loops +* Arrays * Packages and classes with static methods * Objects, constructors, fields +In the bonus koans: + +* Arrays and `for` loops +* Functional interfaces + ### Contributing and learning more If you are interested in learning more about this course or contributing to it, please take a look at [CONTRIBUTING.md](CONTRIBUTING.md) :) @@ -294,307 +314,8 @@ Typos have been fixed by `someonesomething`. Many thanks to early testers who gave me feedback: Andy, Noémie, Chenxin, and Dumitru. -## License - -![CC BY-SA](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/). - ---- - -# ![Drapeau Français](images/french_flag.png) Koans Java pour la FRC - -## Sommaire - -- [Aperçu](#aperçu) -- [Comment commencer avec VS Code](#comment-commencer-avec-vs-code) -- [Comment commencer avec GitHub Codespaces](#comment-commencer-avec-github-codespaces) -- [Apprendre à programmer avec les Koans Java pour la FRC](#apprendre-à-programmer-avec-les-koans-java-pour-la-frc) -- [Utilisation d'un IDE basé sur le navigateur](#utilisation-dun-IDE-bas%C3%A9-sur-le-navigateur) -- [Aide mémoire Java](#aide-mémoire-Java) -- [Et ensuite?](#et-ensuite) -- [Mentors](#mentors-fr) -- [Attributions et remerciements](#attributions-et-remerciements) -- [License](#license-fr) - -## Aperçu - -Les Koans Java pour la FRC forment un cours interactif, pas à pas, pour enseigner le Java à des élèves engagés dans la [Compétition Robotique First](https://www.firstinspires.org/robotics/frc). Il ne requiert aucune expérience préalable en programmation. L'intention n'est pas d'être un cours complet sur Java, mais plutôt d'apprendre juste ce qu'il faut des fondamentaux pour pouvoir commencer à apprendre à programmer un robot FRC avec WPILib. - -## Comment commencer avec VS Code - -Pour commencer, vous devrez soit installer VS Code, soit utiliser GitHub Codespaces, qui est un environnement de développement intégré basé sur navigateur. Si vous ne pouvez pas installer VS Code, par exemple si vous utilisez un Chromebook ou si vous n'avez pas les autorisations nécessaires pour installer une application, vous pouvez passer à l'étape suivante [Comment commencer avec GitHub Codespaces](#comment-commencer-avec-github-codespaces). - -Si ce n'est déjà fait, tu vas devoir installer [WPILib](https://docs.wpilib.org/en/stable/docs/zero-to-robot/step-2/wpilib-setup.html) pour pouvoir exécuter les Koans Java pour la FRC. - -Une fois installé, télécharge [la dernière version](https://github.com/jletroui/FrcJavaKoans/releases/download/v1.1/FrcJavaKoans.zip) des Koans Java. - -Ensuite, décompresse les quelque part sur ton ordinateur. Pour ce faire, va dans le répertoire où tu as téléchargé les koans, clic-droit dessus, et choisis "Extraire Tout". Choisis un répertoire de destination, par exemple, un répertoire `/src` dans ton répertoire `Documents`. - -![Décompression, étape 1](images/extract_step1.png) -![Décompression, étape 2](images/extract_step2.png) - -Ensuite, ouvre le VSCode du WPILib: - -![Ouvrir VS Code](images/launch_vs_code.png) - -Et ouvre le répertoire dans lequel tu as décompressé les Koans (par exemple: `C:\Users\Jane\Documents\src\FrcJavaKoans`) - -![Ouvrir le répertoire dans VS Code](images/open_folder_in_vs_code.png) - -Note: VS Code va te demander si tu fais confiance à ce code. Tu vas devoir répondre "oui". - -Tu es maintenant prêt·e! - -## Comment commencer avec GitHub Codespaces - -Ce dépôt est configuré pour une utilisation aisée avec GitHub Codespaces. GitHub Codespaces fournit un environnement de développement basé sur le cloud. Il vous permet de configurer et d'accéder facilement à un environnement de développement cohérent directement depuis votre navigateur web. Cela est particulièrement utile lorsque vos étudiants n'ont accès qu'à des Chromebooks ou pour d'autres raisons ne peuvent pas installer et configurer Visual Studio Code. - -Pour commencer avec GitHub Codespaces, cliquez simplement sur le bouton "Code" et sélectionnez "Créer un espace de code sur Master" pour créer ou accéder à votre environnement de développement. - -![Codespace](images/codespace_button.png) - -## Apprendre à programmer avec les Koans Java pour la FRC - -Un koan est un défi lancé par un maître zen pour apprendre quelque chose du monde. Ici, ta mission est de résoudre les koans qui vont t'aider à apprendre à programmer en Java. - -### Demande des koans au maître - -Quand tu ouvre les Koans Java pour la FRC dans VS Code, tu devrais voir quelque chose de similaire à ceci: - -![VS Code](images/opened_koans.png) - -Ouvre le répertoire `src`, puis fais un clic droit sur le fichier `src\main\java\EnglishPathToEnlightment.java` et choisis `Run Java`: - -![Run Java](images/run_java_fr.png) - -Note: Windows Defender va peut être te demander si tu autorise l'application à utiliser le réseau. Tu as alors besoin de l'autoriser. - -Cela va ouvrir ce que l'on appelle un _terminal_ dans le bas de la fenêtre de VS Code, et exécuter les Koans Java pour la FRC. Ignore tout le texte cryptique généré par VS Code dans le terminal et concentre toi sur cette partie: - -![Premier résultat](images/result1_fr.png) - -Le maître des Koans Java te dit beaucoup de choses d'un coup. Alors décomposons tout ça: - -![Premier résultat, commenté](images/result1_commented_fr.png) - -Tout d'abord, il te dit qu'il essaie de t'enseigner `AboutConsoleAndVariables`. Ensuite, il te dit que tu n'as pas complété le koan `AboutConsoleAndVariables.sayHelloInConsole`. Ce qui est normal, car tu n'a même pas encore commencé! Ensuite, il te montre une boîte étrange appelée `Console`, que l'on va ignorer pour le moment. Tout en bas,il te dit que tu peux chercher `sayHelloInConsole` dans le fichier `src/main/java/koans/english/AboutConsoleAndVariables.java`. - -### Ouvrir le fichier du koan - -Ouvre ce fichier dans VS Code: - -![open src/main/java/koans/french/AboutConsoleAndVariables.java](images/open_first_koan_fr.png) - -Wow, il y a beaucoup de choses là dedans! Pour comprendre tout ça, nous avons besoin d'apprendre quelques notions de Java. - -### Fichiers Java - -Tout le code écrit en Java doit aller dans des fichiers avec l'extension `.java`. Le contenu de ces fichiers suit une organisation stricte. D'abord, en haut, il y a plusieurs lignes qui aident Java à savoir quels sont les autres fichier dont il va avoir besoin pour comprendre celui-ci:: - -```java -package koans.french; - -import static engine.Helpers.readLine; -``` - -Nous expliquerons en détails ces lignes plus tard, et allons les ignorer pour le moment. Ne t'inquiètes pas, quand tu auras fini avec les koans, tu vas les comprendre! - -### La classe - -Ensuite dans le fichier, nous pouvons voir ceci: - -```java -public class AboutConsoleAndVariables { -``` - -Cela instruit Java que nous créons une classe `AboutConsoleAndVariables`. Tout le code Java est organisé en classes. Tu peux voir une classe comme un tiroir ou une étagère qui contient des morceaux de code. Tu peux seulement avoir une classe par fichier Java, et le nom du fichier doit correspondre au nom de la classe, avec l'extension `.java`. C'est pourquoi le fichier que nous regardons présentement est nommé `AboutConsoleAndVariables.java`. - -Tout le code de la classe est compris entre l'accolade ouvrante `{` et la fermante `}` tout à la fin du fichier. - -**Note**: en Java, partout où tu dois délimiter des morceaux de code, nous allons écrire ce code entre une accolade ouvrante `{` et une fermante `}`. Cela instruit Java que tout ce qu'il y a entre ces accolades appartient à la même chose. - -### Les instructions du koan - -Ensuite, nous pouvons voir ces lignes, en vert: - -```java - /** - * # Afficher du texte dans la console - * - * Afficher 'Hello!' dans la console. - * - * --------- INDICES -------------- - * - * En Java, toutes les instructions de code sur une ligne doivent se terminer avec un ';'. Ex: - * - * System.out.println("Pomme"); - * - * Tu peux utiliser la méthode System.out.println([une valeur]) pour afficher une valeur dans la console. - * - * Tu peux dire à Java qu'une valeur est un texte en l'entourant par des guillemets. Ex: - * - * "Ceci est du texte" - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Hello! - * - */ -``` - -Ces lignes forment ce qu'on appelle un 'commentaire'. Tout le texte vert entre `/**` et `*/` est un commentaire. Un commentaire est une information destinée à des humains, et est ignoré par Java. Ce n'est pas du code. C'est très utile pour documenter ton code quand tu écris du Java. Le maître se sert d'un de ces commentaires pour placer les instructions de chacun de ses koans. - -La première ligne du commentaire te donne le titre du Koan: `# Afficher du texte dans la console`. - -Ensuite, tu peux trouver l'objectif du koan en haut du commentaire: `Afficher 'Hello!' dans la console.`. La console est la façon la plus simple pour un programme Java d'afficher quelque chose dans un terminal. Tu te souviens de ce qui a été affiché lorsque tu as exécuté les koans? Tu as vu ce bout de texte: - -``` -Console: ---------- - - ---------- -``` - -C'est là que le texte que tu vas afficher dans la console va apparaître. - -Après le but du koan, le maître te donne des indices pour résoudre le koan. Par exemple, il t'explique ici que tu peux utiliser l'instruction Java `System.out.println([une valeur])`. - -**Note**: quand tu vois dans les instructions du maître des crochets '[' et ']', ce n'est pas pour taper dans le programme les crochets et ce qu'il y a entre eux. En effet, c'est un moyen pour désigner un espace à remplacer par quelque chose d'autre. - -Finalement, la dernière section des instructions te montre quel résultat ton code est supposé produire. Attention! Les petits détails comme la grammaire et les espaces sont importants! - -Lorsque tu essaies de résoudre un koan, prends ton temps pour comprendre toute l'information qui t'es donnée dans les instructions pour te faciliter la tâche. - -### La méthode du koan - -Nous arrivons enfin à la partie du fichier où tu vas pouvoir écrire du code en Java! En Java, tout le code qui s'exécute doit faire partie d'un "morceau de code" que l'on appelle une méthode. Une méthode n'est rien d'autre qu'un mini-programme. Voici la méthode du premier koan: - -```java - public static void sayHelloInConsole() { - - } -``` - -Nous allons ignorer les mots clefs `public static void` pour l'instant. Ce qui vient après est le nom de la méthode: `sayHelloInConsole`. Toutes les méthodes ont un nom en Java, ce qui permet d'y faire référence plus tard, si tu veux exécuter le code la méthode. Après le nom viennent les parenthèses: `()`. C'est le signe qui dit à Java que `sayHelloInConsole` est une méthode. Et finalment, les fameuses accolades ouvrante `{` et fermante `}`. Tout le code que tu vas écrire devra se trouver entre les 2 accolades d'une méthode, et nulle part ailleurs. Si tu écris du code en dehors, Java va te montrer une erreur. - -Alors essayons de résoudre ce premier koan. Nous devons donc afficher "Hello!" dans la console. Le premier indice nous dit: - -``` - * En Java, toutes les instructions de code sur une ligne doivent se terminer avec un ';'. -``` - -Très bien, alors tapons tout de suite un ';' à la fin de la ligne pour ne pas oublier: - -```java - public static void sayHelloInConsole() { - ; - } -``` - -Le prochain indice nous dit: - -``` - * Tu peux utiliser la méthode System.out.println([une valeur]) pour afficher une valeur dans la console. -``` - -Nous savons que nous pouvons utiliser cette commande, et que nous allons devoir remplacer `[some value]` par le texte _Hello!_ plus tard. Tapons cette commande sans sa valeur pour le moment: - -```java - public static void sayHelloInConsole() { - System.out.println(); - } -``` - -Le dernier indice nous explique comment écrire une valeur textuelle: - -``` - * Tu peux dire à Java qu'une valeur est un texte en l'entourant par des guillemets. -``` - -Parfait! Alors plaçons notre valeur dans la commande tapée précédemment: - -```java - public static void sayHelloInConsole() { - System.out.println("Hello"); - } -``` - -### Redemander son avis au maître - -Maintenant que nous avons complété notre koan, demandons au maître ce qu'il en pense. Pour exécuter les koans, fais un clic droit sur `FrenchPathToEnlightment.java` et choisis `Run Java`. Tu devrais voir ceci: - - -![Second result](images/result2_fr.png) - - -Oups, nous avons fait une erreur! Le maître s'attendait à ce que l'on affiche _Hello!_, mais nous avons oublié le point d'exclamation. Corrigeons ceci dans `src/main/java/koans/french/AboutConsoleAndVariables.java`: - -```java - public static void sayHelloInConsole() { - System.out.println("Hello!"); - } -``` - -Et exécutons les koans de nouveau: - -![Third result](images/result3_fr.png) - - -C'est un message différent cette fois! C'est parce que nous avons complété avec succès le premier koan! Le maître nous parle de notre progrès et du prochain koan. - -Félicitations! Tu as complété ton premier koan! Tu peux maintenant regarder le koan suivant dans `src/main/java/koans/french/AboutConsoleAndVariables.java` et essayer de trouver comment le compléter. - -## Aide mémoire Java - -Pour t'aider à retenir les morceaux de syntaxe Java que tu apprends en complétant tes koans, tu peux imprimer une copie de l'[Aide mémoire Java](./AideMémoireJava.pdf). +Edited for use for FRC 4143 by Cole Hunt -## Et ensuite? - -Une fois avoir complété les koans, tu es prêt·e pour apprendre à programmer un vrai robot. Mykah, de [l'équipe 9153 - Bearcat Robotics](https://sites.google.com/lincolnschoolscb.org/9153-bearcat-robotics/home), entretient [un excellent recueil de ressources pour la programmation FRC](https://docs.google.com/document/d/1jcBLAyJ3iTbsYSnWMVWqHaK8uywGTaTjF98eY_xxpl0/edit#heading=h.21bclvyus8vm). Tu pourras sûrement y trouver tes prochains apprentissages. - -
- -> Expérience est le nom que les gens donnent aux erreurs qu'ils ont faites. - -*Oscar Wilde* - - -## Mentors - -Ce cours se veut fournir toute l'information nécessaire pour qu'un élève motivé puisse apprendre le Java sans aucune autre ressource à sa disposition. Cependant, l'expérience est bien meilleure avec l'assistance de mentors. - -Nous avons trouvé que les élèves passent à travers le cours plus rapidement, et intègrent mieux les notions si le ou la mentor ne lui donne pas de solution à ses problèmes. À la place, le ou la mentor peut garder l'élève dans une posture active en lui demandant ce qu'iel ne comprend pas dans l'énoncé de l'exercice, ou du message d'erreur, et le / la guider dans des stratégies pour comprendre par iel même. La programmation consiste, la plupart du temps, à trouver quel petit détail nous avons oublié. En aidant les élèves à trouver les ressources pour résoudre les problèmes eux-même, ils deviennent au final autonomes plus rapidement lorsqu'ils débogueront un programme de robot. - -### Sujets inclus - -* Écrire dans, et lire de, la console -* Variables -* Types basiques `int`, `boolean`, `String`, `double` -* Instructions `if`, `else`, `if else` -* Méthodes -* Boucles `while` -* Packages et classes avec méthodes statiques -* Objets, constructeurs, champs - -### En savoir plus, contribuer - -Si vous êtes intéressés à en apprendre plus ou à contribuer à ce cours, rendez-vous sur la page [CONTRIBUTING.md](CONTRIBUTING.md) :) - -## Attributions et remerciements - -Créé par un mentor de [l'équipe 3550 Robotronix](https://www.instagram.com/3550robotronix/) (Montréal, Canada). - -Grandement inspiré par les merveilleux [Ruby Koans](https://www.rubykoans.com/). - -La fonctionnalité Github Codespaces est une contribution de `jmcconne10`. - -Plusieurs erreurs de texte ont été corrigées par `someonesomething`. - -Grand merci aux premiers testeurs qui ont donné leur avis: Andy, Noémie, Chenxin, et Dumitru. - - ## License -![CC BY-SA](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) Cette oeuvre est disponible sous une license [Attribution - Partage dans les Mêmes Conditions 4.0 International](https://creativecommons.org/licenses/by-sa/4.0/deed.fr). +![CC BY-SA](https://i.creativecommons.org/l/by-sa/4.0/88x31.png) This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/images/open_first_koan_fr.png b/images/open_first_koan_fr.png deleted file mode 100644 index 324fed0..0000000 Binary files a/images/open_first_koan_fr.png and /dev/null differ diff --git a/images/result1_commented_fr.png b/images/result1_commented_fr.png deleted file mode 100644 index ea9281f..0000000 Binary files a/images/result1_commented_fr.png and /dev/null differ diff --git a/images/result1_fr.png b/images/result1_fr.png deleted file mode 100644 index 976f6fb..0000000 Binary files a/images/result1_fr.png and /dev/null differ diff --git a/images/result2_fr.png b/images/result2_fr.png deleted file mode 100644 index efee4e5..0000000 Binary files a/images/result2_fr.png and /dev/null differ diff --git a/images/result3_fr.png b/images/result3_fr.png deleted file mode 100644 index 0bc6074..0000000 Binary files a/images/result3_fr.png and /dev/null differ diff --git a/images/run_java_fr.png b/images/run_java_fr.png deleted file mode 100644 index c68d382..0000000 Binary files a/images/run_java_fr.png and /dev/null differ diff --git a/src/main/java/FrenchPathToEnlightment.java b/src/main/java/FrenchPathToEnlightment.java deleted file mode 100644 index 3338343..0000000 --- a/src/main/java/FrenchPathToEnlightment.java +++ /dev/null @@ -1,9 +0,0 @@ -import engine.Locale; -import engine.Sensei; -import sensei.Wisdom; - -public class FrenchPathToEnlightment { - public static void main(String[] args) { - new Sensei(Locale.fr, Wisdom.koans).offerKoans(); - } -} diff --git a/src/main/java/bonuses/english/AboutArrays.java b/src/main/java/bonuses/english/AboutArrays.java new file mode 100644 index 0000000..2c33fd1 --- /dev/null +++ b/src/main/java/bonuses/english/AboutArrays.java @@ -0,0 +1,406 @@ +package bonuses.english; + +import java.util.List; + +import engine.Locale; +import engine.Sensei; +import sensei.AboutArraysKoans; + +public class AboutArrays { + /** + * # For loops + * + * Write a method named 'displayNumbers' with an integer parameter 'n', which displays numbers between 1 and n. + * Use a 'for' loop instead of a 'while' loop. Use the shortest form of incrementation / decrementation. + * + * --------- TIPS -------------- + * + * To do things multiple times in Java, you already know the 'while' loop. + * Most of the time, a 'while' loop has very similar code structure: + * + * [Initialize a counter]; + * while ([Condition on the counter variable]) { + * // Do stuff repetedly + * [Modify the counter at the end of the loop]; + * } + * + * Ex: + * + * int times = 3; // Initialize a counter + * while (times > 0) { // Condition on the counter variable + * System.out.println("Still executing"); // Do stuff repetedly + * times = times -1; // Modify the counter at the end of the loop + * } + * + * Since this pattern is used again and again, there is a shortcut in Java to make it terser. It's called a 'for' loop, where all 3 parts seen above are grouped on a single line: + * + * for([Initialize a counter]; [Condition on the counter variable]; [Modify the counter at the end of the loop]) { + * // do stuff repetedly + * } + * + * Ex: + * + * for(int times = 3; times > 0; times = times -1) { + * System.out.println("Still executing"); // Do stuff repetedly + * } + * + * There is even another shortcut: since incrementing or decrementing a number is something that happens very often, there is a short form: + * + * a = a - 2; // Long form + * a -= 2; // Short form + * b = b + 2; // Long form + * b += 2; // Short form + * + * When we increment / decrement by one, there is an even shorter form: + * + * a -= 1; // Short form + * a--; // Shortest form + * b += 1; // Short form + * b++; // Shortest form + * + * Using this, our for loop can become even terser: + * + * for(int times = 3; times > 0; times--) { + * System.out.println("Still executing"); // Do stuff repetedly + * } + * + * ------------------------------- + * + * Expected result: + * + * displayNumbers(3) should display: + * + * 1 + * 2 + * 3 + * + */ + + + /** + * # First element of an array + * + * Write a method named 'first' with an array of integers 'array' as a parameter, which returns the first element of that array. + * + * --------- TIPS -------------- + * + * Sometime, we would like to store a lot of values of the same type. Example: recording the battery level of a robot for every second during a 2m30s match (150 double values). + * Having 150 variable of type 'double' would be very cumbersome to manipulate in our program. Instead, we would like a single variable, of a type storing all 150 values at once. + * + * Such a type is called an 'array'. To create an array of values of type 'SomeType' with n values, you can write: + * + * SomeType[] myArray = new SomeType[n]; + * + * Ex: + * + * double[] batteryLevels = new double[150]; + * + * An array is storing all values sequentially. You can access one value (called an 'element of the array') by using its position called an 'index'. Indices in arrays are starting at 0. You specify the index inside '[' and ']'. + * Therefore, if you want to display the 3rd element of 'batteryLevels': + * + * System.out.println(batteryLevels[2]); + * + * Similarly, if you want to store a value in the 2nd element: + * + * bateryLevels[1] = 12.8; + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 20; + * System.out.println(first(a)); + * + * Should display: + * + * 10 + * + */ + + + /** + * # Last element of an array + * + * Write a method named 'last' with an array of integers 'array' as a parameter, which returns the last element of that array. + * + * --------- TIPS -------------- + * + * To know the size of a given array you did not create yourself, you can use its 'length' field: + * + * double[] batteryLevels = new double[150]; + * System.out.println(batteryLevels.length); // Display 150 + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 20; + * System.out.println(last(a)); + * + * Should display: + * + * 20 + * + */ + + + /** + * # Finding an element + * + * Write a method named 'findFirst' with an array of integers 'array' and a integer 'n' as parameters. + * It returns the first index of the given number within the array. + * If the element is not found, it should return -1. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 20; + * a[2] = 20; + * System.out.println(findFirst(a, 20)); + * + * Should display: + * + * 1 + * + */ + + + /** + * # Finding an element at the end + * + * Write a method named 'findLast' with an array of integers 'array' and a integer 'n' as parameters. + * It returns the last index of the given number within the array. + * If the element is not found, it should return -1. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 20; + * a[2] = 20; + * System.out.println(findLast(a, 20)); + * + * Should display: + * + * 2 + * + */ + + + /** + * # Finding the smallest element + * + * Write a method named 'min' with an array of integers 'array' as a parameter. + * It returns the smallest number within the array. + * If the array is empty, it should return Integer.MAX_VALUE, which is the largest possible int. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 2; + * a[2] = 20; + * System.out.println(min(a)); + * + * Should display: + * + * 2 + * + */ + + + /** + * # Finding the smallest element, revisited + * + * Using a 'for :' loop, write a method named 'min2' with an array of integers 'array' as a parameter. + * It returns the smallest number within the array. + * If the array is empty, it should return Integer.MAX_VALUE, which is the largest possible int. + * + * --------- TIPS -------------- + * + * Believe it or not, but when it comes to looping through all elements of an array, there is yet another shortcut for the 'for' loop. + * + * Since looking at all elements of an array is such a common place, there is a special syntax you can use: + * + * for(type variableName: someArray) { + * // do something with variableName + * } + * + * For example, for displaying all the elements of an array: + * + * for(double level: batteryLevels) { + * System.out.println(level); + * } + * + * Note: this is only useful if you do not need the index of the current element. If, in addition to the element, you also need its index within the loop, use a regular 'for' loop. + * + * ------------------------------- + * + * Expected result: + * + * Same as the min2() method, but using a 'for :' loop instead of a regular 'for' loop. + * + */ + + + /** + * # Computing the sum + * + * Write a method named 'sum' with an array of integers 'array' as a parameter. + * It returns the sum of all numbers within the array. + * If the array is empty, it should return 0. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 2; + * a[2] = 20; + * System.out.println(sum(a)); + * + * Should display: + * + * 32 + * + */ + + + /** + * # Computing the average + * + * Write a method named 'avg' with an array of integers 'array' as a parameter. + * It returns the average of numbers within the array (as a double). + * If the array is empty, it should return 0.0. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 3; + * a[2] = 20; + * System.out.println(avg(a)); + * + * Should display: + * + * 11 + * + */ + + + /** + * # Filling an array + * + * Write a method named 'fill' with an integer 'size' and an integer 'value' as a parameters. + * It returns an integer array with that number of element, all of them having the value 'value'. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * fill(3, 20); + * + * Should return an array with 3 elements, all of them having the value 20. + * + */ + + + /** + * # Creating a serie + * + * Write a method named 'serie' with an integer 'size' as a parameter. + * It returns an integer array with that number of element. + * The first element is 1. The second is 2. The 3rd is 3, and so on. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * serie(4); + * + * Should return the array [1, 2, 3, 4]. + * + */ + + + /** + * # Switch two elements + * + * Write a method named 'switchFirst2' with takes an array 'array' as a parameter. + * If the array has 2 and only 2 elements, the method is switching them within the array. + * Otherwise, it does nothing. + * + * ------------------------------- + * + * Expected result: + * + * After the following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 3; + * switchFirst2(a); + * + * a should be [3, 10]. + * + */ + + + /** + * # Reverse an array + * + * Write a method named 'reverse' with takes an array 'array' as a parameter. + * Returns a new array with all elements reverted. + * + * ------------------------------- + * + * Expected result: + * + * After the following code: + * + * var a = new int[2]; + * a[0] = 10; + * a[1] = 3; + * a[2] = 20; + * + * reverse(a) should return the array [20, 3, 10]. + * + */ + + + public static void main(String[] args) { + new Sensei(Locale.en, List.of(AboutArraysKoans.koans)).offerKoans(); + } +} diff --git a/src/main/java/bonuses/english/AboutInterfaces.java b/src/main/java/bonuses/english/AboutInterfaces.java new file mode 100644 index 0000000..b4bf72b --- /dev/null +++ b/src/main/java/bonuses/english/AboutInterfaces.java @@ -0,0 +1,267 @@ +package bonuses.english; + +import java.util.List; +import java.util.function.IntPredicate; + +import bonuses.teachingmaterial.Combining; +import engine.Locale; +import engine.Sensei; +import sensei.AboutInterfacesKoans; + +public class AboutInterfaces { + /** + * # First interface implementations + * + * Write a class 'numbers.AddNumbers' which implements interface 'bonuses.teachingmaterial.Combining'. + * The implementation of the combine() method should return the 2 numbers added together. + * Write a class 'numbers.MultiplyNumbers' which also implements interface 'bonuses.teachingmaterial.Combining'. + * This implementation of the combine() method should return the 2 numbers multiplied together. + * + * --------- TIPS -------------- + * + * Consider the following situation: + * + * You are in the middle of a large, empty room, when a zombie suddenly attacks you. + * You have no weapon. + * Luckily, a fellow living human is standing in the doorway of the room. + * "Quick!" you shout at him. "Throw me something I can hit the zombie with!" + * + * Now consider: + * You didn't specify (nor do you care) exactly what your friend will choose to toss; ...But it doesn't matter, as long as: + * + * 1) It's something that can be tossed (He can't toss you the sofa) + * 2) It's something that you can grab hold of (Not a wet soap) + * 3) It's something you can use to bash the zombie's brains out (That rules out pillows and such) + * + * It doesn't matter whether you get a baseball bat or a hammer - as long as it implements your three conditions, you're good. + * + * In Java, you often need an object with specific methods, whatever its class is. + * An interface as a kind of contract that an object would respect, by the object implementing the methods listed in the contract. + * + * For example, in a game where the situation above would happen, you could create the following interface: + * + * public interface Weapon { + * void hit(Monster monster); + * } + * + * Now, whether your weapon is a sword inflicting 10 damage points, or a knife inflicting only 4 damage points to the monster, the player will be able to hit a monster with it. + * + * Respecting the contract of a Java interface is called 'implementing the interface'. A class can implement an interface this way: + * + * you declare that this class will implement the Weapon interface + * vvvvvvvvvvvvvvvvv + * + * public class Sword implements Weapon { + * + * // Since Sword implements Weapon, it must implement the hit method. + * @Override // This strange annotation tells Java the method is defined elsewhere (in our interface) + * public void hit(Monster monster) { + * // Some code computing and applying damage to the monster, applying some tear and wear on the weapon, etc... + * } + * + * } + * + * Now, the code in 'hit()' maybe complicated but it does not matter: it follows the contract of the interface, and you can call it. + * For example, you could create an object allowing you to hit a zombie with the following code: + * + * Monster zombie = ...; // Code getting the object for the zombie in the middle of the room + * Weapon tossedWeapon = new Sword(); // or 'new Hammer()' or 'new Axe()' or 'new WhateverImplementsWeapon()' + * tossedWeapon.hit(zombie); // use the Weapon interface + * + * Notice the type of the variable 'tossedWeapon' is a 'Weapon', not a 'Sword'. Interfaces, like classes, are types you can use for your variables, fields, and parameters. + * Because 'Sword' implements 'Weapon', Java considers that a 'Sword' object _is_ a 'Weapon'. Having variables using the interface type allows it to take values from objects from multiple classes, as long as they all implement the interface. + * + * Take a look at the 'bonuses.teachingmaterial.Combining' interface. It defines a method which can be implemented in various ways. + * This exercise is about implementing that interface in 2 ways. + * + * ------------------------------- + * + * Expected result: + * + * The following code: + * + * Combining combining = new AddNumbers(); + * System.out.println(combining.combine(3, 4)); + * combining = new MultiplyNumbers(); + * System.out.println(combining.combine(3, 4)); + * + * Should display: + * + * 7 + * 12 + * + */ + + + /** + * # Anonymous interface implementation + * + * Write a method 'getAnonymousCombining' which returns an anonymous implementation of 'bonuses.teachingmaterial.Combining'. + * The implementation of the combine() method should return the second number subtracted from the first. + * + * --------- TIPS -------------- + * + * Sometimes, creating a file and a public class is a lot of work when we implement a simple interface, and we only use it in a single place. + * In such a situation, you can implement the interface in an anonymous class. It is anonymous, because it does not have a name. + * That class is instantiated immediately where it is created. For example: + * + * public Weapon toss() { + * return new Weapon() { + * @Override + * public void hit(Monster monster) { + * // Some code computing and applying damage to the monster, applying some tear and wear on the weapon, etc... + * } + * } + * } + * + * When looking at this code, you could be tempted to believe there is a constructor for the interface Weapon, but there is not. + * We are really creating a class, for which the only place we will create objects is this 'toss()' method. + * The constructor with empty parameters is the one of this nameless class. + * We can now get and use the tossed weapon this way: + * + * Weapon tossedWeapon = toss(); + * tossedWeapon.hit(zombie); + * + * ------------------------------- + * + * Expected result: + * + * getAnonymousCombining().combine(3, 4) should return -1 + * + */ + + + /** + * # Lambda methods + * + * Write a method 'getLambdaCombining' which returns an lambda method implementing 'bonuses.teachingmaterial.Combining'. + * The implementation of the combine() method should return the first number subtracted from the second. + * + * --------- TIPS -------------- + * + * When an interface have only one method, there is an even shorter form to implement it. You can create what is called a "lambda method". + * A 'lambda method' is a stripped down version of a method. Since our example interface 'Weapon' has only a single method 'hit()', we can use this shortcut: + * + * For example: + * + * public Weapon toss() { + * return (monster) -> { + * // Some code computing and applying damage to the monster, applying some tear and wear on the weapon, etc.. + * }; + * } + * + * We can now get and use the tossed weapon this way: + * + * Weapon tossedWeapon = toss(); + * tossedWeapon.hit(zombie); + * + * The general syntax for lambda method having a body with multiple lines is: + * + * ([param1Name], [param2Name], ...) -> { + * // Lambda method body here + * } + * + * If your lambda is having a single expression, you can even skip the curly brackets and the 'return' statement: + * + * ([param1Name], [param2Name], ...) -> // expression here + * + * Here are some example of methods and their lambda equivalent (assuming the interface has only one of these methods in its contract): + * + * This interface implementation: + * + * public void sayHello() { + * System.out.println("hello"); + * } + * + * Can be replaced by this lambda: + * + * () -> System.out.println("hello") + * + * This interface implementation: + * + * public int square(int x) { + * return x * x; + * } + * + * Can be replaced by this lambda: + * + * (x) -> x * x + * + * This interface implementation: + * + * public int min(int x, int y) { + * if (x < y) { + * return x; + * } + * return y; + * } + * + * Can be replaced by this lambda: + * + * (x, y) -> { + * if (x < y) { + * return x; + * } + * return y; + * } + * + * ------------------------------- + * + * Expected result: + * + * getLambdaCombining().combine(3, 4) should return 1 + * + */ + + + /** + * # Common lambda interfaces + * + * Write a method 'getIsEven' which returns a lambda method testing if an integer is even. + * + * --------- TIPS -------------- + * + * Note: the {@link java.lang.String} notation allows to show a link to a class in a comment. To see the class, you can [CTRL] + clic on its name. + * + * Since lambda methods are so useful, a lot of simple interfaces already exist in the Java standard library, and we don't have to create them ourselves. + * For example, an interface with a method having the same signature as the 'combine()' method already exists. It is called the {@link java.util.function.IntBinaryOperator}. + * + * Other examples: + * + * For a lambda taking no parameter, and returning nothing, {@link java.lang.Runnable}: + * + * Runnable sayHello = () -> System.out.println("Hello"); + * + * For a lambda taking a 'int' parameter, and returning nothing, {@link java.util.function.IntConsumer}: + * + * IntConsumer displayInt = (anInt) -> System.out.println(anInt); + * + * The same exist for other parameter types. For example {@link java.util.function.DoubleConsumer}: + * + * DoubleConsumer displayDouble = (aDouble) -> System.out.println(aDouble); + * + * The reverse methods, taking no parameter, but returning something exist as well: {@link java.util.function.IntSupplier}, {@link java.util.function.DoubleSupplier}. etc... + * + * DoubleSupplier giveMePiPleeeaaase = () -> 3.14159; + * + * There is also a lot of case where you would need to test if a number respect a certain condition. This is where "predicate" interfaces like {@link java.util.function.IntPredicate} shine: + * + * IntPredicate isPositive = (number) -> number >= 0; + * + * For the exercise, you can use the modulo operator, %, which computes the remainder of an integer division: + * + * int remainder = 17 % 5; // remainder equals 2 + * + * ------------------------------- + * + * Expected result: + * + * getIsEven().test(4) should return true + * + */ + + + public static void main(String[] args) { + new Sensei(Locale.en, List.of(AboutInterfacesKoans.koans)).offerKoans(); + } +} diff --git a/src/main/java/bonuses/teachingmaterial/Combining.java b/src/main/java/bonuses/teachingmaterial/Combining.java new file mode 100644 index 0000000..735b66d --- /dev/null +++ b/src/main/java/bonuses/teachingmaterial/Combining.java @@ -0,0 +1,11 @@ +package bonuses.teachingmaterial; + +/** + * This file is used by the AboutInterfaces koans. + */ +public interface Combining { + /** + * somehow combines 2 integers a and b, and returns the result of this combination. + */ + public int combine(int a, int b); +} diff --git a/src/main/java/engine/Assertions.java b/src/main/java/engine/Assertions.java index 296c877..8b3366c 100644 --- a/src/main/java/engine/Assertions.java +++ b/src/main/java/engine/Assertions.java @@ -4,23 +4,27 @@ import java.util.Arrays; import java.util.Optional; import java.util.function.DoubleToIntFunction; -import java.util.function.Function; -import engine.ConsoleFmt.Formats; +import engine.script.Expression; import engine.script.Type; -import static engine.ConsoleFmt.code; -import static engine.ConsoleFmt.format; +import static engine.Fmt.classFullName; +import static engine.Fmt.classSimpleName; +import static engine.Fmt.code; +import static engine.Fmt.green; +import static engine.Fmt.red; +import static engine.Fmt.sameStyle; +import static engine.Fmt.sequence; import static engine.Localizable.global; import static engine.Texts.*; /** - * Library of various assertions which can be run about the result of a koan execution. + * Library of various assertions which can be run either before the koan test runs or about the result of a koan execution. */ public class Assertions { private static String resolveTemplateParam(final KoanResult res, final Object param) { - if (param instanceof FormatParam) { - return ((FormatParam)param).format(res); + if (param instanceof final FormatParam fp) { + return fp.format(res); } return Optional.ofNullable(param).map((v) -> v.toString()).orElse(""); @@ -34,24 +38,30 @@ public static ResultAssertion assertNextStdOutLineEquals(final Localizable { final var realParams = Arrays.stream(params) .map((param) -> Assertions.resolveTemplateParam(res, param)) - .toArray(); - final var expected = String.format(expectedTemplate.get(res.locale), realParams); + .toArray(String[]::new); + final var realParamsFmted = Arrays + .stream(realParams) + .map(param -> sameStyle(global(param))) + .toArray(Fmt[]::new); + final var expected = String.format(expectedTemplate.get(res.locale), (Object[])realParams); + final var expectedFmted = sameStyle(expectedTemplate, realParamsFmted); final var lineContent = res.nextStdOutLine(); + final Fmt expressionFmted = code(res.resultExpressionSourceCode); if (lineContent.isEmpty()) { - p.println(format(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_NOTHING, Formats.Red, expected, code(res.resultExpressionSourceCode))); + p.println(red(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_NOTHING, expectedFmted, expressionFmted)); return false; } if (!lineContent.get().equals(expected)) { if (lineContent.get().equals("")) { - p.println(format(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_NOTHING, Formats.Red, expected, code(res.resultExpressionSourceCode))); + p.println(red(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_NOTHING, expectedFmted, expressionFmted)); } else { - p.println(format(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_INSTEAD, Formats.Red, expected, code(res.resultExpressionSourceCode), lineContent.get())); + p.println(red(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_INSTEAD, expectedFmted, expressionFmted, sameStyle(lineContent.get()))); } return false; } - p.println(format(OK_DISPLAYED_IN_CONSOLE, Formats.Green, expected, code(res.resultExpressionSourceCode))); + p.println(green(OK_DISPLAYED_IN_CONSOLE, expectedFmted, expressionFmted)); return true; }; } @@ -61,15 +71,15 @@ public static ResultAssertion assertNextStdOutLineIsEmpty() { final var lineContent = res.nextStdOutLine(); if (lineContent.isEmpty()) { - p.println(ConsoleFmt.red(EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_NOTHING)); + p.println(red(EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_NOTHING)); return false; } if (!lineContent.get().equals("")) { - p.println(ConsoleFmt.red(EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_INSTEAD), lineContent.get()); + p.println(red(EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_INSTEAD, sameStyle(lineContent.get()))); return false; } - p.println(ConsoleFmt.green(OK_DISPLAYED_EMPTY_LINE_IN_CONSOLE)); + p.println(green(OK_DISPLAYED_EMPTY_LINE_IN_CONSOLE)); return true; }; } @@ -79,7 +89,7 @@ public static ResultAssertion assertNoMoreLineInStdOut() { final var lineContent = res.nextStdOutLine(); if (!lineContent.isEmpty()) { - p.println(format(EXPECTED_TO_SEE_NOTHING_IN_CONSOLE_BUT_SAW_INSTEAD, Formats.Red, code(res.resultExpressionSourceCode), lineContent.get())); + p.println(red(EXPECTED_TO_SEE_NOTHING_IN_CONSOLE_BUT_SAW_INSTEAD, code(res.resultExpressionSourceCode), sameStyle(lineContent.get()))); return false; } @@ -92,240 +102,264 @@ public static ResultAssertion assertAskedInStdIn() { final var lineContent = res.nextStdInLine(); if (lineContent.isPresent()) { - p.println(ConsoleFmt.green(OK_ASKED_FOR_LINE_IN_CONSOLE)); + p.println(green(OK_ASKED_FOR_LINE_IN_CONSOLE)); return true; } - p.println(ConsoleFmt.red(EXPECTED_FOR_USER_TO_ANSWER_IN_CONSOLE)); + p.println(red(EXPECTED_FOR_USER_TO_ANSWER_IN_CONSOLE)); return false; }; } - public static ResultAssertion assertReturnValueEquals(final int expected) { + private static final double EPSILON = 0.0000000001; + private static boolean eq(final Object expected, final Object actual) { + if (actual == null) { + return expected == null; + } else if (expected instanceof final int[] aIntArr && actual instanceof final int[] bIntArr) { + return Arrays.equals(aIntArr, bIntArr); + } else if (expected instanceof final Double aDouble && actual instanceof final Double bDouble) { + final var diff = Math.abs(aDouble.doubleValue() - bDouble); + return diff < EPSILON; + } + return actual.equals(expected); + } + + public static ResultAssertion assertVariableEquals(final String variableName, final Object expected) { return (p, res) -> { - if (res.executionResult == null) { - p.println(format(EXPECTED_TO_RETURN_INT_BUT_RETURNED_NULL, Formats.Red, code(res.resultExpressionSourceCode), code(expected))); + final Object expectedValue = expected instanceof final Localizable localizable ? localizable.get(res.locale) : expected; + final Object val = res.executionContext.get().getVariableValue(variableName); + final Fmt expectedFmted = code(Expression.formatLiteralSourceCode(expectedValue)); + final Fmt actualFmted = code(Expression.formatLiteralSourceCode(val)); + final Fmt expressionFmted = code(res.resultExpressionSourceCode); + if (val == null) { + p.println(red(EXPECTED_VARIABLE_TO_EQUAL_BUT_IS_NULL, expressionFmted, code(variableName), expectedFmted)); return false; - } else if (!(res.executionResult instanceof Integer)) { - p.println(format(EXPECTED_TO_RETURN_INT_BUT_RETURNED_OTHER_TYPE, Formats.Red, code(res.resultExpressionSourceCode), code(res.executionResult.getClass().getSimpleName()))); + } else if (val.getClass() != expectedValue.getClass()) { + p.println(red(EXPECTED_VARIABLE_TO_BE_BUT_WAS_OTHER_TYPE, expressionFmted, code(variableName), classSimpleName(expectedValue.getClass()), classSimpleName(val.getClass()))); return false; - } else if (((Integer)res.executionResult).intValue() != expected) { - p.println(format(EXPECTED_TO_RETURN_INT_BUT_RETURNED, Formats.Red, code(res.resultExpressionSourceCode), code(Integer.toString(expected)), code(res.executionResult.toString()))); + } else if (!eq(expectedValue, val)) { + p.println(red(EXPECTED_VARIABLE_TO_EQUAL_BUT_EQUAL, expressionFmted, code(variableName), expectedFmted, actualFmted)); return false; } - p.println(format(OK_RETURNED_INT, Formats.Green, code(res.resultExpressionSourceCode), code(expected))); + p.println(green(OK_VARIABLE_EQUAL, code(variableName), expectedFmted)); return true; }; } - private static final double EPSILON = 0.0000000001; - private static boolean equals(Double actual, double expected) { - var diff = Math.abs(actual.doubleValue() - expected); - return diff < EPSILON; - } - - public static ResultAssertion assertReturnValueEquals(final double expected) { + public static ResultAssertion assertReturnValueEquals(final Object expected) { return (p, res) -> { - if (res.executionResult == null) { - p.println(format(EXPECTED_TO_RETURN_DOUBLE_BUT_RETURNED_NULL, Formats.Red, res.resultExpressionSourceCode, expected)); + final Object actual = res.executionResult; + final Object expectedValue = expected instanceof final Localizable localizable ? localizable.get(res.locale) : expected; + final Fmt expectedFmted = code(Expression.formatLiteralSourceCode(expectedValue)); + final Fmt actualFmted = code(Expression.formatLiteralSourceCode(actual)); + final Fmt expressionFmted = code(res.resultExpressionSourceCode); + if (actual == null) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_NULL, expressionFmted, expectedFmted)); return false; - } else if (!(res.executionResult instanceof Double)) { - p.println(format(EXPECTED_TO_RETURN_DOUBLE_BUT_RETURNED_OTHER_TYPE, Formats.Red, code(res.resultExpressionSourceCode), res.executionResult.getClass().getSimpleName())); + } else if (actual.getClass() != expectedValue.getClass()) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_OTHER_TYPE, expressionFmted, classSimpleName(expectedValue.getClass()), classSimpleName(actual.getClass()))); return false; - } else if (!equals((Double)res.executionResult, expected)) { - p.println(format(EXPECTED_TO_RETURN_DOUBLE_BUT_RETURNED, Formats.Red, res.resultExpressionSourceCode, expected, ((Double)res.executionResult).doubleValue())); + } else if (!eq(expectedValue, actual)) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED, expressionFmted, expectedFmted, actualFmted)); return false; } - p.println(format(OK_RETURNED_DOUBLE, Formats.Green, code(res.resultExpressionSourceCode), code(expected))); + p.println(green(OK_RETURNED, expressionFmted, expectedFmted)); return true; }; } - public static ResultAssertion assertReturnValueEquals(final boolean expected) { + /** + * Since we can't ask students to provide a proper equals() method, it is a bit tricky to assert the content of the objects they create. + * The idea is to make them create a "toString()" method, and then assert the String representation of those objects. + */ + public static ResultAssertion assertReturnValueStringRepresentationEquals(final Localizable expected, final String expectedType) { return (p, res) -> { - if (res.executionResult == null) { - p.println(format(EXPECTED_TO_RETURN_BOOLEAN_BUT_RETURNED_NULL, Formats.Red, res.resultExpressionSourceCode, expected)); + final Fmt expressionFmted = code(res.resultExpressionSourceCode); + final Object actual = res.executionResult; + if (actual == null) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_NULL, expressionFmted, sameStyle(expected))); return false; - } else if (!(res.executionResult instanceof Boolean)) { - p.println(format(EXPECTED_TO_RETURN_BOOLEAN_BUT_RETURNED_OTHER_TYPE, Formats.Red, code(res.resultExpressionSourceCode), res.executionResult.getClass().getSimpleName())); + } else if (!Type.UNBOXED.getOrDefault(actual.getClass(), actual.getClass()).getName().equals(expectedType)) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_OTHER_TYPE, expressionFmted, code(expectedType), classSimpleName(actual.getClass()))); return false; - } else if (((Boolean)res.executionResult).booleanValue() != expected) { - p.println(format(EXPECTED_TO_RETURN_BOOLEAN_BUT_RETURNED, Formats.Red, res.resultExpressionSourceCode, expected, ((Boolean)res.executionResult).booleanValue())); + } else if (!actual.toString().equals(expected.get(res.locale))) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED, expressionFmted, sameStyle(expected), sameStyle(actual.toString()))); return false; } - p.println(format(OK_RETURNED_BOOLEAN, Formats.Green, code(res.resultExpressionSourceCode), code(expected))); + p.println(green(OK_RETURNED, expressionFmted, code(expected.get(res.locale)))); return true; }; } - public static ResultAssertion assertReturnValueEquals(final Localizable expected) { + public static ResultAssertion assertReturnValueWithRandomEquals(final DoubleToIntFunction expected) { return (p, res) -> { - if (res.executionResult == null) { - p.println(format(EXPECTED_TO_RETURN_STRING_BUT_RETURNED_NULL, Formats.Red, res.resultExpressionSourceCode, expected.get(res.locale))); + final var randomNumber = res.randomNumber(); + final Object actual = res.executionResult; + final Fmt expressionFmted = code(res.resultExpressionSourceCode); + if (actual == null) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_NULL, expressionFmted, code(expected.applyAsInt(randomNumber)))); return false; - } else if (!(res.executionResult instanceof String)) { - p.println(format(EXPECTED_TO_RETURN_STRING_BUT_RETURNED_OTHER_TYPE, Formats.Red, code(res.resultExpressionSourceCode), res.executionResult.getClass().getSimpleName())); + } else if (!(actual instanceof Integer)) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_OTHER_TYPE, expressionFmted, code("int"), classSimpleName(actual.getClass()))); return false; - } else if (!((String)res.executionResult).equals(expected.get(res.locale))) { - p.println(format(EXPECTED_TO_RETURN_STRING_BUT_RETURNED, Formats.Red, res.resultExpressionSourceCode, expected.get(res.locale), (String)res.executionResult)); + } else if (((Integer)actual).intValue() != expected.applyAsInt(randomNumber)) { + p.println(red( + EXPECTED_TO_RETURN_INT_FROM_RANDOM_BUT_RETURNED, + expressionFmted, + code(expected.applyAsInt(randomNumber)), + code(randomNumber), + code(actual.toString()) + )); return false; } - p.println(format(OK_RETURNED_STRING, Formats.Green, code(res.resultExpressionSourceCode), code(expected.get(res.locale)))); + p.println(green(OK_RETURNED_INT_FROM_RANDOM, expressionFmted, code(expected.applyAsInt(randomNumber)), code(randomNumber))); return true; }; } - public static ResultAssertion assertReturnValueStringRepresentationEquals(final Localizable expected, final String expectedType) { + /** + * Assert the result of a method using random numbers. + * @param count the number of random numbers expected to be generated + * @param buildExpected given the generated numbers, returns the expected result + */ + public static ResultAssertion assertReturnValueWithRandomEquals(final int count, final ResToIntFunction buildExpected) { return (p, res) -> { - if (res.executionResult == null) { - p.println(format(EXPECTED_TO_RETURN_BUT_RETURNED_NULL, Formats.Red, res.resultExpressionSourceCode, expected.get(res.locale))); + final Object actual = res.executionResult; + final var randomNumbersFmted = sequence(res.randomNumbers(count), Style.Code); + final Fmt expressionFmted = code(res.resultExpressionSourceCode); + final int expected = buildExpected.apply(res); + + if (actual == null) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_NULL, expressionFmted, code(expected))); return false; - } else if (!res.executionResult.getClass().getName().equals(expectedType)) { - p.println(format(EXPECTED_TO_RETURN_BUT_RETURNED_OTHER_TYPE, Formats.Red, res.resultExpressionSourceCode, expectedType, res.executionResult.getClass().getSimpleName())); + } else if (!(actual instanceof Integer)) { + p.println(red(EXPECTED_TO_RETURN_BUT_RETURNED_OTHER_TYPE, expressionFmted, code("int"), classSimpleName(actual.getClass()))); return false; - } else if (!res.executionResult.toString().equals(expected.get(res.locale))) { - p.println(format(EXPECTED_TO_RETURN_BUT_RETURNED, Formats.Red, res.resultExpressionSourceCode, expected.get(res.locale), res.executionResult.toString())); + } else if (((Integer)actual).intValue() != expected) { + p.println(red( + EXPECTED_TO_RETURN_INT_FROM_RANDOMS_BUT_RETURNED, + expressionFmted, + code(expected), + randomNumbersFmted, + code(actual.toString()) + )); return false; } - p.println(format(OK_RETURNED, Formats.Green, code(res.resultExpressionSourceCode), code(expected.get(res.locale)))); + p.println(green(OK_RETURNED_INT_FROM_RANDOMS, expressionFmted, code(expected), randomNumbersFmted)); return true; }; } - public static ResultAssertion assertReturnValueWithRandomEquals(final DoubleToIntFunction expected) { + public static ResultAssertion assertReturnValueIsAnonymousObject() { return (p, res) -> { - final var randomNumber = res.randomNumber(); - if (res.executionResult == null) { - p.println(format(EXPECTED_TO_RETURN_INT_BUT_RETURNED_NULL, Formats.Red, res.resultExpressionSourceCode, expected.applyAsInt(randomNumber))); - return false; - } else if (!(res.executionResult instanceof Integer)) { - p.println(format(EXPECTED_TO_RETURN_INT_BUT_RETURNED_OTHER_TYPE, Formats.Red, res.resultExpressionSourceCode, res.executionResult.getClass().getSimpleName())); - return false; - } else if (((Integer)res.executionResult).intValue() != expected.applyAsInt(randomNumber)) { - p.println( - format( - EXPECTED_TO_RETURN_INT_FROM_RANDOM_BUT_RETURNED, - Formats.Red, - code(res.resultExpressionSourceCode), - code(expected.applyAsInt(randomNumber)), - randomNumber, - code(res.executionResult.toString()) - ) - ); + final Object actual = res.executionResult; + if (actual == null) { + p.println(red(EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED_NULL, code(res.resultExpressionSourceCode))); + return false; + } else if (actual.getClass().getSimpleName().contains("$$Lambda$")) { // Kind of hacky, but only way as far as I know + p.println(red(EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED_LAMBDA, code(res.resultExpressionSourceCode))); + return false; + } else if (!actual.getClass().isAnonymousClass()) { + p.println(red(EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED, code(res.resultExpressionSourceCode), classFullName(actual.getClass()))); return false; } - p.println(format(OK_RETURNED_INT_FROM_RANDOM, Formats.Green, code(res.resultExpressionSourceCode), code(expected.applyAsInt(randomNumber)), randomNumber)); + p.println(green(OK_RETURNED_OBJECT_IS_ANONYMOUS, code(res.resultExpressionSourceCode))); return true; - }; + }; } - /** - * Assert the result of a method using random numbers. - * @param count the number of random numbers expected to be generated - * @param buildExpected given the generated numbers, returns the expected result - */ - public static ResultAssertion assertReturnValueWithRandomEquals(final int count, final ResToIntFunction buildExpected) { - return assertReturnValueWithRandomEquals(res -> res.randomNumbers(count), buildExpected); - } + public static ResultAssertion assertReturnValueIsLambda() { + return (p, res) -> { + final Object actual = res.executionResult; + if (actual == null) { + p.println(red(EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED_NULL, code(res.resultExpressionSourceCode))); + return false; + } else if (actual.getClass().isAnonymousClass()) { + p.println(red(EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED_ANONYMOUS, code(res.resultExpressionSourceCode))); + return false; + } else if (!actual.getClass().getSimpleName().contains("$$Lambda")) { // Kind of hacky, but only way as far as I know + p.println(red(EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED, code(res.resultExpressionSourceCode), classFullName(actual.getClass()))); + return false; + } - /** - * Assert the result of a method using random numbers. - * @param fromOffset the start of the random numbers sequence to pick - * @param count the number of random numbers to pick - * @param buildExpected given the generated numbers, returns the expected result - */ - public static ResultAssertion assertReturnValueWithRandomEquals(final int fromOffset, final int count, final ResToIntFunction buildExpected) { - return assertReturnValueWithRandomEquals(res -> res.randomNumbers(fromOffset, count), buildExpected); + p.println(green(OK_RETURNED_OBJECT_IS_LAMBDA, code(res.resultExpressionSourceCode))); + return true; + }; } - private static ResultAssertion assertReturnValueWithRandomEquals(final Function randomNumbersFunc, final ResToIntFunction buildExpected) { + public static ResultAssertion assertReturnValueImplements(Class valueInterface) { return (p, res) -> { - final var randomNumbers = randomNumbersFunc.apply(res); - final var formatRandomNumbers = Helpers.formatSequence(res.locale, randomNumbers); - - final int expected = buildExpected.apply(res); - if (res.executionResult == null) { - p.println(format(EXPECTED_TO_RETURN_INT_BUT_RETURNED_NULL, Formats.Red, res.resultExpressionSourceCode, expected)); - return false; - } else if (!(res.executionResult instanceof Integer)) { - p.println(format(EXPECTED_TO_RETURN_INT_BUT_RETURNED_OTHER_TYPE, Formats.Red, res.resultExpressionSourceCode, res.executionResult.getClass().getSimpleName())); - return false; - } else if (((Integer)res.executionResult).intValue() != expected) { - p.println( - format( - EXPECTED_TO_RETURN_INT_FROM_RANDOMS_BUT_RETURNED, - Formats.Red, - code(res.resultExpressionSourceCode), - code(expected), - formatRandomNumbers, - code(res.executionResult.toString()) - ) - ); + final Object actual = res.executionResult; + final var interfaceName = code(valueInterface.getName()); + if (actual == null) { + p.println(red(EXPECTED_TO_RETURN_IMPLEMENTING_BUT_RETURNED_NULL, code(res.resultExpressionSourceCode), interfaceName)); + return false; + } else if (!valueInterface.isInstance(actual)) { + p.println(red(EXPECTED_TO_RETURN_IMPLEMENTING_BUT_NOT, code(res.resultExpressionSourceCode), interfaceName, classFullName(actual.getClass()))); return false; } - p.println(format(OK_RETURNED_INT_FROM_RANDOMS, Formats.Green, res.resultExpressionSourceCode, expected, formatRandomNumbers)); + p.println(green(OK_RETURNED_OBJECT_IMPLEMENTS, code(res.resultExpressionSourceCode), interfaceName)); return true; - }; + }; } public static BeforeTestAssertion assertConstructorIsInvokable(final String className, final Type... constructorParamTypes) { - return (p, locale, _koan) -> { + return (p, locale, koan) -> { final var type = new Type(className); - try { - final var clasz = type.resolve(); - if (!Type.isInstantiable(clasz)) { - p.println(ConsoleFmt.red(EXPECTED_CLASS_TO_BE_INSTANTIABLE), className); - return false; - } + if (!assertCanInstantiateClass(global(type)).validate(p, locale, koan)) { + return false; + } + try { + final var clasz = type.unsafeResolve(); final var constructor = clasz.getConstructor(Type.unsafeResolveTypes(constructorParamTypes)); if (!Modifier.isPublic(constructor.getModifiers())) { - p.println( - ConsoleFmt.red(EXPECTED_CONSTRUCTOR_TO_BE_PUBLIC), - type.simpleClassName - ); + p.println(red(EXPECTED_CONSTRUCTOR_TO_BE_PUBLIC, classSimpleName(type))); } } catch(NoSuchMethodException nsme) { if (constructorParamTypes.length == 0) { - p.println( - ConsoleFmt.red(EXPECTED_TO_FIND_CONSTRUCTOR_NO_PARAMS), - type.simpleClassName - ); + p.println(red(EXPECTED_TO_FIND_CONSTRUCTOR_NO_PARAMS, classSimpleName(type))); } else if (constructorParamTypes.length == 1) { - p.println( - ConsoleFmt.red(EXPECTED_TO_FIND_CONSTRUCTOR_ONE_PARAM), - type.simpleClassName, - constructorParamTypes[0] - ); + p.println(red(EXPECTED_TO_FIND_CONSTRUCTOR_ONE_PARAM, classSimpleName(type), classSimpleName(constructorParamTypes[0]))); } else { final var expectedParams = Arrays .stream(constructorParamTypes) - .map(t -> "'" + t + "'") - .toArray(String[]::new); - p.println( - ConsoleFmt.red(EXPECTED_TO_FIND_CONSTRUCTOR_MANY_PARAMS), - type.simpleClassName, - Helpers.formatSequence(locale, expectedParams) - ); + .map(t -> global(t.simpleClassName)) + .toList(); + p.println(red(EXPECTED_TO_FIND_CONSTRUCTOR_MANY_PARAMS, classSimpleName(type), sequence(expectedParams, Style.Code))); } return false; - } catch (ClassNotFoundException cnfe) { - p.println(ConsoleFmt.red(EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE), type.simpleClassName,type.packageName); - return false; } return true; }; } - + + public static BeforeTestAssertion assertCanInstantiateClass(final Localizable type) { + return (p, locale, _koan) -> { + final var actualType = type.get(locale); + try { + final var clasz = actualType.resolve(); + if (!Type.isInstantiable(clasz)) { + p.println(red(EXPECTED_CLASS_TO_BE_INSTANTIABLE, classSimpleName(actualType))); + return false; + } + return true; + } catch (final ClassNotFoundException _cnfe) { + p.println(red(EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE, classSimpleName(actualType), code(actualType.packageName))); + return false; + } + }; + } + /** * This asserts that the method in the current Koan class is invokable with the given param types. * @@ -344,52 +378,46 @@ public static BeforeTestAssertion assertObjectMethodIsInvokable(final String cla } private static BeforeTestAssertion assertMethodIsInvokable(final Localizable type, final String methodName, final boolean isStatic, final Type... paramTypes) { - return (p, locale, _koan) -> { + return (p, locale, koan) -> { + + if (!assertCanInstantiateClass(type).validate(p, locale, koan)) { + return false; + } + final var clasz = type.get(locale).unsafeResolve(); final var methodParamClasses = Type.unsafeResolveTypes(paramTypes); + final Fmt classLocationFmted = sameStyle(clasz.getName().replace(".", "/")); try { final var method = clasz.getMethod(methodName, methodParamClasses); if (isStatic && !Modifier.isStatic(method.getModifiers())) { - p.println(ConsoleFmt.red(EXPECTED_METHOD_TO_NOT_BE_STATIC), methodName, clasz.getName().replace(".", "/")); + p.println(red(EXPECTED_METHOD_TO_BE_STATIC, sameStyle(methodName), classLocationFmted)); return false; } if (!isStatic && Modifier.isStatic(method.getModifiers())) { - p.println(ConsoleFmt.red(EXPECTED_METHOD_TO_BE_STATIC), methodName, clasz.getName().replace(".", "/")); + p.println(red(EXPECTED_METHOD_TO_NOT_BE_STATIC, sameStyle(methodName), classLocationFmted)); return false; } if (!Modifier.isPublic(method.getModifiers())) { - p.println( - ConsoleFmt.red(EXPECTED_METHOD_TO_BE_PUBLIC), - methodName - ); + p.println(red(EXPECTED_METHOD_TO_BE_PUBLIC, sameStyle(methodName))); } } - catch(NoSuchMethodException nsme) { + catch(final NoSuchMethodException _nsme) { if (methodParamClasses.length == 0) { - p.println( - ConsoleFmt.red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS), - methodName, - clasz.getName().replace(".", "/") - ); + p.println(red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle(methodName), classLocationFmted)); } else if (methodParamClasses.length == 1) { - p.println( - ConsoleFmt.red(EXPECTED_TO_FIND_MEHOD_ONE_PARAM), - methodName, - clasz.getName().replace(".", "/"), - methodParamClasses[0].getSimpleName() - ); + p.println(red(EXPECTED_TO_FIND_MEHOD_ONE_PARAM, sameStyle(methodName), classLocationFmted, classSimpleName(methodParamClasses[0]))); } else { final var expectedParams = Arrays .stream(methodParamClasses) - .map(t -> "'" + t.getSimpleName() + "'") - .toArray(String[]::new); + .map(t -> global(t.getSimpleName())) + .toList(); p.println( - ConsoleFmt.red(EXPECTED_TO_FIND_MEHOD_MANY_PARAMS), - methodName, - clasz.getName().replace(".", "/"), - Helpers.formatSequence(locale, expectedParams) - ); + red(EXPECTED_TO_FIND_MEHOD_MANY_PARAMS, + sameStyle(methodName), + classLocationFmted, + sequence(expectedParams, Style.Code) + )); } return false; } @@ -407,35 +435,49 @@ public static BeforeTestAssertion assertPrivateField(final String className, fin return assertField(className, fieldName, false, fieldType); } - public static BeforeTestAssertion assertField(final String className, final String fieldName, final Boolean isFinal, final Type fieldType) { + private static BeforeTestAssertion assertField(final String className, final String fieldName, final Boolean isFinal, final Type expectedFieldType) { return (p, locale, koan) -> { final var type = new Type(className); try { final var clasz = type.unsafeResolve(); - final var field = clasz.getDeclaredField(fieldName); - if (!Modifier.isPrivate(field.getModifiers())) { - p.println(ConsoleFmt.red(EXPECTED_FIELD_TO_BE_PRIVATE), fieldName, className); + final var actualField = clasz.getDeclaredField(fieldName); + if (!Modifier.isPrivate(actualField.getModifiers())) { + p.println(red(EXPECTED_FIELD_TO_BE_PRIVATE, code(fieldName), code(className))); return false; } - if (isFinal && !Modifier.isFinal(field.getModifiers())) { - p.println(ConsoleFmt.red(EXPECTED_FIELD_TO_BE_FINAL), fieldName, className); + if (isFinal && !Modifier.isFinal(actualField.getModifiers())) { + p.println(red(EXPECTED_FIELD_TO_BE_FINAL, code(fieldName), code(className))); return false; - } else if (!isFinal && Modifier.isFinal(field.getModifiers())) { - p.println(ConsoleFmt.red(EXPECTED_FIELD_TO_NOT_BE_FINAL), fieldName, className); + } else if (!isFinal && Modifier.isFinal(actualField.getModifiers())) { + p.println(red(EXPECTED_FIELD_TO_NOT_BE_FINAL, code(fieldName), code(className))); return false; } - if (!field.getType().equals(fieldType.unsafeResolve())) { - p.println(ConsoleFmt.red(EXPECTED_FIELD_TO_BE_OF_TYPE), fieldName, className, fieldType, field.getType().getSimpleName()); + if (!actualField.getType().equals(expectedFieldType.unsafeResolve())) { + p.println(red(EXPECTED_FIELD_TO_BE_OF_TYPE, code(fieldName), code(className), classSimpleName(expectedFieldType), classSimpleName(actualField.getType()))); return false; } } catch(NoSuchFieldException nsfe) { - p.println(ConsoleFmt.red(EXPECTED_TO_FIND_FIELD_IN_CLASS), fieldName, className); + p.println(red(EXPECTED_TO_FIND_FIELD_IN_CLASS, code(fieldName), code(className))); return false; } return true; }; } + + public static BeforeTestAssertion assertImplementsInterface(final String className, final Class interfaceClass) { + return (p, locale, koan) -> { + final var type = new Type(className); + final var clasz = type.unsafeResolve(); + final var success = interfaceClass.isAssignableFrom(clasz); + + if (!success) { + p.println(red(EXPECTED_CLASS_TO_IMPLEMENT, code(className), code(interfaceClass.getName()))); + } + + return success; + }; + } } diff --git a/src/main/java/engine/BeforeTestAssertion.java b/src/main/java/engine/BeforeTestAssertion.java index 1794fa2..59151dd 100644 --- a/src/main/java/engine/BeforeTestAssertion.java +++ b/src/main/java/engine/BeforeTestAssertion.java @@ -3,6 +3,7 @@ /** * Asserts something about student's code before koan tests are run. */ +@FunctionalInterface public interface BeforeTestAssertion { /** * Validates that the structure of student code for a given Koan is correct, and display feedback using the given Printer. diff --git a/src/main/java/engine/ConsoleFmt.java b/src/main/java/engine/ConsoleFmt.java deleted file mode 100644 index 3035892..0000000 --- a/src/main/java/engine/ConsoleFmt.java +++ /dev/null @@ -1,104 +0,0 @@ -package engine; - -import java.util.Arrays; - -/** - * Console text coloring. - */ -public final class ConsoleFmt { - private static final String NO_FORMAT = "\033[0m"; - - private static final String RED = "\033[0;31m"; - private static final String GREEN = "\033[0;32m"; - private static final String CYAN = "\033[0;36m"; - private static final String DODGERBLUE2="\033[38;5;27m"; - - private static final String BOLD= "\033[1m"; - private static final String RED_BOLD= RED + BOLD; - - public static enum Formats { - None(NO_FORMAT), - Red(RED), - Green(GREEN), - Cyan(CYAN), - Code(DODGERBLUE2), - Strong(BOLD), - StrongRed(RED_BOLD); - - public final String tags; - - private Formats(String tags) { - this.tags = tags; - } - } - - public record Formatted(T text, Formats fmt) {} - - public static Formatted fmt(T text, Formats fmt) { - return new Formatted(text, fmt); - } - - public static Formatted code(T text) { - return new Formatted(text, Formats.Code); - } - - public static Formatted strong(T text) { - return new Formatted(text, Formats.Strong); - } - - public static Formatted strongRed(T text) { - return new Formatted(text, Formats.StrongRed); - } - - public static Localizable red(final Localizable text) { - return text.map(ConsoleFmt::red); - } - - public static Localizable green(final Localizable text) { - return text.map(ConsoleFmt::green); - } - - public static Localizable cyan(final Localizable text) { - return text.map(ConsoleFmt::cyan); - } - - public static Localizable strongRed(final Localizable text) { - return text.map(ConsoleFmt::strongRed); - } - - public static Localizable strong(final Localizable text) { - return text.map(ConsoleFmt::strong); - } - - public static String red(final String text) { - return String.format("%s%s%s", RED, text, NO_FORMAT); - } - - public static String green(final String text) { - return String.format("%s%s%s", GREEN, text, NO_FORMAT); - } - - public static String cyan(final String text) { - return String.format("%s%s%s", CYAN, text, NO_FORMAT); - } - - public static String strong(final String text) { - return String.format("%s%s%s", BOLD, text, NO_FORMAT); - } - - public static String strongRed(final String text) { - return String.format("%s%s%s", RED_BOLD, text, NO_FORMAT); - } - - public static String format(final String template, Formats fmt, final Object... params) { - final var formattedParams = Arrays - .stream(params) - .map(param -> param instanceof Formatted fmted ? String.format("%s%s%s", fmted.fmt.tags, fmted.text, fmt.tags) : param) - .toArray(Object[]::new); - return String.format("%s%s%s", fmt.tags, String.format(template, formattedParams) , NO_FORMAT); - } - - public static Localizable format(final Localizable template, final Formats fmt, final Object... params) { - return template.map(txt -> format(txt, fmt, params)); - } -} diff --git a/src/main/java/engine/Fmt.java b/src/main/java/engine/Fmt.java new file mode 100644 index 0000000..66d4b8b --- /dev/null +++ b/src/main/java/engine/Fmt.java @@ -0,0 +1,134 @@ +package engine; + +import java.util.Arrays; +import java.util.List; + +import engine.script.Type; +import static engine.Localizable.global; +import static engine.Texts.AND; + +/** + * A piece of localizable and stylized text in the console. Allow to create colored output in the console. + */ +public final record Fmt(Localizable templ, Style style, Fmt... children) { + public String format(final Locale locale) { + return format(locale, Style.None); + } + + private String format(final Locale locale, final Style parentStyle) { + final var actualStyle = style == Style.Inherit ? parentStyle : style; + final var formattedChildren = Arrays + .stream(children) + .map(child -> child.format(locale, actualStyle)) + .toArray(Object[]::new); + return String.format("%s%s%s", actualStyle.tags, String.format(templ.get(locale), formattedChildren) , parentStyle.tags); + } + + public static Fmt notStyled(final Localizable templ, final Fmt... children) { + return new Fmt(templ, Style.None, children); + } + + public static Fmt notStyled(final String value) { + return new Fmt(global(value), Style.None); + } + + public static Fmt notStyled(final int value) { + return new Fmt(global(Integer.toString(value)), Style.None); + } + + public static Fmt red(final Localizable templ, final Fmt... children) { + return new Fmt(templ, Style.Red, children); + } + + public static Fmt red(final String text) { + return new Fmt(global(text), Style.Red); + } + + public static Fmt green(final Localizable templ, final Fmt... children) { + return new Fmt(templ, Style.Green, children); + } + + public static Fmt green(final String text) { + return new Fmt(global(text), Style.Green); + } + + public static Fmt cyan(final Localizable templ, final Fmt... children) { + return new Fmt(templ, Style.Cyan, children); + } + + public static Fmt cyan(final String text) { + return new Fmt(global(text), Style.Cyan); + } + + public static Fmt strong(final Localizable templ, final Fmt... children) { + return new Fmt(templ, Style.Strong, children); + } + + public static Fmt strongRed(final Localizable templ, final Fmt... children) { + return new Fmt(templ, Style.StrongRed, children); + } + + public static Fmt sameStyle(final Localizable templ, final Fmt... children) { + return new Fmt(templ, Style.Inherit, children); + } + + public static Fmt sameStyle(final String value) { + return new Fmt(global(value), Style.Inherit); + } + + public static Fmt code(final T expressionTextOrValue) { + return new Fmt(global(expressionTextOrValue.toString()), Style.Code); + } + + public static Fmt classSimpleName(final Type type) { + return code(type.simpleClassName); + } + + public static Fmt classSimpleName(final Class clasz) { + final var unboxed = Type.UNBOXED.getOrDefault(clasz, clasz); + return code(unboxed.getSimpleName()); + } + + public static Fmt classFullName(final Class clasz) { + final var unboxed = Type.UNBOXED.getOrDefault(clasz, clasz); + return code(unboxed.getName()); + } + + public static Fmt sequence(final double[] items, final Style itemStyle) { + return sequence( + Arrays.stream(items).mapToObj(item -> global(Double.toString(item))).toList(), + itemStyle + ); + } + + public static Fmt sequence(final String[] items, final Style itemStyle) { + return sequence( + Arrays.stream(items).map(item -> global(item)).toList(), + itemStyle + ); + } + + public static Fmt sequence(final List> items, final Style itemStyle) { + if (items.isEmpty()) { + return new Fmt(global(""), Style.Inherit); + } + + final var children = items + .stream() + .map(item -> new Fmt(item, itemStyle)) + .toArray(Fmt[]::new); + + final var templ = AND.map(andText -> { + final var result = new StringBuilder("%s"); + if (items.size() > 1) { + for(int i=1; i < items.size() - 1; i++) { + result.append(", %s"); + } + result.append(andText); + } + return result.toString(); + }); + + return new Fmt(templ, Style.Inherit, children); + } +} diff --git a/src/main/java/engine/FormatParam.java b/src/main/java/engine/FormatParam.java index 525019d..83e4df4 100644 --- a/src/main/java/engine/FormatParam.java +++ b/src/main/java/engine/FormatParam.java @@ -3,6 +3,7 @@ /** * A functional interface offering the possibility to format a piece of feedback to the user given the koan's result. */ +@FunctionalInterface public interface FormatParam { String format(final KoanResult res); @@ -18,7 +19,7 @@ static FormatParam addToStdInInput(final int inputIndex, final int increment) { try { final int value = Integer.parseInt(line); return String.valueOf(value + increment); - } catch(NumberFormatException nfe) { + } catch(final NumberFormatException _nfe) { // Ignore } return ""; diff --git a/src/main/java/engine/Helpers.java b/src/main/java/engine/Helpers.java index 95f3835..7a716cb 100644 --- a/src/main/java/engine/Helpers.java +++ b/src/main/java/engine/Helpers.java @@ -1,8 +1,5 @@ package engine; -import static engine.Texts.AND; - -import java.util.Arrays; import java.util.Random; import java.util.Scanner; @@ -31,29 +28,4 @@ static void setupRandomForKoan(final long seed) { public static double random() { return rng.nextDouble(); } - - static String formatSequence(final Locale locale, final double[] toFormat) { - return formatSequence( - locale, - Arrays.stream(toFormat).mapToObj(Double::toString).toArray(String[]::new) - ); - } - - static String formatSequence(final Locale locale, final Object[] toFormat) { - if (toFormat == null || toFormat.length == 0) { - return ""; - } - final var result = new StringBuilder(); - result.append(toFormat[0]); - - if (toFormat.length > 1) { - for(int i=1; i < toFormat.length - 1; i++) { - result.append(", "); - result.append(toFormat[i]); - } - result.append(String.format(AND.get(locale), toFormat[toFormat.length - 1])); - } - - return result.toString(); - } } diff --git a/src/main/java/engine/KoanResult.java b/src/main/java/engine/KoanResult.java index 9a2af8d..1778b88 100644 --- a/src/main/java/engine/KoanResult.java +++ b/src/main/java/engine/KoanResult.java @@ -2,6 +2,8 @@ import java.util.Optional; +import engine.script.ExecutionContext; + /** * Stores all the information about what happened during the execution of a koan. */ @@ -11,6 +13,7 @@ public final class KoanResult { private final String[] stdInLines; private int stdInIndex = 0; public final Object executionResult; + public final Optional executionContext; /** * The source code of the last script expression, the one we are actually asserting the result and/or the console output. */ @@ -18,12 +21,13 @@ public final class KoanResult { public final KoanTest test; public final Locale locale; - public KoanResult(final Locale locale, final KoanTest test, final String[] stdOutLines, final String[] stdInLines, final Object executionResult) { + public KoanResult(final Locale locale, final KoanTest test, final String[] stdOutLines, final String[] stdInLines, final Object executionResult, final Optional executionContext) { this.locale = locale; this.test = test; this.stdOutLines = stdOutLines; this.stdInLines = stdInLines; this.executionResult = executionResult; + this.executionContext = executionContext; resultExpressionSourceCode = test.script[test.script.length - 1].formatSourceCode(locale); } @@ -100,7 +104,7 @@ public boolean executeAssertions(Printer p) { } public boolean executeAssertions(final Printer p, final ResultAssertion... assertions) { - for (ResultAssertion as : assertions) { + for (final ResultAssertion as : assertions) { if (!as.validate(p, this)) { return false; } diff --git a/src/main/java/engine/Printer.java b/src/main/java/engine/Printer.java index 9aee459..7c362c8 100644 --- a/src/main/java/engine/Printer.java +++ b/src/main/java/engine/Printer.java @@ -1,15 +1,9 @@ package engine; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import engine.test.Line; - /** * A Printer allows to print feedback to the student. */ -public sealed interface Printer { +public interface Printer { /** * A Printer which is silent, hence displaying nothing. * Useful when executing koans outside the view of the student. @@ -20,6 +14,10 @@ public sealed interface Printer { * Prints a single empty line. */ void println(); + /** + * Prints a styled text. + */ + void println(final Fmt fmt); /** * Prints the given line out of the given String template and its parameters. */ @@ -49,61 +47,27 @@ public void println() { } @Override - public void println(final String template, final Object... params) { + public void println(final Fmt fmt) { synchronized(System.out) { - System.out.println(String.format(template, params)); + System.out.println(fmt.format(locale)); System.out.flush(); } } @Override - public void println(final Localizable template, final Object... params) { + public void println(final String template, final Object... params) { synchronized(System.out) { - System.out.println(String.format(template.get(locale), params)); + System.out.println(String.format(template, params)); System.out.flush(); } - } -} - -/** - * A printer capturing feedback displayed to the student in unit tests. - */ -final class CapturingPrinter implements Printer { - private final Locale locale; - private final List output = new ArrayList<>(); - - public CapturingPrinter(final Locale locale) { - this.locale = locale; - } - - public boolean hasCaptured(final Line... lines) { - return output.equals( - Arrays - .stream(lines) - .map(line -> line.resolve(locale)) - .toList() - ); - } - - public void displayToConsole() { - for(var line: output) { - System.out.println(line); - } - } - - @Override - public void println() { - output.add(""); - } - - @Override - public void println(final String template, final Object... params) { - output.add(String.format(template, params)); } @Override public void println(final Localizable template, final Object... params) { - println(template.get(locale), params); + synchronized(System.out) { + System.out.println(String.format(template.get(locale), params)); + System.out.flush(); + } } } @@ -122,6 +86,11 @@ public void println() { // Silent } + @Override + public void println(final Fmt _fmt) { + // Silent + } + @Override public void println(final String _template, final Object... _params) { // Silent diff --git a/src/main/java/engine/ResToIntFunction.java b/src/main/java/engine/ResToIntFunction.java index 1bf915f..42e7a66 100644 --- a/src/main/java/engine/ResToIntFunction.java +++ b/src/main/java/engine/ResToIntFunction.java @@ -1,5 +1,6 @@ package engine; +@FunctionalInterface public interface ResToIntFunction { - int apply(KoanResult res); + int apply(final KoanResult res); } diff --git a/src/main/java/engine/ResultAssertion.java b/src/main/java/engine/ResultAssertion.java index f89f7c9..81944ca 100644 --- a/src/main/java/engine/ResultAssertion.java +++ b/src/main/java/engine/ResultAssertion.java @@ -3,6 +3,7 @@ /** * An assertion is the main way to assert that the student succeeded a koan. */ +@FunctionalInterface public interface ResultAssertion { /** * Validates that the given result is correct, and display feedback using the given Printer. @@ -10,5 +11,5 @@ public interface ResultAssertion { * @param result The details of the Koan execution. * @return Whether or not the assertion is successful or not. */ - boolean validate(Printer p, KoanResult result); + boolean validate(final Printer p, final KoanResult result); } diff --git a/src/main/java/engine/Sensei.java b/src/main/java/engine/Sensei.java index 0b2ec3f..2fac819 100644 --- a/src/main/java/engine/Sensei.java +++ b/src/main/java/engine/Sensei.java @@ -2,17 +2,23 @@ import java.lang.reflect.InvocationTargetException; import java.util.List; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; -import engine.ConsoleFmt.Formats; import engine.script.Expression; import engine.script.ScriptExecutionException; import engine.script.ScriptRunner; -import static engine.ConsoleFmt.code; -import static engine.ConsoleFmt.format; -import static engine.ConsoleFmt.strong; -import static engine.ConsoleFmt.strongRed; +import static engine.Fmt.classSimpleName; +import static engine.Fmt.code; +import static engine.Fmt.cyan; +import static engine.Fmt.green; +import static engine.Fmt.notStyled; +import static engine.Fmt.red; +import static engine.Fmt.sameStyle; +import static engine.Fmt.sequence; +import static engine.Fmt.strong; +import static engine.Fmt.strongRed; import static engine.Texts.*; /** @@ -47,7 +53,7 @@ public void offerKoans() { } private boolean tryOfferKoan(Koan koan, int successfulCount) { - for(var test: koan.tests) { + for(final var test: koan.tests) { if (!tryOfferTest(test, successfulCount)) { return false; } @@ -80,20 +86,20 @@ private boolean offerTest(KoanTest test, int successfulCount) { final var thread = new Thread(() -> { try { success.set(executeCall(test)); - } catch (ScriptExecutionException see) { + } catch (final ScriptExecutionException see) { // Special case: since the executeCall() method did not complete, the console conclusion was not displayed. concludeConsole(koan); - if (see.getCause() instanceof InvocationTargetException ite && ite.getTargetException() instanceof StackOverflowError) { - p.println(ConsoleFmt.red(THE_METHOD_SEEMS_TO_RECURSE_INFINITELY), see.methodName); - } else if (see.getCause() instanceof InvocationTargetException ite) { - p.println(ConsoleFmt.red(THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR), see.methodName, ite.getCause().getMessage()); + if (see.getCause() instanceof final InvocationTargetException ite && ite.getTargetException() instanceof StackOverflowError) { + p.println(red(THE_METHOD_SEEMS_TO_RECURSE_INFINITELY, code(see.methodName))); + } else if (see.getCause() instanceof final InvocationTargetException ite) { + p.println(red(THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR, code(see.methodName), sameStyle(ite.getCause().getMessage()))); } else { throw see; // Serious bug } - } catch (KoanBugException kbe) { + } catch (final KoanBugException kbe) { // Special case: since the executeCall() method did not complete, the console conclusion was not displayed. concludeConsole(koan); - p.println(ConsoleFmt.red(BUG_FOUND), kbe.getMessage()); + p.println(red(BUG_FOUND, sameStyle(kbe.getMessage()))); } }); @@ -107,14 +113,14 @@ private boolean offerTest(KoanTest test, int successfulCount) { thread.join(TIMEOUT_INFINITE_LOOPS_MS); } } - catch(InterruptedException ie) { + catch(final InterruptedException ie) { throw new IllegalStateException(String.format("Something very weird happened. We should not have been interrupted: %s.", ie.getMessage())); } if (thread.isAlive()) { StdStreamsInterceptor.reset(); concludeConsole(koan); - p.println(ConsoleFmt.red(THE_CODE_TRIED_BY_THE_SENSEI_SEEMS_TO_NOT_FINISH), Expression.formatSourceCode(test.script, locale)); + p.println(red(THE_CODE_TRIED_BY_THE_SENSEI_SEEMS_TO_NOT_FINISH, code(Expression.formatSourceCode(test.script, locale)))); } offerToMeditate(koan); @@ -127,7 +133,7 @@ private boolean executeCall(final KoanTest test) { test.setupRandomForKoan(); - for(var assertion: test.koan.beforeAssertions) { + for(final var assertion: test.koan.beforeAssertions) { if (!assertion.validate(p, locale, test.koan)) { return false; } @@ -146,7 +152,8 @@ private boolean executeCall(final KoanTest test) { test, interceptionResult.stdOutLines, interceptionResult.stdInLines, - interceptionResult.returnValue + interceptionResult.returnValue.executionResult(), + Optional.of(interceptionResult.returnValue.context()) ); concludeConsole(test.koan); @@ -160,18 +167,18 @@ private void introduceConsole(final KoanTest test) { if (test.script.length > 1 || test.koan.showStdInInputs) { p.println(THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN); // Seulement si code de prep ou stdin input if (test.koan.showStdInInputs) { - p.println(WHEN_ANSWERING, Helpers.formatSequence(locale, test.stdInInputs(locale))); + p.println(notStyled(WHEN_ANSWERING, sequence(test.stdInInputs(locale), Style.Inherit))); } if (test.script.length > 1) { p.println(WHEN_EXECUTING); p.println(); - p.println(format(Expression.formatPreparationSourceCode(test.script, locale, " "), Formats.Code)); - p.println(format(AND_FINALLY_LOOKING_THE_RESULT_OF, Formats.None, testedExpression)); + p.println(code(Expression.formatPreparationSourceCode(test.script, locale, " "))); + p.println(notStyled(AND_FINALLY_LOOKING_THE_RESULT_OF, testedExpression)); } else { - p.println(format(WHEN_LOOKING_THE_RESULT_OF, Formats.None, testedExpression)); + p.println(notStyled(WHEN_LOOKING_THE_RESULT_OF, testedExpression)); } } else { - p.println(format(THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN_LOOKING_AT, Formats.None, testedExpression)); + p.println(notStyled(THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN_LOOKING_AT, testedExpression)); } p.println(); @@ -194,10 +201,10 @@ private void concludeConsole(final Koan koan) { private void encourage(final Koan koan) { p.println(); p.println(THINKING, koan.koanClass.get(locale).simpleClassName); - p.println(format(HAS_DAMAGED_YOUR_KARMA, Formats.Red, strongRed(koan.koanName.get(locale)))); + p.println(red(HAS_DAMAGED_YOUR_KARMA, strongRed(koan.koanName))); p.println(); p.println(THE_MASTER_SAYS); - p.println(ConsoleFmt.cyan(YOU_HAVE_NOT_REACHED_ENLIGHTMENT)); + p.println(cyan(YOU_HAVE_NOT_REACHED_ENLIGHTMENT)); p.println(); p.println("---------"); p.println(); @@ -207,14 +214,11 @@ private void encourage(final Koan koan) { private void offerToMeditate(final Koan koan) { p.println(); - p.println( - format( - PLEASE_MEDITATE_ON, - Formats.None, - strong(koan.koanName.get(locale)), - koan.koanClass.get(locale).simpleClassName - ) - ); + p.println(notStyled( + PLEASE_MEDITATE_ON, + strong(koan.koanName), + sameStyle(koan.koanClass.get(locale).simpleClassName) + )); p.println(); } @@ -224,12 +228,13 @@ private void showProgress(final int successfulCount) { return; } - final var bar = String.format( - "%s%s%s", - ConsoleFmt.green(repeat(".", successfulCount)), - ConsoleFmt.red("X"), - ConsoleFmt.cyan(repeat("_", allKoans.size() - successfulCount - 1))); - p.println(YOUR_PROGRESS_THUS_FAR, bar, successfulCount, allKoans.size()); + p.println(green( + YOUR_PROGRESS_THUS_FAR, + green(repeat(".", successfulCount)), + red("X"), + cyan(repeat("_", allKoans.size() - successfulCount - 1)), + notStyled(String.format("%s/%s", successfulCount, allKoans.size())) + )); p.println(); } diff --git a/src/main/java/engine/StdStreamsInterceptor.java b/src/main/java/engine/StdStreamsInterceptor.java index 25b5838..61150b2 100644 --- a/src/main/java/engine/StdStreamsInterceptor.java +++ b/src/main/java/engine/StdStreamsInterceptor.java @@ -23,7 +23,7 @@ public static class InterceptionResult { public final String[] stdInLines; public final T returnValue; - public InterceptionResult(String[] stdOutLines, String[] stdInLines, T returnValue) { + public InterceptionResult(final String[] stdOutLines, final String[] stdInLines, final T returnValue) { this.stdOutLines = stdOutLines; this.stdInLines = stdInLines; this.returnValue = returnValue; @@ -34,13 +34,13 @@ private static class OutputStreamMultiplexer extends OutputStream { private final OutputStream stream1; private final OutputStream stream2; - public OutputStreamMultiplexer(OutputStream stream1, OutputStream stream2) { + public OutputStreamMultiplexer(final OutputStream stream1, final OutputStream stream2) { this.stream1 = stream1; this.stream2 = stream2; } @Override - public void write(int b) throws IOException { + public void write(final int b) throws IOException { stream1.write(b); stream2.write(b); } @@ -52,7 +52,7 @@ private static class StdInInterceptor extends InputStream { @Override public int read() throws IOException { - int b = realIn.read(); + final int b = realIn.read(); bos.write(b); return b; } @@ -63,8 +63,8 @@ public int available() throws IOException { } @Override - public int read(byte[] b, int off, int len) throws IOException { - var n = realIn.read(b, off, len); + public int read(final byte[] b, final int off, final int len) throws IOException { + final var n = realIn.read(b, off, len); bos.write(b, off, len); return n; } @@ -73,27 +73,27 @@ public String[] lines() { try { bos.flush(); } - catch(IOException ioe) { + catch(final IOException _ioe) { // Do nothing: the assertion based on this will not pass anyway. } return StdStreamsInterceptor.lines(bos); } } - private static String[] lines(ByteArrayOutputStream bos) { - var lines = bos.toString().split("\\r?\\n", -1); + private static String[] lines(final ByteArrayOutputStream bos) { + final var lines = bos.toString().split("\\r?\\n", -1); return Arrays.copyOf(lines, lines.length - 1); } public static InterceptionResult capture( - boolean silent, - Supplier executeFunc, - String[] stdInputs + final boolean silent, + final Supplier executeFunc, + final String[] stdInputs ) { - var bos = new ByteArrayOutputStream(); - var printStream = new PrintStream(silent ? bos : new OutputStreamMultiplexer(bos, realOut), true); + final var bos = new ByteArrayOutputStream(); + final var printStream = new PrintStream(silent ? bos : new OutputStreamMultiplexer(bos, realOut), true); - var inputStream = silent ? new ByteArrayInputStream(String.join(System.lineSeparator(), stdInputs).getBytes()) : new StdInInterceptor(); + final var inputStream = silent ? new ByteArrayInputStream(String.join(System.lineSeparator(), stdInputs).getBytes()) : new StdInInterceptor(); System.setOut(printStream); System.setIn(inputStream); diff --git a/src/main/java/engine/Style.java b/src/main/java/engine/Style.java new file mode 100644 index 0000000..1806db0 --- /dev/null +++ b/src/main/java/engine/Style.java @@ -0,0 +1,21 @@ +package engine; + +/** + * Console text styles. + */ +public enum Style { + None("\033[0m"), // Normal style + Red("\033[0;31m"), + Green("\033[0;32m"), + Cyan("\033[0;36m"), + Code("\033[38;5;27m"), + Strong("\033[1m"), + StrongRed("\033[0;31m" + "\033[1m"), + Inherit(""); // Inherit from parent text's style + + public final String tags; + + private Style(final String tags) { + this.tags = tags; + } +} diff --git a/src/main/java/engine/TestSensei.java b/src/main/java/engine/TestSensei.java index bbb2e90..27dd7a7 100644 --- a/src/main/java/engine/TestSensei.java +++ b/src/main/java/engine/TestSensei.java @@ -1,68 +1,81 @@ package engine; -import java.util.List; +import java.util.Optional; import java.util.stream.IntStream; import engine.script.ScriptRunner; -import engine.test.Line; +import engine.test.runner.CapturingPrinter; +import engine.test.runner.OutputCapture; +/** + * A sensei used by automated tests. Used by koans engine contributors. + */ public class TestSensei { - public static final Locale TEST_LOCALE = Locale.en; + private static final Locale DEFAULT_TEST_LOCALE = Locale.en; - public record TestResult(int testIndex, KoanTest test, boolean succeeded, CapturingPrinter output) { - public boolean hasCaptured(final Line... lines) { + public record TestResult(Locale locale, int testIndex, KoanResult koanResult, boolean succeeded, OutputCapture output) { + public boolean hasCaptured(final Fmt... lines) { return output.hasCaptured(lines); } - public void displayOutputToConsole() { - output.displayToConsole(); + public String toString() { + return String.format("%s/%s[%d]", koanResult.test.koan.koanClass.get(locale).simpleClassName, koanResult.test.koan.koanName.get(locale), testIndex); } + } - public String toString() { - return String.format("%s/%s[%d]", test.koan.koanClass.get(TEST_LOCALE).simpleClassName, test.koan.koanName, testIndex); + private record KoanTestIndex(Locale locale, int testIndex, KoanTest test) { + TestResult toResult(final KoanResult koanResult, final boolean succeed, final CapturingPrinter output) { + return new TestResult(locale, testIndex, koanResult, succeed, output); } } - public static List execute(Koan koan) { + public static TestResult[] execute(final Koan koan) { + return execute(koan, DEFAULT_TEST_LOCALE); + } + + public static TestResult[] execute(final Koan koan, final Locale locale) { return IntStream .range(0, koan.tests.length) - .mapToObj(i -> new KoanTestIndex(i, koan.tests[i])) + .mapToObj(i -> new KoanTestIndex(locale, i, koan.tests[i])) .map(TestSensei::executeTest) - .toList(); - } - - private record KoanTestIndex(int testIndex, KoanTest test) { - TestResult toResult(final boolean succeed, final CapturingPrinter output) { - return new TestResult(testIndex, test, succeed, output); - } + .toArray(TestResult[]::new); } private static TestResult executeTest(final KoanTestIndex testIndex) { final var test = testIndex.test; - final var capturingPrinter = new CapturingPrinter(TEST_LOCALE); + final var capturingPrinter = new CapturingPrinter(testIndex.locale()); test.setupRandomForKoan(); - for(var assertion: test.koan.beforeAssertions) { - if (!assertion.validate(capturingPrinter, TEST_LOCALE, test.koan)) { - return testIndex.toResult(false, capturingPrinter); + for(final var assertion: test.koan.beforeAssertions) { + if (!assertion.validate(capturingPrinter, testIndex.locale(), test.koan)) { + final var emptyResult = new KoanResult( + testIndex.locale(), + test, + new String[0], + new String[0], + null, + Optional.empty() + ); + return testIndex.toResult(emptyResult, false, capturingPrinter); } } final var interceptionResult = StdStreamsInterceptor.capture( true, - () -> ScriptRunner.execute(test.koan.koanClass, TEST_LOCALE, test.script), - test.stdInInputs(TEST_LOCALE) + () -> ScriptRunner.execute(test.koan.koanClass, testIndex.locale(), test.script), + test.stdInInputs(testIndex.locale()) ); final var result = new KoanResult( - TEST_LOCALE, + testIndex.locale(), test, interceptionResult.stdOutLines, interceptionResult.stdInLines, - interceptionResult.returnValue + interceptionResult.returnValue.executionResult(), + Optional.of(interceptionResult.returnValue.context()) ); - return testIndex.toResult(result.executeAssertions(capturingPrinter), capturingPrinter); + return testIndex.toResult(result, result.executeAssertions(capturingPrinter), capturingPrinter); } } diff --git a/src/main/java/engine/Texts.java b/src/main/java/engine/Texts.java index cccae20..11eb79d 100644 --- a/src/main/java/engine/Texts.java +++ b/src/main/java/engine/Texts.java @@ -6,147 +6,147 @@ public class Texts { // Sensei - public static Localizable MOUNTAINS_ARE_AGAIN_MERELY_MOUNTAINS = + public static final Localizable MOUNTAINS_ARE_AGAIN_MERELY_MOUNTAINS = local("Mountains are again merely mountains.") .fr("Les montagnes sont de nouveau de simples montagnes."); - public static Localizable THINKING = + public static final Localizable THINKING = local("Thinking %s ...") .fr("En pensant %s ..."); - public static Localizable HAS_DAMAGED_YOUR_KARMA = + public static final Localizable HAS_DAMAGED_YOUR_KARMA = local("%s has damaged your karma.") .fr("%s a alourdi ton karma."); - public static Localizable THE_MASTER_SAYS = + public static final Localizable THE_MASTER_SAYS = local("The master says:") .fr("Le maître dit:"); - public static Localizable THE_ANSWERS_YOU_SEEK = + public static final Localizable THE_ANSWERS_YOU_SEEK = local("The answers you seek...") .fr("Les réponses que tu cherches..."); - public static Localizable YOU_HAVE_NOT_REACHED_ENLIGHTMENT = + public static final Localizable YOU_HAVE_NOT_REACHED_ENLIGHTMENT = local(" You have not yet reached enlightment ...") .fr(" Tu n'as pas encore atteint l'illumination ..."); - public static Localizable EXPECTED_METHOD_TO_BE_PUBLIC = + public static final Localizable EXPECTED_METHOD_TO_BE_PUBLIC = local("Expected method %s to be public, but found it not public.") .fr("Attendu à ce que la méthode %s soit publique, mais ce n'est pas le cas."); - public static Localizable THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR = - local("The method %s() appears to produce an error: %s.") - .fr("La méthode %s() a produit une erreur: %s."); - public static Localizable THE_CODE_TRIED_BY_THE_SENSEI_SEEMS_TO_NOT_FINISH = + public static final Localizable THE_METHOD_APPEARS_TO_PRODUCE_AN_ERROR = + local("The method %s appears to produce an error: %s.") + .fr("La méthode %s a produit une erreur: %s."); + public static final Localizable THE_CODE_TRIED_BY_THE_SENSEI_SEEMS_TO_NOT_FINISH = local("The following code, tried by the Sensi, appears to not finish. Did you code an infinite loop?\n\n%s") .fr("Le code suivant, essayé par le Sensei, semble ne jamais terminer. As tu codé une boucle infinie?\n\n%s"); - public static Localizable THE_METHOD_SEEMS_TO_RECURSE_INFINITELY = + public static final Localizable THE_METHOD_SEEMS_TO_RECURSE_INFINITELY = local("The method %s() appears to not finish. Did you call the method in itself, forming an infinite loop?") .fr("La méthode %s() semble ne jamais terminer. As tu appelé la méthode dans elle-même, formant une boucle infinie?"); - public static Localizable THE_CONSTRUCTOR_APPEARS_TO_PRODUCE_AN_ERROR = + public static final Localizable THE_CONSTRUCTOR_APPEARS_TO_PRODUCE_AN_ERROR = local("The constructor of %s appears to produce an error: %s.") .fr("Le constructeur de %s a produit une erreur: %s."); - public static Localizable EXPECTED_TO_FIND_MEHOD_NO_PARAMS = - local("Expected to find a method called '%s' in src/main/java/%s.java but did not find any.") - .fr("Attendu à une méthode nommée '%s' dans src/main/java/%s.java, mais ne la trouve pas."); - public static Localizable EXPECTED_METHOD_TO_BE_STATIC = + public static final Localizable EXPECTED_TO_FIND_MEHOD_NO_PARAMS = + local("Expected to find a public method without parameters called '%s' in src/main/java/%s.java but did not find any.") + .fr("Attendu à une méthode publique sans paramètres nommée '%s' dans src/main/java/%s.java, mais ne la trouve pas."); + public static final Localizable EXPECTED_METHOD_TO_BE_STATIC = local("Expected method '%s' in src/main/java/%s.java to have the 'static' modifier, but it had not.") .fr("Attendu à ce que la méthode '%s' dans src/main/java/%s.java ai le modifieur 'static', mais elle ne l'a pas."); - public static Localizable EXPECTED_METHOD_TO_NOT_BE_STATIC = - local("Expected method '%s' %s to not have the 'static' modifier, but it had.") - .fr("Attendu à ce que la méthode '%s' dans %s n'ai pas le modifieur 'static', mais elle l'a."); - public static Localizable EXPECTED_TO_FIND_MEHOD_RETURN_TYPE = + public static final Localizable EXPECTED_METHOD_TO_NOT_BE_STATIC = + local("Expected method '%s' in src/main/java/%s.java to not have the 'static' modifier, but it had.") + .fr("Attendu à ce que la méthode '%s' dans src/main/java/%s.java n'ai pas le modifieur 'static', mais elle l'a."); + public static final Localizable EXPECTED_TO_FIND_MEHOD_RETURN_TYPE = local("Expected to find a method called '%s' in src/main/java/%s.java with a '%s' return type but did not find any.") .fr("Attendu à une méthode nommée '%s' dans src/main/java/%s.java, avec un type de retour '%s' mais ne la trouve pas."); - public static Localizable EXPECTED_TO_FIND_MEHOD_ONE_PARAM = - local("Expected to find a method called '%s' in src/main/java/%s.java with a '%s' parameter but did not find any.") - .fr("Attendu à une méthode nommée '%s' dans src/main/java/%s.java, avec un paramètre '%s' mais ne la trouve pas."); - public static Localizable EXPECTED_TO_FIND_MEHOD_MANY_PARAMS = - local("Expected to find a method called '%s' in src/main/java/%s.java with parameters of type %s but did not find any.") - .fr("Attendu à une méthode nommée '%s' dans src/main/java/%s.java, avec des paramètres de type %s mais ne la trouve pas."); - public static Localizable EXPECTED_CONSTRUCTOR_TO_BE_PUBLIC = + public static final Localizable EXPECTED_TO_FIND_MEHOD_ONE_PARAM = + local("Expected to find a public method called '%s' in src/main/java/%s.java with a %s parameter but did not find any.") + .fr("Attendu à une méthode publique nommée '%s' dans src/main/java/%s.java, avec un paramètre %s mais ne la trouve pas."); + public static final Localizable EXPECTED_TO_FIND_MEHOD_MANY_PARAMS = + local("Expected to find a public method called '%s' in src/main/java/%s.java with parameters of type %s but did not find any.") + .fr("Attendu à une méthode publique nommée '%s' dans src/main/java/%s.java, avec des paramètres de type %s mais ne la trouve pas."); + public static final Localizable EXPECTED_CONSTRUCTOR_TO_BE_PUBLIC = local("Expected constructor in %s to be public but it is not.") .fr("Attendu à ce que le constructeur dans %s soit publique, mais il ne l'est pas."); - public static Localizable EXPECTED_TO_FIND_CONSTRUCTOR_NO_PARAMS = - local("Expected to find a constructor in %s but did not find any.") - .fr("Attendu à un constructeur dans %s, mais ne le trouve pas."); - public static Localizable EXPECTED_TO_FIND_CONSTRUCTOR_ONE_PARAM = - local("Expected to find a constructor in %s with a '%s' parameter but did not find any.") - .fr("Attendu à constructeur dans %s, avec un paramètre '%s' mais ne le trouve pas."); - public static Localizable EXPECTED_TO_FIND_CONSTRUCTOR_MANY_PARAMS = - local("Expected to find a constructor in %s with parameters of type %s but did not find any.") - .fr("Attendu à un constructeur dans %s, avec des paramètres de type %s mais ne le trouve pas."); - public static Localizable EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE = + public static final Localizable EXPECTED_TO_FIND_CONSTRUCTOR_NO_PARAMS = + local("Expected to find a public constructor without parameters in %s but did not find any.") + .fr("Attendu à un constructeur publique sans paramètre dans %s, mais ne le trouve pas."); + public static final Localizable EXPECTED_TO_FIND_CONSTRUCTOR_ONE_PARAM = + local("Expected to find a public constructor in %s with a '%s' parameter but did not find any.") + .fr("Attendu à constructeur publique dans %s, avec un paramètre '%s' mais ne le trouve pas."); + public static final Localizable EXPECTED_TO_FIND_CONSTRUCTOR_MANY_PARAMS = + local("Expected to find a public constructor in %s with parameters of type %s but did not find any.") + .fr("Attendu à un constructeur publique dans %s, avec des paramètres de type %s mais ne le trouve pas."); + public static final Localizable EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE = local("Expected to find a class %s in the %s package, but did not find any.") .fr("Attendu à trouver une classe %s dans le package %s, mais ne la trouve pas."); - public static Localizable EXPECTED_CLASS_TO_BE_INSTANTIABLE = + public static final Localizable EXPECTED_CLASS_TO_BE_INSTANTIABLE = local("Expected class %s to be instantiable, but it is not.") .fr("Attendu ce que la classe %s soit instantiable, mais ne l'est pas."); - public static Localizable PLEASE_MEDITATE_ON = + public static final Localizable PLEASE_MEDITATE_ON = local("Please meditate on %s in src/main/java/koans/english/%s.java") .fr("Tu peux méditer sur %s dans src/main/java/koans/french/%s.java"); - public static Localizable THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN_ANSWERING = + public static final Localizable THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN_ANSWERING = local("The master sensed the harmony dissolving when planning to answer %s.") .fr("Le maître a senti l'harmonie se briser en voulant répondre %s."); - public static Localizable THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN_LOOKING_AT = + public static final Localizable THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN_LOOKING_AT = local("The master sensed the harmony dissolving when looking at the result of %s") .fr("Le maître a senti l'harmonie se briser en regardant le résultat de %s"); - public static Localizable THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN = + public static final Localizable THE_MASTER_SENSED_AN_HARMONY_BREACH_WHEN = local("The master sensed the harmony dissolving when:") .fr("Le maître a senti l'harmonie se briser en voulant:"); - public static Localizable WHEN_ANSWERING = + public static final Localizable WHEN_ANSWERING = local(" - planning to answer %s") .fr(" - répondre %s"); - public static Localizable WHEN_LOOKING_THE_RESULT_OF = + public static final Localizable WHEN_LOOKING_THE_RESULT_OF = local(" - and looking at the result of %s") .fr(" - et regarder le résultat de %s"); - public static Localizable WHEN_EXECUTING = + public static final Localizable WHEN_EXECUTING = local(" - executing:") .fr(" - exécuter:"); - public static Localizable AND_FINALLY_LOOKING_THE_RESULT_OF = + public static final Localizable AND_FINALLY_LOOKING_THE_RESULT_OF = local(" - and finally looking at the result of %s") .fr(" - et finalement regarder le résultat de %s"); - public static Localizable CONSOLE = + public static final Localizable CONSOLE = local("Console:") .fr("Console:"); - public static Localizable YOUR_PROGRESS_THUS_FAR = - local(ConsoleFmt.green("Your progress thus far: [") + "%s" + ConsoleFmt.green("]") + " %d/%d") - .fr(ConsoleFmt.green("Ton progrès jusqu'à maintenant: [") + "%s" + ConsoleFmt.green("]") + " %d/%d"); - public static Localizable AND = - local(", and %s") + public static final Localizable YOUR_PROGRESS_THUS_FAR = + local("Your progress thus far: [%s%s%s] %s") + .fr("Ton progrès jusqu'à maintenant: [%s%s%s] %s "); + public static final Localizable AND = + local(" and %s") .fr(" et %s"); - public static Localizable EXPECTED_TO_FIND_FIELD_IN_CLASS = + public static final Localizable EXPECTED_TO_FIND_FIELD_IN_CLASS = local("Expected to find a '%s' field in class %s, but did not find any.") .fr("Attendu à trouver un champ '%s' dans la classe %s, mais ne le trouve pas."); - public static Localizable BUG_FOUND = + public static final Localizable BUG_FOUND = local("Oops, it seems there is a design issue in this Koan. Please transmit the following message to maintainers: '%s'.") .fr("Oups, il semble y avoir un problème de conception avec ce Koan. SVP transmettre ce message aux créateurs: '%s'."); // Assertions - public static Localizable EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_NOTHING = + public static final Localizable EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_NOTHING = local("Expected to see an empty line in the console, but saw nothing!") .fr("Attendu à voir une ligne vide dans la console, mais n'a rien vu!"); - public static Localizable EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_INSTEAD = + public static final Localizable EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_INSTEAD = local("Expected to see an empty line in the console, but saw '%s' instead!") .fr("Attendu à voir une ligne vide dans la console, mais vu '%s' à la place!"); - public static Localizable OK_DISPLAYED_EMPTY_LINE_IN_CONSOLE = + public static final Localizable OK_DISPLAYED_EMPTY_LINE_IN_CONSOLE = local("Ok: displayed an empty line in the console.") .fr("Ok: affiché une ligne vide dans la console."); - public static Localizable EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_NOTHING = + public static final Localizable EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_NOTHING = local("Expected to see '%s' in the console when calling %s, but did not see it!") .fr("Attendu à voir '%s' dans la console en appelant %s, mais ne l'a pas vu!"); - public static Localizable EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_INSTEAD = + public static final Localizable EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_INSTEAD = local("Expected to see '%s' in the console when calling %s, but saw '%s' instead!") .fr("Attendu à voir '%s' dans la console en appelant %s, mais vu '%s' à la place!"); - public static Localizable OK_DISPLAYED_IN_CONSOLE = + public static final Localizable OK_DISPLAYED_IN_CONSOLE = local("Ok: displayed '%s' in the console when calling %s.") .fr("Ok: affiché '%s' dans la console en appelant %s."); - public static Localizable EXPECTED_TO_SEE_NOTHING_IN_CONSOLE_BUT_SAW_INSTEAD = + public static final Localizable EXPECTED_TO_SEE_NOTHING_IN_CONSOLE_BUT_SAW_INSTEAD = local("Expected to not see anything more in the console when calling %s, but saw '%s' instead!") .fr("Attendu à ne plus rien voir dans la console en appelant %s, mais vu '%s' à la place!"); - public static Localizable OK_ASKED_FOR_LINE_IN_CONSOLE = + public static final Localizable OK_ASKED_FOR_LINE_IN_CONSOLE = local("Ok: asked for a line in the console.") .fr("Ok: demandé du texte dans la console."); - public static Localizable EXPECTED_FOR_USER_TO_ANSWER_IN_CONSOLE = + public static final Localizable EXPECTED_FOR_USER_TO_ANSWER_IN_CONSOLE = local("Expected the user to be able to answer in the console, but they were not!") .fr("Attendu à ce que l'utilisateur puisse répondre dans la console, mais iel ne la pas pu!"); - public static Localizable EXPECTED_TO_RETURN_INT_BUT_RETURNED_NULL = - local("Expected %s to return %s but returned null instead!") - .fr("Attendu à ce que %s retourne %s mais a retourné null à la place!"); + public static final Localizable EXPECTED_VARIABLE_TO_EQUAL_BUT_IS_NULL = + local("Expected %s to result in %s to be equal to %s but was null instead!") + .fr("Attendu à ce que %s résulte en %s égal à %s; mais était null à la place!"); public static Localizable EXPECTED_TO_RETURN_INT_BUT_RETURNED_OTHER_TYPE = local("Expected %s to return an integer but returned a '%s' instead!") .fr("Attendu à ce que %s retourne un entier mais a retourné un '%s' à la place!"); @@ -192,47 +192,98 @@ public class Texts { public static Localizable OK_RETURNED_STRING = local("Ok: %s returned \"%s\".") .fr("Ok: %s a retourné \"%s\"."); + public static Localizable EXPECTED_TO_RETURN_INT_ARRAY_BUT_RETURNED_NULL = + local("Expected %s to return %s but returned null instead!") + .fr("Attendu à ce que %s retourne %s mais a retourné null à la place!"); + public static Localizable EXPECTED_TO_RETURN_INT_ARRAY_BUT_RETURNED_OTHER_TYPE = + local("Expected %s to return a int[] but returned a %s instead!") + .fr("Attendu à ce que %s retourne une int[] mais a retourné un '%s' à la place!"); + public static Localizable EXPECTED_TO_RETURN_INT_ARRAY_BUT_RETURNED = + local("Expected %s to return %s but returned %s instead!") + .fr("Attendu à ce que %s retourne %s mais a retourné %s à la place!"); + public static Localizable OK_RETURNED_INT_ARRAY = + local("Ok: %s returned %s.") + .fr("Ok: %s a retourné %s."); public static Localizable EXPECTED_TO_RETURN_BUT_RETURNED_NULL = local("Expected %s to return %s but returned null instead!") .fr("Attendu à ce que %s retourne un %s mais a retourné null à la place!"); - public static Localizable EXPECTED_TO_RETURN_BUT_RETURNED_OTHER_TYPE = + public static final Localizable EXPECTED_TO_RETURN_BUT_RETURNED_OTHER_TYPE = local("Expected %s to return a %s but returned a %s instead!") .fr("Attendu à ce que %s retourne un %s mais a retourné un %s à la place!"); - public static Localizable EXPECTED_TO_RETURN_BUT_RETURNED = + public static final Localizable EXPECTED_TO_RETURN_IMPLEMENTING_BUT_RETURNED_NULL = + local("Expected %s to return a %s but returned null instead!") + .fr("Attendu à ce que %s retourne un %s mais a retourné null à la place!"); + public static final Localizable EXPECTED_TO_RETURN_IMPLEMENTING_BUT_NOT = + local("Expected %s to return an object implementing %s but %s is not!") + .fr("Attendu à ce que %s retourne un object implémentant %s mais %s ne l'implémente pas!"); + public static final Localizable EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED_NULL = + local("Expected %s to return an anonymous implementation but returned null instead!") + .fr("Attendu à ce que %s retourne une implémentation anonyme mais a retourné null à la place!"); + public static final Localizable EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED = + local("Expected %s to return an anonymous implementation but returned a %s instead!") + .fr("Attendu à ce que %s retourne une implémentation anonyme mais a retourné un %s à la place!"); + public static final Localizable EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED_LAMBDA = + local("Expected %s to return an anonymous implementation but returned a lambda method instead!") + .fr("Attendu à ce que %s retourne une implémentation anonyme mais a retourné une méthode lambda à la place!"); + public static final Localizable EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED_NULL = + local("Expected %s to return a lambda method but returned null instead!") + .fr("Attendu à ce que %s retourne une méthode lambda mais a retourné null à la place!"); + public static final Localizable EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED = + local("Expected %s to return a lambda method but returned a %s instead!") + .fr("Attendu à ce que %s retourne une méthode lambda mais a retourné un %s à la place!"); + public static final Localizable EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED_ANONYMOUS = + local("Expected %s to return a lambda method but returned an anonymous implementation instead!") + .fr("Attendu à ce que %s retourne une méthode lambda mais a retourné une implémentation anonyme à la place!"); + public static final Localizable EXPECTED_VARIABLE_TO_BE_BUT_WAS_OTHER_TYPE = + local("Expected %s to result in %s being a %s but was a %s instead!") + .fr("Attendu à ce que %s résulte en %s étant un %s mais était un %s à la place!"); + public static final Localizable EXPECTED_VARIABLE_TO_EQUAL_BUT_EQUAL = + local("Expected %s to result in %s being equal to %s but was equal to %s instead!") + .fr("Attendu à ce que %s résulte en %s étant égal à %s mais était égal à %s à la place!"); + public static final Localizable EXPECTED_TO_RETURN_BUT_RETURNED = local("Expected %s to return %s but returned %s instead!") .fr("Attendu à ce que %s retourne %s mais a retourné %s à la place!"); - public static Localizable OK_RETURNED = + public static final Localizable OK_VARIABLE_EQUAL = + local("Ok: %s is equal to %s.") + .fr("Ok: %s est égal à %s."); + public static final Localizable OK_RETURNED = local("Ok: %s returned %s.") .fr("Ok: %s a retourné %s."); - public static Localizable EXPECTED_TO_LOOK_LIKE_BUT_LOOKED_LIKE = - local("Expected object to look like \"%s\" but looked like \"%s\" instead!") - .fr("Attendu à ce que l'objet ressemble à \"%s\" mais ressemble à \"%s\" à la place!"); - public static Localizable OK_LOOKED_LIKE = - local("Ok: object looked like \"%s\".") - .fr("Ok: l'object ressemble à \"%s\"."); - public static Localizable EXPECTED_TO_RETURN_INT_FROM_RANDOM_BUT_RETURNED = - local("Expected %s to return %d from random number %f but returned %d instead!") - .fr("Attendu à ce que %s retourne %d à partir du nombre aléatoire %f mais a retourné %d à la place!"); - public static Localizable OK_RETURNED_INT_FROM_RANDOM = - local("Ok: %s returned %d from random number %f.") - .fr("Ok: %s a retourné %d à partir du nombre aléatoire %f."); - public static Localizable EXPECTED_TO_RETURN_INT_FROM_RANDOMS_BUT_RETURNED = - local("Expected %s to return %d from random numbers %s but returned %d instead!") - .fr("Attendu à ce que %s retourne %d à partir des nombres aléatoires %s mais a retourné %d à la place!"); - public static Localizable OK_RETURNED_INT_FROM_RANDOMS = - local("Ok: %s returned %d from random numbers %s.") - .fr("Ok: %s a retourné %d à partir du nombre aléatoire %s."); - public static Localizable EXPECTED_FIELD_TO_BE_PRIVATE = + public static final Localizable OK_RETURNED_OBJECT_IMPLEMENTS = + local("Ok: return of %s implements %s.") + .fr("Ok: le retour de %s implémente %s."); + public static final Localizable OK_RETURNED_OBJECT_IS_ANONYMOUS = + local("Ok: return of %s is an anonymous implementation.") + .fr("Ok: le retour de %s est une implémentation anonyme."); + public static final Localizable OK_RETURNED_OBJECT_IS_LAMBDA = + local("Ok: return of %s is a lambda method.") + .fr("Ok: le retour de %s est une méthode lambda."); + public static final Localizable EXPECTED_TO_RETURN_INT_FROM_RANDOM_BUT_RETURNED = + local("Expected %s to return %s from random number %s but returned %s instead!") + .fr("Attendu à ce que %s retourne %s à partir du nombre aléatoire %s mais a retourné %s à la place!"); + public static final Localizable OK_RETURNED_INT_FROM_RANDOM = + local("Ok: %s returned %s from random number %s.") + .fr("Ok: %s a retourné %s à partir du nombre aléatoire %s."); + public static final Localizable EXPECTED_TO_RETURN_INT_FROM_RANDOMS_BUT_RETURNED = + local("Expected %s to return %s from random numbers %s but returned %s instead!") + .fr("Attendu à ce que %s retourne %s à partir des nombres aléatoires %s mais a retourné %s à la place!"); + public static final Localizable OK_RETURNED_INT_FROM_RANDOMS = + local("Ok: %s returned %s from random numbers %s.") + .fr("Ok: %s a retourné %s à partir du nombre aléatoire %s."); + public static final Localizable EXPECTED_FIELD_TO_BE_PRIVATE = local("Expected '%s' field in class %s to be private, but it is not.") .fr("Attendu à ce que le champ '%s' dans la classe %s soit privé, mais il ne l'est pas."); - public static Localizable EXPECTED_FIELD_TO_BE_FINAL = + public static final Localizable EXPECTED_FIELD_TO_BE_FINAL = local("Expected '%s' field in class %s to be final, but it is not.") .fr("Attendu à ce que le champ '%s' dans la classe %s soit final, mais il ne l'est pas."); - public static Localizable EXPECTED_FIELD_TO_NOT_BE_FINAL = + public static final Localizable EXPECTED_FIELD_TO_NOT_BE_FINAL = local("Expected '%s' field in class %s to not be final, but it is.") .fr("Attendu à ce que le champ '%s' dans la classe %s ne soit pas final, mais il l'est."); - public static Localizable EXPECTED_FIELD_TO_BE_OF_TYPE = + public static final Localizable EXPECTED_FIELD_TO_BE_OF_TYPE = local("Expected '%s' field in class %s to be a '%s', but it is a '%s'.") .fr("Attendu à ce que le champ '%s' dans la classe %s soit un '%s', mais il est un '%s'."); + public static final Localizable EXPECTED_CLASS_TO_IMPLEMENT = + local("Expected class %s to implement %s, but it does not.") + .fr("Attendu à ce la classe %s implémente %s, mais elle ne l'implémente pas."); } diff --git a/src/main/java/engine/script/Expression.java b/src/main/java/engine/script/Expression.java index f2054c1..5cc4afc 100644 --- a/src/main/java/engine/script/Expression.java +++ b/src/main/java/engine/script/Expression.java @@ -18,6 +18,8 @@ * import static engine.script.Expression.newObject; * * var expressionToCreatePoint = newObject("geom.Point", 2.0, 2.0); + * + * Koans can then define assertions based on this expression, and display approriate errors when those assertions are failing. */ public sealed interface Expression { /** @@ -30,6 +32,11 @@ public sealed interface Expression { */ String formatSourceCode(final Locale locale); + /** + * Fetch what would be the resulting type of this expression. + */ + Class expressionClass(final ExecutionContext ctx); + /** * Executes all the given expressions. Useful for getting actual values for parameters of a method or constructor. */ @@ -62,6 +69,13 @@ public default Expression call(String methodName, Object... params) { return new CallMethod(this, methodName, params); } + /** + * Creates an expression representing a literal. + */ + public static Expression lit(Object value) { + return value instanceof Localizable ? new LocalizedLiteral((Localizable)value) : new Literal(value); + } + /** * Creates an expression representing a call to the constructor of the given class. */ @@ -70,12 +84,22 @@ public static Expression newObject(String className, Object... params) { } /** - * Creates an expression representing a variable assignment. + * Creates an expression representing a variable assignment with the given expression. */ public static Expression assignVariable(String variableName, Expression value) { + if (value == null) { + return assignVariable(variableName, new Literal(value)); + } return new AssignVariable(variableName, value); } + /** + * Creates an expression representing a variable assignment with the given literal. + */ + public static Expression assignVariable(String variableName, Object value) { + return assignVariable(variableName, new Literal(value)); + } + /** * Creates an expression representing a reference to variable's value. */ @@ -83,6 +107,9 @@ public static Expression variable(String variableName) { return new Variable(variableName); } + /** + * Formats what the code defined by the given script would look like in Java. + */ public static String formatSourceCode(final Expression[] script, final Locale locale) { StringBuilder builder = new StringBuilder(); @@ -94,7 +121,7 @@ public static String formatSourceCode(final Expression[] script, final Locale lo } /** - * Same as formatSourceCode(), except it ommits the last expression (considered the main one a test is testing). + * Same as formatSourceCode(), except it ommits the last expression (considered the main one a Koan is testing). */ public static String formatPreparationSourceCode(final Expression[] script, final Locale locale, final String identation) { StringBuilder builder = new StringBuilder(); @@ -109,13 +136,24 @@ public static String formatPreparationSourceCode(final Expression[] script, fina static Expression[] normalizeToExpression(final Object[] objects) { return Arrays .stream(objects) - .map(p -> p instanceof Expression ? p : new Literal(p)) + .map(p -> p instanceof Expression ? p : lit(p)) .toArray(Expression[]::new); } static String formatLiteralSourceCode(final Object value) { - if (value instanceof String) { + if (value == null) { + return "null"; + } else if (value instanceof String) { return String.format("\"%s\"", value); + } else if (value instanceof int[] arr) { + final var elts = String.join( + ",", + Arrays.stream(arr).mapToObj(Integer::toString).toList() + ); + return String.format("new int[]{%s}", elts); + } + if (value.getClass().isArray()){ + return Arrays.toString((int[])value); } return value.toString(); } @@ -133,8 +171,34 @@ static String formatParamListSourceCode(final Expression[] params, final Locale ); } - private static Method getMethod(final Class clasz, final String methodName, final Expression[] params) { - final var methodCandidates = Arrays + private static boolean paramIsMatching(final Class actualParam, final Class expressionParam) { + return actualParam.isAssignableFrom(expressionParam) || actualParam.equals(Type.UNBOXED.get(expressionParam)); + } + + private static boolean paramsAreMatching(final ExecutionContext ctx, final Method method, final Expression[] params) { + final var actualParams = method.getParameters(); + if (actualParams.length != params.length) { + return false; + } + for(int i=0; i clasz, final String methodName, final Expression[] params) { + // Because anonymous and lambda classes are package private, only their interface methods are accessible. So if one of the interfaces + // has that method, use this one instead of the concrete class version. + final var interfaceMethodCandidates = Arrays + .stream(clasz.getInterfaces()) + .flatMap(cls -> Arrays.stream(cls.getMethods())) + .filter(meth -> meth.getName().equals(methodName) && paramsAreMatching(ctx, meth, params)) + .toList(); + final var methodCandidates = interfaceMethodCandidates.size() > 0 ? interfaceMethodCandidates : Arrays .stream(clasz.getMethods()) .filter(meth -> meth.getName().equals(methodName) && meth.getParameterCount() == params.length) .toList(); @@ -159,31 +223,31 @@ private static Method getMethod(final Class clasz, final String methodName, f return methodCandidates.get(0); } - static Method getStaticMethod(final Class clasz, final String methodName, final Expression[] params) { - final var method = getMethod(clasz, methodName, params); + static Method findStaticMethod(final ExecutionContext ctx, final Class clasz, final String methodName, final Expression[] params) { + final var method = findMethod(ctx, clasz, methodName, params); if (!Modifier.isStatic(method.getModifiers())) { throw new KoanBugException(String.format("The method %s is static, which should have been already caught by a missing assertion in this or a previous Koans.", methodName)); } return method; } - static Method getObjectMethod(final Class clasz, final String methodName, final Expression[] params) { - final var method = getMethod(clasz, methodName, params); + static Method findObjectMethod(final ExecutionContext ctx, final Class clasz, final String methodName, final Expression[] params) { + final var method = findMethod(ctx, clasz, methodName, params); if (Modifier.isStatic(method.getModifiers())) { throw new KoanBugException(String.format("The method %s is not static, which should have been already caught by a missing assertion in this or a previous Koans.", methodName)); } return method; } - static Object invoke(final ExecutionContext ctx, final Object instance, final Method method, final Expression[] params) { + static Object invokeMethod(final ExecutionContext ctx, final Object instance, final Method method, final Expression[] params) { try { return method.invoke(instance, Expression.executeAll(ctx, params)); - } catch(IllegalAccessException iae) { + } catch(final IllegalAccessException iae) { throw new KoanBugException(String.format("The method %s is not accessible, which should have been already caught by a missing assertion in this or a previous Koans.", method.getName())); - } catch(IllegalArgumentException iae) { + } catch(final IllegalArgumentException iae) { throw new KoanBugException(String.format("The method %s do not possess the right parameters, which should have been already caught by a missing assertion in this or a previous Koans.", method.getName())); - } catch(InvocationTargetException ite) { - throw new ScriptExecutionException(ite, String.format("%s.%s()", method.getClass().getSimpleName(), method.getName())); + } catch(final InvocationTargetException ite) { + throw new ScriptExecutionException(ite, String.format("%s.%s()", method.getDeclaringClass().getSimpleName(), method.getName())); } } } @@ -196,17 +260,22 @@ final record Literal(Object value) implements Expression { Double.class, Boolean.class, Character.class, - String.class + String.class, + int[].class ); public Literal { if (value != null && !ALLOWED_LITERAL_TYPES.contains(value.getClass())) { - throw new KoanBugException("Only null, String, and primitive types are allowed as literal in an Expression"); + throw new KoanBugException("Only null, String, int arrays, and primitive types are allowed as literal in an Expression"); } } @Override public Object execute(final ExecutionContext ctx) { + // Make sure we clone mutable values first, so we preserve initial value if we later need to display it. + if (value instanceof final int[] arrayValue) { + return arrayValue.clone(); + } return value; } @@ -214,6 +283,11 @@ public Object execute(final ExecutionContext ctx) { public String formatSourceCode(final Locale locale) { return Expression.formatLiteralSourceCode(value); } + + @Override + public Class expressionClass(final ExecutionContext _ctx) { + return value.getClass(); + } } final record Variable(String name) implements Expression { @@ -227,6 +301,11 @@ public Object execute(final ExecutionContext ctx) { public String formatSourceCode(final Locale locale) { return name; } + + @Override + public Class expressionClass(final ExecutionContext ctx) { + return execute(ctx).getClass(); + } } final record LocalizedLiteral(Localizable local) implements Expression { @@ -239,6 +318,11 @@ public Object execute(final ExecutionContext ctx) { public String formatSourceCode(final Locale locale) { return Expression.formatLiteralSourceCode(local.get(locale)); } + + @Override + public Class expressionClass(final ExecutionContext _ctx) { + return local.get(Locale.en).getClass(); + } } final record NewObject(String className, Object[] constructorParams) implements Expression { @@ -251,20 +335,32 @@ public Object execute(final ExecutionContext ctx) { public String formatSourceCode(final Locale locale) { return String.format("new %s(%s)", className, Expression.formatParamListSourceCode(constructorParams, locale)); } + + @Override + public Class expressionClass(final ExecutionContext _ctx) { + return new Type(className).unsafeResolve(); + } } final record CallKoanMethod(String methodName, Object[] params) implements Expression { @Override public Object execute(final ExecutionContext ctx) { final var paramExpressions = Expression.normalizeToExpression(params); - final var method = Expression.getStaticMethod(ctx.koanClass, methodName, paramExpressions); - return Expression.invoke(ctx, null, method, paramExpressions); + final var method = Expression.findStaticMethod(ctx, ctx.koanClass, methodName, paramExpressions); + return Expression.invokeMethod(ctx, null, method, paramExpressions); } @Override public String formatSourceCode(final Locale locale) { return String.format("%s(%s)", methodName, Expression.formatParamListSourceCode(params, locale)); } + + @Override + public Class expressionClass(final ExecutionContext ctx) { + final var paramExpressions = Expression.normalizeToExpression(params); + final var method = Expression.findStaticMethod(ctx, ctx.koanClass, methodName, paramExpressions); + return method.getReturnType(); + } } final record CallStaticMethod(String className, String methodName, Object... params) implements Expression { @@ -272,14 +368,22 @@ final record CallStaticMethod(String className, String methodName, Object... par public Object execute(final ExecutionContext ctx) { final var paramExpressions = Expression.normalizeToExpression(params); final var clasz = new Type(className).unsafeResolve(); - final var method = Expression.getStaticMethod(clasz, methodName, paramExpressions); - return Expression.invoke(ctx, null, method, paramExpressions); + final var method = Expression.findStaticMethod(ctx, clasz, methodName, paramExpressions); + return Expression.invokeMethod(ctx, null, method, paramExpressions); } @Override public String formatSourceCode(final Locale locale) { return String.format("%s.%s(%s)", className, methodName, Expression.formatParamListSourceCode(params, locale)); } + + @Override + public Class expressionClass(final ExecutionContext ctx) { + final var paramExpressions = Expression.normalizeToExpression(params); + final var clasz = new Type(className).unsafeResolve(); + final var method = Expression.findStaticMethod(ctx, clasz, methodName, paramExpressions); + return method.getReturnType(); + } } final record CallMethod(Expression instance, String methodName, Object... params) implements Expression { @@ -287,14 +391,22 @@ final record CallMethod(Expression instance, String methodName, Object... params public Object execute(final ExecutionContext ctx) { final var paramExpressions = Expression.normalizeToExpression(params); final var instanceValue = instance.execute(ctx); - final var method = Expression.getObjectMethod(instanceValue.getClass(), methodName, paramExpressions); - return Expression.invoke(ctx, instanceValue, method, paramExpressions); + final var method = Expression.findObjectMethod(ctx, instanceValue.getClass(), methodName, paramExpressions); + return Expression.invokeMethod(ctx, instanceValue, method, paramExpressions); } @Override public String formatSourceCode(final Locale locale) { return String.format("%s.%s(%s)", instance.formatSourceCode(locale), methodName, Expression.formatParamListSourceCode(params, locale)); } + + @Override + public Class expressionClass(final ExecutionContext ctx) { + final var paramExpressions = Expression.normalizeToExpression(params); + final var instanceValue = instance.execute(ctx); + final var method = Expression.findObjectMethod(ctx, instanceValue.getClass(), methodName, paramExpressions); + return method.getReturnType(); + } } final record AssignVariable(String variableName, Expression value) implements Expression { @@ -308,4 +420,9 @@ public Object execute(final ExecutionContext ctx) { public String formatSourceCode(final Locale locale) { return String.format("var %s = %s", variableName, value.formatSourceCode(locale)); } + + @Override + public Class expressionClass(final ExecutionContext _ctx) { + return void.class; + } } diff --git a/src/main/java/engine/script/ScriptRunner.java b/src/main/java/engine/script/ScriptRunner.java index e8a62bb..d84479e 100644 --- a/src/main/java/engine/script/ScriptRunner.java +++ b/src/main/java/engine/script/ScriptRunner.java @@ -4,17 +4,19 @@ import engine.Localizable; public final class ScriptRunner { + public record ExecutionReport(Object executionResult, ExecutionContext context) {} + /** * Executes the given script and return the value generated by the last executed. */ - public static Object execute(final Localizable declaredKoanClass, final Locale locale, final Expression[] script) { + public static ExecutionReport execute(final Localizable declaredKoanClass, final Locale locale, final Expression[] script) { Object lastCallValue = null; final var ctx = new ExecutionContext(declaredKoanClass, locale); - for(var stmt: script) { + for(final var stmt: script) { lastCallValue = stmt.execute(ctx); } - return lastCallValue; + return new ExecutionReport(lastCallValue, ctx); } } diff --git a/src/main/java/engine/script/Type.java b/src/main/java/engine/script/Type.java index 1506840..6c1662b 100644 --- a/src/main/java/engine/script/Type.java +++ b/src/main/java/engine/script/Type.java @@ -3,10 +3,20 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.Map; import engine.KoanBugException; public class Type { + public static final Map, Class> UNBOXED = Map.of( + Integer.class, int.class, + Double.class, double.class, + Boolean.class, boolean.class, + Long.class, long.class, + Float.class, float.class, + Character.class, char.class + ); + private Class clasz = null; public final String className; public final String simpleClassName; @@ -37,7 +47,7 @@ public Class unsafeResolve() { if (clasz == null) { try { clasz = Class.forName(className); - } catch(ClassNotFoundException cnfe) { + } catch(final ClassNotFoundException cnfe) { throw new KoanBugException(String.format("The class %s is not found, which should have been already caught by a missing assertion in this or a previous Koans.", className)); } } @@ -72,18 +82,18 @@ public Object unsafeInstantiate(final ExecutionContext ctx, final Expression[] c try { return constructorCandidates.get(0).newInstance(Expression.executeAll(ctx, constructorParams)); - } catch (InstantiationException ie) { + } catch (final InstantiationException _ie) { throw new KoanBugException(String.format( "Koans should assert that class %s is instantiable.", clasz.getName() )); - } catch(IllegalAccessException iae) { + } catch(final IllegalAccessException _iae) { throw new KoanBugException(String.format("The constructor of %s is not accessible, which should have been already caught by a missing assertion in this or a previous Koans.", clasz.getSimpleName())); - } catch(SecurityException se) { + } catch(final SecurityException _se) { throw new KoanBugException(String.format("Stop messing with class loaders ;). Cannot instantiate %s.", clasz.getSimpleName())); - } catch(IllegalArgumentException iae) { + } catch(final IllegalArgumentException _iae) { throw new KoanBugException(String.format("The constructor of %s do not possess the right parameters, which should have been already caught by a missing assertion in this or a previous Koans.", clasz.getSimpleName())); - } catch(InvocationTargetException ite) { + } catch(final InvocationTargetException ite) { throw new ScriptExecutionException(ite, String.format("new %s(...)", clasz.getSimpleName())); } } diff --git a/src/main/java/engine/test/ConsoleAssertionsTests.java b/src/main/java/engine/test/ConsoleAssertionsTests.java new file mode 100644 index 0000000..4cefd71 --- /dev/null +++ b/src/main/java/engine/test/ConsoleAssertionsTests.java @@ -0,0 +1,176 @@ +package engine.test; + +import static engine.Assertions.assertAskedInStdIn; +import static engine.Assertions.assertNextStdOutLineEquals; +import static engine.Assertions.assertNextStdOutLineIsEmpty; +import static engine.Assertions.assertNoMoreLineInStdOut; +import static engine.Fmt.code; +import static engine.Fmt.green; +import static engine.Fmt.red; +import static engine.Fmt.sameStyle; +import static engine.Localizable.global; + +import engine.Koan; +import engine.Localizable; +import engine.TestSensei; +import engine.test.simulation.StudentSolutions; + +import static engine.script.Expression.callKoanMethod; +import static engine.test.runner.RunnerAssertions.assertKoanFails; +import static engine.test.runner.RunnerAssertions.assertKoanPass; +import static engine.Texts.*; + +public class ConsoleAssertionsTests { + private static Localizable> CLASS = global(StudentSolutions.class); + + public static void whenAssertNextStdOutLineAndItIsThere() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNextStdOutLineAndItIsThere")) + .useConsole() + .when( + callKoanMethod("simpleConsoleOutput") + ) + .then( + assertNextStdOutLineEquals(global("hello")) + ) + ); + + assertKoanPass(res[0], green(OK_DISPLAYED_IN_CONSOLE, sameStyle("hello"), code("simpleConsoleOutput()"))); + } + + public static void whenAssertNextStdOutLineAndItIsDifferent() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNextStdOutLineAndItIsDifferent")) + .useConsole() + .when( + callKoanMethod("simpleConsoleOutput") + ) + .then( + assertNextStdOutLineEquals(global("bye")) + ) + ); + + assertKoanFails(res[0], red(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_INSTEAD, sameStyle("bye"), code("simpleConsoleOutput()"), sameStyle("hello"))); + } + + public static void whenAssertNextStdOutLineAndThereIsNothing() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNextStdOutLineAndThereIsNothing")) + .useConsole() + .when( + callKoanMethod("doNothing") + ) + .then( + assertNextStdOutLineEquals(global("hello")) + ) + ); + + assertKoanFails(res[0], red(EXPECTED_TO_SEE_IN_CONSOLE_BUT_SAW_NOTHING, sameStyle("hello"), code("doNothing()"))); + } + + public static void whenAssertNoMoreLineInStdOutAndThereIsNone() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNoMoreLineInStdOutAndThereIsNone")) + .useConsole() + .when( + callKoanMethod("doNothing") + ) + .then( + assertNoMoreLineInStdOut() + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertNoMoreLineInStdOutAndThereIsOne() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNoMoreLineInStdOutAndThereIsOne")) + .useConsole() + .when( + callKoanMethod("simpleConsoleOutput") + ) + .then( + assertNoMoreLineInStdOut() + ) + ); + + assertKoanFails(res[0], red(EXPECTED_TO_SEE_NOTHING_IN_CONSOLE_BUT_SAW_INSTEAD, code("simpleConsoleOutput()"), sameStyle("hello"))); + } + + public static void whenAssertNextStdOutLineIsEmptyAndItIs() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNextStdOutLineAndItIsDifferent")) + .useConsole() + .when( + callKoanMethod("emptyConsoleOutput") + ) + .then( + assertNextStdOutLineIsEmpty() + ) + ); + + assertKoanPass(res[0], green(OK_DISPLAYED_EMPTY_LINE_IN_CONSOLE)); + } + + public static void whenAssertNextStdOutLineIsEmptyAndItIsNot() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNextStdOutLineIsEmptyAndItIsNot")) + .useConsole() + .when( + callKoanMethod("simpleConsoleOutput") + ) + .then( + assertNextStdOutLineIsEmpty() + ) + ); + + assertKoanFails(res[0], red(EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_INSTEAD, sameStyle("hello"))); + } + + public static void whenAssertNextStdOutLineIsEmptyAndThereIsNone() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertNextStdOutLineIsEmptyAndThereIsNone")) + .useConsole() + .when( + callKoanMethod("doNothing") + ) + .then( + assertNextStdOutLineIsEmpty() + ) + ); + + assertKoanFails(res[0], red(EXPECTED_TO_SEE_EMPTY_LINE_IN_CONSOLE_BUT_SAW_NOTHING)); + } + + public static void whenAssertAskedInStdInAndAsked() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertAskedInStdInAndAsked")) + .useConsoleAndShowStdinInputs() + .when( + callKoanMethod("readFromConsole") + ) + .withStdInInputs("abc") + .then( + assertAskedInStdIn() + ) + ); + + assertKoanPass(res[0], green(OK_ASKED_FOR_LINE_IN_CONSOLE)); + } + + public static void whenAssertAskedInStdInAndDidNotAsked() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertAskedInStdInAndAsked")) + .useConsoleAndShowStdinInputs() + .when( + callKoanMethod("doNothing") + ) + .then( + assertAskedInStdIn() + ) + ); + + assertKoanFails(res[0], red(EXPECTED_FOR_USER_TO_ANSWER_IN_CONSOLE)); + } +} diff --git a/src/main/java/engine/test/ConsoleUnitTests.java b/src/main/java/engine/test/ConsoleUnitTests.java deleted file mode 100644 index f742505..0000000 --- a/src/main/java/engine/test/ConsoleUnitTests.java +++ /dev/null @@ -1,52 +0,0 @@ -package engine.test; - -import static engine.Assertions.assertKoanMethodIsInvokable; -import static engine.Assertions.assertNextStdOutLineEquals; - import static engine.Assertions.assertNoMoreLineInStdOut; -import static engine.ConsoleFmt.code; -import static engine.ConsoleFmt.format; -import static engine.Localizable.global; - -import java.util.List; - -import engine.ConsoleFmt; -import engine.Koan; -import engine.Localizable; -import engine.ConsoleFmt.Formats; -import engine.test.simulation.StudentSolutions; -import static engine.script.Expression.callKoanMethod; -import static engine.Texts.*; -import static engine.test.UnitTestExpectation.assertSuccess; -import static engine.test.UnitTestExpectation.assertFailure; - -public class ConsoleUnitTests { - private static Localizable> CLASS = global(StudentSolutions.class); - - public static final List items = List.of( - new UnitTest( - new Koan(CLASS, global("simpleConsoleOutput()")) - .useConsole() - .beforeFirstTest(assertKoanMethodIsInvokable("simpleConsoleOutput")) - .when(callKoanMethod("simpleConsoleOutput")) - .then( - assertNextStdOutLineEquals(global("hello")), - assertNoMoreLineInStdOut() - ), - assertSuccess( - new Line.Localized(format(OK_DISPLAYED_IN_CONSOLE, Formats.Green, "hello", code("simpleConsoleOutput()"))) - ) - ), - new UnitTest( - new Koan(CLASS, global("noMethod")) - .useConsole() - .beforeFirstTest(assertKoanMethodIsInvokable("noMethod")) - .when(callKoanMethod("noMethod")) - .then( - assertNextStdOutLineEquals(global("hello")) - ), - assertFailure( - new Line.Localized(ConsoleFmt.red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS), "noMethod", "engine/test/simulation/StudentSolutions") - ) - ) - ); -} diff --git a/src/main/java/engine/test/EqualityAssertionsTests.java b/src/main/java/engine/test/EqualityAssertionsTests.java new file mode 100644 index 0000000..683a26c --- /dev/null +++ b/src/main/java/engine/test/EqualityAssertionsTests.java @@ -0,0 +1,568 @@ +package engine.test; + +import static engine.Assertions.assertReturnValueEquals; +import static engine.Assertions.assertReturnValueStringRepresentationEquals; +import static engine.Assertions.assertReturnValueWithRandomEquals; +import static engine.Assertions.assertVariableEquals; +import static engine.Fmt.classSimpleName; +import static engine.Fmt.code; +import static engine.Fmt.green; +import static engine.Fmt.red; +import static engine.Fmt.sameStyle; +import static engine.Fmt.sequence; +import static engine.Localizable.global; +import static engine.Localizable.local; + +import engine.Style; +import engine.Helpers; +import engine.Koan; +import engine.Locale; +import engine.Localizable; +import engine.ResToIntFunction; +import engine.TestSensei; + +import static engine.script.Expression.assignVariable; +import static engine.script.Expression.callKoanMethod; +import static engine.test.runner.RunnerAssertions.assertKoanFails; +import static engine.test.runner.RunnerAssertions.assertKoanPass; + +import java.util.List; +import java.util.function.DoubleToIntFunction; +import static engine.Texts.*; + +public class EqualityAssertionsTests { + private static Localizable> CLASS = global(EqualityAssertionsTests.class); + private record ValueData(String methodName, Object expected, Object returnedNotEquals, String codeExpected, String codeNotEquals) { + Object actual() { + return expected instanceof final Localizable localizable ? localizable.get(Locale.en) : expected; + } + } + private static final List VALUE_EXAMPLES = List.of( + new ValueData("returnInt", 3, 4, "3", "4"), + new ValueData("returnDouble", 3.0, 4.0, "3.0", "4.0"), + new ValueData("returnBoolean", false, true, "false", "true"), + new ValueData("returnString", "abc", "def", "\"abc\"", "\"def\""), + new ValueData("returnString", local("english").fr("french"), "english2", "\"english\"", "\"english2\""), + new ValueData("returnIntArray", new int[]{1, 3}, new int[]{1, 2}, "new int[]{1,3}", "new int[]{1,2}") + ); + private static final DoubleToIntFunction EXAMPLE_RANDOM_LOGIC = randomNumber -> (int)Math.floor(randomNumber * 1000); + private static final ResToIntFunction EXAMPLE_MULTIPLE_RANDOM_LOGIC = res -> EXAMPLE_RANDOM_LOGIC.applyAsInt(res.randomNumber(0)) + EXAMPLE_RANDOM_LOGIC.applyAsInt(res.randomNumber(1)); + + public static Object returnNull() { + return null; + } + public static int returnInt(int val) { + return val; + } + public static double returnDouble(double val) { + return val; + } + public static boolean returnBoolean(boolean val) { + return val; + } + public static String returnString(String val) { + return val; + } + public static int[] returnIntArray(int[] val) { + return val; + } + public static int returnRandom() { + final var r = Helpers.random(); + final var res = EXAMPLE_RANDOM_LOGIC.applyAsInt(r); + return res; + } + public static int returnRandoms() { + final var r1 = returnRandom(); + final var r2 = returnRandom(); + return r1 + r2; + } + + public static void whenAssertVariableEquals() { + for(int i=0; i expressionClass(Expression expr) { + final var ctx = context(); + return expr.expressionClass(ctx); + } + + private static Object format(Expression expr) { + return expr.formatSourceCode(Locale.en); + } + + private static ExecutionContext context() { + return new ExecutionContext(global(type(ExpressionTests.class)), Locale.en); + } +} diff --git a/src/main/java/engine/test/FmtTests.java b/src/main/java/engine/test/FmtTests.java new file mode 100644 index 0000000..6afebb6 --- /dev/null +++ b/src/main/java/engine/test/FmtTests.java @@ -0,0 +1,170 @@ +package engine.test; + +import engine.Locale; +import engine.Localizable; + +import static engine.Localizable.local; +import static engine.script.Type.type; +import static engine.Fmt.*; +import static engine.test.runner.RunnerAssertions.assertEquals; + +import java.util.List; + +import engine.Fmt; +import engine.Style; + +public class FmtTests { + private static final Locale DEFAULT_LOCALE = Locale.en; + private static final Localizable SOME_TEXT = local("english").fr("french"); + private static final Localizable SOME_TEMPLATE = local("english(%s, %s)").fr("french(%s, %s)"); + + public static void whenFormatSameStyle() { + assertFormatted("\033[0menglish\033[0m", sameStyle(SOME_TEXT)); + } + + public static void whenFormatSameStyleText() { + assertFormatted("\033[0mabc\033[0m", sameStyle("abc")); + } + + public static void whenFormatNotStyled() { + assertFormatted("\033[0menglish\033[0m", notStyled(SOME_TEXT)); + } + + public static void whenFormatNotStyledText() { + assertFormatted("\033[0mabc\033[0m", notStyled("abc")); + } + + public static void whenFormatRed() { + assertFormatted("\033[0;31menglish\033[0m", red(SOME_TEXT)); + } + + public static void whenFormatRedText() { + assertFormatted("\033[0;31mabc\033[0m", red("abc")); + } + + public static void whenFormatGreenText() { + assertFormatted("\033[0;32menglish\033[0m", green(SOME_TEXT)); + } + + public static void whenFormatCyan() { + assertFormatted("\033[0;36menglish\033[0m", cyan(SOME_TEXT)); + } + + public static void whenFormatCyanText() { + assertFormatted("\033[0;36mabc\033[0m", cyan("abc")); + } + + public static void whenFormatStrong() { + assertFormatted("\033[1menglish\033[0m", strong(SOME_TEXT)); + } + + public static void whenFormatStrongRed() { + assertFormatted("\033[0;31m\033[1menglish\033[0m", strongRed(SOME_TEXT)); + } + + public static void whenFormatNotStyledTemplate() { + assertFormatted("\033[0menglish(\033[0menglish\033[0m, \033[0;31menglish\033[0m)\033[0m", notStyled(SOME_TEMPLATE, notStyled(SOME_TEXT), red(SOME_TEXT))); + } + + public static void whenFormatRedTemplate() { + assertFormatted("\033[0;31menglish(\033[0menglish\033[0;31m, \033[0;31menglish\033[0;31m)\033[0m", red(SOME_TEMPLATE, notStyled(SOME_TEXT), red(SOME_TEXT))); + } + + public static void whenFormatGreenTemplate() { + assertFormatted("\033[0;32menglish(\033[0menglish\033[0;32m, \033[0;31menglish\033[0;32m)\033[0m", green(SOME_TEMPLATE, notStyled(SOME_TEXT), red(SOME_TEXT))); + } + + public static void whenFormatCyanTemplate() { + assertFormatted("\033[0;36menglish(\033[0menglish\033[0;36m, \033[0;31menglish\033[0;36m)\033[0m", cyan(SOME_TEMPLATE, notStyled(SOME_TEXT), red(SOME_TEXT))); + } + + public static void whenFormatStrongTemplate() { + assertFormatted("\033[1menglish(\033[0menglish\033[1m, \033[0;31menglish\033[1m)\033[0m", strong(SOME_TEMPLATE, notStyled(SOME_TEXT), red(SOME_TEXT))); + } + + public static void whenFormatStrongTemplateInFrench() { + assertFormattedInFrench("\033[1mfrench(\033[0mfrench\033[1m, \033[0;31mfrench\033[1m)\033[0m", strong(SOME_TEMPLATE, notStyled(SOME_TEXT), red(SOME_TEXT))); + } + + public static void whenFormatCodeText() { + assertFormatted("\033[38;5;27mnew String()\033[0m", code("new String()")); + } + + public static void whenFormatClassSimpleNameWithAClass() { + assertFormatted("\033[38;5;27mString\033[0m", classSimpleName(String.class)); + } + + public static void whenFormatClassSimpleNameWithAType() { + assertFormatted("\033[38;5;27mString\033[0m", classSimpleName(type(String.class))); + } + + public static void whenFormatClassFullName() { + assertFormatted("\033[38;5;27mjava.lang.String\033[0m", classFullName(String.class)); + } + + public static void whenFormatEmptyDoubleSequence() { + assertFormatted("\033[0m\033[0m", sequence(new double[0], Style.Green)); + } + + public static void whenFormat1DoubleSequence() { + assertFormatted("\033[0m\033[0;32m2.5\033[0m\033[0m", sequence(new double[]{2.5}, Style.Green)); + } + + public static void whenFormat2DoublesSequence() { + assertFormatted("\033[0m\033[0;32m2.5\033[0m and \033[0;32m-1.2\033[0m\033[0m", sequence(new double[]{2.5, -1.2}, Style.Green)); + } + + public static void whenFormatManyDoublesSequence() { + assertFormatted("\033[0m\033[0;32m2.5\033[0m, \033[0;32m-1.2\033[0m, \033[0;32m0.0\033[0m and \033[0;32m3.14\033[0m\033[0m", sequence(new double[]{2.5, -1.2, 0, 3.14}, Style.Green)); + } + + public static void whenFormatManyDoublesSequenceInFrench() { + assertFormattedInFrench("\033[0m\033[0;32m2.5\033[0m, \033[0;32m-1.2\033[0m, \033[0;32m0.0\033[0m et \033[0;32m3.14\033[0m\033[0m", sequence(new double[]{2.5, -1.2, 0, 3.14}, Style.Green)); + } + + public static void whenFormatEmptyStringSequence() { + assertFormatted("\033[0m\033[0m", sequence(new String[0], Style.Green)); + } + + public static void whenFormat1ItemStringSequence() { + assertFormatted("\033[0m\033[0;32ma\033[0m\033[0m", sequence(new String[]{"a"}, Style.Green)); + } + + public static void whenFormat2ItemsStringSequence() { + assertFormatted("\033[0m\033[0;32ma\033[0m and \033[0;32mb\033[0m\033[0m", sequence(new String[]{"a", "b"}, Style.Green)); + } + + public static void whenFormatManyStringsSequence() { + assertFormatted("\033[0m\033[0;32ma\033[0m, \033[0;32mb\033[0m, \033[0;32mc\033[0m and \033[0;32md\033[0m\033[0m", sequence(new String[]{"a", "b", "c", "d"}, Style.Green)); + } + + public static void whenFormatEmptySequence() { + assertFormatted("\033[0m\033[0m", sequence(List.of(), Style.Green)); + } + + public static void whenFormat1ItemSequence() { + assertFormatted("\033[0m\033[0;32menglish\033[0m\033[0m", sequence(List.of(SOME_TEXT), Style.Green)); + } + + public static void whenFormat2ItemsSequence() { + assertFormatted("\033[0m\033[0;32menglish\033[0m and \033[0;32menglish\033[0m\033[0m", sequence(List.of(SOME_TEXT, SOME_TEXT), Style.Green)); + } + + public static void whenFormatManyItemsSequence() { + assertFormatted("\033[0m\033[0;32menglish\033[0m, \033[0;32menglish\033[0m, \033[0;32menglish\033[0m and \033[0;32menglish\033[0m\033[0m", sequence(List.of(SOME_TEXT, SOME_TEXT, SOME_TEXT, SOME_TEXT), Style.Green)); + } + + public static void whenFormatManyItemsSequenceInFrench() { + assertFormattedInFrench("\033[0m\033[0;32mfrench\033[0m, \033[0;32mfrench\033[0m, \033[0;32mfrench\033[0m et \033[0;32mfrench\033[0m\033[0m", sequence(List.of(SOME_TEXT, SOME_TEXT, SOME_TEXT, SOME_TEXT), Style.Green)); + } + + private static void assertFormatted(String expected, Fmt fmt) { + String actual = fmt.format(DEFAULT_LOCALE); + assertEquals(expected, actual); + } + + private static void assertFormattedInFrench(String expected, Fmt fmt) { + String actual = fmt.format(Locale.fr); + assertEquals(expected, actual); + } +} diff --git a/src/main/java/engine/test/ImplementationAssertionsTests.java b/src/main/java/engine/test/ImplementationAssertionsTests.java new file mode 100644 index 0000000..8499628 --- /dev/null +++ b/src/main/java/engine/test/ImplementationAssertionsTests.java @@ -0,0 +1,212 @@ +package engine.test; + +import static engine.Assertions.assertReturnValueImplements; +import static engine.Assertions.assertReturnValueIsAnonymousObject; +import static engine.Assertions.assertReturnValueIsLambda; +import static engine.Fmt.code; +import static engine.Fmt.green; +import static engine.Fmt.red; +import static engine.Localizable.global; + +import java.util.function.IntPredicate; +import engine.Koan; +import engine.Localizable; +import engine.TestSensei; +import engine.test.simulation.SomeInterface; +import engine.test.simulation.StudentSolutions; + +import static engine.script.Expression.callKoanMethod; +import static engine.test.runner.RunnerAssertions.assertKoanFails; +import static engine.test.runner.RunnerAssertions.assertKoanPass; +import static engine.Texts.*; + +public class ImplementationAssertionsTests { + private static Localizable> CLASS = global(StudentSolutions.class); + + public static void whenAssertReturnedValueImplementsInterfaceAndReturnedValueDoesImplementIt() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueImplementsInterfaceAndReturnedValueDoesImplementIt")) + .when( + callKoanMethod("returnedValueImplements") + ) + .then( + assertReturnValueImplements(SomeInterface.class) + ) + ); + + assertKoanPass( + res[0], + green(OK_RETURNED_OBJECT_IMPLEMENTS, code("returnedValueImplements()"), code("engine.test.simulation.SomeInterface")) + ); + } + + public static void whenAssertReturnedValueImplementsInterfaceAndReturnedValueIsNull() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueImplementsInterfaceAndReturnedValueIsNull")) + .when( + callKoanMethod("returnedValueNull") + ) + .then( + assertReturnValueImplements(SomeInterface.class) + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_IMPLEMENTING_BUT_RETURNED_NULL, code("returnedValueNull()"), code("engine.test.simulation.SomeInterface")) + ); + } + + public static void whenAssertReturnedValueImplementsInterfaceAndReturnedValueDoesNotImplementIt() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueImplementsInterfaceAndReturnedValueDoesNotImplementIt")) + .when( + callKoanMethod("returnedValueImplements") + ) + .then( + assertReturnValueImplements(IntPredicate.class) + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_IMPLEMENTING_BUT_NOT, code("returnedValueImplements()"), code("java.util.function.IntPredicate"), code("engine.test.simulation.SomeImplementation")) + ); + } + + public static void whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsOne() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsOne")) + .when( + callKoanMethod("returnedValueAnonymousImplementation") + ) + .then( + assertReturnValueIsAnonymousObject() + ) + ); + + assertKoanPass( + res[0], + green(OK_RETURNED_OBJECT_IS_ANONYMOUS, code("returnedValueAnonymousImplementation()")) + ); + } + + public static void whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsNull() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsNull")) + .when( + callKoanMethod("returnedValueNull") + ) + .then( + assertReturnValueIsAnonymousObject() + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED_NULL, code("returnedValueNull()")) + ); + } + + public static void whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsALambda() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsALambda")) + .when( + callKoanMethod("returnedValueLambda") + ) + .then( + assertReturnValueIsAnonymousObject() + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED_LAMBDA, code("returnedValueLambda()")) + ); + } + + public static void whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsRegularClass() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsAnonymousClassAndReturnedValueIsRegularClass")) + .when( + callKoanMethod("returnedValueImplements") + ) + .then( + assertReturnValueIsAnonymousObject() + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_ANONYMOUS_BUT_RETURNED, code("returnedValueImplements()"), code("engine.test.simulation.SomeImplementation")) + ); + } + + public static void whenAssertReturnedValueIsLambdaAndReturnedValueIsOne() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsLambdaAndReturnedValueIsOne")) + .when( + callKoanMethod("returnedValueLambda") + ) + .then( + assertReturnValueIsLambda() + ) + ); + + assertKoanPass( + res[0], + green(OK_RETURNED_OBJECT_IS_LAMBDA, code("returnedValueLambda()")) + ); + } + + public static void whenAssertReturnedValueIsLambdaAndReturnedValueIsNull() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsLambdaAndReturnedValueIsNull")) + .when( + callKoanMethod("returnedValueNull") + ) + .then( + assertReturnValueIsLambda() + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED_NULL, code("returnedValueNull()")) + ); + } + + public static void whenAssertReturnedValueIsLambdaAndReturnedValueIsAnonymousClass() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsLambdaAndReturnedValueIsAnonymousClass")) + .when( + callKoanMethod("returnedValueAnonymousImplementation") + ) + .then( + assertReturnValueIsLambda() + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED_ANONYMOUS, code("returnedValueAnonymousImplementation()")) + ); + } + + public static void whenAssertReturnedValueIsLambdaAndReturnedValueIsRegularClass() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertReturnedValueIsLambdaAndReturnedValueIsRegularClass")) + .when( + callKoanMethod("returnedValueImplements") + ) + .then( + assertReturnValueIsLambda() + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_RETURN_LAMBDA_BUT_RETURNED, code("returnedValueImplements()"), code("engine.test.simulation.SomeImplementation")) + ); + } +} diff --git a/src/main/java/engine/test/Line.java b/src/main/java/engine/test/Line.java deleted file mode 100644 index 77fcbf5..0000000 --- a/src/main/java/engine/test/Line.java +++ /dev/null @@ -1,43 +0,0 @@ -package engine.test; - -import engine.Locale; -import engine.Localizable; - -public sealed interface Line { - String resolve(Locale locale); - - public static final class Empty implements Line { - @Override - public String resolve(final Locale _locale) { - return ""; - } - } - - public static final class Formatted implements Line { - private final String resolved; - - public Formatted(final String template, final String... params) { - this.resolved = String.format(template, (Object[])params); - } - - @Override - public String resolve(Locale locale) { - return resolved; - } - } - - public static final class Localized implements Line { - private final Localizable template; - private final String[] params; - - public Localized(final Localizable template, final String... params) { - this.template = template; - this.params = params; - } - - @Override - public String resolve(final Locale locale) { - return String.format(template.get(locale), (Object[])params); - } - } -} diff --git a/src/main/java/engine/test/ReflectionAssertionsTests.java b/src/main/java/engine/test/ReflectionAssertionsTests.java new file mode 100644 index 0000000..6c18a6f --- /dev/null +++ b/src/main/java/engine/test/ReflectionAssertionsTests.java @@ -0,0 +1,797 @@ +package engine.test; + +import static engine.Assertions.assertImplementsInterface; +import static engine.Assertions.assertKoanMethodIsInvokable; +import static engine.Assertions.assertObjectMethodIsInvokable; +import static engine.Assertions.assertPrivateField; +import static engine.Assertions.assertPrivateFinalField; +import static engine.Assertions.assertStaticMethodIsInvokable; +import static engine.Assertions.assertCanInstantiateClass; +import static engine.Assertions.assertConstructorIsInvokable; +import static engine.Localizable.global; + +import java.util.List; +import java.util.function.IntConsumer; + +import engine.Style; +import engine.Koan; +import engine.Localizable; +import engine.TestSensei; +import engine.test.simulation.SomeInterface; +import engine.test.simulation.StudentSolutions; +import static engine.Fmt.code; +import static engine.Fmt.red; +import static engine.Fmt.sameStyle; +import static engine.Fmt.sequence; +import static engine.script.Expression.callKoanMethod; +import static engine.script.Type.type; +import static engine.test.runner.RunnerAssertions.assertKoanFails; +import static engine.test.runner.RunnerAssertions.assertKoanPass; +import static engine.Texts.*; + +public class ReflectionAssertionsTests { + private static Localizable> CLASS = global(StudentSolutions.class); + + public static void whenAssertClassImplementsInterfaceAndItDoes() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassImplementsInterfaceAndItDoes")) + .beforeFirstTest( + assertImplementsInterface("engine.test.simulation.SomeImplementation", SomeInterface.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertClassImplementsInterfaceAndItDoesNot() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassImplementsInterfaceAndItDoesNot")) + .beforeFirstTest( + assertImplementsInterface("engine.test.simulation.SomeImplementation", IntConsumer.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_CLASS_TO_IMPLEMENT, code("engine.test.simulation.SomeImplementation"), code("java.util.function.IntConsumer")) + ); + } + + public static void whenAssertStaticMethodIsInvokableAndMethodDoesExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodIsInvokableAndMethodDoesExist")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "simpleConsoleOutput") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertStaticMethodIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertStaticMethodIsInvokableAndMethodIsNotStatic() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodIsInvokableAndMethodIsNotStatic")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "nonStaticMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_METHOD_TO_BE_STATIC, sameStyle("nonStaticMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertStaticMethodNoParamIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodNoParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertStaticMethodOneParamIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodOneParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod", int.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_ONE_PARAM, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions"), code("int")) + ); + } + + public static void whenAssertStaticMethodMultipleParamsIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodOneParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod", int.class, double.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_MANY_PARAMS, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions"), sequence(List.of(global("int"), global("double")), Style.Code)) + ); + } + + public static void whenAssertStaticMethodIsPublicAndItIsNot() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodIsPublicAndItIsNot")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "privateMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("privateMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertStaticMethodIsInvokableAndClassDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertStaticMethodIsInvokableAndClassDoesNotExist")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.NoClass", "noMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE, code("NoClass"), code("engine.test.simulation")) + ); + } + + public static void whenAssertKoanMethodIsInvokableAndMethodDoesExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertKoanMethodIsInvokableAndMethodDoesExist")) + .beforeFirstTest( + assertKoanMethodIsInvokable("simpleConsoleOutput") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertKoanMethodIsInvokableAndMethodIsNotStatic() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertKoanMethodIsInvokableAndMethodIsNotStatic")) + .beforeFirstTest( + assertKoanMethodIsInvokable("nonStaticMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_METHOD_TO_BE_STATIC, sameStyle("nonStaticMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertKoanMethodNoParamIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertKoanMethodNoParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertKoanMethodIsInvokable("noMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertKoanMethodOneParamIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertKoanMethodOneParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertStaticMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod", int.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_ONE_PARAM, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions"), code("int")) + ); + } + + public static void whenAssertKoanMethodMultipleParamsIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertKoanMethodOneParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertKoanMethodIsInvokable("noMethod", int.class, double.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red( + EXPECTED_TO_FIND_MEHOD_MANY_PARAMS, + sameStyle("noMethod"), + sameStyle("engine/test/simulation/StudentSolutions"), + sequence(List.of(global("int"), global("double")), Style.Code) + ) + ); + } + + public static void whenAssertKoanMethodIsPublicAndItIsNot() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertKoanMethodIsPublicAndItIsNot")) + .beforeFirstTest( + assertKoanMethodIsInvokable("privateMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("privateMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertObjectMethodIsInvokableAndMethodDoesExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodIsInvokableAndMethodDoesExist")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.StudentSolutions", "nonStaticMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertObjectMethodIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertObjectMethodIsInvokableAndMethodIsStatic() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodIsInvokableAndMethodIsNotStatic")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.StudentSolutions", "doNothing") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_METHOD_TO_NOT_BE_STATIC, sameStyle("doNothing"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertObjectMethodNoParamIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodNoParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertObjectMethodOneParamIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodOneParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod", int.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_ONE_PARAM, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions"), code("int")) + ); + } + + public static void whenAssertObjectMethodMultipleParamsIsInvokableAndMethodDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodOneParamIsInvokableAndMethodDoesNotExist")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.StudentSolutions", "noMethod", int.class, double.class) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_MANY_PARAMS, sameStyle("noMethod"), sameStyle("engine/test/simulation/StudentSolutions"), sequence(List.of(global("int"), global("double")), Style.Code)) + ); + } + + public static void whenAssertObjectMethodIsInvokableAndItIsNotPublic() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodIsInvokableAndItIsNotPublic")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.StudentSolutions", "nonStaticPrivateMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_MEHOD_NO_PARAMS, sameStyle("nonStaticPrivateMethod"), sameStyle("engine/test/simulation/StudentSolutions")) + ); + } + + public static void whenAssertObjectMethodIsInvokableAndClassDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertObjectMethodIsInvokableAndClassDoesNotExist")) + .beforeFirstTest( + assertObjectMethodIsInvokable("engine.test.simulation.NoClass", "noMethod") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE, code("NoClass"), code("engine.test.simulation")) + ); + } + + public static void whenAssertClassIsInstantiableAndItIs() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassIsInstantiableAndItIs")) + .beforeFirstTest( + assertCanInstantiateClass(global(type("engine.test.simulation.StudentSolutions"))) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertClassIsInstantiableAndItDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassIsInstantiableAndItDoesNotExist")) + .beforeFirstTest( + assertCanInstantiateClass(global(type("engine.test.simulation.Unkown"))) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_A_CLASS_IN_THE_PACKAGE, code("Unkown"), code("engine.test.simulation")) + ); + } + + public static void whenAssertClassIsInstantiableAndItIsInterface() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassIsInstantiableAndItIsInterface")) + .beforeFirstTest( + assertCanInstantiateClass(global(type("engine.test.simulation.SomeInterface"))) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_CLASS_TO_BE_INSTANTIABLE, code("SomeInterface")) + ); + } + + public static void whenAssertClassIsInstantiableAndItIsAbstract() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassIsInstantiableAndItIsAbstract")) + .beforeFirstTest( + assertCanInstantiateClass(global(type("engine.test.simulation.AbstractClass"))) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_CLASS_TO_BE_INSTANTIABLE, code("AbstractClass")) + ); + } + + public static void whenAssertClassIsInstantiableAndItIsPrimitive() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassIsInstantiableAndItIsPrimitive")) + .beforeFirstTest( + assertCanInstantiateClass(global(type(int.class))) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_CLASS_TO_BE_INSTANTIABLE, code("int")) + ); + } + + public static void whenAssertClassIsInstantiableAndItIsArray() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassIsInstantiableAndItIsArray")) + .beforeFirstTest( + assertCanInstantiateClass(global(type(String[].class))) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_CLASS_TO_BE_INSTANTIABLE, code("String[]")) + ); + } + + public static void whenAssertClassIsInstantiableAndItIsVoid() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertClassIsInstantiableAndItIsVoid")) + .beforeFirstTest( + assertCanInstantiateClass(global(type(void.class))) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_CLASS_TO_BE_INSTANTIABLE, code("void")) + ); + } + + public static void whenAssertConstructorIsInvokableAndItIs() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertConstructorIsInvokableAndItIs")) + .beforeFirstTest( + assertConstructorIsInvokable("engine.test.simulation.StudentSolutions") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertConstructorIsInvokableAndItIsPrivate() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertConstructorIsInvokableAndItIsPrivate")) + .beforeFirstTest( + assertConstructorIsInvokable("engine.test.simulation.NoPublicConstructor") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_CONSTRUCTOR_NO_PARAMS, code("NoPublicConstructor")) + ); + } + + public static void whenAssertConstructorNoParamsIsInvokableAndItNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertConstructorNoParamsIsInvokableAndItNotExist")) + .beforeFirstTest( + assertConstructorIsInvokable("engine.test.simulation.ManyParamsConstructor") + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_CONSTRUCTOR_NO_PARAMS, code("ManyParamsConstructor")) + ); + } + + public static void whenAssertConstructorOneParamIsInvokableAndItNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertConstructorOneParamIsInvokableAndItNotExist")) + .beforeFirstTest( + assertConstructorIsInvokable("engine.test.simulation.ManyParamsConstructor", type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_CONSTRUCTOR_ONE_PARAM, code("ManyParamsConstructor"), code("int")) + ); + } + + public static void whenAssertConstructorManyParamsIsInvokableAndItNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertConstructorManyParamsIsInvokableAndItNotExist")) + .beforeFirstTest( + assertConstructorIsInvokable("engine.test.simulation.ManyParamsConstructor", type(int.class), type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_CONSTRUCTOR_MANY_PARAMS, code("ManyParamsConstructor"), sequence(List.of(global("int"), global("int")), Style.Code)) + ); + } + + public static void whenAssertPrivateFinalFieldAndItIs() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFinalFieldAndItIs")) + .beforeFirstTest( + assertPrivateFinalField("engine.test.simulation.StudentSolutions", "privateFinalField", type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertPrivateFinalFieldAndItIsPublic() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFinalFieldAndItIsPublic")) + .beforeFirstTest( + assertPrivateFinalField("engine.test.simulation.StudentSolutions", "publicFinalField", type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_FIELD_TO_BE_PRIVATE, code("publicFinalField"), code("engine.test.simulation.StudentSolutions")) + ); + } + + public static void whenAssertPrivateFinalFieldAndItIsNotFinal() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFinalFieldAndItIsNotFinal")) + .beforeFirstTest( + assertPrivateFinalField("engine.test.simulation.StudentSolutions", "privateField", type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_FIELD_TO_BE_FINAL, code("privateField"), code("engine.test.simulation.StudentSolutions")) + ); + } + + public static void whenAssertPrivateFinalFieldAndItIsNotRightType() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFinalFieldAndItIsNotRightType")) + .beforeFirstTest( + assertPrivateFinalField("engine.test.simulation.StudentSolutions", "privateFinalField", type(boolean.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_FIELD_TO_BE_OF_TYPE, code("privateFinalField"), code("engine.test.simulation.StudentSolutions"), code("boolean"), code("int")) + ); + } + + public static void whenAssertPrivateFinalFieldAndItDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFinalFieldAndItDoesNotExist")) + .beforeFirstTest( + assertPrivateFinalField("engine.test.simulation.StudentSolutions", "unknownField", type(boolean.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_FIELD_IN_CLASS, code("unknownField"), code("engine.test.simulation.StudentSolutions")) + ); + } + + public static void whenAssertPrivateFieldAndItIs() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFieldAndItIs")) + .beforeFirstTest( + assertPrivateField("engine.test.simulation.StudentSolutions", "privateField", type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanPass(res[0]); + } + + public static void whenAssertPrivateFieldAndItIsPublic() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFieldAndItIsPublic")) + .beforeFirstTest( + assertPrivateField("engine.test.simulation.StudentSolutions", "publicField", type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_FIELD_TO_BE_PRIVATE, code("publicField"), code("engine.test.simulation.StudentSolutions")) + ); + } + + public static void whenAssertPrivateFieldAndItIsFinal() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFieldAndItIsFinal")) + .beforeFirstTest( + assertPrivateField("engine.test.simulation.StudentSolutions", "privateFinalField", type(int.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_FIELD_TO_NOT_BE_FINAL, code("privateFinalField"), code("engine.test.simulation.StudentSolutions")) + ); + } + + public static void whenAssertPrivateFieldAndItIsNotRightType() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFieldAndItIsNotRightType")) + .beforeFirstTest( + assertPrivateField("engine.test.simulation.StudentSolutions", "privateField", type(boolean.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_FIELD_TO_BE_OF_TYPE, code("privateField"), code("engine.test.simulation.StudentSolutions"), code("boolean"), code("int")) + ); + } + + public static void whenAssertPrivateFieldAndItDoesNotExist() { + var res = TestSensei.execute( + new Koan(CLASS, global("whenAssertPrivateFieldAndItDoesNotExist")) + .beforeFirstTest( + assertPrivateField("engine.test.simulation.StudentSolutions", "unknownField", type(boolean.class)) + ) + .when( + callKoanMethod("doNothing") + ) + ); + + assertKoanFails( + res[0], + red(EXPECTED_TO_FIND_FIELD_IN_CLASS, code("unknownField"), code("engine.test.simulation.StudentSolutions")) + ); + } +} diff --git a/src/main/java/engine/test/TestRunner.java b/src/main/java/engine/test/TestRunner.java deleted file mode 100644 index 54115a6..0000000 --- a/src/main/java/engine/test/TestRunner.java +++ /dev/null @@ -1,61 +0,0 @@ -package engine.test; - -import java.util.List; -import java.util.Map; - -import engine.ConsoleFmt; -import engine.TestSensei; -import engine.TestSensei.TestResult; - -public class TestRunner { - private static final List> TO_RUN = List.of( - ConsoleUnitTests.items - ); - private static final Map SUCCESS_WORDING = Map.of( - true, "success", - false, "failure" - ); - private static final String RED_FAILURE = ConsoleFmt.red("FAILURE"); - - public static void main(String[] args) { - var totalCount = 0; - var successCount = 0; - - for(var unitTestSeries: TO_RUN) { - for(var unitTest: unitTestSeries) { - var actualResults = TestSensei.execute(unitTest.koan()); - for(int i=0; i output = new ArrayList<>(); + + public CapturingPrinter(final Locale locale) { + this.locale = locale; + } + + @Override + public boolean hasCaptured(final Fmt... lines) { + return output.equals( + Arrays + .stream(lines) + .map(line -> line.format(locale)) + .toList() + ); + } + + @Override + public String capturedOutputAsString() { + final var res = new StringBuilder(); + for(final var line: output) { + res.append(line); + res.append(System.lineSeparator()); + } + return res.toString(); + } + + @Override + public void println() { + output.add(""); + } + + @Override + public void println(Fmt fmt) { + output.add(fmt.format(locale)); + } + + @Override + public void println(final String template, final Object... params) { + output.add(String.format(template, params)); + } + + @Override + public void println(final Localizable template, final Object... params) { + println(template.get(locale), params); + } +} diff --git a/src/main/java/engine/test/runner/OutputCapture.java b/src/main/java/engine/test/runner/OutputCapture.java new file mode 100644 index 0000000..0b54ff0 --- /dev/null +++ b/src/main/java/engine/test/runner/OutputCapture.java @@ -0,0 +1,8 @@ +package engine.test.runner; + +import engine.Fmt; + +public interface OutputCapture { + boolean hasCaptured(final Fmt... lines); + String capturedOutputAsString(); +} diff --git a/src/main/java/engine/test/runner/RunnerAssertions.java b/src/main/java/engine/test/runner/RunnerAssertions.java new file mode 100644 index 0000000..510a639 --- /dev/null +++ b/src/main/java/engine/test/runner/RunnerAssertions.java @@ -0,0 +1,120 @@ +package engine.test.runner; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Consumer; + +import engine.Fmt; +import engine.TestSensei.TestResult; + +public class RunnerAssertions { + private static final Map SUCCESS_WORDING = Map.of( + true, "PASS", + false, "FAIL" + ); + + public static class AssertionFailure extends RuntimeException { + public AssertionFailure(final String msg) { + super(msg); + } + } + + public static void fail(final String msg, final Object... args) { + throw new AssertionFailure(String.format(msg, args)); + } + + public static void assertTrue(final boolean condition, final String msg, Object... args) { + if (!condition) { + fail(msg, args); + } + } + + private static String format(final int[] array) { + return String.join( + ",", + Arrays.stream(array).mapToObj(Integer::toString).toArray(String[]::new) + ); + } + + public static void assertEquals(final Class expected, final Object actual) { + assertTrue(actual != null, "Expected %s but got null", expected.getName()); + assertTrue(actual instanceof Class, "Expected %s but got a %s", expected.getName(), actual.getClass().getName()); + assertTrue(expected.equals(actual), "%s is not equal to expected %s", ((Class)actual).getName(), expected.getName()); + } + + public static void assertEquals(final String expected, final Object actual) { + assertTrue(actual != null, "Expected \"%s\" but got null", expected); + assertTrue(actual instanceof String, "Expected \"%s\" but got a %s", expected, actual.getClass().getName()); + assertTrue(expected.equals(actual), "\"%s\" is not equal to expected \"%s\"", actual, expected); + } + + public static void assertEquals(final int expected, final Object actual) { + assertTrue(actual != null, "Expected %s but got null", expected); + assertTrue(actual instanceof Integer, "Expected %s but got a %s", expected, actual.getClass().getName()); + assertTrue(expected == (int)actual, "%s is not equal to expected %s", actual, expected); + } + + public static void assertEquals(final double expected, final Object actual) { + assertTrue(actual != null, "Expected %s but got null", expected); + assertTrue(actual instanceof Double, "Expected %s but got a %s", expected, actual.getClass().getName()); + assertTrue(expected == (double)actual, "%s is not equal to expected %s", actual, expected); + } + + public static void assertEquals(final boolean expected, final Object actual) { + assertTrue(actual != null, "Expected %s but got null", expected); + assertTrue(actual instanceof Boolean, "Expected %s but got a %s", expected, actual.getClass().getName()); + assertTrue(expected == (boolean)actual, "%s is not equal to expected %s", actual, expected); + } + + public static void assertEquals(final int[] expected, final Object actual) { + final var expectedFmted = format(expected); + assertTrue(actual != null, "Expected %s but got null", expectedFmted); + assertTrue(actual instanceof int[], "Expected %s but got a %s", expectedFmted, actual.getClass().getName()); + assertTrue(Arrays.equals(expected, (int[])actual), "%s is not equal to expected %s", format((int[])actual), expectedFmted); + } + + public static void assertKoanPass(final TestResult[] results) { + assertTrue(results.length == 0, "Expected koan to not have any assertions."); + } + + public static void assertKoanPass(final TestResult actual, final Fmt... expectedConsoleLines) { + assertResult(true, actual, expectedConsoleLines); + } + + public static void assertKoanFails(final TestResult actual, final Fmt... expectedConsoleLines) { + assertResult(false, actual, expectedConsoleLines); + } + + private static void assertResult(final boolean expectedSucceeded, final TestResult actual, final Fmt... expectedConsoleLines) { + if (expectedSucceeded != actual.succeeded()) { + fail(String.format( + "%s: expected a %s but got a %s%n%s", + actual, + SUCCESS_WORDING.get(expectedSucceeded), + SUCCESS_WORDING.get(actual.succeeded()), + outputDiff(expectedConsoleLines, actual) + )); + } + if (!actual.hasCaptured(expectedConsoleLines)) { + fail(String.format( + "%s: output differ from expected%n%s", + actual, + outputDiff(expectedConsoleLines, actual) + )); + } + } + + private static String outputDiff(final Fmt[] expectedOutput, final TestResult actual) { + final var diff = new StringBuilder(); + final Consumer println = (s) -> diff.append(String.format("%s%n", s)); + + println.accept("Expected output:"); + for(final var line: expectedOutput) { + println.accept(line.format(actual.locale())); + } + println.accept("Actual output:"); + diff.append(actual.output().capturedOutputAsString()); + + return diff.toString(); + } +} diff --git a/src/main/java/engine/test/runner/TestRunner.java b/src/main/java/engine/test/runner/TestRunner.java new file mode 100644 index 0000000..7043d5d --- /dev/null +++ b/src/main/java/engine/test/runner/TestRunner.java @@ -0,0 +1,102 @@ +package engine.test.runner; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +import engine.Fmt; +import engine.Locale; +import engine.test.runner.RunnerAssertions.AssertionFailure; + +/** + * This is a stripped down, poor's version of JUnit, since we don't have access to any dependency. + */ +public class TestRunner { + private static final String RED_FAILURE = Fmt.red("FAILURE").format(Locale.en); + + public static void main(final String[] args) { + var totalCount = 0; + var successCount = 0; + + for(final var testSuite: findTestSuites()) { + for(final var test: testSuite) { + try { + test.run(); + successCount++; + } catch(final AssertionFailure _af) { + // Nothing else to do other than not increasing successCount, since feedback is already given to the console by the test. + } + totalCount ++; + } + } + + System.out.printf("%d/%d tests passed.%n", successCount, totalCount); + } + + private static List> findTestSuites() { + try(final var packageStream = TestRunner.class.getClassLoader().getResourceAsStream("engine/test")) { + try(final var reader = new BufferedReader(new InputStreamReader(packageStream))) { + return reader + .lines() + .filter(line -> line.endsWith("Tests.class")) + .map(TestRunner::loadTestClass) + .map(TestRunner::findTestsInClass) + .toList(); + } + } catch(final IOException ioe) { + throw new RuntimeException("Cannot locate unit tests", ioe); + } + } + + private static Class loadTestClass(final String testClassSimpleName) { + final var className = "engine.test." + testClassSimpleName.substring(0, testClassSimpleName.lastIndexOf(".")); + try { + return Class.forName(className); + } catch(final ClassNotFoundException cnfe) { + throw new RuntimeException(String.format("Cannot load %s", className), cnfe); + } + } + + private static boolean isTestMethod(final Method m) { + final var modifiers = m.getModifiers(); + return + Modifier.isStatic(modifiers) && + Modifier.isPublic(modifiers) && + m.getParameters().length == 0 && + m.getName().startsWith("when"); // Small constraint on test method name, avoid having to define an @Test annotation. + } + + private static Runnable toTest(final Method m) { + return () -> { + try { + m.invoke(null); + } catch(final IllegalAccessException _iae) { + throw new RuntimeException("We should have verified in isTestMethod() that the method is accessible"); + } catch(final IllegalArgumentException _iae) { + throw new RuntimeException("We should have verified in isTestMethod() that the method has no argument"); + } catch(final InvocationTargetException ite) { + if (ite.getCause() instanceof final AssertionFailure af) { + System.out.printf("%s %s.%s%n%s%n", RED_FAILURE, m.getDeclaringClass().getName(), m.getName(), af.getMessage()); + } + if (ite.getCause() instanceof final RuntimeException re) { + throw re; + } else { + throw new RuntimeException(ite); + } + } + }; + } + + private static List findTestsInClass(final Class clasz) { + return Arrays + .stream(clasz.getMethods()) + .filter(TestRunner::isTestMethod) + .map(TestRunner::toTest) + .toList(); + } +} diff --git a/src/main/java/engine/test/simulation/AbstractClass.java b/src/main/java/engine/test/simulation/AbstractClass.java new file mode 100644 index 0000000..9ce8352 --- /dev/null +++ b/src/main/java/engine/test/simulation/AbstractClass.java @@ -0,0 +1,5 @@ +package engine.test.simulation; + +public abstract class AbstractClass { + +} diff --git a/src/main/java/engine/test/simulation/ManyParamsConstructor.java b/src/main/java/engine/test/simulation/ManyParamsConstructor.java new file mode 100644 index 0000000..4f71001 --- /dev/null +++ b/src/main/java/engine/test/simulation/ManyParamsConstructor.java @@ -0,0 +1,7 @@ +package engine.test.simulation; + +public class ManyParamsConstructor { + public ManyParamsConstructor(int a, int b, int c) { + + } +} diff --git a/src/main/java/engine/test/simulation/NoPublicConstructor.java b/src/main/java/engine/test/simulation/NoPublicConstructor.java new file mode 100644 index 0000000..d4c51f2 --- /dev/null +++ b/src/main/java/engine/test/simulation/NoPublicConstructor.java @@ -0,0 +1,7 @@ +package engine.test.simulation; + +public class NoPublicConstructor { + private NoPublicConstructor() { + + } +} diff --git a/src/main/java/engine/test/simulation/SomeImplementation.java b/src/main/java/engine/test/simulation/SomeImplementation.java new file mode 100644 index 0000000..82a86bb --- /dev/null +++ b/src/main/java/engine/test/simulation/SomeImplementation.java @@ -0,0 +1,8 @@ +package engine.test.simulation; + +public class SomeImplementation implements SomeInterface { + @Override + public int op(int a, int b) { + return a + b; + } +} diff --git a/src/main/java/engine/test/simulation/SomeInterface.java b/src/main/java/engine/test/simulation/SomeInterface.java new file mode 100644 index 0000000..52c5c7f --- /dev/null +++ b/src/main/java/engine/test/simulation/SomeInterface.java @@ -0,0 +1,5 @@ +package engine.test.simulation; + +public interface SomeInterface { + int op(int a, int b); +} diff --git a/src/main/java/engine/test/simulation/StudentSolutions.java b/src/main/java/engine/test/simulation/StudentSolutions.java index f92c074..5a68158 100644 --- a/src/main/java/engine/test/simulation/StudentSolutions.java +++ b/src/main/java/engine/test/simulation/StudentSolutions.java @@ -1,7 +1,63 @@ package engine.test.simulation; +import engine.Helpers; + public class StudentSolutions { + @SuppressWarnings("unused") + private final int privateFinalField = 0; + public final int publicFinalField = 0; + @SuppressWarnings("unused") + private int privateField = 0; + public int publicField = 0; + + public static void doNothing() { + } + public static void simpleConsoleOutput() { System.out.println("hello"); } -} \ No newline at end of file + + public static void emptyConsoleOutput() { + System.out.println(); + } + + public static void readFromConsole() { + Helpers.readLine(); + } + + public static int[] returnedValueNull() { + return null; + } + + public static SomeInterface returnedValueImplements() { + return new SomeImplementation(); + } + + public static SomeInterface returnedValueAnonymousImplementation() { + return new SomeInterface() { + @Override + public int op(int a, int b) { + return a+b; + } + }; + } + + public static SomeInterface returnedValueLambda() { + return (a, b) -> a + b; + } + + public void nonStaticMethod() { + } + + public void nonStaticMethodOneParam(int x) { + } + + public void nonStaticMethodMoreParam(int x, double y) { + } + + static void nonStaticPrivateMethod() { + } + + static void privateMethod() { + } +} diff --git a/src/main/java/koans/english/AboutArrays.java b/src/main/java/koans/english/AboutArrays.java new file mode 100644 index 0000000..04d7efb --- /dev/null +++ b/src/main/java/koans/english/AboutArrays.java @@ -0,0 +1,100 @@ +package koans.english; + +public class AboutArrays { + /** + * # Indexing an array + * + * Define the method 'valueAtIndex' which as a parameter for an interger array and an index, and returns the value at that index + * + * --------- TIPS -------------- + * + * Arrays are used to store multiple values in a single variable, instead of declaring separate variables for each value. + * + * In Java arrays can be formed from any data type, but all values in the array must be the same type. They are defined + * by listing the type followed by '[]' and then a name for the variable. + * + * type brackets name + * v v v + * String[] cars; + * + * Values can be add into the array using a comma seperated list and are fixed in size when they are defined. + * + * String[] cars = {"BMW", "Ford", "Tesla"}; + * + * Array can be created using any type and are defined the same way. + * + * int[] nums = {10, 20, 30, 40, 50}; + * + * But whats the point of storing the values in these lists. Glad you asked! You can access these values by referencing the + * name of the array and the index at which the value is stored. However there is one trick to watch out for. Most programming langauges + * start the index count from 0. So that means the first value of the array is actaully at index 0 and not index 1; + * + * nums[0] -> 10; + * cars[2] -> "Tesla" + * + * ------------------------------- + * + * Expected result: + * + * valueAtIndex({20, 15, 10, 5, 0}, 1) should return 15; + * + */ + + public static int valueAtIndex(int[] array, int index){ + return 0; + } + + + /** + * # Searching an Array + * + * Define the method 'indexOfValue' which as a parameter for an interger array and an value, and returns the index of that value. + * - If the value cannot be found in the array return -1. + * - The method should return the first instance of the number if it happens to appear more than once in the array. + * + * --------- TIPS -------------- + * + * Arrays have helpful properties that you can access to get information about their size. This is very helpful for looping through array if + * their size is unknown. You can use the 'length' property to get the size of an array. Remeber though that the index counting may start + * from 0 but that doesn't mean you starting count from 1 for the size. + * + * int[] nums = {5, 10, 15, 20, 25}; + * int length = nums.length; <---- length will equal 5 + * + * ------------------------------ + * + * THE ARRAYS ARE NOT SORTED + * + * Expected result: + * indexOfValue({20, 10, 15, 5, 25}, 3) should return 5; + * + */ + public static int indexOfValue(int[] array, int value){ + return 0; + } + + /** + * # Sorting an Array + * + * Define the method 'sortArray' which as a parameter fo an interger array, and returns the array sorted in rising order + * + * --------- TIPS -------------- + * + * Sometimes it is helpful to sort an array for easier access or searching. In order to do that we will need to be able to edit already existing + * array values. Lucky for us there is away to do that. + * + * int[] nums = {10, 11, 12, 13, 15}; + * nums[4] = 14; <---- nums now equals {10, 11, 12, 13, 14} + * + * ------------------------------- + * + * Expected result: + * sortArray({20, 10, 15, 5, 25}) should return {5, 10, 15, 20, 25}; + * + */ + + public static int[] sortArray(int[] array){ + return array; + } + +} diff --git a/src/main/java/koans/english/AboutConditions.java b/src/main/java/koans/english/AboutConditions.java index b228ac6..a92d7a1 100644 --- a/src/main/java/koans/english/AboutConditions.java +++ b/src/main/java/koans/english/AboutConditions.java @@ -57,6 +57,8 @@ public class AboutConditions { * return 0; // Stop the method execution right there and return 0. * } * + * Note 3: within the curly brackets of a 'if' or 'else', you can write all the code you want, including other 'if' / 'else' !. + * * ------------------------------- * * Expected result: diff --git a/src/main/java/koans/english/AboutConsoleAndVariables.java b/src/main/java/koans/english/AboutConsoleAndVariables.java index 5a1398a..656937f 100644 --- a/src/main/java/koans/english/AboutConsoleAndVariables.java +++ b/src/main/java/koans/english/AboutConsoleAndVariables.java @@ -135,7 +135,7 @@ public static void createAndDisplayAStringVariable() { * * String someText = readLine(); * ^ - * cet appel de méthode va être remplacé par son résultat + * this method call will be replaced by its result * * Will become the equivalent to: * diff --git a/src/main/java/koans/english/AboutLoops.java b/src/main/java/koans/english/AboutLoops.java index 9dea4c9..e20dc00 100644 --- a/src/main/java/koans/english/AboutLoops.java +++ b/src/main/java/koans/english/AboutLoops.java @@ -1,15 +1,42 @@ package koans.english; public class AboutLoops { - /** + /** * # First loop * - * Write a method named 'helloNTimes' which has an integer parameter 'times' and displays 'Hello' in the console times times. + * Write a method named 'helloNTimes' which has an integer parameter 'times' and displays 'Hello' in the console 'times' times. * * --------- TIPS -------------- * - * To do things multiple times in Java, you can use the 'while' loop. - * A while loop looks a lot like an if condition, except it will execute its block of code again and again while the condition stays true. Ex: + * To do things multiple times in Java, you can use the 'for' loop. While the condition is true the code + * within the loop will run on repeat. + * A for loop contains 3 attributes, Initialization, Condition, and Update + * + * - Initialization: This is a line of code that is run when the for loop starts. This is only run once and + * and typically is used to set the initial state of the itteration variable + * + * - Condition: This is a condition that is checked at the start of each cycle of the loop. + * If the condition is met it will end the loop before running the code inside again. + * The condition typically includes some sort of logic check against the itteration variable. + * + * - Update: This is a line of code that is after each cycle of the loop. It is typically used to update the + * itteration variable. + * + * Ex. + * + * for(int times = 0; times < 3; times++) { + * // It will take 3 executions of this block of code before the condition becomes true. + * // So Java will execute it 3 times, and then move on to the rest of the code. + * System.out.println("Still executing"); + * } + * + * There is another loop type 'while' that can be used to repeat sections of code however it is dangerous + * especially in robot code. A while loop will run forever until it's condition is met and this could cause + * your code to lock up if the condition is not met when expected. + * + * A while loop looks a lot like an if condition, except it will execute its block of code again and again while the condition stays true. + * + * Ex: * * int times = 3; * while (times > 0) { @@ -20,6 +47,13 @@ public class AboutLoops { * times = times -1; * } * + * Note 3: like of the 'if', within the curly brackets of a 'while', you can write any code, including other 'while' and 'if' !. + * + * ------------------------------- + * + * PLEASE ATTEMPT TO DO ALL EXERCIES USING FOR LOOPS + * REFRAIN FROM USING WHILE LOOPS + * * ------------------------------- * * Expected result: diff --git a/src/main/java/koans/english/AboutMethods.java b/src/main/java/koans/english/AboutMethods.java index 54aad27..5b0bc1a 100644 --- a/src/main/java/koans/english/AboutMethods.java +++ b/src/main/java/koans/english/AboutMethods.java @@ -169,6 +169,10 @@ public static void computeAgeIn5And10And20YearsConsoleWithMethod() { * Expected result: * * square(3) should return 9 + * square(4) should return 16 + * square(0) should return 0 + * + * Note: from this koan onward, the sensei might show you a single example of a call of your methods in the 'expected result' section. They should however work for any value of their parameters. * */ public static int square(int number) { @@ -190,6 +194,7 @@ public static int square(int number) { * Expected result: * * opposite(2) should return -2 + * opposite(-5) should return 5 * */ diff --git a/src/main/java/koans/english/AboutNot7Game.java b/src/main/java/koans/english/AboutNot7Game.java index eecd693..16b94d2 100644 --- a/src/main/java/koans/english/AboutNot7Game.java +++ b/src/main/java/koans/english/AboutNot7Game.java @@ -125,6 +125,16 @@ public class AboutNot7Game { * * Write a method 'askQuestion(String questionText)' asking a question to the user, and returns a boolean stating if the user answered 'y'. * + * --------- INDICES -------------- + * + * Java has a litle quirk. For testing if 2 String are equal, we can't use '=='. Ex: + * + * "abc" == "abc" // Always false!!! + * + * To compare 2 String, you need to use the 'equals' method on a String: + * + * "abc".equals("abc") // Return true, as it should + * * ------------------------------- * * Expected result: @@ -168,6 +178,9 @@ public class AboutNot7Game { * * Use a loop with a condition on the return value of the askQuestion() method. * You will have to create a boolean variable initialized with 'true' so that during the loop, you record whether the user wants to continue. + * + * You may use a while loop for this specific instance. + * Implementation will be easy if a while loop is used, but be sure to define your condition and updated the values using in the conditions logic correctly. * * ------------------------------- * diff --git a/src/main/java/koans/english/AboutObjects.java b/src/main/java/koans/english/AboutObjects.java index cf29bda..890935b 100644 --- a/src/main/java/koans/english/AboutObjects.java +++ b/src/main/java/koans/english/AboutObjects.java @@ -63,7 +63,7 @@ public class AboutObjects { * private final String name; * private final int age; * - * public introduce() { + * public void introduce() { * // In an object's method, we can use object field' values, like if they were simple variables * System.out.println("Hello, my name is " + name + " and I am " + age); * } @@ -92,7 +92,7 @@ public class AboutObjects { * this.age = age; * } * - * public introduce() { + * public void introduce() { * // In an object's method, we can use object fields * System.out.println("Hello, my name is " + name + " and I am " + age); * } diff --git a/src/main/java/koans/french/AboutClasses.java b/src/main/java/koans/french/AboutClasses.java deleted file mode 100644 index 79b3c86..0000000 --- a/src/main/java/koans/french/AboutClasses.java +++ /dev/null @@ -1,132 +0,0 @@ -package koans.french; - -public class AboutClasses { - /** - * # Classes et packages - * - * Crée une classe 'utils.MathUtils'. Dans cette classe, écris une méthode 'cube' qui prend un entier en paramètre et retourne son cube. - * - * --------- INDICES -------------- - * - * En Java, toutes tes méthodes doivent aller dans des classes. Les classes organisent les méthodes ensemble. - * - * Les classes sont elles-même organisées dans des répertoires appelés 'packages'. - * - * Les packages commencent à la racine Java du projet, généralement 'src/main/java'. - * Par exemple, le package 'koans' est situé dans le répertoire 'src/main/java/koans'. - * - * Les packages peuvent être imbriqués les uns dans les autres. Par exemple: le package 'french' est situé dans le package 'koans'. - * Les classes du package 'koans.french' sont situées dans 'src/main/java/koans/french'. - * - * Note: tu l'as peut être remarqué, en Java, pour localiser quelque chose situé dans quelque chose d'autre, on utilise la notation '.'. - * Par exemple, la classe 'AboutClasses' dans le package 'english' dans le package 'koans' est notée: - * - * koans.english.AboutClasses - * - * Pour créer une classe, crée un fichier nomé '[nom classe].java' dans le répertoire de son package. - * Tu pourrais avoir besoin de créer le répertoire en premier, si c'est la première classe du package. - * - * Par exemple, une classe 'frc.Robot' irait dans un fichier 'Robot.java' dans le répertoire 'src/main/java/frc'. - * - * Dans le fichier de la classe, on déclare la classe ainsi: - * - * // La toute première ligne doit déclarer dans quel package cette classe se situe - * package frc; - * - * public class Robot { - * // Les méthodes de la class Robot vont ici - * } - * - * Rappel: le cube d'un nombre est ce nombre multiplié 3 fois par lui-même. - * - * ------------------------------- - * - * Résultat attendu: - * - * utils.MathUtils.cube(2) devrait retourner 8 - * - */ - - - /** - * # Utiliser une classe différente - * - * En utilisant utils.MathUtils.cube, écris une méthode 'displayCube' dans koans.french.AboutClasses qui affiche le cube d'un nombre dans la console. - * - * --------- INDICES -------------- - * - * Pour appeler une méthode dans une classe qui n'est pas dans le même package, tu dois d'abord "l'importer". - * Les imports vont juste après la déclaration du package, mais avant la déclaration de la classe. Ex: - * - * // Le package de la classe que l'on est en train de déclarer - * package mypackage; - * - * // Exemples d'imports de classes que l'on pourrait utiliser dans ce fichier Java - * import frc.Robot; - * import utils.MathUtils; - * - * Une fois importées, tu peux utiliser des méthodes de ces autres classes: - * - * public class MyClass { - * public static void illuminateRobot() { - * Robot.tunOnDELs(); // Utilisation d'une classe importée en appelant une de ses méthodes - * } - * } - * - * Note: pour utiliser une classe une fois qu'elle est importée, pas besoin de répéter le nom de son package. Le nom de la classe suffit. - * - * ------------------------------- - * - * Résultat attendu: - * - * displayCube(3) devrait afficher '27' dans la console. - */ - - - /** - * # Une classe dans un package imbriqué - * - * Crée une classe 'utils.math.OtherMathUtils'. Dans cette classe, crée une méthode 'max' qui prends 2 entiers, et retourne le plus grand des 2. - * - * ------------------------------- - * - * Résultat attendu: - * - * utils.math.OtherMathUtils.max(2, 3) devrait retourner 3 - * - */ - - - /** - * # Utiliser une classe d'un package imbriqué - * - * En utilisant utils.math.OtherMathUtils.max, écris une méthode 'displayMax' dans koans.french.AboutClasses qui affiche le plus grand de 2 entiers. - * - * ------------------------------- - * - * Résultat attendu: - * - * displayMax(5, -1) devrait afficher '5' dans la console. - * - */ - - - /** - * # Utiliser une classe d'un Koan précédent - * - * En utilisant la méthode 'min' créée dans un précédent Koan, écris une méthode 'displayMin' dans koans.french.AboutClasses qui affiche le plus petit de 2 entiers. - * - * --------- INDICES -------------- - * - * Rappel: tu n'as pas besoin d'importer les classes qui font partie du même package que ta classe courante. Tu peux utiliser ces classes directement. - * - * ------------------------------- - * - * Résultat attendu: - * - * displayMin(5, -1) devrait afficher '-1' dans la console. - * - */ - - -} diff --git a/src/main/java/koans/french/AboutConditions.java b/src/main/java/koans/french/AboutConditions.java deleted file mode 100644 index 283b75b..0000000 --- a/src/main/java/koans/french/AboutConditions.java +++ /dev/null @@ -1,132 +0,0 @@ -package koans.french; - -public class AboutConditions { - /** - * # L'instruction if, et les conditions - * - * Écris une méthode 'sign' qui a un entier en paramètre, et retourne du texte: - * - * - si le nombre est plus grand que ou égal à 0, retourne le texte "positif" - * - sinon retourne le texte "négatif" - * - * --------- INDICES -------------- - * - * Tu peux exécuter du code sélectivement en fonction d'une condition. Une condition entre 2 nombre peut être exprimée ainsi: - * - * 4 == 4 - * ^ - * égalité - * - * 4 != 4 - * ^ - * inégalité (différent de) - * - * 4 > 3 - * ^ - * plus grand que - * - * 4 >= 3 - * ^ - * plus grand que ou égal - * - * 4 < 3 - * ^ - * plus petit que - * - * 4 <= 3 - * ^ - * plus petit que ou égal - * - * Pour utiliser une condition dans ton code, tu peux utiliser une instruction 'if / else'. Ex: - * - * if (number > 10) { - * // Code exécuté seulement si 'number' est plus grand que 10 - * } else { - * // Code exécuté seulement si 'number' n'est pas plus grand que 10 - * } - * - * Note 1: les doubles barres obliques '//' permettent d'écrire un commentaire jusqu'à la fin de la ligne. Ex: - * - * String text = "Hello"; // Assigne la valeur "Hello" à la variable 'text'. - * - * Note 2: tu peux retourner de plusieurs endroits dans le corps d'une méthode. Ex: - * - * if (number > 10) { - * return 1; // Stoppe l'exécution de la méthode ici et retourne la valeur 1. - * } else { - * return 0; // Stoppe l'exécution de la méthode ici et retourne la valeur 0. - * } - * - * ------------------------------- - * - * Résultat attendu: - * - * sign(2) devrait retourner "positive" - * - */ - - - /** - * # L'instruction else if - * - * Écris une méthode 'signOrZero' qui a un entier en paramètre, et retourne du texte: - * - * - si le nombre est plus grand que à 0, retourne le texte "positif" - * - si le nombre est 0, retourne le text "zéro" - * - sinon retourne le texte "négatif" - * - * --------- INDICES -------------- - * - * Pour enchaîner plusieurs conditions, tu peux utiliser l'instruction 'if / else if / else'. Ex: - * - * if (number > 10) { - * // Code exécuté seulement si 'number' est plus grand que 10 - * } else if (number > 0) { - * // Code exécuté seulement si 'number' n'est pas plus grand que 10, mais est plus grand que 0 - * } else { - * // Code exécuté seulement si 'number' n'est ni plus grand que 10, ni plus grand que 0 - * } - * - * Note: tu peux avoir un seul 'if' et un seul 'else', mais tu peux avoir plusieurs 'else if' dans une telle instruction. - * - * ------------------------------- - * - * Résultat attendu: - * - * signOrZero(2) devrait retourner "positif" - * signOrZero(0) devrait retourner "zéro" - * - */ - - - /** - * # Une note d'examen - * - * Écris une méthode 'gradeComment' qui a un paramètre entier représentant une note d'examen, et retourne du texte: - * - * - si la note est moins que 0 ou plus grande que 100, retourne le texte "Tu triches!" - * - si la note est 100, retourne le texte "Bravo, tu as tout bon!" - * - si la note est 0, retourne le texte "Ouch!" - * - si la note est plus grande que 0, mais moins grande que 60, retourne le texte "Échec!" - * - sinon retourne le texte "Bravo, tu passes!" - * - * --------- INDICES -------------- - * - * Tu peux combiner plusieurs conditions ensemble en utilisant les opérateurs '&&' (ET) et '||' (OU). Ex: - * - * if ((4 > 3) && (3 > 4)) { - * // Le code ici ne sera jamais exécuté - * } - * if ((4 > 3) || (3 > 4)) { - * // Le code ici sera exécuté - * } - * - * ------------------------------- - * - * Résultat attendu: - * - * gradeComment(80) devrait retourner "Bravo, tu passes!" - * gradeComment(0) devrait retourner "Ouch!" - * - */ -} diff --git a/src/main/java/koans/french/AboutConsoleAndVariables.java b/src/main/java/koans/french/AboutConsoleAndVariables.java deleted file mode 100644 index 831fcd7..0000000 --- a/src/main/java/koans/french/AboutConsoleAndVariables.java +++ /dev/null @@ -1,272 +0,0 @@ -package koans.french; - -import static engine.Helpers.readLine; - -public class AboutConsoleAndVariables { - /** - * # Afficher du texte dans la console - * - * Afficher 'Hello!' dans la console. - * - * --------- INDICES -------------- - * - * En Java, toutes les instructions de code sur une ligne doivent se terminer avec un ';'. Ex: - * - * System.out.println("Pomme"); - * - * Tu peux utiliser la méthode System.out.println([une valeur]) pour afficher une valeur dans la console. - * - * Tu peux dire à Java qu'une valeur est un texte en l'entourant par des guillemets. Ex: - * - * "Ceci est du texte" - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Hello! - * - */ - public static void sayHelloInConsole() { - - } - - /** - * # Afficher un calcul dans la console - * - * Fait calculer 2 + 2 à Java, et affiche le résultat dans la console. - * - * --------- INDICES -------------- - * - * En Java, tu peux calculer des expressions arithmétiques. Ex: - * - * 3 + 4 - * 3 * 4 - * 3 / 4 - * 3 - 4 - * -3 - * -3 * (4 - 2) - * - * Tu peux utiliser la méthode System.out.println([une expression]) pour afficher le résultat dans la console. - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * 4 - * - */ - public static void computeTwoAndTwo() { - - } - - /** - * # Créer une variable pour stocker une valeur - * - * Calcule 2 + 2 et stocke la valeur dans une variable. Affiche la valeur de la variable. - * - * --------- INDICES -------------- - * - * Une variable est un morceau d'information avec un nom. On peut récupérer cette information en utilisant son nom. - * En conséquence, pour déclarer une variable, tu as besoin de lui donner un nom et une valeur. - * Mais tu as aussi besoin de dire à Java, qui est un peu tatillon, quel genre d'information la variable va contenir (son 'type'). - * Par exemple, quand l'information est un nombre entier, son type est appelé 'int' en Java. - * En appliquant tout cela, nous pouvons créer, par exemple, une variable pour le nombre de pattes d'un cochon: - * - * int pigNbOfLegs = 4; - * ^ ^ ^ - * type nom valeur - * - * The value can be an arithmetic expression. For example: - * La valeur peut aussi être une expression arithmétique: - * - * int pigNbOfLegs = 1 + 1 + 1 + 1; - * - * À chaque fois que vous utilisez le nom d'une variable, Java va le remplacer par la valeur de la variable quand le programme s'exécute. - * Par exemple, pour afficher la valeur contenue dans la variable ci dessus: - * - * System.out.println(pigNbOfLegs); - * ^ - * le nom 'pigNbOfLegs' va être remplacé par la valeur de la variable. - * - * Dans notre exemple, c'est donc équivalent à (car la valeur de pigNbOfLegs est 4): - * - * System.out.println(4); - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * 4 - */ - public static void createAndDisplayAVariable() { - - } - - /** - * # Créer une variable pour stocker du texte - * - * Crée une variable de type texte appelée 'greeting' avec la valeur 'Hello!', et affiche la. - * - * --------- INDICES -------------- - * - * En Java, le type pour des valeurs textuelles est appelé 'String'. - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Hello! - * - */ - public static void createAndDisplayAStringVariable() { - - } - - - /** - * # Demander une réponse à l'utilisatrice·eur - * - * Demande un nom dans la console. Laisse l'utilisateur répondre. Stocke la réponse dans une variable de type 'String'. Affiche 'Ton nom est:', puis la réponse. - * - * --------- INDICES -------------- - * - * Tu peux lire du texte tapé dans la console avec la méthode readLine(). Cette méthode _retourne_ la valeur tapée par l'utilisateur dans la console. - * 'Une méthode retourne un résultat' veut dire que Java va remplacer l'appel à la méthode par le résultat retourné, exactement comme pour une variable. - * - * Par exemple, disons que l'utilisateur va taper 'orange', puis la touche [ENTRÉE] dans la console. Alors: - * - * String someText = readLine(); - * ^ - * cet appel de méthode va être remplacé par son résultat - * - * C'est équivalent à: - * - * String someText = "orange"; - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Quel est ton nom? - * [Tape un nom] - * Ton nom est: - * [Le nom tapé ci dessus] - * - */ - public static void askAndDisplayNameInConsole() { - - } - - /** - * # Jouer avec du texte - * - * La même chose que 'askAndDisplayNameInConsole', mais en affichant le résultat sur une seule ligne. - * - * --------- INDICES -------------- - * - * Tu vas avoir à créer une valeur textuelle en collant 2 valeurs textuelles. Coller 2 textes ensemble s'appelle 'concaténer'. - * En Java, tu peux faire ça avec l'opérateur '+': - * - * String glued = "abc" + "def"; - * ^ - * la valeur de glued est "abcdef" - * - * Attention! Ne pas confondre l'opérateur concaténation avec l'opérateur addition. Par exemple, cela ne peut fonctionner: - * - * 4 + "abc" - * ^ - * Erreur: parceque la valeur à gauche de l'opérateur est un nombre, le '+' ici est l'addition, et pas la concaténation. - * Et donc, Java s'attend à un nombre à droite de l'opérateur également, mais "abc" n'est pas un nombre, c'est un texte. - * - * Note: par contre l'inverse fonctionne: - * - * String glued = "abc" + 4; - * ^ - * la valeur de glued est "abc4" - * - * Pourquoi? Parceque Java peut convertir automatiquement n'importe quelle valeur en texte, incluant les nombre. Donc dans ce cas, c'est équivalent à: - * - * String glued = "abc" + "4"; - * ^ - * Le nombre est converti automatiquement - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Quel est ton nom? - * [Tape un nom] - * Ton nom est: [Le nom tapé ci dessus] - * - */ - public static void askAndDisplayNameOnASingleLineInConsole() { - - } - - /** - * # Jouer plus avec du texte - * - * Demande un nom, puis un âge. Affiche les 2 sur une même ligne. - * - * --------- INDICES -------------- - * - * Tu peux concaténer plus que 2 textes ensemble. Ex: - * - * String glued = "abc" + "def" + "ghi"; - * ^ - * la valeur de glued est "abcdefghi" - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Quel est ton nom? - * [Tape un nom] - * Quel âge as tu? - * [Tape un âge] - * Ton nom est: [Le nom tapé ci dessus] et ton âge est [L'âge tapé ci dessus] ans. - * - */ - public static void askNameAndAgeInConsole() { - - } - - - /** - * # Jouer avec du texte et des nombre en même temps - * - * Demande un âge. Affiche l'âge dans 5 ans. - * - * --------- INDICES -------------- - * - * Comme l'utilisateur peut taper n'importe quoi dans la console, la valeur retournée par readLine() est toujours un texte. - * Donc pour calculer l'âge dans 5 ans, il va te falloir d'abord convertir la réponse de l'utilisateur en un nombre. - * Tu peux convertir un nombre sous forme de texte en un entier comme ceci: - * - * String nbOfOrangesAsText = "3"; - * int nbOfOrangesAsNumber = Integer.parseInt(nbOfOrangesAsText); - * ^ - * la valeur est 3, en nombre - * - * Pour le calcul, tu peux soit créer une nouvelle variable à afficher plus tard: - * - * int nbOfOragesAfterIAteOne = nbOfOrangesAsNumber - 1; - * - * Ou faire le calcul directement à l'endroit où tu va l'afficher, en utilisant des parenthèses: - * - * System.out.println("Oranges left: " + (nbOfOrangesAsNumber - 1)); - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Quel âge as tu? - * [Tape un âge] - * Dans 5 ans, tu auras [L'âge tapé ci dessus + 5] ans. - * - */ - public static void computeAgeIn5YearsConsole() { - - } -} diff --git a/src/main/java/koans/french/AboutDecimalNumbers.java b/src/main/java/koans/french/AboutDecimalNumbers.java deleted file mode 100644 index b20818c..0000000 --- a/src/main/java/koans/french/AboutDecimalNumbers.java +++ /dev/null @@ -1,113 +0,0 @@ -package koans.french; - -public class AboutDecimalNumbers { - /** - * # Convertir une mesure de longueur - * - * Écris une méthode 'toCm' avec un paramètre pour un nombre de pouces, et qui retourne la conversion en centimètres. - * - * --------- INDICES -------------- - * - * Cette fois, le paramètre ne peut pas être un entier, car nous voulons convertir les fractions de pouces également. - * - * En Java, les nombres décimaux sont représentés par le type 'double'. Ex: - * - * double pi = 3.14; - * - * Pour convertir de pouces vers des centimètres, il faut multiplier par 2.54. - * - * ------------------------------- - * - * Résultat attendu: - * - * toCm(2.0) devrait retourner 5.08 - * - */ - - - /** - * # Convertir dans l'autre sens - * - * Écris une méthode 'toInches' qui fait l'inverse de la méthode précédente. - * - * ------------------------------- - * - * Résultat attendu: - * - * toInches(5.08) devrait retourner 2.0 - * - */ - - - /** - * # Calculer de la géométrie - * - * Écris une méthode 'rectangleArea' qui calcule l'aire d'un rectangle, étant donné la longueur de ses cotés. - * - * ------------------------------- - * - * Résultat attendu: - * - * rectangleArea(3.6, 2.0) devrait retourner 7.2 - * - */ - - - /** - * # Calculer la distance parcourue par un robot, étape 1 - * - * Écris une méthode 'wheelCircumference' qui calcule la circonférence d'une roue de robot, étant donné le rayon de la roue. - * - * --------- INDICES -------------- - * - * Utilise la valeur de 3.14 pour pi. - * - * ------------------------------- - * - * Résultat attendu: - * - * wheelCircumference(1) devrait retourner 6.28 - * - */ - - - /** - * # Calculer la distance parcourue par un robot, étape 2 - * - * Écris une méthode 'wheelRotations' qui calcule le nombres de rotations de roue étant donné le nombre de tours de moteur et le ratio de la boîte de transmission: - * - * --------- INDICES -------------- - * - * Le ratio de la boîte de transmission est combien de tours tourne la roue quand le moteur tourne 1 tour. - * Exemple: si le moteur doit faire 5 tours pour que la roue fasse exactement 1 tour, le ratio est de 1/5 = 0.2. - * - * ------------------------------- - * - * Résultat attendu: - * - * wheelRotations(0.2, 2.0) devrait retourner 0.4 - * - */ - - - /** - * # Calculer la distance parcourue par un robot, étape finale - * - * Utilise les 2 méthodes précédentes pour écrire une méthode 'toDistance' qui calcule la distance parcourue par les roues d'un robot étant donné: - * - * - Le nombre de tours du moteur - * - Le ratio de transmission - * - Le rayon des roues - * - * Attention! Il ne faut pas refaire les calculs des méthodes précédentes. À la place, réutilises-les. - * - * ------------------------------- - * - * Résultat attendu: - * - * toDistance(10.0, 0.2, 2.0) devrait retourner 25.12 - * - */ - - -} diff --git a/src/main/java/koans/french/AboutLoops.java b/src/main/java/koans/french/AboutLoops.java deleted file mode 100644 index 86781d2..0000000 --- a/src/main/java/koans/french/AboutLoops.java +++ /dev/null @@ -1,179 +0,0 @@ -package koans.french; - -public class AboutLoops { - /** - * # Première boucle - * - * Écris une méthode 'helloNTimes' avec un paramètre entier 'times', qui affiche 'Hello' dans la console 'times' fois. - * - * --------- INDICES -------------- - * - * Pour exécuter du code plusieurs fois, tu peux utiliser une boucle 'while'. - * Une boucle 'while' ressemble beaucoup à une instruction 'if', sauf que son bloc de code va se faire exécuter encore et encore tant que la condition reste vraie. Ex: - * - * int times = 3; - * while (times > 0) { - * // Cela va prendre 3 exécutions de ce boc de code avant que la condition ne devienne fausse. - * // Donc Java va l'exécuter 3 fois, puis va passer au reste du code. - * System.out.println("Toujours en train d'exécuter"); - * // On peut modifier la valeur d'une variable existente. Nous en profitons ici. - * times = times -1; - * } - * - * ------------------------------- - * - * Résultat attendu: - * - * helloNTimes(2) devrait afficher: - * - * Hello - * Hello - * - */ - - - /** - * # Afficher où nous sommes dans une boucle - * - * Écris une méthode 'displayNumbers' avec un paramètre entier 'n', qui affiche les nombres entre 1 et 'n'. - * - * ------------------------------- - * - * Résultat attendu: - * - * displayNumbers(3) devrait afficher: - * - * 1 - * 2 - * 3 - * - */ - - - /** - * # Compter à l'envers - * - * Écris une méthode 'displayReverseNumbers' avec un paramètre entier 'n', qui affiche les nombres entre 1 et 'n', en ordre inverse. - * - * ------------------------------- - * - * Résultat attendu: - * - * displayReverseNumbers(3) devrait afficher: - * - * 3 - * 2 - * 1 - * - */ - - - /** - * # Multiples de 7 - * - * Écris une méthode 'sevens' avec un paramètre entier 'n', qui affiche tous les multiples de 7 entre 1 et n. - * - * ------------------------------- - * - * Résultat attendu: - * - * sevens(30) devrait afficher: - * - * 7 - * 14 - * 21 - * 28 - * - */ - - - /** - * # Multiple de 7 ou 8 - * - * Écris une méthode 'sevensOrEights' avec un paramètre entier 'n', qui affiche tous les multiples de 7 ou 8 entre 1 et n. - * - * --------- INDICES -------------- - * - * Réutilise la méthode 'isMultiple' dans la class 'AboutMoreMethods'. Pour utiliser une méthode dans une autre classe, écris le nom de la classe, puis un '.' devant l'appel de la méthode. Ex: - * - * AboutConsoleAndVariables.sayHelloInConsole(); // Va afficher 'Hello!' dans la console - * ^ ^ ^ - * nom de classe point appel de méthode - * - * ------------------------------- - * - * Résultat attendu: - * - * sevensOrEights(20) devrait afficher: - * - * 7 - * 8 - * 14 - * 16 - * - */ - - - /** - * # Exposants - * - * Écris une méthode'exponent' qui calcule un entier à l'exposant d'un autre entier. - * - * --------- INDICES -------------- - * - * Nous allons noter l'exposant avec '^'. Ex: 10^3. - * x^n est égal à x multiplié par lui-même n fois. Ex: - * - * 2^4 = 2 * 2 * 2 * 2 = 16 - * - * ------------------------------- - * - * Résultat attendu: - * - * exponent(5, 3) devrait retourner 125 - * - */ - - - /** - * # Exposants, incluant 0 - * - * Écris une méthode'exponent2' qui fait la même chose, mais qui gère également le cas où n est 0. - * - * --------- INDICES -------------- - * - * x^0 vaut toujours 1. Ex: - * - * 34^0 = 1 - * - * ------------------------------- - * - * Résultat attendu: - * - * exponent2(5, 3) devrait retourner 125 - * - */ - - - /** - * # Factorielle - * - * Écris une méthode'factorial' qui calcule la factorielle d'un entier. - * - * --------- INDICES -------------- - * - * La factorielle est notée '!'. Ex: 3!. - * La factorielle d'un nombre n est le produit de tous les nombres entre 1 et n. Ex: - * - * 4! = 1 * 2 * 3 * 4 = 24 - * - * ------------------------------- - * - * Résultat attendu: - * - * factorial(5) devrait retourner 120 - * - */ - - -} diff --git a/src/main/java/koans/french/AboutMethods.java b/src/main/java/koans/french/AboutMethods.java deleted file mode 100644 index 9615af0..0000000 --- a/src/main/java/koans/french/AboutMethods.java +++ /dev/null @@ -1,212 +0,0 @@ -package koans.french; - -import static engine.Helpers.readLine; - -public class AboutMethods { - /** - * # Tâches répétitives - * - * Demande un âge. Affiche l'âge dans 5 ans. - * Demande un âge une deuxième fois. Affiche cet âge dans 10 ans. - * Demande un âge une troisième fois. Affiche cet âge dans 20 ans. - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Quel âge as tu? - * [Tape un âge] - * Dans 5 ans, tu auras [L'âge tapé ci dessus + 5] ans. - * Quel âge as tu? - * [Tape un âge] - * Dans 10 ans, tu auras [Le nouvel âge tapé ci dessus + 10] ans. - * Quel âge as tu? - * [Tape un âge] - * Dans 20 ans, tu auras [Le 3e âge tapé ci dessus + 20] ans. - * - */ - public static void computeAgeIn5And10And20YearsConsole() { - - } - - /** - * # Tâches répétitives: les méthodes à la rescousse - * - * Faisons une pause. Est-ce que ce n'était pas un peu pénible d'écrire 3 fois presque le même code? - * Fais la même chose cette fois ci, mais en appelant 3 fois une nouvelle méthode que tu vas créer. - * - * --------- INDICES -------------- - * - * Une méthode est un mini programme. Tu sais déjà ce qu'une méthode est, car chaque exercice que tu as terminé t'as fait écrire du code dans le _corps_ d'une méthode. - * Il est temps de créer une méthode complète toi même, et de l'appeler. - * Pour créer une méthode, il faut d'abord se placer au bon endroit dans la classe, entre la '{' et la '}' de la classe, mais en dehors d'une méthode existente. Ex: - * - * <-- mauvais endroit, en dehors de la classe - * public class AboutMethods { - * <-- bon endroit - * public static void computeAgeIn5And10And20YearsConsole() { - * <-- mauvais endroit, dans une autre méthode - * } - * <-- bon endroit - * } - * <-- mauvais endroit, en dehors de la classe - * - * Une fois l'endroit choisi, tu vas devoir écrire la _signature_ de la méthode, qui va donner à Java des informations cruciales sur ta méthode. - * Pour l'instant, nous n'allons nous préoccuper que de 2 informations: le _nom_ de la méthode et ses _paramètres_. - * Tu as besoin de donner un nom à ta méthode, pour la même raison que tu dois donner un nom à tes variable: Java va savoir à quoi tu réfère lorsque tu utilise ce nom. - * Les paramètres sont un moyen pour le programme qui appelle la méthode de configurer ce qu'il va se passer dans la méthode. Voici comment on déclare une signature de méthode: - * - * public static void [nom de la méthode]([type paramètre 1] [nom paramètre 1], [type paramètre 2] [nom paramètre 2], ...) { - * - * } - * - * La signature commence par 'public static void', dont nous allons ignorer la signification pour le moment. Ensuite vient le nom de la méthode. - * C'est très important de bien choisir un nom qui renseigne le lecteur de ton code sur la raison qui t'as poussé à écrire cette méthode (comparé à, par exemple, ce que la méthode éxécute à l'intérieur). - * Par exemple, disons que nous voulons créer une méthode qui prend un texte en paramètre, et l'affiche dans la console, suivi de 3 points d'exclamation, comme si la méthode criait le texte. - * Évite des noms du style 'displayInConsoleAndAddExclamations'. Le lecteur voit déjà que c'est ce que la méthode fait en lisant son code. - * Préfère des noms qui expriment l'intention en arrière de la méthode, dans un anglais le moins abstrait possible. Par exemple: 'yell'. - * - * Après le nom viennent les paramètres. La liste des paramètres est donnée entre parenthèses. Si ta méthode n'a pas de paramètres, écrit simplement '()' après son nom. - * Si tu as plus d'un paramètre, sépare les avec des ','. - * Un paramètre est un peu comme une variable pour la méthode, avec la différence que ce n'est pas la méthode qui va décider de sa valeur. - * La méthode va plutôt _recevoir_ la valeur de la part de la partie du programme qui l'appelle. - * Donc le/la programm·euse·eur ne sait pas d'avance, lorsqu'iel écrit sa méthode, quelle sera la valeur. Et c'est ok. - * La méthode peut par contre utiliser la valeur, comme avec une variable normale. Dans notre exemple, la méthode qui 'crie' va avoir besoin de savoir quoi crier. - * Le type du paramètre va donc être du texte, soit 'String' en Java. - * Le nom du paramètre devra rendre facile de se souvenir à quoi il sert. Par exemple: 'textToYell'. - * - * En appliquant tout ça, c'est ainsi que nous écririons une telle méthode: - * - * nom méthode type param nom param - * v v v - * public static void yell(String textToYell) { - * System.out.println(textToYell + " !!!"); - * } - * ^ - * À l'intérieur de la méthode, le paramètre est utilisé comme une variable normale - * - * Maintenant, comment éxécuter la méthode 'yell'? Dans une autre méthod, tu peux l'appeler. Cela veut dire que tu peux demander à Java d'éxécuter le code du corps (l'intérieur) de la méthode. - * Pour cela, tu dois donner à Java som nom et les valeurs des paramètres. Les valeurs sont entre parenthèses. Ex: - * - * public static void otherMethod() { - * yell("Java est trop cool"); - * } - * ^ ^ - * nom méthode valeur paramètre 1 - * - * Pour cet exercice, tu va créer une méthode qui demande et calcule un âge dans un certain nombre d'années. - * Ce nombre d'années sera différent à chaque appel, et devra donc être un paramètre de la méthode. - * Dans la méthode 'computeAgeIn5And10And20YearsConsoleWithMethod()', tu devras simplement appeler la méthode 3 fois: - * - une fois avec la valeur 5 - * - une fois avec la valeur 10 - * - et finalement une fois avec la valeur 20 - * - * ------------------------------- - * - * Résultat attendu dans la console: - * - * Quel âge as tu? - * [Tape un âge] - * Dans 5 ans, tu auras [L'âge tapé ci dessus + 5] ans. - * Quel âge as tu? - * [Tape un âge] - * Dans 10 ans, tu auras [Le nouvel âge tapé ci dessus + 10] ans. - * Quel âge as tu? - * [Tape un âge] - * Dans 20 ans, tu auras [Le 3e âge tapé ci dessus + 20] ans. - * - */ - public static void computeAgeIn5And10And20YearsConsoleWithMethod() { - - } - - /** - * # Les méthodes peuvent aussi retourner des résultats - * - * Écris une méthode qui a un entier pour paramètre, et retourne le carré de ce nomre. - * - * --------- INDICES -------------- - * - * Jusqu'à maintenant, les méthode que tu as écrite 'faisait des choses', comme afficher quelque chose dans la console. - * Mais elles ne communiquaient aucun résultat au reste du programme. - * En programmation, il est très utile, non seulement de recevoir des informations du reste du programme (via les paramètres), mais aussi de renvoyer de l'informaiton au reste du programme. - * Quand une méthode renvoie de l'information au reste du programme, on appelle ça 'retourner une valeur'. Une méthode peut retourner une, et une seule valeur appelée 'la valeur de retour'. - * Jusqu'à maintenant, les méthodes commencaient de cette façon: - * - * public static void [name of the method]() - * ^ - * le type de retour - * - * 'void' veut dire que la méthode ne retourne aucune information. - * Pour que la méthode puisse retourner quelque chose, il faut dire à Java quel type de valeur nous voulons faire retourner à la méthode. - * Par exemple, disons que nous voulons faire une méthode qui divise un entier par 2, et retourne le résultat: - * - * public static int half(int number) { ... } - * ^ - * Maintenant retourne un entier - * - * Cependant, cela indique juste à Java quel type de valeur la méthode va retourner. - * Comment demande-t-on à la méthode de retourner une valeur? En utilisant le mot clef 'return': - * - * public static int half(int number) { - * return number / 2; - * } - * ^ - * une expression de type 'entier' - * - * Finalement, comment utiliser le résultat retourné par la méthode? Nous pouvons appeler la méthode comme n'importe quelle autre: - * - * System.out.println(half(10)); - * - * Et comme vu précédemment, Java, après avoir appelé la méthode, va remplacer l'appel par le résultat de la méthode. - * Dans cet exemple, ce sera équivalent à écrire: - * - * System.out.println(5); - * - * Dans les prochains exercices cependant, tu ne vas pas appeler test méthode, juste les écrire pour le maître. - * - * Pour cet exercice, la signature de la méthode a déjà été écrite pour toi, tu n'as qu'à modifier le corps de la méthode pour respecter l'objectif de l'exercice. - * - * ------------------------------- - * - * Résultat attendu: - * - * square(3) devrait retourner 9 - * - */ - public static int square(int number) { - return 0; - } - - /** - * # Retourner l'opposé d'un nombre - * - * Écris une méthode appelée 'opposite' qui prend un paramètre entier, et retourne l'opposé de cet entier. - * - * --------- INDICES -------------- - * - * Pour cet exercice, tu dois écrire la signature de la méthode toi même. - * - * ------------------------------- - * - * Résultat attendu: - * - * opposite(2) devrait retourner -2 - * - */ - - - /** - * # Aider une fermière - * - * Écris une méthode appelée 'legs', qui aide une fermière à compter le nombre de pattes de ses animaux, étant donné le nombre de chaque type d'animaux. - * Il y a 3 paramètres: le nombre de poulets, le nombre de cochons, et le nombre de vaches. - * - * ------------------------------- - * - * Résultat attendu: - * - * legs(2, 3, 4) devrait retourner 32 - * - */ -} diff --git a/src/main/java/koans/french/AboutMoreMethods.java b/src/main/java/koans/french/AboutMoreMethods.java deleted file mode 100644 index 571d77c..0000000 --- a/src/main/java/koans/french/AboutMoreMethods.java +++ /dev/null @@ -1,131 +0,0 @@ -package koans.french; - -public class AboutMoreMethods { - /** - * # Calculer la valeur absolue - * - * Écris une méthode 'abs' qui a un paramètre entier, et retourne la valeur absolue de cet entier. - * - * ------------------------------- - * - * Résultat attendu: - * - * abs(-2) devrait retourner 2 - * - */ - - - /** - * # Quelle est la plus petite valeur? - * - * Écris une méthode 'min' qui a 2 paramètres entiers, et retourne le plus petit des 2 nombres. - * - * ------------------------------- - * - * Résultat attendu: - * - * min(4, 3) devrait retourner 3 - * - */ - - - /** - * # Calculer le reste de la division entière - * - * Écris une méthode 'remainder' qui prend 2 entiers en paramètres: - * - * - le premier est un dividende - * - le 2e est un diviseur - * - * La méthode retourne le reste de la division entière du dividende par le diviseur. - * - * --------- INDICES -------------- - * - * Tu peux calculer le résultat d'une division entière avec l'opérateur '/': - * - * int result = 13 / 3; // 'result' vaut 4 - * - * Le reste est ce qu'il reste non divisé du dividende. - * Par exemple, quand on divise 13 par 3, 12 est la plus grande part de 13 divisé au complet (en 4 parts de 3), et le reste est donc 13 - 12 = 1. - * - * ------------------------------- - * - * Résultat attendu: - * - * remainder(17, 5) devrait retourner 2 - * - */ - - - /** - * # Calculer is un nombre est pair - * - * Écris une méthode 'isEven' qui prend un entier en paramètre et retourne 'true' si le nombre est pair, 'false' sinon. - * Utilise la méthode précédente 'remainder' pour calculer le résultat. - * - * --------- INDICES -------------- - * - * Le type de valeur qui peut être soit 'true' soir 'false' est appelé un booléen ('boolean' en Java). - * Tu as déjà rencontré les booléens: les conditions utilisent des booléens. Mais tu peux utiliser les booléens en dehors des conditions. Ex: - * - * boolean result = 3 > 4; // 'result' vaut 'false' - * - * Tu peux faire retourner un booléen à une méthode en le précisant dans son type de retour: - * - * public static boolean isCool() { - * // Du code qui retourne un booléen - * } - * - * Pour tester si un nombre est pair, réfléchis à ce qu'il se passe lorsque l'on calcule le reste d'une division par 2. - * - * ------------------------------- - * - * Résultat attendu: - * - * isEven(5) devrait retourner false - * - */ - - - /** - * # Calculer si un nombre est un multiple d'un autre - * - * Écris une méthode 'isMultiple' avec 2 paramètres entiers qui retourne 'true' si le second nombre est un multiple du premier. - * Utilise la méthode 'remainder' pour calculer le résultat. - * - * --------- INDICES -------------- - * - * Pense à ce qu'il se passe sur le reste lorsque l'on divise un nombre par un multiple de ce nombre. - * - * ------------------------------- - * - * Résultat attendu: - * - * isMultiple(15, 5) devrait retourner true - * - */ - - - /** - * # Fizz Buzz - * - * Utilise la précédente méthode 'isMultiple' pour écrire une méthode 'fizzBuzz' prenant un entier en paramètre et qui affiche dans la console: - * - * - "Fizz" si l'entier est un multiple de 3 - * - "Buzz" si l'entier est un multiple de 5 - * - "FizzBuzz" si l'entier est un multiple de 3 et de 5 - * - l'entier lui même sinon - * - * Attention! Il ne faut pas refaire les calculs de la méthode précédente. À la place, réutilises-la. - * - * ------------------------------- - * - * Résultat attendu: - * - * fizzBuzz(20) devrait afficher: - * - * Buzz - * - */ - -} diff --git a/src/main/java/koans/french/AboutNot7Game.java b/src/main/java/koans/french/AboutNot7Game.java deleted file mode 100644 index 3f9dd19..0000000 --- a/src/main/java/koans/french/AboutNot7Game.java +++ /dev/null @@ -1,397 +0,0 @@ -package koans.french; - -import static engine.Helpers.random; -import static engine.Helpers.readLine; - -public class AboutNot7Game { - /** - * # Introduction au jeu 'Pas 7!' - * - * Les Koans suivants sont un peu spéciaux, car vous allez mettre en pratique tout ce que vous avez appris. - * - * Vous allez graduellement coder un jeu pour 2 appelé 'Pas 7!'. - * - * Dans ce jeu, chaque joueur a seulement 1 tour. À son tour, le joueur va lancer 2 dés, le nombre de fois qu'iel le désire. - * - * Cependant, si à un quelconque moment, le résultat du lancer est 7, le joueur perd immédiatement. - * - * Sinon, quand le joueur décide de passer et d'arrêter de lancer les dés, son score est la somme de tous ses lancers. - * - * Voici un exemple du jeu, dans la console: - * - * Joueur 1, c'est votre tour! - * - * Vous avez lancé un 5 et un 4. - * Votre résultat jusqu'à maintenant: 9. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 1 et un 3. - * Votre résultat jusqu'à maintenant: 13. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 6 et un 2. - * Votre résultat jusqu'à maintenant: 21. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 21! - * - * Joueur 2, c'est votre tour! - * - * Vous avez lancé un 3 et un 2. - * Votre résultat jusqu'à maintenant: 5. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 4 et un 4. - * Votre résultat jusqu'à maintenant: 13. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 1 et un 2. - * Votre résultat jusqu'à maintenant: 16. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 3 et un 3. - * Votre résultat jusqu'à maintenant: 22. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 22! - * - * Bravo, le joueur 2 gagne!!! - * - * - * - * Voici un autre exemple avec un joueur malchanceux qui tire un 7: - * - * Joueur 1, c'est votre tour! - * - * Vous avez lancé un 2 et un 3. - * Votre résultat jusqu'à maintenant: 5. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 6 et un 5. - * Votre résultat jusqu'à maintenant: 16. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 16! - * - * Joueur 2, c'est votre tour! - * - * Vous avez lancé un 3 et un 2. - * Votre résultat jusqu'à maintenant: 5. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 5 et un 2. - * Oh non, Pas 7! Vous avez perdu! - * - * Bravo, le joueur 1 gagne!!! - */ - - - /** - * # Lancer un dé - * - * Écris une méthode 'die6()' retournant le résultat d'un dé à 6 faces. - * - * --------- INDICES -------------- - * - * Pour générer un nombre aléatoire entre 0 et 1, utilise la méthode random(). Ex: - * - * double someNumber = random(); // 'someNumber' peut être n'importe quel double entre 0 et 1 - * - * Essaie de penser à quelle opération arithmétique tu pourrais appliquer pour passer d'un mombre entre 0 et 1 à un nombre entre 0 et 6. - * - * Une fois trouvé, tu devras arrondire le nombre à l'entier suivant. - * Par exemple, si le résultat de l'opération précédente est 3.2, tu vas devoir l'arrondir à 4. Tu peux faire cela avec: - * - * double rounded = Math.ceil(3.2) // 'rounded' vaut 4.0 - * - * Finalement, ces 2 opérations te donnent un nombre décimal. Il va te falloir le convertir en entier. - * Tu peux faire cette conversion en mettant le type qui t'intéresse entre parenthèses devant une expression. Ex: - * - * int roundedAsInt = (int)4.0; // converti un 'double' représentant un entier en un 'int' - * - * ------------------------------- - * - * Résultat attendu: - * - * die6() devrait retourner un entier entre 1 et 6 au hasard - * - */ - - - /** - * # Poser une question au joueur - * - * Écris une méthode 'askQuestion(String questionText)' qui pose une question à l'utilisateur, et retourne un booléen indiquant si l'utilisateur a répondu 'o'. - * - * ------------------------------- - * - * Résultat attendu: - * - * askQuestion("Voulez-vous continuer [o/n]? ") devrait: - * - afficher "Voulez-vous continuer [o/n]? " dans la console - * - laisser l'utilisateur répondre avec du texte - * - retourner true si l'utilisateur tape 'o' - - * askQuestion("Aimez vous les oranges [o/n]? ") devrait: - * - afficher "Aimez vous les oranges [o/n]? " dans la console - * - laisser l'utilisateur répondre avec du texte - * - retourner false si l'utilisateur tape 'n' - * - */ - - - /** - * # Lancer 2 dés - * - * Utilise die6() pour écrire une méthode 'throwDice' qui lance 2 dés, affiche le résultat dans la console, et retourne la somme des 2 dés. - * - * ------------------------------- - * - * Résultat attendu: - * - * Quand le résultat des dés est 2 et 3, throwDice() devrait afficher 'Vous avez lancé un 3 et un 2.' et retourner 5. - * - */ - - - /** - * # Programmer une manche du jeu: 1ère étape - * - * Utilise 'throwDice' et 'askQuestion' pour écrire une méthode 'gameRoundv1' qui va de façon répétée, tant que l'utilisateur répond 'o': - * - * - lancer 2 dés - * - demander si l'utilisateur veut continuer à lancer les dés - * - * --------- INDICES -------------- - * - * Utilise une boucle avec une condition sur la valeur retournée par la méthode askQuestion(). - * Tu vas devoir avoir une variable booléenne initialisée avec 'true', de façon à ce que tu puisse y enregistrer ce que l'utilisateur répond à la fin de la boucle. - * - * ------------------------------- - * - * Résultat attendu: - * - * Quand le résultat des dés est 2 et 3, gameRoundv1() devrait afficher: - * - * Vous avez lancé un 3 et un 2. - * Voulez-vous continuer à lancer [o/n]? - * - * Si l'utilisateur répond 'o', alors la méthode doit recommencer un nouveau lancé. - * Si l'utilisateur répond 'n', alors la méthode devrait arrêter. - */ - - - /** - * # Programmer une manche du jeu: 2ème étape - * - * Écris une méthode 'gameRoundv2' qui fait la même chose que gameRoundv1, mais qui affiche également la somme des résultats jusqu'à maintenant. - * - * ------------------------------- - * - * Résultat attendu: - * - * Quand le résultat des dés est 2 et 3, gameRoundv2() devrait afficher: - * - * Vous avez lancé un 3 et un 2. - * Votre résultat jusqu'à maintenant: 5. - * Voulez-vous continuer à lancer [o/n]? - * - * Si l'utilisateur répond 'o', alors la méthode doit recommencer un nouveau lancé. - * Si l'utilisateur répond 'n', alors la méthode devrait arrêter. - */ - - - /** - * # Programmer une manche du jeu: 3ème étape - * - * Écris une méthode 'gameRoundv3' qui fait la même chose que gameRoundv2, mais qui retourne aussi le score total une fois que l'utilisateur choisi de s'arrêter. - * - * ------------------------------- - * - * Résultat attendu: - * - * Quand le résultat des dés est 2 et 3, gameRoundv3() devrait afficher: - * - * Vous avez lancé un 3 et un 2. - * Votre résultat jusqu'à maintenant: 5. - * Voulez-vous continuer à lancer [o/n]? - * - * Si l'utilisateur répond 'n', alors la méthode devrait arrêter et retourner 5. - */ - - - /** - * # Programmer une manche du jeu: 4ème étape - * - * Écris une méthode 'gameRoundv4' qui fait la même chose que gameRoundv3, mais qui s'arrête également si le résultat du lancer est 7. - * Si la méthode s'arrête de cette manière, une ligne vide devrait être affichée à la fin et le résultat retourné devrait être 0. - * - * ------------------------------- - * - * Résultat attendu: - * - * Quand le résultat des dés est 4 et 3, gameRoundv4() devrait retourner 0 et afficher: - * - * Vous avez lancé un 3 et un 4. - * Oh non, Pas 7! Vous avez perdu! - * - */ - - - /** - * # Programmer une manche du jeu: dernière étape - * - * Écris une méthode 'gameRoundv5' qui fait la même chose que gameRoundv4, mais si le joueur s'arrête avant d'avoir fait un 7, - * elle affiche aussi son score entre des lignes vides. - * - * ------------------------------- - * - * Résultat attendu: - * - * Quand le résultat des dés est 4 et 3, gameRoundv5() devrait retourner 0 et afficher: - * - * Vous avez lancé un 3 et un 4. - * Oh non, Pas 7! Vous avez perdu! - * - * - * Quand le résultat des dés est 4 et 5, et que le joueur répond 'n', gameRoundv5() devrait retourner 9 et afficher: - * - * Vous avez lancé un 5 et un 4. - * Votre résultat jusqu'à maintenant: 9. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 9! - * - */ - - - /** - * # Programmer le jeu: 1ère étape - * - * En utilisant 'gameRoundv5', écris une méthode 'not7Gamev1' qui affiche le joueur dont c'est le tour, et fait jouer son tour au joueur. - * - * ------------------------------- - * - * Example de partie: - * - * Joueur 1, c'est votre tour! - * - * Vous avez lancé un 5 et un 4. - * Votre résultat jusqu'à maintenant: 9. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 1 et un 3. - * Votre résultat jusqu'à maintenant: 13. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 6 et un 2. - * Votre résultat jusqu'à maintenant: 21. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 21! - * - * Joueur 2, c'est votre tour! - * - * Vous avez lancé un 3 et un 2. - * Votre résultat jusqu'à maintenant: 5. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 4 et un 4. - * Votre résultat jusqu'à maintenant: 13. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 1 et un 2. - * Votre résultat jusqu'à maintenant: 16. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 3 et un 3. - * Votre résultat jusqu'à maintenant: 22. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 22! - * - */ - - - /** - * # Programmer le jeu: dernière étape - * - * Écris une méthode 'not7Gamev2' qui fait la même chose que not7Gamev1, mais qui affiche également qui est le gagnant. - * En cas d'égalité, elle affiche simplement: 'Égalité!'. - * - * ------------------------------- - * - * Example de partie: - * - * Joueur 1, c'est votre tour! - * - * Vous avez lancé un 5 et un 4. - * Votre résultat jusqu'à maintenant: 9. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 1 et un 3. - * Votre résultat jusqu'à maintenant: 13. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 6 et un 2. - * Votre résultat jusqu'à maintenant: 21. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 21! - * - * Joueur 2, c'est votre tour! - * - * Vous avez lancé un 3 et un 2. - * Votre résultat jusqu'à maintenant: 5. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 4 et un 4. - * Votre résultat jusqu'à maintenant: 13. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 1 et un 2. - * Votre résultat jusqu'à maintenant: 16. - * Voulez-vous continuer à lancer [o/n]? - * o - * Vous avez lancé un 3 et un 3. - * Votre résultat jusqu'à maintenant: 22. - * Voulez-vous continuer à lancer [o/n]? - * n - * - * Bravo, votre score est 22! - * - * Bravo, le joueur 2 gagne!!! - * - */ - - - /** - * # BONUS - * - * Si tu t'es rendu jusque là, félicitations! Tu connais maintenant les bases de la programmation!!! - * Avant le prochain Koan, cependant, que dirais tu d'être capable de lancer ton jeu, afin de pouvoir jouer avec un ami? - * - * En Java, pour qu'un programme sache quoi exécuter, tu as besoin de créer une méthode spéciale appelée 'main' quelque part. - * La signature de la méthode doit être exactement comme celle ci: - * - * public static void main(String[] args) { - * // Le code à exécuter lorsque le programme démarre - * } - * - * Par exemple, tu as peut-être remarqué que les Koans s'exécutent car la classe 'FrenchPathToEnlightment', sur laquelle tu fais un clic droit, possède une telle méthode. - * - * Alors n'hésites pas, et crée une telle méthode ici dans la classe 'AboutNot7Game', et appelle ta méthode 'not7Gamev2' à l'intérieur. - * - * Puis, pour démarrer ton jeu, fais simplement un clic droit sur le fichier src\main\java\koans\french\AboutNot7Game.java, et choisis 'Run Java'. - * Ton jeu s'exécute pour vrai! - * - */ - -} diff --git a/src/main/java/koans/french/AboutObjects.java b/src/main/java/koans/french/AboutObjects.java deleted file mode 100644 index e83e3d2..0000000 --- a/src/main/java/koans/french/AboutObjects.java +++ /dev/null @@ -1,351 +0,0 @@ -package koans.french; - -public class AboutObjects { - /** - * # Le premier objet - * - * Crée une classe 'geom.Point' avec 2 champs décimaux (toujours privés et finaux) 'x' et 'y'. Le constructeur devrait prendre 'x' et 'y' en paramètres. - * - * --------- INDICES -------------- - * - * Jusqu'à maintenant, nous avons seulement attaché des méthodes aux classes elles-même. Cela permet d'organiser les méthodes dans ton programme. - * Mais les classes peuvent être bien plus puissantes que de simples 'répertoires à méthodes'. - * - * Une classe peut servir de gabarit pour des éléments de code appelés 'objets'. Des objets groupent ensemble des valeurs et des méthodes qui peuvent agir sur ces valeurs. - * La classe devient alors un gabarit qui permet de créer plusieurs objets différents, qui ont tous la même structure de méthode et de valeurs. - * - * Par exemple, disons que nous voulons, dans le code, représenter des personnes qui peuvent se présenter. Un objet 'personne' pourrait donc avoir: un nom, un âge, et une méthode 'introduce'. - * - * Le nom et l'âge sont des sortes de variables attachées à un objet de type 'personne'. Les valeurs de ces variables peuvent être différentes d'un objet 'personne' à l'autre. - * Une telle 'variable' est appelée un 'champ de l'objet'. - * - * De la même façon, la méthode 'introduce' est attachée à l'objet. L'appeler sur 2 objets 'personne' différents va produire un résultat différent, puis que la valeur des champs pourront être différentes d'un objet à l'autre. - * - * Tu peux créer un objet à partir d'une classe en appelant une méthode très spéciale appelée 'constructeur'. Le constructeur... construit un objet à partir de la classe. - * - * Le résultat d'un constructeur est une valeur, dont le type est la classe elle-même. - * Tu peux appeler un constructeur en utilisant le mot clef 'new', suivit du nom de la classe. - * Par exemple, pour la classe Person: - * - * // Dans cet exemple, le constructeur de Person prend un nom et et âge - * Person julien = new Person("Julien", 44); - * Person stephane = new Person("Stéphane", 26); - * - * Une fois que nous avons des objets, nous pouvons appeler leurs méthodes. Par exemple, si les objets Person ont une méthode 'introduce': - * - * julien.introduce(); - * - * Résultat dans la console: - * - * Salut, mon nom est Julien et j'ai 44 ans - * - * Que se passe-t-il quand nous appelons la même méthode sur l'objet 'stephane'? - * - * stephane.introduce(); - * - * Résultat dans la console: - * - * Salut, mon nom est Stéphane et j'ai 26 ans - * - * Même méthode, mais un résultat différent! C'est parce que la méthode est appelée sur un object qui accède aux champs de l'objet. - * Comme les valeurs des champs sont différents d'un object à l'autre, la méthode produit un résultat différent. - * - * Qu'arrive-t-il si nous essayons d'appeler la méthode sur la classe elle-même? - * - * Person.introduce(); // Erreur, this ce n'est pas du code Java valide - * - * Cela produit une erreur, car la méthode est attachée aux _objets_ de la classe Person, pas à la classe Person elle-même. - * Par défaut, une méthode est attachée aux objets de la classe. Si tu veux attacher une méthode à la classe elle-même, tu dois dire qu'elle est 'static'. - * 'static' veut dire 'attaché à la classe'. - * - * Maintenant, comment on déclare des champs dans une classe? Comme ceci: - * - * public class Person { - * // Les champs vont au début de la classe. - * private final String name; - * private final int age; - * - * public introduce() { - * // Dans une méthode attachée à l'objet, nous pouvons utiliser les valeurs des champs, comme si c'était de simples variables - * System.out.println("Salut, mon nom est " + name + " et j'ai " + age + " ans"); - * } - * } - * - * Comme les variables, les champs ont un type et un nom. Par contre, nous devons également spécifier s'ils sont visibles en dehors de la classe. - * Bien que Java permette d'avoir des champs visibles en dehors de la classe, c'est une mauvaise pratique qui facilite la création de bugs non intentionnels. - * Donc, ils devraient tous être privés ('private' en Java), c'est à dire utilisable seulement à l'intérieur de la classe. - * Les champs devraient aussi être 'final', ce qui signifie que leur valeurs sont assignées une fois pour toute dans le constructeur, et ne changera jamais au cours de la vie d'un objet. - * - * Et comment déclarer un constructeur? Le constructeur est une méthode un peu spéciale. - * D'abord, nous n'avons pas besoin de déclarer un type de retour, puisqu'il retourne toujours un objet du type de la classe. - * Et le nom de cette méthode spéciale doit être le nom de la classe. Ex: - * - * public class Person { - * private final String name; - * private final int age; - * - * // Le constructeur de la class Person - * // Type de retour de la méthode: aucun. - * // Nom de la méthode: même nom que la classe. - * public Person(String name, int age) { - * // Nous pouvons assigner les valeurs des champs de l'objet dans le constructeur. - * this.name = name; - * this.age = age; - * } - * - * public introduce() { - * System.out.println("Hello, my name is " + name + " and I am " + age); - * } - * } - * - * Tu peux voir que nous utilisons le mot clef 'this' pour faire la différence entre les champs de l'objets et les paramètres du constructeur, qui ont les même noms. - * 'this' est comme un champ spécial qui contient l'objet courant. Donc 'this.[nom d'un champ ou d'une méthode]' réfère à un champ ou une méthode de l'objet courant. - * Par défaut, dans une méthode, un nom sans 'this' réfère à un paramètre ou une variable de la méthode, et non un champ de la classe. - * - * ------------------------------- - * - * Résultat attendu: - * - * nous pouvons créer un nouvel objet geom.Point - * - */ - - - /** - * # Une méthode d'objet - * - * Écris une méthode 'toString' in 'geom.Point' qui retourne une représentation d'un objet de type Point selon ce gabarit: - * - * Point([valeur de x], [valeur de y]) - * - * ------------------------------- - * - * Résultat attendu: - * - * new Point(2.5, 4.3).toString() devrait retourner le texte "Point(2.5, 4.3)" - * - */ - - - /** - * # Une autre méthode d'objet - * - * Écris une méthode 'translate' dans 'geom.Point' qui prend 2 coordonnées de translation 'tx' et 'ty', et retourne un nouveau objet Point, qui est le point translaté par les coordonnées tx et ty. - * - * --------- INDICES -------------- - * - * Quand on translate un point A de coordonnées x et y, le point résultant a les coordonnées x + tx et y + ty. - * - * Tu vas avoir besoin de créer un nouvel objet de type Point pour le retourner. Tu auras donc peut-être besoin de te raffraichir la mémoire en regardant les indices du premier exercice de cette série. - * - * ------------------------------- - * - * Résultat attendu: - * - * new Point(2.5, 4.3).translate(2, -1) devrait retourner un nouveau point avec les coordonnées 4.5 et 3.3 - * - */ - - - /** - * # Un objet... utilisant d'autres objets - * - * Crée une classe 'geom.Triangle' avec 3 champs privés 'a', 'b', et 'c' de type Point. Le constructeur devrait prendre 'a', 'b', et 'c' en paramètres. - * - * ------------------------------- - * - * Résultat attendu: - * - * nous pouvons créer un nouvel objet geom.Triangle - * - */ - - - /** - * # Utiliser la méthode d'un autre objet - * - * En utilisant 'Point.toString', écris une méthode 'toString' dans 'geom.Triangle' qui retourne une représentation textuelle d'un objet Triangle suivant ce gabarit: - * - * Triangle(Point([valeur x de 'a'], [valeur y de 'a']), Point([valeur x de 'b'], [valeur y de 'b']), Point([valeur x de 'c'], [valeur y de 'c'])) - * - * ------------------------------- - * - * Résultat attendu: - * - * new Triangle(new Point(1.0, 1.0), new Point(2.0, 2.0), new Point(3.0, 3.0)).toString() devrait retourner le texte "Triangle(Point(1.0, 1.0), Point(2.0, 2.0), Point(3.0, 3.0))" - * - */ - - - /** - * # Utiliser une autre méthode d'un autre objet - * - * En utilisant 'Point.translate', écris une méthode 'translate' dans 'geom.Triangle' qui prend 2 coordonnées de translation 'tx' et 'ty', et retourne un nouvel objet Triangle, qui est le triangle initial, translaté par tx et ty. - * - * ------------------------------- - * - * Résultat attendu: - * - * Le code suivant: - * - * Triangle myTriangle = new Triangle(new Point(1.0, 1.0), new Point(2.0, 2.0), new Point(3.0, 3.0)).translate(2.0, -1.0); - * - * Devrait résulter en 'myTriangle' étant un objet Triangle avec: - * - * - myTriangle.a qui vaut Point(3.0, 0.0) - * - myTriangle.b qui vaut Point(4.0, 1.0) - * - myTriangle.c qui vaut Point(5.0, 2.0) - * - */ - - - /** - * # Appliquer ses connaissances: la classe Circle - * - * Crée une classe 'geom.Circle' avec les champs 'center' (Point), et 'radius' (double). Le constructeur prend 'center' et 'radius' en paramètres. - * En utilisant 'Point.toString', écris une méthode 'toString' dans 'geom.Circle' qui retourne une représentation textuelle d'un objet Circle suivant ce gabarit: - * - * Circle(Point([valeur x de 'center'], [valeur y de 'center']), [valeur de 'radius']) - * - * ------------------------------- - * - * Résultat attendu: - * - * new Circle(new Point(2.0, 1.0), 3.6).toString() devrait retourner le texte "Circle(Point(2.0, 1.0), 3.6)" - * - */ - - - /** - * # Appliquer ses connaissances: translation d'un cercle - * - * En utilisant 'Point.translate', écris une méthode 'translate' dans 'geom.Circle' qui prend 2 coordonnées de translation 'tx' et 'ty', et retourne un nouvel objet Circle, qui est le cercle initial, translaté par tx et ty. - * - * ------------------------------- - * - * Résultat attendu: - * - * Le code suivant: - * - * Circle myCircle = new Circle(new Point(1.0, 1.0), 3.6).translate(2.0, -1.0); - * - * Devrait résulter en 'myCircle' étant un objet Circle avec: - * - * - myCircle.center qui vaut Point(3.0, 0.0) - * - myCircle.radius qui vaut 3.6 - * - */ - - - /** - * # Objets avec champs muables - * - * Maintenant, programmons une application de scoutisme pour le jeu de 2024 'Crescendo', dans lequel les robot ont besoin de lancer des cercles en mousse dans un 'haut parleur' et un 'amplificateur' ('amp'). - * En mode auto, envoyer une note dans le haut parleur rapporte 5 points, et dans l'amp, 2 points. Créons des classes pour nous aider à calculer le score de robots durant la période autonome. - * Créé une classe 'frc.RobotAutoScore' avec des champs non finaux 'notesInSpeaker' (int), 'notesInAmp' (int), initialisé à 0. - * Écris une méthode 'toString' dans 'frc.RobotAutoScore' qui retourne la représentation suivante d'un objet RobotAutoScore: - * - * ScoreRobot: notes dans le haut parleur = [valeur de notesInSpeaker]; notes dans le amp = [valeur de notesInAmp] - * - * ------------------------------- - * - * Résultat attendu: - * - * new RobotAutoScore().toString() devrait retourner la String "ScoreRobot: notes dans le haut parleur = 0; notes dans le amp = 0" - * - */ - - - /** - * # Changer la valeur des champs d'un objet - * - * Écris une méthode 'noteScoredInSpeaker' dans 'frc.RobotAutoScore' qui ne prend aucun paramètre, et augmente la valeur du champ 'notesInSpeaker' de 1. - * - * ------------------------------- - * - * Résultat attendu: - * - * Le code suivant: - * - * RobotAutoScore robotScore = new RobotAutoScore(); - * robotScore.noteScoredInSpeaker(); - * System.out.println(robotScore); - * - * Devrait afficher: - * - * ScoreRobot: notes dans le haut parleur = 1; notes dans le amp = 0 - * - */ - - - /** - * # Plus de changement de valeur - * - * Écris une méthode 'noteScoredInAmp' dans 'frc.RobotAutoScore' qui ne prend aucun paramètre, et augmente la valeur du champ 'notesInAmp' de 1. - * - * ------------------------------- - * - * Résultat attendu: - * - * Le code suivant: - * - * RobotAutoScore robotScore = new RobotAutoScore(); - * robotScore.noteScoredInAmp(); - * System.out.println(robotScore); - * - * Devrait afficher: - * - * ScoreRobot: notes dans le haut parleur = 0; notes dans le amp = 1 - * - */ - - - /** - * # Calculer le score total d'un robot - * - * Écris une méthode 'totalScore' dans 'frc.RobotAutoScore' qui ne prend aucun paramètre, et retourne le score total (2 points pour les notes dans le amp, et 5 point pour les notes dans le haut parleur). - * - * ------------------------------- - * - * Résultat attendu: - * - * Le code suivant: - * - * RobotAutoScore robotScore = new RobotAutoScore(); - * robotScore.noteScoredInSpeaker(); - * robotScore.noteScoredInAmp(); - * System.out.println(robotScore.totalScore()); - * - * Devrait afficher: - * - * 7 - * - */ - - - /** - * # Calculer le score total d'une alliance au complet - * - * Écris une classe 'frc.AllianceAutoScore' avec les champs finaux 'robotAScore', 'robotBScore' et 'robotCScore' (tous de type RobotAutoScore). Le constructeur prend une valeur pour chacun de ces champs en paramètres. - * Écris une méthode 'totalScore' dans 'frc.AllianceAutoScore' qui calcule le score total pour toute l'alliance. - * - * ------------------------------- - * - * Résultat attendu: - * - * RobotAutoScore robotAScore = new RobotAutoScore(); - * robotAScore.noteScoredInSpeaker(); - * robotAScore.noteScoredInAmp(); - * RobotAutoScore robotBScore = new RobotAutoScore(); - * RobotAutoScore robotCScore = new RobotAutoScore(); - * robotAScore.noteScoredInSpeaker(); - * AllianceAutoScore allianceScore = new AllianceAutoScore(robotAScore, robotBScore, robotCScore); - * System.out.println(allianceScore.totalScore()); - * - * Devrait afficher: - * - * 12 - * - */ - -} diff --git a/src/main/java/sensei/AboutArraysKoans.java b/src/main/java/sensei/AboutArraysKoans.java new file mode 100644 index 0000000..531e06c --- /dev/null +++ b/src/main/java/sensei/AboutArraysKoans.java @@ -0,0 +1,290 @@ +package sensei; + +import static engine.Assertions.assertNextStdOutLineEquals; +import static engine.Assertions.assertNoMoreLineInStdOut; +import static engine.Assertions.assertReturnValueEquals; +import static engine.Assertions.assertVariableEquals; +import static engine.Assertions.assertKoanMethodIsInvokable; +import static engine.Localizable.localClass; +import static engine.Localizable.global; +import static engine.script.Expression.assignVariable; +import static engine.script.Expression.callKoanMethod; +import static engine.script.Expression.variable; +import static sensei.Texts.*; + +import java.util.List; + +import engine.Koan; +import engine.Localizable; + + +public class AboutArraysKoans { + private static final Localizable> CLASS = + localClass(bonuses.english.AboutArrays.class); + + public static final List koans = List.of( + new Koan(CLASS, FOR_LOOPS) + .useConsole() + .beforeFirstTest( + assertKoanMethodIsInvokable("displayNumbers", int.class) + ) + .when(callKoanMethod("displayNumbers", 2)) + .then( + assertNextStdOutLineEquals(global("1")), + assertNextStdOutLineEquals(global("2")), + assertNoMoreLineInStdOut() + ) + .when(callKoanMethod("displayNumbers", 3)) + .then( + assertNextStdOutLineEquals(global("1")), + assertNextStdOutLineEquals(global("2")), + assertNextStdOutLineEquals(global("3")), + assertNoMoreLineInStdOut() + ) + .when(callKoanMethod("displayNumbers", 1)) + .then( + assertNextStdOutLineEquals(global("1")), + assertNoMoreLineInStdOut() + ) + .when(callKoanMethod("displayNumbers", 0)) + .then( + assertNoMoreLineInStdOut() + ), + new Koan(CLASS, FIRST_ELEMENT_OF_AN_ARRAY) + .beforeFirstTest( + assertKoanMethodIsInvokable("first", int[].class) + ) + .when(callKoanMethod("first", new int[]{4})) + .then( + assertReturnValueEquals(4) + ) + .when(callKoanMethod("first", new int[]{10, 4, 8})) + .then( + assertReturnValueEquals(10) + ), + new Koan(CLASS, LAST_ELEMENT_OF_AN_ARRAY) + .beforeFirstTest( + assertKoanMethodIsInvokable("last", int[].class) + ) + .when(callKoanMethod("last", new int[]{4})) + .then( + assertReturnValueEquals(4) + ) + .when(callKoanMethod("last", new int[]{10, 4, 8})) + .then( + assertReturnValueEquals(8) + ), + new Koan(CLASS, FINDING_AN_ELEMENT) + .beforeFirstTest( + assertKoanMethodIsInvokable("findFirst", int[].class, int.class) + ) + .when(callKoanMethod("findFirst", new int[]{3}, 3)) + .then( + assertReturnValueEquals(0) + ) + .when(callKoanMethod("findFirst", new int[]{10}, 3)) + .then( + assertReturnValueEquals(-1) + ) + .when(callKoanMethod("findFirst", new int[0], 3)) + .then( + assertReturnValueEquals(-1) + ) + .when(callKoanMethod("findFirst", new int[]{10, 4, 8, 8, 10}, 10)) + .then( + assertReturnValueEquals(0) + ) + .when(callKoanMethod("findFirst", new int[]{10, 4, 8, 8, 10}, 8)) + .then( + assertReturnValueEquals(2) + ), + new Koan(CLASS, FINDING_AN_ELEMENT_AT_THE_END) + .beforeFirstTest( + assertKoanMethodIsInvokable("findLast", int[].class, int.class) + ) + .when(callKoanMethod("findLast", new int[]{3}, 3)) + .then( + assertReturnValueEquals(0) + ) + .when(callKoanMethod("findLast", new int[]{10}, 3)) + .then( + assertReturnValueEquals(-1) + ) + .when(callKoanMethod("findLast", new int[0], 3)) + .then( + assertReturnValueEquals(-1) + ) + .when(callKoanMethod("findLast", new int[]{10, 4, 8, 8, 10}, 10)) + .then( + assertReturnValueEquals(4) + ) + .when(callKoanMethod("findLast", new int[]{10, 4, 8, 8, 10}, 8)) + .then( + assertReturnValueEquals(3) + ), + new Koan(CLASS, FINDING_THE_SMALLEST_ELEMENT) + .beforeFirstTest( + assertKoanMethodIsInvokable("min", int[].class) + ) + .when(callKoanMethod("min", new int[]{3})) + .then( + assertReturnValueEquals(3) + ) + .when(callKoanMethod("min", new int[0])) + .then( + assertReturnValueEquals(Integer.MAX_VALUE) + ) + .when(callKoanMethod("min", new int[]{10, 4, 8, 2, 10})) + .then( + assertReturnValueEquals(2) + ), + new Koan(CLASS, FINDING_THE_SMALLEST_ELEMENT_REVISITED) + .beforeFirstTest( + assertKoanMethodIsInvokable("min2", int[].class) + ) + .when(callKoanMethod("min2", new int[]{3})) + .then( + assertReturnValueEquals(3) + ) + .when(callKoanMethod("min2", new int[0])) + .then( + assertReturnValueEquals(Integer.MAX_VALUE) + ) + .when(callKoanMethod("min2", new int[]{10, 4, 8, 2, 10})) + .then( + assertReturnValueEquals(2) + ), + new Koan(CLASS, COMPUTING_THE_SUM) + .beforeFirstTest( + assertKoanMethodIsInvokable("sum", int[].class) + ) + .when(callKoanMethod("sum", new int[]{3})) + .then( + assertReturnValueEquals(3) + ) + .when(callKoanMethod("sum", new int[]{10})) + .then( + assertReturnValueEquals(10) + ) + .when(callKoanMethod("sum", new int[0])) + .then( + assertReturnValueEquals(0) + ) + .when(callKoanMethod("sum", new int[]{10, 4, 8})) + .then( + assertReturnValueEquals(22) + ), + new Koan(CLASS, COMPUTING_THE_AVERAGE) + .beforeFirstTest( + assertKoanMethodIsInvokable("avg", int[].class) + ) + .when(callKoanMethod("avg", new int[]{3})) + .then( + assertReturnValueEquals(3.0) + ) + .when(callKoanMethod("avg", new int[]{10})) + .then( + assertReturnValueEquals(10.0) + ) + .when(callKoanMethod("avg", new int[0])) + .then( + assertReturnValueEquals(0.0) + ) + .when(callKoanMethod("avg", new int[]{9, 4, 8})) + .then( + assertReturnValueEquals(7.0) + ), + new Koan(CLASS, FILLING_AN_ARRAY) + .beforeFirstTest( + assertKoanMethodIsInvokable("fill", int.class, int.class) + ) + .when(callKoanMethod("fill", 1, 11)) + .then( + assertReturnValueEquals(new int[]{11}) + ) + .when(callKoanMethod("fill", 0, 11)) + .then( + assertReturnValueEquals(new int[0]) + ) + .when(callKoanMethod("fill", 5, 11)) + .then( + assertReturnValueEquals(new int[]{11, 11, 11, 11, 11}) + ), + new Koan(CLASS, CREATING_A_SERIE) + .beforeFirstTest( + assertKoanMethodIsInvokable("serie", int.class) + ) + .when(callKoanMethod("serie", 1)) + .then( + assertReturnValueEquals(new int[]{1}) + ) + .when(callKoanMethod("serie", 0)) + .then( + assertReturnValueEquals(new int[0]) + ) + .when(callKoanMethod("serie", 5)) + .then( + assertReturnValueEquals(new int[]{1, 2, 3, 4, 5}) + ), + new Koan(CLASS, SWITCH_TWO_ELEMENTS) + .beforeFirstTest( + assertKoanMethodIsInvokable("switchFirst2", int[].class) + ) + .when( + assignVariable("a", new int[]{1, 5}), + callKoanMethod("switchFirst2", variable("a")) + ) + .then( + assertVariableEquals("a", new int[]{5, 1}) + ) + .when( + assignVariable("a", new int[]{1}), + callKoanMethod("switchFirst2", variable("a")) + ) + .then( + assertVariableEquals("a", new int[]{1}) + ) + .when( + assignVariable("a", new int[]{1, 2, 3}), + callKoanMethod("switchFirst2", variable("a")) + ) + .then( + assertVariableEquals("a", new int[]{1, 2, 3}) + ) + .when( + assignVariable("a", new int[]{}), + callKoanMethod("switchFirst2", variable("a")) + ) + .then( + assertVariableEquals("a", new int[]{}) + ), + new Koan(CLASS, REVERSE_AN_ARRAY) + .beforeFirstTest( + assertKoanMethodIsInvokable("reverse", int[].class) + ) + .when( + assignVariable("a", new int[]{1, 5, 8}), + callKoanMethod("reverse", variable("a")) + ) + .then( + assertReturnValueEquals(new int[]{8, 5, 1}), + assertVariableEquals("a", new int[]{1, 5, 8}) + ) + .when( + assignVariable("a", new int[]{1}), + callKoanMethod("reverse", variable("a")) + ) + .then( + assertReturnValueEquals(new int[]{1}), + assertVariableEquals("a", new int[]{1}) + ) + .when( + assignVariable("a", new int[]{}), + callKoanMethod("reverse", variable("a")) + ) + .then( + assertReturnValueEquals(new int[]{}), + assertVariableEquals("a", new int[]{}) + ) + ); +} diff --git a/src/main/java/sensei/AboutClassesKoans.java b/src/main/java/sensei/AboutClassesKoans.java index 576ac56..b650d44 100644 --- a/src/main/java/sensei/AboutClassesKoans.java +++ b/src/main/java/sensei/AboutClassesKoans.java @@ -18,9 +18,8 @@ public class AboutClassesKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutClasses.class) - .fr(koans.french.AboutClasses.class); - + localClass(koans.english.AboutClasses.class); + public static final List koans = List.of( new Koan(CLASS, CLASSES_AND_PACKAGES) .beforeFirstTest( @@ -69,17 +68,17 @@ public class AboutClassesKoans { ), new Koan(CLASS, AN_OTHER_CLASS_IN_A_NESTED_PACKAGE) .beforeFirstTest( - assertStaticMethodIsInvokable("utils.OtherMathUtils", "max", int.class, int.class) + assertStaticMethodIsInvokable("utils.math.OtherMathUtils", "max", int.class, int.class) ) - .when(callStaticMethod("utils.OtherMathUtils", "max", 2, 2)) + .when(callStaticMethod("utils.math.OtherMathUtils", "max", 2, 2)) .then( assertReturnValueEquals(2) ) - .when(callStaticMethod("utils.OtherMathUtils", "max", 1, 4)) + .when(callStaticMethod("utils.math.OtherMathUtils", "max", 1, 4)) .then( assertReturnValueEquals(4) ) - .when(callStaticMethod("utils.OtherMathUtils", "max", 4, 1)) + .when(callStaticMethod("utils.math.OtherMathUtils", "max", 4, 1)) .then( assertReturnValueEquals(4) ), diff --git a/src/main/java/sensei/AboutConditionsKoans.java b/src/main/java/sensei/AboutConditionsKoans.java index 3bb9731..170263f 100644 --- a/src/main/java/sensei/AboutConditionsKoans.java +++ b/src/main/java/sensei/AboutConditionsKoans.java @@ -14,9 +14,8 @@ public class AboutConditionsKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutConditions.class) - .fr(koans.french.AboutConditions.class); - + localClass(koans.english.AboutConditions.class); + public static final List koans = List.of( new Koan(CLASS, IF_CONSTRUCT_AND_CONDITIONS) .beforeFirstTest( diff --git a/src/main/java/sensei/AboutConsoleAndVariablesKoans.java b/src/main/java/sensei/AboutConsoleAndVariablesKoans.java index 2ae3288..3b83828 100644 --- a/src/main/java/sensei/AboutConsoleAndVariablesKoans.java +++ b/src/main/java/sensei/AboutConsoleAndVariablesKoans.java @@ -18,9 +18,8 @@ public class AboutConsoleAndVariablesKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutConsoleAndVariables.class) - .fr(koans.french.AboutConsoleAndVariables.class); - + localClass(koans.english.AboutConsoleAndVariables.class); + public static final List koans = List.of( new Koan(CLASS, DISPLAYING_SOME_TEXT_IN_THE_CONSOLE) .useConsole() diff --git a/src/main/java/sensei/AboutDecimalNumbersKoans.java b/src/main/java/sensei/AboutDecimalNumbersKoans.java index 4c39a47..073df84 100644 --- a/src/main/java/sensei/AboutDecimalNumbersKoans.java +++ b/src/main/java/sensei/AboutDecimalNumbersKoans.java @@ -13,9 +13,8 @@ public class AboutDecimalNumbersKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutDecimalNumbers.class) - .fr(koans.french.AboutDecimalNumbers.class); - + localClass(koans.english.AboutDecimalNumbers.class); + public static final List koans = List.of( new Koan(CLASS, CONVERTING_A_MEASURE_OF_LENGTH) .beforeFirstTest( diff --git a/src/main/java/sensei/AboutInterfacesKoans.java b/src/main/java/sensei/AboutInterfacesKoans.java new file mode 100644 index 0000000..c0d14fe --- /dev/null +++ b/src/main/java/sensei/AboutInterfacesKoans.java @@ -0,0 +1,129 @@ +package sensei; + +import static engine.Assertions.assertReturnValueEquals; +import static engine.Assertions.assertReturnValueImplements; +import static engine.Assertions.assertReturnValueIsAnonymousObject; +import static engine.Assertions.assertReturnValueIsLambda; +import static engine.Assertions.assertConstructorIsInvokable; +import static engine.Assertions.assertImplementsInterface; +import static engine.Assertions.assertKoanMethodIsInvokable; +import static engine.Localizable.localClass; +import static engine.script.Expression.callKoanMethod; +import static engine.script.Expression.newObject; +import static sensei.Texts.*; + +import java.util.List; +import java.util.function.IntPredicate; + +import bonuses.teachingmaterial.Combining; +import engine.Koan; +import engine.Localizable; + + +public class AboutInterfacesKoans { + private static final Localizable> CLASS = + localClass(bonuses.english.AboutInterfaces.class); + + public static final List koans = List.of( + new Koan(CLASS, FIRST_INTERFACE_IMPLEMENTATIONS) + .beforeFirstTest( + assertConstructorIsInvokable("numbers.AddNumbers"), + assertConstructorIsInvokable("numbers.MultiplyNumbers"), + assertImplementsInterface("numbers.AddNumbers", Combining.class), + assertImplementsInterface("numbers.MultiplyNumbers", Combining.class) + ) + .when( + newObject("numbers.AddNumbers").call("combine", 5, 7) + ) + .then( + assertReturnValueEquals(12) + ) + .when( + newObject("numbers.AddNumbers").call("combine", 0, -1) + ) + .then( + assertReturnValueEquals(-1) + ) + .when( + newObject("numbers.MultiplyNumbers").call("combine", 5, 7) + ) + .then( + assertReturnValueEquals(35) + ) + .when( + newObject("numbers.MultiplyNumbers").call("combine", 0, -1) + ) + .then( + assertReturnValueEquals(0) + ), + new Koan(CLASS, ANONYMOUS_INTERFACE_IMPLEMENTATION) + .beforeFirstTest( + assertKoanMethodIsInvokable("getAnonymousCombining") + ) + .when( + callKoanMethod("getAnonymousCombining") + ) + .then( + assertReturnValueIsAnonymousObject(), + assertReturnValueImplements(Combining.class) + ) + .when( + callKoanMethod("getAnonymousCombining").call("combine", 7, 5) + ) + .then( + assertReturnValueEquals(2) + ) + .when( + callKoanMethod("getAnonymousCombining").call("combine", 5, 7) + ) + .then( + assertReturnValueEquals(-2) + ), + new Koan(CLASS, LAMBDA_METHODS) + .beforeFirstTest( + assertKoanMethodIsInvokable("getLambdaCombining") + ) + .when( + callKoanMethod("getLambdaCombining") + ) + .then( + assertReturnValueIsLambda(), + assertReturnValueImplements(Combining.class) + ) + .when( + callKoanMethod("getLambdaCombining").call("combine", 7, 5) + ) + .then( + assertReturnValueEquals(-2) + ) + .when( + callKoanMethod("getLambdaCombining").call("combine", 5, 7) + ) + .then( + assertReturnValueEquals(2) + ), + new Koan(CLASS, COMMON_LAMBDA_INTERFACES) + .beforeFirstTest( + assertKoanMethodIsInvokable("getIsEven") + ) + .when( + callKoanMethod("getIsEven") + ) + .then( + assertReturnValueIsLambda(), + assertReturnValueImplements(IntPredicate.class) + ) + .when( + callKoanMethod("getIsEven").call("test", 8) + ) + .then( + assertReturnValueEquals(true) + ) + .when( + callKoanMethod("getIsEven").call("test", 3) + ) + .then( + assertReturnValueEquals(false) + ) + ); +} diff --git a/src/main/java/sensei/AboutLoopsKoans.java b/src/main/java/sensei/AboutLoopsKoans.java index c57ae49..1ce9040 100644 --- a/src/main/java/sensei/AboutLoopsKoans.java +++ b/src/main/java/sensei/AboutLoopsKoans.java @@ -17,9 +17,8 @@ public class AboutLoopsKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutLoops.class) - .fr(koans.french.AboutLoops.class); - + localClass(koans.english.AboutLoops.class); + public static final List koans = List.of( new Koan(CLASS, FIRST_LOOP) .useConsole() diff --git a/src/main/java/sensei/AboutMethodsKoans.java b/src/main/java/sensei/AboutMethodsKoans.java index bcc89e2..67cc722 100644 --- a/src/main/java/sensei/AboutMethodsKoans.java +++ b/src/main/java/sensei/AboutMethodsKoans.java @@ -19,9 +19,8 @@ public class AboutMethodsKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutMethods.class) - .fr(koans.french.AboutMethods.class); - + localClass(koans.english.AboutMethods.class); + public static final List koans = List.of( new Koan(CLASS, REPETITIVE_TASKS) .useConsole() diff --git a/src/main/java/sensei/AboutMoreMethodsKoans.java b/src/main/java/sensei/AboutMoreMethodsKoans.java index 2316653..ed8c995 100644 --- a/src/main/java/sensei/AboutMoreMethodsKoans.java +++ b/src/main/java/sensei/AboutMoreMethodsKoans.java @@ -16,9 +16,8 @@ public class AboutMoreMethodsKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutMoreMethods.class) - .fr(koans.french.AboutMoreMethods.class); - + localClass(koans.english.AboutMoreMethods.class); + public static final List koans = List.of( new Koan(CLASS, COMPUTING_THE_ABSOLUTE_VALUE_OF_A_NUMBER) .beforeFirstTest( diff --git a/src/main/java/sensei/AboutNot7GameKoans.java b/src/main/java/sensei/AboutNot7GameKoans.java index 924b79e..1e48285 100644 --- a/src/main/java/sensei/AboutNot7GameKoans.java +++ b/src/main/java/sensei/AboutNot7GameKoans.java @@ -27,9 +27,8 @@ public class AboutNot7GameKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutNot7Game.class) - .fr(koans.french.AboutNot7Game.class); - + localClass(koans.english.AboutNot7Game.class); + private static final GameRoundv5Assertions GAME_ROUND_ASSERTIONS = new GameRoundv5Assertions(false); public static final List koans = List.of( diff --git a/src/main/java/sensei/AboutObjectsKoans.java b/src/main/java/sensei/AboutObjectsKoans.java index 0358f14..a3e1c4e 100644 --- a/src/main/java/sensei/AboutObjectsKoans.java +++ b/src/main/java/sensei/AboutObjectsKoans.java @@ -23,8 +23,8 @@ public class AboutObjectsKoans { private static final Localizable> CLASS = - localClass(koans.english.AboutObjects.class) - .fr(koans.french.AboutObjects.class); + localClass(koans.english.AboutObjects.class); + private static final Type DOUBLE = type(double.class); private static final Type POINT = type("geom.Point"); private static final Type ROBOT_AUTO_SCORE = type("frc.RobotAutoScore"); @@ -44,13 +44,13 @@ public class AboutObjectsKoans { newObject("geom.Point", 2.0, 2.0).call("toString") ) .then( - assertReturnValueEquals(global("Point(2.0, 2.0)")) + assertReturnValueEquals("Point(2.0, 2.0)") ) .when( newObject("geom.Point", -2.0, 4.5).call("toString") ) .then( - assertReturnValueEquals(global("Point(-2.0, 4.5)")) + assertReturnValueEquals("Point(-2.0, 4.5)") ), new Koan(CLASS, AN_OTHER_OBJECT_METHOD) .beforeFirstTest( @@ -99,7 +99,7 @@ public class AboutObjectsKoans { ).call("toString") ) .then( - assertReturnValueEquals(global("Triangle(Point(2.0, 2.0), Point(0.0, 0.0), Point(2.0, -1.0))")) + assertReturnValueEquals("Triangle(Point(2.0, 2.0), Point(0.0, 0.0), Point(2.0, -1.0))") ) .when( newObject( @@ -110,7 +110,7 @@ public class AboutObjectsKoans { ).call("toString") ) .then( - assertReturnValueEquals(global("Triangle(Point(0.0, -2.0), Point(0.0, 0.0), Point(2.5, 5.3))")) + assertReturnValueEquals("Triangle(Point(0.0, -2.0), Point(0.0, 0.0), Point(2.5, 5.3))") ), new Koan(CLASS, USING_ANOTHER_OBJECT_S_METHOD) .beforeFirstTest( @@ -159,13 +159,13 @@ public class AboutObjectsKoans { newObject("geom.Circle", newObject("geom.Point", 2.0, 2.0), 1.7).call("toString") ) .then( - assertReturnValueEquals(global("Circle(Point(2.0, 2.0), 1.7)")) + assertReturnValueEquals("Circle(Point(2.0, 2.0), 1.7)") ) .when( newObject("geom.Circle", newObject("geom.Point", -2.5, 8.2), 4.5).call("toString") ) .then( - assertReturnValueEquals(global("Circle(Point(-2.5, 8.2), 4.5)")) + assertReturnValueEquals("Circle(Point(-2.5, 8.2), 4.5)") ), new Koan(CLASS, APPLY_LEARNINGS_CIRCLE_TRANSLATION) .beforeFirstTest( diff --git a/src/main/java/sensei/Texts.java b/src/main/java/sensei/Texts.java index be88336..5b33ad0 100644 --- a/src/main/java/sensei/Texts.java +++ b/src/main/java/sensei/Texts.java @@ -245,7 +245,15 @@ public class Texts { .fr("Bravo, le joueur 2 gagne!!!"); static final Localizable TIE = local("Tie!") - .fr("Égalité!"); + .fr("Égalité!"); + + // About Arrays + static final Localizable INDEXING_AN_ARRAY = + local("Indexing an array"); + static final Localizable SEARCHING_AN_ARRAY = + local("Searching an array"); + static final Localizable SORTING_AN_ARRAY = + local("Sorting an array"); // AboutClasses static final Localizable CLASSES_AND_PACKAGES = @@ -320,5 +328,60 @@ public class Texts { static final Localizable ROBOT_SCORE_TO_STRING_0_2 = local("RobotScore: notes in speaker = 0; notes in amp = 2") .fr("ScoreRobot: notes dans le haut parleur = 0; notes dans le amp = 2"); + + // AboutArrays + static final Localizable FOR_LOOPS = + local("For loops") + .fr("Boucles for"); + static final Localizable FIRST_ELEMENT_OF_AN_ARRAY = + local("First element of an array") + .fr("Premier élément d'un tableau"); + static final Localizable LAST_ELEMENT_OF_AN_ARRAY = + local("Last element of an array") + .fr("Dernier élément d'un tableau"); + static final Localizable FINDING_AN_ELEMENT = + local("Finding an element") + .fr("Trouver un élément"); + static final Localizable FINDING_AN_ELEMENT_AT_THE_END = + local("Finding an element at the end") + .fr("Trouver un élément à la fin"); + static final Localizable FINDING_THE_SMALLEST_ELEMENT = + local("Finding the smallest element") + .fr("Trouver l'élément le plus petit"); + static final Localizable FINDING_THE_SMALLEST_ELEMENT_REVISITED = + local("Finding the smallest element, revisited") + .fr("Trouver l'élément le plus petit, revisité."); + static final Localizable COMPUTING_THE_SUM = + local("Computing the sum") + .fr("Calculer la somme"); + static final Localizable COMPUTING_THE_AVERAGE = + local("Computing the average") + .fr("Calculer la moyenne"); + static final Localizable FILLING_AN_ARRAY = + local("Filling an array") + .fr("Remplir un tableau"); + static final Localizable CREATING_A_SERIE = + local("Creating a serie") + .fr("Créer une série"); + static final Localizable SWITCH_TWO_ELEMENTS = + local("Switch two elements") + .fr("Intervertir 2 éléments"); + static final Localizable REVERSE_AN_ARRAY = + local("Reverse an array") + .fr("Inverser un tableau"); + + // AboutInterfaces + static final Localizable FIRST_INTERFACE_IMPLEMENTATIONS = + local("First interface implementations") + .fr("Première implémentations d'interface"); + static final Localizable ANONYMOUS_INTERFACE_IMPLEMENTATION = + local("Anonymous interface implementation") + .fr("Anonymous interface implementation"); + static final Localizable LAMBDA_METHODS = + local("Lambda methods") + .fr("Méthodes lambda"); + static final Localizable COMMON_LAMBDA_INTERFACES = + local("Common lambda interfaces") + .fr("Interfaces lambda communes"); } diff --git a/src/main/java/sensei/Wisdom.java b/src/main/java/sensei/Wisdom.java index d87eeeb..ae7888f 100644 --- a/src/main/java/sensei/Wisdom.java +++ b/src/main/java/sensei/Wisdom.java @@ -16,6 +16,7 @@ public final class Wisdom { AboutDecimalNumbersKoans.koans, AboutLoopsKoans.koans, AboutNot7GameKoans.koans, + AboutArraysKoans.koans, AboutClassesKoans.koans, AboutObjectsKoans.koans );