stupeflix

Projet de démo d'un environnement de dev automatisé

L'idée

On va faire un portail de diffusion de vidéo

Environnement de dev

Le projet est en crystal-lang mais ça importe peu. Le reste est agnostique du langage.

initialisation

crystal init app stupeflix
cd stupeflix
rm .travis.yml

Dans le fichiers shards.yml, j'ajoute kemal :

dependencies:
  kemal:
    github: kemalcr/kemal

Je lance la récupération de la librairie

shards install

Puis j'écris le code Hello World pour démarrer

require "kemal"

get "/" do
  "Hello World!"
end

Kemal.run

Je lance le serveur pour vérifier que ça fonctionne

crystal src/stupeflix.cr
[development] Kemal is ready to lead at http://0.0.0.0:3000

Ajoutons maintenant les lib de test

Dans le shards.yml

dependencies:
  kemal:
    github: kemalcr/kemal


development_dependencies:
  ameba:
    github: crystal-ameba/ameba
    version: ~> 0.13.0
  spec-kemal:
    github: kemalcr/spec-kemal
  crytic:
    github: hanneskaeufler/crytic
    version: ~> 7

Écrivons notre premier test

On modifie le spec_helper.cr pour intégré spec-kemal

require "spec"
require "spec-kemal"
require "../src/stupeflix"

puis on rédige le premier test dans stupeflix_spec.cr

require "./spec_helper"

describe "Stupeflix" do
  it "renders /" do
      get "/"
      response.body.should eq "Hello World!"
    end
end"

Maintenant, je peux lancer mon premier test

KEMAL_ENV=test crystal spec
.

Finished in 363 microseconds
1 examples, 0 failures, 0 errors, 0 pending

Ma première analyse ameba

./bin/ameba
Inspecting 4 files.

....

Finished in 1.24 milliseconds

4 inspected, 0 failures.

Et mon premier mutation testing

KEMAL_ENV=test ./bin/crytic test
✅ Original test suite passed.
Running 2 mutations.
    ✅ StringLiteralChange
        in ./src/stupeflix.cr:3:5
    ✅ StringLiteralChange
        in ./src/stupeflix.cr:4:3

Finished in 14.41 seconds:
2 mutations, 2 covered, 0 uncovered, 0 errored, 0 timeout. Mutation Score Indicator (MSI): 100.0%

Tout est ok mais on commence à voir les différentes actions que je dois mener pour le dev.

Déclenchement automatique

Maintenant que j'ai mes outils, j'aurais besoin que certaines actions soit automatiques

Pour cela, j'utilise guardian.

la commande est guardian init

La version généré est simpliste mais elle permet de comprendre le format.

files: ./**/*.cr
run: crystal build ./src/stupeflix.cr
---
files: ./shard.yml
run: shards install

Si un evenement se produit sur un fichier qui réponds à l'argument files alors l'argument run est lancé.
Dans le fichier initialisé, dès qu'un fichier .cr est touché, ça lance un build. Dès que le fichier decripteur du projet shard.yml est touché, on lance l'installation des dépendances.

C'est un bonne base. Mais je vais enlever le build qui ne m'intéresse pas pour le moment. Et ajouter nos tests.

Ajout du Linter

Le linter ameba analyse la syntaxe crystal, nous avons besoin qui se déclenche à chaque modification de fichier .cr

On reprends la base du build mais avec la commande ameba

files: ./**/*.cr
run: ./bin/ameba
---
files: ./shard.yml
run: shards install

Argh, celà analyse aussi les fichier .cr qui sont dans les lib.

Je vais faire une correction temporaire en spécifiant les répertoires que je veux analyser. J'avoue, j'anticipe sur la suite.

files: ./spec/**/*.cr
run: ./bin/ameba
---
files: ./src/**/*.cr
run: ./bin/ameba
---
files: ./shard.yml
run: shards install

Je lance guardian et quand je modifie un fichier :

guardian
💂  Guardian is on duty!
± ./spec/stupeflix_spec.cr
└ 1 insertion(+), 1 deletion(-)
$ ./bin/ameba
  Inspecting 4 files.

  ....

  Finished in 13.02 milliseconds

  4 inspected, 0 failures.

Ajout des tests

Même combat pour les tests, j'ai besoin qu'il se lance à chaque modification de fichier crystal.

files: ./spec/**/*.cr
run: ./bin/ameba
---
files: ./src/**/*.cr
run: ./bin/ameba
---
files: ./spec/**/*.cr
run: KEMAL_ENV=test crystal spec
---
files: ./src/**/*.cr
run: KEMAL_ENV=test crystal spec
---
files: ./shard.yml
run: shards install

