poniedziałek, 19 marca 2012

Gopher test

W standardowej bibliotece Go nie ma klasycznej wersji Unit Testów. Nie załamujcie się przeto wyznawcy TDD, nie popadajcie w marazm, niech nie pochłonie was rozpacz, albowiem istnieje światełko nadziei dla Was. Wielki i wszechmocny Gopher Was nie opuści.

Daje on narzędzia do prostego testowania i uruchamiania tychże testów. Są proste, a jednocześnie dość efektowne, jak cały język.

W niniejszym wpisie postaram się wyłożyć prosty przykład Gophowego pakietu wraz z klasą i przykładami testów.

Po pierwsze, tworzymy pakiet strdict, a w nim tworzymy Gopherowy typ Dict wraz z jego metodami.
Wszystko to zapisujemy w pliki strdict.go w katalogu GOPATH/src/strdict.

--------------
package strdict

import (
    "fmt"
)
// new type
type Dict map[string]string

// get value with key
// if key not present then raise panic
func(m Dict) Get(key string) string {
    k, ok := m[key]
    if !ok{
        panic(fmt.Sprintf("No key '%s' in Dict", key))
    }
    return k
}
// get value with key
// if key not present then return def
func(m Dict) GetDef(key, def string) string {
    k, ok := m[key]
    if !ok{
        return def
    }
    return k
}

// check if m has key value
func (m Dict) HasKey(key string) bool{
    _, ok := m[key]
    return ok
}
// set value with key
func(m Dict) Set(key, value string)  {
    m[key] = value
} 
// print all keys and values 
func (m Dict) Print(){
    for k, v := range(m){
        fmt.Println(k, "  ", v)
    }
}

--------------

Teraz tworzymy plik z testami do pakietu. Nazywamy go strdict_test.go i zapisujemy w tym samym katalogu co strdict.go.

--------------
package strdict

import (
    "testing"
    "fmt"
)

func TestSet(t *testing.T){
    m1 := Dict{}
    m2 := Dict{}
    m1.Set("a", "a")
    m1.Set("a", "b")
    m1.Set("a", "v")
    m1.Set("d", "f")
    m2.Set("a", "zzz")
    if m1["a"] != "v"{
        t.Error("Set error. No value in map")
    }
}

func TestGet(t *testing.T){
    m := make(Dict)
    m["a"] = "b"
    m["b"] = "c"
    m["c"] = "d"
    if m.Get("a") != "b"{
        t.Error("Get error. Wrong value for key ")
    }
    defer func(){
        if err := recover(); err == nil{
            t.Error("Get error. Getting non existing value should raise panic")
        }
    }()
    m.Get("non existing key")
}

func TestHasKey(t *testing.T){
    m := make(Dict)
    m["a"] = "a"
    m["b"] = "b"
    if !m.HasKey("a"){
        t.Error("Error checking existing key")
    }
    if m.HasKey("c"){
        t.Error("Error checking none existing key")
    }
}
func ExampleGet(){
    m := Dict{}
    m["a"] = "123"
    fmt.Println(m.Get("a"))
    //Output:
    // 123
}

func BenchmarkGet(b *testing.B){
    b.StopTimer()
    m := Dict{}
    m["aa"] = "aa"
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        _ =m.Get("aa")
    }
}

func BenchmarkSet(b *testing.B){
    b.StopTimer()
    m := Dict{}
    b.StartTimer()
    for i := 0; i < b.N; i++ {
        m.Set("aa", "1")
    }
}
    

--------------

Parę słów objaśniających testy:

TestSet, TestGet, TestHasKey - klasyczne przykłady testów jednostkowych. Ich nazwy muszą rozpoczynać się od Test... 
W tym jednak wypadku nie mamy instrukcji assert, lecz używamy instrukcji if i t.Error(). 

ExampleGet - ta funkcja testująca porównuje wyjście standardowe, czyli w tym przypadku wynik instrukcji fmt.Println ze spodziewanym wyjściem które umieściliśmy w komentarzu. Jeśli będą różne wówczas testy zakończą się niepowodzeniem. Musi się zaczynać od Example.

