19 mars 2014

Appium et cibles multiples

Pour les tests automatisés d’applications iOS, j’ai dit utiliser Appium — voir : iOS et tests automatisés

Pour plusieurs raisons (public ciblé, non régression, couverture des tests, captures d’écrans…) il est important de tester sur plusieurs cibles. En se limitant à l’iPhone / iPod Touch, citons en particulier :
  • iOS Simulator : iOS 6.0 / 6.1 (non Retina)
  • iOS Simulator : iOS 6.0 / 6.1, Retina 3.5-inch
  • iOS Simulator : iOS 6.0 / 6.1, Retina 4-inch
  • iOS Simulator : iOS 7.0 / 7.1, Retina 3.5-inch
  • iOS Simulator : iOS 7.0 / 7.1, Retina 4-inch 
  • (real device) iPod Touch 4e génération : iOS 6.1.3
  • (real device) iPod Touch 5e génération : iOS 7.0.3
iOS Simulator Résolution iOS 6.0 / 6.1 iOS 7.0 / 7.1
iPhone 320×480 OK n/a
iPhone Retina 3.5-inch 640×960 OK OK
iPhone Retina 4-inch 640×1136 OK OK

Voici comment je m’en sors avec Appium 0.16.0, Xcode 5.1 et Xcode 4.6.3 installés sous OS X 10.8.5.

Pour iOS 7.1, facile, puisque c’est le niveau de SDK du simulateur par défaut. On fera donc simplement dans le test JUnit :
final DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("deviceName", "iPhone Retina (3.5-inch)");
driver = new RemoteWebDriver(new URL("http://localhost:4723/wd/hub"), capabilities);
ou :
(...)
capabilities.setCapability("deviceName", "iPhone Retina (4-inch)");
(...)
Rappelons que pour le simulateur, la capability « app » pointe vers l’application sur le poste de développement :
capabilities.setCapability("app", "/Users/dandriana/Library/Developer/Xcode/DerivedData/"
    + MON_APPLI_DIR // XXX-abcdefetc.
    + "/Build/ProductsDebug-iphonesimulator/"                       
    + MON_APPLI_NAME + ".app"
);
Pour iOS 6.1, c’est un peu plus compliqué, car pour lancer le simulateur, Apple Instruments sélectionne systématiquement le dernier SDK du simulateur par défaut dans Xcode. Il va donc falloir se passer de Xcode 5.1 et utiliser Xcode 4.6.3, grâce à la ligne de commande suivante :
$ sudo xcode-select -switch /Applications/Xcode-4.6.3.app/Contents/Developer/
Et pour choisir la résolution, c’est le même code que ci-dessus, avec comme possibilités :
capabilities.setCapability("deviceName", "iPhone");

capabilities.setCapability("deviceName", "iPhone Retina (3.5-inch)");

capabilities.setCapability("deviceName", "iPhone Retina (4-inch)");
Il n’y a pas à changer la capability "app".

En ce qui concerne les real devices, Appium va seulement avoir besoin de l’UDID et du bundleID :
capabilities.setCapability("device", "f98cda012etc.");
capabilities.setCapability("app", "com.avcompris.MON-APPLI"); 
À propos des simulateurs, pour info voici ce qui est donné par défaut :
$ xcode-select --print-path 
/Applications/Xcode.app/Contents/Developer 
$ defaults read com.apple.iphonesimulator 
{ 
   LocationMode = 3102; 
   SimulateDevice = "iPhone Retina (4-inch 64-bit)"; 
   SimulatorWindowCenterX = 657; 
   SimulatorWindowCenterY = 497; 
   SimulatorWindowLastScale = 1; 
   currentSDKRoot = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.1.sdk"; 
   windowOrientation = 3; 
}
Modifier ces valeurs soi-même (defaults write) ne change rien : Apple Instruments les remet à jour avec le SDK du simulateur par défaut.

Notons aussi que, même si Xcode 4.6.3 est également installé sur ma machine et sert bien d’aiguillage pour lancer un autre simulateur que celui du SDK 7.1, il est a priori impossible d’ouvrir le projet iOS avec cette installation de Xcode, puisque des formats de fichiers, notamment les Xib, sont incompatibles entre 4.6.3 et 5.1.

03 février 2014

iOS et tests automatisés

En faisant un peu de programmation iOS, je me suis posé la question d’automatiser mes tests. Apple fournit une panoplie d’outils tels que UIAutomation, qui permet d’écrire des tests automatisés en Javascript et inclus dans le projet, mais vu que j’ai déjà quelques sanity checks codés en Java (typiquement, l’extraction de points de contrôles des fichiers statiques xib de la configuration de l’UI, et leur vérification), bah, j’aimerais bien :
  1. pouvoir coder mes tests en Java,
  2. qu’ils se lancent à partir de Maven
  3. et que les résultats soient lisibles par Jenkins.
Je débute et j’explore.

Appium


J’apprends qu’on peut utiliser un WebDriver (oui, le même qu’avec Selenium) qui va taper sur un outil Appium (open source), qui va lui-même interagir avec Instruments (Apple), qui va déclencher l’application iOS à tester.

Note : j’ai découvert Appium grâce à cette vidéo : GTAC 2013: Appium: Automation for Mobile Apps — 43 min.

On a deux chaînes intéressantes :
  • Dev : Eclipse → JUnit → Appium (en local) → iOS Simulator
  • CI : Jenkins → Maven → JUnit → Appium (par HTTP) → iOS Simulator

Cela dit, le WebDriver ne permet pas de récupérer toutes les informations de l’application iOS, et notamment la totalité des caractéristiques des UIViews. Il va donc falloir instrumentaliser nous-mêmes l’application pour en savoir davantage.

J’ai par exemple quatre cas de tests qui font intervenir des chaînes différentes :
  1. Vérification de layout
    — exemple : tels boutons doivent être alignés et avoir les mêmes dimensions
     
  2. Vérification de la mise à jour du texte d’un UILabel
    — exemple : après avoir cliqué sur tel bouton, tel label doit afficher tel texte
      
  3. Vérification de la mise à jour de la couleur d’un UILabel
    — exemple : après avoir cliqué sur tel bouton, tel texte doit être affiché de telle couleur

