logo - Organizzazione del progetto C++(con gtest, cmake e doxygen)




java doxygen (3)

Sono nuovo alla programmazione in generale, quindi ho deciso di iniziare creando una semplice classe vettoriale in C ++. Tuttavia mi piacerebbe entrare in buone abitudini fin dall'inizio piuttosto che cercare di modificare il mio flusso di lavoro in seguito.

Al momento ho solo due file vector3.hpp e vector3.cpp . Questo progetto inizierà lentamente a crescere (rendendolo molto più di una biblioteca algebrica lineare generale) man mano che divento più familiare con tutto, quindi mi piacerebbe adottare un layout di progetto "standard" per semplificare la vita in seguito. Quindi dopo aver guardato in giro ho trovato due modi per organizzare i file hpp e cpp, il primo è:

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

e il secondo è:

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

Quale consiglieresti e perché?

In secondo luogo vorrei utilizzare il Google C ++ Testing Framework per l'unità testare il mio codice in quanto sembra abbastanza facile da usare. inc/gtest raggruppare questo con il mio codice, ad esempio in una cartella inc/gtest o contrib/gtest ? Se in bundle, suggerisci di utilizzare lo script fuse_gtest_files.py per ridurre il numero oi file o lasciarlo così com'è? Se non raggruppato come viene gestita questa dipendenza?

Quando si tratta di test di scrittura, come sono generalmente organizzati? Stavo pensando di avere un file cpp per ogni classe ( test_vector3.cpp per esempio) ma tutto è stato compilato in un unico binario in modo che possano essere tutti eseguiti insieme facilmente?

Dato che la libreria gtest è generalmente costruita usando cmake e make, stavo pensando che avrebbe senso anche per il mio progetto essere costruito in questo modo? Se ho deciso di utilizzare il seguente layout di progetto:

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

In che modo CMakeLists.txt deve apparire in modo che possa creare solo la libreria o la libreria e i test? Inoltre ho visto parecchi progetti con una directory build e una bin . La compilazione avviene nella directory di build e quindi i file binari vengono spostati nella directory bin? I file binari per i test e la libreria sarebbero nello stesso posto? O avrebbe più senso strutturarlo come segue:

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

Vorrei anche usare doxygen per documentare il mio codice. È possibile farlo funzionare automaticamente con cmake e make?

Ci scusiamo per così tante domande, ma non ho trovato un libro su C ++ che risponda in modo soddisfacente a questo tipo di domande.


Strutturare il progetto

Generalmente preferirei quanto segue:

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

Ciò significa che hai un set di file API chiaramente definito per la tua libreria, e la struttura significa che i client della tua libreria farebbero

#include "project/vector3.hpp"

piuttosto che il meno esplicito

#include "vector3.hpp"


Mi piace la struttura dell'albero / src in modo che corrisponda a quella dell'albero / include, ma questa è una preferenza personale. Tuttavia, se il progetto si espande in modo da contenere sottodirectory all'interno di / include / project, in genere è utile far corrispondere quelle all'interno dell'albero / src.

Per i test, preferisco tenerli "vicini" ai file che testano, e se finisci con le sottodirectory all'interno di / src, è un paradigma piuttosto facile che gli altri possano seguire se vogliono trovare il codice di test di un determinato file.

analisi

In secondo luogo vorrei utilizzare il Google C ++ Testing Framework per l'unità testare il mio codice in quanto sembra abbastanza facile da usare.

Gtest è davvero semplice da usare ed è abbastanza completo in termini di capacità. Può essere usato insieme a gmock molto facilmente per estendere le sue capacità, ma le mie esperienze con gmock sono state meno favorevoli. Sono abbastanza pronto ad accettare che questo potrebbe essere dovuto alle mie mancanze, ma i test di gmock tendono ad essere più difficili da creare e molto più fragili / difficili da mantenere. Un grosso chiodo nella bara gmock è che non gioca davvero bene con i puntatori intelligenti.

Questa è una risposta molto banale e soggettiva ad una domanda enorme (che probabilmente non appartiene veramente a SO)

