Organisation de projet C ++ (avec gtest, cmake et doxygen)

123

Je suis nouveau dans la programmation en général, j'ai donc décidé de commencer par créer une simple classe vectorielle en C ++. Cependant, j'aimerais avoir de bonnes habitudes dès le départ plutôt que d'essayer de modifier mon flux de travail plus tard.

Je n'ai actuellement que deux fichiers vector3.hppet vector3.cpp. Ce projet commencera lentement à se développer (ce qui en fera beaucoup plus une bibliothèque générale d'algèbre linéaire) au fur et à mesure que je me familiariserai avec tout, donc j'aimerais adopter une disposition de projet "standard" pour me faciliter la vie plus tard. Donc, après avoir regardé autour de moi, j'ai trouvé deux façons d'organiser les fichiers hpp et cpp, la première étant:

project
└── src
    ├── vector3.hpp
    └── vector3.cpp

et le second étant:

project
├── inc
   └── project
       └── vector3.hpp
└── src
    └── vector3.cpp

lequel recommandez vous et pourquoi?

Deuxièmement, je voudrais utiliser le cadre de test Google C ++ pour tester mon code unitaire car il semble assez facile à utiliser. Proposez-vous de le regrouper avec mon code, par exemple dans un dossier inc/gtestou contrib/gtest? S'il est fourni, suggérez-vous d'utiliser le fuse_gtest_files.pyscript pour réduire le nombre de fichiers ou de le laisser tel quel? Si ce n'est pas groupé, comment cette dépendance est-elle gérée?

Lorsqu'il s'agit d'écrire des tests, comment sont-ils généralement organisés? Je pensais avoir un fichier cpp pour chaque classe ( test_vector3.cpppar exemple) mais tous compilés dans un seul binaire afin qu'ils puissent tous être exécutés ensemble facilement?

Puisque la bibliothèque gtest est généralement construite avec cmake et make, je pensais qu'il serait logique que mon projet soit également construit comme ça? Si j'ai décidé d'utiliser la mise en page de projet suivante:

├── CMakeLists.txt
├── contrib
   └── gtest
       ├── gtest-all.cc
       └── gtest.h
├── docs
   └── Doxyfile
├── inc
   └── project
       └── vector3.cpp
├── src
   └── vector3.cpp
└── test
    └── test_vector3.cpp

À quoi devrait CMakeLists.txtressembler le logiciel pour pouvoir soit construire uniquement la bibliothèque, soit la bibliothèque et les tests? J'ai aussi vu pas mal de projets qui ont un buildet un binrépertoire. La construction se produit-elle dans le répertoire de construction, puis les binaires sont-ils déplacés vers le répertoire bin? Les binaires pour les tests et la bibliothèque vivraient-ils au même endroit? Ou serait-il plus logique de le structurer comme suit:

test
├── bin
├── build
└── src
    └── test_vector3.cpp

J'aimerais également utiliser doxygen pour documenter mon code. Est-il possible que cela s'exécute automatiquement avec cmake et make?

Désolé pour tant de questions, mais je n'ai pas trouvé de livre sur C ++ qui réponde de manière satisfaisante à ce type de questions.

rozzy
la source
6
Excellente question, mais je ne pense pas que ce soit une bonne solution pour le format de questions / réponses de Stack Overflow . Je suis cependant très intéressé par une réponse. +1 & fav
Luchian Grigore
1
Ce sont de nombreuses questions sur énormes. Puisse-t-il être préférable de le diviser en plusieurs questions plus petites et de créer des liens les uns avec les autres. Quoi qu'il en soit, pour répondre à la dernière partie: avec CMake, vous pouvez choisir de construire à l'intérieur et à l'extérieur de votre répertoire src (je recommanderais à l'extérieur). Et oui, vous pouvez utiliser doxygen avec CMake automatiquement.
Mistapink

Réponses:

84

