Fichiers gargantuesques et duplications de tests

Archéocode #2 - Les coulisses (poussiéreuses) de Zeste de Savoir

Zeste de Savoir est un projet au long cours, avec presque 8 ans depuis la sortie officielle du site, encore plus depuis le début du développement, et plus encore quand on considère que la base de code est issue d’un projet antérieur. Avec le temps et la succession des développeurs, il n’est pas rare de voir apparaître quelques signes d’obsolescence, qu’il est de bon ton de corriger au fur et à mesure pour maintenir le code dans un état le plus optimal possible, sur la durée.

Cette série de billet intitulée Archéocode vise à montrer comment le code de Zeste de Savoir tente de garder une certaine jeunesse, loin des nouvelles fonctionnalités visibles pour les utilisateurs.

Dans ce deuxième billet, je raconte comment nous avons rendu le code plus facilement gérable, en découpant des fichiers de plusieurs milliers de lignes de code, ce qui a amené à la découverte… de plusieurs centaines de lignes de tests unitaires dupliquées !

Quand l'IDE se traîne et qu'on se perd dans le code

Certains fichiers Python dans le code de ZdS ont pris de l’embonpoint au cours du temps, jusqu’à atteindre plusieurs milliers de lignes ! En soi, avoir des fichiers très longs peut être acceptable, mais dans notre cas, ils posaient quelques soucis, du moins pour moi.

D’abord, cela causait un manque de réactivité des outils d’analyse temps-réel de l’IDE. Mon ordinateur n’est pas une machine guerre, sans être anémique. Quand l’IDE a besoin d’une dizaine de secondes pour reconnaître qu’un identifiant est désormais correctement défini dans le fichier, c’est qu’il y a un petit problème.

Plus gênant, il devenait difficile de naviguer dans le code sans utiliser intensivement les outils de l’IDE. Quand on se retrouve face à des dizaines de vues de quelques dizaines de lignes de code dans un fichier, on navigue avec les outils (aller à la définition, recherche des usages, etc.) jusqu’à ne plus savoir où est rangé quoi. Je me suis perdu plus d’une fois.

Un problème voisin était qu’il devenait délicat de trouver des choses dont on ne connaît pas le nom. Par exemple, si on cherche une fonctionnalité particulière et qu’on pense qu’elle se trouve quelque part parmi les 2000 lignes d’un fichier sur la base du nom de ce fichier, il faut potentiellement se résoudre à parcourir le fichier lui-même… Si on ne sait pas quels mots-clés chercher, l’IDE ne peut pas forcément beaucoup nous aider. Ce n’est pas non plus très facile d’abord pour de nouveaux contributeurs au code, qui ne peuvent pas comprendre aisément sa structure.

Trouver où se cachent les géants

Pour trouver les gros fichiers sans se fatiguer à fureter dans le code, cette petite ligne de commande est fort utile :

