Python

Installation d’Autobahn|Testsuite sous Xubuntu

L’année dernière, dans le cadre du travail, j’ai fait une implémentation de la partie client du protocole WebSocket, décrit par la RFC 6455, pour une utilisation dans un environnement Java embarqué contraint. Au début, j’utilisais un petit serveur, écrit en Java SE,  pour avoir quelques tests simples. Quand mon implémentation à commencer à devenir mature, j’ai cherché comment tester de manière un peu plus complète ma bibliothèque et j’ai trouvé Autobahn|Testsuite. C’est une test suite automatique, écrite en Python 2 et elle est plutôt réputée pour ce protocole, à en juger par la liste affirmée des utilisateurs. Elle est en tout cas, elle est suffisamment réputée pour qu’Oracle fasse un article quand leur implémentation de référence de WebSocket en Java a atteint pour la première fois un score de 100 %. J’ai remis en branle cette test suite cette semaine car je vais me resservir de cette bibliothèque pour un nouveau projet.

Le site officiel donne une procédure d’installation qui est en apparence simple mais j’ai rencontré plusieurs problèmes en l’installant sous Xubutu. J’écris cet article pour retracer les problèmes et les solutions.

La première étape est d’installer les dépendance décrites. Python 2.7 est installé par défaut, il me suffit donc d’installer les modules pip et Twisted :

sudo apt-get install python-pip
sudo apt-get install python-twisted

On peut ensuite passer à l’installation de la test suite en elle-même. Je vous conseille de demander les droits root pour éviter des erreurs telles que error: could not create '/usr/local/lib/python2.7/dist-packages/autobahntestsuite': Permission denied. Dans le terminal :

sudo pip install autobahntestsuite>

Après des téléchargements et un peu de défilement dans la console, vous rencontrerez sans doute une erreur comme celle-ci :

wsaccel/utf8validator.c:8:22: fatal error: pyconfig.h: No such file or directory

 #include "pyconfig.h"

                      ^

compilation terminated.

error: command 'i686-linux-gnu-gcc' failed with exit status 1

Pour que l’installation réussisse, j’ai dû installé quelques paquets supplémentaires :

sudo apt-get install python-dev
sudo apt-get install libffi-dev
sudo apt-get install libssl-dev

L’installation est maintenant terminée ! Il n’y a plus qu’à lancer le test d’installation, qui vérifie que tout s’est bien installé :

$ wstest --help
Traceback (most recent call last):
  File "/usr/local/bin/wstest", line 5, in 
    from pkg_resources import load_entry_point
  File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 2749, in 
    working_set = WorkingSet._build_master()
  File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 444, in _build_master
    ws.require(__requires__)
  File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 725, in require
    needed = self.resolve(parse_requirements(requirements))
  File "/usr/lib/python2.7/dist-packages/pkg_resources.py", line 628, in resolve
    raise DistributionNotFound(req)
pkg_resources.DistributionNotFound: characteristic>=14.0.0
$ echo $?
1

Caramba ! Encore raté ! Python ne possède pas le module characteristic ou alors il en possède une version trop ancienne. pip est la solution pour installer des modules Python :

sudo pip install characteristic

Histoire de vous faire gagner du temps, je vous annonce qu’il manque aussi d’autres modules, installez-les avant de retenter wtest :

sudo pip install pyasn1-modules
sudo pip install cryptography
sudo pip install unittest2

Finalement :

$ wstest --help && echo $?
Usage: wstest [options]
Options:
  -d, --debug            Debug output [default: off].
  -a, --autobahnversion  Print version information for Autobahn and
                         AutobahnTestSuite.
  -m, --mode=            Test mode, one of: echoserver, echoclient,
                         broadcastclient, broadcastserver, fuzzingserver,
                         fuzzingclient, testeeserver, testeeclient, massconnect,
                         serializer [required]
  -t, --testset=         Run a test set from an import test spec.
  -s, --spec=            Test specification file [required in some modes].
  -o, --outfile=         Output filename for modes that generate testdata.
  -w, --wsuri=           WebSocket URI [required in some modes].
  -u, --webport=         Web port for running an embedded HTTP Web server;
                         defaults to 8080; set to 0 to disable. [optionally used
                         in some modes: fuzzingserver, echoserver,
                         broadcastserver, wsperfmaster]. [default: 8080]
  -i, --ident=           Testee client identifier [optional for client testees].
  -k, --key=             Server private key file for secure WebSocket (WSS)
                         [required in server modes for WSS].
  -c, --cert=            Server certificate file for secure WebSocket (WSS)
                         [required in server modes for WSS].
      --version          Display Twisted version and exit.
      --help             Display this help and exit.

0

\o/

