Rails 3 vient de sortir. Je vous encourage à lire l'annonce de la sortie sur le blog officiel, Riding Rails si vous ne l'avez pas déjà fait. Pour les nouvelles applications, il n'y a pas de doutes à avoir : Rails 3 est la route à suivre. Mais pour les applications existantes qui utilisent actuellement Rails 2.3, qu'en est-il ? C'est ce que nous allons voir en quelques questions-réponses.

Ai-je intérêt à passer à Rails 3 ?

Oui, Rails 3 apporte de nombreuses améliorations, et à peu près n'importe quelle application va tirer parti de l'une ou l'autre de ces améliorations. Par exemple, la protection par défaut contre les attaques XSS est un grand pas en avant et va simplifier la vie des développeurs. Rails 3 est également plus performant et permet de remplacer plus facilement ActiveRecord par un autre ORM. Bref, les raisons ne manquent pas de passer à Rails 3.

Puis-je passer dès aujourd'hui à Rails 3 ?

Cela dépend des projets. Rails 3 a été bien testé au cours des derniers mois avec 4 versions beta et 2 release candidates, il n'y a pas de problème de ce coté là. Mais passer à Rails 3 ne se fait pas magiquement. Il faut prendre le temps de découvrir la nouvelle version et migrer son application. Cela prend un peu de temps (de quelques jours à quelques semaines, selon la taille de l'application). Il faut donc voir si l'intérêt de passer à Rails 3 est suffisant pour justifier de passer ce temps, puis planifier cette migration, si possible lors d'une période calme.

Et pour les gems/plugins ?

La plupart des gems courants ont des branches sur leur dépôt pour fonctionner avec Rails 3. Des versions stables de ce plugin sont sorties ou vont sortir dans les jours à venir. Vous pouvez vérifier sur le site RailsPlugin.org que les gems que vous utilisez sont compatibles avec Rails 3.

Que faire si une gem n'est pas compatible avec Rails 3 ?

Si la gem n'est plus maintenue, c'est peut être l'occasion de passer à une gem équivalente plus évoluée. Vous pouvez aussi décider de reprendre le projet à votre compte en contactant l'auteur initial.

Si la gem est maintenue mais n'est pas encore fonctionnelle avec Rails 3, vous pouvez aider son auteur de différentes façons :

  1. Ouvrir un ticket sur le bug tracker du projet
  2. Suivre les développements en cours, les tester et remonter les problèmes
  3. Participer au développement en proposant des patchs, voir en créant une branche de code Rails3 spécialement pour l'occasion.

Pour une gem critique, une possibilité peut être de sponsoriser le passage à Rails à son auteur ou à un expert Rails 3.

Je suis décidé à passer à Rails 3, comment je fais ?

La première chose est de migrer vers la version la plus récente de Rails 2.3. La version 2.3.9 va sortir (demain si tout va bien), et permettra de migrer en douceur vers Rails 3. Si vous utilisez des API dépréciées, vous serez prévenu dans les logs. Cela vous permet de préparer une partie du terrain avant le grand saut. Ensuite, vous pouvez utiliser le plugin Rails upgrade pour vous aider à migrer, en suivant les explications de Jeremy McAnally.

Je vous conseille également la lecture de The Rails 3 Upgrade handbook. Le temps gagné grâce à sa lecture remboursera largement les quelques dollars qu'il coûte. Pour ceux qui aiment les vidéos, les 3 Railscasts valent également d'être regardés : partie 1, partie 2 et partie 3.

J'ai un dernier conseil pour la route : utilisez une branche séparée dans votre dépôt git/svn pour faire la migration. Vous aurez probablement besoin d'aller voir plusieurs fois votre application avant la migration et les changements faits depuis si vous rencontrez un point de blocage.

Et pour apprendre Rails 3, quelles ressources sont à ma disposition ?

Certains ont été plus rapides que moi, je vais donc me contenter de donner deux liens :

Sachez également qu'un livre en français est en cours de rédaction et devrait sortir dans les semaines à venir.

Et pour les autres questions ?

Vous pouvez poser votre question en commentaire de ce billet ou sur http://questions.rubyfr.org/.

Ce week-end avait lieu la PyCONFR (organisée par l'AFPy à la Cité des Sciences et de l'Industrie de la Vilette à Paris). J'ai pour ma part assisté à deux présentations très intéressantes :

  • "Aiguille et botte de foin : scikit-learn et joblib pour explorer des données volumineuses" de Gael Varoquaux;
  • "Analyse statistique et classification automatique de texte avec nltk et scikit-learn" de Olivier Grisel.

Les deux présentations tournaient autour des concepts de classification de l'information, que ce soit du texte (sur lequel était plus orientée la seconde présentation), ou de données autres (photos par exemple). Quelques outils intéressant on été cités :

  • scikit-learn: extension pour numpy/scipy qui fournit des implémentations de références des principaux algorithmes d'apprentissage artificiel (pour faire de la classification et prédiction entre autres) ;
  • nltk: natural language toolkit, outils permettant de parser du texte en anglais (ou quelques autres langues parlées) afin d'en extraire la structure des phrases (là aussi classification...) ;
  • joblib: un outils permettant d'organiser des jobs en pipeline, pour ne pas avoir à recalculer certains calculs et organiser des exécutions en parallèle.

Du côté du node.JS KO, quelques projets intéressants ont vu le jour, en voici quelques uns :

