cours: n°2

master
Raito Bezarius 4 years ago
parent 2dbcc1d3a6
commit c8fafe2ce3

2
.gitignore vendored

@ -0,0 +1,2 @@
.hypothesis
__pycache__

@ -0,0 +1,38 @@
# Nix dans les cours
On a vu au premier cours comment avoir NixOS, il est tout à fait possible de se contenter d'avoir Nix sur votre machine, cela peut être fait de plusieurs façons selon votre distro (y compris sous WSL, mais je déconseille).
## Quelques liens selon les distros
- Arch Linux: <https://wiki.archlinux.org/index.php/Nix>
- Debian: <https://nokomprendo.frama.io/tuto_fonctionnel/posts/tuto_fonctionnel_03/2017-12-23-README.html>
- Ubuntu: <https://doc.ubuntu-fr.org/nix>
- WSL: <https://dev.to/notriddle/installing-nix-under-wsl-2eim>
- Gentoo: <https://trofi.github.io/posts/196-nix-on-gentoo-howto.html>
- macOS (compliqué sur Catalina): <https://www.softinio.com/post/moving-from-homebrew-to-nix-package-manager/>
## Comment utiliser Nix dans ce cours?
On utilisera <https://direnv.net/> que vous pouvez installer avec Nix en faisant `nix-env -iA nixpkgs.direnv` par exemple.
Il faudra ensuite le configurer pour votre shell: <https://direnv.net/docs/hook.html>
Après, chaque dossier d'illustration **devrait** en principe comporter un `.envrc`, ce qui fait que vous devriez avoir une indication de la part de direnv qui vous invite à autoriser l'exécution de celui-ci, ce qui déclenchera automatiquement nix pour évaluer un fichier Nix qui mettra en place l'environnement pour juste le dossier courant, en sortant de celui-ci, tout disparaîtra comme s'il n'avait jamais existé, cela se fait par `direnv allow`.
N.B. : En vrai, non. Tout finit dans votre /nix/store local, faites attention à bien exécuter `nix-collect-garbage -d` pour nettoyer un peu votre /nix/store et pas vous retrouver avec des gigaoctets de choses inutilisés.
Une fois ceci fait, vous devriez pouvoir essayer les exemples du cours en un clic.
## Comment ne pas avoir à utiliser Nix?
Le choix d'utiliser Nix est très simple, tous les étudiants n'ont pas les mêmes environnements, distributions, système et hardware.
Docker est évidemment une mauvaise solution pour un cours fondamental, puisqu'on va se retrouver avec des conteneurs qui pèsent des tonnes, des galères avec ceux qui ont pas les bons systèmes de fichiers (overlay2, etc.), les bons noyaux, etc.
Ici, Nix nous permet de streamliner les dépendances et les environnements de tout le monde, pour ceux qui ne sont pas adeptes de l'administration système. C'est aussi une simplicité pour les auteurs du cours qui n'ont pas à savoir toutes les subtilités de toutes les distros des gens en plus du fait que certains étudiants modifient les choses sans se rendre compte et obtiennent des résultats bizarres parce que leur `~/.bashrc` est bizarre.
Si en dépit de cela, vous refusez Nix (parce que vous utilisez Guix, je peux rien faire pour vous dans ce cas.), c'est OK.
Il vous faudra pour suivre le cours comprendre par vous même comment mettre en place les dépendances, i.e. utiliser pip/Poetry ou alors votre package manager système pour installer les dépendances, les lancer, etc. Vous pouvez bien sûr nous demander, mais on a pas accès à votre machine, donc à chaque demande, incluez un max de logs afin qu'on ne passe pas notre temps à faire des diagnostics dans le vide.
De toute évidence, à nouveau, c'est pour simplifier à fond votre vie et la nôtre, et passer du temps à faire des choses intéressantes, plutôt qu'ennuyeuses.