Quand j’avais réussi à faire fonctionner wtest au travail, l’aventure n’était pas vraiment terminée. J’avais fait cela sur mon PC avec Internet mais je souhaitais monter un serveur de tests sur le réseau privé non relié à Internet… Il y avait une solution !

pip install --download . autobahntestsuite

Cette commande permet de télécharger toutes les dépendances nécessaires (y compris les récalcitrants pyasn1-modules et autre characteristic…) dans le dossier courant. On peut ensuite déplacer ce dossier sur le PC non connecté à Internet et lancer une installation hors-ligne. En effet, pip est capable d’installer à partir de dépendances qu’il cherche dans le dossier précisé plutôt que d’aller les chercher dans ses dépôts habituels :

pip install --no-index --find-links="/path/to/downloaded/dependencies" packagename

Dans mon cas, les dépendances n’étaient pas super bien gérées. J’ai eu des erreurs tels que distutils.errors.DistutilsError: Could not find suitable distribution for Requirement.parse('enum34') que j’ai corrigé en installant manuellement enum34 et cffi. Ces dépendances avaient pourtant bien été téléchargées mais l’installation du module autobahntestsuite n’arrivait pas à les installer toute seule.

Maintenant que tout est installé, on va quand même essayé de faire un vrai test du protocole, non ? Vous trouverez des détails sur l’utilisation de wtest sur cette page dédiée. Pour faire simple, je vais lancer un serveur de tests et je vais y connecter mon navigateur pour quel score il obtient. Le serveur se lance avec la commande suivante :

$ wstest -m fuzzingserver
Auto-generating spec file 'fuzzingserver.json'
Loading spec from /home/pgradot/fuzzingserver.json

Using Twisted reactor class 
Using UTF8 Validator class 
Using XOR Masker classes 

Autobahn WebSockets 0.7.2/0.10.9 Fuzzing Server (Port 9001)
Ok, will run 521 test cases for any clients connecting
Cases = ['1.1.1', '1.1.2', '1.1.3', '1.1.4', '1.1.
...
'13.7.16', '13.7.17', '13.7.18']

La commande ne rend pas la main tant que le serveur tourne. J’ai tronqué (avec ...) la liste des 521 test cases qui sont en attente de clients. Il suffit ensuite de lancer Firefox et d’aller à l’adresse http://localhost:8080 :

autobahn start test

Vous l’aurez deviner, il suffit de cliquer sur Test your browser pour lancer le test :

autobahn test running

On voit le compteur de tests exécutés se mettre à jour sur la page. Dans le terminal, on peut voir les traces d’exécution du serveur. En voici un extrait :

Running test case ID 12.5.11 for agent Firefox/40.0-20100101 from peer tcp4:127.0.0.1:60352
Running test case ID 12.5.12 for agent Firefox/40.0-20100101 from peer tcp4:127.0.0.1:60353
Running test case ID 12.5.13 for agent Firefox/40.0-20100101 from peer tcp4:127.0.0.1:60354

Quand les tests sont terminés, il faut cliquer sur le bouton Update Reports (Manual) pour mettre à jour les résultats, retourner à la page précédente et afficher les résultats des tests locaux :

autobahn results

Cette version 40 de Firefox ne fait pas un score de 100 %, certains tests sur les trames de fermeture de connexion sont à « fail« . Il affiche tout de même de très bons résultats 🙂

Bons tests !


Python is beautiful (again)

J’aime bien Python. Mon utilisation de ce langage est fortement axé sur l’écriture de petits scripts pour décortiquer les traces en console d’autres d’applications. Et pour ça, Python est très fort. Déjà parce qu’on peut commencer dans l’interpréteur interactif et ensuite mettre le code dans un fichier *.py pour le conserver. Ensuite, parce qu’il y a des écritures très efficaces pour faire cela. Dans cet article, je vous montre mon dernier script pour dire une nouvelle fois que « Python is beautiful ».

Pour commencer, voici la trace d’exécution. Il a appairage de deux périphériques au début, puis deux threads font des ON et des OFF, tandis qu’un troisième thread traite les communications et affiche les temps de traitement :

0
SwitchDeviceListener.deviceRegistered()
OFF
SwitchDeviceListener.deviceRegistered()
OFF
110
78
ON
390
ON
78
OFF
437
OFF
79
ON
421
ON
94
OFF
421
OFF
95
ON
405
[...]

L’objectif ici est de trouver les temps minimum, maximum et moyen. Pour cela, les listes marchent à merveille :

  • On crée une liste avec toutes les lignes du fichier.
  • On en dérive une liste ne contenant que les éléments qui ne sont pas dans une liste d’éléments à exclure grâce à not in.
  • On transforme les éléments de type string en int.
  • On utilise les opérateurs adaptés pour récupérer les valeurs recherchées.
from statistics import mean, median

