Unit Testing with Catch

Es gibt CATCH, das Testing für C++ ermöglicht. Es gibt einen Blogeintrag, der den Einsatz von CATCH auf hohem Niveau beschreibt. Zum Starten ist das Tutorial sehr gut.

#define CATCH_CONFIG_MAIN  // This tells Catch to provide a main() - only do this in one cpp file
#include "catch.hpp"

unsigned int Factorial( unsigned int number ) {
    return number <= 1 ? number : Factorial(number-1)*number;
}

TEST_CASE( "Factorials are computed", "[factorial]" ) {
    REQUIRE( Factorial(1) == 1 );
    REQUIRE( Factorial(2) == 2 );
    REQUIRE( Factorial(3) == 6 );
    REQUIRE( Factorial(10) == 3628800 );
}

Catch innerhalb eines Files einbauen und ausführen

1. Catch als Header in File einbinden

#define CATCH_CONFIG_RUNNER  
#include "catch.hpp"

2. Funktion, die getetst werden will

double calculate(string calculation) {
    double result = 0.0;
    ts.set(calculation);
    result = expression();
    return result;
}

3. Testfälle aufstellen

TEST_CASE( "r", "[digit]" ) {
    REQUIRE(calculate("4") == 4);
    REQUIRE(calculate("6") == 6);
    REQUIRE(calculate("4+2") == 6);
    REQUIRE(calculate("4+2+2") == 8);
    REQUIRE(calculate("4+2+2+4+2+2+1") == 17);
    REQUIRE(calculate("4-2") == 2);
    REQUIRE(calculate("8-2") == 6);
    REQUIRE(calculate("8-2-2") == 4);
    REQUIRE(calculate("3*2") == 6);
    REQUIRE(calculate("3*3") == 9);
    REQUIRE(calculate("3*2*3") == 18);
    REQUIRE(calculate("3+2*4") == 11);
    REQUIRE(calculate("3*2+4") == 10);
    REQUIRE(calculate("4 + 2") == 6);
    REQUIRE(calculate("10 + 2") == 12);
    REQUIRE(calculate("10 + 123") == 133);
    REQUIRE(calculate("-4 + 5") == 1);
    REQUIRE(calculate("-100 + 5") == -95);
    REQUIRE(calculate("(4 + 5)*2") == 18);
    REQUIRE(calculate("3+(-4 * 5)") == -17);
    REQUIRE(calculate("1.5 * 2") == 3);
}

Am Ende des Files folgend die Testfälle.

4. Datei kompilieren
5. Ausführen über Konsole und Argumente mitgeben
Gibt man keine Argumente mit, werden alle Tests durchgelauften.

Struktur der Testfälle

REQUIRE(calculate("6") == 6);

Nach REQUIRE schreibt man die Funktion, die aufgerufen werden soll und gibt ihr die notwendigen Werte mit.
Nach dem == folgt das zu erwartende Ergebnis.

Basic Error-Library

#include <exception>

Der Standardteil dieser Bibliothek umfasst folgende Exceptions:
Logische Fehler                                 Runtime Fehler
– logic_error                                         – runtime_error
– domain_error                                  – range_error
– invalid_argument                          – overflow_error
– length_error                                    – underflow_error
– out_of_range

In der Funktion

void Token_stream::put_back(Token t) {
    if (full) {
        throw std::overflow_error("buffer already full");
    }
    full = true;
    buffer = t;
}

 

 

own Error-Class (Basics)

class neg_sum{};

int main(){
  try {
.     ...
.     
.     ...
  } catch(neg_sum const & e){
.     std::cout << "negative value"<<"\n";
  }   
}

In der Funktion

double solve_quadratic(int a, int b, int c) {
    double delta = pow(b, 2) - (4 * a * c);
    if (delta < 0) {
        throw Negative_delta{};
    }
.   .....
}

 

VHDL Code von Speicher starten

Datei.pof
Der Speicher braucht ein bistream als Synthese.  Deshalb muss neben der *.sof-Datei auch eine *.pof-Datei erzeugt werden.

Grundeinstellung ändern
Bei der Synthese, soll gleich auch die *.pof-Datei generiert werden.
Assignments/Device     Taste: Device and Pin Options    /Configuration
Auswählen von:           Active Serial
Hacken bei:                     v   Use configuration device:       Auto (oder auswählen)
.                                            v   Generate compressed bitstream


Konvertieren *.sof zu *.pof

File/Convert Programming Files…/    Folgende EinstellungeProgramming file type:        Programmer Object File (.pof)
Configuration device:           (siehe Namen auf Board)
File name:                                  <pfad>/name.pof
Input files to convert:            – Anklicken SOF Data
.                                                      – Add File..
.                                                      – outputfile/datei.sof  aus Ordner Synthese
Generate

