risorse | good unit tests

Good Unit Tests /18

Questa parte (la diciottesima; 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 e qui la diciasettesima) introduce alcune macro per il benchmarking del codice.

Introduzione

Capita a volte di avere la necessità di misurare le prestazioni di una porzione di codice; può altre volte risultare utile assicurarsi che un'attività di refactoring non introduca delle pesanti inefficienze. Ho esteso gut per gestire questi due scenari.

Rifattorizzazioni preliminari

Ho leggermente riorganizzato i file di gut:

Implementazione

La stima del tempo di esecuzione avviene allocando un oggetto Timer immediatamente prima della valutazione dell'espressione di interesse, e determinando il tempo trascorso dalla sua allocazione subito dopo; si può usare successivamente la misura così ottenuta per emettere un warning, generare una failure alla stregua di un CHECK o far fallire il test come accade in una REQUEST:

// file timing.h
...

#define GUT_DURATION(expr_) \
  [&] { \
  gut::Timer t_; \
  (void)(expr_); \
  return t_.elapsedTime(); \
  }()

GUT_DURATION è di fatto un'invocazione in-place di una lambda-expression.

SHOULD_LAST_AT_MOST

SHOULD_LAST_AT_MOST emette un warning nel caso il tempo impiegato per la valutazione dell'espressione specificata si riveli essere maggiore della soglia predefinita:

...

