# Introduction

Ce didacticiel est destiné aux utilisateurs maîtrisant la programmation en C ou C++, ainsi que l'architecture de la Simple Virtual Machine.

Dans ce didacticiel, vous allez apprendre à ajouter des tests aux extensions.

Le temps de lecture de ce didacticiel est estimé à 15 minutes.

# Mise en place

Pour commencer, créez le canevas de l'extension dans le fichier tests.svm_plugin en utilisant ce code :

    PLUGIN test

    DEFINE

    INSTRUCTION test.instruction INT INT -> INT
    %{
    	auto l = ARGV_VALUE(0,integer);
    	auto r = ARGV_VALUE(1,integer);
    	if(r==0)
    	{
    		ERROR_INTERNAL(NUMERIC,"Division by zero.");
    	}
    	return NEW_VALUE(integer,l/r);
    %}

# Ecriture des tests

## Tests simples

Lorsqu'une extension est destinée à être publiée à large échelle, il peut être important d'ajouter des tests pour vérifier le bon fonctionnement de l'extension dans le temps.

Les tests s'écrivent dans la partie générale de l'extension.

Modifiez le code de l'extension :

    PLUGIN test

    test: "Basic"
    %{
    PLUGIN "svmint.so"
    PROCESS "test"
    	CODE "main" INLINE
    		:memory INT/i, BLN/b
    		:test.instruction 12 3 -> &i
    		:int.cmp @&i = 4 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 5 -> &i
    		:int.cmp @&i = 2 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 30 -> &i
    		:int.cmp @&i = 0 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 -3 -> &i
    		:int.cmp @&i = -4 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:shutdown 0
    	END
    END
    %}

    test: "Divisionbyzero"
    %{
    -- 42 "is the answer"
    %}
    %{
    PROCESS "test"
    	CODE "main" INLINE
    		:interruption NUMERIC trap
    		:memory INT/i
    		:test.instruction 12 0 -> &i
    		:shutdown 1
    	:label trap
    		:shutdown 2 :when &i INITIALISED
    		:shutdown 0
    	END
    END
    %}

    DEFINE

    INSTRUCTION test.instruction INT INT -> INT
    %{
    	auto l = ARGV_VALUE(0,integer);
    	auto r = ARGV_VALUE(1,integer);
    	if(r==0)
    	{
    		ERROR_INTERNAL(NUMERIC,"Division by zero.");
    	}
    	return NEW_VALUE(integer,l/r);
    %}

Générez et compilez l'extension.

Chaque test comporte un titre, également utilisé comme nom de fichier pour contenir le test, un bloc facultatif contenant les options et arguments à passer à l'extension, et un bloc de code en langage machine. Dans ce code, l'invocation de la machine et l'inclusion de l'extension à tester est automatique.

Lorsque la machine renvoie un code de retour :

- 0 : le test est un succès,
- 77 : le test est annulé,
- 99 : le test échoue durement,
- autre : le test échoue simplement.

## Tests avec environnement

Parfois, les tests doivent manipuler des éléments du système tels que des fichiers. Il existe des directives particulières pour créer et détruire ces éléments.

Modifiez le code de l'extension :

    PLUGIN test

    test: initialisation
    %{
    cat > input_file << EOM
    12
    4
    EOM
    %}

    test: finalisation
    %{
    rm input_file
    %}

    test: "Basic"
    %{
    PLUGIN "svmint.so"
    PROCESS "test"
    	CODE "main" INLINE
    		:memory INT/i, BLN/b
    		:test.instruction 12 3 -> &i
    		:int.cmp @&i = 4 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 5 -> &i
    		:int.cmp @&i = 2 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 30 -> &i
    		:int.cmp @&i = 0 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 -3 -> &i
    		:int.cmp @&i = -4 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:shutdown 0
    	END
    END
    %}

    test: "Divisionbyzero"
    %{
    -- 42 "is the answer"
    %}
    %{
    PROCESS "test"
    	CODE "main" INLINE
    		:interruption NUMERIC trap
    		:memory INT/i
    		:test.instruction 12 0 -> &i
    		:shutdown 1
    	:label trap
    		:shutdown 2 :when &i INITIALISED
    		:shutdown 0
    	END
    END
    %}

    test: "File"
    %{
    PLUGIN "svmcom.so"
    PLUGIN "svmstr.so"
    PLUGIN "svmint.so"
    PROCESS "test"
    	CODE "main" INLINE
    		:memory com.device/f, STR/s, INT/i, INT*2/r, BLN/b
    		:com.open com.file < "input_file" -> &f
    		0 -> &i
    	:label input
    		:com.read @&f com.line -> &s
    		:str.replace @&s 1 CONST str.pattern "\n" => ""
    		:int.parse @&s -> (r/@&i)
    		:shift &i
    		:goto input :when @&i IN r
    		:test.instruction @(r/0) @(r/1) -> &i
    		:int.cmp @&i = 3 -> &b
    		:shutdown 1 :unless @&b TRUE
    	END
    END
    %}

    DEFINE

    INSTRUCTION test.instruction INT INT -> INT
    %{
    	auto l = ARGV_VALUE(0,integer);
    	auto r = ARGV_VALUE(1,integer);
    	if(r==0)
    	{
    		ERROR_INTERNAL(NUMERIC,"Division by zero.");
    	}
    	return NEW_VALUE(integer,l/r);
    %}