A. Vérification de layout


— exemple : tels boutons doivent être alignés et avoir les mêmes dimensions

C’est la toute première chose à laquelle j’ai dû penser après avoir vu que,  pour définir le design des écrans, Interface Builder allait être utilisé plus ou moins systématiquement. Ce machin est tellement instable que même en consultant ses propres travaux précédents et en faisant « Pomme-S » (réflexe d’avant le cloud), on n’est pas sûr que des modifications n’aient pas eu lieu. En sus, on a beau définir toutes les contraintes imaginables, elles sautent à la moindre occasion et/ou au final ne s’appliquent pas comme prévu.

La seule vérification fiable est au runtime, d’où l’écriture de tests qui pilotent iOS Simulator.

Voyons comment on s’y prend.

Primo, on va donner dans Xcode (en fait, dans le xib) un identifiant au bouton auquel on va vouloir accéder en test. À savoir, le xib permet entre autres de définir pour une UIView les identifiants suivants :
  • userLabel : un texte arbitaire pour aider à retrouver ses petits dans Interface Builder → ne passe pas l’étape du build : n’est pas accessible au runtime
  • tag : un numérique (1, 2, 3…), interne → éventuellement utile pendant le coding Objective-C, présent ensuite au runtime, mais pas accessible avec le WebDriver
  • accessibilityLabel : utilisé par VoiceOver notamment → accessible par By.name(xxx)
  • accessibilityIdentifier : interne → accessible par By.id(xxx)

Pour définir un tag, aller dans l’Attributes inspector :


Pour définir un accessibilityLabel, aller dans l’Identity inspector : 


Rappel : c’est cet accessibilityLabel que lira à haute voix VoiceOver s’il est activé.

Pour définir un accessibilityIdentifier, aller aussi dans l’Identity inspector…


et ajouter à la main un champ String « accessibilityIdentifier » :


On accède ensuite au bouton depuis le code Java du test de la façon suivante :
final WebElement button = driver.findElement(By.id("L20"));
// or: button = driver.findElement(By.name("L20"));
// or: button = driver.findElement(By.name("Bouton de validation"));
et à ses propriétés comme ceci :
assertEquals(75, button.getLocation().x);
assertEquals(61, button.getLocation().y);
assertEquals(106, button.getDimension().width);
assertEquals(127, button.getDimension().height);
Noter qu’on peut également aller chercher soi-même ces informations dans le driver.getPageSource(), où elles apparaissent comme ceci :
{
 "name":"L20",
 "type":"UIAButton",
 "label":"Bouton de validation",
 "value":"Bouton de validation",
 "rect":{"origin":{"x":75,"y":61},"size":{"width":106,"height":127}},
 "dom":null,"enabled":true,"valid":true,"visible":true,
 "children":[],
 "hint":"Blah Blah Blah"
}
La présence des deux attributs accessibilityIdentifier et accessibilityLabel, ou d’un seul des deux, a une influence sur les méthodes qu’on va utiliser et les attributs renvoyés :

id, label="toto" id="123", label="toto" id="123", label
findElement(By.id(?)) By.id("toto") By.id("123") By.id("123")
findElement(By.name(?)) By.name("toto") By.name("123") By.name("123")
getAttribute("name") "toto" "123" "123"
getAttribute("value") "toto" "toto" "" (vide)


Bon, je préfère évidemment l’accessibilityIdentifier :
  • il peut être unique à travers les écrans de l’appli, alors que l’accessibilityLabel sera probablement répété dans des pages connexes (ben oui, pour des histoires d’accessibilité, on répète forcément les mêmes accessibilityLabels pour les mêmes boutons, afin de ne pas perdre l’utilisateur),
  • il est fixe dans le temps, alors que l’accessibilityLabel sera appelé à évoluer en fonction des retours utilisateurs,
  • il n’a pas à être traduit.

B. Vérification de la mise à jour du texte d’un UILabel


— exemple : après avoir cliqué sur tel bouton, tel label doit afficher tel texte

Bon, on se dit que ça devrait être simple :

final WebElement button = driver.findElement(By.id("truc"));
final WebElement label = driver.findElement(By.id("machin"));

assertEquals("Avant", label.getText()); // Alors, ça marche ?
button.click();
assertEquals("Après", label.getText()); // Alors, ç
a marche ?
Eh ben non, ça ne fonctionne pas, getText() ne renvoie en fait rien, et ce simplement parce qu’il y a un bug dans l’UIAutomation d’Apple : Appium does not return correct value from UIAStaticText

Dommage, l’API de WebDriver avait pourtant tout prévu : la méthode getText() existe pour WebElement, ça sentait à peu près bon.

Bien, alors quel est le souci, en fait ? C’est que lorsqu’Appium traduit nos appels Java en appels Javascript pour UIAutomation, celui-ci a un bug qui ne récupère pas les bonnes valeurs auprès de l’environnement d’exécution de notre application en Objective-C.

Du coup, on va aller invoquer Objective-C directement depuis Java, et cela nous amène au cas suivant.

C.  Vérification de la mise à jour de la couleur d’un UILabel


— exemple : après avoir cliqué sur tel bouton, tel texte doit être affiché de telle couleur

Pour ce cas, c’est clair : l’interface WebDriver ne prévoit absolument pas de récupérer la couleur d’un élément. Il y a bien une méthode getAttribute(xxx) dans WebElement, mais, euh, un coup d’œil à la pageSource (voir plus haut) confirme que c’est assez pauvre et qu’en l’occurrence la couleur n’en fait pas partie.

Il va falloir instrumentaliser d’une façon ou d’une autre notre propre application iOS en Objective-C, d’une façon qui rende ces informations disponibles au code Java de nos tests.

Alors, en ce qui me concerne, j’ai ajouté ceci :
  1. Un bouton « DumpUIViews » à mon IHM, actif uniquement en phase de debug (et donc désactivé à tous les niveaux en phase de release).
     
  2. Une méthode « dumpUIViews » qui se déclenche quand on clique sur ce bouton, et qui écrit dans un fichier toutes les caractéristiques que j’ai trouvées intéressantes, et ce pour toutes les UIViews : tag, accessibilityIdentifier, backgroundColor… en plus de caractéristiques des sous-classes : UILabel.text, [UIButton titleColorForState:xxx], etc.
     
  3. Côté instrumentalisation Java, de quoi lire ce fichier, le transformer en XML (et lire à l’intérieur grâce à avc-binding-dom, évidemment juste un détail).
     



