Wednesday, December 30, 2009

Migrer vers ruby 1.9

Je viens de migrer deux applications Rails 2.3.5 vers Ruby 1.9.1 avec deux problèmes majeurs concernant les encodings. Bien que Ruby soit unicode par défaut, pas mal de gems ne parlent pas l'UTF-8 par défaut...

Premier soucis avec ERB qui compile les templates en utilisant ASCII-8BIT par défaut. Cela génère l'erreur suivante:
ActionView::TemplateError (incompatible character encodings: ASCII-8BIT and UTF-8)

Il y a pas mal de discussions sur le sujet ici: https://rails.lighthouseapp.com/projects/8994/tickets/2188-i18n-fails-with-multibyte-strings-in-ruby-19-similar-to-2038

Une solution hacky à coup de monkey patch est de créer un fichier lib/actionview_utf8 comme ceci (c'est la ligne avec le source.force_encoding qui est rajoutée au code original):

module ActionView
  module Renderable #:nodoc:
    private
      def compile!(render_symbol, local_assigns)
        locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join

        source = <<-end_src
          def #{render_symbol}(local_assigns)
            old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
          ensure
            self.output_buffer = old_output_buffer
          end
        end_src
        source.force_encoding(Encoding::UTF_8) if source.respond_to?(:force_encoding)

        begin
          ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
        rescue Errno::ENOENT => e
          raise e # Missing template file, re-raise for Base to rescue
        rescue Exception => e # errors from template code
          if logger = defined?(ActionController) && Base.logger
            logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
            logger.debug "Function body: #{source}"
            logger.debug "Backtrace: #{e.backtrace.join("\n")}"
          end

          raise ActionView::TemplateError.new(self, {}, e)
        end
      end
  end
end

puis d'ajouter les lignes suivantes à environnement.rb:

Rails::Initializer.run do |config|
  ...
  config.after_initialize do 
    require 'lib/actionview_utf8'
  end
end

Ensuite je suis tombé sur le problème des adaptateurs aux bases de données qui retournent de l'ASCI-8BIT au lieu d'UTF-8 à la lecture des données. Une solution du même acabit est décrite ici: http://gnuu.org/2009/11/06/ruby19-rails-mysql-utf8/. Ceci dit ça ne fonctionne qu'avec MySql.

Donc on créé le fichier lib/mysql_utf8:

require "mysql"

class Mysql::Result
  def encode(value, encoding = "utf-8")
    String === value ? value.force_encoding(encoding) : value
  end

  def each_utf8(&block)
    each_orig do |row|
      yield row.map {|col| encode(col) }
    end
  end
  alias each_orig each
  alias each each_utf8

  def each_hash_utf8(&block)
    each_hash_orig do |row|
      row.each {|k, v| row[k] = encode(v) }
      yield(row)
    end
  end
  alias each_hash_orig each_hash
  alias each_hash each_hash_utf8
end

et pour environnement.rb on se retrouve avec:

Rails::Initializer.run do |config|
  ...
  config.after_initialize do 
    require 'lib/actionview_utf8'
    require 'lib/mysql_utf8'
  end
end

ouf, tous les tests passent...

Tuesday, December 29, 2009

Zend Framework avec lighthttpd

Je vais travailler sur un projet en PHP avec Zend Framework. Venant du monde Rails, on s'habitue assez vite à lancer son serveur web dans un terminal, voir plusieurs en parallèle sans avoir à manipuler la configuration d'un Apache qui tourne sous un autre compte utilisateur.

En PHP je n'ai pas trouvé d'équivalent à Webrick ou Mongrel intégré à ZF, mais je m'en suis finalement sorti avec lighthttpd.

A la racine du projet ZF, créez un lighthttpd.conf dans lequel vous configurez entre autres:
  • le port 3000 (on ne change pas de vieilles habitudes :)
  • l'URL rewriting pour que toutes les requêtes passent par index.php (l'équivalent du .htaccess tel que créé par ZF)
  • l'accès à votre application via FastCGI


ce qui donne quelque chose comme suit:


server.document-root = env.PWD + "/public"
server.port = 3000

server.modules = (
  "mod_rewrite",
  "mod_fastcgi"
)


mimetype.assign = (
  ".html" => "text/html", 
  ".txt" => "text/plain",
  ".jpg" => "image/jpeg",
  ".png" => "image/png" 
)

url.rewrite-once = (
  ".*\?(.*)$" => "/index.php?$1",
  ".*\.(js|ico|gif|jpg|png|css)$" => "$0",
  "" => "/index.php"
)

static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

server.indexfiles = ( "index.php" )

fastcgi.server = (
  ".php" =>
    ( "localhost" =>
      (
        "socket" => "/tmp/php-fastcgi.socket",
        "bin-path" => "/usr/bin/php-cgi"
      )
    )
)

Ensuite un petit script bash pour nous faciliter le lancement du serveur et l'initialisation de l'environnement ZF, par exemple start_zf.sh.

#!/bin/sh
export APPLICATION_ENV=development
lighttpd -f lighthttpd.conf -D

N'oubliez pas de rendre le script exécutable:
chmod +x start_zf.sh

puis lancez l'accès à votre projet ZF:

./start_zf.sh

Si tout va bien le site est accessible sur http://localhost:3000

Friday, December 18, 2009

SimpleWebDoc et yUML

Je viens d'interfacer SimpleWebDoc et yUML pour générer les diagrammes d'héritage entre les classes d'un même package. Le tout à un clic de souris.

Le package Tests:



Le package Kernel:



Saturday, December 12, 2009

SimpleWebDoc sur SqueakSource

Suite à mon billet précédent, Pharo et documentation des packages, j'ai continué a explorer Pharo pour ressortir les méthodes des classes et les commentaires contenus en en-tête des méthodes.