Programmer Einstellungen
Mode:                                          Active Serial Programming
File:                                              *.sof löschen
.                                                      *.pof adden
Device:                                          <name des Speichers> ! nicht des FPGA

Auf Board
!!!!! Schalter auf Programm !!! nur so entsteht Verbindung zu Speicher.

CUDA Speicher-Verwaltung

Über Pointers kann von einem Speicher (CPU), auf den anderen (GPU) zugewiesen werden.

  • Device-Pointer zeigen auf Speicher im GPU
    – Kann im host-code gebraucht, aber nicht dereferenziert werden
  • Host-Pointer zeigen auf den Speicher der CPU. Kann vom GPU Code gebraucht, aber nicht dereferenziert werden.

    Funktionen
    cudaMalloc( Wert, Grösse);
    cudaFree();
    cudaMemcpy();

Bsp. aus der Einführung

int main( void ) {
int a, b, c;                  // host: a, b, c
int*dev_a, *dev_b, *dev_c;    // device copies of a, b, c
intsize =sizeof(int);         

// allocate device copies of a, b, c
cudaMalloc( (void**)&dev_a, size );
cudaMalloc( (void**)&dev_b, size );
cudaMalloc( (void**)&dev_c, size );

a = 2;
b = 7;

// copy inputs to device
cudaMemcpy(dev_a, &a, size,cudaMemcpyHostToDevice);
cudaMemcpy(dev_b, &b, size,cudaMemcpyHostToDevice);

// launch add() kernel on GPU, passing parameters
add<<< 1, 1 >>>(dev_a,dev_b,dev_c);

// copy device result back to host copy of c
cudaMemcpy( &c,dev_c, size,cudaMemcpyDeviceToHost);

cudaFree(dev_a);
cudaFree(dev_b);
cudaFree(dev_c);
return 0;
}

 

CUDA Introduction: Sprache

Cuda ist von Nvidia eine Sprache wie auch Architektur zur GPU-Verarbeitung. Die Sprache wird im Buch CUDA_by_Example. ausführlich beschrieben. Einen ersten Üblick gibt folgender Vortrag, der vom Autor desselben Buches ebd. über Cuda_Introduction gegeben wurde.

Kompiler
nvcc

Trennung: Ablage in GPU – CPU
Der nvcc-Kompiler trennt den Code in CPU-Speicher und GPU-Speicher.
– Device-Funktionen werden im kernel()  bearbeitet und im GPU-Speicher abgelegt.
– Normale Funktionen werden im der CPU (host) im main()  bearbeitet und ihre Variablen liegen im Speicher der CPU. Kompiliert wird mit  gcc.

int main( void) {
    kernel <<< 1, 1 >>> ();
    printf("hallo \n");
    return 0;
}


Von CPU auf GPU

__global__ :             cuda Schlüssewort für Funktionen die auf GPU laufen,
.                                    Diese Funktionen müssen  von CPU – im main() – aufgerufen
.                                    werden
<funktion> <<< :     Befehl zum Ausführen der Funktion auf der GPU
>>> <parameter> :   Mitgabe der Argumente für die GPU-Funktion.

Bsp.

__global__ void kernel( void ) { 
} 

__global__ void add (int *a, int *b, int *c)
{
 *c = *a + *b;
}

Dies funktioniert nur, wenn man im main() die Funktion auf die GPU verschiebt.

int main( void) {
    int a, b, c;
    int *dev_a, *dev_b, *dev_c;

    // Funktion auf GPU   // Argumente mitgeben
    add <<< 1, 1 >>> (dev_a, dev_b, dev_c);
    printf("hallo \n");
    return 0;
}

CPU (host) ruft GPU (device) auf. Ist notwendig, um eine Funktion auf der GPU aufzurufen.

GPU Introduction

GPU ist ein Grafikprozessor. Also ein Prozessor, mit spezifischem Profil. Hersteller sind Intel, AMD und Nvidia.

CPU <-> GPU
Wird von einem Host gesprochen, meint man damit die CPU (inkl. Memory). Ein C-Programm läuft auf dem Host.

Cuda <-> Metal
Cuda ist das Framework und die Sprache von Nvidio. Will man eine GPU für ein iOS konfigurieren, so braucht man metal als Framework und swift als Sprache.

Testbench compile.do ausführen

Die Testbench wird am einfachsten über die Konsole über das Script gestartet. (Mit tab selbständig vervollständigen)

QuestaSim>   do <pfad>./compile_<projekt>.do
QuestaSim>   do ../simulation/script/compile_<projekt>.do


Script

Das Script compile.do , gibt den Befehl zum Einbinden der Bibliothek, zum Kompilieren der Dateien und startet schlussendlich die testbench.
Das Script liegt im Ordner Work . Alle Pfade bezieht sich vom Ort des Scripts aus.

# create work library
vlib work