Les systèmes de construction C ++ sont un peu un art noir et plus le projet est ancien, plus vous pouvez trouver de choses étranges, il n'est donc pas surprenant que beaucoup de questions se posent. Je vais essayer de parcourir les questions une par une et de mentionner certaines choses générales concernant la construction de bibliothèques C ++.

Séparation des en-têtes et des fichiers cpp dans les répertoires. Ceci n'est essentiel que si vous créez un composant censé être utilisé comme bibliothèque plutôt que comme application réelle. Vos en-têtes sont la base pour les utilisateurs d'interagir avec ce que vous proposez et doivent être installés. Cela signifie qu'ils doivent être dans un sous-répertoire (personne ne veut que beaucoup d'en-têtes se retrouvent au niveau supérieur /usr/include/) et que vos en-têtes doivent pouvoir s'inclure dans une telle configuration.

└── prj
    ├── include
       └── prj
           ├── header2.h
           └── header.h
    └── src
        └── x.cpp

fonctionne bien, car les chemins d'inclusion fonctionnent et vous pouvez utiliser un globbing facile pour les cibles d'installation.

Regroupement des dépendances: je pense que cela dépend en grande partie de la capacité du système de construction à localiser et configurer les dépendances et à quel point votre code dépend d'une seule version. Cela dépend également de la capacité de vos utilisateurs et de la facilité d'installation de la dépendance sur leur plate-forme. CMake est livré avec unfind_package script pour Google Test. Cela rend les choses beaucoup plus faciles. J'irais avec le groupage uniquement lorsque cela est nécessaire et je l'éviterais autrement.

Comment construire: évitez les builds in-source. CMake facilite la création de sources et rend la vie beaucoup plus facile.

Je suppose que vous souhaitez également utiliser CTest pour exécuter des tests pour votre système (il est également livré avec un support intégré pour GTest). Une décision importante pour la mise en page des répertoires et l'organisation des tests sera la suivante: vous retrouvez-vous avec des sous-projets? Si tel est le cas, vous avez besoin d'un peu plus de travail lors de la configuration de CMakeLists et devez diviser vos sous-projets en sous-répertoires, chacun avec ses propresincludesrc fichiers et . Peut-être même leurs propres exécutions et sorties doxygen (combiner plusieurs projets doxygen est possible, mais pas facile ni joli).

Vous vous retrouverez avec quelque chose comme ceci:

└── prj
    ├── CMakeLists.txt <-- (1)
    ├── include
       └── prj
           ├── header2.hpp
           └── header.hpp
    ├── src
       ├── CMakeLists.txt <-- (2)
       └── x.cpp
    └── test
        ├── CMakeLists.txt <-- (3)
        ├── data
           └── testdata.yyy
        └── testcase.cpp

  • (1) configure les dépendances, les spécificités de la plateforme et les chemins de sortie
  • (2) configure la bibliothèque que vous allez construire
  • (3) configure les exécutables de test et les cas de test

Au cas où vous auriez des sous-composants, je suggérerais d'ajouter une autre hiérarchie et d'utiliser l'arborescence ci-dessus pour chaque sous-projet. Ensuite, les choses se compliquent, car vous devez décider si les sous-composants recherchent et configurent leurs dépendances ou si vous le faites au niveau supérieur. Cela devrait être décidé au cas par cas.

Doxygen: Après avoir réussi à passer par la danse de configuration de doxygen, il est trivial d'utiliser CMake add_custom_commandpour ajouter une cible doc.

C'est ainsi que mes projets se terminent et j'ai vu des projets très similaires, mais bien sûr, ce n'est pas un remède.

Addendum À un moment donné, vous voudrez générer un config.hpp fichier qui contient une définition de version et peut-être une définition pour un identifiant de contrôle de version (un hachage Git ou un numéro de révision SVN). CMake a des modules pour automatiser la recherche de ces informations et pour générer des fichiers. Vous pouvez utiliser CMake configure_filepour remplacer des variables dans un fichier modèle par des variables définies dans le CMakeLists.txt.

Si vous construisez des bibliothèques, vous aurez également besoin d'une définition d'exportation pour faire la différence entre les compilateurs, par exemple __declspecsur MSVC et les visibilityattributs sur GCC / clang.