AF n'a finalement pas participé à l'évènement faute de volontaires disponibles (il faut dire ques les horaires locales + la Pyconfr n'ont pas arrangé les choses).

Bonne rentrée, et bonne semaine,
Pierre

Today I am going through whereever we are using the twitter api just a last minute verification we are basic auth free (and btw this basically just a test that all is fine).

Long live oauth.

We have been chasing a bug for quite a long time that made our lives miserable, and our client, very unhappy: When submitting a large form, intermittently the post data is broken (basically only the the first parts of the submitted form data get to Rails). The bug is caused by an incorrect implementation of parsing multipart/form-data in Rack 1.0 and earlier. This is the commit that fixes the issue : http://github.com/rack/rack/commit/a9440bc752be9b3093669614c6b56bf78d592958 you can also look at the added test case to try and reproduce the error.

We are without doubt not the first ones to fall on this, but as finding the bug was very, very hard. I am posting this as a public service hoping someone down the line will have an easier time finding the cause... the solution is easy. Either upgrade Rack to at least 1.1 or patch lib/rack/utils.rb. Also updating Ruby on Rails to > 2.3.6 will solve this as it has a dependency on Rack 1.1

The symptoms can vary, and what makes this bug very very hard to find is that it needs a lot of parameters to manifest itself. You might get validation errors on you models, or missing required parameters at the controller level: or just some unexpected behavior (Not tested, but might result in corrupted uploads). Basically what happened is that when the body data of a post is longer then 16384 and a multipart boundary is found at precisely 16384 bytes... the parser will return with therefor only the first parts parsed. So first, you need to have a huge form, now imagine the form represents something in your database, and you change the data: you need your new data to make the boundary arrive at precisely the slicing point. Considering that each browser may have a different size of boundary, the same change to the same data will not reproduce the error between two different browsers. Also because this executes inside the Application Controller at Racks level, you will find that the app's logs are going to be of very little service. We had to go wireshark on this bug, and follow the post data through all the levels to find where it broke. And we got lucky: we found offending POST data while testing... So basically you might find yourself with an application where from time to time, in what seems to be a completely random way, you will find fields missing from the POST data in production... but reproducing this on test servers might prove very very hard.

Note: this bug is not Rails specific and will happen with any framework using Rack. So just to help people find this: Broken form data on Sinatra, ruby corrupted post, big form fails intermittently with rack. Uploading Files randomly fails Ruby on Rails. Missing Post Data Ruby. Ruby multipart bug. Rails multipart problem.

Rest-mongo is a javascript library for NodeJS and the browser. On the node's side it features an ORM in front of a Mongo DB. On the client's side, it features the same ORM but with a REST API backend. The server-side part can also expose the REST API needed by the client (but there is no notion of authorizations yet). It works by specifying a JS schema of your data (schema that can be shared by both your client and server code). Here is an example of schema:

  1. var schema = {
  2.   "Person": {
  3.     resource: "/people",
  4.     schema: {
  5.       id: "Person",
  6.       description: "someone, blablabla",
  7.       type: "object",
  8.  
  9.       properties: {
  10.         firstname: {type: "string"},
  11.         friends: {type: "array", items: {"$ref": "Person"}},
  12.         mother: {"$ref": "Person"}
  13.       }
  14.     }
  15.   }
  16. };

Here we have described a Person that has the following properties:

  • firstname, string;
  • friends, list of references to other persons;
  • mother: reference to another Person.

The "resource" property of the Person object is not mandatory, but is needed if you want to expose/use the REST API.

Once we have a schema describing our data, we need to get the "R" object (describing a unit of work). Get a new R every time you need to get a new context to work in (at every client request for example).

  1. var rest_mongo = require("rest-mongo");
  2. var RFactory = rest_mongo.getRFactory(schema, "db_name");
  3. var R = RFactory();

Then we can start hacking around:

  1.  // Create and save an object into the DB:
  2.  var lilly = new R.Person({firstname: "Lilly"});
  3.  lilly.save(function() {
  4.    sys.puts("Lilly saved in DB with id " + lilly.id());
  5.  });
  6.  
  7.  // Get one or more objects from DB:
  8.  R.Person.get({
  9.    ids: lilly.id()
  10.  }, function(lilly2){
  11.   // lilly and lilly2 are the same
  12.  });
  13.  
  14.  // Search in DB:
  15.  R.Person.index({
  16.    query: {firstname: "Lilly"}
  17.  }, function(data){
  18.    var lilly = data[0];
  19.  });
  20.  
  21.  // Delete an object:
  22.  lilly.delete_(function(){
  23.    sys.puts('Lilly deleted from DB!');
  24.  }, function(err) { // the 2nd callback is a fallback (not mandatory)
  25.    sys.puts('You can not delete Lily!');
  26.  });
  27.  
  28.  // Usage of references:
  29.  var harry = new R.Person({firstname: "Harry", mother: lilly});
  30.  harry.save(function() {
  31.    sys.puts('Only the id of Lilly has been saved in harry.mother in DB.');
  32.  });
  33.  
  34.  // Update more than one field in once:
  35.  R.Person.update({
  36.    ids: [lilly.id(), harry.id()], 
  37.    data: {firstname: 'anonymous'}
  38.  }, function() {
  39.    sys.puts("Voldemort cannot find them anymore...");
  40.  });
  41.  
  42.  // Save more than one object in once:
  43.  var p1 = new R.Person({firstname: 'Hermione'});
  44.  var p2 = new R.Person({firstname: 'Ron'});
  45.  R.save([p1, p2], function() {
  46.    console.log('Now Harry has friends.')
  47.  }, function(error) {
  48.    console.log('Harry has no friends, because of ', error);
  49.  });
  50.  

Of course the project is lacking a lot of features that could be interesting to have, but it's well documented and tested, so don't hesitate to hack on it. It has been tested and works fine on node v0.1.100, we should soon update it for node 0.2.0.

It uses node-mongodb-native to connect to mongoDB and nodetk for orchestration, tests and other miscellaneous tools. It has been used to build Geeks, a fun events based {office, place, ...} map to locate people (and eventually do a lot of other things).

Have fun and don't hesitate to comment,
Pierre

Rest-mongo on GitHub: http://github.com/AF83/rest-mongo.

For the Whyday, I've hacked two ZSH scripts for completion of RVM and bundler 1.0. You can find them on my dotfile repository on github: http://github.com/nono/dotfiles/blob/master/zsh/Completion/_rvm and http://github.com/nono/dotfiles/blob/master/zsh/Completion/_bundler.

To install them, you can put these 2 files in $HOME/.zsh/Completion (create this directory if it doesn't exist), and add it to fpath by adding this line to your .zshrc:

fpath=(~/.zsh/Completion $fpath)

A quick example of completion:

% bundle install --[TAB][TAB]
--binstubs    -- generate bin stubs for bundled gems to ./bin
--deployment  -- install using defaults tuned for deployment environments
--frozen      -- do not allow the Gemfile.lock to be updated after this install
--gemfile     -- use the specified gemfile instead of Gemfile
--local       -- do not attempt to connect to rubygems.org
--no-color    -- disable colorization in output
--path        -- specify a different path than the system default
--quiet       -- only output warnings and errors
--system      -- install to the system location
--without     -- exclude gems that are part of the specified named group

Nous tenions à remercier tous ceux d’entre vous qui ont eu la possibilité de nous rejoindre samedi pour le WebWorkersCamp !

Les documents de présentation d’un certain nombre d’ateliers sont disponibles depuis la page du wiki : http://barcamp.org/WebWorkersCamp#digest. Nous compléterons cette section lorsque d'autres ressources seront disponibles, notamment les vidéos des présentations.

Durant ce BarCamp, nous avons eu l'honneur d'avoir avec nous Ryan Dahl de NodeJS, Richard Kreuters de MongoDB, Paul Rouget et Antony Ricaud de Mozilla, mais aussi Benoît Chesneau de CouchDB, Olivier Gutknecht, Nicolas Mérouze de Oahu et Tigerlily, et plus d'une centaine d'autres participants très actifs qui ont contribué à faire de ce WebWorkersCamp un très bel événement que nous ne manquerons pas de rééditer :)

A venir : Pour ceux qui comme nous sont archi-convaincus par les promesses de Node.js, à l’occasion du « Node.js knock-out » auquel nous participerons les 28-29 août, nous ouvrirons nos bureaux pour les 48h du défi et nous servirons des pizzas froides, du café tiède et des bières chaudes (ou l'inverse) aux heureux développeurs qui souhaitent participer. Nous vous donnerons confirmation dès que possible (nous attendons le retour des organisateurs pour ne pas forker l'événement). Les équipes se forment sur http://nodeknockout.com.

Merci !

Voici de nouveaux événements qui viennent compléter la précédente liste.

1er juillet : Drumbeat Paris - http://www.drumbeat.org/events/drumbeat-paris
Est-ce que le Web sera ouvert dans dix, vingt – voire dans 50 ans ? Mozilla pense qu'il peut – et doit – l'être. C'est pourquoi nous lançons l'initiative Drumbeat, une invitation à tout passionné du Web à participer à des projets qui améliorent le Web et le rendent plus ouvert à long terme.

21 juillet : WebPerf User Group - https://sites.google.com/a/survol.fr/webperf-user-group/evenements/21-ju...
Le WebPerf User Group une réunion informelle où tout le monde est le bienvenu, quel que soit son rôle ou sa connaissance du sujet, pour discuter et échanger à propos des performances web.

19 août : Whyday - http://whyday.org/
Le 19 août sera l'occasion de célébrer le Whyday, en l'honneur de Why the lucky stiff. Ce hacker a très fortement influencé la communauté Ruby, par ses écrits et notamment le guide poignant de why, ses projets Ruby (Camping, Shoes, RedCloth, Try Ruby, etc.), et plus généralement par sa présence en ligne. Le 19 août de l'année dernière, il a décidé de supprimer toute son activité en ligne, probablement pour préserver son anonymat. Le Why Day sera l'occasion de lui rendre hommage et de se lancer dans des projets fous.

28 et 29 août : Node.js Knockout - http://nodeknockout.com/
Le node.js knockout est une compétition qui consiste à construire un site web avec node.js en moins de 48h. Les équipes d'au plus 4 personnes seront jugées, et les sites les plus impressionnants récompensés.

15 au 17 octobre : Rails Rumble - http://blog.railsrumble.com/
Le Rails Rumble a les mêmes règles que le Node.js knockout, mais pour Rails à la place de Node.js. Ce n'est pas étonnant car les précédentes éditions du Rails Rumble ont inspirées le Node.js Knockout.

9 et 10 novembre : Forum PHP - http://afup.org/pages/forumphp2010/
Le Forum PHP est la grand messe française autour de PHP. Il sera placé sous le signe des 15 ans de PHP et des 10 ans de l'Afup. A cette occasion, l'Afup organise un Forum plus ambitieux que jamais, prévoyant de multiples conférences, des ateliers et débats, des invités renommés, mais aussi des espaces d'intervention et d'exposition plus nombreux !

12 novembre : fullfrontal - http://2010.full-frontal.org/
Fullfrontal est une conférence javascript qui promet d'être bien sympathique.

One Ring to rule them all, One Ring to find them, One Ring to bring them all and in the darkness bind them.

Here the "them" doesn't refer to other rings, but to Javascript modules/packages, server side or browser/client side. This article is about running the same code on nodejs (server) and on the browser (client), without having to change nodejs module codes.

If you know Javascript, then you already know it is a good language to express asynchronous treatments. The Javascript language have other strong points, but weak points as well. One of the often quoted advantage of JS, is that it can run on both server and client. You can then share your code as needed, which avoid you the pain to have two code bases to do the same thing (like forms validation) : once on the browser, once on the server.

My goal here is to be able to run my server side JS (written for nodejs) on the browser, without having to change the sources manually nor passing by a "compilation / transformation" process I would have to run before testing on the browser side. Of course I don't want to start a TCP server from the browser, but I'd like to be able to reuse general purpose libraries at ease.

The first problem comes from the fact that there is no isolation (or namespaces) between different modules on browser side, so if there are not wrapped in a function, then you'll end-up polluting your global namespace. The second problem comes from the fact that "require" and "exports" statements are not standard JS, but a proposal from CommonJS, and so won't be defined in the browser by default. Luckily, James Brantly wrote Yabble - Yet Another (CommonJS) Browser Loader, which can solve these two problems (Thanks James!). Here is how we can use it:

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr" id="html">
  3.   <head>
  4.     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  5.     <title>Yabble demo</title>
  6.  
  7.     <script type="text/javascript" src="/yabble.js"></script>
  8.     <script type="text/javascript">
  9.       require.reset();
  10.  
  11.       require.useScriptTags(); // to avoid using XHR
  12.       require.setModuleRoot('wrapped_js/');
  13.  
  14.       require.ensure(['A','B'], function() {
  15.         require.run('A');
  16.       });
  17.   </head>
  18.  
  19.   <body>
  20.     <h1>Yabble demo</h1>
  21.   </body>
  22. </html>

Your server has to be able to serve the following JS files: "/wrapped_js/A.js" and "/wrapped_js/B.js". Yabble will fetch those two JS modules and then run A (which can require B, since it has been loaded). The fine part with the "require.ensure", is that it will load JS modules in parallel.

By default, Yabble uses XHR requests to load the JS modules, and then eval them. This is not the most convenient way of using it since you won't be able to debug them. By writting require.useScriptTags(); we ask Yabble to retrieve the module as scripts files; but the modules code then need to be wrapped as:

  1. require.define({'B': function(require, exports, module) {
  2.   // code module B
  3. }}, ['A']); // B depends of A

Here we say that 'B' depends of 'A', so Yabble will ensure A has been loaded before running B. If we know for sure all dependencies are loaded, we don't have to specify them though.

Yabble features a tool that can wrap the modules for us, but it's pretty static (and I don't want to run it every time I change my sources). So I wrote some code (part of nodetk) that automatically serve JS files: you just have to specify which modules / packages / static files you want to serve, and it will do so. It also wraps automatically JS module files on the fly.

