risorse | good unit tests
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.
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.
Ho leggermente riorganizzato i file di gut:
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 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 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
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
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_); }doubleDuration elapsedTime() { using namespace std::chrono; returnduration_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(); }doubleDuration 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); }
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); } };
È 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,doubleconst Duration& duration) { ... void endTest( bool failed,doubleconst Duration& /*duration*/) { ... };
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). */
Pagina modificata il 03/03/2016