@ -0,0 +1,24 @@
let nixpkgs = <nixpkgs>;
in
{
pkgs ? import nixpkgs {}
, pythonPackageName ? "python3"
, python ? pkgs.${pythonPackageName}}:
rec {
pythonDependencies = (python.withPackages
(ps: [
ps.pytest
ps.pytest_xdist
ps.pytest-mock
ps.pytestcov
ps.hypothesis
]));
shell = pkgs.mkShell {
buildInputs = with pkgs; [
pythonDependencies
];
};
}

@ -0,0 +1,2 @@
[pytest]
addopts = --doctest-modules

@ -0,0 +1 @@
(import ./default.nix {}).shell

@ -0,0 +1,24 @@
import os
import tempfile
class UnixFS:
@staticmethod
def rm(filename):
os.remove(filename)
# Sans intégration
def test_monkeypatche(mocker):
mocker.patch('os.remove')
UnixFS.rm('contrôle complet')
os.remove.assert_called_once_with('contrôle complet')
# Avec intégration
def test_db_with_db():
with tempfile.NamedTemporaryFile(delete=False) as fp:
fp.write(b'Bonjour!')
fp.seek(0)
assert fp.read() == b'Bonjour!'
UnixFS.rm(fp.name)
# assert que le fichier a été supprimé.

@ -0,0 +1,35 @@
from hypothesis import given
from hypothesis.strategies import text
def encode(input_string):
count = 1
prev = ''
lst = []
for character in input_string:
if character != prev:
if prev:
entry = (prev,count)
lst.append(entry)
#print lst
count = 1
prev = character
else:
count += 1
else:
try:
entry = (character,count)
lst.append(entry)
return (lst, 0)
except Exception as e:
print("Exception encountered {e}".format(e=e))
return (e, 1)
def decode(lst):
q = ""
for character, count in lst:
q += character * count
return q
@given(text())
def test_decode_inverts_encode(s):
assert decode(encode(s)) == s

@ -0,0 +1,32 @@
from hypothesis import given, assume
from hypothesis.strategies import lists, permutations
from collections import Counter, defaultdict
# Ici, on va observer un paradoxe classique en théorie des votes juste en utilisant Hypothesis.
# Imaginez, ici que vous fabriquez un algorithme de couplage, type APB/Admissions Parallèles/Parcoursup.
# On se donne par Hypothesis au moins trois candidats et trois voteurs.
@given(lists(permutations(["A", "B", "C"]), min_size=3))
def test_condorcet_paradox(election):
all_candidates = {"A", "B", "C"}
# On calcule les préférences paires à paires (u, v) de chaque candidat.
counts = Counter()
for vote in election:
for i, vi in enumerate(vote):
for j in range(i + 1, len(vote)):
counts[(vi, vote[j])] += 1
# On regarde quelle paires de candidats a la majorité.
graph = defaultdict(set)
for i in all_candidates:
for j in all_candidates:
if counts[(i, j)] > counts[(j, i)]:
graph[i].add(j)
# Maintenant, on vérifie que les préférences sont transitives, i.e. si A préfère B, B préfère C, alors A préfère C non?
for x in all_candidates:
for y in graph[x]:
for z in graph[y]:
# vérifions que x n'est pas dans graph[z]
assert x not in graph[z]

@ -0,0 +1,15 @@
def est_premier(n):
"""
>>> est_premier(3)
True
>>> est_premier(5)
True
>>> est_premier(4)
True
"""
for k in range(n):
if k % n == 0:
return False
return True

