risorse | good unit tests

Good Unit Tests /20

Questa parte (la ventesima; qui la prima, qui la seconda, qui la terza, qui la quarta, qui la quinta, qui la sesta, qui la settima, qui l'ottava, qui la nona, qui la decima, qui l'undicesima, qui la dodicesima, qui la tredicesima, qui la quattordicesima, qui la quindicesima, qui la sedicesima, qui la diciasettesima, qui la diciottesima e qui la diciannovesima) introduce il supporto alle initialization list.

Introduzione

Le macro di gut non gestiscono l'inizializzazione con le parentesi graffe:

// file test-gut.cpp
...

struct Point {
  int x_;
  int y_;
  Point(int x, int y) : x_(x), y_(y) {
    if (x < 0 || y < 0)
        throw std::runtime_error("point out of domain");
    }
  bool operator==(const Point& p) const {
    return x_ == p.x_ && y_ == p.y_;
  }
};

std::ostream& operator<<(std::ostream& os, const Point& pt) {
  return os << "(" << pt.x_ << ", " << pt.y_ << ")";
}
...

int main() {
  ...

  // initialization lists
  Point pt(1, 2);

  CHECK(pt == Point(1, 2));
  CHECK(pt == Point{1, 2}); // does not compile!
}

Il messaggio d'errore emesso dal compilatore è il seguente:

test-gut.cpp:621:26: error: macro "CHECK" passed 2 arguments, but takes just 1
   CHECK(pt == Point{1, 2});
                          ^

Ciò è dovuto al fatto che il preprocessore, trovando una virgola al di fuori delle parentesi tonde, la considera come il separatore tra due parametri della macro, anziché come separatore tra gli elementi della lista. L'espressione:

  CHECK(pt == Point{1, 2});

viene così interpretata dal processore:

  CHECK(A, B);

dove:

  A = `pt == Point{1`
  B = `2}`

Racchiudendo l'espressione all'interno della macro tra parentesi il codice compila:

int main() {
  ...

  // initialization lists
  Point pt(1, 2);

  CHECK(pt == Point(1, 2));
  CHECK((pt == Point{1, 2}));
}

L'aggiunta delle parentesi rende però la diagnostica in caso d'errore meno esplicativa, poiché trasforma l'espressione booleana in un valore booleano. La libreria, invece di espandere i due termini a sinistra e a destra dell'uguaglianza, registra semplicemente il valore di verità complessivo:

CHECK(pt == Point(0, 0));   // pt == Point(0, 0) evaluates to (1, 2) == (0, 0)

CHECK((pt == Point{0, 0})); // (pt == Point{0, 0}) evaluates to false

Conviene quindi modificare gut in modo da supportare anche il costrutto in questione.

CHECK, REQUIRE

La modifica per le due asserzioni è immediata, basta infatti trasformarle in macro variadiche:

// file gut.h
...

#define CHECK(expr_...) \
  GUT_BEGIN \
    if (!(gut::Capture()->*expr___VA_ARGS__)) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::CheckFailure( \
          #expr___VA_ARGS__, gut::Expression::last, __FILE__, __LINE__)); \
...

#define REQUIRE(expr_...) \
  GUT_BEGIN \
    if (!(gut::Capture()->*expr___VA_ARGS__)) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::RequireFailure( \
          #expr___VA_ARGS__, gut::Expression::last, __FILE__, __LINE__)); \
      throw gut::AbortTest(); \
    } \
  GUT_END

THROWS_ANYTHING, THROWS_NOTHING

Lo stesso trattamento è applicato alle catture applicate ad una singola espressione, nella fattispecie THROWS_ANYTHING, REQUIRE_THROWS_ANYTHING, THROWS_NOTHING e REQUIRE_THROWS_NOTHING:

...

#define THROWS_ANYTHING(expr_...) \
  GUT_BEGIN \
    try { \
      (void)(expr___VA_ARGS__); \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::NoThrowFailure( \
          #expr___VA_ARGS__, __FILE__, __LINE__)); \
    } catch(...) { \
    } \
  GUT_END

#define REQUIRE_THROWS_ANYTHING(expr_...) \
  GUT_BEGIN \
    bool threw_ = false; \
    try { \
      (void)(expr___VA_ARGS__); \
    } catch(...) { \
      threw_ = true; \
    } \
    if (!threw_) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::FatalNoThrowFailure( \
          #expr___VA_ARGS__, __FILE__, __LINE__)); \
      throw gut::AbortTest(); \
    } \
  GUT_END
...

#define THROWS_NOTHING(expr_...) \
  GUT_BEGIN \
    try { \
      (void)(expr___VA_ARGS__); \
    } catch(const std::exception& e_) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::UnexpectedExceptionFailure( \
          e_, __FILE__, __LINE__)); \
    } catch(...) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::UnknownExceptionFailure( \
          __FILE__, __LINE__)); \
    } \
  GUT_END