Example of use (on server side):

  1. var http = require("http"),
  2.     bserver = require('nodetk/browser/server');
  3.  
  4. // Here we create a http server...
  5. var server = http.createServer();
  6. // ... and do whatever we want with it
  7.  
  8. // we use that previous server to serve JS modules
  9. bserver.serve_modules(server, {
  10.   modules: ['assert', 'sys'],
  11.   packages: ['nodetk', 'rest-mongo'],
  12.   additional_files: {
  13.     '/tests.html': __dirname + '/tests.html',
  14.     '/tests.js': __dirname + '/tests.js'
  15.   }
  16. })
  17.  
  18. server.listen(8080)

Now with this code, on client side, we can do any of the followings:

  1. var sys = require('sys'),
  2.     assert = require('assert'),
  3.     utils = require('nodetk/utils'),
  4.     callbacks = require('nodetk/orchestration/callbacks'),
  5.     rest = require('rest-mongo/core');

nodetk also features a javascript file to initialize some variables such as __dirname or process.

For a complete working example, I encourage you to have a look at nodetk tests, which are running on both server and client side (except for modules which can not run on browser): http://github.com/AF83/nodetk/tree/master/src/nodetk/browser_tests/.

Pierre

Vous avez peur de vous ennuyer cet été ? Alors, voici un petit programme pour vous occuper :

