What's next once you have change management, unit testing, and code reviews? Bug tracking systems!
The bug tracking system is essentially a glorified TODO list.
The bug or ticket is a structured form with fields suitable for searching and reporting. Most systems are customizable so they can be adapted site-specific requirements.
Tracking ticket: ticket to hold links to several related issues.
A bug/ticket codifies a workflow
The most important part of the bug tracking system is triage: a periodic meeting to (re-)set priorities.
Include detail:
Ideally, testers will spend the time to characterize and narrow down the scope of the problem.
Additional advice:
It is important to encode some sort of version information into the executable program so that the production build can be traced back to the source for trouble-shooting.
To date, used a constant in the file containing the
main
function, which we had to remember to manually edit.
This is both tedious and error prone.
An alternate approach is to put the version string into a separate package:
package main
import (
"fmt"
"version"
)
func main() {
fmt.Printf("This is version \"%s\"\n", version.Version())
}
Then, we can writ a program to
generate
the
version
package. Like this:
package main
import {
"fmt"
"io/ioutil"
"os"
"os/user"
"strings"
"text/template"
"time"
)
type VersionInfo struct {
VersionNumber string
Time string
User string
Host string
}
const version = `{{define "version"}}package version
func Version() string {
return "{{.}}"
}
{{end}}`
func main() {
rawVersionNumber, err := ioutil.ReadFile("etc/version.txt")
if err != nil {
fmt.Fprintf(os.Stderr, "Reading version file")
os.Exit(1)
}
host, err := os.Hostname()
if err != nil {
fmt.Fprintf(os.Stderr, "Extracting hostname: %s\n", err)
os.Exit(1)
}
userInfo, err := user.Current()
if err != nil || userInfo == nil {
userInfo = &user.User{Name: "unknown"}
}
now := time.Now().Format(time.RFC3339Nano)
tmpl, err := template.New("version").Parse(version)
if err != nil {
fmt.Fprintf(os.Stderr, "Parsing token: %s\n", err)
os.Exit(1)
}
tmpl.ExecuteTemplate(os.Stdout, "version",
&VersionInfo{
VersionNumber: strings.TrimRight(string(rawVersionNumber), "\n"),
Time: now,
User: userInfo.Name,
Host: host,
})
}
Sample output:
package version
func Version() string {
return "{01.00.00 2015-02-01T14:50:02.679727009-08:00 morris yogi}"
}
The thread pool implementation presented in lecture 8 was refactored, extracting the inline function literals out to separate functions. Please visit the lecture notes to compare the two versions.
Channels are new and shiny. Their synchonization semantics provide an alternative to conventional mutexes. The general advice tends toward pragmatism: use whatever is simplest for the problem at hand.
The Go wiki offers more specific advice:
Our approach of guarding shared dictionaries and counters using a mutex is sound, but an alternative aproach is to have a single goroutine manage the data and communicate with it via a channel. This may sound like it will be bottleneck, but on closer examination, the bottleneck is no better or worse than using a mutual exclusion. The critical section is the same whether or not it runs in a separate thread.
Benchmarking experiments would have to be conducted to determine which implementation performs best.
While counters and maps are best protected by a mutex, here is an alternate counter implementation that uses a separate thread and communication via channels:
package counter
type request struct {
// response changgel for Get requests
resp chan int
// flag to determine which operation is requested: increment
// or get
increment bool
// variable name
key string
// amount to increment (or decrement if negative)
delta int
}
type Counter struct {
req chan request
}
// New creates a Counter object
// A counter is a map between name and value, where each value can be
// incremented atomically.
//
// There is also an unimplemented Dump function to return a copy of
// the dictionary (for exporting to another service..
func New() *Counter {
// A counter object just holds a (private) channel for
// communication with the goroutine service.
c := &Counter{
req: make(chan request),
}
// spawn off the service goroutine, passing the counter
// object to establish communication
go counter(c)
// And return the counter object.
return c
}
// Get returns the value of the given counter object. It provides a
// conventional interface, so the calling function does not need to
// know that this is implemented by communicating with another
// thread.
func (c *Counter) Get(key string) int {
// Create a channel to receive the response.
resp := make(chan int)
// Send a request to the service routine via the counter's
// request field.
c.req <- request{
// Create channel to receive response
resp: resp,
// Operation is get, not increment
increment: false,
// Variable to get value of.
key: key,
}
// And return the response when you get it.
return <-resp
}
// Incr increments (or decrements if delta is negative) a couunter.
// Again, we have a convtional interface that hides the fact that it
// is implemented by communication with another thread (goroutine).
func (c *Counter) Incr(key string, delta int) {
c.req <- request{
resp: nil,
increment: true,
key: key,
delta: delta,
}
}
// counter runs in its own goroutine to service Counter object
// requests.
func counter(c *Counter) {
// private variable to hold the map
data := make(map[string]int)
// range on the channel means we can exit the loop and hence
// terminate the thread by closing the channel
for req := range c.req {
// Service the request
if req.increment {
data[req.key] = data[req.key] + req.delta
} else {
req.resp <- data[req.key]
}
}
}
Here is the corresponding test routine:
package counter
import (
"sync"
"testing"
)
const (
// Symbolic constants to let the compiler find typos.
Zeus = "zeus"
Hera = "hera"
Ares = "ares"
Athena = "athena"
)
func body(c *Counter, t *testing.T) {
// Spawn off 4 concurrent threads and wait until they
// complete.
var wg sync.WaitGroup
wg.Add(4)
go func() {
c.Incr(Zeus, 2)
c.Incr(Hera, 1)
c.Incr(Athena, 1)
c.Incr(Ares, 1)
wg.Done()
}()
go func() {
c.Incr("hera", 21)
c.Incr("zeus", 6)
c.Incr("ares", 1)
c.Incr("athena", 4)
wg.Done()
}()
go func() {
c.Incr("zeus", 2)
c.Incr("hera", 6)
c.Incr("athena", 1)
c.Incr("ares", 1)
wg.Done()
}()
go func() {
c.Incr("athena", 2)
c.Incr("hera", 1)
c.Incr("zeus", 3)
c.Incr("ares", 1)
wg.Done()
}()
// sync.WaitGroups: wait until all 4 threads report Done.
// See the documentation.
wg.Wait()
expected := map[string]int{
"zeus": 13,
"hera": 29,
"ares": 4,
"athena": 8,
}
for k, v := range expected {
if v != c.Get(k) {
t.Errorf("counter %s: expected %d, got %d", k, v, c.Get(k))
}
}
}
func TestCounter(t *testing.T) {
c := New()
body(c, t)
}
The reason for maintaining counters via a map is so the set of counters may be dumped and passed to another service for further analysis. But it does make the interface a little clunky. Serviceable, but slightly clunky.
Here is an alternate implementation which preserve the ability to dump the map while providing a more convenient (and less error-prone) interface. Back to the mutex implementation.
package countervar
import "sync"
type Counter struct {
name string
value int
}
var (
symbolTable = make(map[string]*Counter)
mutex sync.RWMutex
)
// New returns a new counter variable (or the existing one of the
// given name if there is one already).
func New(name string) *Counter {
mutex.Lock()
defer mutex.Unlock()
// Get the existing counter if there is one; otherwise create
// a new counter. Note the idomatic use of map sematics:
// returns the zero value (nil for pointer) when the key is
// not present.
c := symbolTable[name]
if c == nil {
c = &Counter{
name: name,
value: 0,
}
symbolTable[name] = c
}
return c
}
// Dump returns a copy of the map of counter variables.
func Dump() map[string]int {
mutex.RLock()
defer mutex.RUnlock()
dump := make(map[string]int)
for k, v := range symbolTable {
dump[k] = v.value
}
return dump
}
func (c *Counter) Get() int {
mutex.RLock()
defer mutex.RUnlock()
return c.value
}
func (c *Counter) Incr(delta int) {
mutex.Lock()
defer mutex.Unlock()
c.value += delta
}
Here is the corresponding test routine. Compare how the counters are used versus the usage of the previous test.
package countervar
import (
"sync"
"testing"
)
func body(t *testing.T) {
zeus := New("zeus")
hera := New("hera")
athena := New("athena")
ares := New("ares")
var wg sync.WaitGroup
wg.Add(4)
go func() {
zeus.Incr(2)
hera.Incr(1)
athena.Incr(1)
ares.Incr(1)
wg.Done()
}()
go func() {
hera.Incr(21)
zeus.Incr(6)
ares.Incr(1)
athena.Incr(4)
wg.Done()
}()
go func() {
zeus.Incr(2)
hera.Incr(6)
athena.Incr(1)
ares.Incr(1)
wg.Done()
}()
go func() {
athena.Incr(2)
hera.Incr(1)
zeus.Incr(3)
ares.Incr(1)
wg.Done()
}()
wg.Wait()
expected := map[string]int{
"zeus": 13,
"hera": 29,
"ares": 4,
"athena": 8,
}
actual := Dump()
for k, v := range expected {
if v != actual[k] {
t.Errorf("counter %s: expected %d, got %d", k, v, actual[k])
}
}
}
func TestCounter(t *testing.T) {
body(t)
}