git ls-files zds | xargs wc -l | awk '$1 > 1000' | sort -gr`

Concrètement, elle liste les fichiers versionnés dans le dossier zds (le dossier avec tous les fichiers Python du backend), compte leurs lignes, demande à awk de filtrer les fichiers trop petits (avec une limite arbitraire à 1000 lignes pour limiter le nombre de résultats) puis utilise grep pour les trier par nombre de lignes décroissant. Cette ligne pourrait probablement être ajustée, mais elle fait le boulot.

On obtient la liste suivante :

  73985 total
   5506 zds/tutorialv2/tests/tests_views/tests_content.py
   2042 zds/tutorialv2/tests/tests_views/tests_published.py
   1897 zds/forum/tests/tests_views.py
   1820 zds/member/tests/tests_views.py
   1542 zds/member/views.py
   1518 zds/tutorialv2/models/database.py
   1441 zds/tutorialv2/models/versioned.py
   1381 zds/tutorialv2/forms.py
   1213 zds/forum/tests/tests.py
   1175 zds/member/api/tests.py
   1152 zds/mp/api/tests.py

On voit qu’il y a un poids lourd avec 5 506 lignes ! Il s’en suit une dizaine de fichiers entre 1 000 et environ 2 000 lignes. On notera que la ligne total parle du total de lignes de code dans tous les fichiers, y compris ceux non affichés. On a donc un peu plus de 70 000 lignes de Python dans notre base de code pour faire tourner le site (sans compter tout Django derrière) !

Beaucoup de ces fichiers sont des fichiers de tests (ce qui ne rend pas leur taille plus pratique à gérer), mais il y a aussi quelques fichiers de code tels que views.py, database.py and versioned.py.

Découpage et nettoyage

Atelier découpage

Découper des fichiers, c’est surtout intéressant si c’est fait de manière intelligente.

Malheureusement, une partie des fichiers est très complexe, notamment database.py et versioned.py, qui gèrent le cœur du site, à savoir les publications, j’ai préféré ne pas y toucher pour le moment, mais peut-être que cela viendra plus tard. D’autres gros fichiers ont aussi été laissés de côté, mieux vaut faire le boulot progressivement que de risquer de casser quelque chose.

Les fichiers concernés par le découpage ont ainsi été les suivants :

  • zds/member/views.py, qui gère tout ce qui concerne le système de membres (login, logout, oubli de mot de passe, karma, ban et lecture seule, signalement de profils et j’en passe).
  • zds/member/tests/tests_views.py qui contient les tests des fonctionalités du fichier précédent.

De manière assez intuitive, j’ai redécoupé les fonctionnalités par thème, aussi logiquement que possible et en essayant de trouver un compromis pour avoir une bonne unité des fonctionnalités tout en évitant le surdécoupage, nuisible lui aussi. Je ne dis pas que le résultat est parfait, mais c’est certainement mieux qu’avant.

On se retrouve avec un module views structuré ainsi :

  • admin : tout ce qui concerne l’administration, pas très intéressant en soi.
  • emailproviders.py : la gestion des fournisseurs de mails (un outil d’administration essentiellement invisible pour la plupart des utilisateurs).
  • hats.py : pour ce qui concerne les casquettes.
  • login.py : login, logout et associé.
  • moderation.py : karma, bannissement, lecture seule…
  • password_recovery.py : pour gérer les étourdis qui oublient leur mot de passe. :D
  • profile.py : mise à jour des paramètres du profil (pseudo, mail, biographie, …).
  • register.py : l’inscription ou la désinscription.
  • reports.py : pour le signalement de profils.

Cela fait un sacré nombre de fichiers en plus, mais ils ne font chacun que quelques centaines de lignes et sont donc faciles à parcourir, voire même refactoriser à l’avenir.

Les tests sont réorganisés exactement pareil : chaque fichier ci-dessus se retrouve avec son fichier tests_*.py correspondant. Facile !

Atelier nettoyage

Quand j’ai farfouillé dans tests_content.py et tests_published.py et que j’ai décidé de ne pas les découper pour le moment, j’ai fait une découverte remarquable.

Dans tests_content.py, on trouve une classe de tests PublishedContentTests qui se retrouve à l’identique (à l’exception de deux tests ajoutés a posteriori) dans tests_published.py.

Je passe sur comment c’est arrivé parce que ce n’est pas le sujet, mais cela fait la modique somme de 1500 lignes de tests dupliqués qu’on peut retirer !

En plus de s’économiser des prises de têtes lors de l’ajout de tests (les deux classes dupliquées avaient commencé à diverger), on économise 2 min sur les environ 8 minutes nécessaires pour faire tourner cette partie des tests ! Si GitHub n’offrait pas la CI aux projets open source, ça ferait une petite économie financière. Quoi qu’il en soit, ce sont des calculs inutiles évités, et donc un gain environnemental en plus du gain de temps.

Résultats

Grâce à cela, on fait disparaître deux mastodontes (pas les plus gros malheureusement), et on vire 1500 lignes de code inutiles de la base de code.

  72524 total
   3577 zds/tutorialv2/tests/tests_views/tests_content.py
   2085 zds/tutorialv2/tests/tests_views/tests_published.py
   1897 zds/forum/tests/tests_views.py
   1518 zds/tutorialv2/models/database.py
   1441 zds/tutorialv2/models/versioned.py
   1389 zds/tutorialv2/forms.py
   1213 zds/forum/tests/tests.py
   1175 zds/member/api/tests.py
   1152 zds/mp/api/tests.py

La CI prend aussi environ 2 minutes de moins.

Pour les curieux, il est possible de regarder la PR ayant implémenté ces changements.


Voilà, de la poussière en moins sous le tapis ! Quand les fichiers deviennent impossibles à naviguer facilement sans les outils de l’IDE, il y a des questions à se poser. Dans notre cas, cela rend plus facile de naviguer dans le code comme un humain, mais aussi rend service à l’environnement, en économisant quelques minutes de calcul à chaque PR fusionnée !

À noter que ce n’est pas la première fois que de gros fichiers ont scindés. Une PR précédente avait déjà contribué à ce genre de réorganisations.

À bientôt, je l’espère, pour parler d’une autre vieillerie découverte dans le code de Zeste de Savoir et du coup de ménage associé !

Liens utiles

7 commentaires

Un problème voisin était qu’il devenait délicat de trouver des choses dont on ne connaît pas le nom. Par exemple, si on cherche une fonctionnalité particulière et qu’on pense qu’elle se trouve quelque part parmi les 2000 lignes d’un fichier sur la base du nom de ce fichier, il faut potentiellement se résoudre à parcourir le fichier lui-même

J’èspere bien que ton IDE te permet de lister toutes les classes, méthodes, fonctions et que leurs noms/documentation orientent fortement sur le bout de code à utiliser

Un problème voisin était qu’il devenait délicat de trouver des choses dont on ne connaît pas le nom. Par exemple, si on cherche une fonctionnalité particulière et qu’on pense qu’elle se trouve quelque part parmi les 2000 lignes d’un fichier sur la base du nom de ce fichier, il faut potentiellement se résoudre à parcourir le fichier lui-même

J’èspere bien que ton IDE te permet de lister toutes les classes, méthodes, fonctions et que leurs noms/documentation orientent fortement sur le bout de code à utiliser

DonKnacki

Mon IDE permet tout ça, bien sûr, mais ça ne remplace pas une vraie structuration en fichiers pour ségréguer les choses qui n’ont que peu à voir les unes avec les autres.

En tant que maigre contributeur, je suis content que cette division soit faite parce que c’est un enfer de comprendre la répartition du code 😅

Moté

Moi aussi, j’en suis bien content ! D’ailleurs, n’hésite pas si tu as des remarques à faire sur l’organisation du code. Ton point de vue en tant que contributeur moins familier avec la base de code a de la valeur pour jauger l’aspect intuitif (si on peut dire) de la base de code.

+1 -0

J’essayerai d’y penser quand je me repencherai dessus ! Dans tous les cas, il y a un risque que je râle (à ce propos) auprès de @Amaury.

Moté

Pourquoi moi spécifiquement 🤔

Amaury

Parce que je râle toujours auprès de toi :D

Moté

C’est… pas tout faux, certes 🙃

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte