Blog entries

  • A Salt Configuration for C++ Development

    2014/01/24 by Damien Garaud
    http://www.logilab.org/file/204916/raw/SaltStack-Logo.png

    At Logilab, we've been using Salt for one year to manage our own infrastructure. I wanted to use it to manage a specific configuration: C++ development. When I instantiate a Virtual Machine with a Debian image, I don't want to spend time to install and configure a system which fits my needs as a C++ developer:

    This article is a very simple recipe to get a C++ development environment, ready to use, ready to hack.

    Give Me an Editor and a DVCS

    Quite simple: I use the YAML file format used by Salt to describe what I want. To install these two editors, I just need to write:

    vim-nox:
      pkg.installed
    
    emacs23-nox:
      pkg.installed
    

    For Mercurial, you'll guess:

    mercurial:
     pkg.installed
    

    You can write these lines in the same init.sls file, but you can also decide to split your configuration into different subdirectories: one place for each thing. I decided to create a dev and editor directories at the root of my salt config with two init.sls inside.

    That's all for the editors. Next step: specific C++ development packages.

    Install Several "C++" Packages

    In a cpp folder, I write a file init.sls with this content:

    gcc:
        pkg.installed
    
    g++:
        pkg.installed
    
    gdb:
        pkg.installed
    
    cmake:
        pkg.installed
    
    automake:
        pkg.installed
    
    libtool:
        pkg.installed
    
    pkg-config:
        pkg.installed
    
    colorgcc:
        pkg.installed
    

    The choice of these packages is arbitrary. You add or remove some as you need. There is not a unique right solution. But I want more. I want some LLVM packages. In a cpp/llvm.sls, I write:

    llvm:
     pkg.installed
    
    clang:
        pkg.installed
    
    libclang-dev:
        pkg.installed
    
    {% if not grains['oscodename'] == 'wheezy' %}
    lldb-3.3:
        pkg.installed
    {% endif %}
    

    The last line specifies that you install the lldb package if your Debian release is not the stable one, i.e. jessie/testing or sid in my case. Now, just include this file in the init.sls one:

    # ...
    # at the end of 'cpp/init.sls'
    include:
      - .llvm
    

    Organize your sls files according to your needs. That's all for packages installation. You Salt configuration now looks like this:

    .
    |-- cpp
    |   |-- init.sls
    |   `-- llvm.sls
    |-- dev
    |   `-- init.sls
    |-- edit
    |   `-- init.sls
    `-- top.sls
    

    Launching Salt

    Start your VM and install a masterless Salt on it (e.g. apt-get install salt-minion). For launching Salt locally on your naked VM, you need to copy your configuration (through scp or a DVCS) into /srv/salt/ directory and to write the file top.sls:

    base:
      '*':
        - dev
        - edit
        - cpp
    

    Then just launch:

    > salt-call --local state.highstate
    

    as root.

    And What About Configuration Files?

    You're right. At the beginning of the post, I talked about a "ready to use" Mercurial with some HG extensions. So I use and copy the default /etc/mercurial/hgrc.d/hgext.rc file into the dev directory of my Salt configuration. Then, I edit it to set some extensions such as color, rebase, pager. As I also need Evolve, I have to clone the source code from https://bitbucket.org/marmoute/mutable-history. With Salt, I can tell "clone this repo and copy this file" to specific places.

    So, I add some lines to dev/init.sls.

    https://bitbucket.org/marmoute/mutable-history:
        hg.latest:
          - rev: tip
          - target: /opt/local/mutable-history
          - require:
             - pkg: mercurial
    
    /etc/mercurial/hgrc.d/hgext.rc:
        file.managed:
          - source: salt://dev/hgext.rc
          - user: root
          - group: root
          - mode: 644
    

    The require keyword means "install (if necessary) this target before cloning". The other lines are quite self-explanatory.

    In the end, you have just six files with a few lines. Your configuration now looks like:

    .
    |-- cpp
    |   |-- init.sls
    |   `-- llvm.sls
    |-- dev
    |   |-- hgext.rc
    |   `-- init.sls
    |-- edit
    |   `-- init.sls
    `-- top.sls
    

    You can customize it and share it with your teammates. A step further would be to add some configuration files for your favorite editor. You can also imagine to install extra packages that your library depends on. Quite simply add a subdirectory amazing_lib and write your own init.sls. I know I often need Boost libraries for example. When your Salt configuration has changed, just type: salt-call --local state.highstate.

    As you can see, setting up your environment on a fresh system will take you only a couple commands at the shell before you are ready to compile your C++ library, debug it, fix it and commit your modifications to your repository.


  • Ecriture de liaisons C++ pour Python

    2014/03/13 by Laura Médioni

    Dans le cadre des travaux d'interfaçage de l'application Code_TYMPAN avec du code Python, nous avons réalisé l'étude ci-dessous sur les différents moyens de générer des liaisons Python pour du code C++. Cette étude n'a pas vocation à être exhaustive et s'est concentrée sur les aspects qui nous intéressaient directement pour les travaux susmentionnés.

    Solutions existantes

    Une recherche des solutions existantes a été effectuée, qui a permis d'obtenir la liste suivante pour une écriture manuelle du code d'interfaçage :

    • Cython, un langage de programmation inspiré de Python, basé sur Pyrex
    • Boost.Python, une librairie C++ de la collection Boost permettant d'écrire des liaisons Python
    • PyBindGen, un outil implémenté en Python et permettant de décrire des liaisons C++ directement dans ce langage
    • Swig, un outil permettant de générer des liaisons C++ pour plusieurs langages de programmation
    • Shiboken, un générateur de code d'enrobage pour des bibliothèques C/C++ basé sur du CPython

    Des solutions existent pour automatiser cette écriture. Ce sont des outils qui se basent sur des compilateurs (gcc, clang) pour faire l'analyse grammaticale du code C++ et générer le code d'interfaçage correspondant. Par exemple :

    • XDress, qui permet de générer des fichiers Cython (.pyx, .pxd) à partir de gcc-xml ou de libclang
    • PyBindGen dispose de fonctionnalités permettant de générer des liaisons python à partir de gcc
    • Ce billet explique comment utiliser libclang pour parcourir l'AST d'un code C++ et générer des liaisons Boost.Python

    Aspects pris en compte

    Cet article est intéressant car il aborde de façon très complète les problématiques découlant de l'écriture de liaisons C++ pour des langages de haut niveau. Il a été écrit lors des travaux de développement de Shiboken.

    Dans notre cas, les critères pour le choix d'une solution finale portaient sur différents aspects :

    • Le coût de développement : prise en main de l'outil, quantité de code à écrire pour enrober une classe C++ donnée, coût de l'intégration dans le système de build, degré d'automatisation de la solution, lisibilité du code généré, etc.
    • La gestion de la mémoire : comptage de référence, gestion de la propriété des objets
    • La qualité et l'exhaustivité du support C++ : compatibilité STL, gestion des références et pointeurs, des templates, surcharges d'opérateurs, etc.
    • La pérennité de la solution : technologies mises en œuvre par l'outil, qualité de la documentation, support, taille et degré d'activité de la communauté de développeurs

    Solutions envisagées

    Swig n'a pas été retenu partant de l'a priori que c'était une solution plutôt lourde et davantage orientée C que C++, constat tiré lors de travaux réalisés par Logilab il y a quelques mois de cela. La solution Boost.Python n'a pas été explorée car notre souhait était de nous rapprocher davantage du Python que du C++. Shiboken semble prometteur, bien que peu documenté et mal référencé (les premières recherches tombent sur d'anciennes pages du projet, donnant l'impression que la dernière release date d'il y a plusieurs années, alors qu'en fait, non). Il a été écarté par manque de temps.

    PyBindGen et Cython ont fait l'objet de tests.

    La cible des tests a été l'interfaçage de smart pointers, puisque cela correspond à un de nos besoins sur le projet Code_TYMPAN.

    Les essais ont été réalisés sur des classes simplifiées:

    • MyElement, classe qui représente un élément à encapsuler dans un smart pointer et hérite de IRefCount qui implémente un comptage de référence
    • SmartPtr, classe smart pointer "maison" de l'application
    • Quelques fonctions de test manipulant des smart pointers SmartPtr

    Voici un extrait des en-têtes du code C++:

    #ifndef MY_ELEMENT_H
    #define MY_ELEMENT_H
    #include <iostream>
    using namespace std;
    #include "SmartPtr.h"
    
    class MyElement : public IRefCount
    {
        public:
            MyElement ();
            MyElement (string);
                string Name(){ return _name; }
                virtual ~MyElement ();
    
        protected:
            string _name;
    };
    typedef SmartPtr<MyElement> SPMyElement;
    #endif
    
    #ifndef SMART_PTR_H
    #define SMART_PTR_H
    template <class T> class SmartPtr
    {
        public:
            SmartPtr();
            SmartPtr(T*);
            const T* getRealPointer() const;
    
        protected:
            T* _pObj;
    }
    #endif
    
    SPMyElement BuildElement();
    void UseElement(SPMyElement elt);
    

    Cython

    Cet outil offre maintenant un bon support du C++ (globalement depuis la version 0.17). Son avantage est qu'il permet la manipulation d'objets à la fois C++ et Python dans des fichiers Cython.

    Utilisation
    • Écriture (facultative) d'un fichier .pxd qui contient une recopie des headers à enrober (avec un lien vers les fichiers): déclarations des types, classes, fonctions...
    • Écriture d'un fichier .pyx qui contient des appels de fonctions, constructions d'objets C ou python. Les fonctions et classes de ce module sont utilisables depuis un script Python
    • Compilation du code Cython décrivant les interfaçages C++, génération et compilation du code C++ correspondant et production d'une librairie Python.

    Cython offre un support pour les conteneurs de la STL, les templates, la surcharge de la plupart des opérateurs ("->" non supporté), le passage d'arguments par référence et par pointeur, etc.

    Actuellement en version 0.20.1, la dernière release date du 11 février 2014. Les outils Cython sont relativement bien documentés et sa communauté de développeurs est active.

    Exemple

    Voici le code d'interfaçage Cython correspondant à l'exemple exposé ci-dessus:

    setup.py:

    from distutils.core import setup
    from Cython.Build import cythonize
    
    setup(name='smartptr',
        ext_modules=cythonize('*.pyx',
            ),
    )
    

    smartptr.pxd:

    from libcpp.string cimport string
    
    cdef extern from "src/SmartPtr.h":
        cdef cppclass SmartPtr[T]:
            SmartPtr()
            SmartPtr(T *)
            T *getRealPointer() # Pas de surcharge de ->. L'accès à l'objet ne peut être qu'explicite
    
    cdef extern from "src/MyElement.h":
        cdef cppclass MyElement:
            MyElement()
            MyElement(string)
            string Name()
    
    cdef extern from "src/Test.h":
        SmartPtr[MyElement] BuildSPElement()
        void UseSPElement(SmartPtr[MyElement])
    

    smartptr.pyx:

    # distutils: language = c++
    # distutils: libraries = element
    
    cimport smartptr
    cimport cython
    
    cdef class PySPMyElement:
        cdef SmartPtr[MyElement] thisptr
    
        def __cinit__(self, name=""):
            """ PySPMyElement constructor """
            if name == "":
                self.thisptr = SmartPtr[MyElement](new MyElement())
            else:
                self.thisptr = SmartPtr[MyElement](new MyElement(name))
    
        def get_name(self):
            """ Returns the name of the element """
            return self.thisptr.getRealPointer().Name()
    
    @cython.locals(elt=PySPMyElement)
    def build_sp_elt():
        """ Calls the C++ API to build an element """
        elt = PySPMyElement.__new__(PySPMyElement)
        elt.thisptr = BuildSPElement()
        return elt
    
    @cython.locals(elt=PySPMyElement)
    def use_sp_elt(elt):
        """ Lends elt to the C++ API """
        UseSPElement(elt.thisptr)
    

    XDress

    XDress est un générateur automatique de code d'interfaçage C/C++ écrit en Python, basé sur Cython.

    Utilisation
    • On liste dans un fichier xdressrc.py les classes et fonctions à envelopper (il n'est pas nécessaire de mettre la signature, le nom suffit. On peut choisir d'envelopper seulement certaines classes d'un .h).
    • On exécute xdress qui génère les .pyx et .pxd correspondants

    XDress permet d'envelopper des conteneurs STL via son générateur stlwrap (les conteneurs à enrober doivent être listés dans le xdressrc.py). A titre d'exemple, les vecteurs sont convertis en numpy array du type contenu.

    Ce projet est récent et pas très documenté, mais il semble prometteur.

    PyBindGen

    Utilisation
    • Écriture d'un script Python qui décrit les classes/fonctions C++ à enrober en s'appuyant sur le module PyBindGen (1) → permet de générer un fichier .cpp
    • Compilation du code C++ généré, avec la librairie du programme à envelopper et génération d'une librairie Python.

    Ce processus peut être automatisé:

    • Écriture d'un script Python qui utilise les outils PyBindGen pour lister les modules (headers) à envelopper, les lire et lancer la génération automatique des liaisons c++

    ou:

    • Écriture d'un script Python qui utilise les outils PyBindGen pour lister les modules (headers) à envelopper et générer le script Python décrit en (1) (ce qui permettrait une étape intermédiaire pour personnaliser les liaisons)

    PyBindGen offre un support pour la STL, l'héritage (multiple), la gestion des exceptions C++ côté Python, la surcharge d'opérateurs, le comptage de référence, la gestion de la propriété des objets. Mais il supporte mal les templates.

    Actuellement en version 0.17, la dernière release date du 15 février 2014 (entre autres ajout de la compatibilité Python 3.3).

    Exemple

    PyBindGen, en l'état, n'offre pas la possibilité d'envelopper simplement des templates, ni des smart pointers "maison" par extension.

    Une classe de ce package permet d'envelopper des shared pointers de Boost (boost::shared_ptr). Il serait à priori possible de la modifier légèrement pour enrober les smart pointers de l'application Code_TYMPAN (non testé).

    Voici néanmoins à titre d'exemple le code permettant d'envelopper la classe MyElement et des fonctions manipulant non plus des smart pointers mais des 'MyElement *'

    Test.h :

    MyElement *BuildElement();
    void UseElement(MyElement *elt);
    

    smartptr.py :

    import pybindgen
    import sys
    from pybindgen import retval
    from pybindgen import param
    
    mod = pybindgen.Module('smartptr')
    
    # File includes
    mod.add_include('"src/MyElement.h"')
    mod.add_include('"src/Test.h"')
    
    # Class MyElement
    MyElement = mod.add_class('MyElement')
    MyElement.add_constructor([])
    MyElement.add_method('Name', retval('std::string'), [])
    
    
    # Test functions
    # transfer_ownership=False : here Python program keeps the ownership of the element it passes to the C++ API
    mod.add_function('UseElement', None, [param('MyElement *', 'elt', transfer_ownership=False)])
    # caller_owns_return=True : here Python program will be responsible for destructing the element returned by BuildElement
    mod.add_function('BuildElement', retval('MyElement *',  caller_owns_return=True), [])
    
    if __name__ == '__main__':
        mod.generate(sys.stdout)
    

    Boost.Python

    Les liaisons Python s'écrivent directement en C++.

    C'est un outil très fiable et pérenne, avec de par sa nature un très bon support C++ : gestion de la mémoire, templates, surcharges d'opérateurs, comptage de référence, smart pointers, héritage, etc.

    Inconvénient : la syntaxe (en mode templates C++) n'est pas très intuitive.

    Conclusion

    Les solutions Cython et PyBindGen ont été explorées autour de la problématique d'enrobage de smart pointers. Il en est ressorti que:

    • Il est possible d'enrober facilement des smart pointers Code_TYMPAN en Cython. L'approche qui a été choisie est de manipuler depuis Python les objets C++ directement au travers de smart pointers (les objets Python contenus dans le .pyx encapsulent des objets SmartPtr[T *], agissant donc comme des proxys vers les objets). De cette façon, l'utilisation depuis Python d'un objet C++ incrémente le compteur de référence côté C++ et cela garantit qu'on ne perdra pas la référence à un objet au cours de son utilisation côté Python. Un appel à getRealPointer() pour enrober des fonctions manipulant directement des T * sera toujours possible dans le code Cython au besoin.
    • PyBindGen présente l'intérêt d'offrir des moyens de gérer l'attribution de la propriété des éléments entre C++ et Python (transfer_ownership, caller_owns_return). Malheureusement, il n'offre pas la possibilité d'enrober des smart pointers sans modification de classes PyBindGen, ni d'envelopper des templates.

    Par ailleurs, après utilisation de PyBindGen, il nous a semblé que bien qu'il présente des idées intéressantes, sa documentation, ses tutoriels et son support sont trop succints. Le projet est développé par une seule personne et sa viabilité est difficile à déterminer. Cython en revanche offre un meilleur support et plus de fiabilité.

    Le choix final s'est donc porté sur Cython. Il a été motivé par un souci d'utiliser un outil fiable limitant les coûts de développement (élimination de PyBindGen), aussi proche de Python que possible (élimination de Boost.Python). Cet outil semble fournir un support C++ suffisant par rapport à nos besoins tels que perçus à ce stade du projet.

    De plus si on cherche un moyen de générer automatiquement les liaisons Python, XDress présente l'avantage de permettre l'utilisation de libclang comme alternative à gcc-xml (PyBindGen est basé sur gcc-xml uniquement). Une possibilité serait par ailleurs d'utiliser XDress pour générer uniquement le .pxd et d'écrire le .pyx manuellement.

    Une question qui n'a pas été abordée au cours de cette étude car elle ne correspondait pas à un besoin interne, mais qui est néanmoins intéressante est la suivante: est-il possible de dériver depuis Python des classes de base définies en C++ et enveloppées en Cython, et d'utiliser les objets résultants dans l'application C++ ?


  • Tester MPI avec CMake et un framework de test comme Boost

    2014/06/20 by Damien Garaud

    Objectif

    Compiler et exécuter un fichier de test unitaire avec MPI.

    Je suppose que :

    • une implémentation de MPI est installée (nous prendrons OpenMPI)
    • les bibliothèques Boost MPI et Boost Unit Test Framework sont présentes
    • vous connaissez quelques rudiments de CMake

    CMake

    On utilise le bien connu find_package pour Boost et MPI afin de récupérer tout ce qu'il nous faut pour les headers et les futurs links.

    find_package (MPI REQUIRED)
    find_package (Boost COMPONENTS mpi REQUIRED)
    
    # Boost dirs for headers and libs.
    include_directories (SYSTEM ${Boost_INCLUDE_DIR})
    link_directories (${Boost_LIBRARY_DIRS})
    

    Par la suite, on a essentiellement besoin des variables CMake :

    • Boost_MPI_LIBRARY pour le link avec Boost::MPI
    • MPI_CXX_LIBRARIES pour le link avec la bibliothèque OpenMPI
    • MPIEXEC qui nous donne la commande pour lancer un exécutable via MPI

    On prend un fichier example_mpi.cpp (des exemples simples sont faciles à trouver sur la Toile). Pour le compiler, on fait :

    set(CMAKE_CXX_COMPILER mpicxx)
    
    # MPI example.
    add_executable(example_mpi example_mpi.cpp)
    target_link_libraries(example_mpi ${MPI_CXX_LIBRARIES})
    

    voire juste

    target_link_libraries(example_mpi ${Boost_MPI_LIBRARY})
    

    si on décide d'utiliser la bibliothèque Boost MPI.

    Note

    mpicxx est une commande qui enrobe le compilateur (g++ par exemple). On dit à CMake d'utiliser mpicxx au lieu du compilo par défaut.

    Note

    Boost::MPI n'est pas une implémentation de MPI. C'est une bibliothèque plus haut niveau qui s'abstrait de l'implémentation de MPI. Il faut nécessairement en installer une (OpenMPI, LAM/MPI, MPICH2, ...).

    L'exemple peut très simplement ressembler à :

    #include <boost/mpi/environment.hpp>
    #include <boost/mpi/communicator.hpp>
    #include <iostream>
    
    namespace mpi = boost::mpi;
    
    int main()
    {
      mpi::environment env;
      mpi::communicator world;
      std::cout << "I am process " << world.rank() << " of " << world.size()
                << "." << std::endl;
      return 0;
    }
    

    Exécuter

    Une fois la compilation effectuée, faire simplement :

    > mpiexec -np 4 ./example_mpi
    

    pour lancer l'exécutable sur 4 cœurs.

    Problème : mais pourquoi tu testes ?

    On veut pouvoir faire des exécutables qui soient de vrais tests unitaires et non pas un exemple avec juste une fonction main. De plus, comme j'utilise CMake, je veux pouvoir automatiser le lancement de tous mes exécutables via CTest.

    Problème : il faut bien initialiser et bien dire à MPI que j'en ai fini avec toutes mes MPI-series.

    En "vrai" MPI, on a :

    int main(int argc, char* argv[])
    {
      MPI_Init(&argc, &argv);
    
      int rank;
      MPI_Comm_rank(MPI_COMM_WORLD, &rank);
      // Code here..
      // ... and here
      MPI_Finalize();
    
      return 0;
    }
    

    Note

    C'est ce que fait le constructeur/destructeur de boost::mpi::environment.

    En d'autres termes, je veux me faire ma propre fonction main pour l'initialisation de tous mes cas tests Boost (ou avec n'importe quel autre framework de test unitaire C/C++).

    La documentation de Boost Unit Test, que je trouve parfois très peu claire avec un manque cruel d'exemple, m'a fait galérer quelques heures avant de trouver quelque chose de simple qui fonctionne.

    Conseil : aller regarder les exemples des sources en faisant quelques grep est parfois plus efficace que de trouver la bonne info dans la doc en ligne. On peut aussi en lire sur https://github.com/boostorg/test/tree/master/example

    Deux solutions :

    1. la première que j'ai trouvée dans les tests de Boost::MPI lui-même. Ils utilisent le minimal testing facility. Mais seule la macro BOOST_CHECK est utilisable. Et oubliez les BOOST_CHECK_EQUAL ainsi que l'enregistrement automatique de vos tests dans la suite de tests.
    2. la deuxième redéfinit la fonction main et appelle boost::unit_test::unit_test_main sans définir ni la macro BOOST_TEST_MODULE ni BOOST_TEST_MAIN qui impliquent la génération automatique de la fonction main par le framework de test (que l'on compile en statique ou dynamique). Pour plus de détails, lire http://www.boost.org/doc/libs/release/libs/test/doc/html/utf/user-guide/test-runners.html

    J'utiliserai la deuxième solution.

    Note

    Ne pensez même pas faire une fixture Boost pour y mettre votre boost::mpi::environment puisque cette dernière sera construite/détruite pour chaque cas test (équivalent du setUp/tearDown). Et il est fort probable que vous ayez ce genre d'erreur :

    *** The MPI_Errhandler_set() function was called after MPI_FINALIZE was invoked.
    *** This is disallowed by the MPI standard.
    *** Your MPI job will now abort.
    [hostname:11843] Abort after MPI_FINALIZE completed successfully; not able to guarantee that all other processes were killed!
    

    Un exemple qui marche

    On souhaite ici tester que le nombre de procs passés en argument de mpiexec est au moins 2.

    Le BOOST_TEST_DYN_LINK dit juste que je vais me lier dynamiquement à Boost::Test.

    #define BOOST_TEST_DYN_LINK
    #include <boost/test/unit_test.hpp>
    
    #include <boost/mpi/environment.hpp>
    #include <boost/mpi/communicator.hpp>
    
    namespace mpi = boost::mpi;
    
    BOOST_AUTO_TEST_CASE(test_check_world_size)
    {
        mpi::communicator world;
        BOOST_CHECK(world.size() > 1);
    }
    
    
    // (empty) Initialization function. Can't use testing tools here.
    bool init_function()
    {
        return true;
    }
    
    int main(int argc, char* argv[])
    {
        mpi::environment env(argc, argv);
        return ::boost::unit_test::unit_test_main( &init_function, argc, argv );
    }
    

    On lance tout ça avec un joli mpiexec -np 2 ./test_mpi et on est (presque) content.

    Et un peu de CTest pour finir

    Une dernière chose : on aimerait dire à CMake via CTest de lancer cet exécutable, mais avec mpiexec et les arguments qui vont bien.

    Un cmake --help-command add_test nous indique qu'il est possible de lancer n'importe quel exécutable avec un nombre variable d'arguments. On va donc lui passer : /usr/bin/mpiexec -np NB_PROC ./test_mpi.

    Un œil au module FindMPI.cmake nous dit aussi qu'on peut utiliser d'autres variables CMake MPI pour ce genre de chose.

    Reprenons donc notre fichier CMake et ajoutons :

    add_executable(test_mpi test_mpi.cpp)
    target_link_libraries(test_mpi ${Boost_MPI_LIBRARY})
    # Number of procs for MPI.
    set (PROCS 2)
    # Command to launch by CTest. Need the MPI executable and the number of procs.
    add_test (test_mpi ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${PROCS}
       ${MPIEXEC_PREFLAGS}
       test_mpi
       ${MPIEXEC_POSTFLAGS})
    

    où mon exécutable et mon test porte le même nom : test_mpi. On lance le tout avec la commande ctest dans notre dossier de build et hop, le tour est joué !