21 juin : MongoFR à la Cantine - http://www.10gen.com/conferences/event_mongofr_21june10
Cette journée organisée par 10gen et sponsorisée par af83 sera entièrement consacrée à MongoDB.

26 juin : Social Innovation BarCamp Paris à la Cantine - http://barcamp.org/SocialInnovationBarcampParis
Ce barcamp autour du thème de l'innovation sociale sera la rencontrer de plein de gens intéressants. Rappelons que l'innovation sociale est toute forme d'innovation économique, organisationnelle ou technologique appliquée à des problématiques à caractère social ou à toute problématique liée aux individus et à la façon dont la société fonctionne ou pourrait être améliorée.

3 juillet : Web Workers Camp, organisé par af83 (Pierre et Louis A.) à la Cantine - http://barcamp.org/WebWorkersCamp
Ce barcamp sera l'occasion de parler de plein de technos cools comme Node.js, les bases NoSQL, les websockets, les files d'attentes, etc. Nous aurons la chance d'accueillir Ryan Dahl, le créateur de Node.js.

Du 6 au 11 juillet : RMLL 2010 à Bordeaux - http://2010.rmll.info/
Les RMLL 2010 sont un cycle non commercial et d’accès gratuit de conférences, ateliers et tables rondes autour du Logiciel Libre et de ses usages. L’objectif est de fournir un lieu d’échanges et de rencontres entre utilisateurs, développeurs et acteurs du Logiciel Libre. N'oubliez pas de venir à ma conférence sur Ruby 1.9 ;-)

Tout l'été : NoSQL Summer Paris - http://nosqlsummer.org/city/paris
Le NoSQL Summer Paris est l'occasion de découvrir des papiers sur les bases de données, relationnelles ou non, et d'en discuter avec d'autres dévs.

28 et 29 août : Pycon Fr 2010 - http://zope.afpy.org/Members/jpcw2002/pycon2010news
L'Association Francophone Python organise comme à son habitude son rendez-vous annuel autour de Python : Pycon FR. Venez échanger sur Python avec des passionnés à la Cyberbase de la Villette à Paris les 28 et 29 août. Des dizaines de conférences, courtes présentations et tutoriels seront égrainés tout au long du week-end, sur des sujets tout public ou parfois un peu plus techniques.

25 et 26 septembre : JSConf.eu à Berlin - http://jsconf.eu/2010/
La JSConf.eu est l'événement européen autour du javascript (coté client et coté serveur) à ne pas manquer. Vous pouvez encore répondre au call for speakers, mais il ne vous reste plus beaucoup de temps.

14 au 16 octobre : Paris Web - http://www.paris-web.fr/
La cinquième édition de la conférence Paris Web explorera les thèmes de l’accessibilité Web, du design numérique et des standards ouverts.

Last month, Ori was debugging an IE / Flex URLRequest / SSL bug, and the solution to his problem was a Pragma. Today I was debugging on nodejs, and the origin of my problem was a pragma...

Well, to be precise, the pragma was at the origin of the problem as I encountered it, but it could have been any header field. First things first, let's start by the beginning:

  • I'm writing an application using nodejs, the über asynchronous JS framework by Ryan Dahl.
  • For some reasons, this application collects URLs people post in different places, to have them in one place. Because nodejs make it easy, the application checks the URLs posted are valid and if the server answer by a redirect, keep the result of the redirection.
  • Yesterday's night, Shakaman (the author of ShakaCSS) posted an URL on IRC and here is another corresponding one: http://bonjourlechat.fr, but the link didn't appeared in the collected URLs.
  • I first thought it was the server fault, so I came back home. But this morning, I checked the answer from the server, just to be sure, and it was a regular 301 reply. There was a bug somewhere.

