Hallo ! Die eigentliche Frage steht ja schon im Betreff. Mir geht es darum ein paar Regeln zu sammeln um Fehler festzustellen bzw. im Ansatz schon zu erkennen. Sowas wie keine Zuweisungen in Ifs oder den Wertebereich prüfen. Was für "Tricks" habt ihr euch so angewöhnt um Fallstricke zu umschiffen ? Was macht Ihr gegen Laufzeitfehler ? Schreibt ihr sowas wie Testbedingungen für eure Funktionen ?
Sehr hilfreich ist die Angewohnheit, sich schon im Vorfeld klar zu werden, was eine Funktion können soll. Damit drängt sich direkt auf, wie man sie testen kann - nämlich genau gegen diese Soll-Vorgaben. Bei Auftragsarbeiten sollte es hier eine gewisse Verbindung zum Pflichtenheft geben... Besonders bei kleineren Sache schreibe ich dann zu den Funktionen auch gleich in die selbe Datei den Testcode rein, z.B.
1 | // dieses ... macht folgendes: ...
|
2 | |
3 | #include "meine_funktionen.h" |
4 | |
5 | void meinefunkltion1(){...} |
6 | void meinefunkltion2(){...} |
7 | void meinefunkltion3(){...} |
8 | |
9 | #ifdef TESTMEINEFUNKTIONEN
|
10 | |
11 | #include <iostream> |
12 | |
13 | int main( int nargs, char**args ) |
14 | {
|
15 | // Funktionen aufrufen, Ergebnisse prüfen...
|
16 | // das ergibt sich 1:1 aus den Anforderungen
|
17 | // ...
|
18 | }
|
19 | |
20 | #endif TESTMEINEFUNKTIONEN
|
Zumindest bis zu einer gewissen Größe ist das praktisch, weil man dann den einen Quelltext mit -DTESTMEINEFUNKTIONEN direkt zu einem Testprogramm kompilieren kann, ohne -DTESTMEINEFUNKTIONEN wird es in einem größeren Programm als Objekt verwendet ohne die Testaufrufe. Soweit sich Tests automatisieren lassen, habe ich dafür gelegentlich ein eigenes Target im Makefile. Zudem verwende ich viel assert() (nicht auf MC) und nutze reichlich Ausnahmen, die mir auch Datei und Ort des Entstehens und der beim Werfen durchlaufenen Ebenen sammeln. Über den Präprozessor habe ich weiterhin meist eine Testversion, die mehr testet und mehr ausgibt, als die endgültige Version. Sehr sinnvoll ist es auch, nach Möglichkeit ein und denselben Quelltext mit möglichst verschiedenen Compilern zu testen. Das offenbart etliche kritische Stellen. Hilft jetzt natürlich nicht bei reinen MC-Geschichten. Dort kann und sollte man aber auch oft die systemspezifischen Dinge (PORT...) von den allgemeineren Funktionen (PID-Regler, Numerik, ...) trennen und letztere einzeln auf anderen Systemen testen. Z.B. kann man für etwas wie einen PID-Regler, der zuletzt auf einem MC laufen soll, prima ein Testprogramm als Simulation auf einem PC bauen; das eröffnet viele Erkenntnisse. Beim Stil des Quelltextes ("niemals ..., weil das böse ist") bin ich etwas reserviert, weil es mir oft zu dogmatisch ist. Aber auf jeden Fall soll der Quelltext so sauber und aufgeräumt wie nur irgend möglich sein, und auf den ersten Blick verständlich. Nur so sieht man auch die Fehler mit vertretbarem Zeitaufwand. Dazu gehören Einrückungen, Klammern etc.; vor allem sprechende Namen. Auch bei der Struktur eines Programms (Threads, wer plaudert wie mit wem etc.) muß man sich im Klaren sein, was man will und das auch im Stil und der Dokumentation deutlich ausdrücken. Alleine der Übersichtlichkeit wegen würde ich Zuweisungen deshalb eher weniger direkt in if() schreiben, pauschal ausschließen möchte ich es aber nicht. Zumindest in while-Bedingungen mache ich schon gelegentlich auch gleich eine Zuweisung, weil es den Quelltext manchmal deutlich vereinfacht (while( (c=fgetc(f)) ){...}). In dem Stil kann man jetzt natürlich vom Hundertsten ins Tausendste kommen... Es gibt dazu auch durchaus interessante Bücher, die meist nicht zu 100% auf den eigenen Fall übertragbar sind, aber manchmal doch lesenswert. Konkret für C fällt mir da ein "The Practice of Programming" von Kernighan und Pike.
1) ich spicke meinen Queltext mit einem selbst gestrickten ASSERT-macros, das nur in der Debugversion aktiv ist.
1 | #ifdef DEBUG
|
2 | # define ASSERT(test) { \
|
3 | if (!(test)) { \
|
4 | fprintf(stderr, "Assertion failed: '%s' ( %s line %d)\n\r", \
|
5 | #test, __FILE__, __LINE__); \
|
6 | asm("brk"); \
|
7 | } \
|
8 | }
|
9 | #else
|
10 | # define ASSERT(test)
|
11 | #endif
|
12 | |
13 | ...
|
14 | |
15 | void foo(int *ptr) |
16 | {
|
17 | ASSERT(ptr!=NULL); |
18 | ...
|
19 | }
|
2) teste ich modular, so das ich mich 99% sicher bin das die verwendeten Unterfunktionen funktionieren. Eigentlich schreibe ich fast immer erst den Testcode und dan die Unterfunktionen. zB. foo.c
1 | int foo(int x) |
2 | {
|
3 | ...
|
4 | }
|
5 | |
6 | #ifdef MODULTEST
|
7 | void main(void) |
8 | {
|
9 | ASSERT(foo(3)); |
10 | }
|
11 | #endif
|
3) simuliere ich viel auf dem PC. Wegen der bessere Debugmöglichkeiten. Ja auch LED-Ausgaben, eeprom-Routinen und Timmer-Interrupts. Wie ? - Port-ausgaben landen in einer normalen Variablen. - eeprom zugriffe lenke ich auf die Festplatte um. - Für periodische Timmer nehme ich einen eigenen Thread. (nicht so genau aber zum Testen reicht es meist) So kann ich 60-90% der Code auf einem PC simulieren und Testen. ciao Volker
Hi, um "Fehler im Ansatz zu erkennen", helfen Dir Tests bzw. Testmethoden nicht, da diese ja erst nach der Implementierung gemacht werden können. Hier helfen ein vernünftiges Anforderungsmanagement ("Wissen, was wie zu tun ist") und ein Design (z.B. Flowchart, Architektur). Um das Verhalten einzelner Funktionen zu testen, gibt es sog. Unit-Testtools (z.B. CUNIT); diesen gibtst Du Deine Funktion plus einer Liste von konkreten Eingangsparametern und den erwarteten Ausgaben. CUNIT lässt die Funktion dann laufen und vergleicht die tatsächliche mit der erwarteten.
Wollte schreiben: "CUNIT lässt die Funktion dann laufen und vergleicht die tatsächliche Ausgabe mit der erwarteten."
Hmmm, also ich habe mir mehr intuitiv angewöhnt die Aufgabenstellung als Problem anzusehen das ich mit Teilmengen von Lösungen angehe. Dieses Problem teile ich in immer kleiner Teilprobleme bzw Teillösungen, die für mich noch überschaubar sind auf. Ich denke das man so eine gute Abstraktion hinbekommt. Allerdings lerne ich beim Programmieren (für mich reinhacken des Source), immer wieder neues über "meine" Probleme dazu. Man könnte sagen mein Wissen um die Lösungen steigt ?!? :) Glaube auch das die Teilmengen einfacher zu testen sind... ---- @Mehmet Kendi Ich habe mir mal dieses Splint angesehen... Das Produziert schon einen gawaltigen Output ;) :0 Alleine bei obigem Source. Aber das meckert ja schon wenn man anstelle eines >int< einen >uint8_t< für einen Boolean "missbraucht". Bzw. mit sowas kann ich gerade im moment noch nichts anfagen : >Function exported but not used outside main: adc_init > main.c:107:1: Definition of adc_init --- @Klaus Wachtler Dieses
1 | assert(); |
prüft als nur den Rückgabewert einer Funktion im Laufenden Betrieb. Wenn ich eine Funktion auf Herz und Nieren Testen will, habe ich in C keine andere Möglichkeit als aus einem (ev. bedingt kompilierten) Programmteil meine Funktion mit verschiedenen Werten/Zuständen aufzurufen und die erwarteten Ergebnisse zu prüfen. Das blöde ist ja, ich muss sehr viele Bedingungen durchspielen. Allerdings müssen die Testfälle dann auch ohne Bezug auf die zu Prüfende Funktion sein. (Das kann schon Spaghetti im Kopf machen, oder ?) Selbst bei einem einfachen (a > b) - Vergleich können meine Testfälle einfach ausfallen : 10 > 5 -> wahr 10 > 10 -> falsch 5 > 10 -> falsch Da mein {a,b} vielleicht vom Typ int16_t ist, müsste ich für meinen Test (heisst das Validierung?) doch den gesamten Wertebereich und alle Kombinationen austesten ?!?? --- @Volker Zabe Wow ! Das bläht den Umfang doch bestimmt auf das 1,5fache auf, oder ? OK, und es reduziert die Fehleranfälligkeit enorm. Wobei ein Testfall auch nur so gut wie der Programmierer ist... >2) teste ich modular, so das ich mich 99% sicher bin das die >verwendeten Unterfunktionen funktionieren. Eigentlich schreibe ich fast >immer erst den Testcode und dan die Unterfunktionen. zB. Es scheint also auch bei den Informatikern beliebt zu sein die Teilmenge der Lösung auf die kleinste Abstraktionsebene zu bringen. --- @ Gast Dieses CUnit werde ich mir noch ansehen, dafür hat es bis jetzt noch nicht gereicht. ---- Hat denn vielleicht jemand ein kleines Progrämmelchen um mir ein Praktisches Beispiel zu geben ? Das scheint ja ein weites feld zu sein... Vielen Dank schonmal für die guten Antworten !!! Thomas PS: Ich schreibe halt nur in C auf dem µC. Wobei ich merke das ich mich damit wieder befassen müsste !
Thomas W. schrieb: > ... > @Klaus Wachtler > > Dieses
1 | assert(); |
prüft als nur den Rückgabewert einer Funktion im > Laufenden Betrieb. Es prüft einen beliebigen Ausdruck, je nachdem, was man reinschreibt. Dadurch hat man natürlich keine umfassende Kontrolle, ob ein Programm alles richtig macht, sondern viele sinnvoll gesetzte assert() sind nur ein Mosaiksteinchen, um Fehler zu vermeiden. Gedacht ist es für Fälle, in denen man an irgendeiner Stelle davon ausgeht, daß bestimmte Bedingungen doch hoffentlich auch erfüllt sind, ohne sie immer tatsächlich zur Laufzeit testen zu wollen. Beispiel: in einer Funktion, der ein Zeiger übergeben wird, will man (aus Effizienzgründen) nicht immer testen, ob ein übergebener Zeiger !=NULL ist, sondern will das beim Aufrufer einfach voraussetzen. In der Debugphase kann man das in der Funktion dennoch mit assert() testen (assert(p_irgendwas!=NULL)), ohne daß es in der ausgelieferten Version Rechenzeit kostet. assert() ist sicher kein Ersatz, um Benutzereingaben zu testen oder ähnlich unkontrollierbares Verhalten. > > Wenn ich eine Funktion auf Herz und Nieren Testen will, > habe ich in C keine andere Möglichkeit als aus einem (ev. bedingt > kompilierten) Programmteil meine Funktion mit verschiedenen > Werten/Zuständen aufzurufen und die erwarteten Ergebnisse zu prüfen. > > Das blöde ist ja, ich muss sehr viele Bedingungen durchspielen. > Allerdings müssen die Testfälle dann auch ohne Bezug auf die zu Prüfende > Funktion sein. Ein voller Test alle Eingabemöglichkeiten scheitert in aller Regel an der Menge der Möglichkeiten. > ... > Das scheint ja ein weites feld zu sein... Ja. > ...
Klaus Wachtler schrieb: > Gedacht ist es für Fälle, in denen man an irgendeiner Stelle > davon ausgeht, daß bestimmte Bedingungen doch hoffentlich auch erfüllt > sind, ohne sie immer tatsächlich zur Laufzeit testen zu wollen. assertions können auch dann sinnvoll sein, wenn man den bewussten Laufzeittest auch tatsächlich macht um Abstürze bzw. Fehlverhalten zu vermeiden. Die Assertion schlägt dann an und teilt mir frühzeitig mit, dass die Funktion fehlerhaft aufgerufen wurde. Die Alternative wäre langwieriges Suchen, wo ein Sicherheitsmechanismus zugeschlagen hat und Schlimmerers verhindert hat, die globale Funktion jedoch trotzdem nicht ausgeführt wurde. > assert() ist sicher kein Ersatz, um Benutzereingaben zu testen > oder ähnlich unkontrollierbares Verhalten. Das sicher nicht. Aber assertions können schon sehr hilfreich sein, weil man nicht zufällig über Fehler stolpert, sondern sich der Code selber meldet, dass etwas nicht stimmt.
OK, diese Asserts benachrichtigen mich bei einer nicht erfüllten Erwartungshaltung. Das hat ein wenig von LED-Debugging... --- Kann man sagen, selbst wenn man alle Teilmengen (Funktionen) eines C-Programms geprüft hätte (was anscheinend nur begrenzt möglich ist, obiges bsp. mit dem einfachen Vergleich würde ja bedeuten 2^32 Zustände zu prüfen), kann man immer noch nicht sicher sein das dass ganze Programm Fehlerfrei ist ? Ich kann mir ja sogar die dollsten Fehler reinzaubern, wenn ich in meinen defines einfach mal einen Wertebereich überschreite... (z.B. Bedingung wird nie mehr erfüllt weil Variable nicht weit genug Zählt) Theoretisch müsste man bei allem unbekannten den Wertebereich abfragen. ...scheint wohl einer der Gründe zu sein, warum man Informatik studiert und nicht als Ausbildung erlernt.
Das erinnert mich stark an : http://de.wikipedia.org/wiki/Falsifizierung Wir erarbeiten etwas im guten, um es danach kaputt zu prüfen...
Thomas W. schrieb: > Kann man sagen, selbst wenn man alle Teilmengen (Funktionen) eines > C-Programms geprüft hätte (was anscheinend nur begrenzt möglich ist, > obiges bsp. mit dem einfachen Vergleich würde ja bedeuten 2^32 Zustände > zu prüfen), > kann man immer noch nicht sicher sein das dass ganze Programm Fehlerfrei > ist ? Das ist richtig. Denn es gilt: Durch testen kann man immer nur das Vorhandensein von Fehlern nachweisen, nie das Nichtvorhandensein. Theoretisch ginge auch die Umkehrung (Nachweis der Fehlerfreiheit) allerdings ist es normalerweise nicht praktikabel, weil du in einem realen Programm nicht alle überhaupt möglichen Inputs durchtesten kannst.
... zumal die Blackbox (unser Programm) nicht nur ein Verhalten abhängig zu den Eingangszuständen und deren Feedback besitzt, sondern auch noch abhängig von der Zeit ist. Somit ist doch prinzipiell schon bewiesen das nach dem jetzigen Stand es prinzipiell immer zu fehlerhafter Programmausführung kommen kann -- auch wenn der Algorithmus noch so gut ist. Ich als Programmierer, bei meinem Wissen kann ich nur ein schlechter sein, muss also dafür sorgen das Fehler die zu unbekannten Zuständen (???) führen können irgendwie abgefangen werden und in einem kontrollierten enden. z.B. Neustart ausführen, unkritische Defaultwerte nehmen (Notlauf), ... (der Freibrief für den MS-BSOD) Bei obiger Alarmanlage sieht das alles noch recht überschaubar aus, aber immer wenn ich nach kurzer Zeit wieder mal drüberschaue fallen mir irgendwelche Unzulänglichkeiten auf. z.B. wäre die Sensor.Codeschloss - abfrage als Funktion die automatisch die Variable zurücksetzt mit Sicherheit schöner...
Thomas W. schrieb: > Somit ist doch prinzipiell schon bewiesen das nach dem jetzigen Stand es > prinzipiell immer zu fehlerhafter Programmausführung kommen kann -- > auch wenn der Algorithmus noch so gut ist. Na ja, ganz so ist es nicht. Man kann schon mathematisch beweisen, ob ein Algorithmus korrekt ist. Das Problem ist die Definition der Korrektheit. Sie wird dadurch bestimmt, dass man einen Satz von Ausgangseigenschaften (mathematisch) festlegt, sowie einen Satz von Endeigenschaften. Man kann dann mathematisch zeigen, ob die Endeigenschaften durch anwenden des Algorithmuses entstehen. Das schützt natürlich nicht vor Implementierungsblunder, aber wenn der Algorithmus korrekt in eine Programmiersprache übertragen wurde, dann hat man somit den Nachweis, dass diese Codestelle in dem Sinne korrekt ist, dass sie die Anforderungen der Transformation von Ausgangseigenschaften zu Endeigenschaften liefert. Solche Beweisführungen sind aber extrem aufwändig (zumindest waren sie das, als ich sowas zuletzt im Studium machen musste). Was hier natürlich auch völlig aussen vor bleibt, sind die Einflüsse der Umgebung, wie 'End of Memory', um nur die Prominenteste zu nennen.
>Was hier natürlich auch völlig aussen vor bleibt, sind die Einflüsse der >Umgebung, wie 'End of Memory', um nur die Prominenteste zu ne ^--- Umgebung in welchem Sinn ? End of Memory ? Meinst Du z.B. die Hardware auf der das Prg. läuft ? Also kann man zwischen folgenden Fehlerquellen unterscheiden : 1 Ungenaue/falsche Aufgabenstellung. 2 Aufgabe nicht verstanden. 3 Algorithums -- d.h. Aufgabe/Problem falsch gelöst. 4 Fehlerhafte Verarbeitung/Implementierung : if (x=1) {..}, Overflows 5 Die Hardware auf der Ausgeführt wird. (Wobei der Programmierer dabei keine "schuld" hat) Wobei 1 und 2 wohl eher Kommunikationsprobleme sind... Kann mir jemand ein paar Bücher empfehlen ? (wollte mir eigentlich die Übersetzung Programmieren in C kaufen...)
Also könnte man behaupten das jede Programmiersprache sich auf die Axiome der Mathematik und deren Ableitungen (wohl angefangen bei der Aussagenlogik, Grundrechenarten, ...) beruft. Eine Programmiersprache ist also eine Art formal mathematisch beschreibende Sprache ? Die beiden sind also verwandt ! lach
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.