#define REQUIRE_THROWS_NOTHING(expr_...) \
  GUT_BEGIN \
    bool threw_ = true; \
    try { \
      (void)(expr___VA_ARGS__); \
      threw_ = false; \
    } catch(const std::exception& e_) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::FatalUnexpectedExceptionFailure( \
          e_, __FILE__, __LINE__)); \
    } catch(...) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::FatalUnknownExceptionFailure( \
          __FILE__, __LINE__)); \
    } \
    if (threw_) \
      throw gut::AbortTest(); \
  GUT_END

THROWS, THROWS_WITH_MESSAGE

Il caso delle catture THROWS e THROWS_WITH_MESSAGE richiede più attenzione, perché queste presentano dei parametri aggiuntivi che vanno trattati in modo specifico:

#define THROWS(expr_, exception_) \
  ...

#define THROWS_WITH_MESSAGE(expr_, exception_, what_) \
  ...

Ora non si può più contare sul fatto che il tipo di eccezione attesa sia il secondo parametro passato alla macro; piuttosto, sarà l'ultimo nel primo caso, il penultimo nel secondo. Le definizioni delle due macro dovrebbero essere così riscritte:

// does not compile!
#define THROWS(..., exception_) \
  ...

// does not compile either!
#define THROWS_WITH_MESSAGE(..., exception_, what_) \
  ...

Il preprocessore vuole tuttavia l'elenco variabile di parametri in coda alla macro, non in testa:

#define THROWS(exception_, ...) \
  ...

#define THROWS_WITH_MESSAGE(exception_, what_, ...) \
  ...

Invertire l'ordine dei parametri delle due macro vorrebbe dire rendere invalido il codice C++ di test già scritto, oltre a renderne meno agevole la lettura. L'idea è quindi quella di invertire l'ordine dei parametri a livello implementativo, preservando l'ordine originale nelle macro utilizzate dall'utente. Servirà quindi riordinare internamente l'elenco dei parametri. Si inizia con l'adattamento delle macro di supporto esistenti:

#define THROWS_(expr_, exception_, prefix_, abort_, ...) \
  GUT_BEGIN \
    bool catched_ = false; \
    try { \
      (void)(expr___VA_ARGS__); \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::prefix_ ## NoThrowFailure( \
          #expr___VA_ARGS__, __FILE__, __LINE__)); \
    } catch(const exception_&) { \
      catched_ = true; \
    } catch(const std::exception& e_) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::prefix_ ## WrongTypedExceptionFailure( \
          #expr___VA_ARGS__, e_, __FILE__, __LINE__)); \
    } catch(...) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::prefix_ ## WrongExceptionFailure( \
          #expr___VA_ARGS__, __FILE__, __LINE__)); \
    } \
    if (!catched_ && abort_) \
      throw gut::AbortTest(); \
  GUT_END

Si tratta ora di riscrivere THROWS in modo che sposti in testa i parametri che seguono l'espressione da valutare; per esempio, supponendo di avere a disposizione una macro GUT_ROTATE che prende l'ultimo parametro e lo porta in prima posizione, potremmo scrivere:

#define THROWS(expr_, exception_...) \
  THROWS_(expr_, exception_ \
    GUT_ROTATE( \
      GUT_ROTATE( \
        GUT_ROTATE(__VA_ARGS__, , false))))

Come vengono riordinati i parametri? Ecco un esempio:

THROWS(std::vector<int>{1, 2, 3}.at(5), std::out_of_range);