Générez et compilez l'extension.

Trois tests ont été ajoutés :

- Le dernier est un test tout à fait normal. Il utilise cependant un fichier pour lire ses données d'entrée.
- Le premier est un test dont le nom n'est pas une chaine de caractères, mais le mot-clef `initialisation`. Ce test spécial indique le script shell à exécuter avant de jouer les tests. C'est ce script qui va créer le fichier d'entrée.
- Le second est un test dont le nom n'est pas une chaine de caractères, mais le mot-clef `finalisation`. Ce test spécial indique le script shell à exécuter après avoir joué les tests. C'est ce script qui va supprimer le fichier d'entrée.

- Les tests s'écrivent avec la directive `test: "nom_test"` suivi d'un bloc de code en langage machine.
- Lorsque la directive `test` contient deux blocs, le premier contient les options et arguments de l'extension testée, et le second contient le test.
- Quand le nom du test est le mot-clef `initialisation`, le contenu est un script shell d'initialisation de l'environnement de test.
- Quand le nom du test est le mot-clef `finalisation`, le contenu est un script shell de vérification finale et de nettoyage de l'environnement de test.
- Chaque directive décrit un test. Les tests sont tous placés dans la partie générique de l'extension.
- Dans le code de chaque test, l'invocation de la machine et l'invocation de l'extension à tester est automatique.
- Le code de retour de la machine détermine si le test est valide : 0 pour un test réussi, une autre valeur pour un échec, et 77 pour une annulation du test.

# Lancement des tests

## Extension locale

Pour lancer les tests apres une compilation locale, il suffit de lancer la commande :

    ./plugin_install -l check
    [...]

    Test suite:
      init: OK
      Basic.svm: OK
      Divisionbyzero.svm: OK
      File.svm: OK
      fini: OK

    Result: 3/3 passed.

Chaque test est exécuté, puis son résultat est affiché. A la fin de tous les tests, un résumé de l'état des tests est donné.

## Avec Autotools

Pour lancer les tests avec Autotools, il faut d'abord configurer et compiler l'extension. Ensuite, la commande `make check` jouera les tests :

    ./plugin_install -c
    [...]
    ./configure
    [...]
    make
    [...]
    make check
    [...]
    PASS: init.sh
    PASS: Basic.svm
    PASS: Divisionbyzero.svm
    PASS: File.svm
    PASS: fini.sh
    ============================================================================
    Testsuite summary for svmplugintest 0.0
    ============================================================================
    # TOTAL: 5
    # PASS:  5
    # SKIP:  0
    # XFAIL: 0
    # FAIL:  0
    # XPASS: 0
    # ERROR: 0
    ============================================================================
    [...]

Ces tests sont également lancés lorsque cette commande est lancée :

    make distcheck

- Les tests sont lancés sur une extension compilée avec le Makefile local en utilisant la cible `check`.
- Avec Autotools, ils sont également lancés par la cible `check` une fois l'extension compilée.

# Investiguer un test en échec

L'intérêt des tests est de prévenir d'un changement non voulu dans le code de l'extension.

Modifiez le code de l'instruction sans changer les tests :

    PLUGIN test

    test: initialisation
    %{
    cat > input_file << EOM
    12
    4
    EOM
    %}

    test: finalisation
    %{
    rm input_file
    %}

    test: "Basic"
    %{
    PLUGIN "svmint.so"
    PROCESS "test"
    	CODE "main" INLINE
    		:memory INT/i, BLN/b
    		:test.instruction 12 3 -> &i
    		:int.cmp @&i = 4 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 5 -> &i
    		:int.cmp @&i = 2 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 30 -> &i
    		:int.cmp @&i = 0 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:test.instruction 12 -3 -> &i
    		:int.cmp @&i = -4 -> &b
    		:shutdown 1 :unless @&b TRUE
    		:shutdown 0
    	END
    END
    %}

    test: "Divisionbyzero"
    %{
    -- 42 "is the answer"
    %}
    %{
    PROCESS "test"
    	CODE "main" INLINE
    		:interruption NUMERIC trap
    		:memory INT/i
    		:test.instruction 12 0 -> &i
    		:shutdown 1
    	:label trap
    		:shutdown 2 :when &i INITIALISED
    		:shutdown 0
    	END
    END
    %}

    test: "File"
    %{
    PLUGIN "svmcom.so"
    PLUGIN "svmstr.so"
    PLUGIN "svmint.so"
    PROCESS "test"
    	CODE "main" INLINE
    		:memory com.device/f, STR/s, INT/i, INT*2/r, BLN/b
    		:com.open com.file < "input_file" -> &f
    		0 -> &i
    	:label input
    		:com.read @&f com.line -> &s
    		:str.replace @&s 1 CONST str.pattern "\n" => ""
    		:int.parse @&s -> (r/@&i)
    		:shift &i
    		:goto input :when @&i IN r
    		:test.instruction @(r/0) @(r/1) -> &i
    		:int.cmp @&i = 3 -> &b
    		:shutdown 1 :unless @&b TRUE
    	END
    END
    %}

    DEFINE

    INSTRUCTION test.instruction INT INT -> INT
    %{
    	auto l = ARGV_VALUE(0,integer);
    	auto r = ARGV_VALUE(1,integer);
    	if(r<=0)
    	{
    		ERROR_INTERNAL(NUMERIC,"Division by zero.");
    	}
    	return NEW_VALUE(integer,l/r);
    %}

