Site logo

Triceraprog
La programmation depuis le Crétacé

Forth sur 6502, épisode 3 ()

Des tests

Depuis le dernier article, je me suis surtout concentré sur la mise en place d'un framework de tests, ainsi que sur une réflexion de « comment commencer » ?

La lecture de l'article 2 de Moving Forth m'a donné une liste de mots à implémenter en priorité : NEXT, DOCOL et ;S. Cela afin d’arriver rapidement sur une boucle écrite en Forth. J'y reviendrai plus loin dans l'article.

Côté tests, ça a été une aventure en plusieurs étapes. Dans l'idée d'augmenter le nombre de tests, je voulais m'appuyer sur un framework de tests LUA tout fait. J'en ai trouvé un, luaunit qui m'a semblé tout à fait répondre à mes attentes. Pour l'utiliser, je dois utiliser un require("luaunit") dans mon script de tests. Et là ont commencé les ennuis. Tout d'abord, require() n'est pas permis par défaut dans Mesen2, il faut aller permettre les fonction E/S dans les paramètres.

Malgré cela, le script ne semble pas être trouvé, alors qu'il est bien dans un répertoire de recherche (vérifié avec le contenu de la variable package.path). En tout cas pas en mode --testrunner, car en debug depuis l'interface graphique, ça fonctionne ! Après étude des sources de Mesen2, je vois le soucis (qui était bien affiché dans mes logs, mais assez subtil pour que je passe à côté) : le mode testrunner et le mode UI résolvent différemment le chemin des scripts, à un séparateur de chemin près.

La correction est simple, je la fais et je suis prêt à faire un merge request... pour m'apercevoir que le dépôt est fermé aux contributions. Après enquête, il semblerait que le développeur a disparu de la circulation en juillet 2025. Puisque le dépôt est fermé, j'imagine que c'est un retrait volontaire et non un événement malheureux. Je l'espère. On me dit qu'il aurait déjà fait ça dans le passé lorsqu'il avait eu besoin de se concentrer.

En attendant, j'ai donc créé ma version de Mesen2 avec cette correction (et une autre trouvée dans un autre fork qui me semblait intéressante). Merci l'open source.

Vient ensuite mon deuxième soucis : luaunit ne va pas convenir. Comme tous (?) les frameworks de tests, le framework gère des contextes cloisonnés, s'occupe de lancer les tests, de capturer les erreurs, etc. Bref, il a les commandes. Sauf que Mesen2 requiert que certaines fonctions de contrôle de l'émulateur soient appelées depuis des callbacks. Autrement dit, Mesen2 veut aussi avoir le contrôle.

Après avoir retourné le problème quelques temps, je me suis dit que finalement, j'allais faire un petit framework sur mesure adapté à Mesen2. Il aura certainement moins de fonctionnalités qu'une solution complète, mais tant pis. J'ai donc écrit un framework basé sur une machine à états qui fait avancer les tests au fur et à mesure que l'émulateur lance des callbacks. Ça fonctionne, mais c'est un peu verbeux à l'écriture pour le moment. J'espère pouvoir dégager quelques fonctions utilitaires pour simplifier l'écriture des tests par la suite, si besoin.

Ah, et j'englobe l'appel des tests dans un xpcall(), une fonction bien pratique en LUA qui permet de capturer les erreurs, et donc d'arrêter l'émulateur proprement en cas de problème avec affichage du message d'erreur.

Mes trois tests

Pour le moment, j'ai trois tests :

  • Le premier test vérifie que les symboles des registres Forth sont bien présents dans les symboles. Il y a peu de chance que ça ne passe pas, mais c'est ce qu'on se dit avant que ça ne passe pas...
  • Le deuxième test vérifie que le programme arrive jusqu'à la boucle principale. Un smoke test de base.
  • Le troisième test vérifie qu'une adresse mémoire particulière est modifiée. C'est actuellement l'action qui est faite dans la boucle principale.

En fait, il y a pas mal d'autres choses qui sont faites dans la boucle principale par le squelette de projet que j'utilise. Je verrai si j'ai besoin à un moment de tester cette partie, mais pour le moment, dans une logique TDD, tant que je n'y touche pas, je ne teste pas.

La suite

Pas de lecture de la partie 3 de Moving Forth pour le moment. En effet, celle-ci se concentre sur le CFA (Code Field) et le PFA (Parameter Field), puis enchaîne sur les mots de construction. Je n'en suis pas encore là. Je reprendrai les résumés lorsque j'aurai une boucle Forth fonctionnelle.

Car c'est la prochaine étape : pour le moment, le code contient une boucle assembleur qui :

  • lit les entrées des pads
  • écrit une valeur à une adresse mémoire fixe (pour le test)
  • attend la synchronisation verticale
  • exécute une file d'instruction PPU (pour l'affichage)
  • boucle

Ma prochaine étape est de remplacer cette boucle par la même chose en Forth. Pour cela, je dois implémenter au moins le mot NEXT qui appellera ce même code suivant le schéma Forth. Autrement dit, ces routines devront se terminer par un appel à NEXT pour continuer l'exécution, plutôt que de se terminer par un RTS (return from subroutine).

Point d'étape

J'ai donc à présent un environnement d'assemblage et de tests, une boucle minimale. Le prochain article sera écrit lorsque j'aurai a minima un NEXT fonctionnel.

Et pour terminer, voici la sortie des tests actuels :

mesen --testrunner out/nes_template.nes start_tests.lua
-- STARTING TESTS --
[1/3] Running: Forth symbols exist
  ✓ PASSED
[2/3] Running: Program boots and reaches main loop
  ✓ PASSED
[3/3] Running: Verify memory change at specific address
  ✓ PASSED
============================================================
TEST REPORT
============================================================
Total: 3 | Passed: 3 | Failed: 0
============================================================