J'ai mis le package SimpleWebDoc sur SqueakSource. Cela me donne l'occasion d'expliquer comment charger un package depuis Monticello. Tout d'abord lancez le Monticello Browser depuis le menu World.




Cela ouvre une fenêtre dans laquelle on voit les packages installés dans le panneau de gauche, les repository (dépôts) dans le panneau de droite. L'étape suivante est d'ajouter le repository où se trouve SimpleWebDoc dans la liste de ceux enregistrés dans Monticello. Cliquez sur le bouton +Repository et sélectionnez HTTP (Note: un repository Monticello est un simple répertoire local ou distant où se trouve les packages zippés, avec l'extension .mcz).



Saisissez ensuite l'emplacement du repository, ici: http://www.squeaksource.com/SimpleWebDoc




Le repository ajouté, sélectionnez-le et cliquez Open.



Une fenêtre s'ouvre listant tous les packages présents dans le repository (panneau de gauche) et la liste des versions disponibles (panneau de droite), la plus récente en tête. Sélectionnez la version la plus récente et cliquez sur Load pour charger le package SimpleWebDoc.



SWDocView est automatique enregistré comme application Seaside au chargement. Si vous regardez la méthode de classe initialize de SWDocView:


initialize
  self registerAsApplication: 'view-doc'

Une classe est un objet, et comme tout objet son instanciation appelle initialize. En chargeant le package SimpleWebDoc, on instancie la meta-classe SWDocView (elle même instance de MetaClass). SWDocView s'enregistre alors comme application nommée view-doc.


Vous pouvez donc ouvrir votre navigateur web préféré et aller à l'adresse:




Les méthodes de classe sont en italique et soulignées:



Thursday, December 10, 2009

The Smalltalk way to learn Smalltalk

Pour apprendre un nouveau langage, avant de me jeter sur le compilateur / interpréteur et tenter d'écrire un (mauvais) programme, j'achète un ou deux livres bien perçus pour m'imprégner de la philosophie et objectifs du langage. Je lis quelques programmes.

Pour Smalltalk je crois avoir commis une erreur et j'ai mis du temps à tout simplement "rentrer dedans", bien plus que tous les langages profondément objets étudiés auparavant (dont Ruby ).

I'm still amazed by how many people think they can grok Smalltalk by seeing syntax examples. Smalltalk isn't its syntax, it's its environment. Smalltalk is a living world of running objects, there are no files, no applications, just what's running. To understand Smalltalk, you have to either actually use it for a while, or have a seasoned Smalltalker demonstrate it to you. Reading sample code just won't cut it.
(Ramon Leon, Why Smalltalk)

C'est un vrai conseil ;)

Et je comprends maintenant ceci: Object oriented programming means something entirely different to a Smalltalker than to someone doing OO in another language.

Au passage, le blog de Ramon Leon OnSmalltalk est une vrai mine d'or, articles complets, bien écrits, profonds et les commentaires n'en valent pas moins.

Sunday, December 6, 2009

Pharo et documentation des packages

Voici quelques expérimentations faites alors que je voulais explorer les documentations contenues dans Pharo. Dans ce billet nous verrons comment ressortir les commentaires des classes d'un package donné. Puis je décrirai le codage d'une application Seaside minimaliste pour visualiser cette documentation depuis un navigateur Web.

1. Voyage au centre du package

Les classes sont regroupées dans des packages gérées par Monticello. On peut faire l'analogie avec les gems du monde Ruby ou les gestionnaires de paquets des distributions Linux.

Chaque package étant une instance de la classe MCPackage, on peut en sortir la liste. Ouvrez un Workspace et affichez le résultat du code suivant (via click-droit, print it)

MCPackage allInstances



Cela devrait vous retourner une longue liste de packages:



Comptons en le nombre en évaluant la taille du tableau retourné:

MCPackage allInstances size.

Sur mon image je trouve 155 packages.

Chaque package a un nom via l'accesseur name. On peut ainsi en sortir la liste:

MCPackage allInstances collect: [:p| p name]

En triant par ordre alphabétique:

(MCPackage allInstances collect: [:p| p name]) sort

Affichons le tout dans un Transcript. Ouvrez-le via le menu World > Tools > Transcript.



Puis évaluez le code suivant dans le Workspace:

|packageNames|
packageNames := MCPackage allInstances collect: [:p| p name].
packageNames sort do:[:name| Transcript show:name; cr]





Les objects PackageInfo référencent toutes les classes d'un package. Chaque objet MCPackage est associé à un objet PackageInfo.

Prenons par exemple le premier package et affichons toutes les classes qu'il contient via l'accesseur classes:

MCPackage allInstances first packageInfo classes




Pour extraire toutes les classes d'un package donné, une petite sélection fait l'affaire. Le code suivant renvoie toutes les classes du package System-Tools:

|systemTools|
systemTools := (MCPackage allInstances select: [:p| p name = 'System-Tools']) first.
systemTools packageInfo classes.





On peut accéder au commentaire de chaque classe via l'accesseur comment. Affichons le commentaire de la classe SpaceTally dans un Transcript:

Transcript show:SpaceTally comment



De là à afficher toutes les classes d'un package avec leurs commentaires, il n'y a qu'un pas:

|systemTools|
systemTools := (MCPackage allInstances select: [:p| p name = 'System-Tools']) first.
systemTools packageInfo classes do:[:c|
    Transcript show: '***',c name; cr.
    Transcript show: c comment; cr; cr]






2. Application Seaside pour consulter les commentaires

Rajoutons maintenant un peu de bling-bling à tout ça. Un Transcript c'est bien, une page web c'est plus rigolo.

Créons le package SimpleWebDoc qui contiendra une application Seaside minimaliste permettant de parcourir les commentaires des classes des packages.

Quelques précisions pour commencer. J'utilise ici l'image Pharo 1.0 RC1 09.11.04 avec Seaside et Pier, téléchargeable sur le site de Pharo à l'heure où j'écris ces lignes.
Le navigateur de classes utilisé par défaut est Browser (pour des raisons de performances et stabilité), or je vais utiliser le Package Browser. Pour changer le navigateur par défaut, ouvrez le navigateur de classe et dans le menu de la fenêtre sélectionnez Choose new default Browser.



Ici sélectionnez O2PackageBrowserAdaptator.



Lancez un nouveau navigateur de classe, vous devriez obtenir le Package Browser.



Ajoutons maintenant le package SimpleWebDoc: click droit, create package.



Nouveau clic droit sur le package et choisissez various > add to smart groups pour le rajouter dans l'onglet groups du Package Browser.





Créer application Seaside nécessite d'hériter WAComponent et de l'enregistrer comme application. Voici la déclaration de la classe SWDocView:

WAComponent subclass: #SWDocView
 instanceVariableNames: ''
 classVariableNames: ''
 poolDictionaries: ''
 category: 'SimpleWebDoc'

Pour afficher un composant, Seaside lui envoie le messager renderContentOn: en lui passant un objet WARenderCanvas sur lequel dessiner notre page web (en utilisant des brosses/brush). Nous allons ici construire un formulaire affichant tous les packages:

renderContentOn: html
 html form: [
  html select 
   list: (MCPackage allInstances collect: [:p| p name])
 ].

Améliorons la lisibilité du code en créant allPackageNames qui retourne les noms de tous les packages, dans l'ordre alphabétique:

allPackageNames 
 ^ (MCPackage allInstances collect: [:p| p name]) sort

et ajustons renderContentOn: en conséquence:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
 ].