# Read file content
f = open("C:\\Users\\pgradot\\Desktop\\data.txt", "r")
lines = f.readlines()
f.close()

# Clean content
to_exclude = ["SwitchDeviceListener.deviceRegistered()\n", "ON\n",  "OFF\n"]
cleaned = [line for line in lines if line not in to_exclude]
print(cleaned)

# Convert to numbers
numbers = list(map(int, cleaned))
print(numbers)

# Print statistics
print("Min = ", min(numbers))
print("Max = ", max(numbers))
print("Mean = ", mean(numbers))
print("Median = ", median(numbers))

Le script nous donne les valeurs recherchées :

Min =  0
Max =  578
Mean =  272.37404580152673
Median =  235.0

Le module statistics a été ajouté en version 3.4 de Python.

C’est beau, on est contents 🙂


Appeler Python depuis Ant

Aujourd’hui, pour des raisons professionnelles, j’ai eu besoin d’appeler Python depuis Ant. Il était en effet bien plus facile d’effectuer le traitement souhaité en Python qu’en Ant. Cet article retrace les points principaux pour appeler un script Python depuis un script Ant.

Pour l’exemple, je crée dans Eclipse un projet de type général nommé PythonFromAnt qui contient le script Ant, ant.xml, et le script Python, script.py.

Le script Python est très simple, il affiche son nom et renvoie le code passé via la ligne de commande :

import sys
print("hello, this script is " + sys.argv[0])
ret = int(sys.argv[1])
print("exit code = {0}".format(ret))
sys.exit(int(sys.argv[1]))

Pour appeler Python depuis Ant, il suffit d’utiliser la tâche Ant exec :

<?xml version="1.0" encoding="UTF-8"?>
<project name="example" default="main">

  <target name="main">
    <!– La propriété ant.file.PROJECT_NAME (ici, PROJECT_NAME="example") est toujours fournie par Ant –>
    <dirname file="{ant.file.example}" property="current.folder"/>

    <!– Le script Python retournera 0, signifiant que tout s’est bien passé –>
    <exec executable="python" dir="${current.folder}">
      <arg line="script.py 0"/>
    </exec>

    <!– Le script Python retournera 1 mais l’exécution Ant continue. On voit le résultat en rouge dans la console –>
    <exec executable="python" dir="${current.folder}">
      <arg line="script.py 1"/>
    </exec>

    <!– Le script Python retournera 1 et cela activera le "failonerror" d’Ant –>
    <exec executable="python" dir="${current.folder}" failonerror="true">
      <arg line="script.py 1"/>
    </exec>

    <echo>Ce message ne sera pas affiché</echo>
  </target>

</project>

Pour faire simple, j’ai ajouté Python au path, pour ne pas avoir à donner le chemin complet à exec. En arguments, je donne le chemin vers mon script Python et les paramètres à fournir à ce script. L’attribut dir permet de s’assurer que Python est lancé dans le dossier où se trouve le script qu’il cherche. Mon exemple est très simple, on aurait sans doute pu faire sans, mais dans un environnement de build plus important, le script Ant peut-être exécuté depuis un autre dossier et Python renverra une erreur car il ne trouve pas script.py.

Exécutez le script en faisant un clic-droit Run as…Ant build pour avoir la sortie suivante dans la console d’Eclipse :

Python from Ant - Sortie console


Voir le bytecode Python

Comme on en apprend tous les jours, j’ai aujourd’hui appris qu’il est très simple de voir le bytecode d’une fonction (en tout cas, pour l’implémentation de référence, CPython). Il suffit pour cela d’utiliser le module dis, qui est fourni par défaut dans Python.

Logo Python

Voici un exemple en Python 2.7.5 :

>>> def op(a,b,c):
...     return a + b - c * a
... 
>>> op
<function op at 0x10b815938>
>>> import dis
>>> dis.dis(op)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD          
              7 LOAD_FAST                2 (c)
             10 LOAD_FAST                0 (a)
             13 BINARY_MULTIPLY     
             14 BINARY_SUBTRACT     
             15 RETURN_VALUE        
>>> 

La page de documentation décrit aussi les instructions du bytecode.


Enregistrer l’historique de l’interpréteur de Python

Il est possible d’enregistrer l’historique des commandes exécutées par l’interpréteur Python. Il suffit pour cela d’utiliser la fonction built-in dédiée :
[codesource language=python]>>> import readline
>>> readline.write_history_file

>>> readline.write_history_file(« /Users/pierregradot/history.txt »)
>>> exit()[/codesource]
Voilà le contenu du fichier obtenu :

_HiStOrY_V2_
import40readline
readline.write_history_file
readline.write_history_file("/Users/pierregradot/history.txt")

Pour plus d’informations sur la gestion de l’historique de l’interpréteur, je vous renvoie à la documentation du module readline.