Ajout des mutations testing

les mutations testing ne vont m'interessé qu'à la modification des fichiers de test.

files: ./spec/**/*.cr
run: ./bin/ameba
---
files: ./src/**/*.cr
run: ./bin/ameba
---
files: ./spec/**/*.cr
run: KEMAL_ENV=test crystal spec
---
files: ./src/**/*.cr
run: KEMAL_ENV=test crystal spec
---
files: ./spec/**/*.cr
run: KEMAL_ENV=test ./bin/crytic test
---
files: ./shard.yml
run: shards install

Testons

Je relance guardian et voilà ce qui ce produit.

💂  Guardian is on duty!
± ./spec/stupeflix_spec.cr
└ 1 insertion(+), 1 deletion(-)
$ ./bin/ameba
  Inspecting 4 files.

  ....

  Finished in 1.73 milliseconds

  4 inspected, 0 failures.

$ KEMAL_ENV=test crystal spec
  .

  Finished in 313 microseconds
  1 examples, 0 failures, 0 errors, 0 pending
$ KEMAL_ENV=test ./bin/crytic test
  ✅ Original test suite passed.
  Running 2 mutations.
      ✅ StringLiteralChange
          in ./src/stupeflix.cr:3:5
      ✅ StringLiteralChange
          in ./src/stupeflix.cr:4:3

  Finished in 14.35 seconds:
  2 mutations, 2 covered, 0 uncovered, 0 errored, 0 timeout. Mutation Score Indicator (MSI): 100.0%

Les trois commandes prévues pour se lancer a la modification d'un fichier de test, se sont exécutées dans l'ordre du fichier guardian.

Les processus de développement

Commençons maintenant à lister les processus dont nous avons besoin.

En l'état actuel, j'ai trois processus qui tourne sur ma machine pour le projet

Le Procfile

Ecrivons notre première version de Procfile avec ces 2 process.

web: crystal src/stupeflix.cr
fsevent: guardian

Le format est assez simple <process type>: <command>
Tant que vous êtes en local, les process type reste le nom que vous voulez donner au processus.
Par contre, si vous comptez deployer sur Heroku ou Pivotal Cloud Foundry certains process type sont reservés comme web, worker, release, ...

Le lanceur.

Le cli d'Heroku peut faire process manager mais quand vous deployer sur Heroku, certaines commandes peuvent preter à confusion sur l'endroit où elles s'executent.

Du coup, je lui préfère overmind. Il a tmux en pré-requis mais rien d'insurmontable.

Pour lancer nos processus, on utilise la commande overmind start

overmind start
system  | Tmux socket name: overmind-stupeflix-7BuOIz8444130fHluw-1C_
system  | Tmux session ID: stupeflix
system  | Listening at ./.overmind.sock
web     | Started with pid 60114...
fsevent | Started with pid 60115...
fsevent | 💂  Guardian is on duty!
web     | [development] Kemal is ready to lead at http://0.0.0.0:3000

A partir de maintenant, la sortie standard de chaque process apparait dans le terminal avec le process type en préfixe.

Pour la suite, ce que l'on va vouloir c'est rebooter le process web si les tests sont OK. Pour ce faire, la commande est overmind restart web dans un nouveau terminal.

La CI locale.

nous avons notre liste de processus locaux et l'execution automatique. Mais je ne peux pas paralleliser les jobs avec guardian ni définir des actions en cas de réussite ou d'echec.

C'est là que va intervenir werk.

Initialisation

Werk fournit une commande d'initialisation werk init qui génère un fichier werk.yml simple mais facilement compréhensible.

---
version: "1"
description: Lorem ipsum dolor sic amet ...
variables: {}
jobs:
  main:
    description: Default job
    variables: {}
    commands:
    - echo "Hello world!"
    needs: []
    can_fail: false
    silent: false

A minima, Werk a besoin d'un job main. Apprenons le reste par la pratique.

Pipeline pour les fichiers src/**/*.cr

Si nous reprenons notre fichier guardian, nous pouvons voir que nous lançons 2 actions pour les fichiers src :

On va y ajouter le redemarrage du serveur avec overmind.

Les tests et le linter peuvent se faire en parallèle. Par contre, je ne rebooterais le serveur que si les tests passent. Commençons à formaliser ça dans werk

D'abords le restart du serveur web.

  web_server:
    description: Restart web server
    commands:
    - overmind restart web