# compile files
vcom -2008 -explicit -work work ../../source/counter.vhdl
vcom -2008 -explicit -work work <pfad>/componente_1.vhd
vcom -2008 -explicit -work work <pfad>/top_level.vhd

# compile testbench
vcom -2008 -explicit -work work <pfad>/testbench_top_level.vhd

# run the simulation
vsim -t 1ns -lib work work.testbench_entity

do ../scripts/wave_projekt.do

run 3000.0 ns

Die Reihenfolge der zu kompilierenden Dateien spielt eine Rolle:
– Zuerst die Bibliotheken und Packete
– Dann VHDL: tiefstes Level (spezfischste Komponente)
– Dann VHDL: höheres Level (Zusammenfügender Block)
– Am Schluss: Top-Level
– Testbench

Beim Kompilieren spielt die Version eine Rolle. Gebraucht man process(all), so wird mit der Version 2008 kompiliert. Für die std_textio-Bibliothek braucht man oft 2002, damit man keine Probleme der Doppeldefinition erhält.

Testbench Aufbau

In VHDL gehört die Simulation (schreiben einer Testbench) fix zur Entwicklung dazu. Die Testbench ist in VHDL geschrieben und liegt im Ordner vhdl.
Achtung: Nie in Quartus-Projekt einbinden.  Quartus kann waits und asserts nicht kompilieren.
Ausgeführt (kompiliert) wird die Testbench über die Konsole vom Simlationsprogramm (per Script):

Simulationsprogramm
Bei altera ist questasim die Vollversion des Simulationsprogramms.

work
Einbinden der Bibliothek

Ablage
Im Projektordner wird ein Ordner simulation angelegt. In diesen Ordner kommt – über das Einbinden der Bibliothek  work – der Unterordner Work.
In diesen legt man das Script zum Starten der Simulation compile.do .


Testbench Datei

– die entity hat keine Ports. Sie besteht nur aus Signalen
– die IOs jeder Componente werden an Signale gehängt
– die Testsignale (stimuli) werden in processen gesetzt
– Signale dürfen nur Signalen zugewiesen werden
– die Tests (Vergleichsergebnis von Signalen)
.  finden in den Prozessen statt. Die Processe haben keine Klammern

stimuli:   sind selbst entworfene Signalverläufe
waits:     bestimmen die zeitliche Abfolge unter den
.                  Signalen
reports: geben die Resultat aus
.                  (Konsole oder in File (textbasiertes Testing).
clk:         der clk muss simuliert (erzeugt) werden.
.                 Dies geschieht durch einen eigenen clk-prozess

clk_generator  : process
begin
.   wait for 1 * clk_halfp;
.       clock_sig <= '1';
.   wait for 1 * clk_halfp;
.       clock_sig <= '0';
end process;
entity ram_tb is
end ram_tb;


architecture struct of ram_tb is

---------------------------------------------------------------
-- component declaration
---------------------------------------------------------------
component ram
port(
.    address     : IN STD_LOGIC_VECTOR (15 DOWNTO 0);
.    clock       : IN STD_LOGIC  := '1';
.    data        : IN STD_LOGIC_VECTOR (7 DOWNTO 0);
.    wren        : IN STD_LOGIC ;
.    q           : OUT STD_LOGIC_VECTOR (7 DOWNTO 0)
);
end component;


---------------------------------------------------------------
-- signal instantiation
---------------------------------------------------------------

signal address_sig : STD_LOGIC_VECTOR (15 DOWNTO 0);
signal clock_sig   : STD_LOGIC  := '1';
signal data_sig    : STD_LOGIC_VECTOR (7 DOWNTO 0);
signal wren_sig    : STD_LOGIC ;
signal q_sig       : STD_LOGIC_VECTOR (7 DOWNTO 0);

signal clk_halfp   : time := 10 ns;
signal clk_period  : time := 20 ns;

-- eventuell constanten mit resultaten definieren

begin
---------------------------------------------------------------
-- component instantiation
---------------------------------------------------------------

inst_1 : ram
port map (
.   address  => address_sig,
.   clock    => clock_sig,
.   data     => data_sig,
.   wren     => wren_sig,
.   q        => q_sig
);

---------------------------------------------------------------
-- Set stimuli in process with timing
---------------------------------------------------------------
test_1: process(all)
.    -- Initialwerte setzen

.    -- Signale an Komponenten legen (Stimulation)

.   -- Warten

.   -- Signal von Ausgang Komponente abfangen

.   -- Vergleichen des Outputs mit Erwartung

.   -- Ausgabe Testresulat

end; -- test_1

end; -- architektur

Wie man die Tests startet, wird unter dem Eintrag Testbench ausführen erklärt. Eine professioneller Form des Testens basiert auf textbasierten Scripts.

Hier wird die Syntax der Testbench-Fehler (assert, report, severity,..) beschrieben.