pmr
la source
2
Bonne réponse, mais je ne comprends toujours pas pourquoi vous devez mettre vos fichiers d'en-tête dans un sous-répertoire supplémentaire nommé projet: "/prj/include/prj/foo.hpp", ce qui me semble redondant. Pourquoi pas simplement "/prj/include/foo.hpp"? Je suppose que vous aurez l'occasion de re-jig les répertoires d'installation au moment de l'installation, donc vous obtenez <INSTALL_DIR> /include/prj/foo.hpp lorsque vous installez, ou est-ce difficile sous CMake?
William Payne
6
@William C'est en fait difficile à faire avec CPack. De plus, à quoi ressembleraient vos inclusions dans les fichiers source? S'ils ne sont que "header.hpp" sur une version installée, "/ usr / include / prj /" doit être dans le chemin d'inclusion et non seulement "/ usr / include".
pmr
37

Pour commencer, il existe des noms conventionnels de répertoires que vous ne pouvez pas ignorer, ils sont basés sur la longue tradition du système de fichiers Unix. Ceux-ci sont:

trunk
├── bin     : for all executables (applications)
├── lib     : for all other binaries (static and shared libraries (.so or .dll))
├── include : for all header files
├── src     : for source files
└── doc     : for documentation

C'est probablement une bonne idée de s'en tenir à cette mise en page de base, au moins au niveau supérieur.

À propos du fractionnement des fichiers d'en-tête et des fichiers source (cpp), les deux schémas sont assez courants. Cependant, j'ai tendance à préférer les garder ensemble, il est simplement plus pratique dans les tâches quotidiennes de rassembler les fichiers. De plus, lorsque tout le code est dans un dossier de niveau supérieur, c'est-à-dire le trunk/src/dossier, vous pouvez remarquer que tous les autres dossiers (bin, lib, include, doc, et peut-être un dossier de test) au niveau supérieur, en plus de le répertoire "build" pour une build out-of-source, sont tous les dossiers qui ne contiennent rien de plus que des fichiers qui sont générés dans le processus de build. Et ainsi, seul le dossier src doit être sauvegardé, ou mieux, conservé sous un système / serveur de contrôle de version (comme Git ou SVN).

Et quand il s'agit d'installer vos fichiers d'en-tête sur le système de destination (si vous voulez éventuellement distribuer votre bibliothèque), eh bien, CMake a une commande pour installer les fichiers (crée implicitement une cible "install", pour faire "make install") qui vous pouvez utiliser pour mettre tous les en-têtes dans le /usr/include/répertoire. J'utilise juste la macro cmake suivante à cet effet:

# custom macro to register some headers as target for installation:
#  setup_headers("/path/to/header/something.h" "/relative/install/path")
macro(setup_headers HEADER_FILES HEADER_PATH)
  foreach(CURRENT_HEADER_FILE ${HEADER_FILES})
    install(FILES "${SRCROOT}${CURRENT_HEADER_FILE}" DESTINATION "${INCLUDEROOT}${HEADER_PATH}")
  endforeach(CURRENT_HEADER_FILE)
endmacro(setup_headers)

Où se SRCROOTtrouve une variable cmake que j'ai définie dans le dossier src, et INCLUDEROOTest la variable cmake que je configure à l'endroit où les en-têtes doivent aller. Bien sûr, il existe de nombreuses autres façons de le faire, et je suis sûr que ma méthode n'est pas la meilleure. Le fait est qu'il n'y a aucune raison de diviser les en-têtes et les sources simplement parce que seuls les en-têtes doivent être installés sur le système cible, car il est très facile, en particulier avec CMake (ou CPack), de choisir et de configurer les en-têtes pour être installés sans avoir à les avoir dans un répertoire séparé. Et c'est ce que j'ai vu dans la plupart des bibliothèques.