Innymi ciekawymi przykładami są funkcje BenchmarkSet i BenchmarkGet. Mierzą one średni czas wykonania operacji.



Sposób wywołania testów w konsoli:

----------------------------------------------------------------------------
user@user-home:~$ go test strdict
ok   strdict 0.006s
----------------------------------------------------------------------------

Sposób wywołania testów wraz z benchmarkami:

----------------------------------------------------------------------------
go test -test.bench="." strdict
PASS
BenchmarkGet 20000000        109 ns/op
BenchmarkSet 20000000        114 ns/op
ok   strdict 4.718s
----------------------------------------------------------------------------

środa, 7 marca 2012

Pierwszy konkretny przykład

W ramach nauki postanowiłem napisać aplikację, która korzystając z charakterystycznego dla go przetwarzania równoległego będzie przeszukiwać foldery w poszukiwaniu plików z nazwami pasującymi do wyrażenia regularnego.

Pierwsze założenie było takie, żeby każdy folder przetwarzany był w osobnej gorutynie i zwracał wyniki kanałem, bądź porostu wypisywał je na standardowym wyjściu. 

Program działał całkiem fajnie i wydajnie przy małej ilości plików do przeszukania. Kiedy natomiast uruchomiłem go dla własnego katalogu HOME, lub dysku C na windowsie, po chwili produkował sporą ilość błędów wynikającą ze zbyt dużej liczby uchwytów do plików. Okazało się ze otwieranie i przetwarzanie katalogu w osobnym procesie powodowało lawinowy przyrost otwartych plików, z którymi zarówno Ubuntu jak i Windows7 nie były sobie w stanie poradzić. 

Wróciłem więc i przeprojektowałem pogram w ten sposób, aby pliku otwierane były tylko w głównej gorutynie, natomiast proces dopasowywania nazw plików do wyrażenia regularnego odbywał się w osobnych procesach.

Zaczynając od początku. Tworzymy strukturę będzie przechowywać konfigurację naszego przeszukiwania, czyli katalog od którego startujemy, wyrażenie którego szukamy. Struktura w Go pełni rolę swego rodzaju obiektu. Posiada zaalokowaną pamięć oraz swoje atrybuty. Przypomnieć można, że jedynie atrybuty, które zaczynają się wielgachną literą będą widoczne poza strukturą, czyli można powiedzieć, publiczne.

------------------------------
type Config struct {
    StartPath            string
    SearchNamePattern    *regexp.Regexp
    SearchContentPattern *regexp.Regexp
}
------------------------------

Następnie przygotowujemy funkcje konstruującą.  W Go nie istnieją znane z innych języków konstruktory, dlatego często tworzy się funkcje, które inicjują i zwracają wskaźnik do nowego obszaru pamięci zarezerwowanej przez strukturę. Nazwy w Go powinny być jednoznaczne, możliwie krótkie i oczywiście znaczące. Wszystko w myśl zasady KISS you FOOL.
W naszym przypadku funkcja taka mogła by mieć postać:

------------------------------
func NewConfig(startDir string, snp string) Config {
    snpRe := regexp.MustCompile(snp)
    return Config{startDir, snpRe}
}
------------------------------
Funkcja zaczyna się wielgachną literą więc będzie widoczna na zewnątrz modułu. Skoro jednak nasza struktura również zaczyna się od dużej litery, alternatywnym sposobem utworzenia naszego konfiga byłoby: return Config{startDir, snpRe}.

Przygotujmy teraz funkcje odczytujące zawartość katalogu i przetwarzające listę plików z niego. Prawdziwe wąskie gardło naszej aplikacji.
------------------------------
//
// Process directory given as a path
//
func processDir(p string) []os.FileInfo {
    var (
        f   *os.File
        fi  []os.FileInfo
        err error
    )

    if f, err = os.Open(p); err != nil {
        return fi
    }
    defer f.Close()
    if fi, err = f.Readdir(-1); err != nil {
        return fi
    }
    return fi
}