After reproducing the bug I found out that nodejs headers had no "location" header field but a "pragmalocation" one. I first thought "WTF, is that some kind of new header I haven't heard about?!", then looked up on the Internet... To find nothing.

So the "pragmalocation" header field does not exist, but shows up in nodejs... Firebug doesn't tell me anything about any pragma header, but Chromium does: it seems there is a prama header field with an empty value. wget gave me the exact order in which header fields were given:

  1.   HTTP/1.0 301 Moved Permanently
  2.   Date: Thu, 03 Jun 2010 09:56:32 GMT
  3.   Server: Apache/2.2.3 (Red Hat)
  4.   X-Tumblr-Perf: "ch:0/ cm:0/ ce:0/ c:0/0 d:0/0 e:0/0"
  5.   P3P: CP="ALL ADM DEV PSAi COM OUR OTRo STP IND ONL"
  6.   Cache-Control: public
  7.   Pragma: 
  8.   Location: http://www.bonjourlechat.fr/
  9.   Vary: Accept-Encoding
  10.   X-Tumblr-Usec: D=47851
  11.   Content-Length: 0
  12.   Content-Type: text/html; charset=UTF-8
  13.   X-Cache: MISS from rack1.tumblr.com
  14.   X-Cache-Lookup: MISS from rack1.tumblr.com:80
  15.   Via: 1.0 rack1.tumblr.com:80 (squid/2.6.STABLE6)
  16.   Connection: keep-alive

From now it was clear what the cause of the bug was: no value in a header field would provoke the field name to be concatenated to the next one... I was able to reproduce the bug with other header field names.