Testons ce que donne ce premier code. Déclarez le composant SWDocView comme application en évaluant ce qui suit dans un Workspace:

SWDocView registerAsApplication: 'simple-web-doc'

Pointez ensuite votre navigateur Web à l'adresse: http://localhost:8080/seaside/simple-web-doc



Ajoutons un bouton pour valider le formulaire:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
  html submitButton with:'show'
 ].

Maintenant la partie intéressante: lorsqu'on valide le formulaire, on affiche la documentation du package. Pour cela nous stockons le nom sélectionné dans la variable d'instance selectedPackage en utilisant le callback de la brosse WASelectTag:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
   callback: [:value| selectedPackage := value].
  html submitButton with:'show'
 ].

Si selectedPackage est définit, affichons la documentation:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
   callback: [:value| selectedPackage := value].
  html submitButton with:'show'
 ].
 
 selectedPackage ifNotNil: [
   self renderPackage:selectedPackage On:html.
 ]

La méthode SWDocView>>renderPackage:On: qui reprends l'exemple précédent:

renderPackage:packageName On:html
  |package|
  package := (MCPackage allInstances select: [:p| p name = packageName]) first.
  package packageInfo classes do: [:c| 
    html heading level:2; with: c name.
      html break.
      html text: c comment.
      html horizontalRule.
  ]

et voici le résultat:



Amenons un peu de lisibilté en écrivant une méthode qui retourne toutes les classes d'un package:

classesOfPackageNamed: packageName
 |package|
 package := (MCPackage allInstances select: [:p| p name = packageName]) first.
 ^ package packageInfo classes

d'où une mise à jour de renderPackage:On:

renderPackage:packageName On:html
 (self classesOfPackageNamed:packageName) do: [:c| 
  html heading level:2; with: c name.
  html break.
  html text: c comment.
  html horizontalRule.
  ]


Bon nombre de choses sont à réaliser pour avoir une navigation correcte. Pour aller plus loin, consultez Dynamic Web Development with Seaside.

Monday, November 16, 2009

Minitube: youtube sans flash

Attention, killer app. en vue: minitube permet de rechercher et visionner les vidéos de YouTube, le tout sans flash ! C'est rapide, simple et agréable à utiliser.


Minitube apporte l'originalité de lire les résultats de recherche les uns à la suite des autres, créant ainsi une sorte de programme TV sur demande.




Sous ArchLinux, j'ai eu besoin d'installer:

  • le backend xine pour phonon: sudo pacman -S phonon-xine
  • minitube via AUR: yaourt -S minitube

Tuesday, November 3, 2009

L'outil n'est pas le problème...

(...ni forcément la solution)