Quote: Deuxièmement, je voudrais utiliser le cadre de test Google C ++ pour tester mon code unitaire car il semble assez facile à utiliser. Proposez-vous de regrouper ceci avec mon code, par exemple dans un dossier "inc / gtest" ou "contrib / gtest"? S'il est fourni, suggérez-vous d'utiliser le script fuse_gtest_files.py pour réduire le nombre de fichiers ou de le laisser tel quel? Si ce n'est pas groupé, comment cette dépendance est-elle gérée?

Ne regroupez pas les dépendances avec votre bibliothèque. C'est généralement une idée assez horrible, et je déteste toujours ça quand je suis coincé à essayer de construire une bibliothèque qui a fait ça. Cela devrait être votre dernier recours et méfiez-vous des pièges. Souvent, les gens regroupent les dépendances avec leur bibliothèque soit parce qu'ils ciblent un environnement de développement terrible (par exemple, Windows), soit parce qu'ils ne supportent qu'une ancienne version (obsolète) de la bibliothèque (dépendance) en question. Le principal piège est que votre dépendance groupée peut entrer en conflit avec des versions déjà installées de la même bibliothèque / application (par exemple, vous avez groupé gtest, mais la personne essayant de construire votre bibliothèque a déjà une version plus récente (ou plus ancienne) de gtest déjà installée, alors les deux pourraient entrer en conflit et donner à cette personne un mal de tête très méchant). Alors, comme je l'ai dit, fais-le à tes risques et périls, et je dirais qu'en dernier recours. Demander aux gens d'installer quelques dépendances avant de pouvoir compiler votre bibliothèque est un bien moindre mal que d'essayer de résoudre les conflits entre vos dépendances groupées et les installations existantes.

Quote: Quand il s'agit d'écrire des tests, comment sont-ils généralement organisés? Je pensais avoir un fichier cpp pour chaque classe (test_vector3.cpp par exemple) mais tous compilés en un seul binaire afin qu'ils puissent tous être exécutés ensemble facilement?

Un fichier cpp par classe (ou petit groupe cohésif de classes et de fonctions) est à mon avis plus courant et pratique. Cependant, ne les compilez pas tous dans un seul binaire juste pour "qu'ils puissent tous être exécutés ensemble". C'est une très mauvaise idée. En général, quand il s'agit de codage, vous voulez diviser les choses autant qu'il est raisonnable de le faire. Dans le cas des tests unitaires, vous ne voulez pas qu'un binaire exécute tous les tests, car cela signifie que tout petit changement que vous apportez à quoi que ce soit dans votre bibliothèque est susceptible de provoquer une recompilation presque totale de ce programme de test unitaire , et ce ne sont que des minutes / heures perdues en attente de recompilation. Tenez-vous en à un schéma simple: 1 unité = 1 programme de test unitaire. Ensuite,

Quote: Puisque la bibliothèque gtest est généralement construite en utilisant cmake et make, je pensais qu'il serait logique que mon projet soit également construit comme ça? Si j'ai décidé d'utiliser la mise en page de projet suivante:

Je suggérerais plutôt cette mise en page:

trunk
├── bin
├── lib
   └── project
       └── libvector3.so
       └── libvector3.a        products of installation / building
├── docs
   └── Doxyfile
├── include
   └── project
       └── vector3.hpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── src
   └── CMakeLists.txt
   └── Doxyfile.in
   └── project                 part of version-control / source-distribution
       └── CMakeLists.txt
       └── vector3.hpp
       └── vector3.cpp
       └── test
           └── test_vector3.cpp
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

├── build
└── test                        working directories for building / testing
    └── test_vector3

Quelques points à noter ici. Tout d'abord, les sous-répertoires de votre répertoire src doivent refléter les sous-répertoires de votre répertoire d'inclusion, c'est juste pour garder les choses intuitives (aussi, essayez de garder la structure de votre sous-répertoire raisonnablement plate (superficielle), car l'imbrication profonde des dossiers est souvent plus compliqué qu'autre chose). Deuxièmement, le répertoire "include" n'est qu'un répertoire d'installation, son contenu correspond à tous les en-têtes sélectionnés dans le répertoire src.

