Pour se remettre du précédent épisode sur le contrat réactif ça va faire du bien de regarder quelques petits utilitaires de Chiron, utilisables indépendemment des fonctions de connectivité.
=== Configuration
La bibliothèque **Chiron-configuration** fournit de quoi lire des fichiers de configuration ou des paramètres en ligne de commande. Cet outil a déjà été mentionné sur Techos alors qu'il portait le nom de "Wrench"
. L'idée c'est de définir les propriétés dans une interface dont les méthodes renvoient les types attendus, de rajouter des métadonnées ou du comportement dans une classe ``ConfigurationFactory``, puis de lire une source de données avec toujours le meilleur typage possible et les meilleurs retours d'erreur possibles. Il y a 3 ans aucune autre bibliothèque Java ne fournissait ce niveau de fonctionnalité et ça n'a pas du changer depuis.
=== Diagnostic
Dans **Chiron-toolbox**, l'interface ``Diagnostic`` définit le contrat pour représenter le paramétrage de l'application sous forme textuelle, avec des indentations. En général au démarrage on imprime ce rapport dans le journal d'exécution, ou on l'affiche dans la fenêtre "À propos".
La classe ``GeneralDiagnostic`` produit un rapport avec les propriétés système, les variables d'environnement et le classpath de Java.
La classes``InternetProxyAccessDiagnostic`` montre les paramètres de connexion à un proxy, tels qu'utilisés par le Downend.
La classes ``ConfigurationDiagnostic`` développe le contenu d'un objet ``Configuration`` (celui cité plus haut, issu de ``ConfigurationFactory``).
On peut bien sûr ajouter ses propres diagnostics. Un ``Diagnostic`` est quelque chose qui possède un nom, des paires clé-valeur sous forme de ``String``, et une liste de sous-diagnostics.
=== Proxy HTTP
Dans **Chiron-fixture**, le ``ConnectProxy`` est un proxy HTTP supportant uniquement la commande CONNECT, c'est à dire qu'il se contente paresseusement de faire le passe-plat sans s'intéresser au contenu HTTP.
Quel intérêt par rapport à un proxy HTTP embarquable en pur Java comme "LittleProxy"
? Le ``ConnectProxy`` permet d'ajouter du délai dans la transmission, chose que LittleProxy ne permet pas (les points d'extension de LittleProxy sont synchrones). Grâce à l'ajout de latence, on simule un réseau encombré, ou un proxy HTTP qui ne répercute pas correctement la perte de connexion au serveur-cible.
Attention le ``ConnectProxy`` est conçu pour tester unitairement la connexion à travers un proxy HTTP. Il n'est pas fait pour encaisser de fortes charges (contrairement à LittleProxy).
=== Redirection de port
Toujours dans **Chiron-fixture** on a le ``PortForwarder``, petit frère du ``ConnectProxy`` qui redirige tout ce qu'il reçoit vers un autre port, éventuellement sur une autre machine.
=== Client HTTP
Dans **Chiron-toolbox**, le ``NettyHttpClient`` est un client HTTP léger basé sur Netty. À l'origine il est conçu à l'origine pour écrire des tests, mais c'est aussi lui qui sert à envoyer des requêtes HTTP à Twilio.
<<<
final Recorder recorder = new NettyHttpClient.Recorder() ;
final NettyHttpClient httpClient = new NettyHttpClient() ;
httpClient.start() ;
final NettyHttpClient.Outcome response = recorder.nextResponse() ;
WatchedResponseAssert.assertThat( response )
.isComplete()
.hasStatusCode( HttpResponseStatus.OK )
;
>>>
Le ``NettyHttpClient`` peut utiliser une boucle d'événements externe. Dans sa version de base, il ne contient aucun état. Quand on exécute une requête, on passe une instance de ``NettyHttpClient.Watcher`` qui définit des méthodes pour traiter la réussite, l'échec, le délai de réponse expiré. Il n'y a que le code effectuant la connexion HTTP qui conserve une référence sur le ``Watcher`` (à travers les retours d'appel et la tâche planifiée pour indiquer l'expiration du délai de réponse). Même si ce n'est pas une fonctionnalité en soi c'est marrant de voir que la boucle d'événements Netty permet ça.
Dans l'exemple ci-dessus on remarque que l'instance du ``Watcher`` est un enregistreur qui accumule toutes les réponses reçues (y compris les erreurs comme l'expiration du délai de réponse). Comme ça on transforme un comportement asynchrone en comportement synchrone : le code du test bloque jusqu'à obtenir une réponse, ce qui permet de bien montrer la séquence attendue.
Selon les goûts et les besoins on pourra préférer le "Netty HTTP Client"
de Tim Boudreau, qui offre beaucoup plus de fonctionnalités, y compris pour les tests.
=== Certificats TLS
Dans **Chiron-toolbox**, le ``KeystoreAccess`` encapsule l'accès à un Keystore Java protégé par mot de passe. L'idée, c'est que l'application soit livrée avec un Keystore par défaut, versionné en même temps que les sources. Dans les paramètres de démarrage de l'application, il suffit de préciser le mot de passe pour le certificat correspondant à l'alias par défaut dans le Keystore par défaut. De cette façon la clé privée n'est dévoilée que sur les serveurs où ont lieu le déploiement. Mais souvent il faut tester un certificat alternatif en production, ou tester du code avec un certificat autosigné. La solution c'est que l'alias et l'URL de la ressource du Keystore soient des valeurs par défaut. Concrètement, la représentation textuelle du ``KeystoreAccess`` est la suivante :
<<<
[alias:]password[@URL-with-no-authority]
>>>
Le ``KeystoreAccess`` a un bon ami, l'``Autosigner``. L'``Autosigner`` sait générer une paire Keystore-Truststore, en mémoire ou dans un fichier. L'interface programmatique basée sur le modèle de conception "Bâtisseur" ("Builder") permet plein de variations sympas. Par exemple on peut charger un Keystore existant, ou exporter la clé privée dans un fichier. Comme ça on n'a pas besoin de redémarrer Wireshark à chaque fois qu'on démarre un test utilisant l'``Autosigner`` (Wireshark sait décoder le TLS si on lui file un fichier avec la clé privée). Et une fois que tout est chargé ou généré, l'``Autosigner`` produit l'objet ``KeystoreAccess`` approprié.
On a vu que le ``KeystoreAccess`` fournissait le Keystore sous forme d'un flux binaire tout droit sorti d'une URL. Ça peut être un fichier (``file:///``) mais si on veut tout faire en mémoire il y a moyen (pour accélérer les tests notamment). L'``Autosigner`` crée alors une URL spéciale avec un protocole ``keystore://`` et un ``URLStreamHandler`` renvoyant la copie d'un tableau d'octets en mémoire. Je sais que tu ne savais pas qu'on pouvait faire ça.
Voici un exemple d'utilisation de l'``Autosigner`` :
<<<
final CertificateHolder certificateHolder = Autosigner.builder()
.createCertificate( Feature.SUBJECT_KEY_IDENTIFIER )
;
final KeyStore keystore = certificateHolder.createKeystore() ;
final KeyStore trustStore = certificateHolder.createTruststore() ;
final String keypass = certificateHolder.certificateDescription().keypass ;
// etc.
final FileGenerator fileGenerator1 = certificateHolder
.withKeystoreFile( new File( "my-own-keystore.jks" ) )
.withTruststoreFile( new File( "my-own-truststore.jks" ) )
.withPrivateKeyFile( new File( "my-own-private-key.jks" ) )
;
fileGenerator1.generateFiles() ;
final FileGenerator fileGenerator2 = certificateHolder
.withDirectory( new File( "securitystores" ) ) ;
fileGenerator2.generateFiles() ;
final InterningGenerator interningGenerator = certificateHolder.interning() ;
final KeystoreAccess keystoreAccess = interningGenerator.keystoreAccess() ;
final URL truststoreAccess = interningGenerator.truststoreUrl() ;
>>>
Plus moyen de prétendre c'est trop compliqué d'écrire des tests pour le HTTPS !
=== Temps
Dans **Chiron-Toolbox** on trouve quelques trucs pour modéliser l'écoulement du temps.
``Clock`` est une interface pour obtenir une mesure du temps universel.
Le ``Pulse`` planifie des tâches à intervalles réguliers (chaque heure, minute, ou seconde) au moment où on passe à l'unité suivante. C'est beaucoup moins souple qu'un ``ScheduledExecutorService``, mais on a la garantie que le moment de l'exécution ne dérive pas car le ``Pulse`` replanifie chaque exécution au plus juste. Le ``PulseModulator`` est une implantation du``Pulse`` basée sur une horloge instrumentée pour les tests. Le ``Pulse`` est une alternative ultra-légère à "Quartz"
.
Un ``Stamp`` est un identifiant unique basé sur un horodatage à la milliseconde près, et un compteur pour différencier des instances créées durant la même milliseconde. Un ``Stamp.Generator`` génère des instances de ``Stamp`` avec des garanties d'unicité. Le code pour supporter des accès concurrents aux compteurs sans verrouillage est sympa.
Le ``TimeKit`` fournit une instance de ``Clock``, une instance de ``Stamp.Factory``, un générateur de ``Designator`` et une instance de ``Pulse``. Le ``TimeKit`` garantit qu'on utilise toujours la même ``Clock``. On peut créer un ``TimeKit`` avec une ``UpdateableClock`` pour modifier l'écoulement du temps à la main (dans les tests) et donc déclencher des événements de ``Pulse``.
Ces classes souffrent maintenant de la concurrence de `Java 8` qui offre notamment ``java.util.Clock``. De plus, Chiron utilise toujours les classes de Joda Time, et il faudrait migrer vers leur équivalent `Java 8`.
=== Collections
Guava fournit à peu près tout ce dont on peut rêver pour triturer les collections de Java sans se faire mal. Toujours dans **Chiron-toolbox** on trouve quelques classes pour boucher les derniers trous.
``KeyHolder`` est une interface pour les objets identifiables par une clé.
``KeyHolderMap`` est une ``java.util.Map`` pour référencer des instances de ``KeyHolder``. Chaque élément est référencé par une clé qui correspond à celle fournie par la valeur. Autrement dit la ``KeyHolderMap`` est une collection qui garantit que chaque élément est unique en regard de sa clé.
``ImmutableKeyHolderMap`` est une variante immuable qui respecte les conventions établies par Guava (y compris le ``Builder``). Une ``ImmutableKeyHolderMap`` garantit automatiquement la cohérence clé-valeur, la non-nullité, ainsi que le respect de l'ordre d'ajout lors d'une itération.
=== Enumération extensible
``Autoconstant`` est une classe de base de **Chiron-toolbox** pour une énumération typée qui supporte l'extension (et qui ne repose donc pas sur ``java.lang.Enum``). Une instance d'``Autoconstant`` garantit que son nom renvoyé par ``name()`` est identique à celui de la déclaration dans du code Java. On écrit donc ``static final MyConstant FOO = new MyConstant() ;`` Il y a aussi quelques trucs pour passer un paramètre de type par instance, et obtenir la liste de toutes les instances. Une classe dérivée d'``Autoconstant`` garantit que chaque instance (y compris parmi les sous-classes) possède un nom unique.
=== Retours d'erreur
Dans **Chiron-middle**, la classe ``TypedNotice`` fournit la base pour des retours d'erreurs avec un code normalisé (basé sur une ``enum``) et un message personnalisable (par défaut défini par l'``enum``). On peut la dériver de façon à utiliser n'importe quelle énumération qui implante l'interface ``EnumeratedMessageKind`` qui reprend les méthodes d'une ``enum``. (Oui ça serait bien de migrer vers ``Autoconstant`` qui est un peu là pour ça, plus le temps passe plus c'est dur de garder un code homogène.)
Une ``TypedNotice`` est destinée à être envoyée au Downend, c'est à dire qu'elle ne doit pas contenir d'information sensible ; notamment elle ne contient pas d'exception Java.
=== SSH
Dans **Chiron-ssh** il y a la merveille des merveilles : une coquille pour exécuter des programmes sur une machine distante, sans autre prérequis qu'un accès par SSH. Le ``SshDriver`` exécute des programmes tous nus, tandis que le ``SshJavaDriver`` exécute des programmes Java en recopiant tous les fichiers accessibles dans le classpath courant. Autrement dit, tu écris une classe exécutable qui communique par les entrées-sorties standard, tu appuies sur le bouton, et magiquement elle s'exécute sur une ou plusieurs machines distantes pourvu que tu aies préalablement déployé une JVM et ta clé publique. Pour des tests d'intégration c'est l'arme absolue : à part le petit délai pour la synchronisation des fichiers ``.class`` tu as l'impression de sauter la phase construction-déploiement.
Le ``SshDriver`` supporte plein d'options sympathiques :
- Une condition arbitraire sur la sortie standard pour indiquer que le démarrage a eu lieu.
- Une ``CompletableFuture`` pour savoir quand le démarrage a eu lieu.
- Une ``CompletableFuture`` pour savoir quand l'arrêt a eu lieu.
- Un arrêt immédiat avec ``#stop()``.
- Des fils d'exécution convenablement nommé pour des journaux d'exécution plus lisibles.
- Une option pour ne pas allouer un pseudo-terminal ("pty"), ce qui conserve la distinction entre la sortie d'erreur et la sortie standard, et empêche que le démon SSH tue le processus à la fin de la session (un peu le même effet que le ``nohup``).
Le ``SshDriver`` utilise l'excellentissime bibliothèque SshJ, ainsi que Ssh-Agent pour éviter d'avoir à saisir son mot de passe à chaque exécution. Il faut donc déployer préalablement sa clé publique.
Le ``SshJavaDriver`` étend le ``SshDriver``. Tu lui files juste :
- Le répertoire d'exécution.
- Le chemin vers l'exécutable de la JVM.
- Des propriétés système.
- Un énumérateur des ``.jar`` ou ``.class`` à copier sur la machine distante (par défaut le classpath courant).
- La classe exécutable et les paramètres.
- Un port JMX si on veut faire du JMX non-authentifié.
Oui on peut passer des options pour profiler ou déboguer à distance.
Le ``SshJavaDriver`` devrait à lui tout seul apporter des millions d'étoiles au projet Chiron sur GitHub. Ça serait bien qu'un contributeur fasse évoluer le code pour ne pas lancer une commande SCP pour chaque fichier.
=== Autres
Allez encore quelques petits trucs de **Chiron-toolbox**.
``ToStringTools`` pour faire des ``toString()`` dans la joie, notamment extraire le nom de classe le plus lisible possible.
``TcpPortBooker`` pour trouver un port TCP de libre. C'est utile pour écrire des tests qui ne se marchent pas dessus. Le principe est d'affecter des ports "sentinelle" à intervalle régulier pour marquer les plages de port utilisées par un ``TcpPortBooker``. Comme l'ouverture d'une Socket est une opération atomique, un autre ``TcpPortBooker`` dans une autre machine virtuelle ira chercher une autre plage de ports.
``SafeSystemProperty`` pour enrober des propriétés système dans de jolies constantes typées. Comme ça on sait si la propriété est définie, si elle bien formée, quelle est sa valeur, et comme une ``SafeSystemProperty`` est immuable on peut recharger sa valeur en créant une nouvelle instance. En standard on a des ``SafeSystemProperty`` pour ``String``, ``List< String >``, ``Boolean``, ``Integer``, ``File``, ``Hostname``. On peut ajouter ses propres types.
``AbstractService`` pour un une unité d'exécution qui se démarre et s'arrête. C'est la classe de base de ``SshDriver`` mais c'est drôlement pratique pour écrire son propre conteneur d'application embarquable. Voir la Javadoc de "``Service``"
pour une comparaison avec son homologue dans Guava.
On a aussi les classiques : ``Holder``, une référence à un objet qu'on ne peut affecter qu'une seule fois, et ``Lazy``, une référence à un objet à travers une fonction évaluée une seule fois et le plus tard possible.