#define SHOULD_LAST_AT_MOST(expr_, limit_) \
  GUT_BEGIN \
  auto duration_ = GUT_DURATION(expr_); \
  if (duration_ > limit_) \
    WARN( \
    std::string(#expr_) \
      + " took " \
      + gut::toString(gut::match_duration(duration_, limit_)) \
      + " (expected less than " \
      + gut::toString(gut::match_duration(limit_, duration_)) \
      + ")"); \
  GUT_END

La funzione match_duration ha lo scopo di uniformare la prima durata alla seconda, in modo di che il messaggio risultante presenti durate (effettiva e massima ammessa) con unità di misura e formato uguali.

LASTS_AT_MOST

LASTS_AT_MOST causa un errore quando la valutazione dell'espressione impiega più tempo del previsto:

...

#define LASTS_AT_MOST(expr_, limit_) \
  GUT_BEGIN \
  auto duration_ = GUT_DURATION(expr_); \
  if (duration_ > limit_) \
    gut::theListener.failure( \
    gut::DurationFailure( \
      #expr_, \
      gut::match_duration(duration_, limit_), \
      gut::match_duration(limit_, duration_), \
      __FILE__, \
      __LINE__)); \
  GUT_END

REQUIRE_LASTS_AT_MOST

La macro REQUIRE_LASTS_AT_MOST causa l'uscita anticipata dal test in caso di durata eccessiva:

...

#define REQUIRE_LASTS_AT_MOST(expr_, limit_) \
  GUT_BEGIN \
  auto duration_ = GUT_DURATION(expr_); \
  if (duration_ > limit_) { \
    gut::theListener.failure( \
    gut::FatalDurationFailure( \
      #expr_, \
      gut::match_duration(duration_, limit_), \
      gut::match_duration(limit_, duration_), \
      __FILE__, \
      __LINE__)); \
    throw gut::AbortTest(); \
  } \
  GUT_END

Rifattorizzazione di Timer

Le nuove macro hanno richiesto un piccolo intervento sulla classe Timer, oltre alla definizione di due nuove notifiche, DurationFailure e FatalDurationFailure rispettivamente. La classe Timer è stata aggiornata per generalizzare il valore di ritorno del metodo elapsedTime:

// file timing-chrono.h
...
class Timer {
  std::chrono::steady_clock::time_point start_;
public:
  Timer() { reset (); }
  void reset() { start_ = std::chrono::steady_clock::now(); }
  Duration elapsedTime() {
  return Duration(std::chrono::steady_clock::now() - start_);
  }
  double Duration elapsedTime() {
  using namespace std::chrono;
  return duration_cast<milliseconds>(
    Duration(steady_clock::now() - start_)).count() / 1000.;
  }
};

La versione custom del timer, quella cioè implementata in assenza del supporto della libreria std::chrono, viene specializzata in funzione della piattaforma, poiché la funzione clock su Linux considera solo il tempo CPU impiegato per la valutazione dell'espressione, non quello effettivamente trascorso. La versione per Windows è ricavata da quella presente nel file gut.h:

// file windows/timing-custom.h

#ifndef GUT_WINDOWS_TIMINGCUSTOM_H
#define GUT_WINDOWS_TIMINGCUSTOM_H

#include <time.h> // clock

namespace gut {

class Timer {
  clock_t start_;
public:
  Timer() { reset(); }
  void reset() { start_ = clock(); }
  double Duration elapsedTime() {
  return Duration(
    static_cast<double>(clock() - start_)
    / CLOCKS_PER_SEC);
  }
};

} // namespace gut

#endif // GUT_WINDOWS_TIMINGCUSTOM_H

Quella per Linux è simile, ma fa uso della funzione times al posto di clock:

// file linux/timing-custom.h

#ifndef GUT_LINUX_TIMINGCUSTOM_H
#define GUT_LINUX_TIMINGCUSTOM_H

#include <sys/times.h> // times
#include <unistd.h>  // _SC_CLK_TCK

namespace gut {

class Timer {
  clock_t start_;
public:
  Timer() { reset(); }
  void reset() {
  struct tms start;
  start_ = times(&start);
  }
  Duration elapsedTime() {
  struct tms end;
  return Duration(
    static_cast<double>(times(&end) - start_)
    / sysconf(_SC_CLK_TCK));
  }
};

} // namespace gut

#endif // GUT_LINUX_TIMINGCUSTOM_H

La classe Duration ha lo scopo di modellare un intervallo di tempo e convertirlo in stringa; esiste nelle due versioni custom/chrono:

// file timing-chrono.h
...

class Duration {
  typedef std::chrono::duration<double> duration_t;
  duration_t seconds_;
public:
  explicit Duration(double seconds) : seconds_(seconds) {}
  template<class Rep, class Period>
  explicit Duration(std::chrono::duration<Rep, Period> duration)
   : seconds_(std::chrono::duration_cast<duration_t>(duration)) {}
  double seconds() const { return seconds_.count(); }
  template<class T>
  bool operator>(const T& duration) const { return seconds_ > duration; }
  bool operator>(int duration) const { return seconds_.count() > duration; }
  bool operator>(double duration) const { return seconds_.count() > duration; }
};

...

std::string toString(const Duration& value) {
  std::ostringstream oss;
  oss << value.seconds() << "s";
  return oss.str();
}

template<class T, class U>
Duration match_duration(const T& source, const U& /*target*/) {
  return Duration(source);
}

Come si può dedurre dal codice, le soglie temporali possono essere specificate come istanze di oggetti di tipo std::chrono::duration oppure come semplici valori scalari, nel qual caso l'unità di misura sottintesa è il secondo. La versione custom è la seguente:

// file timing-custom.h
...

class Duration {
  double seconds_;
public:
  explicit Duration(double seconds) : seconds_(seconds) {}
  double seconds() const { return seconds_; }
  bool operator>(double duration) const { return seconds_ > duration; }
};

std::string toString(const Duration& value) {
  std::ostringstream oss;
  oss << value.seconds() << "s";
  return oss.str();
}

template<class T>
Duration match_duration(const Duration& source, const T& /*target*/) {
  return source;
}

template<class T>
Duration match_duration(const T& source, const Duration& /*target*/) {
  return Duration(source);
}

DurationFailure, FatalDurationFailure

Le due nuove notifiche si occupano di comporre il messaggio da presentare all'utente, demandando alla funzione toString il rendering degli intervalli di tempo:

// file gut.h
...

struct DurationFailure : public Error {
  DurationFailure(
  const char* expression,
  const Duration& measured,
  const Duration& limit,
  const char* file,
  int line) : Error(file, line) {
  content()
    << expression
    << " took "
    << gut::toString(measured)
    << ", expected less than "
    << gut::toString(limit);
  }
};

struct FatalDurationFailure : public Fatal {
  FatalDurationFailure(
  const char* expression,
  const Duration& measured,
  const Duration& limit,
  const char* file,
  int line) : Fatal(file, line) {
  content()
    << expression
    << " took "
    << gut::toString(measured)
    << ", expected less than "
    << gut::toString(limit);
  }
};

Interfaccia DefaultReport

È stata infine adattata l'interfaccia DefaultReport, che riceve la durata del test come oggetto Duration anziché come valore scalare double:

// file gut.h
...

class DefaultReport {
...

void end(
  int tests,
  int failedTests,
  int failures,
  double const Duration& duration) {
...

  void endTest(
    bool failed,
    double const Duration& /*duration*/) {
...

};

Esempio

Segue un esempio d'uso delle nuove macro:

// file example-timing.cpp

#include "gut.h"

#include <thread>

int f(int, int) {
  std::this_thread::sleep_for(std::chrono::milliseconds(250));
  return 0;
}

TEST("duration detection") {
  // define a threshold in s
  auto limit = std::chrono::duration<double>(.1);

  // emits a warning
  SHOULD_LAST_AT_MOST(f(1, 2), limit);

  // emits an error
  LASTS_AT_MOST(f(1, 2), limit);

  // can use threshold defined in other units
  LASTS_AT_MOST(f(1, 2), std::chrono::milliseconds(100));

  // thresholds are in s by default
  LASTS_AT_MOST(f(1, 2),  0);
  LASTS_AT_MOST(f(1, 2),  1);
  LASTS_AT_MOST(f(1, 2), .1);

  // causes the test to end
  REQUIRE_LASTS_AT_MOST(f(1, 2), limit);

  // this check won't be executed
  CHECK(1 == 2);
}

/* output:
 *
 * Test suite started...
 * duration detection: FAILED
 *  example-timing.cpp(15) : [warning] f(1, 2) took 0.250148s (expected less than 0.1s)
 *  example-timing.cpp(18) : [error] f(1, 2) took 0.250879s, expected less than 0.1s
 *  example-timing.cpp(21) : [error] f(1, 2) took 0.250853s, expected less than 0.1s
 *  example-timing.cpp(24) : [error] f(1, 2) took 0.250963s, expected less than 0s
 *  example-timing.cpp(26) : [error] f(1, 2) took 0.250195s, expected less than 0.1s
 *  example-timing.cpp(29) : [fatal] f(1, 2) took 0.250745s, expected less than 0.1s
 * Ran 1 test(s) in 2s.
 * FAILED - 5 failure(s) in 1 test(s).
 */

Codice sorgente



Pagina modificata il 03/03/2016