Troisièmement, le système CMake est destiné à être distribué sur les sous-répertoires source, et non comme un fichier CMakeLists.txt au niveau supérieur. Cela maintient les choses locales, et c'est bien (dans l'esprit de diviser les choses en morceaux indépendants). Si vous ajoutez une nouvelle source, un nouvel en-tête ou un nouveau programme de test, il vous suffit d'éditer un petit et simple fichier CMakeLists.txt dans le sous-répertoire en question, sans rien affecter d'autre. Cela vous permet également de restructurer facilement les répertoires (les CMakeLists sont locales et contenues dans les sous-répertoires déplacés). Les CMakeLists de niveau supérieur doivent contenir la plupart des configurations de niveau supérieur, telles que la configuration des répertoires de destination, des commandes personnalisées (ou macros) et la recherche de packages installés sur le système. Les CMakeLists de niveau inférieur ne doivent contenir que de simples listes d'en-têtes, de sources,

Quote: Comment le CMakeLists.txt devrait-il ressembler pour qu'il puisse soit construire uniquement la bibliothèque, soit la bibliothèque et les tests?

La réponse de base est que CMake vous permet d'exclure spécifiquement certaines cibles de "tous" (ce qui est construit lorsque vous tapez "make"), et vous pouvez également créer des ensembles spécifiques de cibles. Je ne peux pas faire un didacticiel CMake ici, mais il est assez simple de le découvrir par vous-même. Dans ce cas précis, cependant, la solution recommandée est, bien sûr, d'utiliser CTest, qui est juste un ensemble supplémentaire de commandes que vous pouvez utiliser dans les fichiers CMakeLists pour enregistrer un certain nombre de cibles (programmes) qui sont marquées comme unité- des tests. Donc, CMake placera tous les tests dans une catégorie spéciale de builds, et c'est exactement ce que vous avez demandé, donc le problème est résolu.

Quote: J'ai également vu un certain nombre de projets qui ont une création et un répertoire bin. La construction se produit-elle dans le répertoire de construction, puis les binaires sont-ils déplacés vers le répertoire bin? Les binaires pour les tests et la bibliothèque vivraient-ils au même endroit? Ou serait-il plus logique de le structurer comme suit:

Avoir un répertoire de construction en dehors de la source (version "out-of-source") est vraiment la seule chose sensée à faire, c'est la norme de facto de nos jours. Donc, définitivement, ayez un répertoire "build" séparé, en dehors du répertoire source, tout comme le recommandent les gens de CMake, et comme le font tous les programmeurs que j'ai rencontrés. Quant au répertoire bin, eh bien, c'est une convention, et c'est probablement une bonne idée de s'y tenir, comme je l'ai dit au début de cet article.

Quote: Je voudrais également utiliser doxygen pour documenter mon code. Est-il possible que cela s'exécute automatiquement avec cmake et make?

Oui. C'est plus que possible, c'est génial. Selon la fantaisie que vous souhaitez obtenir, il existe plusieurs possibilités. CMake a un module pour Doxygen (c'est-à-dire find_package(Doxygen)) qui vous permet d'enregistrer des cibles qui exécuteront Doxygen sur certains fichiers. Si vous voulez faire des choses plus sophistiquées, comme mettre à jour le numéro de version dans le Doxyfile, ou entrer automatiquement une date / des timbres d'auteur pour les fichiers source et ainsi de suite, tout est possible avec un peu de CMake kung-fu. Généralement, cela impliquera que vous gardiez un Doxyfile source (par exemple, le "Doxyfile.in" que j'ai mis dans la disposition du dossier ci-dessus) qui a des jetons à trouver et à remplacer par les commandes d'analyse de CMake. Dans mon fichier CMakeLists de niveau supérieur , vous trouverez un tel morceau de CMake kung-fu qui fait quelques choses fantaisistes avec cmake-doxygen ensemble.

Mikael Persson
la source
Alors main.cppdevrait aller trunk/bin?
Ugnius Malūkas
17