Henrik Kniberg (gourou de l'agilité) utilise régulièrement cette image dans ses présentations:


Cela peut prêter à sourire, mais j'en ressort quelques règles:

  • évaluez les outils avant d'en choisir un, vérifiez qu'il correspond bien à vos besoins
  • connaissez intimement votre outil avant de l'utiliser en production: lisez des livres, retours d'expérience, internet est là pour vous aider
  • sachez dans quels cas ne pas l'utiliser
J'ai vu des équipes choisir Ruby et Rails après des déboires en PHP, pensant que Rails "c'est mieux". Aucun test unitaire, aucun apprentissage des conventions,... bref, retour au spaghetti code et déboires.

D'autres mettent en place un Subversion et pestent contre l'outil car ils se retrouvent constamment avec des conflits à chaque commit. Le fonctionnement par lock était mieux.

Ne parlons pas de tous les projets bancals en Visual Basic car VB, "c'est simple !".

Contrairement aux slogans, l'informatique, la programmation sont de plus en plus complexes. Il est de la responsabilité du développeur de se former.

Ruby, Python, Git, Scrum... sont des outils très puissants. "With great power, comes great responsibility": apprenez à maîtriser votre pouvoir !!

Wednesday, October 21, 2009

Groupe de lecture en entreprise

J'apprends essentiellement par la lecture de (bons) livres: nouveaux langages, pratiques de développement, management... Or j'ai toujours éprouvé beaucoup de difficultés à faire lire ces mêmes livres à des collègues. Il semble de notoriété publique qu'un livre qui touche de près ou de loin à l'informatique est forcément difficile et ennuyeux à lire (pour être poli). Si de plus rédigé en anglais ...

Pour se donner le courage d'étudier un livre ensemble, d'apprendre et d'échanger autour de nouvelles connaissances, nous avons formé il y a deux mois un groupe de lecture dans mon entreprise. Nous avons commencé à trois personnes et maintenant nous sommes cinq, je croise les doigts pour que ça continue !

Voici la démarche que nous avons adopté:

1. Sélectionner un ouvrage:

  • Choisir un domaine dans lequel le groupe veut acquérir de nouvelles connaissances. Par exemple: Ruby, Python, Scrum, ...
  • Chercher et proposer une liste réduite de livres et/ou essais sur un thème donné. Se fier aux critiques des lecteurs pour avoir la certitude que le livre est bon. Attention aux mauvaises traductions: les budgets de traduction entre un Harry Potter et un livre d'informatique semblent très différents ;).
  • Acheter les livres (ou le PDF et imprimer). Chacun doit avoir son exemplaire.

2. Choisir une organisation:

A quelle heure se rencontre t'on ? Combien de fois par semaine ? Quels jours ? Quelle est la durée de la session ?
Dans notre cas, nous nous réunissons tous les Lundi et Jeudi à 12h, pendant 45 mn maximum.

Une personne doit être leader du groupe et s'assurer du respect de l'organisation (un Scrum Master du groupe de lecture en quelque sorte).

3. La préparation de la prochaine rencontre:

On choisit collectivement ce qui doit être lu pour la prochaine rencontre: les trois sections suivantes, le prochain chapitre.

Une personne doit être désignée (à tour de rôle) pour faire le résumé de la partie à lire.


4: La rencontre:

Tout le monde se retrouve autour d'une table. La personne en tâche de faire le résumé s'exécute. L'objectif est d'échanger, débattre et clarifier chacun des points abordés.

A la fin de la rencontre, retour au point 3 pour préparer la rencontre suivante.


Nous avons commencé cette pratique par la lecture de l'essai How to be a Programmer: A Short, Comprehensive, and Personal Summary de Robert L.Dead. Cela nous a permis de nous mettre en jambe avec un document court (quatre rencontres) et de valider notre fonctionnement.

Nous sommes ensuite passés à Plonger au cœur de Python (traduction de Dive Into Python de Mark Pilgrim) qui a l'avantage d'être ludique (et en français me diront certains).

Friday, October 16, 2009

Privoxy et NetworkManager

Je suis passé à NetworkManager pour gérer les connexions réseaux sur mon portable et je trouve enfin cet outil fonctionnel et pratique. Seul petit soucis: privoxy (un proxy que j'utilise comme bloqueur de pubs, ce qui donne l'avantage de fonctionner quel que soit le navigateur web utilisé) perdait un peu les pédales à chaque fois que NetworkManager reconfigurait le réseau et j'étais obligé de le redémarrer.

Mais je viens de découvrir que NeworkManager peut exécuter des scripts à chaque reconfiguration réseau. Je dégaine mon éditeur et voici un script à placer dans /etc/NetworkManager/dispatcher.d qui démarre ou arrête privoxy selon que le réseau soit disponible ou non (attention, c'est pour ArchLinux):


#!/bin/bash

LOGGER="/usr/bin/logger -s -p user.notice -t NetworkManagerDispatcher"
PID=`pidof -o %PPID /usr/sbin/privoxy`

if [ -n $1 ] && [ $2 == "up" ]; then
if [ ! -z "$PID" ]; then
$LOGGER "Privoxy is running, restart"
/etc/rc.d/privoxy restart
else
$LOGGER "Privoxy is stopped, start"
/etc/rc.d/privoxy start
fi
fi

if [ -n $1 ] && [ $2 == "down" ]; then
if [ ! -z "$PID" ]; then
$LOGGER "Privoxy is running, stop"
/etc/rc.d/privoxy stop
fi
fi



En prime vous aurez droit à quelques logs pour vérifier que ça fonctionne:

$ cat /var/log/messages.log |grep "NetworkManagerDispatcher"
...
Oct 15 18:42:09 magalo NetworkManagerDispatcher: Privoxy is stopped, start
Oct 15 18:42:11 magalo NetworkManagerDispatcher: Privoxy is running, stop
Oct 15 18:42:15 magalo NetworkManagerDispatcher: Privoxy is stopped, start
...

Friday, October 2, 2009

Embarquer Webkit

Pour une application embarquée j'avais besoin vite fait d'un navigateur web sans fioritures et support CSS + Javascript. Finalement c'est tout simple avec Python et Webkit.

Avec pywebkitgtk :

import gtk 
import webkit 

view = webkit.WebView() 

sw = gtk.ScrolledWindow() 
sw.add(view) 

win = gtk.Window(gtk.WINDOW_TOPLEVEL) 
win.add(sw) 
win.show_all() 

view.open('http://acid3.acidtests.org/') 
gtk.main()


La mếme chose avec pyQt:

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtWebKit import *

app = QApplication(sys.argv)
web = QWebView()

sa = QScrollArea()
sa.setWidget(web)
sa.show()

web.load(QUrl('http://acid3.acidtests.org/'))

sys.exit(app.exec_())


Thursday, September 17, 2009

Ruby snippets pour Emacs

Je viens de découvrir une excellente bibliothèque de gestion de snippets pour Emacs: yasnippet. Rapide à mettre en oeuvre, une bonne documentation et plein de snippets disponibles !!

Saturday, September 12, 2009

Les lectures de l'été

Je me suis fait une nouvelle petite cure pragmatique cet été. Les ouvrages du Pragmatic Bookshelf me plaisent toujours autant: bien construits, écriture fluide. Le soucis c'est que le livre finit on s'aperçoit qu'on a encore bien plus à apprendre qu'avant de commencer...


The Passionate Programmer: Creating a Remakable Career in Software Development de Chad Fowler adopte une structure similaire à Pragmatic Programmers: From Journeyman To Master. Chacune des 53 sections détaille une pratique, une attitude, une réflexion pour améliorer la prise en main de notre carrière de développeur et être reconnu par les différentes "tribus" (programmeurs, managers, clients, ...) avec lesquelles on se doit de communiquer. Le livre est très intéressant, convaincant, avec quelques bonnes anecdotes.
J'ai surtout aimé les parallèles avec l'apprentissage d'un instrument de musique et les habitudes, aspirations des musiciens. En musique on se doit de pratiquer régulièrement notre instrument et de travailler aux limites de ses possibilités pour progresser. L'amélioration de ses compétences de programmeur nécessite aussi d'adopter un rythme d'entraînement pour pratiquer sa technique.
Apprendre un instrument de musique différent de celui qu'on joue constitue un bon moyen de s'améliorer. Par exemple, un saxophoniste gagnera beaucoup à apprendre la basse ou la batterie, un bassiste le piano, ... Il est en effet plus rapide d'acquérir un jeu rythmique avec la basse ou la batterie, un jeu polyphonique avec le piano ou la guitare, ... De même, un développeur C peut apprendre Python, un développeur Java le Lisp, Erlang, ... pour découvrir des techniques plus facilement qu'avec le langage qu'il connaît. Si vous voulez comprendre la méta-programmation, utilisez Ruby. Pour l'objet prenez Smalltalk....


Pragmatic Version Control Using Git de Travis Swicegood fait partie du Pragmatic Starter Kit (contrôle de version, test unitaires, automatisation) qui constitue la boîte à outil de base du développeur pragmatique ;). Travis Swicegood présente les différents types de VCS puis détaille l'utilisation de Git jusqu'à des concepts avancés. Le livre est bien structuré, les exemples manquent parfois un peu de profondeur à mon goût. J'ai bien aimé la présentation de l'interface Subversion de Git (git svn) qui permet de synchroniser son dépôt Git local avec un dépôt Git central. Cela permet de profiter des apports de Git même si l'équipe utilise Subversion, ou bien faire une migration en douceur. Les fonctionnalités de séparer un commit en plusieurs et vice-versa constituent un bon complément aux lacunes de Subversion sur ce point (par exemple en période de mise au point où on corrige plusieurs bugs en même temps et qu'on veut faire un commit par correction de retour au bureau).