Générez et compilez l'extension, puis lancez les tests en compilation locale :

    ./plugin_install -l check
    [...]

    Test suite:
      init: OK
      Basic.svm: FAIL (return code: 125)
      Divisionbyzero.svm: OK
      File.svm: OK
      fini: OK

    Result: 2/3 passed.

Un test a bien échoué ici ! En effet, une des évaluations basiques lève maintenant une interruption (d'après le code de retour à 125, signifiant que le processus a été interrompu). Ici, comme plusieurs tests sont faits dans le même code, il faut déterminer où se situe l'erreur.

Ouvrez le fichier produit par la suite de test test/Basic.log :

    Test case: Basic.svm
    ### Simple Virtual Machine 1234 : PROCESS test | 2023-01-01 00:00:00 GMT #############################################################################
    Kernel interrupted: @(main, line 11) Interruption NUMERIC not handled: Division by zero.

    Core dump:
    Kernel:
    State: I, transmit_interruption, interrupted @(main, line 11) Interruption NUMERIC not handled: Division by zero.

    Processor:
     State:
       Next instruction: <main:1/11> (Current one: <main:1/10>)
       Current memory  : &0*0
       Allocated memory: &0*2
       Aliases         : b i
       Flags           :
       Cascaded flags  :
       Local interruption handlers:
       Cascaded local interruption handlers:
     Saved states:
     Global interruption handlers:
     Waiting interruptions:
      NUMERIC
    Code at <main:1/10>:
         :memory INT/i , BLN/b	# <0> @(main, line 1)
         :test.instruction 12 3 -> &i	# <1> @(main, line 2)
         :int.cmp @&i = 4 -> &b	# <2> @(main, line 3)
         :shutdown 1 :unless @&b TRUE	# <3> @(main, line 4) SYSTEM
         :test.instruction 12 5 -> &i	# <4> @(main, line 5)
         :int.cmp @&i = 2 -> &b	# <5> @(main, line 6)
         :shutdown 1 :unless @&b TRUE	# <6> @(main, line 7) SYSTEM
         :test.instruction 12 30 -> &i	# <7> @(main, line 8)
         :int.cmp @&i = 0 -> &b	# <8> @(main, line 9)
         :shutdown 1 :unless @&b TRUE	# <9> @(main, line 10) SYSTEM
    ====== HERE ======
         :test.instruction 12 -3 -> &i	# <10> @(main, line 11)
    ==================
         :int.cmp @&i = -4 -> &b	# <11> @(main, line 12)
         :shutdown 1 :unless @&b TRUE	# <12> @(main, line 13) SYSTEM
         :shutdown 0	# <13> @(main, line 14) SYSTEM
     Labels:
     Symbols:

    Memory:
      &0: INT 0
      &1: BLN TRUE
     Aliases:
      b -> &1*1
      i -> &0*1
     Free space:

    ### Simple Virtual Machine 1234 : SYSTEM | 2023-01-01 00:00:00 GMT ###################################################################################
    Process test interrupted: @(main, line 11) Interruption NUMERIC not handled: Division by zero.

    Result: FAIL (return code: 125)

L'instruction qui a levé une interruption inattendue est celle qui contient un argument négatif, ce qui est conforme au code de l'instruction, mais pas pris en compte dans le test :

- si le nouveau comportement est voulu, le test peut être alors modifié pour valider ce nouveau comportement,
- sinon, le code doit être corrigé jusqu'à ce que le test passe à nouveau.

Lorsqu'un test échoue avec la commande `make check` d'Autotools, l'outil vous invite à ouvrir le fichier test/test-suite.log qui contient les mêmes informations.

- Les tests permettent de fiabiliser le code des extensions, car ils peuvent révéler des comportements inattendus.
- L'investigation d'un test qui échoue se fait en regardant le fichier .log correspondant au test.
- Si le comportement est valide, le test doit être changé. Sinon, le code doit être modifié jusqu'à ce que le test passe à nouveau.

# Conclusion

Vous venez de voir comment ajouter des tests aux extensions.

Pour une extension qui doit rester stable dans le temps, il est toujours préférable d'y ajouter des tests pour assurer que le fonctionnement reste sous contrôle, même en cas d'évolution ultérieure de l'extension.

Les tests sont un outil simple et efficace pour écrire des extensions de qualité industrielle.