Structurer le projet

Je préférerais généralement ce qui suit:

├── CMakeLists.txt
|
├── docs/
│   └── Doxyfile
|
├── include/
│   └── project/
│       └── vector3.hpp
|
├── src/
    └── project/
        └── vector3.cpp
        └── test/
            └── test_vector3.cpp

Cela signifie que vous avez un ensemble très clairement défini de fichiers API pour votre bibliothèque, et la structure signifie que les clients de votre bibliothèque feraient l'affaire

#include "project/vector3.hpp"

plutôt que le moins explicite

#include "vector3.hpp"


J'aime que la structure de l'arborescence / src corresponde à celle de l'arborescence / include, mais c'est vraiment une préférence personnelle. Cependant, si votre projet se développe pour contenir des sous-répertoires dans / include / project, il serait généralement utile de faire correspondre ceux à l'intérieur de l'arborescence / src.

Pour les tests, je préfère les garder "proches" des fichiers qu'ils testent, et si vous vous retrouvez avec des sous-répertoires dans / src, c'est un paradigme assez facile pour les autres à suivre s'ils veulent trouver le code de test d'un fichier donné.


Essai

Deuxièmement, j'aimerais utiliser le cadre de test Google C ++ pour tester mon code unitaire car il semble assez facile à utiliser.

Gtest est en effet simple à utiliser et est assez complet en termes de capacités. Il peut être utilisé avec gmock très facilement pour étendre ses capacités, mais mes propres expériences avec gmock ont ​​été moins favorables. Je suis tout à fait prêt à accepter que cela puisse être dû à mes propres défauts, mais les tests gmock ont ​​tendance à être plus difficiles à créer, et beaucoup plus fragiles / difficiles à maintenir. Un gros clou dans le cercueil gmock est qu'il ne joue vraiment pas bien avec des pointeurs intelligents.

C'est une réponse très triviale et subjective à une énorme question (qui n'appartient probablement pas vraiment à SO)

Proposez-vous de regrouper ceci avec mon code, par exemple dans un dossier "inc / gtest" ou "contrib / gtest"? S'il est fourni, suggérez-vous d'utiliser le script fuse_gtest_files.py pour réduire le nombre de fichiers ou de le laisser tel quel? Si ce n'est pas groupé, comment cette dépendance est-elle gérée?

Je préfère utiliser le ExternalProject_Addmodule de CMake . Cela vous évite d'avoir à conserver le code source gtest dans votre référentiel ou à l'installer n'importe où. Il est téléchargé et intégré automatiquement dans votre arborescence de construction.

Voir ma réponse traitant des détails ici .

Lorsqu'il s'agit d'écrire des tests, comment sont-ils généralement organisés? Je pensais avoir un fichier cpp pour chaque classe (test_vector3.cpp par exemple) mais tous compilés en un seul binaire afin qu'ils puissent tous être exécutés ensemble facilement?

Bon plan.


Bâtiment

Je suis un fan de CMake, mais comme pour vos questions liées aux tests, SO n'est probablement pas le meilleur endroit pour demander des opinions sur une question aussi subjective.

À quoi le CMakeLists.txt devrait-il ressembler pour pouvoir soit construire uniquement la bibliothèque, soit la bibliothèque et les tests?

add_library(ProjectLibrary <All library sources and headers>)
add_executable(ProjectTest <All test files>)
target_link_libraries(ProjectTest ProjectLibrary)

La bibliothèque apparaîtra comme une cible "ProjectLibrary" et la suite de tests comme une cible "ProjectTest". En spécifiant la bibliothèque comme dépendance de l'exe de test, la construction de l'exe de test entraînera automatiquement la reconstruction de la bibliothèque si elle est obsolète.

J'ai également vu un certain nombre de projets qui ont une création et un répertoire bin. La construction se produit-elle dans le répertoire de construction, puis les binaires sont-ils déplacés vers le répertoire bin? Les binaires pour les tests et la bibliothèque vivraient-ils au même endroit?