Enfin, Andy Hunt et Dave Thomas décrivent différentes techniques de test unitaires dans Pragmatic Unit Testing in C# with NUnit. Le livre détaille les cas auxquels nous sommes couramment confrontés dans l'écriture de tests unitaires (mocks, travailler avec des bases de codes non testées, le test d'interfaces graphiques) et liste exhaustivement les points à vérifier pour avoir des tests complets. Ceci dit, pour apprendre le développement piloté par les tests, je conseillerais plutôt l'ouvrage de Dave Astels "Test Driven Development: A Practical Guide". Le livre de Hunt et Thomas, certes de qualité, me paraît moins accessibles par les développeurs qui n'ont jamais écrit de tests.

Friday, August 21, 2009

Deux outils UML

J'aime les outils UML qui permettent de créer des diagrammes rapidement. Malheureusement la plupart des outils offrent une interface très clickodrome avec des popups dans tous les sens qui alourdit leur utilisation.

Deux outils UML vraiment très sympas sortent du lot:

  • UMLet: écrit en Java, chaque élément graphique est affiché selon une entrée texte avec une syntaxe légère. Par exemple:



Movies
--
-moviesCollection:OrderedCollection
--
+add(aMovie)
+includes(aMovie):Boolean

affichera ceci:





  • yuml.me: c'est un service web qui génère un diagramme de classe à partir d'une description texte. On peut ensuite intégrer ce diagramme sur une page web. Par exemple:


[Movies|-moviesCollection:OrderedCollection|+add(aMovie);
+includes(aMovie):Boolean]+-*[Movie|+title],
[Movies]++-moviesCollection[OrderedCollection]

Génère ceci:


Monday, July 13, 2009

Pharo et tests unitaires (4): 1 Trait acheté, 4 tests offerts !!

Dernière partie de la série:
Smalltalk ajoute à l'héritage un autre mécanisme de réutilisation de code, les Traits. Pour (sur-)simplifier, un Trait fournit un ensemble de méthodes définissant un comportement. Plusieurs classes peuvent utiliser un même Trait et une classe peut être composée de plusieurs Traits. Cela nous offre un moyen d'éviter des duplications de code en mutualisant des fonctions ou comportements génériques entre les classes.

On peut faire l'analogie avec les modules de Ruby (appelés aussi mixins). Par exemple, le module Observable implémente le design pattern du même nom. Si une classe inclut ce module, elle possède de fait les méthodes add_observer, notify_observers, ... Si cette même classe doit aussi être Singleton, il suffit de lui inclure le module en question.

Voici un exemple extrêmement scientifique:
require 'observer'
require 'singleton'

class Agathe
include Observable
# Agathe est unique
include Singleton