//
// Proces list of file infos to search re in files
//
func processList(basePath string, list []os.FileInfo, re *regexp.Regexp, namesCh, dirCh, processedCh chan string) {
    for _, fileInfo := range list {
        if fileInfo.IsDir() {
            dirCh <- path.Join(basePath, fileInfo.Name())
        }
        if re.MatchString(fileInfo.Name()) {
            namesCh <- path.Join(basePath, fileInfo.Name())
        }
    }
    processedCh <- basePath
}
------------------------------

I tu zaczynamy używać kanałów.
Kanał namesCh przekazuje informacje o plikach których nazwy spełniają kryterium.
Do kanału dirCh zapisujemy ścieżkę do katalogu jeśli takowy znajdziemy.
Katalog processedCh informuje nas, że skończyliśmy przetwarzać katalog o nazwie basePath.

Poniżej zaś implementacja głównej procedury. Ta wersja gromadzi listę plików w splice. Zwraca pełną listę wyników.
Do monitorowania, które katalogi zostały już przetworzone służy mapa balancer. Kiedy  będzie ona pusta oznacza to, że nie ma więcej katalogów do przetworzenia.
------------------------------
func process(conf Config, namesCh, dirCh, processedCh chan string) []string {
    startPath := conf.StartPath
    re := conf.SearchNamePattern
    fi := processDir(startPath)
    balancer := make(map[string]bool, 10)
    var fileList []string
    go processList(startPath, fi, re, namesCh, dirCh, processedCh)
    balancer[startPath] = true
    for {
        if len(balancer) <= 0 {
            break
        }
        select {
        case dir := <-dirCh:
            fi := processDir(dir)
            go processList(dir, fi, re, namesCh, dirCh, processedCh)
            balancer[dir] = true
        case dir := <-processedCh:
            delete(balancer, dir)
        case fpath := <-namesCh:
            fileList = append(fileList, fpath)
        }
    }
    close(namesCh)
    return fileList
}
------------------------------

Dodajmy jeszcze główną funkcję, która będzie wyeksportowana w pakiecie.
------------------------------
//
// Func returns list of files which maches to patterns from conf
//
func Find(conf Config) []string {
    // Chanel to recive names of matching files
    var namesCh = make(chan string)
    // Chanel to recive names of dir names 
    var dirCh = make(chan string)
    // chanel to recive finished dirs
    var processedCh = make(chan string)
    files := process(conf, namesCh, dirCh, processedCh)
    return files
}
------------------------------

A teraz pomyślmy dlaczego nasz program który korzysta z naszej biblioteki musi czekać aż skończymy przetwarzać wszystkie pliki? Czemu nie może przekazać do funkcji kanału i pobierać z niego pasujące pliki kiedy tylko zostaną one zapisane do kanału? Ależ oczywiście że może. Co pokazuje poniższa implementacja.

------------------------------
//
// Function gets channel as parameters. Sends to outCh files
// which matches to patters from conf
func GoFind(conf Config, outCh chan string) {
    // Chanel to recive names of dir names 
    var dirCh = make(chan string)
    // chanel to recive finished dirs
    var processedCh = make(chan string)

    goprocess(conf, outCh, dirCh, processedCh)

}

//
//
//
func goprocess(conf Config, namesCh, dirCh, processedCh chan string) {
    startPath := conf.StartPath
    re := conf.SearchNamePattern
    fi := processDir(startPath)
    balancer := make(map[string]bool, 10)
    go processList(startPath, fi, re, namesCh, dirCh, processedCh)
    balancer[startPath] = true
    for {
        if len(balancer) <= 0 {
            break
        }
        select {
        case dir := <-dirCh:
            fi := processDir(dir)
            go processList(dir, fi, re, namesCh, dirCh, processedCh)
            balancer[dir] = true
        case dir := <-processedCh:
            delete(balancer, dir)
        }
    }
    close(namesCh)
}
------------------------------