CMake recommande les builds "hors source", c'est-à-dire que vous créez votre propre répertoire de build en dehors du projet et exécutez CMake à partir de là. Cela évite de «polluer» votre arborescence source avec des fichiers de construction, et est hautement souhaitable si vous utilisez un vcs.

Vous pouvez spécifier que les binaires sont déplacés ou copiés dans un répertoire différent une fois construits, ou qu'ils sont créés par défaut dans un autre répertoire, mais ce n'est généralement pas nécessaire. CMake fournit des moyens complets pour installer votre projet si vous le souhaitez, ou permet aux autres projets CMake de "trouver" facilement les fichiers pertinents de votre projet.

En ce qui concerne la prise en charge par CMake de la recherche et de l'exécution des tests gtest , cela serait largement inapproprié si vous construisez gtest dans le cadre de votre projet. Le FindGtestmodule est vraiment conçu pour être utilisé dans le cas où gtest a été construit séparément en dehors de votre projet.

CMake fournit son propre cadre de test (CTest), et idéalement, chaque cas gtest serait ajouté en tant que cas CTest.

Cependant, la GTEST_ADD_TESTSmacro fournie par FindGtestpour permettre l'ajout facile de cas gtest en tant que cas ctest individuels manque quelque peu dans la mesure où elle ne fonctionne pas pour les macros de gtest autres que TESTet TEST_F. VALUE- ou -type paramétrées tests à l' aide TEST_P, TYPED_TEST_Petc. ne sont pas traitées du tout.

Le problème n'a pas de solution facile que je connaisse. Le moyen le plus robuste d'obtenir une liste de cas gtest est d'exécuter l'exe de test avec l'indicateur --gtest_list_tests. Cependant, cela ne peut être fait qu'une fois que l'exe est construit, donc CMake ne peut pas l'utiliser. Ce qui vous laisse avec deux choix; CMake doit essayer d'analyser le code C ++ pour en déduire les noms des tests (non trivial à l'extrême si vous voulez prendre en compte toutes les macros gtest, les tests commentés, les tests désactivés), ou les cas de test sont ajoutés manuellement au Fichier CMakeLists.txt.

J'aimerais également utiliser doxygen pour documenter mon code. Est-il possible que cela s'exécute automatiquement avec cmake et make?

Oui - même si je n'ai aucune expérience sur ce front. CMake fournit FindDoxygenà cet effet.

Fraser
la source
6

En plus des autres (excellentes) réponses, je vais décrire une structure que j'utilise pour des projets de relativement grande envergure .
Je ne vais pas aborder la sous-question concernant Doxygen, car je voudrais simplement répéter ce qui est dit dans les autres réponses.


Raisonnement