Alors c’est un peu long et un peu riche, sans doute, de dumper toute la hiérarchie d’objets UIView après chaque interaction et notamment après chaque clic, mais, euh… ça fonctionne.

Il faut voir ça comme une capture d’écran — que permet le WebDriver relié à Appium, d’ailleurs.

Considérations diverses


Sur les tags : rien n’impose qu’un tag d’objet UIView soit unique dans toute l’application, et d’ailleurs parfois il vaut mieux avoir des objets différents avec le même tag. Donc, si c’est une contrainte de développement, autant avoir un garde-fou d’une sorte ou d’une autre : test JUnit…

Sur les accessibilityIdentifiers : même remarque, sauf que là, on s’attendrait quand même à ce que l’unicité soit obligatoire.

Sur le fait de réaliser des captures de layouts en plus de captures d’écrans : cela permet une analyse a posteriori de certains éléments plus fins. On peut donc voir le processus de test en deux phases :
  • Une première phase qui remplit les formulaires, clique sur les boutons, et opère toutes les captures de layouts et d’écrans possible ;
  • Une deuxième phase qui analyse ces données à froid : vérification des contraintes de tailles, de couleurs, d’alignements, validation des libellés…

Et ce peut être le même code qui, passé deux fois, s’occupe de chacune des phases.



21 janvier 2014

avc-binding-dom et polymorphisme

On a des cas où une structure XML a évolué et où l’on aimerait pouvoir accéder aux contenus des différentes versions à travers une unique interface Java. Pour cela, avc-binding-dom permet de déclarer des sous-interfaces et suit un polymorphisme.

Prenons un exemple, avec une interface Java candidate à récupérer des informations d’un fichier xib (Apple iOS) :
public interface Xib { boolean hasUseAutoLayout(); }
Les deux versions du XML qu’on veut savoir traiter renseignent l’information « useAutoLayout » de deux façons différentes.
Ici avec Xcode 5.0.2 :
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="4514" systemVersion="12F45" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES"> ... </document>
Ici avec Xcode 4.6.3 :
<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="8.00"> <data> ... <int key="IBDocument.defaultPropertyAccessControl">3</int> <bool key="IBDocument.UseAutolayout">YES</bool> <string key="IBCocoaTouchPluginVersion">2083</string> </data> </archive>
Écrivons des interfaces de binding pour ces deux versions :
@XPath("/document[@version = '3.0']") public interface Xib3 extends Xib { @Override @XPath("@useAutolayout = 'YES'") boolean hasUseAutoLayout(); }
et :
@XPath("/archive[@version = '8.00']") public interface Xib8 extends Xib { @Override @XPath( "data/bool[@key = 'IBDocument.UseAutolayout'] = 'YES'") boolean hasUseAutoLayout(); }