Voyons ce que donne le werk plan web_server

┌─────────────────────────────────────────────┐
│                   Stage 0                   │
├────────────┬────────────────────┬───────────┤
│    NameDescriptionCan fail? │
├────────────┼────────────────────┼───────────┤
│ web_server │ Restart web server │    No     │
└────────────┴────────────────────┴───────────┘

maintenant si je lance werk run web_server, overmind relance bien le serveur.

Ajoutons les tests et mettons les en pré-requis du web_server

  web_server:
    description: Restart web server
    commands:
    - overmind restart web
    needs:
    - spec
  spec:
    description: Launch spec test
    commands:
    - KEMAL_ENV=test crystal spec

Voyons ce que donne le werk plan web_server

┌─────────────────────────────────────────────┐
│                   Stage 0                   │
├────────────┬────────────────────┬───────────┤
│    NameDescriptionCan fail? │
├────────────┼────────────────────┼───────────┤
│    spec    │ Launch spec test   │    No     │
├────────────┴────────────────────┴───────────┤
│                   Stage 1                   │
├────────────┬────────────────────┬───────────┤
│    NameDescriptionCan fail? │
├────────────┼────────────────────┼───────────┤
│ web_server │ Restart web server │    No     │
└────────────┴────────────────────┴───────────┘

Lançons le job werk run web_server et là, gros FAIL. Les tests essayent de lancer un serveur web sur le même port que celui de mon serveur d'exploration. Il faut donc modifier la commande de lancement dans le Procfile en ajoutant l'argument -p 3001

web: crystal src/stupeflix.cr -p 3001
fsevent: guardian

Et si je relance overmind puis werk tout se passe bien. Les tests sont lancé, sont passant et le serveur redémarre.

Ajoutons le linter mais en non bloquant cette fois.

  web_server:
    description: Restart web server
    commands:
    - overmind restart web
    needs:
    - spec
    - linter
  spec:
    description: Launch spec test
    commands:
    - KEMAL_ENV=test crystal spec
  linter:
    description: Linter with ameba
    commands:
    - ./bin/ameba
    can_fail: true

Voyons ce que donne le werk plan web_server

┌─────────────────────────────────────────────┐
│                   Stage 0                   │
├────────────┬────────────────────┬───────────┤
│    NameDescriptionCan fail? │
├────────────┼────────────────────┼───────────┤
│   linter   │ Linter with ameba  │    Yes    │
├────────────┼────────────────────┼───────────┤
│    spec    │ Launch spec test   │    No     │
├────────────┴────────────────────┴───────────┤
│                   Stage 1                   │
├────────────┬────────────────────┬───────────┤
│    NameDescriptionCan fail? │
├────────────┼────────────────────┼───────────┤
│ web_server │ Restart web server │    No     │
└────────────┴────────────────────┴───────────┘

On voit bien que les deux jobs sont placés sur le même stage et donc seront executés en même temps.

On peut même en profiter pour ajouter l'utilisation d'un outil natif crystal sur le format.

  web_server:
    description: Restart web server
    commands:
    - overmind restart web
    needs:
    - spec
    - linter
    - format
  spec:
    description: Launch spec test
    commands:
    - KEMAL_ENV=test crystal spec
  linter:
    description: Linter with ameba
    commands:
    - ./bin/ameba
    can_fail: true
  format:
    description: Native crystal tool format
    commands:
    - crystal tool format --check
    can_fail: true

maintenant, dans guardian je modifie pour ne lancer que werk

files: ./src/**/*.cr
run: werk run web_server

Attaquons nous aux tests.

Pour les tests, nous n'avons pas besoin de relancer le serveur web, par contre, nous allons lancer les tests de mutation. Et ces tests ont besoin que les autres tests soient passant. Et l'on peut y appliquer les linter/format/etc...

Je peux donc factoriser avec ce qui a été fait avant.

  mutation_test:
    description: Launch mutation test after others test
    commands:
    - KEMAL_ENV=test ./bin/crytic test
    needs:
    - spec
    - linter
    - format

On voit ainsi rapidement l'avantage de werk pour l'organisation et la factorisation des actions automatiques. Reste à mettre à jour guardian

files: ./src/**/*.cr
run: werk run web_server
---
files: ./spec/**/*.cr
run: werk run mutation_test
---
files: ./shard.yml
run: shards install

Voici qui clot le premier chapitre.

Pour la suite :

Par rapport à crystal-lang, ajout de la configuration de Visual Studio Code.