risorse | good unit tests

Good Unit Tests /19

Questa parte (la diciannovesima; 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 e qui la diciottesima) introduce la possibilità di sospendere l'esecuzione di un test case in corrispondenza del primo fallimento all'interno di un debugger, quando questo è presente.

Introduzione

Per passare il controllo al debugger è “sufficiente” impostare programmaticamente un breakpoint nel punto in cui si è verificata la non conformità del test, nella fattispecie prima della chiamata gut::theListener.failure:

// file gut.h
...

#include "colors.h"
#include "debugger.h"
#include "timing.h"
...

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

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

L'uso delle interruzioni è opzionale e sono normalmente disattivate; alla stregua di quanto già fatto per la colorazione del testo in console, l'attivazione avviene per mezzo di una macro, in questo caso denominata GUT_ENABLE_BREAKINDEBUGGER:

// file debugger.h
#ifndef GUT_DEBUGGER_H
#define GUT_DEBUGGER_H

#include "utils.h"

namespace gut {

namespace debugger {

// enable/disable breakpoints
struct BreakInDebugger_ {};
typedef StaticFlag<BreakInDebugger_> BreakInDebugger;

#define GUT_ENABLE_BREAKINDEBUGGER \
  gut::debugger::BreakInDebugger breakInDebugger_;

} // namespace debugger

} // namespace gut

#ifdef _WIN32
#include "windows/debugger.h"
#elif  __linux__
#include "linux/debugger.h"
#endif

#ifdef NDEBUG
#define GUT_DEBUG_BREAK
#else
#define GUT_DEBUG_BREAK \
  if (gut::debugger::BreakInDebugger::enabled() \
    && gut::debugger::isAttached()) \
    GUT_DEBUG_BREAK_;
#endif

#endif // GUT_DEBUGGER_H

La macro GUT_DEBUG_BREAK è disponibile solo in modalità debug, quando cioé la macro NDEBUG non è definita, e il breakpoint viene attivato solo nel caso un debugger stia effettivamente monitorando il programma di test. L'implementazione del breakpoint, GUT_DEBUG_BREAK_, così come quella della funzione di controllo della presenza del debugger, debugger::isAttached, dipende dalla piattaforma; quella per Windows è:

// file windows/debugger.h
#ifndef GUT_WINDOWS_DEBUGGER_H
#define GUT_WINDOWS_DEBUGGER_H

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

namespace gut {

namespace debugger {

bool isAttached() {
  return IsDebuggerPresent() == TRUE;
}

} // namespace debugger

} // namespace dex

#define GUT_DEBUG_BREAK_ \
  __debugbreak()

#endif // GUT_WINDOWS_DEBUGGER_H

Quella per Linux è leggermente più articolata:

// file linux/debugger.h
#ifndef GUT_LINUX_DEBUGGER_H
#define GUT_LINUX_DEBUGGER_H

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

namespace gut {

namespace debugger {

bool isAttached() {
  int status = open("/proc/self/status", O_RDONLY);

  if (status == -1)
    return false;

  char buffer[1024];
  ssize_t num_read = read(status, buffer, sizeof(buffer));

  if (num_read > 0) {
    buffer[num_read] = 0;
    static const char prop[] = "TracerPid:";
    char* tracer = strstr(buffer, prop);

    if (tracer)
      return static_cast<bool>(atoi(tracer + sizeof(prop) - 1));
  }
  return false;
}

} // namespace debugger

} // namespace dex

#define GUT_DEBUG_BREAK_ \
  __asm__ volatile("int $0x03");

#endif // GUT_LINUX_DEBUGGER_H

Esempio

Segue un banale test case d'esempio:

// file example-breakpoint.cpp
#include "gut.h"

GUT_ENABLE_BREAKINDEBUGGER

TEST("break into debugger") {
  int i = 1;
  CHECK(i == 2);
}

Il codice è predisposto per causare l'interruzione all'interno del debugger; l'interruzione tuttavia avverrà se saranno rispettate due condizioni: il codice dovrà essere compilato senza macro NDEBUG, e un debugger dovrà risultare presente al momento dell'errore. La presenza delle informazioni di debug sarà sicuramente preziosa, in un contesto d'uso realistico:

$ g++ -g -std=c++11 example-breakpoint.cpp -o example-breakpoint

Lanciato in assenza di un debugger, il programma termina normalmente:

$ ./example-breakpoint
Test suite started...
break into debugger: FAILED
 example-breakpoint.cpp(7) : [error] i == 2 evaluates to 1 == 2
Ran 1 test(s) in 0s.
FAILED - 1 failure(s) in 1 test(s).

Lanciato da debugger, il programma si blocca in corripondenza del test che fallisce:

$ gdb ./example-breakpoint
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
...
Reading symbols from ./example-breakpoint...done.
(gdb) run
Starting program: /home/dex/projects/dex/ext/gut/example-breakpoint
Test suite started...

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00000000004029ea in test_5 () at example-breakpoint.cpp:7
7	    CHECK(i == 2);
(gdb)

Codice sorgente



Pagina modificata il 22/03/2016