Pour lire des documents XML de versions différentes, on utilise les sous-interfaces pour le binding effectif, en ne déclarant pour l’accès aux données qu’une seule variable du type de la super-interface :
Xib xib; xib = DomBinderUtils.xmlContentToJava(new File( "MyViewController_iPhone_3_0.xib"), Xib3.class); assertTrue(xib.hasUseAutoLayout()); xib = DomBinderUtils.xmlContentToJava(new File( "MyViewController_iPhone_8_0.xib"), Xib8.class); assertTrue(xib.hasUseAutoLayout());
Maintenant, ajoutons des méthodes pour lire des sous-structures contenues dans le fichier xib :
public interface Xib { boolean hasUseAutoLayout(); IBUIView getIBUIViewByTag(int tag); // e.g. 1104 interface IBUIView { String getFrame(); // e.g. "{{82, 77}, {43, 43}}" String getUserLabel(); // e.g. "LButton 20" } }

Évidemment, les expressions XPath ne sont pas du tout les mêmes pour les deux structures XML.

Pour Xcode 5.0.2 :
@XPath("/document[@version = '3.0']") public interface Xib3 extends Xib { @Override @XPath("@useAutolayout = 'YES'") boolean hasUseAutoLayout(); @Override @XPath("//*[@tag = $arg0]") IBUIView3 getIBUIViewByTag(int tag); interface IBUIView3 extends Xib.IBUIView { @Override @XPath("concat('{{', rect[@key = 'frame']/@x, ', '" + ", rect[@key = 'frame']/@y, '}, {'" + ", rect[@key = 'frame']/@width, ', '" + ", rect[@key = 'frame']/@height, '}}')") String getFrame(); // e.g. "{{82, 77}, {43, 43}}" @Override @XPath("@userLabel") String getUserLabel(); // e.g. "LButton 20" } }
Et pour Xcode 4.6.3 :
@XPath("/archive[@version = '8.00']") public interface Xib8 extends Xib { @Override @XPath( "data/bool[@key = 'IBDocument.UseAutolayout'] = 'YES'") boolean hasUseAutoLayout(); @Override @XPath("//object[int[@key = 'IBUITag'] = $arg0] ") IBUIView8 getIBUIViewByTag(int tag); interface IBUIView8 extends Xib.IBUIView { @Override @XPath("string[@key = 'NSFrame']") String getFrame(); // e.g. "{{82, 77}, {43, 43}}" @Override @XPath("//object[@class = 'IBObjectRecord'" + " and reference[@key = 'object']/" + "@ref = $this/@id]/" + "string[@key = 'objectName']") String getUserLabel(); // e.g. "LButton 20" } }
Quel que soit le binding initial de l’objet « xib », on pourra valider le code suivant grâce au polymorphisme :
final IBUIView view = xib.getIBUIViewByTag(1104); assertEquals("{{130, 100}, {128, 128}}", view.getFrame()); assertEquals("Bouton OK", view.getUserLabel());

Passons maintenant à de l’héritage qui ajoute des fonctionnalités :


Dans notre exemple, les interfaces IBUIButton dérivent des interfaces IBUIView, en leur ajoutant la méthode « getNormalTitle() », dont l’annotation @XPath dépend encore une fois de la version du document XML.

avc-binding-dom permet alors d’écrire ceci :
final IBUIView view = xib.getIBUIViewByTag(1104); final IBUIButton button = BinderUtils.rebind( view, IBUIButton8.class); assertEquals("OK", button.getNormalTitle());
Eh oui, les relations d’héritage dans notre diagramme ne permettent pas de remonter à IBUIButton8 à partir de « Xib8.getIBUIViewByTag(int): IBUIView8 ». Il faut faire une sorte de transtypage grâce à la méthode rebind().
Noter que d’autres possibilités de transtypage existent, par exemple la méthode self(Class<?>) quand l’interface de binding dérive explicitement de Binding<Node>, mais rebind() est une solution qui fonctionne dans tous les cas.

Pour que le transtypage fonctionne de façon transparente avec des interfaces génériques, c’est-à-dire si on voulait pouvoir écrire « rebind(view, IBUIButton.class) » au lieu de « rebind(view, IBUIButton8.class) », il faudrait plutôt que « Xib8.getIBUIViewByTag(int) » renvoie directement un IBUIButton8 et non un IBUIView8.
Le diagramme devient alors :


Et le code :
final IBUIView view = xib.getIBUIViewByTag(1104); // ni rebind(), ni IBUIButton8, ni IBUIButton3 final IBUIButton button = (IBUIButton) view; assertEquals("OK", button.getNormalTitle());

Voilà :-)

Edit : pour le dernier exemple, je montre qu’on peut utiliser un cast, plus simple qu’un rebind().


Note : les bouts de code et les exemples de cet article sont inclus dans les tests unitaires de avc-binding-dom.

11 juin 2013

Une API de binding Java/XML : avc-binding-dom

Si vous avez un besoin d’extraire en Java des données d’un document XML, je vous présente avc-binding-dom, une magnifique petite API.

Le principe est simple : on déclare le chemin XPath de la donnée qu’on veut lire à l’aide d’une annotation Java.

Exemple :
interface Annuaire {
@XPath("/annuaire/departement/@id")  
  String getDepartementId(); 
  @XPath("/annuaire/@annee") 
  int getAnnee(); 
} 
... 
// static import net.avcompris.binding.dom.helper.DomBinderUtils.*;
File file = ... ; // un fichier XML 
Annuaire annuaire = xmlContentToJava(file, Annuaire.class);
... annuaire.getAnnee(); // hop, ça marche

L’API est en 0.1.1, et, si elle est prometteuse, elle n’a été testée que sur des petits projets – en particulier, il doit lui manquer quelques types primitifs, la gestion des dates doit être brute de décoffrage, des trucs comme ça.

L’API permet d’ores et déjà de faire des requêtes paramétrées :
@XPath("//person[email = $arg0]/name") 
String getPersonNameByEmail(String email);

À partir d’un document XML, elle peut récupérer des tableaux, des couples clés / valeurs, divers types Java, des sous-structures, elle peut vérifier qu’un élément est présent, compter le nombre de éléments vérifiant une condition, faire des calculs à la volée…
Je vous renvoie à la page Return Types de la documentation.

Pour ceux qu’intéresse la mécanique interne, l’API attache l’interface Java passée en paramètre au nœud DOM. Cela signifie en particulier que si le document DOM est modifié, les méthodes Java getXxx() annotées avec des chemins XPath renverront les données mises à jour.

L’écosystème de cette API est le suivant :

Pour la petite histoire : j’avais initialisé une première version de cette API chez Capgemini pour un projet Java à l’automne 2010. Capgemini a ensuite validé le fait de passer ce développement open source, qui a continué de vivre sa vie sous la forme de XmlField, maintenu notamment par Nicolas Richeton, Jean-Pierre Grillon et Mabrouk Belhout.

Liens pour XmlField :
Blog de Nicolas Richeton : http://blog.richeton.com/
L’API XmlField a notamment la capacité de modifier les nœuds DOM depuis des méthodes Java de type setXxx() en suivant la même logique que les getXxx(), ce qui est très pratique pour de la manipulation de données XML.

J’ai ouvert le projet avc-binding-dom pour des besoins spécifiques, en réécrivant tout à la base, et en me concentrant sur mes besoins immédiats. Les méthodes setXxx() ne sont ainsi quasiment pas gérées dans avc-binding-dom.


27 décembre 2012

Deux ans…

Eh, ça fait plus de deux ans maintenant.
Voyons, que s’est-il passé en deux ans ?…

18 septembre 2010

Je passe à Mercurial

J'avais des repositories Subversion sur un serveur à la cave, et il a planté hier. Heureusement qu'il y a les backups automatiques journaliers (backup-manager + cron qui envoyait tout ça sur une VM xen sur un serveur distant).

Je vais en profiter pour migrer la totalité de mes projets vers Mercurial, un DVCS bien sympa, même que le Project Hosting de Google Code l'utilise.

L'architecture que j'ai choisie est celle-ci :
  • hébergement chez Google Code quand le projet a vocation à être open source
  • hébergement sur une VM sur un serveur dédié loué quand le projet est privé
  • backups dans tous les sens grâce aux repositories locaux sur mes machines
Simple et pas prise de tête.

Le serveur qui vient de flancher hébergeait déjà quelques repositories Mercurial, histoire de découvrir. J'avais fait ceci :
  • accès par SSH et clefs publiques (tout comme j'utilisais Subversion)
  • j'avais installé mercurial-server (mauvaise idée car j'ai dû pour cela passer à la sid (squeeze) au lieu de rester en lenny),
  • et tout le bataclan des clefs nommées, etc. (cela dit c'est propre)
Maintenant je fais la chose suivante :

Fichier .hgrc sous Windows
[ui]
username = dandriana
ssh="C:\Program Files\Putty\PLINK.EXE" -ssh -l hg -i "D:\Mes Documents\ma_clef.ppk"
J'utilise PAGEANT sous Windows pour ajouter la clef en début de session.

Sur la VM (xen) du serveur distant, j'installe Mercurial et crée un utilisateur Unix hg. Mes projets seront dans /home/hg/projects/

Sur le Dom0 (le serveur distant), je crée un utilisateur Unix hg, mais sans installer Mercurial.

Pour cet utilisateur je crée un exécutable de nom hg, que je mets dans le PATH, et qui contient ceci :
#!/bin/sh
ssh "hg $*"
En effet, quand je ferai sous Windows « hg -v clone ssh:////home/hg/projects/toto » (attention au double slash « // » avant « home » quand il s'agit d'un chemin absolu) pour récupérer le projet « toto », voici ce qui se passera :
  • le programme hg sous Windows (installation standard de Mercurial) se connectera via SSH au serveur distant avec le nom d'utilisateur hg (voir plus haut, directive « -ssh -l hg » de PLINK),
  • il essaiera d'y lancer la commande « hg -R /home/hg/projects/toto serve --stdio » (c'est l'option « hg -v » plus haut qui permet d'afficher ce détail),
  • donc le serveur distant lancera en fait « ssh "hg -R /home/hg/projects/toto serve --stdio" »,
  • c'est-à-dire se connectera à la VM, où s'exécuteront les opérations Mercurial.
Boah, ça fonctionne bien.

Je trouve deux énormes intérêts à un DVCS :
  1. les commits en mode déconnecté. Je peux poser des savepoints alors que je travaille dans le train, ou que je progresse pas à pas.
  2. le fait de ne pas à faire de choix structurant quant à utiliser un serveur plutôt qu'un autre (à la maison ? loué ? partagé ?…)
Je vais sans doute utiliser hgsvn pour finir la migration.

14 août 2010

Réorientation professionnelle

Mes années dans le service informatique…

2000, Sinclair&Partners à Paris (La Défense),

2001, Logica > LogicaCMG > Logica à Bordeaux (Gradignan),

2004, Cap Gemini Ernst & Young > Capgemini à Bordeaux (Pessac > Mérignac),

2006, indépendant sous l'enseigne Avantage Compris à Bordeaux (Pessac > Mérignac),

2010, je signe un nouveau contrat chez Capgemini, cette fois-ci à Paris (Suresnes et La Défense).

Capgemini est un grand groupe, avec qui j'avais déjà pu faire de belles choses en étant basé à Bordeaux, et notamment une petite mission Flying Squad à New York en 2005. Gageons que l'avenir saura se montrer à la hauteur :-)
Je remarque d'ores et déjà les changements sur le marché : dans les cas où l'agilité est une évidence méthodologique elle ne fait plus peur, SOA est une notion intégrée dans le paysage — j'ai même vu des clients parler naturellement de gouvernance —, et les développements offshore maîtrisés semblent être la norme.
Confronté à un nombre réduit de clients, même s'ils étaient souvent prestigieux — technologies obligent —, je n'avais pas forcément en tant qu'indépendant suffisamment de champ de vision pour arriver à cette idée. Le fait de me retrouver dans l'entreprise (80 000 collaborateurs inscrits sur la partie KM d'un intranet, ça calme toujours) est une bonne chose.

Et le statut d'indépendant : plus jamais ? C'était pas bien ? Je dirais au contraire que c'est une excellente expérience, qui outre le fait d'obliger à gérer chacun des aspects de sa structure, rapproche du sens business de notre métier. En tant qu'indépendant on voit mieux ses forces et ses faiblesses… celles des autres aussi. Je pense qu'on est davantage poussé à la coopération. On attache peut-être davantage d'importance à la complémentarité, ce qui est un plus ensuite.

Bref, c'est reparti :-)

Ah, et je laisse mes amis kabbalistes discuter du sens de « Rê-orientation » et de la figure de l'as de pique :-)

Blog de « Création Mohair », à Limoges

Tiens, du coup « Création Mohair » a mis à jour son blog : http://www.creationmohair.com/blog/

02 juillet 2010

Boutique « Création Mohair », à Limoges

J'ai découvert tout à l'heure la boutique « Création Mohair », à Limoges.

La boutique expose de nombreuses créations très chouettes, en mohair, mohair et soie, ou coton. J'avoue en être tombé sous le charme, et du coup je me fends d'une petite note.

Les créateurs de « Création Mohair » ont une approche assez sympa et écolo : oui, la matière première vient d'animaux estampillés bio (Ecocert), et les teintes sont naturelles (en tout cas celles que j'ai vues). Mais il y aussi le soin du détail dans toute la chaîne de production, qui se retrouve évidemment dans leurs produits et la finition (eh oui, j'aime bien ceux qui fignolent les détails).

Leur site web permet d'acheter par correspondance.

Leur blog, pas hyper mis à jour, affiche des photos des chèvres et du chien :



Manque de bol, je ne trouve pas sur le site les créations les plus récentes, notamment une petite robe en coton tricotée main assez géniale, et qui à mon avis mériterait un site de vente pour elle toute seule.

Ils ont aussi des petits bonnets pour enfants tout à fait craquants, assez originaux, mais hélas je ne les retrouve pas non plus sur le site (là, j'ai peut-être mal cherché).

Alors, est-ce que c'est vraiment le moment, alors qu'il fait 30°C en ville, de parler mohair ? Eh bien pour l'été ils ont une étole mohair et soie tricotée machine, très légère, euh… enfin à chacun de se faire son idée, bien sûr ! En vitrine en tout cas, elle tape pas mal à l'œil :-)

Quant à la toute récente petite robe en coton, c'est clairement une robe d'été. Il faut demander à la voir, car elle n'est même pas encore exposée !

J'ai beaucoup aimé ce magasin et son esprit :-)

28 mars 2010

Gilles Lipovetsky

Chez Nicolas Bordas, un article qui parle de Gilles Lipovetsky intitulé « Et si la culture-monde avait aussi du bon ? ».

Pour reprendre quelques thèmes évoqués dans l'article, je ne crois pour ma part ni à une fin programmée de la morale, ni à une disparition des questions de classes économiques, ni à la possibilité de se démarquer asymptotiquement de l'hyperconsommation.

Le bon point, c'est que se poser ces questions à l'heure où la confirmation affective est accessible, en particulier via le web quand il consolide et nourrit nos réseaux, nous aide à nous considérer de nouveau comme des êtres multidimensionnels.

28 février 2010

Application packagée et ressources externes

Dans le monde J2EE/JEE on déploie une application web sous forme d'archive compressée : l'extension « .war » du fichier signifie « webapp archive ».

Dans le monde PHP, pour ne citer que lui, on peut rencontrer le même fonctionnement, avec les fichiers à déployer fournis sous forme d'archive compressée « .zip » par exemple.

Pourtant, sauf en de rares exceptions, une application ne se limite pas à du code exécutable et à des ressources statiques, mais a besoin de ressources externes telles qu'une base de données, un service de messages, ou des services tiers comme des services web.

La base de données est une ressource externe au code déployé

Prenons le cas le plus courant, où l'application utilise une base de données seulement.


En ce qui concerne ce lien, l'application contient du code exécutable, qui encapsule du SQL, qui attaque base de données.

Pour le déploiement, d'un côté on a créé la structure de la base de données, de l'autre on a déposé l'archive de l'application.


Ces deux déploiements conjoints doivent produire un environnement global cohérent. Penser aux « contrats d'interfaces ».


La correspondance d'interfaces entre le client et la ressource invoquée, est cruciale.

Apporter la cohérence par le déploiement lui-même

L'application qui crée la base de données dont elle a besoin

Une astuce qu'emploient de nombreuses applications destinées au grand public est d'opérer la création de la base de données depuis la base de données elle-même.


(J'ai barré pour dire que cela ne me convient pas)

On met l'accent sur la facilité pour l'utilisateur lambda d'initialiser l'environnement d'exécution.

C'est le modèle choisi pour quasiment toutes les applications PHP. Plusieurs frameworks Java offrent également cette possibilité.

Cette approche est sympathique, mais n'est pas forcément cohérente avec une logique de production, et, surtout, de maintenance.

Je trouve d'autre part que c'est prendre un risque opérationnel, car même en production, le code qui permet de supprimer et de recréer les tables de la base de données est embarqué.

L'outil de déploiement couteau suisse


En posant la distinction entre composants déployés et outillage d'administration, on peut utiliser un outil de déploiement qui saura s'occuper de déployer et d'administrer aussi bien l'application que la base de données.


Même si elle a un très gros intérêt, une faiblesse de cette approche est qu'elle réclame la mise en œuvre d'un outil qui sache tout faire, et surtout son utilisation systématique : que se passe-t-il si un administrateur passe outre la procédure outillée ? Un ALTER TABLE en manuel est si vite arrivé…


Contrôler la cohérence avant le démarrage de l'application

Pour la qualité il n'est souvent pas nécessaire d'engager de grandes manœuvres ou de mettre en place des outils contraignants : il suffit parfois de contrôler quelques critères et de bloquer ou de continuer la procédure en fonction.

Une approche consiste alors à embarquer dans le module applicatif déployé une description du modèle physique de données attendu, et de le comparer avec la structure réelle de la base de données.


On bloque le processus de démarrage de l'application si les deux ne correspondent pas (un simple « hash » peut suffire).

Personnellement j'utilise comme référence du modèle un fichier YAML posé dans le répertoire WEB-INF/ de l'archive déployée, ce qui me permet d'avoir une approche unique pour PHP et J2EE/JEE.
Pour Java, on peut penser à introspecter les classes persistantes afin d'en déduire le modèle attendu.

Une sécurité supplémentaire est d'embarquer dans l'application elle-même, non pas du code de manipulation de base de données, mais l'outil de comparaison. L'application elle-même refuse alors de démarrer si la structure de la base de données n'est pas cohérente avec le modèle attendu.



Autres vérifications

Le fichier de référence qui décrit le MPD cohérent avec l'exécution de l'application peut servir à d'autres contrôles, et notamment les suivants :
  • cohérence entre le MPD et les requêtes SQL externalisées (penser aux objets PreparedStatement)
  • cohérence entre le MPD et les classes persistantes des divers frameworks ou approches (Hibernate, JDO…)
Environnements de tests

En phase de tests, on souhaite d'une façon ou d'une autre que l'application soit autoporteuse : à la fois déployer une application et exécuter des scripts SQL de modification de la base de données (de tests), peut être inutilement coûteux.

J'ai parlé plus haut des frameworks qui permettent ça nativement, en disant cependant que l'approche qui mixe modules applicatif et d'administration ne me convenait pas.

Le fait d'embarquer, dans le module applicatif lui-même, une description de référence du modèle attendu, permet de recréer à volonté une base de données de tests cohérente.



Pour ma part je fais cela avec un plugin Maven.

Approche générale, autres ressources

Le point crucial de ce qui précède est de bloquer le démarrage de modules dans le cas où on a décelé que les chaînes de liaison ne sont pas correctes.

Pour cela, l'idée proposée est simplement que le module client sache dire de quelle interface il a besoin, et que la ressource invoquée sache dire au runtime quelle est sa structure.


On obtient une cartographie « à froid » des composants déployés.

On fait ensuite une vérification statique des structures lues, grâce à des outils externes aux modules et ressources déployés.

Aucun outillage lourd n'est nécessaire en amont.

Il faut se rappeler du reste que, dans certains cas réels, l'outillage lourd en amont n'est pas suffisant à assurer la cohérence entre tous les composants déployés, en particulier quand les procédures de déploiement ne sont pas entièrement automatisées.

L'approche pragmatique présentée, complémentaire à l'outillage amont, et qui fait des sanity checks simples, permet de détecter des failles évidentes.

26 février 2010

Asso&Co

Je cite : « La Banque Postale est à l'initiative d'un nouveau site dédié au monde associatif » : http://assoandco.fr/

De bonnes idées. Pas vu si un même compte peut servir à gérer plusieurs associations.

13 février 2010

Restauration quotidienne de sauvegardes

Voici un état de mon architecture de sauvegardes, qui a un peu évolué depuis que j'avais parlé du sujet il y a deux ans (voir : Validation de backups, avril 2008).

Architecture

Les nouveautés concernent essentiellement l'ordonnancement.

Sur le plan fonctionnel, les principes sont :
  • une sauvegarde complète quotidienne
  • une restauration complète quotidienne
  • des tests d'acceptabilité sur les données restaurées (par exemple, vérifier que les données qu'on a restaurées ne sont pas constituées de répertoires vides)
Sur le plan technique, les principes sont :
  • pour tout ce qui concerne les flux entre machines, l'ordonnancement est centralisé
  • les flux sont sécurisés (SSH)
  • les accès sont sécurisés de machine à machine (restrictions des adresses IP autorisées, etc.)
  • on limite les volumes des transferts réseaux (compression BZ2, regroupement des commandes envoyées par SSH…)
L'architecture se synthétise ainsi :


Légende :
  • ref — référentiels, « Repositories » : Ce sont les éléments à sauvegarder. En l'occurrence Subversion, un wiki…
  • m2 — ordonnanceur, « Scheduler »
  • bkp — espace de sauvegarde, « Archives »
  • tmp — espace temporaire pour restauration
  • ci — outil d'intégration continue, « Continuous Integration »
Les machines « ref » et « m2 » sont sur un même réseau local (LAN).
  • « ref » est visible sur internet, ports 22 et 80. Sur le LAN elle est dans une DMZ, et en particulier n'accède pas à « m2 ».
  • « m2 » n'est pas visible sur internet.
Les machines « bkp + tmp » et « ci » sont sur internet, avec leurs accès SSH limités à certaines adresses IP.

Les étapes du processus sont les suivantes :
  1. sauvegardes locales sur « ref », par l'outil « backup-manager »
  2. copie vers une machine distante qui stocke les archives, avec éventuellement une copie intermédiaire locale sur l'ordonnanceur lui-même
  3. décompression de la dernière sauvegarde présente dans les archives, restauration, contrôles et mesures
  4. copie des résultats des contrôles et mesures, vers une machine d'intégration continue
  5. analyse des résultats des contrôles et mesures, tests sur des critères d'acceptabilité
  6. [E] si une erreur est décelée, envoi d'un e-mail à l'administrateur
En gros, j'arrête de poser des crontabs partout comme je faisais il y a deux ans (voir article : Validation de backups) ; c'est un plat de spaghetti à administrer, et, quand une machine tombe ou est recyclée, les autres continuent leur semoule sans que ça serve à quoi que ce soit.

L'ordonnancement centralisé facilite les enchaînements.

En revanche, il ne facilite pas forcément la gestion de verrous.

Autre changement par rapport à il y a deux ans, j'écris mes scripts système en Bash et délaisse Ruby, qui n'apportait pas tant que ça.


Contrôles et mesures

Pour un repository Subversion, après restauration des données, les tests d'intégration continue se feront sur les mesures suivantes :
  • nom et taille du fichier de sauvegarde restauré => la date doit être celle du jour
  • dates de début et fin de la restauration => le traitement ne doit pas avoir été trop rapide
  • numéro de version (Revision) => ?
  • nombre total de fichiers après checkout => il doit être supérieur à telle valeur
  • nombre de fichiers « pom.xml » après checkout => il doit être supérieur à telle valeur
  • nombre de fichiers « build.xml » après checkout => il doit être supérieur à telle valeur
L'idée est de repérer les cas de sauvegardes vides, ainsi que les sauvegardes obsolètes.


Quelques idées rejetées

Il est bon de fonder ses choix aussi sur l'historique des choix qui n'ont pas été retenus ;-)


Idée rejetée : copie directe depuis la machine de référentiel vers la machine de backup.


Explication : pourquoi la machine de référentiel devrait-elle avoir connaissance de l'existence d'une machine de backup ? Et en être dépendante ? Et dépenser de la charge et être administrée pour ça ?
L'idée est que les processus de restauration d'archives doivent être vus par ailleurs. La machine à sauvegarder n'a pas elle-même à savoir qu'elle devrait envoyer un e-mail, etc.


Idée rejetée : accès direct depuis la machine de backup à la machine de référentiel.


Explication : on a choisi dans l'architecture de limiter les accès SSH à « ref» depuis l'internet à l'utilisation de Subversion. En particulier, pas d'accès aux utilitaires « ls » et « tar ».


Idée rejetée : stocker les archives sur la machine d'ordonnancement.


Explication : cette machine est dans les mêmes locaux que la machine référentiel. Or on veut évidemment que les données archivées soient physiquement découplées des données d'origine.


Idée rejetée : ordonnancement par cron de la restauration depuis l'espace temporaire.

C'est ce que faisait l'ancienne architecture.



Explication : l'espace temporaire n'a pas à être administré et donc n'a pas à contenir de scripts résiduels ni de crontab.


Idée rejetée : restauration sur la machine d'ordonnancement.


Explication : Il faut que la restauration ait lieu à partir d'un élément archivé (celui qu'on ira réellement chercher en cas de problème), et pas depuis une copie intermédiaire, qui par définition disparaîtra.
De plus, ni la mémoire ni le CPU ne sont suffisants sur cette machine pour une restauration.


Idée rejetée : envoi des résultats directement depuis l'espace temporaire vers la machine d'intégration continue.


Explication : l'espace temporaire n'a pas à connaître la machine d'intégration continue, à y avoir posé sa clef publique, etc.


Idée rejetée : récupération des résultats par la machine d'intégration continue auprès de l'espace temporaire.


Explication : la machine d'intégration continue n'a pas à savoir qu'il existe un espace temporaire (en plus, comment s'y connecter s'il n'est pas résiduel ?)


Variante possible

Une variante qui semble être digne d'intérêt, consiste à stocker les résultats des contrôles et mesures dans les référentiels, et que la machine d'intégration continue y accède.


Quelques avantages :