I parametri della macro THROWS sono, secondo il preprocessore:

 - std::vector<int>{1
 - 2
 - 3}.at(5)
 - std::out_of_range

Gli stessi sono passati alla macro GUT_ROTATE più interna, assieme ai due introdotti dalla THROWS stessa:

 - std::vector<int>{1
 - 2
 - 3}.at(5)
 - std::out_of_range
 - <NULL>
 - false

L'espansione di GUT_ROTATE porta l'ultimo parametro in testa:

 - false
 - std::vector<int>{1
 - 2
 - 3}.at(5)
 - std::out_of_range
 - <NULL>

L'espansione della seconda GUT_ROTATE ha il medesimo effetto:

 - <NULL>
 - false
 - std::vector<int>{1
 - 2
 - 3}.at(5)
 - std::out_of_range

L'applicazione per la terza e ultima volta di GUT_ROTATE lascia in coda i token dell'espressione originale:

 - std::out_of_range
 - <NULL>
 - false
 - std::vector<int>{1   <---
 - 2                    <--- componenti di expr_, nell'ordine giusto!
 - 3}.at(5)             <---

L'espressione da valutare è quindi catturata interamente dai parametri variadici. Serve ora introdurre un ulteriore livello di indirezione per consentire al processore di espandere la macro __VA_ARGS__:

#define THROWS__(...) \
  THROWS_(__VA_ARGS__)

#define THROWS(...) \
  THROWS_THROWS__( \
    GUT_ROTATE( \
      GUT_ROTATE( \
        GUT_ROTATE(__VA_ARGS__, , false))))

Lo stesso procedimento va applicato alla macro REQUIRE_THROWS:

#define REQUIRE_THROWS(expr_, exception_...) \
  THROWS_(expr_, exception_, Fatal, true)
  THROWS__(GUT_ROTATE(GUT_ROTATE(GUT_ROTATE(__VA_ARGS__, Fatal, true))))

Si opera allo stesso modo sulla macro THROWS_WITH_MESSAGE, con l'accortezza di effettuare una rotazione in più in considerazione del fatto che ha un parametro in più rispetto a THROWS, what_:

#define THROWS_WITH_MESSAGE_(expr_, exception_, what_, prefix_, abort_, ...) \
  GUT_BEGIN \
    bool catched_ = false; \
    try { \
      (void)(expr___VA_ARGS__); \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::prefix_ ## NoThrowFailure( \
          #expr___VA_ARGS__, __FILE__, __LINE__)); \
    } catch(const exception_& e_) { \
      if (strcmp(e_.what(), static_cast<const char*>(what_)) != 0) { \
        GUT_DEBUG_BREAK \
        gut::theListener.failure( \
          gut::prefix_ ## WrongExceptionMessageFailure( \
            #expr___VA_ARGS__, e_.what(), what_, __FILE__, __LINE__)); \
      } else \
        catched_ = true; \
    } catch(const std::exception& e_) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::prefix_ ## WrongTypedExceptionFailure( \
          #expr___VA_ARGS__, e_, __FILE__, __LINE__)); \
    } catch(...) { \
      GUT_DEBUG_BREAK \
      gut::theListener.failure( \
        gut::prefix_ ## WrongExceptionFailure( \
          #expr___VA_ARGS__, __FILE__, __LINE__)); \
    } \
    if (!catched_ && abort_) \
      throw gut::AbortTest(); \
  GUT_END

#define THROWS_WITH_MESSAGE__(...) \
  THROWS_WITH_MESSAGE_(__VA_ARGS__)

#define THROWS_WITH_MESSAGE(expr_, exception_, what_...) \
  THROWS_WITH_MESSAGE_(expr_, exception_, what_, , false)
  THROWS_WITH_MESSAGE__( \
    GUT_ROTATE( \
      GUT_ROTATE( \
        GUT_ROTATE( \
          GUT_ROTATE(__VA_ARGS__, , false)))))

#define REQUIRE_THROWS_WITH_MESSAGE(expr_, exception_, what_...) \
  THROWS_WITH_MESSAGE_(expr_, exception_, what_, Fatal, true)
  THROWS_WITH_MESSAGE__( \
    GUT_ROTATE( \
      GUT_ROTATE( \
        GUT_ROTATE( \
          GUT_ROTATE(__VA_ARGS__, Fatal, true)))))

EVAL

L'ultima macro soggetta al problema qui affrontato è EVAL che, avendo come unico parametro l'espressione da valutare, subisce la stessa trasformazione cui è stata sottoposta CHECK:

#define EVAL(expr_...) \
  GUT_BEGIN \
    gut::theListener.info( \
      gut::Eval(#expr___VA_ARGS__, expr___VA_ARGS__, __FILE__, __LINE__)); \
  GUT_END

GUT_ROTATE

Come effettuare la rotazione dei parametri? Se il numero di parametri fosse noto, il problema si potrebbe risolverebbe definendo una macro per ogni numerosità supportata:

// file rotate.h
...

#define GUT_ROTATE_3(_1, _2, _3)         _3, _1, _2
#define GUT_ROTATE_4(_1, _2, _3, _4)     _4, _1, _2, _3
#define GUT_ROTATE_5(_1, _2, _3, _4, _5) _5, _1, _2, _3, _4
#define GUT_ROTATE_6…
...
#define GUT_ROTATE_98…
#define GUT_ROTATE_99…
#define GUT_ROTATE_100…

Se si accetta questo compromesso (un limite superiore prefissato di parametri gestiti), non è difficile selezionare la macro giusta in funzione del numero di parametri passati. Supponendo di avere a disposizione la macro NARGS (di esempi in rete ce ne sono diversi) che ritorna il numero di parametri passati, la soluzione è a portata di mano, anzi, di macro:

// file rotate.h
...

#define NARGS(...) \
  NARGS_(__VA_ARGS__, 100, 99, 98, …, 3, 2, 1)

#define NARGS_(_100, _99, _98, … _3, _2, _1, N, ...) N


#define GUT_ROTATE(...) \
  GUT_ROTATE_(NARGS(__VA_ARGS__), __VA_ARGS__)

#define GUT_ROTATE_(N, ...) \
  GUT_ROTATE__(N, __VA_ARGS__)

#define GUT_ROTATE__(N, ...) \
  GUT_ROTATE_##N(__VA_ARGS__)

Si noti che anche in questo caso si è reso necessario introdurre un livello di indirezione per permettere al preprocessore di espandere la macro __VA_ARGS__.

Codice sorgente




Aggiornamento [17/02/2018]

Il codice non compila con Visual Studio 2017 (probabilmente nemmeno con le versioni precedenti) a causa della logica secondo la quale il compilatore Microsoft espande la macro __VA_ARGS__ (maggiori dettagli qui). Gli interventi correttivi sono comunque minimi:

[file rotate.h]

#ifndef GUT_ROTATE_H
#define GUT_ROTATE_H

#define GUT_EXPAND(x) x

#define GUT_NARGS(...) \
    GUT_EXPAND( \
        GUT_NARGS_( \
            __VA_ARGS__, \
            100, 99, 98, 97, 96, 95, 94, 93, 92, 91, \
             90, 89, 88, 87, 86, 85, 84, 83, 82, 81, \
             80, 79, 78, 77, 76, 75, 74, 73, 72, 71, \
             70, 69, 68, 67, 66, 65, 64, 63, 62, 61, \
             60, 59, 58, 57, 56, 55, 54, 53, 52, 51, \
             50, 49, 48, 47, 46, 45, 44, 43, 42, 41, \
             40, 39, 38, 37, 36, 35, 34, 33, 32, 31, \
             30, 29, 28, 27, 26, 25, 24, 23, 22, 21, \
             20, 19, 18, 17, 16, 15, 14, 13, 12, 11, \
             10,  9,  8,  7,  6,  5,  4,  3,  2,  1))
...

#define GUT_ROTATE__(N, ...) \
    GUT_EXPAND(GUT_ROTATE_##N(__VA_ARGS__))
[file gut.h]
...

#define THROWS__(...) \
    GUT_EXPAND(THROWS_(__VA_ARGS__))
...

#define THROWS_WITH_MESSAGE__(...) \
    GUT_EXPAND(THROWS_WITH_MESSAGE_(__VA_ARGS__))

Pagina modificata il 22/03/2016