def reveille_toi
# ça peut mettre un peu de temps 
sleep 10*rand

changed true
notify_observers
end
end

class Laurent
# Laurent est unique aussi ;)
include Singleton

def initialize
Agathe.instance.add_observer self
  end

def update
puts "C'est pas trop tôt"
end

def reveille_agathe
Agathe.instance.reveille_toi
end
end

Laurent.instance.reveille_agathe

et le résultat:

$ ruby test_modules.rb
C'est pas trop tôt

Pour plus de détails sur les Traits de Smalltalk, vous pouvez consulter le document Traits: Composable Units of Behaviour(Nathanel Schärli, Stéphane Ducasse, Oscar Nierstrasz, Andrew P.Black).

La manière d'utiliser les Traits pour les tests unitaires dans Pharo m'a interpellé.

On écrit de nombreux tests unitaires de base qui se ressemblent, surtout lorsqu'on manipule des listes, des collections. Si on reprends notre exemple d'une classe qui représente une collection de films (Movies), on écrit à peu près les tests suivants:
  • pour une instance de Movies toute fraîche:
    • size retourne 0
    • isEmpty retourne true
  • j'ajoute un film (via Movies#add)
    • size retourne 1
    • isEmpty retourne false
  • j'ajoute trois films
    • size retourne 3
  • je retire un film
    • ....
C'est sympa les premières fois mais la pratique devient vite répétitive. Heureusement, certains frameworks permettent de réduire ce genre de tests à une simple ligne de code (avec Shoulda dans le monde Rails par exemple).

Dans Pharo, des Traits dédiés à faciliter l'écriture des tests ont été écrits. On peut le voir en parcourant le package Collection-Tests. Les Traits sont par convention préfixés de la lettre T.







1. Test de la classe Movies avec le Trait TSizeTest

Créons notre champ d'expérimentation en ajoutant la classe MoviesWithTraitsTest:

Object subclass: #MoviesWithTraitsTest
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Movies'

Si vous avez bien suivi le premier billet sur les tests d'une Collection, nous testons la méthode Movies#size avec une collection vide, puis contenant un et plusieurs éléments. Et ça tombe bien, le Trait que TSizeTest existe.



Indiquons à notre classe de test d'utiliser ce Trait. Modifiez la définition de la classe comme suit:

Object subclass: #MoviesWithTraitsTest
uses: TSizeTest
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Movies'

et auto-magiquement, les méthodes du Trait sont ajoutées dans MovieWithTraitsTest.



Etudions la méthode MovieWithTraitsTest#test0TSizeTest:

test0TSizeTest
self shouldnt: [self empty] raise: Error.
self shouldnt: [self sizeCollection] raise: Error.
self assert: self empty isEmpty.
self deny: self sizeCollection isEmpty.


Les deux première lignes vérifient que les méthodes MovieWithTraitsTest#empty et MovieWithTraitsTest#sizeCollection ne lancent pas d'exception.

D'une part, les Traits ne peuvent définir de variables. D'autre part l'utilisateur du Trait a le devoir d'implémenter certaines méthodes pour le bon fonctionnement ce ce Trait.

Nous pouvons le vérifier en ouvrant MovieWithTraitsTest#sizeCollection:

sizeCollection
"Answers a collection not empty"
    ^ self explicitRequirement


Bon, allons examiner explicitRequirement pour en connaître les détails. Clic droit sur la méthode et sélectionnez implementors.



Cela nous amène à la définition de Object#explicitRequirement:

explicitRequirement
self error: 'Explicitly required method'

qui lance explicitement une jolie erreur.

Nous devons donc implémenter MovieWithTraitsTest#empty et MovieWithTraitsTest#sizeCollection. Commençons par la première en retournant une instance de Movies vide:

empty
^ Movies new

et une instance de Movies avec quelques éléments pour la suivante:

sizeCollection
"Answers a collection not empty"
    | movies |
movies := Movies new.
#('Amélie' 'Alien 4' 'Délicatessen') do: [:aTitle|
movies add: (Movie newWithTitle: aTitle)].
^ movies

et lançons les tests. Le debugger se manifeste:




MovieWithTraitsTest#test0TSizeTest vérifie le retour de la méthode Movies#isEmpty qui n'existe pas. Créons la méthode:



isEmpty
^ moviesCollection isEmpty



puis relançons les tests. C'est maintenant le test MovieWithTraitsTest#testSize qui s'arrête car Movies#do: aBlock n'est pas implémentée. Appuyons nous toujours sur moviesCollection:


do: aBlock
moviesCollection do: aBlock 


et puis tout va beaucoup mieux.




2. Notre propre Trait

Allons plus loin en implémentant notre propre Trait de test. Pour reprendre la démarche du billet précédent, nous allons créer des tests vérifiant que Movies#includes: aMovie retourne true pour les instances de Movie qu'il contient, false dans le cas contraire.

Le test à false est plus simple à réaliser. Voici la méthode MoviesWithTraitsTest#testIncludesElementNotInReturnsFalse:
testIncludesElementNotInReturnsFalse
self deny: (self sizeCollection includes: self elementNotIn).
elementNotIn représente une instance qui n'est pas censée se trouver dans sizeCollection. Comme les Traits ne possèdent pas de variables d'instances, nous passons par une méthode:
elementNotIn
^ Movie newWithTitle: 'City Of Lost Children'.
Vérifiez le bon fonctionnement des tests, cela doit passer comme une lettre à la poste (quand il n'y a pas grève).

Passons au test opposé:
testIncludesElementInReturnsTrue
self assert: (self sizeCollection includes: self elementIn).
Cette fois-ci, MoviesWithTraitsTest#elementIn doit retourner une instance de Movie précédemment ajoutée à la collection retournée par sizeCollection. Nous devons passer par une variable d'instance.
elementIn
^ aVeryLongEngagement
et déclarez aVeryLongEngagement comme variable d'instance.




Initialisez ensuite cette variable dans la méthode setUp:
setUp
aVeryLongEngagement := Movie newWithTitle: 'A Very Long Engagement'

et nous pouvons ajouter l'instance dans MoviesWithTraitsTest#sizeCollection:
sizeCollection
"Answers a collection not empty"
| movies |
movies := Movies new.
#('Amélie' 'Alien 4' 'Délicatessen') do: [:aTitle|
movies add: (Movie newWithTitle: aTitle)].
movies add: aVeryLongEngagement.
^ movies

