From 7c00f0d65290226570d945b30e42a7ecc83898b1 Mon Sep 17 00:00:00 2001 From: Daniel Fiser Date: Tue, 23 Oct 2007 22:40:11 +0200 Subject: [PATCH] Added Levenshtein table to compute distances between strings. --- src/Makefile | 6 +- src/diff/levenshtein_distances.cpp | 58 ++++++++++ src/diff/levenshtein_distances.h | 89 +++++++++++++++ src/diff/levenshtein_table.cpp | 214 +++++++++++++++++++++++++++++++++++++ src/diff/levenshtein_table.h | 97 +++++++++++++++++ src/diff/snippet.cpp | 26 ++++- src/diff/snippet.h | 25 ++++- src/diff/text.cpp | 7 ++ src/diff/text.h | 6 ++ tests/Makefile | 4 +- tests/levenshtein.cpp | 150 ++++++++++++++++++++++++++ tests/main.cpp | 2 + 12 files changed, 675 insertions(+), 9 deletions(-) create mode 100644 src/diff/levenshtein_distances.cpp create mode 100644 src/diff/levenshtein_distances.h create mode 100644 src/diff/levenshtein_table.cpp create mode 100644 src/diff/levenshtein_table.h create mode 100644 tests/levenshtein.cpp diff --git a/src/Makefile b/src/Makefile index dbf1a9e..6a5ba24 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,7 +1,8 @@ CC = g++ MOC = /opt/qt4/bin/moc -CXXFLAGS = -Wall -Wno-long-long -pedantic -O2 +CXXFLAGS = -Wall -Wno-long-long -pedantic \ + -march=k8 -mtune=k8 -msse2 -O2 -pipe DEBUGFLAGS = -g QT_INCLUDE = -DQT_SHARED -I/opt/qt4/include -I/opt/qt4/include/QtCore \ -I/opt/qt4/include/QtGui @@ -20,7 +21,8 @@ VPATH = diff qt parser TARGETS = qshowdiff -DIFF_OBJS = snippet.o diff.o file.o hunk.o text.o +DIFF_OBJS = snippet.o diff.o file.o hunk.o text.o \ + levenshtein_table.o levenshtein_distances.o QT_OBJS = diff_view.o main_window.o PARSER_OBJS = parser.o diff --git a/src/diff/levenshtein_distances.cpp b/src/diff/levenshtein_distances.cpp new file mode 100644 index 0000000..d451235 --- /dev/null +++ b/src/diff/levenshtein_distances.cpp @@ -0,0 +1,58 @@ +#include "levenshtein_distances.h" +#include "../debug.h" +using std::vector; +using std::pair; +using std::list; + + +/* LevenshteinDistances */ +LevenshteinDistances::LevenshteinDistances(const Text &original, const Text &modified) +{ + int orig_len = original.numLines(); + int modif_len = modified.numLines(); + int len = std::min(orig_len, modif_len); + int i; + LevenshteinTable *table; + range_t range; + vector ranges; + vector::const_iterator it; + vector::const_iterator it_end; + + _ranges_original.resize(std::max(orig_len, modif_len)); + _ranges_modified.resize(std::max(orig_len, modif_len)); + for (i=0; i < len; i++){ + ranges.clear(); + + table = new LevenshteinTable(original.getLine(i), modified.getLine(i)); + + table->fillRanges(ranges); + it = ranges.begin(); + it_end = ranges.end(); + for (;it != it_end; it++){ + if ((*it).original){ + _ranges_original[i].push_back(*it); + }else{ + _ranges_modified[i].push_back(*it); + } + } + + delete table; + } + for (;i < orig_len; i++){ + range.type = range_t::INSERTION; + range.original = true; + range.from = 0; + range.length = original.getLine(i).size(); + + _ranges_original[i].push_back(range); + } + for (;i < modif_len; i++){ + range.type = range_t::INSERTION; + range.original = false; + range.from = 0; + range.length = modified.getLine(i).size(); + + _ranges_modified[i].push_back(range); + } +} +/* LevenshteinDistances END */ diff --git a/src/diff/levenshtein_distances.h b/src/diff/levenshtein_distances.h new file mode 100644 index 0000000..87237be --- /dev/null +++ b/src/diff/levenshtein_distances.h @@ -0,0 +1,89 @@ +#ifndef _LEVENSHTEIN_DISTANCES_H_ +#define _LEVENSHTEIN_DISTANCES_H_ + +#include +#include +#include + +#include "levenshtein_table.h" +#include "text.h" + + +/** + * Class which describes Levenshtein distances between two texts given in + * constructor. + */ +class LevenshteinDistances{ + private: + /** + * Deletion ranges. + * Each item of list represents one line which is built from list of + * deletion ranges. + */ + std::vector > _ranges_original; + std::vector > _ranges_modified; + + /** + * Iterators pointing to current deletion range which will be returned + * by nextDeletionRangeOriginal/Modified. + */ + std::vector::iterator _current_it_original; + std::vector::iterator _current_it_modified; + + /** + * Which line is currently managed. + */ + int _current_line; + + public: + /** + * Constructor. + * Must build the table, fill _ranges_original/modified + * structures and set default values to _current_it_original/modified + * and _current_line. + */ + LevenshteinDistances(const Text &original, const Text &modified); + + /** + * Number of lines of original and modified text. + */ + int numLines() const { return _ranges_original.size(); } + + /** + * Change current read line to line. + */ + void setCurrentLine(int line) + { if (line >= numLines()) return; + _current_line = line; + _current_it_original = _ranges_original[_current_line].begin(); + _current_it_modified = _ranges_modified[_current_line].begin();} + + /** + * Store into r current range on which is pointing + * internal iterator. + * Returns true, if range was filled. + * Returns false, if range could not be filled because there is no more + * ranges. + */ + bool nextRangeOriginal(range_t &r) + { if (_current_it_original == _ranges_original[_current_line].end()) + return false; + r = *(_current_it_original++); return true; } + bool nextRangeModified(range_t &r) + { if (_current_it_modified == _ranges_modified[_current_line].end()) + return false; + r = *(_current_it_modified++); return true; } + + const list_of_ranges_t &getRangesOriginal() const + { return const_cast(_ranges_original); } + const list_of_ranges_t &getRangesModified() const + { return const_cast(_ranges_modified); } + + /** + * Reset table to initial values. + */ + void reset() { _current_line = 0; + _current_it_original = _ranges_original[0].begin(); + _current_it_modified = _ranges_modified[0].begin(); } +}; +#endif diff --git a/src/diff/levenshtein_table.cpp b/src/diff/levenshtein_table.cpp new file mode 100644 index 0000000..282727f --- /dev/null +++ b/src/diff/levenshtein_table.cpp @@ -0,0 +1,214 @@ +#include "levenshtein_table.h" +#include "../debug.h" +using std::vector; +using std::pair; +using std::list; + +/* LevenshteinTable */ +LevenshteinTable::LevenshteinTable(const QString &orig, const QString &modif) + : _original(orig), _modified(modif) +{ + _allocateMemory(); + _fillTable(); + _findTrace(); +} + +void LevenshteinTable::fillRanges(std::vector &ranges) const +{ + int row = 0; + int col = 0; + pair from(0,0); + range_t::range_type current_type = range_t::NOCHANGE; + list >::const_iterator it = _trace.begin(); + list >::const_iterator it_end = _trace.end(); + + do{ + // substitution / nochange + if ((*it).first-1 == row && (*it).second-1 == col){ + DBG("Substitution"); + if (_table[row][col] != _table[(*it).first][(*it).second]){ + if (current_type != range_t::SUBSTITUTION){ + _finishRange(ranges, current_type, from, *it); + from = (*it); + } + current_type = range_t::SUBSTITUTION; + }else{ + _finishRange(ranges, current_type, from, *it); + current_type = range_t::NOCHANGE; + } + }else + // insertion to modified + if ((*it).first-1 == row){ + DBG("Insertion"); + if (current_type != range_t::INSERTION){ + _finishRange(ranges, current_type, from, *it); + from = *it; + } + current_type = range_t::INSERTION; + }else + // insertion to original / deletion in modified + if ((*it).second-1 == col){ + DBG("Deletion"); + if (current_type != range_t::DELETION){ + _finishRange(ranges, current_type, from, *it); + from = *it; + } + current_type = range_t::DELETION; + } + + row = (*it).first; + col = (*it).second; + + it++; + }while (it != it_end); + + _finishRange(ranges, current_type, from, pair(_table.size(), + _table[0].size())); +} + + +void LevenshteinTable::dumpTable(vector > &dump) const +{ + int rows = _table.size(); + + dump.resize(rows); + for (int i=0; i < rows; i++){ + dump[i] = _table[i]; + } +} +void LevenshteinTable::dumpTrace(list > &dump) const +{ + list >::const_iterator it = _trace.begin(); + list >::const_iterator it_end = _trace.end(); + for (; it != it_end; it++){ + dump.push_back(*it); + } +} + + + +// private: +void LevenshteinTable::_allocateMemory() +{ + int orig_len = _original.size(); + int modif_len = _modified.size(); + + _table.resize(modif_len + 1); + for (int i=0; i < modif_len + 1; i++){ + _table[i].resize(orig_len + 1); + } +} + +void LevenshteinTable::_fillTable() +{ + int rows = _table.size(); + int cols = _table[0].size(); + int cost; + + for (int i=0; i < rows; i++) + _table[i][0] = i; + for (int i=0; i < cols; i++) + _table[0][i] = i; + + for (int i=1; i < rows; i++){ + for (int j=1; j < cols; j++){ + cost = _original[j - 1] == _modified[i - 1] ? 0 : 1; + _table[i][j] = _min(_table[i - 1][j] + 1, + _table[i][j - 1] + 1, + _table[i - 1][j - 1] + cost); + } + } +} + +void LevenshteinTable::_findTrace() +{ + int row = _table.size() - 1; + int column = _table[0].size() - 1; + pair tmp; + + do { + _trace.push_front(pair(row, column)); + _minAncestor(row, column); + }while(row != 0 && column != 0); +} + +void LevenshteinTable::_minAncestor(int &row, int &column) const +{ + if (row == 0 && column == 0){ + return; + }else if (row == 0){ + column = column - 1; + return; + }else if (column == 0){ + row = row - 1; + return; + } + + int up = _table[row - 1][column]; + int left = _table[row][column - 1]; + int askan = _table[row - 1][column - 1]; + int min = _min(up, left, askan); + + if (min == askan){ + row = row - 1; + column = column - 1; + }else if (min == up){ + row = row - 1; + column = column; + }else if (min == left){ + row = row; + column = column - 1; + } +} + +void LevenshteinTable::_finishRange(vector &ranges, + range_t::range_type type, + const pair &from, const pair &to) const +{ + range_t range; + + if (type == range_t::NOCHANGE) + return; + + if (type == range_t::INSERTION){ + range.original = false; + range.type = range_t::INSERTION; + range.from = from.first - 1; + range.length = to.first - 1 - range.from; + + ranges.push_back(range); + }else if (type == range_t::DELETION){ + range.original = true; + range.type = range_t::INSERTION; + range.from = from.second - 1; + range.length = to.second - 1 - range.from; + + ranges.push_back(range); + }else if (type == range_t::SUBSTITUTION){ + range.original = true; + range.type = range_t::SUBSTITUTION; + range.from = from.second - 1; + range.length = to.second - 1 - range.from; + ranges.push_back(range); + + range.original = false; + range.type = range_t::SUBSTITUTION; + range.from = from.first - 1; + range.length = to.first - 1 - range.from; + ranges.push_back(range); + } +} + +int LevenshteinTable::_min(int a, int b) const +{ + return a < b ? a : b; +} +int LevenshteinTable::_min(int a, int b, int c) const +{ + int m = a < b ? a : b; + return m < c ? m : c; +} +/* LevenshteinTable END */ + + + diff --git a/src/diff/levenshtein_table.h b/src/diff/levenshtein_table.h new file mode 100644 index 0000000..780dcaa --- /dev/null +++ b/src/diff/levenshtein_table.h @@ -0,0 +1,97 @@ +#ifndef _LEVENSHTEIN_TABLE_H_ +#define _LEVENSHTEIN_TABLE_H_ + +#include +#include +#include +#include + +struct range_t{ + enum range_type { + NOCHANGE, + INSERTION, + DELETION, + SUBSTITUTION + }; + + range_type type; + bool original; + int from; + int length; + + bool operator==(const range_t &r) const + { return r.type == this->type && r.original == this->original && + r.from == this->from && r.length == this->length; } + bool operator!=(const range_t &r) const { return !(r == *this); } +}; + +typedef std::vector > list_of_ranges_t; + +class LevenshteinTable{ + private: + const QString _original; + const QString _modified; + + /** + * Levenshtein table. + * O r i g i n a l S t r i n g + * + * M | Insertion + * o v + * d + * i --> Deletion + * f + * i \ + * e \ + * d Substitution + * + */ + std::vector > _table; + + /** + * List of coordinates in which can be levenshtein table traced. + * The pair is in form (row, column). + */ + std::list > _trace; + + /** + * Allocate memory into _table. + */ + void _allocateMemory(); + + /** + * Fill _table as levenshtein table. + */ + void _fillTable(); + + void _findTrace(); + void _minAncestor(int &row, int &column) const; + + /** + * Helper method for fillRanges. + */ + void _finishRange(std::vector &ranges, range_t::range_type, + const std::pair &from, const std::pair &to) const; + + /** + * Returns minimum of numbers. + */ + int _min(int, int, int) const; + int _min(int, int) const; + public: + LevenshteinTable(const QString &orig, const QString &modif); + + /** + * Fill ranges given in argument. + */ + void fillRanges(std::vector &) const; + + /** + * Dump internal _table/_trace into given argument. + * This method is usable for debuging or testing. + */ + void dumpTable(std::vector > &) const; + void dumpTrace(std::list > &) const; +}; + +#endif diff --git a/src/diff/snippet.cpp b/src/diff/snippet.cpp index 1699046..d1d9a6b 100644 --- a/src/diff/snippet.cpp +++ b/src/diff/snippet.cpp @@ -3,15 +3,30 @@ void Snippet::_copy(const Snippet &s) { + if (this == &s) + return; + if (s._original == s._modified){ _original = new Text(*s._original); _modified = _original; + _levenshtein = NULL; }else{ _original = new Text(*s._original); _modified = new Text(*s._modified); + _levenshtein = new LevenshteinDistances(*_original, *_modified); } } +void Snippet::_free() +{ + if (_modified != _original){ + delete _original; + } + delete _modified; + + if (_levenshtein != NULL) + delete _levenshtein; +} int Snippet::_paint(Text *text, QPainter &painter, int offset, int from_line) const { int lines = numLines(); @@ -21,7 +36,16 @@ int Snippet::_paint(Text *text, QPainter &painter, int offset, int from_line) co painter.setPen(getBackgroundColor()); painter.drawRect(0, offset, painter.window().width(), lines*height); - offset = text->paint(painter, offset, lines, from_line); + if (_levenshtein == NULL){ + offset = text->paint(painter, offset, lines, from_line); + }else{ + if (text == _original) + offset = text->paint(painter, offset, lines, from_line, + _levenshtein->getRangesOriginal()); + if (text == _modified) + offset = text->paint(painter, offset, lines, from_line, + _levenshtein->getRangesModified()); + } return offset; } diff --git a/src/diff/snippet.h b/src/diff/snippet.h index 876f3a3..09c5ae7 100644 --- a/src/diff/snippet.h +++ b/src/diff/snippet.h @@ -3,6 +3,7 @@ #include "../settings.h" #include "text.h" +#include "levenshtein_distances.h" /** * Example how diff will be parsed to Snippets: @@ -23,6 +24,10 @@ * */ +//TODO: Create Levenshtein distance class for Changed class. +// Levenshtein distance must be computed during creating a class, not +// during printing! + /** * Base class for Context, Added, Deleted and Changed classes. * This class represents snippet of text of original and modified file. @@ -36,9 +41,10 @@ class Snippet{ Text *_original; Text *_modified; + LevenshteinDistances *_levenshtein; + void _copy(const Snippet &); - void _free(){ if (_modified != _original){delete _original;} - delete _modified;} + void _free(); /** * Paint text by painter on offset (from beginning). from_line is @@ -50,9 +56,11 @@ class Snippet{ Snippet(); public: - Snippet(Text *text) : _original(text), _modified(text){} + Snippet(Text *text) : _original(text), _modified(text), + _levenshtein(NULL){} Snippet(Text *original, Text *modified) : _original(original), - _modified(modified){} + _modified(modified) + { _levenshtein = new LevenshteinDistances(*original, *modified);} Snippet(const Snippet &s){ _copy(s); } virtual ~Snippet(){ _free();} virtual Snippet &operator=(const Snippet &s){_free();_copy(s);return *this;} @@ -85,6 +93,11 @@ class Snippet{ virtual int numModifiedLines() const { return _modified->numLines(); } virtual int numLines() const { return numOriginalLines() > numModifiedLines() ? numOriginalLines() : numModifiedLines(); } + + virtual bool isContext() const { return false; } + virtual bool isAdded() const { return false; } + virtual bool isDeleted() const { return false; } + virtual bool isChanged() const { return false; } }; class Context : public Snippet{ @@ -93,6 +106,7 @@ class Context : public Snippet{ public: Context(Text *t) : Snippet(t){} Context(const Context &c) : Snippet(c){} + bool isContext() const { return true; } }; class Added : public Snippet{ @@ -103,6 +117,7 @@ class Added : public Snippet{ Text original() const{ return Text();} QColor &getBackgroundColor() const { return Settings::Text::background_color_added;} + bool isAdded() const { return true; } }; @@ -114,6 +129,7 @@ class Deleted : public Snippet{ Text modified() const{ return Text();} QColor &getBackgroundColor() const { return Settings::Text::background_color_deleted;} + bool isDeleted() const { return true; } }; class Changed : public Snippet{ @@ -123,6 +139,7 @@ class Changed : public Snippet{ Changed(Text *t, Text *t2) : Snippet(t,t2){} QColor &getBackgroundColor() const { return Settings::Text::background_color_changed;} + bool isChanged() const { return true; } }; #endif /* vim: set sw=4 ts=4 et ft=cpp : */ diff --git a/src/diff/text.cpp b/src/diff/text.cpp index 634f15a..028e6cd 100644 --- a/src/diff/text.cpp +++ b/src/diff/text.cpp @@ -3,6 +3,13 @@ int Text::paint(QPainter &painter, int offset, int lines, int from_line) const { + list_of_ranges_t ranges; + return paint(painter, offset, lines, from_line, ranges); +} + +int Text::paint(QPainter &painter, int offset, int lines, int from_line, + const list_of_ranges_t &ranges) const +{ int max_width = 0; QFontMetrics metrics = QFontMetrics(Settings::Text::font); diff --git a/src/diff/text.h b/src/diff/text.h index 0d262a2..7927a28 100644 --- a/src/diff/text.h +++ b/src/diff/text.h @@ -6,13 +6,19 @@ #include #include "utils.h" +#include "levenshtein_table.h" class Text : public VectorOfPointers{ public: void addLine(QString *qs){ VectorOfPointers::_add(qs);} int numLines() const { return VectorOfPointers::_size(); } + const QString &getLine(int num) const + { return *(VectorOfPointers::_get(num)); } + int paint(QPainter &painter, int offset, int lines, int from_line) const; + int paint(QPainter &painter, int offset, int lines, int from_line, + const list_of_ranges_t &) const; }; #endif diff --git a/tests/Makefile b/tests/Makefile index 442fcaf..43507d7 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -12,8 +12,8 @@ ALL_LD_FLAGS = $(QT_LIBS) ALL_FLAGS += -I../src/diff ALL_LD_FLAGS += -lcppu -TEST_SUITES = text.cpp snippet.cpp hunk.cpp diff.cpp -OBJS_LIST = snippet.o settings.o text.o hunk.o diff.o file.o +TEST_SUITES = text.cpp snippet.cpp hunk.cpp diff.cpp levenshtein.cpp +OBJS_LIST = snippet.o settings.o text.o hunk.o diff.o file.o levenshtein.o OBJS = $(foreach file, $(OBJS_LIST), ../src/objs/$(file)) all: run_tests diff --git a/tests/levenshtein.cpp b/tests/levenshtein.cpp new file mode 100644 index 0000000..319d5fd --- /dev/null +++ b/tests/levenshtein.cpp @@ -0,0 +1,150 @@ +#include +#include +#include +#include "text.h" +#include "levenshtein.h" +using namespace std; +void printTable(vector > &table) +{ + int rows = table.size(); + int cols = table[0].size(); + + for (int i=0; i < rows; i++){ + for (int j=0; j < cols; j++){ + cout << table[i][j] << " "; + } + cout << endl; + } +} +void printTrace(list > &trace) +{ + list >::const_iterator it = trace.begin(); + list >::const_iterator it_end = trace.end(); + for (; it != it_end; it++){ + cout << "(" << (*it).first << ", " << (*it).second << ") "; + } + cout << endl; +} + +void printRanges(vector &ranges) +{ + int len = ranges.size(); + cout << "INSERTION: " << range_t::INSERTION << endl; + cout << "SUBSTITUTION: " << range_t::SUBSTITUTION << endl; + for (int i=0; i < len; i++){ + cout << "type: " << ranges[i].type << ", " + << "original: " << ranges[i].original << ", " + << "from: " << ranges[i].from << ", " + << "length: " << ranges[i].length << endl; + } +} + +TEST_CASE(TestCaseLevenshtein); + +vector > table; +list > trace; +vector ranges; +QString original; +QString modified; + +void setUp() +{ + original = "kitten"; + modified = "sitting"; + + table.resize(8); + for (int i=0; i < 8; i++){ + table[i].resize(7); + table[i][0] = i; + } + for (int i=0; i < 7; i++) + table[0][i] = i; + + table[1][1] = 1; table[1][2] = 2; table[1][3] = 3; + table[1][4] = 4; table[1][5] = 5; table[1][6] = 6; + + table[2][1] = 2; table[2][2] = 1; table[2][3] = 2; + table[2][4] = 3; table[2][5] = 4; table[2][6] = 5; + + table[3][1] = 3; table[3][2] = 2; table[3][3] = 1; + table[3][4] = 2; table[3][5] = 3; table[3][6] = 4; + + table[4][1] = 4; table[4][2] = 3; table[4][3] = 2; + table[4][4] = 1; table[4][5] = 2; table[4][6] = 3; + + table[5][1] = 5; table[5][2] = 4; table[5][3] = 3; + table[5][4] = 2; table[5][5] = 2; table[5][6] = 3; + + table[6][1] = 6; table[6][2] = 5; table[6][3] = 4; + table[6][4] = 3; table[6][5] = 3; table[6][6] = 2; + + table[7][1] = 7; table[7][2] = 6; table[7][3] = 5; + table[7][4] = 4; table[7][5] = 4; table[7][6] = 3; + + trace.push_front(pair(7, 6)); + trace.push_front(pair(6, 6)); + trace.push_front(pair(5, 5)); + trace.push_front(pair(4, 4)); + trace.push_front(pair(3, 3)); + trace.push_front(pair(2, 2)); + trace.push_front(pair(1, 1)); + + range_t range; + + range.type = range_t::SUBSTITUTION; + range.original = true; + range.from = 0; + range.length = 1; + ranges.push_back(range); + + range.original = false; + ranges.push_back(range); + + range.original = true; + range.from = 4; + range.length = 1; + ranges.push_back(range); + + range.original = false; + ranges.push_back(range); + + range.type = range_t::INSERTION; + range.original = false; + range.from = 6; + range.length = 1; + ranges.push_back(range); +} + +void testTable() +{ + LevenshteinTable l_table(original, modified); + vector > table2; + list > trace2; + l_table.dumpTable(table2); + l_table.dumpTrace(trace2); + + assertEqualsM(table, table2, "Incorrect Levenshtein table."); + assertEqualsM(trace, trace2, "Incorrect trace back."); +} + +void testRanges() +{ + LevenshteinTable l_table(original, modified); + vector ranges2; + l_table.fillRanges(ranges2); + + //printRanges(ranges); + //printRanges(ranges2); + + assertEqualsM(ranges.size(), ranges2.size(), "Incorrect size of ranges."); + int len = ranges.size(); + for (int i=0; i < len; i++){ + assertTrue(ranges2.end() != std::find(ranges2.begin(), ranges2.end(), ranges[i])); + } +} + +TESTS{ + REG_TEST(testTable); + REG_TEST(testRanges); +} +TEST_CASE_END; diff --git a/tests/main.cpp b/tests/main.cpp index c822660..a9c3edf 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -2,12 +2,14 @@ #include "snippet.cpp" #include "hunk.cpp" #include "diff.cpp" +#include "levenshtein.cpp" TEST_SUITE(TestSuiteAll); REG(TestCaseText); REG(TestCaseSnippet); REG(TestCaseHunk); REG(TestCaseDiff); + REG(TestCaseLevenshtein); TEST_SUITE_END; int main(int argc, char *argv[]) -- 2.11.4.GIT