Obsah
1. Nová funkcionalita v Go 1.20: detekce skutečně volaných řádků v programovém kódu
2. Nástroj cover a jednotkové testy
3. Jednoduchá webová služba a její jednotkové testy
4. Zjištění pokrytí kódu jednotkovými testy
5. Výpis informací o pokrytí kódu jednotkovými testy
6. Vygenerování upraveného zdrojového kódu aplikace s explicitními testy pokrytí
7. Ukázka transformace kódu nástrojem cover
8. Od jednotkových testů k detekci řádků, které jsou spuštěny při reálném běhu aplikace
9. Složitější aplikace s přepínači zadávanými na příkazovém řádku
10. Projektový soubor testované aplikace
11. Překlad aplikace s přidáním „sledovacího“ kódu
12. Spuštění aplikace s uvedením několika přepínačů na příkazovém řádku (různé režimy činnosti)
13. Spojení a konverze automaticky vytvořených souborů s informacemi o činnosti aplikace
14. Výpis informací bězích aplikace na úrovni funkcí a metod
15. Vizualizace volaných a nevolaných řádků na webové stránce
17. Repositář s demonstračními příklady
1. Nová funkcionalita v Go 1.20: detekce skutečně volaných řádků v programovém kódu
V Go verze 1.20 se objevila poměrně dlouho očekávaná funkcionalita. Jedná se o relativně snadno využitelnou technologii umožňující detekci skutečně volaných řádků v programovém kódu. Díky této nové funkcionalitě je tedy možné například detekovat sémanticky „mrtvý“ kód, kód, jenž je použit jen ve specifických případech atd. Při bližším pohledu zjistíme, že se vlastně ve skutečnosti nejedná o zcela novou technologii, protože již v prvních verzích programovacího jazyka Go měli programátoři možnost zjistit pokrytí kódu testy, což ovšem byla technologie do značné míry svázaná s nástrojem go test a taktéž se standardním balíčkem testing. Použití mimo takto definované mantinely bylo velmi složité a mnohdy i nemožné.
Není tedy divu, že praktické využití této technologie bylo omezeno na jednotkové testy (unit tests) popř. pro testy komponent (ovšem za předpokladu, že testy komponent jsou založeny na výše zmíněném balíčku testing). S příchodem Go verze 1.20 je nově umožněno například spustit backend naprogramovaný v jazyce Go, provést několik operací na frontendu (UI) a následně zjistit, které části backendu byly v tomto konkrétním případě využity. Totéž je pochopitelně možné provést i v případě mikroslužeb (bez frontendu) atd. atd. – nyní se již žádné překážky nekladou, jak ostatně uvidíme v dalším textu.
2. Nástroj cover a jednotkové testy
Jak jsme si již řekli v úvodní kapitole, bylo zjištění, které části programového kódu jsou volány, v předchozích verzích jazyka Go omezeno na jednotkové testy, resp. přesněji řečeno na standardní balíček testing a na něj navazujícího nástroje cover. Pro úplnost si tedy v dalších kapitolách ukážeme dnes již zcela standardní způsob použití balíčku testing, a to včetně zobrazení (vizualizace) těch řádků v programovém kódu, které jsou jednotkovými testy přímo či nepřímo volány (tedy „zasaženy“). Jak uvidíme dále, je výsledkem této analýzy buď výstup ve formě seznamu funkcí a metod s procentuálním výpisem pokrytí testy, popř. HTML stránka (stránky) s podrobnějšími informacemi o tom, které programové řádky byly testy „zasaženy“ a které nikoli (samozřejmě se ignorují řádky bez spustitelného kódu).
3. Jednoduchá webová služba a její jednotkové testy
Nejprve si ukažme kód HTTP serveru, který budeme chtít testovat. Tento server po svém spuštění poskytuje statické soubory umístěné v aktuálním adresáři a na endpointech /data a /other odpovídá posláním odpovědi s nastaveným typem „application/json“. V obou případech je kód odpovědi 200 OK (a ve skutečnosti druhý handler nevrací validní JSON). Zdrojový kód tohoto HTTP serveru naleznete na adrese https://github.com/tisnik/go-root/blob/master/article_A5/httpServer/httpServer1.go
package main import ( "fmt" "log" "net/http" ) func dataHandler(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`) } func otherHandler(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `foobar`) } func startHttpServer(address string) { log.Printf("Starting server on address %s", address) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/data", dataHandler) http.HandleFunc("/other", otherHandler) http.ListenAndServe(address, nil) } func main() { startHttpServer(":8080") }
Typickou úlohou je otestování funkcionality jednotlivých handlerů. Realizaci si ukážeme na jednoduchém testu pro handler obsluhující endpoint /data. Nejdříve vytvoříme objekt realizující dotaz provedený HTTP metodou GET:
request, err := http.NewRequest("GET", "/data", nil) if err != nil { t.Fatal(err) }
Dále vytvoříme objekt, který bude zaznamenávat provedené operace:
recorder := httptest.NewRecorder()
Třetím a posledním objektem je adaptér umožňující použít libovolnou funkci s příslušnou signaturou jako handler HTTP serveru:
handler := http.HandlerFunc(dataHandler)
Nyní spustíme „záznam“ činnosti HTTP serveru pro již dříve vytvořený dotaz (HTTP GET na endpointu /data):
handler.ServeHTTP(recorder, request)
Celý průběh se zaznamená, což znamená, že později můžeme činnost handleru prozkoumat čtením atributů struktury recorder.
Otestování HTTP kódu odpovědi (očekáváme 200 OK):
if status := recorder.Code; status != http.StatusOK { t.Errorf("improper status code: got %v instead of %v", status, http.StatusOK) }
Otestování, zda odpověď obsahuje hlavičku „Content-Type“ s očekávaným obsahem „application/json“:
if ctype := recorder.Header().Get("Content-Type"); ctype != "application/json" { t.Errorf("content type header does not match: got %s want %s", ctype, "application/json") }
A pochopitelně můžeme přistupovat i k datům poslaným v těle odpovědi:
body := recorder.Body.String() if body != `"x": [1, 2, 3, 4, 5]` { t.Errorf("wrong response body: %s", body) }
Úplný zdrojový kód jednotkového testu je umístěn na adrese: https://github.com/tisnik/go-root/blob/master/article_A5/httpServer/httpServer1_test.go
package main import ( "net/http" "net/http/httptest" "testing" ) func TestDataHandler(t *testing.T) { request, err := http.NewRequest("GET", "/data", nil) if err != nil { t.Fatal(err) } recorder := httptest.NewRecorder() handler := http.HandlerFunc(dataHandler) handler.ServeHTTP(recorder, request) if status := recorder.Code; status != http.StatusOK { t.Errorf("improper status code: got %v instead of %v", status, http.StatusOK) } body := recorder.Body.String() if body != `"x": [1, 2, 3, 4, 5]` { t.Errorf("wrong response body: %s", body) } if ctype := recorder.Header().Get("Content-Type"); ctype != "application/json" { t.Errorf("content type header does not match: got %s want %s", ctype, "application/json") } }
4. Zjištění pokrytí kódu jednotkovými testy
Jednotkové testy pro naši implementaci služby spustíme příkazem go test:
$ go help test usage: go test [build/test flags] [packages] [build/test flags & test binary flags] 'Go test' automates testing the packages named by the import paths. It prints a summary of the test results in the format: ok archive/tar 0.011s FAIL archive/zip 0.022s ok compress/gzip 0.033s ... followed by detailed output for each failed package. ... ... ...
Ovšem navíc budeme specifikovat, že je nutné zjistit pokrytí kódu testy a současně uložit naměřená data do souboru nazvaného „coverage.out“. K oběma zmíněným účelům slouží přepínač -coverprofile:
$ go test -coverprofile coverage.out
Výsledky budou vypadat následovně:
PASS coverage: 25.0% of statements ok _/home/ptisnovs/src/go-root/article_A5/httpServer 0.003s
Soubor „coverage.out“ obsahuje informace o spuštěných programových řádcích v průběhu testů; současně se ovšem jedná o soubor, který nebudeme přímo dekódovat, ale použijeme na to k tomu určené nástroje zmíněné v navazující kapitole:
mode: set /home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:18.69,22.2 3 1 /home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:24.70,28.2 3 0 /home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:30.38,36.2 5 0 /home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:38.13,40.2 1 0
5. Výpis informací o pokrytí kódu jednotkovými testy
V této kapitole využijeme standardní nástroj cover, jenž se volá příkazem go tool cover a který akceptuje různé parametry vypsané v nápovědě:
$ go tool cover Usage of 'go tool cover': Given a coverage profile produced by 'go test': go test -coverprofile=c.out Open a web browser displaying annotated source code: go tool cover -html=c.out Write out an HTML file instead of launching a web browser: go tool cover -html=c.out -o coverage.html Display coverage percentages to stdout for each function: go tool cover -func=c.out Finally, to generate modified source code with coverage annotations (what go test -cover does): go tool cover -mode=set -var=CoverageVariableName program.go Flags: -V print version and exit -func string output coverage profile information for each function -html string generate HTML representation of coverage profile -mode string coverage mode: set, count, atomic -o string file for output; default: stdout -var string name of coverage variable to generate (default "GoCover") Only one of -html, -func, or -mode may be set.
Z vytvořeného souboru „coverage.out“ vytvoříme čitelný výpis s informacemi o tom, jaké funkce HTTP serveru byly skutečně otestovány:
$ go tool cover -func=coverage.out
Výsledek by mohl vypadat následovně (cesty se samozřejmě budou ve vašem případě odlišovat, ovšem čísla řádků, jména funkcí a konkrétní naměřené hodnoty budou shodné):
/home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:18: dataHandler 100.0% /home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:24: otherHandler 0.0% /home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:30: startHttpServer 0.0% /home/ptisnovs/src/go-root/article_A5/httpServer/httpServer1.go:38: main 0.0% total: (statements) 25.0%
Vidíme, že handler realizovaný funkcí dataHandler je skutečně plně pokryt testy, na rozdíl od ostatního programového kódu.
Navíc si můžeme nechat zobrazit HTML stránku (stránky), na nichž bude zvýrazněn kód pokrytý testy, ostatní programový kód a řádky, které programový kód netvoří:
$ go tool cover -html=coverage.out
S tímto výsledkem:
Obrázek 1: Vizualizace pokrytí programového kódu jednotkovými testy.
6. Vygenerování upraveného zdrojového kódu aplikace s explicitními testy pokrytí
Nástroj cover navíc dokáže transformovat zvolený zdrojový kód takovým způsobem, že do něj přidá nový kód testující vstup do zvolené funkce, metody nebo programového bloku. Tento nový kód používá strukturu se jménem specifikovaným na příkazovém řádku, přičemž nový kód může pouze nastavovat příznak přístupu do funkce nebo programového bloku (tedy „vstoupilo se/nevstoupilo se“) nebo může příslušný čítač zvýšit o jedničku (tedy můžeme testovat, kolikrát byla funkce nebo programový blok volán) nebo je zvýšení hodnoty čítače realizováno atomickou operací (použití v gorutinách atd.). Pro generování transformovaného kódu jsou k dispozici tyto přepínače předávané nástroji cover:
Přepínač | Stručný popis |
---|---|
-mode=set | ve vygenerované části kódu se nastavuje příznak vstupu do funkce/bloku |
-mode=count | ve vygenerované části kódu se zvyšuje hodnota čítače vstupu do funkce/bloku |
-mode=atomic | podobné předchozímu, ovšem čítače jsou realizovány atomicky měnitelnými hodnotami |
-var=xyz | jméno datové struktury s čítači |
7. Ukázka transformace kódu nástrojem cover
Podívejme se pro zajímavost, jak vlastně vypadají zdrojové kódy transformované nástrojem cover. Nejdříve si necháme vytvořit kód se „sledovací“ strukturou, jejíž prvky se nastavují na 0 nebo na 1, podle toho, zda je nějaký blok kódu volán či nikoli. Tento kód se vytvoří příkazem:
$ go tool cover -mode=set -var=coverageVariable httpServer1.go
Výsledkem bude následující kód, jehož upravené (přidané) části jsou zvýrazněny:
package main import ( "fmt" "log" "net/http" ) func dataHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[0] = 1; writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`) } func otherHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[1] = 1; writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `foobar`) } func startHttpServer(address string) {coverageVariable.Count[2] = 1; log.Printf("Starting server on address %s", address) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/data", dataHandler) http.HandleFunc("/other", otherHandler) http.ListenAndServe(address, nil) } func main() {coverageVariable.Count[3] = 1; startHttpServer(":8080") } var coverageVariable = struct { Count [4]uint32 Pos [3 * 4]uint32 NumStmt [4]uint16 } { Pos: [3 * 4]uint32{ 18, 22, 0x20045, // [0] 24, 28, 0x20046, // [1] 30, 36, 0x20026, // [2] 38, 40, 0x2000d, // [3] }, NumStmt: [4]uint16{ 3, // 0 3, // 1 5, // 2 1, // 3 }, }
Druhý zdrojový kód byl získán přidáním čítačů do původního kódu. Pro jeho vygenerování použijeme tento příkaz:
$ go tool cover -mode=count -var=coverageVariable httpServer1.go
Výsledek je podobný předchozím výsledku, až na odlišné chování – při vstupu do testovaných funkcí se hodnota čítačů zvyšuje o jedničku a nikoli přímo nastavuje na jedničku, takže lze snadno získat přehled o počtu volání:
package main import ( "fmt" "log" "net/http" ) func dataHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[0]++; writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`) } func otherHandler(writer http.ResponseWriter, request *http.Request) {coverageVariable.Count[1]++; writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `foobar`) } func startHttpServer(address string) {coverageVariable.Count[2]++; log.Printf("Starting server on address %s", address) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/data", dataHandler) http.HandleFunc("/other", otherHandler) http.ListenAndServe(address, nil) } func main() {coverageVariable.Count[3]++; startHttpServer(":8080") } var coverageVariable = struct { Count [4]uint32 Pos [3 * 4]uint32 NumStmt [4]uint16 } { Pos: [3 * 4]uint32{ 18, 22, 0x20045, // [0] 24, 28, 0x20046, // [1] 30, 36, 0x20026, // [2] 38, 40, 0x2000d, // [3] }, NumStmt: [4]uint16{ 3, // 0 3, // 1 5, // 2 1, // 3 }, }
A konečně poslední příklad ukazuje, jak se namísto běžných čítačů (tj. prvků typu uint32) používají atomické čítače. Kód tedy bude připraven pro běh ve více gorutinách:
$ go tool cover -mode=atomic -var=coverageVariable httpServer1.go
Podívejme se na výsledek:
package main; import _cover_atomic_ "sync/atomic" import ( "fmt" "log" "net/http" ) func dataHandler(writer http.ResponseWriter, request *http.Request) {_cover_atomic_.AddUint32(&coverageVariable.Count[0], 1); writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `"x": [1, 2, 3, 4, 5]`) } func otherHandler(writer http.ResponseWriter, request *http.Request) {_cover_atomic_.AddUint32(&coverageVariable.Count[1], 1); writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) fmt.Fprintf(writer, `foobar`) } func startHttpServer(address string) {_cover_atomic_.AddUint32(&coverageVariable.Count[2], 1); log.Printf("Starting server on address %s", address) http.Handle("/", http.FileServer(http.Dir("."))) http.HandleFunc("/data", dataHandler) http.HandleFunc("/other", otherHandler) http.ListenAndServe(address, nil) } func main() {_cover_atomic_.AddUint32(&camp;overageVariable.Count[3], 1); startHttpServer(":8080") } var coverageVariable = struct { Count [4]uint32 Pos [3 * 4]uint32 NumStmt [4]uint16 } { Pos: [3 * 4]uint32{ 18, 22, 0x20045, // [0] 24, 28, 0x20046, // [1] 30, 36, 0x20026, // [2] 38, 40, 0x2000d, // [3] }, NumStmt: [4]uint16{ 3, // 0 3, // 1 5, // 2 1, // 3 }, } var _ = _cover_atomic_.LoadUint32
8. Od jednotkových testů k detekci řádků, které jsou spuštěny při reálném běhu aplikace
V předchozím textu jsme si ukázali, jakým způsobem je možné zjistit pokrytí zdrojových kódů (code coverage) jednotkovými testy (unit tests) a taktéž to, že samotné zdrojové kódy je možné transformovat takovým způsobem, že se na důležitá místa v kódu (začátky funkcí a metod, vstupy do programových bloků) explicitně vloží nové příkazy určené pro zjištění, které části programu jsou vlastně použity (ovšem samotný export dat atd. je již ponechán na vývojáři). V jazyce Go verze 1.19 i ve všech starších variantách jazyka Go však nebyla k dispozici žádná jednoduše použitelná technologie, která by oba přístupy sjednotila tak, aby bylo možné aplikaci spustit (klidně i vícekrát) a posléze získat informaci o tom, do kterých funkcí/metod/bloků se skutečně vstoupilo. Tato technologie byla přidána až do Go 1.20 a způsob jejího využití si ukážeme na nepatrně složitějším demonstračním příkladu (který částečně vychází ze zdrojových kódů reálné aplikace).
9. Složitější aplikace s přepínači zadávanými na příkazovém řádku
Aplikace, kterou přeložíme, spustíme a budeme analyzovat v navazujících kapitolách, podporuje přepínače (flags) nastavované na příkazovém řádku. Tyto přepínače slouží pro spuštění některé části aplikace, popř. pro spuštění HTTP služby (samotná implementace této služby je vynechána – zbytečně by celý programový kód ještě více prodlužovala). Taktéž je možné, aby se aplikace pouze pokusila připojit k Apache Kafce atd. – jedná se tedy o aplikaci, která se po spuštění může ubírat několika větvemi. Ty se budeme snažit detekovat nástrojem cover:
package main import ( "flag" "fmt" "net/http" "os" "github.com/Shopify/sarama" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog/log" ) // Messages to be displayed on terminal or written into logs const ( versionMessage = "Spejbl version 1.0" authorsMessage = "Pavel Tisnovsky, Red Hat Inc." connectionToBrokerMessage = "Connection to broker" operationFailedMessage = "Operation failed" notConnectedToBrokerMessage = "Not connected to broker" brokerConnectionSuccessMessage = "Broker connection OK" ) // Exit codes const ( // ExitStatusOK means that the tool finished with success ExitStatusOK = iota // ExitStatusConsumerError is returned in case of any consumer-related error ExitStatusConsumerError // ExitStatusKafkaError is returned in case of any Kafka-related error ExitStatusKafkaError // ExitStatusHTTPServerError is returned in case the HTTP server can not be started ExitStatusHTTPServerError ) // CliFlags represents structure holding all command line arguments and flags. type CliFlags struct { CheckConnectionToKafka bool ShowVersion bool ShowAuthors bool } type ConfigStruct struct { // Address represents Kafka address Address string `mapstructure:"address" toml:"address"` // SecurityProtocol represents the security protocol used by the broker SecurityProtocol string `mapstructure:"security_protocol" toml:"security_protocol"` // CertPath is the path to a file containing the certificate to be used with the broker CertPath string `mapstructure:"cert_path" toml:"cert_path"` // SaslMechanism is the SASL mechanism used for authentication SaslMechanism string `mapstructure:"sasl_mechanism" toml:"sasl_mechanism"` // SaslUsername is the username used in case of PLAIN mechanism SaslUsername string `mapstructure:"sasl_username" toml:"sasl_username"` // SaslPassword is the password used in case of PLAIN mechanism SaslPassword string `mapstructure:"sasl_password" toml:"sasl_password"` // Topic is name of Kafka topic Topic string `mapstructure:"topic" toml:"topic"` // Group is name of Kafka group Group string `mapstructure:"group" toml:"group"` // Enabled is set to true if Kafka consumer is to be enabled Enabled bool `mapstructure:"enabled" toml:"enabled"` MetricsAddress string } // showVersion function displays version information to standard output. func showVersion() { fmt.Println(versionMessage) } // showAuthors function displays information about authors to standard output. func showAuthors() { fmt.Println(authorsMessage) } // tryToConnectToKafka function just tries to establish connection to Kafka // broker func tryToConnectToKafka(configuration *ConfigStruct) (int, error) { log.Info().Msg("Checking connection to Kafka") // display basic info about broker that will be used log.Info(). Str("broker address", configuration.Address). Msg("Broker address") // create new broker instance (w/o any checks) broker := sarama.NewBroker(configuration.Address) // check broker connection err := broker.Open(nil) if err != nil { log.Error().Err(err).Msg(connectionToBrokerMessage) return ExitStatusKafkaError, err } // check if connection remain connected, err := broker.Connected() if err != nil { log.Error().Err(err).Msg(connectionToBrokerMessage) return ExitStatusKafkaError, err } if !connected { log.Error().Err(err).Msg(notConnectedToBrokerMessage) return ExitStatusConsumerError, err } // connection was established log.Info().Msg(brokerConnectionSuccessMessage) // everything seems to be ok return ExitStatusOK, nil } // startService function tries to start the notification writer service, // connect to storage and initialize connection to message broker. func startService(configuration *ConfigStruct) (int, error) { // prepare HTTP server with metrics exposed err := startHTTPServer(configuration.MetricsAddress) if err != nil { log.Error().Err(err) return ExitStatusHTTPServerError, err } return ExitStatusOK, nil } // startHTTP server starts HTTP or HTTPS server with exposed metrics. func startHTTPServer(address string) error { // setup handlers http.Handle("/metrics", promhttp.Handler()) // start the server log.Info().Str("HTTP server address", address).Msg("Starting HTTP server") err := http.ListenAndServe(address, nil) // #nosec G114 if err != nil { log.Error().Err(err).Msg("Listen and serve") return err } return nil } func doSelectedOperation(configuration *ConfigStruct, cliFlags CliFlags) (int, error) { switch { case cliFlags.ShowVersion: showVersion() return ExitStatusOK, nil case cliFlags.ShowAuthors: showAuthors() return ExitStatusOK, nil case cliFlags.CheckConnectionToKafka: return tryToConnectToKafka(configuration) default: exitCode, err := startService(configuration) return exitCode, err } // this can not happen: return ExitStatusOK, nil } // main function is entry point to the Notification writer service. func main() { var cliFlags CliFlags // define and then parse all command line options flag.BoolVar(&cliFlags.CheckConnectionToKafka, "check-kafka", false, "check connection to Kafka") flag.BoolVar(&cliFlags.ShowVersion, "version", false, "show version") flag.BoolVar(&cliFlags.ShowAuthors, "authors", false, "show authors") flag.Parse() configuration := ConfigStruct{} // perform selected operation exitStatus, err := doSelectedOperation(&configuration, cliFlags) if err != nil { log.Err(err).Msg("Do selected operation") os.Exit(exitStatus) return } log.Debug().Msg("Finished") }
10. Projektový soubor testované aplikace
Pro úplnost se podívejme na to, jak vypadá projektový soubor testované aplikace, tedy soubor se jménem go.mod:
module app go 1.20 require ( github.com/Shopify/sarama v1.35.0 github.com/prometheus/client_golang v1.12.1 github.com/rs/zerolog v1.21.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/eapache/go-resiliency v1.3.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.0.0 // indirect github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/klauspost/compress v1.15.8 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect google.golang.org/protobuf v1.26.0 // indirect )
11. Překlad aplikace s přidáním „sledovacího“ kódu
Výše uvedenou aplikaci můžeme přeložit běžným způsobem; konkrétně tímto příkazem:
$ go build
Při použití Go 1.20 na architektuře x86–64 bude mít výsledný binární spustitelný soubor velikost 14 963 996 bajtů.
My ovšem budeme potřebovat, aby výsledný spustitelný program obsahoval i logiku pro zjištění těch částí kódu, které jsou skutečně volány. Proto budeme muset provést překlad tímto příkazem:
$ go build -cover
Výsledkem bude binární spustitelný soubor o velikosti 15 061 624 bajtů. Větší velikost je pochopitelná, protože nyní jsme vlastně do výsledné aplikace přidali další funkcionalitu (a to ne zcela triviální).
12. Spuštění aplikace s uvedením několika přepínačů na příkazovém řádku (různé režimy činnosti)
Nyní tedy máme k dispozici binární spustitelný kód aplikace, která dokáže po každém svém spuštění vygenerovat soubor s informacemi o tom, které bloky programového kódu byly skutečně volány. Ovšem export těchto informací je nejprve nutné nakonfigurovat, protože bez korektní konfigurace se aplikace sice bez problémů spustí, ale export dat neprovede (viz podtržené chybové hlášení):
$ ./app --help warning: GOCOVERDIR not set, no coverage data emitted Usage of ./app: -authors show authors -check-kafka check connection to Kafka -version show version
Ve skutečnosti je konfigurace velmi snadná. Nejdříve si vytvoříme nový podadresář, do kterého se budou ukládat soubory s informacemi o pokrytí:
$ mkdir coverage
Jméno tohoto adresáře uložíme do proměnné prostředí nazvané GOCOVERDIR:
$ export GOCOVERDIR=coverage/
Aplikaci spustíme; nejprve si necháme zobrazit nápovědu:
$ ./app --help Usage of ./app: -authors show authors -check-kafka check connection to Kafka -version show version
Dále si necháme vypsat informaci o verzi aplikace:
$ ./app -version Spejbl version 1.0 {"level":"debug","time":"2023-02-22T14:53:26+01:00","message":"Finished"}
A konečně se pokusíme o připojení k message brokeru, což se v tomto případě nepodaří (podle očekávání), protože broker v daném okamžiku na počítači neběžel:
$ ./app -check-kafka {"level":"info","time":"2023-02-22T14:53:44+01:00","message":"Checking connection to Kafka"} {"level":"info","broker address":"","time":"2023-02-22T14:53:44+01:00","message":"Broker address"} {"level":"error","error":"dial tcp: missing address","time":"2023-02-22T14:53:44+01:00","message":"Connection to broker"} {"level":"error","error":"dial tcp: missing address","time":"2023-02-22T14:53:44+01:00","message":"Do selected operation"}
13. Spojení a konverze automaticky vytvořených souborů s informacemi o činnosti aplikace
Vzhledem k tomu, že jsme testovanou aplikaci spustiti třikrát, měli bychom v podadresáři coverage najít minimálně tři soubory obsahující zjištěné informace o běhu aplikace. Tyto soubory začínají slovem covcounters. Navíc by se zde měl nacházet čtvrtý soubor s metainformacemi, jehož jméno začíná slovem covmeta. Na mém testovacím počítači se jedná o tuto čtveřici souborů (ve vašem případě se budou konkrétní jména odlišovat, to však vůbec nevadí):
$ ls -l coverage total 16 -rw-rw-r-- 1 ptisnovs ptisnovs 142 Feb 20 17:52 covcounters.810ac8e7733136bd7e57b4cac39e2180.2759513.1676911936041875369 -rw-rw-r-- 1 ptisnovs ptisnovs 154 Feb 20 17:52 covcounters.810ac8e7733136bd7e57b4cac39e2180.2759662.1676911945240574602 -rw-rw-r-- 1 ptisnovs ptisnovs 164 Feb 20 17:52 covcounters.810ac8e7733136bd7e57b4cac39e2180.2759853.1676911951857842965 -rw-rw-r-- 1 ptisnovs ptisnovs 426 Feb 20 17:52 covmeta.810ac8e7733136bd7e57b4cac39e2180
Nyní musíme provést dvojí konverzi – spojení souborů covcounters do souboru jediného a konverze výsledku do textového souboru se (strojově zpracovatelnými) informacemi o tom, které části kódu byly volány a které nikoli.
Spojení souborů do souboru jediného zajišťuje příkaz:
$ go tool covdata merge -i=coverage/ -o=.
Dojde k vytvoření souboru s názvem covcounters.810ac8e7733136bd7e57b4cac39e2180.0.1677074073986857547 (ovšem konkrétní jméno se může lišit).
Export dat do textového formátu provedeme příkazem:
$ go tool covdata textfmt -i=. -o=out.txt
Výsledkem činnosti těchto nástrojů by měl být soubor nazvaný out.txt, který obsahuje (strojově čitelné) informace o tom, které řádky původního programového kódu byly skutečně spuštěny. Obsah tohoto souboru si můžeme snadno vypsat – měl by vypadat takto:
$ cat out.txt mode: set app/app.go:69.20,71.2 1 1 app/app.go:74.20,76.2 1 0 app/app.go:80.68,93.16 5 1 app/app.go:93.16,96.3 2 0 app/app.go:99.2,100.16 2 1 app/app.go:100.16,103.3 2 1 app/app.go:104.2,104.16 1 0 app/app.go:104.16,107.3 2 0 app/app.go:110.2,113.26 2 0 app/app.go:118.61,121.16 2 0 app/app.go:121.16,124.3 2 0 app/app.go:126.2,126.26 1 0 app/app.go:130.44,137.16 4 0 app/app.go:137.16,140.3 2 0 app/app.go:141.2,141.12 1 0 app/app.go:144.87,145.9 1 1 app/app.go:146.28,148.27 2 1 app/app.go:149.28,151.27 2 0 app/app.go:152.39,153.44 1 1 app/app.go:154.10,156.23 2 0 app/app.go:162.13,175.16 8 1 app/app.go:175.16,179.3 3 1 app/app.go:181.2,181.29 1 1
14. Výpis informací bězích aplikace na úrovni funkcí a metod
Nyní již můžeme soubor out.txt, který byl vygenerován příkazy popsanými v předchozí kapitole, použít pro zobrazení informace o tom, které funkce a metody byly volány a kolik řádků z každé funkce/metody bylo spuštěno. Pro snazší porovnání je počet řádků uveden relativně vůči celkovému počtu řádků a hodnota je ze zlomku převedena na procenta.
Výsledek vypadá následovně:
$ go tool cover -func=out.txt app/app.go:69: showVersion 100.0% app/app.go:74: showAuthors 0.0% app/app.go:80: tryToConnectToKafka 56.2% app/app.go:118: startService 0.0% app/app.go:130: startHTTPServer 0.0% app/app.go:144: doSelectedOperation 50.0% app/app.go:162: main 100.0% total: (statements) 52.0%
Ze zobrazených výsledků je patrné, že pouze u dvou funkcí byly spuštěny všechny řádky, tři funkce nebyly zavolány vůbec a u dvou funkcí bylo spuštěna jen přibližně polovina příkazů.
15. Vizualizace volaných a nevolaných řádků na webové stránce
V případě, že nám výše uvedené výsledky nedostačují (například když je nutné přesně znát řádky, které byly spuštěny), je možné si nechat nástrojem cover spustit webový server, který v prohlížeči zobrazí podrobnější informace o analyzovaném zdrojovém kódu, a to právě až na úrovni jednotlivých programových řádků:
$ go tool cover -html=out.txt
Výsledky budou v našem konkrétním případě vypadat takto:
Obrázek 2: Funkce showVersion byla zavolána, zatímco funkce showAuthors nikoli.
Obrázek 3: Ve funkci tryToConnectToKafka se nepodařilo připojení k brokeru, takže byl z funkce proveden výskok ještě před jejím koncem.
Obrázek 4: Tyto funkce nebyly volány vůbec.
Obrázek 5: Rozeskok ve funkci doSelectedOperation a plné pokrytí hlavní funkce main.
16. Závěrečné zhodnocení
Rozšíření možností poskytovaných nástrojem cover je velmi užitečné. Například je možné zjistit, jak kvalitní a podrobné jsou integrační testy, testy komponent, nebo BDD testy (které navíc mohou být psány v jiném jazyce, neboť Go v tomto případě nemusí být ideálním řešením). Popř. je možné v testovacím nebo předprodukčním prostředí detekovat zcela mrtvé části kódu.
17. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/go-root. V případě, že nebudete chtít klonovat celý repositář, můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
18. Odkazy na Internetu
- Go 1.20 Release Notes
https://go.dev/doc/go1.20 - Go 1.20 Release Notes: Cover
https://go.dev/doc/go1.20#cover - Working with coverage data files
https://go.dev/testing/coverage/#working - Proposal: extend code coverage testing to include applications
https://github.com/golang/go/issues/51430 - Package trace
https://golang.org/pkg/runtime/trace/ - An Introduction to Benchmarking Your Go Programs
https://tutorialedge.net/golang/benchmarking-your-go-programs/ - How the Go runtime implements maps efficiently (without generics)
https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics - Go18DS (Go 1.18+ Data Structures)
https://github.com/daichi-m/go18ds - TreeMap v2
https://github.com/igrmk/treemap - Go Data Structures: Binary Search Tree
https://flaviocopes.com/golang-data-structure-binary-search-tree/ - Generics in Go
https://bitfieldconsulting.com/golang/generics - Tutorial: Getting started with generics
https://go.dev/doc/tutorial/generics - Gobs of data
https://blog.golang.org/gobs-of-data - Performance at Scale: MinIO Pushes Past 1.4 terabits per second with 256 NVMe Drives
https://blog.min.io/performance-at-scale-minio-pushes-past-1–3-terabits-per-second-with-256-nvme-drives/ - Benchmarking MinIO vs. AWS S3 for Apache Spark
https://blog.min.io/benchmarking-apache-spark-vs-aws-s3/ - Know Go: Generics (Kniha)
https://bitfieldconsulting.com/books/generics - Go 1.18 Generics based slice package
https://golangexample.com/go-1–18-generics-based-slice-package/ - Highly extensible Go source code linter providing checks currently missing from other linters
https://github.com/go-critic/go-critic - Fast linters runner for Go
https://github.com/golangci/golangci-lint - Checkers from the “performance” group
https://go-critic.com/overview#checkers-from-the-performance-group - rangeValCopy
https://go-critic.com/overview#rangeValCopy-ref - C vs Rust vs Go: performance analysis
https://medium.com/@marek.michalik/c-vs-rust-vs-go-performance-analysis-945ab749056c - Golang Performance Comparison | Why is GO Fast?
https://www.golinuxcloud.com/golang-performance/ - Go mutex vs channels benchmark
https://github.com/danil/go_mutex_vs_channels_benchmark - Techniques to Maximize Your Go Application’s Performance
https://golangdocs.com/techniques-to-maximize-your-go-applications-performance - Go language performance optimization
https://www.programmerall.com/article/8929467838/ - Ultimate Golang Performance Optimization Guide
https://www.bacancytechnology.com/blog/golang-performance - Optimizing a Golang service to reduce over 40% CPU
https://medium.com/coralogix-engineering/optimizing-a-golang-service-to-reduce-over-40-cpu-366b67c67ef9 - Tutorial for optimizing golang program
https://github.com/caibirdme/hand-to-hand-optimize-go/blob/master/README.md - How to optimise your Go code
https://codeburst.io/how-to-optimise-your-go-code-c6b27d4f1452