Ceci fait, nos tests passent.



3. Création du Trait


Nous allons maintenant extraire nos deux méthodes de test vers un nouveau Trait. Faites un clic droit sur la méthode testIncludesElementInReturnsTrue et sélectionnez move to trait...



Pharo demande le Trait dans lequel déplacer la méthode. Nous créons un nouveau Trait, cliquez *New Trait*:



puis choisissez lui un joli nom, ici TCollectionIncludesTest:



et votre nouveau Trait est né.



Si vous revenez à la définition de la classe MoviesWithTraitsTest, vous remarquerez qu'elle utilise notre Trait:



Celui-ci s'appuie sur deux méthodes: sizeCollection et elementIn:
testIncludesElementInReturnsTrue
self assert: (self sizeCollection includes: self elementIn).
elles sont destinées à être implémentées par les classes qui utilisent le Trait. Autant le rendre explicite. Rajoutons les méthodes suivantes à TCollectionIncludesTest:

sizeCollection
"Answers a collection not empty"
^ self explicitRequirement


elementIn
"Answers an instance included in self sizeCollection"
^ self explicitRequirement


Ajoutons enfin MoviesWithTraitsTest#testIncludesElementNotInReturnsFalse au Trait:



et complétons par la méthode elementNotIn:

elementNotIn
"Answers an instance not included in self sizeCollection"
^ self explicitRequirement


Voilà, le Trait peut être utilisé par d'autres classes.

4. Avantages et inconvénients.

L'utilisation des Traits apporte les avantages suivants:
  • la cohérence: un Trait définit un comportement, et les tests utilisant des Traits testeront ces mêmes comportements. Cela garantit que les mêmes noms de méthodes sur des objets de type différent aboutiront à un comportement similaire. On peut vérifier les conventions.
  • réutilisation: la mutualisation des méthodes de test évite les duplications de code pour tester des comportements similaires.

Ceci dit des effets de bord apparaissent. Si la granularité des Traits n'est pas assez fine, on se retrouve obligé à implémenter beaucoup de méthodes d'un coup. Par exemple, lorsque nous avons utilisé TSizeTest, nous avons dû implémenter Movies#do: et Movies#isEmpty, méthodes qui ne nous intéressaient pas forcément. Un Trait avec plus de tests nous aurait obligé à implémenter encore plus de méthodes. Cela peut casser la démarche de développement piloté par les tests.

Les Traits de test sont un atout, mais je pense important:
  • de bien étudier un Trait avant de l'utiliser
  • de garder des Traits avec un périmètre de test limité

Sunday, July 5, 2009

Pharo et tests unitaires (3): les collections, seconde partie

Episode 3, saison 1.
Résumé de l'épisode précédent:

Inspector Thomas D. Derrick was facing Mister M., a supposed movies dealer. He had to find the truth. What did Mister M. do with the movies ?

- Where do you put the movies ? T.D.Derrick asked.
- I'm a Collection. I keep them.
- You're a Collection, aren't you ? So what's your size ?
- Well ... Zero now. I'm empty !!
- I'm sure you lie. Take this movie ... "Star Wars". Take it !
- OK OK, done.
- And tell me what's your size now ...
- One. You see, I have it.

These right answers did not disturbed T.D.Derrick.

- Play again with me. Add these movies. "Blade Runner" and ... this one, "Alien".
- Done.
- And now, your size ?
- Three. Can I go now ?


Mister M. seemed to be a real Collection. T.D.Derrick felt he was missing something. Then a subtil smile appeared on his face:

- Do you include Star Wars ?
- Err ... hard to tell. I don't understand.
- What ?
- I don't understand "include"
- Yep, I got you now !!
- Damned ...

Au dernier billet, j'ai décrit l'implémentation, pilotée par les tests, de la méthode Movies#size. La méthode retourne le nombre de films ajoutés via Movies#add.


Néanmoins, Movies utilise un compteur et ne passe pas par une Collection interne pour stocker les instances de Movie. Les tests nous ont permis de développer la solution la plus simple possible.

1. Test: Movies#includes: aMovie

Ajoutons un autre test pour notre collection: Movies#includes: aMovie pour une instance de Movie précédemment ajoutée retourne vrai, sinon faux.

testIncludesAnAddedMovieReturnsTrue
| movies starWars bladeRunner alien |
movies := Movies new.

starWars := Movie newWithTitle: 'Star Wars'.
bladeRunner := Movie newWithTitle: 'Blade Runner'.
alien := Movie newWithTitle: 'Alien'.

movies add: starWars; add: bladeRunner.        

self assert: (movies includes: starWars).
self assert: (movies includes: bladeRunner).
self deny:  (movies includes: alien).

Notez l'appel self deny : si la méthode TestCase#assert vérifie que l'expression en paramètre retourne true, TestCase#deny passe si l'expression testée retourne false.

Lancez le test. Le debugger s'arrête car Movies#includes:aMovie n'existe pas.

Comme vu au billet précédent, utilisez le bouton Create Method pour la déclarer.