Suggerite di raggrupparlo con il mio codice, ad esempio in una cartella "inc / gtest" o "contrib / gtest"? Se in bundle, suggerisci di utilizzare lo script fuse_gtest_files.py per ridurre il numero oi file o lasciarlo così com'è? Se non raggruppato come viene gestita questa dipendenza?

Preferisco usare il modulo ExternalProject_Add di CMake. Questo evita di dover conservare il codice sorgente gtest nel tuo repository o installarlo ovunque. Viene scaricato e integrato automaticamente nel tuo albero delle build.

Vedi la mia risposta trattando le specifiche qui .

Quando si tratta di test di scrittura, come sono generalmente organizzati? Stavo pensando di avere un file cpp per ogni classe (test_vector3.cpp per esempio) ma tutto è stato compilato in un unico binario in modo che possano essere tutti eseguiti insieme facilmente?

Buon piano.

Costruzione

Sono un fan di CMake, ma come per le domande relative ai test, SO non è probabilmente il posto migliore per chiedere opinioni su una questione così soggettiva.

In che modo CMakeLists.txt deve apparire in modo che possa creare solo la libreria o la libreria e i test?

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

La libreria apparirà come target "ProjectLibrary" e la suite di test come target "ProjectTest". Specificando la libreria come dipendenza del test exe, la creazione del test exe causerà automaticamente la ricostruzione della libreria se non è aggiornata.

Inoltre ho visto parecchi progetti che hanno una directory build ad a bin. La compilazione avviene nella directory di build e quindi i file binari vengono spostati nella directory bin? I file binari per i test e la libreria sarebbero nello stesso posto?

CMake raccomanda build "out-of-source", ovvero crea la tua directory di build al di fuori del progetto ed esegui CMake da lì. Questo evita di "inquinare" il tuo albero dei sorgenti con i file di build, ed è altamente auspicabile se stai usando un vcs.

È possibile specificare che i binari vengano spostati o copiati in una directory diversa una volta creati o che vengano creati di default in un'altra directory, ma in genere non è necessario. CMake fornisce metodi completi per installare il tuo progetto, se lo desideri, o per rendere più semplice per altri progetti di CMake "trovare" i file rilevanti del tuo progetto.

Per quanto riguarda il supporto di CMake per la ricerca e l'esecuzione di test gtest , questo sarebbe in gran parte inappropriato se si crea gtest come parte del proprio progetto. Il modulo FindGtest è davvero progettato per essere utilizzato nel caso in cui gtest sia stato costruito separatamente al di fuori del tuo progetto.

CMake fornisce il proprio framework di test (CTest) e, idealmente, ogni caso più sarebbe aggiunto come caso CTest.

Tuttavia, la macro GTEST_ADD_TESTS fornita da FindGtest per consentire l'aggiunta semplice dei casi gtest come casi di ctest individuali è piuttosto carente in quanto non funziona per le macro di gtest diverse da TEST e TEST_F . Value- test Type-parameterised con Value- o Type-parameterised utilizzano TEST_P , TYPED_TEST_P , ecc. Non vengono gestiti affatto.

Il problema non ha una soluzione facile che io conosca. Il modo più efficace per ottenere una lista dei casi più gtest è eseguire il test exe con il flag --gtest_list_tests . Tuttavia, questo può essere fatto solo una volta che l'exe è stato creato, quindi CMake non può farne uso. Che ti lascia con due scelte; CMake deve provare ad analizzare il codice C ++ per dedurre i nomi dei test (non banali all'estremo se si vogliono prendere in considerazione tutte le macro gtest, i test commentati, i test disabilitati) o i casi di test vengono aggiunti manualmente File CMakeLists.txt.

Vorrei anche usare doxygen per documentare il mio codice. È possibile farlo funzionare automaticamente con cmake e make?

Sì, anche se non ho esperienza su questo fronte. CMake fornisce FindDoxygen per questo scopo.


Come antipasto, ci sono alcuni nomi convenzionali per le directory che non puoi ignorare, questi sono basati sulla lunga tradizione con il file system Unix. Questi sono:

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

