risorse | good unit tests

Good Unit Tests /22

Questa parte (la ventiduesima; qui l'elenco delle precedenti) affronta un problema che si presenta quando fallisce la comparazione tra due stringhe molto lunghe:

Test on long strings: FAILED
 example.cpp(79) : [error] s1 == s2 evaluates to "Lorem ipsum dolor sit amet, co
nsectetur adipiscing elit. Aliquam suscipit dui a porttitor maximus." == "Lorem
ipsum dolor sit amet, consectetur adipiscing elit. Aliquam suscipit dui a portit
or maximus."

Non è semplice determinare il punto esatto in cui le due stringhe differiscono. Quando mi capita, copio la parte di messaggio che segue evaluates to in un editor di testo, elimino i caratteri in eccesso e pongo le due stringhe su due righe separate; disattivo l'andata a capo automatica e scorrendo le due stringhe da sinistra a destra individuo finalmente la posizione in cui si trova il primo carattere discorde:

... adipiscing elit. Aliquam suscipit dui a porttitor maximus.
... adipiscing elit. Aliquam suscipit dui a portitor maximus.

Perché non lasciare che sia Gut a farlo per me?

Implementazione

Il procedimento è banale: si cerca la posizione della prima mancata corrispondenza, dopodiché si stampa una sopra l'altra una sottostringa centrata su quella posizione per dare un po' di contesto; infine, nella riga sottostante si riporta un marcatore che evidenzi l'esatta posizione del carattere incriminato:

first difference found at index 84:
iscing elit. Aliquam suscipit dui a porttitor maximus.
iscing elit. Aliquam suscipit dui a portitor maximus.
----------------------------------------^

Poiché voglio poter abilitare la funzionalità alla bisogna, sfrutto un flag statico analogamente a quanto fatto per la colorazione del testo nel terminale e l'attivazione del debugger in caso d'errore:

struct FirstDiffInStrings_ {};
typedef StaticFlag<FirstDiffInStrings_> FirstDiffInStrings;
    
#define GUT_ENABLE_HIGHLIGHTFIRSTDIFFINSTRINGS \
    gut::highlight::FirstDiffInStrings firstDiffInStrings_;

Al momento sono interessato ad applicare questa tecnica esclusivamente nel caso in cui fallisca un confronto diretto tra stringhe; la classe sui cui intervenire è perciò Equal:

template<class T, class U>
struct Equal : public BinaryExpression<T, U> {
    Equal(const T& lhs, const U& rhs) : BinaryExpression<T, U>(lhs, rhs) {}
    virtual bool evaluate() const { return this->lhs_ == this->rhs_; }
    virtual std::string getOpName() const { return "=="; }
    virtual std::string toString() const {
        using gut::toString;
        return BinaryExpression<T, U>::toString()
            + highlight::firstDiffInStrings(
                toRawString(this->lhs_),
                toRawString(this->rhs_));
    }
};

Ho sovrascritto il metodo virtuale toString per accodare al testo standard quello che evidenzia il punto in cui le due stringhe oggetto del confronto differiscono. Notare che le stringhe confrontate non sono ottenute attraverso il metodo toString, che spesso decora la rappresentazione testuale del valore passato (racchiudendo le stringhe tra virgolette, i char tra apici, …). Per questa ragione ho introdotto il nuovo metodo toRawString il cui scopo è quello di convertire un valore nella sua rappresentazione testuale più “pura”:

template<class T>
std::string toRawString(const T&) {
    return "";
}
        
std::string toRawString(const std::string& value) {
    return value;
}
        
std::string toRawString(const char* value) {
    return value ? value : "";
}

Fornendo specifiche implementazioni del metodo toRawString la tecnica di evidenziazione delle differenze può essere applicata ai tipi definiti dall'utente più disparati per cui esista una rappresentazione testuale di qualche natura.

Ecco infine l'implementazione della funzione firstDiffInStrings. Le costanti prefixLength e suffixLength — che a ben guardare potrebbero diventare dei parametri — controllano il numero di caratteri mostrati rispettivamente prima e dopo la posizione della prima differenza:

std::string firstDiffInStrings(
    const std::string& lhs, const std::string& rhs)
{
    if (!highlight::FirstDiffInStrings::enabled())
        return "";
    
    static const int prefixLength = 40;
    static const int suffixLength = 32;

    const auto diff = std::mismatch(
        lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
    if (diff.first == lhs.end() && diff.second == rhs.end())
        return "";
    const int diffPos = static_cast<nt>(
        (diff.first != lhs.end())
            ? diff.first - lhs.begin()
            : diff.second - rhs.begin());
    const auto fragmentStartPos = std::max(0, diffPos - prefixLength);
    const auto markerPos = std::min(diffPos, prefixLength);
    const auto fragmentLength = markerPos + suffixLength;
    
    std::ostringstream os;
    os
        << "\n"
        << "first difference found at index "
        << diffPos
        << ":\n"
        << lhs.substr(fragmentStartPos, fragmentLength)
        << "\n"
        << rhs.substr(fragmentStartPos, fragmentLength)
        << "\n"
        << std::string(markerPos, '-')
        << "^\n";
    return os.str();
}    

Test

L'implementazione alla prova dei fatti:

#include "gut.h"

TEST("Test on long strings")
{
    GUT_ENABLE_HIGHLIGHTFIRSTDIFFINSTRINGS

    std::string s1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit...";
    std::string s2 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit...";

	CHECK(s1 == s2);
}

/*
 * output:
 *
 * Test on long strings: FAILED
 * example.cpp(79) : [error] s1 == s2 evaluates to "Lorem ipsum dolor sit amet, co
 * nsectetur adipiscing elit. Aliquam suscipit dui a porttitor maximus." == "Lorem
 * ipsum dolor sit amet, consectetur adipiscing elit. Aliquam suscipit dui a portit
 * or maximus."
 * first difference found at index 84:
 * iscing elit. Aliquam suscipit dui a porttitor maximus.
 * iscing elit. Aliquam suscipit dui a portitor maximus.
 * ----------------------------------------^
 */

Codice sorgente

Il codice è disponibile su GitHub. Il commit associato è 0dbfc72.

Pagina modificata il 20/11/2020