  • l'ordonnanceur n'a plus à connaître la topographie de la machine d'intégration continue
  • les résultats des contrôles et mesures sont archivés
  • on s'appuie sur le lien de l'ordonnanceur vers la machine référentiel, qui existe déjà
  • on s'appuie sur le lien de la machine d'intégration continue vers la machine référentiel, qui existe déjà
  • les tests sur les résultats peuvent être lancés depuis n'importe quel environnement qui a accès au référentiel, pas seulement depuis la machine d'intégration continue (donc, pratique pour développer ces tests)


Quelques inconvénients :
  • le référentiel augmente de façon automatique et ininterrompue, or ce référentiel est lui-même destiné à être sauvegardé et archivé, donnant lieu à des contrôles, qui seront injectés dans le référentiel, etc.
    Certes l'augmentation est d'1 Ko par jour, ce qui est dérisoire, mais techniquement, le principe du mécanisme croissant qui s'alimente lui-même n'est pas bon.
  • d'un point de vue fonctionnel ce serait mélanger production et décisionnel.
    Production = référentiel Subversion.
    Décisionnel = tests en intégration continue après extraction de mesures.

10 février 2010

Curriculum Vitæ à jour

Après la compta à jour et même le bilan d'Avantage Compris au clair, voici que je complète mon CV dans les temps !

CV en ligne : http://www.avantage-compris.com/team/dandriana/

J'en ai même fait une version anglaise, mais pour l'instant le site de ma boîte n'est qu'en français :-P

03 février 2010

IZI-collecte, plate-forme web pour les associations

D'après la page d'accueil, IZI-collecte (http://www.izi-collecte.com/) propose :
  • un paiement sécurisé pour les dons et les cotisations à un tarif bas et fixe, comprenant l’émission du e-reçu fiscal et des fonctionnalités communautaires
  • une plateforme d’envoi d’e-mailings et d’e-newsletters.
  • la gestion de base de données adhérents et donateurs

01 novembre 2009

Démarrer tftpd

J'avais parlé d'une net install de Debian sur Mac PowerPC : Mac mini de NetBSD à Debian

Eh bien il se trouve qu'il est parfois difficile de démarrer from scratch un tftpd pour que le Mac l'attaque. Alors voici une façon de faire, sur une Ubuntu 9.04 (eh oui : la 9.10 est d'une discutable stabilité).
$ sudo aptitude install tftpd
inetd ne veut pas démarrer au cours de l'install ? Pas grave, l'idée n'était justement de le rendre résiduel.

Modifier /etc/inetd.conf tout de même, en rajoutant l'option « -s » à : /usr/sbin/in.tftpd -s /srv/tftp

Mettre les fichiers à servir dans /srv/tftp/ : yaboot, etc.

Démarrer inetd :
$ sudo inetd

11 octobre 2009

La persistance applicative comme un aspect

Dans le développement d'une application on pense souvent très tôt à la solution de persistance. Certains ont même une idée tellement précise du « modèle physique des données » — terminologie qui est une drôle de façon se se rassurer —, qu'ils vont à terme jusqu'à contraindre le comportement de l'application ou du système pour se conformer au schéma de la base. Ainsi du genre de réponses « on ne peut pas faire ce que vous demandez, à cause des clefs étrangères qui sont déclarées dans la base ». Ah… Et pourquoi ne pas faire des batchs d'audit et de rattrapage ? Et pourquoi ne pas gérer des états de mes données ? Et pourquoi ne pas laisser les transitions se faire de façon asynchrone ? Et surtout : si vous vous appuyez sur des clefs étrangères pour assurer l'intégrité métier de mes données, vous me faites plutôt penser que vous n'avez pas compris mon métier !
Quel métier se résume à des contraintes techniques d'intégrité ? (contraintes que du reste les DBAs font la plupart du temps sauter en production afin d'améliorer les perfs).

Cependant je ne veux pas reparler ici de centrer les objectifs du cycle de développement sur les processus métier, je considère que c'est acté.

Non, je voudrais parler de persistance.

Il y a des cas où les processus métier sont suffisamment simples pour être embarqués dans une application transactionnelle, où il n'y a pas ou quasiment pas d'existant, bref, où on a la latitude pour développer. Pléthore de frameworks de persistance viennent en sus à notre rescousse, on peut citer Hibernate (https://www.hibernate.org/) dans les premiers.

Pourtant nombre d'entre nous continuent de vouloir poser le schéma de la base, quand bien même serait-il un mapping naïf des propriétés des objets Java, dès le commencement.

Pourquoi ne pas simplement développer l'application ? On met toutes les données en mémoire, et on optimisera ensuite ! J'ai juste besoin dans un coin d'un compteur transactionnel pour mes états métier (ben, oui), compteur que je peux implémenter par un synchronized tout bête, et je fais mouliner le reste avec des classes ad hoc.

Plus tard, quand j'aurai besoin de partager mes données sur un cluster, ou de performances, ou tout simplement de sauvegardes parce que j'aurai à mettre en place un PRA (Plan de reprise d'activité, en gros comment remettre d'aplomb un deuxième système informatique si le premier vient de calancher, par exemple suite à un incendie), alors là, oui, je me pencherai sur la persistance. Mais c'est un aspect de l'application, au même titre que la sécurité ou les logs. Ce n'en est pas le cœur ni le socle ; ce n'est pas fondamental : l'absence de persistance des données, dans le sens général, n'empêche pas l'application de tourner.

D'ailleurs combien, parmi ceux qui pensent de suite à la solution de persistance, le font réellement parce qu'ils envisagent un PRA ou des performances maximales ? Non, le plus souvent la couche de persistance est envisagée comme une nécessaire façon de réaliser une application. Dans d'autres domaines on sait s'affranchir de ce genre de préjugés : les compilateurs dégagent, avec Ruby ou Groovy et l'aide des IDEs, les descriptions XML sont supplantées par des conventions ou des annotations, le besoin de hardware diminue grâce au cloud, on se fiche de l'OS parce qu'on fait du web, etc.

La couche technique de persistance n'est pas nécessaire dans une application. En revanche la couche logique de manipulation simple des données, notamment avec des verrous voire des états, et des vérifications d'intégrité, oui.
Et il y aura toujours moyen plus tard d'orchestrer tout ça et d'y ajouter les aspects désirés.

Pour finir, l'approche que je décris vise à éjecter la problématique de la persistance lorsqu'on se centre sur du développement applicatif. Pour d'autres types de réalisations, par exemple techniques, la question de la persistance se rencontre évidemment dès le début du chemin.