Probabilmente è una buona idea attenersi a questo layout di base, almeno al livello più alto.

Informazioni sulla suddivisione dei file di intestazione e dei file di origine (cpp), entrambi gli schemi sono abbastanza comuni. Tuttavia, tendo a preferirli tenerli insieme, è solo più pratico nelle attività quotidiane avere i file insieme. Inoltre, quando tutto il codice si trova in una cartella di primo livello, ovvero nella cartella trunk/src/ , puoi notare che tutte le altre cartelle (bin, lib, include, doc e forse qualche cartella di test) al livello più alto , oltre alla directory "build" per una compilazione out-of-source, sono tutte le cartelle che contengono nient'altro che i file che vengono generati nel processo di compilazione. E quindi, solo la cartella src deve essere sottoposta a backup, o molto meglio, mantenuta sotto un sistema / server di controllo versione (come Git o SVN).

E quando si tratta di installare i file di intestazione sul sistema di destinazione (se si desidera eventualmente distribuire la libreria), beh, CMake ha un comando per l'installazione dei file (crea implicitamente un obiettivo "installa", per fare "make install") che puoi usare per mettere tutte le intestazioni nella /usr/include/ . Io uso solo la seguente macro cmake per questo scopo:

# 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)

Dove SRCROOT è una variabile cmake che ho impostato sulla cartella src e INCLUDEROOT è la variabile cmake che devo configurare ovunque sia necessario andare alle intestazioni. Certo, ci sono molti altri modi per farlo, e sono sicuro che la mia strada non è la migliore. Il punto è che non c'è motivo di dividere le intestazioni e le fonti solo perché solo le intestazioni devono essere installate sul sistema di destinazione, perché è molto facile, specialmente con CMake (o CPack), scegliere e configurare le intestazioni per essere installato senza averli in una directory separata. E questo è quello che ho visto nella maggior parte delle biblioteche.

Citazione: in secondo luogo mi piacerebbe utilizzare Google C ++ Testing Framework per unità testare il mio codice in quanto sembra abbastanza facile da usare. Suggerite di raggrupparlo con il mio codice, ad esempio in una cartella "inc / gtest" o "contrib / gtest"? Se in bundle, suggerisci di utilizzare lo script fuse_gtest_files.py per ridurre il numero oi file o lasciarlo così com'è? Se non raggruppato come viene gestita questa dipendenza?

Non raggruppare le dipendenze con la libreria. Questa è generalmente un'idea orribile, e la odio sempre quando sono bloccato a cercare di costruire una libreria che lo faccia. Dovrebbe essere la tua ultima risorsa e stai attento alle insidie. Spesso, le persone raggruppano le dipendenze con la loro libreria perché prendono di mira un terribile ambiente di sviluppo (ad esempio, Windows) o perché supportano solo una versione obsoleta (deprecata) della libreria (dipendenza) in questione. La trappola principale è che la dipendenza in bundle potrebbe essere in conflitto con le versioni già installate della stessa libreria / applicazione (ad esempio, si impacchetta gtest, ma la persona che sta provando a costruire la propria libreria ha già una versione nuova (o precedente) di gtest già installata, quindi i due potrebbero scontrarsi e dare a quella persona un mal di testa molto antipatico). Quindi, come ho detto, lo fai a tuo rischio, e direi solo come ultima risorsa. Chiedere alle persone di installare alcune dipendenze prima di essere in grado di compilare la tua libreria è molto meno male che cercare di risolvere gli scontri tra le dipendenze in bundle e le installazioni esistenti.

Citazione: quando si tratta di test di scrittura, come sono generalmente organizzati? Stavo pensando di avere un file cpp per ogni classe (test_vector3.cpp per esempio) ma tutto è stato compilato in un unico binario in modo che possano essere tutti eseguiti insieme facilmente?