Pour la modularité et la maintenabilité, le projet est organisé comme un ensemble de petites unités. Pour plus de clarté, appelons-les UnitX, avec X = A, B, C, ... (mais ils peuvent avoir n'importe quel nom général). La structure des répertoires est alors organisée pour refléter ce choix, avec la possibilité de regrouper les unités si nécessaire.

Solution

La structure de base du répertoire est la suivante (le contenu des unités est détaillé plus loin):

project
├── CMakeLists.txt
├── UnitA
├── UnitB
├── GroupA
   └── CMakeLists.txt
   └── GroupB
       └── CMakeLists.txt
       └── UnitC
       └── UnitD
   └── UnitE

project/CMakeLists.txt pourrait contenir les éléments suivants:

cmake_minimum_required(VERSION 3.0.2)
project(project)
enable_testing() # This will be necessary for testing (details below)

add_subdirectory(UnitA)
add_subdirectory(UnitB)
add_subdirectory(GroupA)

et project/GroupA/CMakeLists.txt:

add_subdirectory(GroupB)
add_subdirectory(UnitE)

et project/GroupB/CMakeLists.txt:

add_subdirectory(UnitC)
add_subdirectory(UnitD)

Passons maintenant à la structure des différentes unités (prenons, par exemple, UnitD)

project/GroupA/GroupB/UnitD
├── README.md
├── CMakeLists.txt
├── lib
   └── CMakeLists.txt
   └── UnitD
       └── ClassA.h
       └── ClassA.cpp
       └── ClassB.h
       └── ClassB.cpp
├── test
   └── CMakeLists.txt
   └── ClassATest.cpp
   └── ClassBTest.cpp
   └── [main.cpp]

Aux différents composants:

  • J'aime avoir source ( .cpp) et headers ( .h) dans le même dossier. Cela évite une hiérarchie de répertoires en double, facilite la maintenance. Pour l'installation, ce n'est pas un problème (surtout avec CMake) de simplement filtrer les fichiers d'en-tête.
  • Le rôle du répertoire UnitDest d'autoriser ultérieurement l'inclusion de fichiers avec #include <UnitD/ClassA.h>. De plus, lors de l'installation de cet appareil, vous pouvez simplement copier la structure des répertoires telle quelle. Notez que vous pouvez également organiser vos fichiers source dans des sous-répertoires.
  • J'aime un READMEfichier pour résumer le sujet de l'unité et spécifier des informations utiles à son sujet.
  • CMakeLists.txt pourrait simplement contenir:

    add_subdirectory(lib)
    add_subdirectory(test)
  • lib/CMakeLists.txt:

    project(UnitD)
    
    set(headers
        UnitD/ClassA.h
        UnitD/ClassB.h
        )
    
    set(sources
        UnitD/ClassA.cpp
        UnitD/ClassB.cpp    
        )
    
    add_library(${TARGET_NAME} STATIC ${headers} ${sources})
    
    # INSTALL_INTERFACE: folder to which you will install a directory UnitD containing the headers
    target_include_directories(UnitD
                               PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                               PUBLIC $<INSTALL_INTERFACE:include/SomeDir>
                               )
    
    target_link_libraries(UnitD
                          PUBLIC UnitA
                          PRIVATE UnitC
                          )

    Ici, notez qu'il n'est pas nécessaire d'indiquer à CMake que nous voulons les répertoires d'inclusion pour UnitAet UnitC, car cela a déjà été spécifié lors de la configuration de ces unités. De plus, PUBLICindiquera à toutes les cibles qui en dépendent UnitDqu'elles doivent automatiquement inclure la UnitAdépendance, alors qu'elles ne UnitCseront pas requises ( PRIVATE).

  • test/CMakeLists.txt (voir ci-dessous si vous souhaitez utiliser GTest pour cela):

    project(UnitDTests)
    
    add_executable(UnitDTests
                   ClassATest.cpp
                   ClassBTest.cpp
                   [main.cpp]
                   )
    
    target_link_libraries(UnitDTests
                          PUBLIC UnitD
    )
    
    add_test(
            NAME UnitDTests
            COMMAND UnitDTests
    )

Utilisation de GoogleTest

Pour Google Test, le plus simple est si sa source est présente quelque part dans votre répertoire source, mais vous n'avez pas à l'ajouter vous-même. J'ai utilisé ce projet pour le télécharger automatiquement, et j'enveloppe son utilisation dans une fonction pour m'assurer qu'il n'est téléchargé qu'une seule fois, même si nous avons plusieurs cibles de test.

Cette fonction CMake est la suivante:

function(import_gtest)
  include (DownloadProject)
  if (NOT TARGET gmock_main)
    include(DownloadProject)
    download_project(PROJ                googletest
                     GIT_REPOSITORY      https://github.com/google/googletest.git
                     GIT_TAG             release-1.8.0
                     UPDATE_DISCONNECTED 1
                     )
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # Prevent GoogleTest from overriding our compiler/linker options when building with Visual Studio
    add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
  endif()
endfunction()

puis, lorsque je veux l'utiliser dans l'une de mes cibles de test, j'ajouterai les lignes suivantes au CMakeLists.txt(c'est pour l'exemple ci-dessus, test/CMakeLists.txt):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)
oLen
la source
Joli "hack" que vous avez fait avec Gtest et cmake! Utile! :)
Tanasis