After further investigation, it was clear that the problem came from the http-parser used by nodejs (http://github.com/ry/http-parser). This parser is a finite state machine designed to be fast and triggering some callbacks along the parsing. So the state machine is basically triggering two callbacks:

  • on_header_field: called when some pieces of header field names is parsed;
  • on_header_value: called when some pieces of header field values is parsed.

The problem with such approach, is that only using the two provided callbacks, it is not possible to detect the case where a header field has no value... It also makes it a bit tricky to use.

So I proposed a solution, which is just to add another callback to be called once the parser finish to parse a header field. The patch is pretty small (3 lines in the parser), but is makes it easier to use and solve our problem. As any bug need a test, I added a test reproducing the bug. The test making a reference to bonjourlechat.fr, this is how this website could enter the nodejs code base!

To illustrate how simple it makes the code using the parser, here is how could be rewritten the http-parser.js :

Before:

  1.   ...
  2.  
  3.   parser.onMessageBegin = function () {
  4.     parser.incoming = new IncomingMessage(parser.socket);
  5.     parser.field = null;
  6.     parser.value = null;
  7.   };
  8.  
  9.   ...
  10.  
  11.   parser.onHeaderField = function (b, start, len) {
  12.     var slice = b.toString('ascii', start, start+len).toLowerCase();
  13.     if (parser.value) {
  14.       parser.incoming._addHeaderLine(parser.field, parser.value);
  15.       parser.field = null;
  16.       parser.value = null;
  17.     }
  18.     if (parser.field) {
  19.       parser.field += slice;
  20.     } else {
  21.       parser.field = slice;
  22.     }
  23.   };
  24.  
  25.   parser.onHeaderValue = function (b, start, len) {
  26.     var slice = b.toString('ascii', start, start+len);
  27.     if (parser.value) {
  28.       parser.value += slice;
  29.     } else {
  30.       parser.value = slice;
  31.     }
  32.   };
  33.  
  34.   ...

After:

  1.   ...
  2.  
  3.   parser.onMessageBegin = function () {
  4.     parser.incoming = new IncomingMessage(parser.socket);
  5.     parser.field = "";
  6.     parser.value = "";
  7.   };
  8.  
  9.   ...
  10.  
  11.   parser.onHeaderField = function (b, start, len) {
  12.     parser.field += b.toString('ascii', start, start+len).toLowerCase();
  13.   };
  14.  
  15.   parser.onHeaderValue = function (b, start, len) {
  16.     parser.value += b.toString('ascii', start, start+len);
  17.   };
  18.  
  19.   parser.onHeaderValueComplete = function () {
  20.     parser.incoming._addHeaderLine(parser.field, parser.value);
  21.     parser.field = "";
  22.     parser.value = "";
  23.   };
  24.  
  25.   ...

This is much simpler! I like the story of this bug, this is why I posted here. Along the debugging process, I had a look at the way JS and C++ communicate in nodejs, and also probably learned some C tricks reading Ryan code, so thanks Ryan!

Pierre

Update: The patch has not been accepted, Ryan preferring to keep the API as it. But the test case has been kept (cf. commit).

And just to be nice, here is an English version of this tutorial...

Introduction

This tutorial will guide you through installing and configuring an acceptance test environment for PHP project.
For this we use several ruby gems, including cucumber, webrat and selenium-client, and write our scripts as cucumber features.
Here's an example:

Feature: Example Features

  Scenario: I am connected and i visit my page
    Given I am connected as "username password"
    When I visit "home page"
    Then I should see "Hello username"

The advantage of this syntax is that it remains very readable for the mere mortals, no need to delve into the ruby code to understand what the test is supposed to do.

Each "sentence" in this scenario refers to a step which is the user actions necessary to perform that action in the user interface.

The user actions can be triggered by the webrat and selenium-client libraries. Webrat is particularly suited to interactions not involving javascript while selenium-client will interpret them correctly.

Installation

For this example we will use a debian system.

Let's start by installing rubygems and the needed gems.

aptitude install rubygems
gem install rspec cucumber webrat selenium-client

To run the binaries packaged with gems from the command line you must add the path to these binaries to your PATH:

export PATH=$PATH:$HOME/.gem/ruby/1.8/bin

To use selenium, start a selenium-rc server (ie: remote control). Starting selenium server is done automatically by webrat, however the version used caused me many problems with recent versions of Firefox. I suggest you grab the latest stable version at the following address:

http://seleniumhq.org/download/ (1.0.3 au 28 juin 2010).

It is sufficient to replace the file ~/.gem/ruby/1.8/gems/webrat-0.7.1/vendor/selenium-server.jar with the version you just downloaded.

wget http://selenium.googlecode.com/files/selenium-remote-control-1.0.3.zip
unzip selenium-remote-control-1.0.3.zip
cp selenium-server-1.0.3/selenium-server.jar $GEM_HOME/webrat-0.7.1/vendor/selenium-server.jar

(Remplace $GEM_HOME by your gems installation directory.)

Voila, now we can start writing our scenarios.

Note for iceweasel: you must add the path to the folder containing the iceweasel application.ini file to your PATH otherwise

iceweasel will not start.

export PATH=$PATH:/usr/lib/iceweasel

Configuration

Now that the installation is complete, we will have to configure a web environment to run our tests.
This environment can be your development environment, the approach is specific to each project so I will not dwell on this point.

Let's turn to the configuration of our test environment.
We will create a directory features in the root of your project. This directory has the following structure:

$: features % tree
.
├── fixtures
│   └── image.jpg
├── home.feature
├── step_definitions
│   ├── user_steps.rb
│   └── webrat_steps.rb
└── support
    ├── env.rb
    ├── paths.rb
    └── selenium.rb

3 directories, 10 files

Let us see more precisely what role each of these directories is.

The root of the features will contain your cucumber "features", ie scenarios.

fixtures contains the files used in your scripts (eg an image to upload to a file field).

The step_definitions stores definitions of steps used in the scenarios. It will usually contain a file per action type, in our example we have two files:

  • webrat_steps: Common actions, this file can be found in the code of the webrat gem (templates/skeleton/step_definitions/webrat_steps.rb.erb). It contains, among other common helpers to any type of web project (visit a page, check that it contains a text or an html tag, ...)
  • user_steps : the user actions (login, post a comment ...)

The support is the configuration directory, it must contain the following files:

  • env.rb, the webrat configuration file
  • path.rb, is the file that defines the correspondence between the named routes that you call and the actual URL to send to the browser (example: "home page" => "/")
  • selenium.rb, is the file where you can refactor out the specific actions to selenium.

Webrat

By default webrat does not use selenium to control the browser but uses instead mechanize, here is a sample configuration file to be placed in the support/env.rb to specify the use of selenium:
(Note in passing that the address of your test web environment will be configured via an environment variable for simplicities sake):

unless ENV['CUCUMBER_HOST']
  raise 'You must set CUCUMBER_HOST environment variable with the name of your host used to run cucumber features'
end

# RSpec
require 'spec/expectations'

# Webrat
require 'webrat'

require 'test/unit/assertions'
World(Test::Unit::Assertions)

Webrat.configure do |config|
  #config.mode = :mechanize
  config.mode = :selenium
  config.application_framework = :external
  config.application_address = ENV['CUCUMBER_HOST']
  config.application_port = "80"
end

World do
  session = Webrat::Session.new
  session.extend(Webrat::Methods)
  session.extend(Webrat::Matchers)
  session
end

Named Routes

Next you must specify the routes that can be called in your tests and their text versions. Indeed, in cucumber scenarios , a literary correspondence will be much more readable than the raw URL. Here is an example configuration of these routes to be stored in the file support/path.rb :

module NavigationHelpers
  def path_to(page_name)
    case page_name
    when /home/
      '/'
    when /accueil/
      '/accueil'
    when /login/
      '/identification'
    else
      raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
        "Now, go and add a mapping in #{__FILE__}"
    end
  end
end

World(NavigationHelpers)

Helpers

In order to have the scenarios as clear and as understandable as possible, we will define some helpers.
All files in the support will be loaded by cucumber in our example we will add a file selenium.rb for actions related to selenium.

Here is the support/selenium.rb file:

def wait_for_ajax(timeout=50000, request_count = 1)
  request_count.times do |index|
    selenium.wait_for_ajax :javascript_framework => :jquery
  end 
end
def wait_for_gritter
  selenium.wait_for_element "gritter-notice-wrapper"
end

You can do the same for other libraries you might need.

Writing scripts

The scenarios are defined by several cucumber keywords: Given, When, Then and And.

  • Given defines the initial context
  • When defines user actions
  • Then defines an assertion, the expected result
  • And is an association word to make the scenatio more readable, it has the same role as the keyword of the previous line.

Some actions will be called in several scenarios, hence the need for code refactoring.
For example, the action Given I am connected as "username password" will certainly be present in 75% of your tests.
To avoid repeating the X actions necessary to connect, we refactored that in the steps of the step_definitions directory.

Here is an example of steps for the following example:

Given /^I am connected as "(\w)\ (\w)"$/ do |login, password|
  visit path_to "login"
  fill_in 'login', :with =>  login
  fill_in 'password', :with => password
  click_button "login"
  wait_for_ajax # if your form is submitted via ajax
end 

Now in each of our tests we can call: Given I am connected as "MyUsername MyPassword".

Feature: Example Features

  Scenario: I am connected and i visit my page
    Given I am connected as "username password"
    When I visit "home page"
    Then I should see "Hello username"

Run !

Now that you've written your first scenario, go to the root of your project and run the following command:

CUCUMBER_HOST=YOUR_HOST cucumber

(Replace YOUR_HOST by your test enviroment address.)

You should see your browser run your script ... hands free!

Introduction

Ce tutoriel va vous guider dans l'installation et la configuration d'un environnement de test d'acceptance sur un projet PHP. Pour cela nous utiliserons plusieurs gems ruby, notamment cucumber, webrat et selenium-client, et écrirons nos scénarios sous forme de features cucumber. Voici un exemple :

Feature: Example Features

Scenario: I am connected and i visit my page Given I am connected as "username password" When I visit "home page" Then I should see "Hello username"

L'avantage de cette syntaxe est qu'elle reste très lisible pour le communs des mortels, pas besoin de mettre le nez dans le code ruby pour comprendre ce que le test est sensé faire.

Chaque "phrase" de ce scénario fait référence à des "steps" (étapes) qui sont en réalité les actions utilisateurs nécessaires pour effectuer l'action correspondante dans l'interface utilisateur.

Les actions utilisateurs peuvent être déclenchés par les librairies webrat et selenium-client. Webrat est plus particulièrement adapté aux interactions ne faisant pas intervenir de javascript alors que selenium-client saura les interpréter correctement.

Installation

Pour cet exemple nous utiliserons un système debian.

Commençons par installer rubygems et les gems nécessaires.

aptitude install rubygems
gem install rspec cucumber webrat selenium-client

Pour pouvoir exécuter les binaires packagés avec les gems en ligne de commande vous devrez ajouter le chemin vers ces binaires à votre PATH :

export PATH=$PATH:$HOME/.gem/ruby/1.8/bin

Pour pouvoir utiliser selenium, il faut démarrer un serveur selenium-rc (ie: remote control). Le démarrage du serveur selenium est fait automatiquement par webrat, en revanche la version utilisée m'a posé plusieurs problèmes avec les versions récentes de firefox. Je vous conseille donc de récupérer la dernière version stable à l'adresse suivante : http://seleniumhq.org/download/ (1.0.3 au 28 juin 2010). Il suffira ensuite de remplacer le fichier ~/.gem/ruby/1.8/gems/webrat-0.7.1/vendor/selenium-server.jar par la version que vous venez de télécharger.

wget http://selenium.googlecode.com/files/selenium-remote-control-1.0.3.zip
unzip selenium-remote-control-1.0.3.zip
cp selenium-server-1.0.3/selenium-server.jar $GEM_HOME/webrat-0.7.1/vendor/selenium-server.jar

(Remplacez $GEM_HOME par le répertoire d'installation de vos gems.)

Voila, nous allons pouvoir écrire nos scénarios.

Note pour iceweasel : vous devrez ajouter le chemin vers le dossier contenant le fichier application.ini de iceweasel à votre PATH, sinon iceweasel ne démarrera pas.

export PATH=$PATH:/usr/lib/iceweasel

Configuration

Maintenant que l'installation est terminée, il va falloir configurer un environnement web pour lancer nos tests. Cet environnement peut être votre environnement de développement, la démarche est spécifique à chaque projet donc je ne m'attarderai pas sur ce point.

Passons maintenant à la configuration de notre environnement de test. Créons un répertoire features à la racine de votre projet. Ce répertoire aura la structure suivante :

$: features % tree
.
├── fixtures
│   └── image.jpg
├── home.feature
├── step_definitions
│   ├── user_steps.rb
│   └── webrat_steps.rb
└── support
    ├── env.rb
    ├── paths.rb
    └── selenium.rb

3 directories, 10 files

Voyons plus précisément quel est le rôle de chacun de ces répertoires.

Le racine du répertoire features contiendra vos "features" cucumber, c'est à dire les scénarios.

Le répertoire fixtures contiendra les fichiers utilisés dans vos scénarios (par exemple une image à envoyer dans un champs file).

Le répertoire step_definitions stocke les définitions des étapes utilisées dans les scénarios. Il contiendra généralement un fichier par type d'action, dans notre exemple nous aurons deux fichiers :

  • webrat_steps : les actions communes, ce fichier peut être trouvé dans le code du gem webrat (templates/skeleton/step_definitions/webrat_steps.rb.erb). Il contient, entre autre, les helpers communs à tout type de projet web (visiter une page, vérifier qu'elle contient un texte ou une balise, ...)
  • user_steps : les actions utilisateurs (se connecter, poster un commentaire...)

Le répertoire support est le répertoire de configuration, il doit contenir les fichiers suivants :

  • env.rb, le fichier de configuration de webrat
  • path.rb, c'est le fichier qui définit la correspondance entre les routes nommées que vous appellerez et les véritables URL à envoyer au navigateur (par exemple : "home page" => "/")
  • selenium.rb, c'est le fichier où vous pourrez factoriser les actions spécifiques à selenium.

Webrat

Par défaut webrat n'utilise pas selenium pour controler le navigateur mais mechanize, voici un exemple de configuration à placer dans le fichier support/env.rb pour spécifier l'utilisation de selenium (on notera au passage que l'adresse de votre environnement web de test sera configurée via une variable d'environnement pour plus de simplicité) :

unless ENV['CUCUMBER_HOST']
  raise 'You must set CUCUMBER_HOST environment variable with the name of your host used to run cucumber features'
end

RSpec

require 'spec/expectations'

Webrat

require 'webrat'

require 'test/unit/assertions' World(Test::Unit::Assertions)

Webrat.configure do |config| #config.mode = :mechanize config.mode = :selenium config.application_framework = :external config.application_address = ENV['CUCUMBER_HOST'] config.application_port = "80" end

World do session = Webrat::Session.new session.extend(Webrat::Methods) session.extend(Webrat::Matchers) session end

Routes nommées

Ensuite vous devez spécifier les routes pouvant être appelées dans vos tests ainsi que leurs versions texte. En effet, dans les scénarios cucumber, une correspondance littéraire sera beaucoup plus lisible que les URL brutes. Voici un exemple de configuration de ces routes qui seront stockées dans le fichier support/path.rb :

module NavigationHelpers
  def path_to(page_name)
    case page_name
    when /home/
      '/'
    when /accueil/
      '/accueil'
    when /login/
      '/identification'
    else
      raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
        "Now, go and add a mapping in #{FILE}"
    end
  end
end

World(NavigationHelpers)

Helpers

Dans le but d'avoir des scénarios les plus clairs et compréhensibles possibles, nous allons définir quelques helpers. Tous les fichiers présents dans le répertoire support seront chargés par cucumber, dans notre exemple nous allons ajouter un fichier selenium.rb pour les actions relatives à selenium.

Voici donc le fichier support/selenium.rb :

def wait_for_ajax(timeout=50000, request_count = 1)
  request_count.times do |index|
    selenium.wait_for_ajax :javascript_framework => :jquery
  end 
end
def wait_for_gritter
  selenium.wait_for_element "gritter-notice-wrapper"
end

Vous pourrez faire de même pour les autres librairies dont vous pourriez avoir besoin.

Écrire des scénarios

Les scénarios cucumber sont définis par plusieurs mots-clés : Given, When, Then et And.

  • Given définit le contexte de départ.
  • When définit une action utilisateur.
  • Then définit une assertion, le résultat à obtenir.
  • And est un mot de liaison permettant de rendre encore plus lisible le scénario, il a le même rôle que le mot-clé de la ligne précédente.

Certaines d'actions seront appelées dans plusieurs scénarios, d'où le besoin de factorisation de ce code. Par exemple, l'action Given I am connected as "username password" sera très certainement présentes dans 75% de vos tests. Pour éviter de répéter les X actions nécessaires pour se connecter, nous allons factoriser cela dans les étapes du répertoire step_definitions.

Voici un exemple d'étapes correspondant à l'exemple suivant :

Given /^I am connected as "(\w)\ (\w)"$/ do |login, password|
  visit path_to "login"
  fill_in 'login', :with =>  login
  fill_in 'password', :with => password
  click_button "login"
  wait_for_ajax # if your form is submitted via ajax
end 

Désormais dans chacun de nos test nous pourront appeler : Given I am connected as "MyUsername MyPassword".

Feature: Example Features

Scenario: I am connected and i visit my page Given I am connected as "username password" When I visit "home page" Then I should see "Hello username"

Run !

Maintenant que vous avez écrit votre premier scénario, placez-vous à la racine de votre projet et lancer la commande suivante :

CUCUMBER_HOST=YOUR_HOST cucumber

(Remplacez YOUR_HOST par l'adresse de votre environnement de test.)

Vous devriez voir votre navigateur réaliser votre scénario... sans les mains !

af83 est fier d'organiser avec Silicon Sentier le 1er WebWorkersCamp le 3 Juillet 2010 à Paris, à la Cantine.

NodeJS, NoSQL, files d'attentes, programmation asynchrone, WebSockets, Applications Distribuées, Réseaux Sociaux acentrés, générateurs de buzzwords...

Une première rencontre parisienne pour tous ceux qui aiment faire des architectures marrantes.

Qu'est-ce que le WebWorkersCamp ?

Face à l’expansion du web en temps réel et à la multiplication des webservices, les sites web tels qu’ils sont programmés aujourd’hui ont de plus en plus de mal à tenir la charge. Ce BarCamp sera l'occasion d'échanger de manière conviviale sur les technologies permettant cette montée en charge (programmation asynchrone, websockets, NoSQL, files d'attentes...).

Seront entre autres abordés :

nodeJS : Javascript côté serveur, programmation asynchrone ; différentes NoSQL DBs et leurs usages ; les websockets ; les files d'attente (AMQP ou autres) ; ...

De manière générale, ce BarCamp sera l'occasion pour tous de faire des retours d'expérience sur toutes les technologies / solutions / architectures qui nous permettent de répondre à des besoin spécifiques en terme de montée en charge, de connectivité (rester connecté à différents web services ou clients) et autres sujets connexes.

Pour plus d'infos et pour vous inscrire... http://barcamp.org/WebWorkersCamp

Here in AF83, we like to play with nodeJS. NodeJS let you write scalable network programs using Javascript, an asynchronous programming language. When writing asynchronous code, there is a challenge when comes the time of tests: how do I isolate tests, and wait for one to finish before starting the next ones?

There are different strategies to handle that:

  1. Run your tests in parallel. Don't even try to run them as a chain. This mean you have to isolate every single test environment from the others (DB, ...). That is possible, but the overhead might be too big in some cases.

  2. Active polling and waiting. This strategy is used by Jasmine or jasmine-nodejs. Basically, a function is ran periodically to check whether the test is done or not. I don't like this approach, because active polling is bad. Sometimes purity is not the right approach, and maybe this is the time... but this just doesn't seem right to me. Plus, it feels to me like the waitsFor stuff is just too much overhead.

    Example of asynchronous test using jasmine-nodejs:

    1.       describe("name of the test", function() {
    2.           var done = false;
    3.           setTimeout(function(){
    4.             expect(true).toBeTruthy(); // always ok
    5.             done = true;
    6.           }, 13);
    7.           this.waitsFor(100, function() {
    8.             return done;
    9.           });
    10.         });
    11.  
  3. Signal the end of the test. This approach is used by QUnit, the jQuery test suite, but also node-async-testing. Basically, you call a function in your last callback. Once this function is called, your test is considered as complete. This approach introduces the danger of having parts of the tests never ran. That is why QUnit let you specify the number of expected assertions (optional): if there is less or more than expected, then your test is probably wrong.

    Example of asynchronous test using QUnit:

    1.       asyncTest("name of the test", 1, function() { 
    2.         // There is one assert in this test
    3.         setTimeout(function(){
    4.           ok(true, "always fine");
    5.           start(); // This is the function telling to "start" the rest of the tests
    6.         }, 13);
    7.       });
    8.  

    Another caveat of such method is that sometimes it is hard to know which callback is going to finish first, and so where to call the start() function.

  4. Count the number of assertions you are expecting. When this number is reached, you can start the next test. If an assertion fail, you can also catch it and continue the next tests (or just stop the whole suite). This strategy is inspired from the QUnit one, expect this time we rely on the count of assertions to know if a test is finished or not. I have implemented this strategy in a small nodeJS library: nodetk, which includes a test runner. The library just wraps the assert functions provided with node to count how many times they are called.

    Example of test using this strategy:

    1.       ['test name', 2, function() { 
    2.       // This test will be considered as finished 
    3.       // once two assert fcts have been called.
    4.         setTimeout(function(){
    5.           assert.ok(true, "always fine");
    6.         }, Math.random() * 10); // This is just for the example of indeterminism
    7.         setTimeout(function(){
    8.           assert.ok(true, "always fine");
    9.         }, Math.random() * 10);
    10.       }]
    11.  

    I like this strategy because because the overhead it introduces is small, but one might find it painful to count the number of assertions each test is going to make. My argument is that anyway, when you write tests, you really should know what is expected to happen.

And you, what strategy do you prefer? We do not consider here the style of tests (RSpec like, based on assertions...), but strategy to handle asynchronous calls within the tests. I'm really interested in your experiences and feedbacks, so please don't hesitate to comment on your strategy or the ones exposed here!

Pierre