Un file cpp per classe (o un piccolo gruppo coesivo di classi e funzioni) è più usuale e pratico secondo me. Comunque, sicuramente non li compili tutti in un solo binario, in modo che "possano essere tutti gestiti insieme". Questa è una pessima idea. In genere, quando si parla di codifica, si desidera dividere le cose tanto quanto è ragionevole farlo. Nel caso dei test unitari, non si desidera che un solo binario esegua tutti i test, poiché ciò significa che qualsiasi piccola modifica apportata a qualcosa nel proprio catalogo probabilmente causerà una ricompilazione quasi totale di quel programma di test unitario e sono solo pochi minuti / ore persi in attesa di ricompilazione. Basta attenersi a uno schema semplice: 1 unità = 1 programma di test unitario. Quindi, utilizzare uno script o un framework unit test (ad esempio gtest e / o CTest) per eseguire tutti i programmi di test e riportare le percentuali di errore / successo.

Quote: Dal momento che la libreria gtest è generalmente costruita usando cmake e make, stavo pensando che avrebbe senso anche per il mio progetto essere costruito in questo modo? Se ho deciso di utilizzare il seguente layout di progetto:

Vorrei piuttosto suggerire questo layout:

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

Alcune cose da notare qui. Innanzitutto, le sottodirectory della tua directory src dovrebbero rispecchiare le sottodirectory della tua directory di inclusione, questo è solo per mantenere le cose intuitive (inoltre, cerca di mantenere la struttura della sottodirectory abbastanza piatta (superficiale), perché l'annidamento profondo delle cartelle è spesso più di una seccatura che altro). In secondo luogo, la directory "include" è solo una directory di installazione, i suoi contenuti sono solo le intestazioni che vengono selezionate dalla directory src.

Terzo, il sistema CMake è destinato a essere distribuito sulle sottodirectory di origine, non come un file CMakeLists.txt al primo livello. Ciò mantiene le cose locali, e questo è buono (nello spirito di dividere le cose in pezzi indipendenti). Se aggiungi una nuova fonte, una nuova intestazione o un nuovo programma di test, tutto ciò che serve è modificare un file CMakeLists.txt piccolo e semplice nella sottodirectory in questione, senza influire su nient'altro. Ciò consente anche di ristrutturare le directory con facilità (le liste CMake sono locali e contenute nelle sottodirectory che vengono spostate). Le CMakeList di livello superiore dovrebbero contenere la maggior parte delle configurazioni di livello superiore, come l'impostazione di directory di destinazione, comandi personalizzati (o macro) e la ricerca di pacchetti installati sul sistema. Le CMakeLists di livello inferiore devono contenere solo semplici elenchi di intestazioni, origini e origini di test unità e i comandi cmake che li registrano a destinazioni di compilazione.

Citazione: come dovrebbe CMakeLists.txt apparire in modo che possa creare solo la libreria o la libreria e i test?

La risposta di base è che CMake consente di escludere determinati target da "tutti" (che è ciò che viene creato quando si digita "make") e si possono anche creare specifici gruppi di destinazioni. Non posso fare un tutorial su CMake qui, ma è abbastanza semplice scoprirlo da solo. In questo caso specifico, tuttavia, la soluzione consigliata è, ovviamente, utilizzare CTest, che è solo un set aggiuntivo di comandi che è possibile utilizzare nei file CMakeLists per registrare un numero di target (programmi) contrassegnati come unità- test. Quindi, CMake metterà tutti i test in una categoria speciale di build, ed è esattamente quello che hai chiesto, quindi, problema risolto.

Citazione: Inoltre ho visto parecchi progetti che hanno una directory build ad a bin. La compilazione avviene nella directory di build e quindi i file binari vengono spostati nella directory bin? I file binari per i test e la libreria sarebbero nello stesso posto? O avrebbe più senso strutturarlo come segue:

Avere una directory di build al di fuori dell'origine (build "out-of-source") è davvero l'unica cosa sensata da fare, è lo standard di fatto in questi giorni. Quindi, sicuramente, avere una directory "build" separata, al di fuori della directory di origine, proprio come la gente di CMake consiglia, e come ogni programmatore che abbia mai incontrato. Per quanto riguarda la directory bin, beh, questa è una convenzione, ed è probabilmente una buona idea attenersi ad essa, come ho detto all'inizio di questo post.

Citazione: Mi piacerebbe anche usare doxygen per documentare il mio codice. È possibile farlo funzionare automaticamente con cmake e make?

Sì. È più che possibile, è fantastico. A seconda di quanto vuoi ottenere, ci sono diverse possibilità. CMake ha un modulo per Doxygen (cioè, find_package(Doxygen) ) che consente di registrare i target che eseguiranno Doxygen su alcuni file. Se vuoi fare cose più fantasiose, come aggiornare il numero di versione nel Doxyfile, o inserire automaticamente un timbro data / autore per i file sorgente e così via, è tutto possibile con un po 'di kung-fu CMake. In genere, ciò comporta il mantenimento di un Doxyfile di origine (ad esempio, "Doxyfile.in" che ho inserito nel layout della cartella sopra) che ha token da trovare e sostituito dai comandi di parsing di CMake. Nel mio file CMakeList di primo livello , troverai un tale pezzo di CMake kung-fu che fa un paio di cose fantasiose con cmake-doxygen insieme.


Oltre alle altre (eccellenti) risposte, descriverò una struttura che sto usando per progetti relativamente grandi .
Non parlerò della sottoquestione su Doxygen, poiché vorrei solo ripetere ciò che viene detto nelle altre risposte.

Fondamento logico

Per modularità e manutenibilità, il progetto è organizzato come un insieme di piccole unità. Per chiarezza, chiamiamoli UnitX, con X = A, B, C, ... (ma possono avere un nome generico). La struttura della directory viene quindi organizzata per riflettere questa scelta, con la possibilità di raggruppare le unità se necessario.

Soluzione

Il layout di base della directory è il seguente (il contenuto delle unità è dettagliato in seguito):

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

project/CMakeLists.txt potrebbe contenere quanto segue:

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)

