Montag, 24. Oktober 2011

Differenzenermittlung - Problem ist gelöst

Das Problem mit den Differenzen ist nun gelöst. Das folgende Template kann eine beliebige einzelne, benannte Abweichung festhalten und formatiert ausgeben.

Aus Clientsicht vereinfacht sich der Aufruf erheblich: Hier ein Beispiel, der Aufruf einer Planetenberechnung via swe_calc(). Beim Aufruf gibt man die Argumente sowie die erwarteten Rückgabewerte an. Wenn die Methode calcsAsExpected feststellt, dass einige Werte abweichen, gibt sie false zurück. Man kann dann das Member diffs verwenden, um alle festgestellten Differenzen auszugeben.
class TestSwissEphemeris : public Test {
public :
virtual void run() {
SweCalc se;
if (!se.calcsAsExpected(
2451545.0,
0, // <-- SE_SUN
0, // <-- Flags
(double[6]) { 280.368, 0.000227827, 0.983328, 0.,0.,0. })
) {
fail( "Differences for Sun", se.diffs );
}
}
};


Dieser Teil des Programms wird automatisch generiert werden, wobei dann die Erwartungswerte mit hineinkommen. Auch der Text (hier Differences for Sun kann vom Automaten so produziert werden, dass man nachher bei Betrachtung des Logs den Fall nachvollziehen kann. Es könnte z.B. eine swetest-Kommando sein, mit dem man den Unterschied reproduzieren kann.

Hier die oben verwendete Klasse, die einen Aufruf von swe_calc() ausführt und die Differenzen sammelt:

class SweCalc : public TestCalculation {
public :
bool calcsAsExpected(
double juldate,
int planet,
int flags,
const double expectedResult[],
const string expectedMessage = "" ) {

double actualResult[6];
char actualMessage[255];

int return_flags = swe_calc(
juldate,
planet,
flags,
actualResult,
actualMessage );

diffs.check( "xx", 6, actualResult, expectedResult );
diffs.check( "msg", actualMessage, expectedMessage );
diffs.check( "flags", return_flags, flags );

return ! hasDiffs();

}
};


Was für andere Funktionsaufrufe der Swiss Ephemeris wiederverwendbar ist, kommt in eine Oberklasse TestCalculation. Im Moment stehen hier nur die Differenzen-Sätze:

class TestCalculation {
public :
bool hasDiffs();
Diffs diffs;
};


Die ganz allgemeine Schicht, die diese Prüfungen durchführt und in der ich den Fehler hatte, folgt hier:

template<typename T> 
class Diff : public DiffBase {
public :
Diff(string name, T actual, T expected)
: actual(actual), expected(expected) {
this->var = new Var(name);
}
Diff(string name, int index, T actual, T expected)
: actual(actual), expected(expected) {
this->var = new VarMember(name,index);
}
virtual string toString() const {
stringstream s;
s << var->getName()
<< "\t: actual=" << actual
<< ", "
<< "expected=" << expected ;
return s.str();
}
~Diff() {
delete var;
}
private :
Var* var;
T actual;
T expected;
};

Die Klasse, die das Problem machte, war Diffs - eine Sammlung mehrerer solcher Abweichungen. Da ich sie auch im System herumreichte, hat mir der Destruktor in den Fuss geschossen, den ich zuerst vorgesehen hatte. Nach Löschung des Destruktors und Umstellung auf einen boost::shared_ptr (der das delete ja automatisch ausführt, wenn der letzte Besitzer des Pointers sein Leben aushaucht) lief alles problemlos.

class Diffs {
public :
Diffs() {
diffs = boost::shared_ptr<vector<DiffBase*>>
(new vector<DiffBase*>);
}
void getLog( vector<string>& log ) const {
log.clear();
for (vector<DiffBase*>::const_iterator i=diffs->begin();
i!=diffs->end();
++i) {
log.push_back( (*i)->toString() );
}
}
bool hasDiffs() { return diffs->size() > 0; }
template<typename T> void check( const string& name,
const T actual,
const T expected ) {
if (actual != expected) {
diffs->push_back( new Diff<T>(name,actual,expected) );
}
}
template<typename T> void check( const string& name,
const int size,
const T actual[],
const T expected[] ) {
for (int i=0; i<size;i++) {
if (actual[i] != expected[i]) {
diffs->push_back( new Diff<T>(name,i,actual[i],expected[i]) );
}
}
}
string toString() const {
string s;
for (vector<DiffBase*>::const_iterator i=diffs->begin();
i!=diffs->end();
++i) {
s.append((*i)->toString());
s.push_back('\n');
}
return s;
}
private :
boost::shared_ptr<vector<DiffBase*>> diffs;
};


Ein isoliert lauffähiges Beispiel habe ich codepad.org gestellt, wo es auch ausgeführt werden kann (ist sogar ISO-C++, obwohl ich mir für das Testprogramm immer den "Standard" C++0x erlaube).

Auch heute gibt es ein
Todo: Für double sollte ein Test mit einer vorgegebenen Genaugkeit durchgeführt werden. Wie ist das in das Template einzubauen?

Sonntag, 23. Oktober 2011

Problem bei Differenzen-Ermittlung

Die automatischen Tests werden von einem in C++ geschriebenen Programm namens tests durchgeführt werden. Teile dieses Programms werden anhand von erwarteten Testdaten, die z.B. als Ouptput vom bestehenden Programm swetest produziert werden, generiert.

Bin gerade dabei, ein System von Klassen zu schreiben, das über allfällig festgestellte Abweichungen von Ist- und Sollwerten Buch führt: Die entscheidende Klasse Diffs ist im wesentlichen eine Sammlung (Vektor) von einzelnen Abweichungssätzen. Eine einzelne Abweichung ist vom Typ IntegerDiff, DoubleDiff, DoubleMemberDiff, StringDiff o.ä. - alles Subtypen einer gemeinsamen Oberklasse Diff. Gibt es Abweichungen, so wird eine solche Diffs-Instanz als Member der entsprechenden Failure-Ausnahme an den TestRunner gesendet. Dieser protokolliert in der Standardausgabe die Summenzeile des TestAnythingProtocols und gibt die Details in ein Logfile aus.

ToDo - Code-Duplizierungen verhindern durch Templates: Diff<double>, Diff<int> etc.


Hier gibt es bei der Vererbung noch das Problem, dass mir der Vektor irgendwo auf eine Weise kopiert wird, durch die die Typinformationen der aktuellen Typen verlorengeht und alle Einzelabweichungen nur noch den Typ der gemeinsamen Oberklasse Diff haben.

Das Vorhaben wird dargelegt

Vor kurzem fasste ich den Entschluss, die Bibliothek Swiss Ephemeris so umzuschreiben, dass sie nebenläufig lauffähig ist.

Dies ist ein grösseres Refactoring-Projekt, das folgende Schritte beinhaltet:

  • Bevor auch nur eine Zeile Code geändert wird, muss ein dichtes Netz von Tests bereitstehen, die automatisch, z.B. mit einer bestimmten make-Regel, alle relevanten Funktionen im aktuellen Stand der Bibliothek aufrufen und mit den erwarteten Ergebnissen vergleichen.

  • Zwar ist es theoretisch möglich, globalen Speicher in mehreren Threads wiederzuverwenden, der gelesen und geschrieben wird. Wo es möglich ist, ist es jedoch besser, wenn der Aufrufer den Speicherplatz für einen allfällig benötigten Kontext bereitstellt (also Daten "auf dem Stack statt auf dem Heap" verwendet werden). Ausnahme sind natürlich Ressourcen, die wirklich von allen Threads in identischer Form benötigt werden (z.B. die Daten von Ephemeridenfiles). Es soll ein Set von API-Funktionen angeboten werden, die genau dies ermöglichen.

  • Die bestehenden Funktionen werden an die neuen API-Funktionen angeschlossen (die dann den benötigten Speicher intern allokieren), so dass Kompatibilität mit den bisherigen Verwendungen der Swiss Ephemeris erreicht wird.


Angesichts der vielen Arbeit, die in diese Software gesteckt wurde, und angesichts der geringen Freizeit, die mir dafür zur Verfügung steht, habe ich mir für dieses Vorhaben rund ein Jahr Zeit gegeben. Ich kann damit scheitern - oder auch nicht.

Ein Blog scheint mir eine passende Form, um die kontinuierliche Arbeit an diesem Thema zu dokumentieren. Auch diesen Blog schreibe ich - wie meinen persönlichen Blog eigentlich nur für mich selbst: Durch das systematische Aufschreiben von Themen bekomme ich Klarheit in meinem Gedankenkasten. In dieser Funktion ist der Blog identisch mit einem gewöhnlichen Tagebuch. Abweichend ist nur die elektronische und überall verfügbare Form - das Medium.

Dieser Blog bleibt bis auf weiteres nur einem begrenzten Nutzerkreis zugänglich, den ich persönlich einlade.