@ -8,15 +8,71 @@ theme: metropolis
## Distinction entre un test unitaire et un test d'intégration
Un test unitaire s'applique bien souvent à une fonction, une classe complètement isolé de ses « services » ^[Une base de donnée, un micro-service, par exemple.] au sens large. Il y a deux façons d'en écrire:
> - Soit le code est dit « couplé » au service et il faut recourir à du monkey patching et des mocks.
> - Soit le code le permet (une bonne architecture & écriture du code suffit en général) et on peut directement tester sans galère.
## Tests d'intégration
Par opposition, un test d'intégration teste un système entier, couplé à ses éventuels services.
Dans ce cas, ces tests sont plus coûteux, plus longs, mais aussi plus intéressants. Il faut à la fois en même temps ramener l'environnement de test dans un environnement proche de celui de la production puis lancer des tests, nettoyer l'environnement et recommencer.
. . .
Ces tests font émerger les soucis d'intégration, e.g. une implémentation "en RAM" d'une base de données ne se comporte pas comme une implémentation "sur le disque", certains paramétrages se comportement bizarrement et peuvent créer de la perte de données et induire des comportements inattendus en production, qu'on ne voit pas en test.
## Méthodologie BDD/TDD
L'idée est de coder vos fonctionnalités en implémentant d'abord des prototypes de votre architecture sans leur implémentation, puis d'écrire une couverture de test naturelle, et de remplir les blancs, tant que les tests ne passent pas, vous implémentez au fur et à mesure.
. . .
Cette méthodologie est intéressante, puisque dans votre processus de conception d'architecture, la partie « concevoir » est intrinsèquement lié à « tester », vous dites: « Je veux que mon système se comporte ainsi dans tel contraintes », dès lors, vous proclamez et pensez à un test dans votre tête, alors au lieu de l'imaginer, vous pouvez l'écrire d'abord, puis résoudre les problèmes.
## Couverture de test
Une fois que vous testez votre code, vous voudriez savoir si toutes les lignes que vous avez dans votre code sont au moins atteintes, que vous n'oubliez pas des cas subtiles qu'il serait intéressant de tester.
. . .
Les couvertures de test remplissent ce rôle, elles vont tracer l'exécution des chemins de code d'un test et reporter ça dans un format utilisable (XML, JUnit, etc.), ensuite vous pourrez les visualiser pour comprendre ce qui se passe.
Vous verrez donc plusieurs projets OSS qui proclament des couvertures à 98, 99, 100 %, i.e. toutes les lignes sont atteintes par un test au moins. Cependant.
## Attention au piège
Avoir 100 % de couverture de test, ça ne veut pas dire que le code est bien testé et qu'il aura zéro bug voire encore moins aucun bug.
. . .
C'est très naïf, on peut tester dans son entireté un code sans jamais **bien le tester**, faire émerger les cas ou les entrées qui amènent des bugs, ou des états indéterminées, surtout dans des tests unitaires où l'état du système est remis à zéro.
. . .
Les tests d'intégration rendent ça moins flagrant, mais les tests ne remplaceront jamais un vrai monitoring et des leçons tirés d'années de production, puisqu'elle n'accumulent pas le même genre d'état et de requêtes.
## Tester, tester quoi ?
Alors, la question qui peut se poser, c'est tester quoi ou comment **bien tester** ? Il y a jamais de bonne réponse ou de vraie réponse.
. . .
On testera souvent ce qui est susceptible de nous mordre ou ce qui nous a déjà mordu dans le passé (empêcher les regressions), on peut ajouter un peu de robustesse à nos tests en tapant dans les méthodes formelles avec des outils comme [Hypothesis (Python)](https://hypothesis.readthedocs.io/en/latest/) ou [Quickcheck (Haskell)](https://hackage.haskell.org/package/QuickCheck).
## Tests exotiques
Bien sûr, on peut encore aller plus loin, mais c'est pas le but de ce cours, on peut avoir des tests:
> - D'orchestration de déploiements dans des VMs, par exemple. Tester qu'on puisse reproduire l'infrastructure sur plusieurs clouds, ou des choses comme ça.
> - Des tests visuels et cross-browser testing^[Notamment, avec Puppeteer ou des implémentations orienté DOM virtuel]: <https://storybook.js.org/docs/testing/automated-visual-testing/>
## Certification ou vérification
Enfin, ce ne sont pas des tests, mais on peut aller plus loin et **certifier** son implémentation ou la vérifier:
> - Cryptographie: <https://verifpal.com/>
> - Système d'exploitation (ou micro-noyaux): <https://sel4.systems/>
Ces choses là assurent des niveaux de robustesse très proche de ce qu'on sait faire mieux et probablement ce qu'on saura faire de mieux pour les 40 prochaines années, certaines entreprises d'ailleurs en font un cœur de métier: <https://www.provenrun.com/products/provencore/>

Binary file not shown.

Binary file not shown.
Loading…
Cancel
Save