T
for normal unit tests and B
for load tests. Tests in Go are written in the same package as the main program, with the addition of the _test
suffix. Therefore, any private data structures available inside the package are also available inside the tests (it is also true that tests have a common global scope between them). When compiling the main program, test files are ignored.
git checkout step-4
func TestXxx(*testing.T)
func TestNewBin(t *testing.T) { now := time.Now().Unix() bin := NewBin() if assert.NotNil(t, bin) { assert.Equal(t, len(bin.Name), 6) assert.Equal(t, bin.RequestCount, 0) assert.Equal(t, bin.Created, bin.Updated) assert.True(t, bin.Created < (now+1)) assert.True(t, bin.Created > (now-1)) } }
type MockedStorage struct{ mock.Mock } func (s *MockedStorage) CreateBin(_ *Bin) error { args := s.Mock.Called() return args.Error(0) } func (s *MockedStorage) UpdateBin(bin *Bin) error { args := s.Mock.Called(bin) return args.Error(0) } func (s *MockedStorage) LookupBin(name string) (*Bin, error) { args := s.Mock.Called(name) return args.Get(0).(*Bin), args.Error(1) } func (s *MockedStorage) LookupBins(names []string) ([]*Bin, error) { args := s.Mock.Called(names) return args.Get(0).([]*Bin), args.Error(1) } func (s *MockedStorage) LookupRequest(binName, id string) (*Request, error) { args := s.Mock.Called(binName, id) return args.Get(0).(*Request), args.Error(1) } func (s *MockedStorage) CreateRequest(bin *Bin, req *Request) error { args := s.Mock.Called(bin) return args.Error(0) } func (s *MockedStorage) LookupRequests(binName string, from, to int) ([]*Request, error) { args := s.Mock.Called(binName, from, to) return args.Get(0).([]*Request), args.Error(1) }
req, _ := http.NewRequest("GET", "/api/v1/bins/", nil) api = GetApi() mockedStorage := &MockedStorage{} api.MapTo(mockedStorage, (*Storage)(nil)) res = httptest.NewRecorder() mockedStorage.On("LookupBins", []string{}).Return([]*Bin(nil), errors.New("Storage error")) api.ServeHTTP(res, req) mockedStorage.AssertExpectations(t) if assert.Equal(t, res.Code, 500) { assert.Contains(t, res.Body.String(), "Storage error") }
s.Mock.Called(names)
method inside the mock method of an object, it tries to match the specified parameters and method name, and when we return args.Get (0), the first argument returned is returned. , in this case realBin. In addition to the Get method, which returns an object of type interface {}, there are helper methods Int, String, Bool, Error, which transform the interface into the type we need. The mockedStorage.AssertExpectations (t) method checks if all expected methods were called by us during testing.
> go test ./src/skimmer ok _/.../src/skimmer 0.032s
> go help testflag
> go test ./src/skimmer/ -coverprofile=c.out && go tool cover -html=c.out
> go get code.google.com/p/go.tools/cmd/cover
go tool
utility creates an html version that opens in the browser.
Coverage tests in Go, implemented quite interesting. Before compiling the code, the source files are changed, the counters are inserted into the source code. For example, this is the code:
func Size(a int) string { switch { case a < 0: return "negative" case a == 0: return "zero" } return "enormous" }
turns into this:
func Size(a int) string { GoCover.Count[0] = 1 switch { case a < 0: GoCover.Count[2] = 1 return "negative" case a == 0: GoCover.Count[3] = 1 return "zero" } GoCover.Count[1] = 1 return "enormous" }
It is also possible to show not just coverage, but how many times each section of code is being tested. As always, you can read more in the documentation .
> git checkout step-5
api.Get("**", func(r render.Render){ r.HTML(200, "index", nil) })
**
characters say that an index.html file will be displayed for any address. To work correctly with templates, we added options when creating a Renderer, indicating where to get templates. Plus, to avoid conflicts with angular templates, reassigned {{}} to {[{}]}.
api.Use(render.Renderer(render.Options{ Directory: "public/static/views", Extensions: []string{".html"}, Delims: render.Delims{"{[{", "}]}"}, }))
type Bin struct { ... Color [3]byte `json:"color"` Favicon string `json:"favicon"` } func NewBin() *Bin { color:= RandomColor() bin := Bin{ ... Color: color, Favicon: Solid16x16gifDatauri(color), } ... }
> go run ./src/main.go
127.0.0.1:3000
To play around.
> git checkout step-6
Gorilla is a set of tools for implementing web frameworks. All of these tools are weakly interconnected, which allows you to take any part and build it to yourself.
func GetApi(config *Config) *martini.ClassicMartini { ... store := sessions.NewCookieStore([]byte(config.SessionSecret)) ...
The NewCookieStore function accepts a pair of keys as parameters, the first key in the pair is needed for authentication, and the second for encryption. The second key can be skipped. To be able to rotate keys without losing sessions, you can use several pairs of keys. When creating a session, the keys of the first pair will be used, but when checking data, all keys are used in order, starting with the first pair.
// Sessions is a Middleware that maps a session.Session service into the Martini handler chain. // Sessions can use a number of storage solutions with the given store. func Sessions(name string, store Store) martini.Handler { return func(res http.ResponseWriter, r *http.Request, c martini.Context, l *log.Logger) { // Map to the Session interface s := &session{name, r, l, store, nil, false} c.MapTo(s, (*Session)(nil)) // Use before hook to save out the session rw := res.(martini.ResponseWriter) rw.Before(func(martini.ResponseWriter) { if s.Written() { check(s.Session().Save(r, res), l) } }) ... c.Next() } }
type History interface { All() []string Add(string) } type SessionHistory struct { size int name string session sessions.Session data []string } func (history *SessionHistory) All() []string { if history.data == nil { history.load() } return history.data } func (history *SessionHistory) Add(name string) { if history.data == nil { history.load() } history.data = append(history.data, "") copy(history.data[1:], history.data) history.data[0] = name history.save() } func (history *SessionHistory) save() { size := history.size if size > len(history.data){ size = len(history.data) } history.session.Set(history.name, history.data[:size]) } func (history *SessionHistory) load() { sessionValue := history.session.Get(history.name) history.data = []string{} if sessionValue != nil { if values, ok := sessionValue.([]string); ok { history.data = append(history.data, values...) } } } func NewSessionHistoryHandler(size int, name string) martini.Handler { return func(c martini.Context, session sessions.Session) { history := &SessionHistory{size: size, name: name, session: session} c.MapTo(history, (*History)(nil)) } }
type Bin struct { ... Private bool `json:"private"` SecretKey string `json:"-"` }
func (bin *Bin) SetPrivate() { bin.Private = true bin.SecretKey = rs.Generate(32) }
func DecodeJsonPayload(r *http.Request, v interface{}) error { content, err := ioutil.ReadAll(r.Body) r.Body.Close() if err != nil { return err } err = json.Unmarshal(content, v) if err != nil { return err } return nil }
api.Post("/api/v1/bins/", func(r render.Render, storage Storage, history History, session sessions.Session, req *http.Request){ payload := Bin{} if err := DecodeJsonPayload(req, &payload); err != nil { r.JSON(400, ErrorMsg{fmt.Sprintf("Decoding payload error: %s", err)}) return } bin := NewBin() if payload.Private { bin.SetPrivate() } if err := storage.CreateBin(bin); err == nil { history.Add(bin.Name) if bin.Private { session.Set(fmt.Sprintf("pr_%s", bin.Name), bin.SecretKey) } r.JSON(http.StatusCreated, bin) } else { r.JSON(http.StatusInternalServerError, ErrorMsg{err.Error()}) } })
session.Set(fmt.Sprintf("pr_%s", bin.Name), bin.SecretKey)
. Now we need to change other API methods so that they check for the existence of a key in a session for private Bin objects.
api.Get("/api/v1/bins/:bin", func(r render.Render, params martini.Params, session sessions.Session, storage Storage){ if bin, err := storage.LookupBin(params["bin"]); err == nil{ if bin.Private && bin.SecretKey != session.Get(fmt.Sprintf("pr_%s", bin.Name)){ r.JSON(http.StatusForbidden, ErrorMsg{"The bin is private"}) } else { r.JSON(http.StatusOK, bin) } } else { r.JSON(http.StatusNotFound, ErrorMsg{err.Error()}) } })
git checkout step-7
type BaseStorage struct { ... binLifetime int64 }
func (storage *MemoryStorage) clean() { storage.Lock() defer storage.Unlock() now := time.Now().Unix() for name, binRecord := range storage.binRecords { if binRecord.bin.Updated < (now - storage.binLifetime) { delete(storage.binRecords, name) } } }
type MemoryStorage struct { ... cleanTimer *time.Timer } func (storage *MemoryStorage) StartCleaning(timeout int) { defer func(){ storage.cleanTimer = time.AfterFunc(time.Duration(timeout) * time.Second, func(){storage.StartCleaning(timeout)}) }() storage.clean() } func (storage *MemoryStorage) StopCleaning() { if storage.cleanTimer != nil { storage.cleanTimer.Stop() } }
git checkout step-8
type RedisStorage struct { BaseStorage pool *redis.Pool prefix string cleanTimer *time.Timer }
func getPool(server string, password string) (pool *redis.Pool) { pool = &redis.Pool{ MaxIdle: 3, IdleTimeout: 240 * time.Second, Dial: func() (redis.Conn, error) { c, err := redis.Dial("tcp", server) if err != nil { return nil, err } if password != "" { if _, err := c.Do("AUTH", password); err != nil { c.Close() return nil, err } } return c, err }, TestOnBorrow: func(c redis.Conn, _ time.Time) error { _, err := c.Do("PING") return err }, } return pool }
const ( KEY_SEPARATOR = "|" // BIN_KEY = "bins" // Bin REQUESTS_KEY = "rq" // REQUEST_HASH_KEY = "rhsh" // CLEANING_SET = "cln" // , Bin CLEANING_FACTOR = 3 // )
func (storage *RedisStorage) getKey(keys ...string) string { return fmt.Sprintf("%s%s%s", storage.prefix, KEY_SEPARATOR, strings.Join(keys, KEY_SEPARATOR)) }
func (storage *RedisStorage) Dump(v interface{}) (data []byte, err error) { var ( mh codec.MsgpackHandle h = &mh ) err = codec.NewEncoderBytes(&data, h).Encode(v) return } func (storage *RedisStorage) Load(data []byte, v interface{}) error { var ( mh codec.MsgpackHandle h = &mh ) return codec.NewDecoderBytes(data, h).Decode(v) }
func (storage *RedisStorage) UpdateBin(bin *Bin) (err error) { dumpedBin, err := storage.Dump(bin) if err != nil { return } conn := storage.pool.Get() defer conn.Close() key := storage.getKey(BIN_KEY, bin.Name) conn.Send("SET", key, dumpedBin) conn.Send("EXPIRE", key, storage.binLifetime) conn.Flush() return err } func (storage *RedisStorage) CreateBin(bin *Bin) error { if err := storage.UpdateBin(bin); err != nil { return err } return nil }
Redigo supports pipeline mode, we can add a command to the buffer via the Send method, send all data from the buffer using the Flush method and get the result in Receive. The Do command combines all three commands into one. You can also implement transactivity, read more in the redigo documentation .
func (storage *RedisStorage) LookupBin(name string) (bin *Bin, err error) { conn := storage.pool.Get() defer conn.Close() reply, err := redis.Bytes(conn.Do("GET", storage.getKey(BIN_KEY, name))) if err != nil { if err == redis.ErrNil { err = errors.New("Bin was not found") } return } err = storage.Load(reply, &bin) return }
func (storage *RedisStorage) LookupBins(names []string) ([]*Bin, error) { bins := []*Bin{} if len(names) == 0 { return bins, nil } args := redis.Args{} for _, name := range names { args = args.Add(storage.getKey(BIN_KEY, name)) } conn := storage.pool.Get() defer conn.Close() if values, err := redis.Values(conn.Do("MGET", args...)); err == nil { bytes := [][]byte{} if err = redis.ScanSlice(values, &bytes); err != nil { return nil, err } for _, rawbin := range bytes { if len(rawbin) > 0 { bin := &Bin{} if err := storage.Load(rawbin, bin); err == nil { bins = append(bins, bin) } } } return bins, nil } else { return nil, err } }
func (storage *RedisStorage) CreateRequest(bin *Bin, req *Request) (err error) { data, err := storage.Dump(req) if err != nil { return } conn := storage.pool.Get() defer conn.Close() key := storage.getKey(REQUESTS_KEY, bin.Name) conn.Send("LPUSH", key, req.Id) conn.Send("EXPIRE", key, storage.binLifetime) key = storage.getKey(REQUEST_HASH_KEY, bin.Name) conn.Send("HSET", key, req.Id, data) conn.Send("EXPIRE", key, storage.binLifetime) conn.Flush() requestCount, err := redis.Int(conn.Receive()) if err != nil { return } if requestCount < storage.maxRequests { bin.RequestCount = requestCount } else { bin.RequestCount = storage.maxRequests } bin.Updated = time.Now().Unix() if requestCount > storage.maxRequests * CLEANING_FACTOR { conn.Do("SADD", storage.getKey(CLEANING_SET), bin.Name) } if err = storage.UpdateBin(bin); err != nil { return } return }
func (storage *RedisStorage) clean() { for { conn := storage.pool.Get() defer conn.Close() binName, err := redis.String(conn.Do("SPOP", storage.getKey(CLEANING_SET))) if err != nil { break } conn.Send("LRANGE", storage.getKey(REQUESTS_KEY, binName), storage.maxRequests, -1) conn.Send("LTRIM", storage.getKey(REQUESTS_KEY, binName), 0, storage.maxRequests-1) conn.Flush() if values, error := redis.Values(conn.Receive()); error == nil { ids := []string{} if err := redis.ScanSlice(values, &ids); err != nil { continue } if len(ids) > 0 { args := redis.Args{}.Add(storage.getKey(REQUEST_HASH_KEY, binName)).AddFlat(ids) conn.Do("HDEL", args...) } } } }
type RedisConfig struct { RedisAddr string RedisPassword string RedisPrefix string } type Config struct { ... Storage string RedisConfig } func GetApi(config *Config) *martini.ClassicMartini { var storage Storage switch config.Storage{ case "redis": redisStorage := NewRedisStorage(config.RedisAddr, config.RedisPassword, config.RedisPassword, MAX_REQUEST_COUNT, BIN_LIFETIME) redisStorage.StartCleaning(60) storage = redisStorage default: memoryStorage := NewMemoryStorage(MAX_REQUEST_COUNT, BIN_LIFETIME) memoryStorage.StartCleaning(60) storage = memoryStorage } ...
import ( "skimmer" "flag" ) var ( config = skimmer.Config{ SessionSecret: "secret123", RedisConfig: skimmer.RedisConfig{ RedisAddr: "127.0.0.1:6379", RedisPassword: "", RedisPrefix: "skimmer", }, } ) func init() { flag.StringVar(&config.Storage, "storage", "memory", "available storages: redis, memory") flag.StringVar(&config.SessionSecret, "sessionSecret", config.SessionSecret, "") flag.StringVar(&config.RedisAddr, "redisAddr", config.RedisAddr, "redis storage only") flag.StringVar(&config.RedisPassword, "redisPassword", config.RedisPassword, "redis storage only") flag.StringVar(&config.RedisPrefix, "redisPrefix", config.RedisPrefix, "redis storage only") } func main() { flag.Parse() api := skimmer.GetApi(&config) api.Run() }
The init function is special for Go, it is always executed when the package is loaded. Learn more about running programs in Go.
> go run ./src/main.go --help Usage of .../main: -redisAddr="127.0.0.1:6379": redis storage only -redisPassword="": redis storage only -redisPrefix="skimmer": redis storage only -sessionSecret="secret123": -storage="memory": available storages: redis, memory
Source: https://habr.com/ru/post/214425/