Cette fois-ci, on doit vraiment passer par une collection interne.
includes: aMovie
^ moviesCollection includes: aMovie


Déclarons moviesCollection comme variable d'instance pour la rendre accessible aux autres méthodes de la classe Movies.



Comme toute variable d'instance, elle doit être initialisée. Au hasard, utilisons une instance d'OrderedCollection. Editez Movies#initialize:

initialize
super initialize.
size:=0.
moviesCollection:=OrderedCollection new

En relançant le test, le debugger indique que la première assertion ne passe pas. En utilisant la fonctions watch it sur (movies includes: starWars), le retour est en effet false.












Utilisons un autre outil de debuggage. Sélectionner movies, puis clic-droit, inspect it.




La fenêtre qui apparaît permet de visualiser l'état de l'instance Movies. En cliquant sur size, on voit que la méthode retourne 2. Cela semble logique, nous avons ajouté les instances "Star Wars" et "Blade Runner".



Regardons moviesCollection. Nous avons bien une instance d'OrderedCollection, mais vide (rien entre les parenthèses).



Pour s'en assurer, utilisons l'éditeur de la fenêtre pour demander la taille de moviesCollection. Tapez

moviesCollection size

puis utilisez la fonction inspect it.





On voit que size retourne bien 0 (une instance de la classe SmallInteger).

Les instances de Movie ne sont donc pas ajoutées à moviesCollection. Examinons la méthode Movies#add:
add: aMovie
size := size + 1

En effet ... modifions la méthode:
add: aMovie
size := size + 1.
moviesCollection add: aMovie

et maintenant le test passe !




2. Refactoring.


2.1 movies, starWars, bladeRunner et alien

Supprimons les duplications. Avant toute chose, vérifions que tous les tests de notre package Movies passent.

Les duplications se trouvent dans MoviesSizeTest. Les instances de Movie pour "Star Wars", "Blade Runner" et "Alien" sont créées dans les méthodes testWithOneMovieSizeReturnsOne, testWithThreeMoviesSizeReturnsThree et testIncludesAnAddedMovieReturnsTrue. De plus, chacune des méthode de test instancie Movies.

TestCase propose les méthodes setUp et tearDown pour ce genre de situation. TestCase#setUp est exécutée avant chaque test, et tearDown aprés chaque test. Lorsqu'on exécute tous les tests de MoviesSizeTest, l'appel des méthodes est le suivant (à l'ordre des tests près):
  1. setUp
  2. testIncludesAnAddedMovieReturnsTrue
  3. tearDown
  4. setUp
  5. testNewInstanceSizeReturnsZero
  6. tearDown
  7. setUp
  8. testWithOneMovieSizeReturnsOne
  9. tearDown
  10. setUp
  11. testWithThreeMoviesSizeReturnsThree
  12. tearDown
Nous allons mettre en commun la création des instances de Movie dans la méthode setUp. Définissez MoviesSizeTest#setUp comme suit:

setUp
movies := Movies new.
starWars := Movie newWithTitle: 'Star Wars'.
bladeRunner := Movie newWithTitle: 'Blade Runner'.
alien := Movie newWithTitle: 'Alien'.<br />

Les quatre variables sont des variables d'instances. Adaptez ensuite chaque méthode de MoviesSizeTest en vérifiant que les tests passent à chaque fois.


testNewInstanceSizeReturnsZero
self assert: movies size = 0



testWithOneMovieSizeReturnsOne
movies add: starWars
self assert: movies size = 1



testWithThreeMoviesSizeReturnsThree
movies add: starWars;
add: bladeRunner;
add: starWars.
self assert: movies size = 3



testIncludesAnAddedMovieReturnsTrue
movies add: starWars;
add: bladeRunner.
self assert: (movies includes: starWars).
self assert: (movies includes: bladeRunner).
self deny:  (movies includes: alien).

2.2 Renommer MoviesSizeTest

Le nom de la classe ne reflète plus assez la totalité des tests, car on vérifie les retours des méthodes Movies#size et Movies#includes. Le vrai objectif est de s'assurer que Movies#add ajoute bien les instances de Movie. Renommons la classe en MoviesAddMovieTest.


Pour ce faire, clic-droit sur la classe et sélectionnez rename.

Saisissez MoviesAddMovieTest comme nom de classe et validez.


2.3 Supprimer le comteur size de Movies

Regardons Movies#add de nouveau:
add: aMovie
size := size + 1.
moviesCollection add: aMovie

moviesCollection est une instance d'OrderedCollection.

OrderedCollection#size retourne le nombre d'instance stockées. On peut donc supprimer le compteur size qui fait doublon.

add: aMovie
moviesCollection add: aMovie

Relancez les tests:





Oups ... On s'aperçoit via le debugger que Movies#size retourne toujours 0.






L'erreur saute aux yeux en ouvrant Movies#size:

size
^ size

Corrigeons la méthode:

size
^ moviesCollection size

et les tests repassent !

Nettoyons au passage Movies#initialize pour supprimer l'initialisation de size:
initialize
super initialize.
moviesCollection:=OrderedCollection new

et nous pouvons finalement supprimer size des variables d'instances de Movies:

Object subclass: #Movies
instanceVariableNames: 'moviesCollection'
classVariableNames: ''
poolDictionaries: ''
category: 'Movies'

3. Pause publicité

Notre collection est minimale mais testée. Comme pour les billets précédents, nous écrivons d'abord le test puis nous nous laissons guider par les erreurs et le debugger pour implémenter le code fonctionnel.

Après chaque test, on modifie le code testé et de test pour l'améliorer.

La prochaine fois, nous retravaillerons MoviesAddMovieTest mais en utilisant les Traits, un mécanisme de réutilisation de code entre classes.