e project/GroupA/CMakeLists.txt :

add_subdirectory(GroupB)
add_subdirectory(UnitE)

e project/GroupB/CMakeLists.txt :

add_subdirectory(UnitC)
add_subdirectory(UnitD)

Ora alla struttura delle diverse unità (prendiamo, ad esempio, 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]

Ai diversi componenti:

  • Mi piace avere sorgente ( .cpp ) e intestazioni ( .h ) nella stessa cartella. Ciò evita una gerarchia di directory duplicata, facilita la manutenzione. Per l'installazione, non è un problema (specialmente con CMake) solo filtrare i file di intestazione.
  • Il ruolo della directory UnitD è quello di consentire in seguito di includere i file con #include <UnitD/ClassA.h> . Inoltre, quando si installa questa unità, è sufficiente copiare la struttura della directory così com'è. Nota che puoi anche organizzare i tuoi file sorgente in sottodirectory.
  • Mi piace un file README per riepilogare cosa riguarda l'unità e specificare informazioni utili su di esso.
  • CMakeLists.txt potrebbe semplicemente contenere:

    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
                          )
    

    Qui, si noti che non è necessario dire a CMake che vogliamo le directory di UnitA per UnitA e UnitC , poiché questo era già stato specificato durante la configurazione di quelle unità. Inoltre, PUBLIC dirà a tutti gli obiettivi che dipendono da UnitD che dovrebbero includere automaticamente la dipendenza UnitA , mentre UnitC non sarà richiesto allora ( PRIVATE ).

  • test/CMakeLists.txt (vedi più sotto se vuoi usare GTest per questo):

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

Utilizzando GoogleTest

Per Google Test, il più semplice è se la sua origine è presente in qualche parte della tua directory sorgente, ma non devi aggiungerla da solo. Ho utilizzato questo progetto per scaricarlo automaticamente, e lo avvolgo in una funzione per assicurarmi che venga scaricato solo una volta, anche se abbiamo diversi obiettivi di test.

Questa funzione di CMake è la seguente:

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()

e poi, quando voglio usarlo all'interno di uno dei miei target di test, aggiungerò le seguenti righe a CMakeLists.txt (questo è per l'esempio sopra, test/CMakeLists.txt ):

import_gtest()
target_link_libraries(UnitDTests gtest_main gmock_main)




googletest