OSDN Git Service

chain's log and math library.
authorgguoss <1536310027@qq.com>
Tue, 18 Jul 2017 03:00:46 +0000 (11:00 +0800)
committergguoss <1536310027@qq.com>
Tue, 18 Jul 2017 03:00:46 +0000 (11:00 +0800)
log/log.go [new file with mode: 0644]
log/log_test.go [new file with mode: 0644]
log/rotation/file.go [new file with mode: 0644]
log/rotation/file_test.go [new file with mode: 0644]
log/splunk/splunk.go [new file with mode: 0644]
log/stack.go [new file with mode: 0644]
math/checked/checked.go [new file with mode: 0644]
math/checked/checked_test.go [new file with mode: 0644]

diff --git a/log/log.go b/log/log.go
new file mode 100644 (file)
index 0000000..fda1b64
--- /dev/null
@@ -0,0 +1,247 @@
+// Package log implements a standard convention for structured logging.
+// Log entries are formatted as K=V pairs.
+// By default, output is written to stdout; this can be changed with SetOutput.
+package log
+
+import (
+       "context"
+       "fmt"
+       "io"
+       "os"
+       "runtime"
+       "strconv"
+       "strings"
+       "sync"
+       "time"
+
+       "chain/errors"
+)
+
+const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
+
+// context key type
+type key int
+
+var (
+       logWriterMu sync.Mutex // protects the following
+       logWriter   io.Writer  = os.Stdout
+       procPrefix  []byte     // process-global prefix; see SetPrefix vs AddPrefixkv
+
+       // pairDelims contains a list of characters that may be used as delimeters
+       // between key-value pairs in a log entry. Keys and values will be quoted or
+       // otherwise formatted to ensure that key-value extraction is unambiguous.
+       //
+       // The list of pair delimiters follows Splunk conventions, described here:
+       // http://answers.splunk.com/answers/143368/default-delimiters-for-key-value-extraction.html
+       pairDelims      = " ,;|&\t\n\r"
+       illegalKeyChars = pairDelims + `="`
+
+       // context key for log line prefixes
+       prefixKey key = 0
+)
+
+// Conventional key names for log entries
+const (
+       KeyCaller = "at" // location of caller
+       KeyTime   = "t"  // time of call
+
+       KeyMessage = "message" // produced by Message
+       KeyError   = "error"   // produced by Error
+       KeyStack   = "stack"   // used by Printkv to print stack on subsequent lines
+
+       keyLogError = "log-error" // for errors produced by the log package itself
+)
+
+// SetOutput sets the log output to w.
+// If SetOutput hasn't been called,
+// the default behavior is to write to stdout.
+func SetOutput(w io.Writer) {
+       logWriterMu.Lock()
+       logWriter = w
+       logWriterMu.Unlock()
+}
+
+func appendPrefix(b []byte, keyval ...interface{}) []byte {
+       // Invariant: len(keyval) is always even.
+       if len(keyval)%2 != 0 {
+               panic(fmt.Sprintf("odd-length prefix args: %v", keyval))
+       }
+       for i := 0; i < len(keyval); i += 2 {
+               k := formatKey(keyval[i])
+               v := formatValue(keyval[i+1])
+               b = append(b, k...)
+               b = append(b, '=')
+               b = append(b, v...)
+               b = append(b, ' ')
+       }
+       return b
+}
+
+// SetPrefix sets the global output prefix.
+func SetPrefix(keyval ...interface{}) {
+       b := appendPrefix(nil, keyval...)
+       logWriterMu.Lock()
+       procPrefix = b
+       logWriterMu.Unlock()
+}
+
+// AddPrefixkv appends keyval to any prefix stored in ctx,
+// and returns a new context with the longer prefix.
+func AddPrefixkv(ctx context.Context, keyval ...interface{}) context.Context {
+       p := appendPrefix(prefix(ctx), keyval...)
+       // Note: subsequent calls will append to p, so set cap(p) here.
+       // See TestAddPrefixkvAppendTwice.
+       p = p[0:len(p):len(p)]
+       return context.WithValue(ctx, prefixKey, p)
+}
+
+func prefix(ctx context.Context) []byte {
+       b, _ := ctx.Value(prefixKey).([]byte)
+       return b
+}
+
+// Printkv prints a structured log entry to stdout. Log fields are
+// specified as a variadic sequence of alternating keys and values.
+//
+// Duplicate keys will be preserved.
+//
+// Two fields are automatically added to the log entry: t=[time]
+// and at=[file:line] indicating the location of the caller.
+// Use SkipFunc to prevent helper functions from showing up in the
+// at=[file:line] field.
+//
+// Printkv will also print the stack trace, if any, on separate lines
+// following the message. The stack is obtained from the following,
+// in order of preference:
+//   - a KeyStack value with type []byte or []errors.StackFrame
+//   - a KeyError value with type error, using the result of errors.Stack
+func Printkv(ctx context.Context, keyvals ...interface{}) {
+       // Invariant: len(keyvals) is always even.
+       if len(keyvals)%2 != 0 {
+               keyvals = append(keyvals, "", keyLogError, "odd number of log params")
+       }
+
+       t := time.Now().UTC()
+
+       // Prepend the log entry with auto-generated fields.
+       out := fmt.Sprintf(
+               "%s=%s %s=%s",
+               KeyCaller, caller(),
+               KeyTime, formatValue(t.Format(rfc3339NanoFixed)),
+       )
+
+       var stack interface{}
+       for i := 0; i < len(keyvals); i += 2 {
+               k := keyvals[i]
+               v := keyvals[i+1]
+               if k == KeyStack && isStackVal(v) {
+                       stack = v
+                       continue
+               }
+               if k == KeyError {
+                       if e, ok := v.(error); ok && stack == nil {
+                               stack = errors.Stack(errors.Wrap(e)) // wrap to ensure callstack
+                       }
+               }
+               out += " " + formatKey(k) + "=" + formatValue(v)
+       }
+
+       logWriterMu.Lock()
+       logWriter.Write(procPrefix)
+       logWriter.Write(prefix(ctx))
+       logWriter.Write([]byte(out)) // ignore errors
+       logWriter.Write([]byte{'\n'})
+       writeRawStack(logWriter, stack)
+       logWriterMu.Unlock()
+}
+
+// Fatalkv is equivalent to Printkv() followed by a call to os.Exit(1).
+func Fatalkv(ctx context.Context, keyvals ...interface{}) {
+       Printkv(ctx, keyvals...)
+       os.Exit(1)
+}
+
+func writeRawStack(w io.Writer, v interface{}) {
+       switch v := v.(type) {
+       case []byte:
+               if len(v) > 0 {
+                       w.Write(v)
+                       w.Write([]byte{'\n'})
+               }
+       case []errors.StackFrame:
+               for _, s := range v {
+                       io.WriteString(w, s.String())
+                       w.Write([]byte{'\n'})
+               }
+       }
+}
+
+func isStackVal(v interface{}) bool {
+       switch v.(type) {
+       case []byte:
+               return true
+       case []errors.StackFrame:
+               return true
+       }
+       return false
+}
+
+// Printf prints a log entry containing a message assigned to the
+// "message" key. Arguments are handled as in fmt.Printf.
+func Printf(ctx context.Context, format string, a ...interface{}) {
+       Printkv(ctx, KeyMessage, fmt.Sprintf(format, a...))
+}
+
+// Error prints a log entry containing an error message assigned to the
+// "error" key.
+// Optionally, an error message prefix can be included. Prefix arguments are
+// handled as in fmt.Print.
+func Error(ctx context.Context, err error, a ...interface{}) {
+       if len(a) > 0 && len(errors.Stack(err)) > 0 {
+               err = errors.Wrap(err, a...) // keep err's stack
+       } else if len(a) > 0 {
+               err = fmt.Errorf("%s: %s", fmt.Sprint(a...), err) // don't add a stack here
+       }
+       Printkv(ctx, KeyError, err)
+}
+
+// formatKey ensures that the stringified key is valid for use in a
+// Splunk-style K=V format. It stubs out delimeter and quoter characters in
+// the key string with hyphens.
+func formatKey(k interface{}) string {
+       s := fmt.Sprint(k)
+       if s == "" {
+               return "?"
+       }
+
+       for _, c := range illegalKeyChars {
+               s = strings.Replace(s, string(c), "-", -1)
+       }
+
+       return s
+}
+
+// formatValue ensures that the stringified value is valid for use in a
+// Splunk-style K=V format. It quotes the string value if delimeter or quoter
+// characters are present in the value string.
+func formatValue(v interface{}) string {
+       s := fmt.Sprint(v)
+       if strings.ContainsAny(s, pairDelims) {
+               return strconv.Quote(s)
+       }
+       return s
+}
+
+// RecoverAndLogError must be used inside a defer.
+func RecoverAndLogError(ctx context.Context) {
+       if err := recover(); err != nil {
+               const size = 64 << 10
+               buf := make([]byte, size)
+               buf = buf[:runtime.Stack(buf, false)]
+               Printkv(ctx,
+                       KeyMessage, "panic",
+                       KeyError, err,
+                       KeyStack, buf,
+               )
+       }
+}
diff --git a/log/log_test.go b/log/log_test.go
new file mode 100644 (file)
index 0000000..4c608a7
--- /dev/null
@@ -0,0 +1,365 @@
+package log
+
+import (
+       "bytes"
+       "context"
+       "io/ioutil"
+       "os"
+       "reflect"
+       "strings"
+       "testing"
+
+       "chain/errors"
+)
+
+func TestSetOutput(t *testing.T) {
+       var buf bytes.Buffer
+       want := "foobar"
+       SetOutput(&buf)
+       Printf(context.Background(), want)
+       SetOutput(os.Stdout)
+       got := buf.String()
+       if !strings.Contains(got, want) {
+               t.Errorf("log = %q; should contain %q", got, want)
+       }
+}
+
+func TestNoExtraFormatDirectives(t *testing.T) {
+       buf := new(bytes.Buffer)
+       SetOutput(buf)
+       SetPrefix("foo", "bar")
+       Printkv(context.Background(), "baz", 1)
+       SetOutput(os.Stdout)
+       got := buf.String()
+       if strings.Contains(got, "%") {
+               t.Errorf("log line appears to contain format directive: %q", got)
+       }
+}
+
+func TestPrefix(t *testing.T) {
+       buf := new(bytes.Buffer)
+       SetOutput(buf)
+       SetPrefix("foo", "bar")
+       Printkv(context.Background(), "baz", 1)
+       SetOutput(os.Stdout)
+
+       got := buf.String()
+       wantPrefix := "foo=bar "
+       if !strings.HasPrefix(got, wantPrefix) {
+               t.Errorf("output = %q want prefix %q", got, wantPrefix)
+       }
+
+       SetPrefix()
+       if procPrefix != nil {
+               t.Errorf("procPrefix = %q want nil", procPrefix)
+       }
+}
+
+func TestAddPrefixkv0(t *testing.T) {
+       got := prefix(context.Background())
+       var want []byte = nil
+       if !reflect.DeepEqual(got, want) {
+               t.Errorf(`prefix(context.Background()) = %#v want %#v`, got, want)
+       }
+}
+
+func TestAddPrefixkv1(t *testing.T) {
+       ctx := context.Background()
+       ctx1 := AddPrefixkv(ctx, "a", "b")
+       got := prefix(ctx1)
+       want := []byte("a=b ")
+       if !bytes.Equal(got, want) {
+               t.Errorf(`prefix(AddPrefixkv(bg, "a", "b")) = %q want %q`, got, want)
+       }
+}
+
+func TestAddPrefixkv2(t *testing.T) {
+       ctx := context.Background()
+       ctx1 := AddPrefixkv(ctx, "a", "b")
+       ctx2 := AddPrefixkv(ctx1, "c", "d")
+       got := prefix(ctx2)
+       want := []byte("a=b c=d ")
+       if !bytes.Equal(got, want) {
+               t.Errorf(`prefix(AddPrefixkv(AddPrefixkv(bg, "a", "b"), "c", "d")) = %q want %q`, got, want)
+       }
+}
+
+func TestAddPrefixkvAppendTwice(t *testing.T) {
+       ctx := context.Background()
+       ctx1 := AddPrefixkv(ctx, "a", "b")
+       ctx2a := AddPrefixkv(ctx1, "c", "d")
+       ctx2b := AddPrefixkv(ctx1, "e", "f")
+       gota := prefix(ctx2a)
+       wanta := []byte("a=b c=d ")
+       if !bytes.Equal(gota, wanta) {
+               t.Errorf(`prefix(AddPrefixkv(AddPrefixkv(bg, "a", "b"), "c", "d")) = %q want %q`, gota, wanta)
+       }
+       gotb := prefix(ctx2b)
+       wantb := []byte("a=b e=f ")
+       if !bytes.Equal(gotb, wantb) {
+               t.Errorf(`prefix(AddPrefixkv(AddPrefixkv(bg, "a", "b"), "e", "f")) = %q want %q`, gotb, wantb)
+       }
+}
+
+func TestPrintkv(t *testing.T) {
+       examples := []struct {
+               keyvals []interface{}
+               want    []string
+       }{
+               // Basic example
+               {
+                       keyvals: []interface{}{"msg", "hello world"},
+                       want: []string{
+                               "at=log_test.go:",
+                               "t=",
+                               `msg="hello world"`,
+                       },
+               },
+
+               // Duplicate keys
+               {
+                       keyvals: []interface{}{"msg", "hello world", "msg", "goodbye world"},
+                       want: []string{
+                               "at=log_test.go:",
+                               "t=",
+                               `msg="hello world"`,
+                               `msg="goodbye world"`,
+                       },
+               },
+
+               // Zero log params
+               {
+                       keyvals: nil,
+                       want: []string{
+                               "at=log_test.go:",
+                               "t=",
+                       },
+               },
+
+               // Odd number of log params
+               {
+                       keyvals: []interface{}{"k1", "v1", "k2"},
+                       want: []string{
+                               "at=log_test.go:",
+                               "t=",
+                               "k1=v1",
+                               "k2=",
+                               `log-error="odd number of log params"`,
+                       },
+               },
+       }
+
+       for i, ex := range examples {
+               t.Log("Example", i)
+
+               buf := new(bytes.Buffer)
+               SetOutput(buf)
+
+               Printkv(context.Background(), ex.keyvals...)
+
+               read, err := ioutil.ReadAll(buf)
+               if err != nil {
+                       SetOutput(os.Stdout)
+                       t.Fatal("read buffer error:", err)
+               }
+
+               got := string(read)
+
+               for _, w := range ex.want {
+                       if !strings.Contains(got, w) {
+                               t.Errorf("Result did not contain string:\ngot:  %s\nwant: %s", got, w)
+                       }
+               }
+               if !strings.HasSuffix(got, "\n") {
+                       t.Errorf("log output should end with a newline")
+               }
+
+               SetOutput(os.Stdout)
+       }
+}
+
+func TestMessagef(t *testing.T) {
+       buf := new(bytes.Buffer)
+       SetOutput(buf)
+       defer SetOutput(os.Stdout)
+
+       Printf(context.Background(), "test round %d", 0)
+
+       read, err := ioutil.ReadAll(buf)
+       if err != nil {
+               t.Fatal("read buffer error:", err)
+       }
+
+       got := string(read)
+       want := []string{
+               "at=log_test.go:",
+               `message="test round 0"`,
+       }
+
+       for _, w := range want {
+               if !strings.Contains(got, w) {
+                       t.Errorf("Result did not contain string:\ngot:  %s\nwant: %s", got, w)
+               }
+       }
+}
+
+func TestPrintkvStack(t *testing.T) {
+       buf := new(bytes.Buffer)
+       SetOutput(buf)
+       defer SetOutput(os.Stdout)
+
+       root := errors.New("boo")
+       wrapped := errors.Wrap(root)
+       Printkv(context.Background(), KeyError, wrapped)
+
+       read, err := ioutil.ReadAll(buf)
+       if err != nil {
+               t.Fatal("read buffer error:", err)
+       }
+
+       got := string(read)
+       want := []string{
+               "at=log_test.go:",
+               "error=boo",
+
+               // stack trace
+               "TestPrintkvStack\n",
+               "/go/",
+       }
+
+       t.Logf("output:\n%s", got)
+       for _, w := range want {
+               if !strings.Contains(got, w) {
+                       t.Errorf("output %q did not contain %q", got, w)
+               }
+       }
+}
+
+func TestError(t *testing.T) {
+       buf := new(bytes.Buffer)
+       SetOutput(buf)
+       defer SetOutput(os.Stdout)
+
+       root := errors.New("boo")
+       wrapped := errors.Wrap(root)
+       Error(context.Background(), wrapped, "failure x ", 0)
+
+       read, err := ioutil.ReadAll(buf)
+       if err != nil {
+               t.Fatal("read buffer error:", err)
+       }
+
+       got := string(read)
+       want := []string{
+               "at=log_test.go:",
+               `error="failure x 0: boo"`,
+
+               // stack trace
+               "TestError\n",
+               "/go/",
+       }
+
+       t.Logf("output:\n%s", got)
+       for _, w := range want {
+               if !strings.Contains(got, w) {
+                       t.Errorf("output %q did not contain %q", got, w)
+               }
+       }
+}
+
+func TestRawStack(t *testing.T) {
+       var buf bytes.Buffer
+       SetOutput(&buf)
+       defer SetOutput(os.Stdout)
+
+       stack := []byte("this\nis\na\nraw\nstack")
+       Printkv(context.Background(), "message", "foo", "stack", stack)
+
+       got := buf.String()
+       if !strings.HasSuffix(got, "\n"+string(stack)+"\n") {
+               t.Logf("output:\n%s", got)
+               t.Errorf("output did not contain %q", stack)
+       }
+}
+
+func TestIsStackVal(t *testing.T) {
+       cases := []struct {
+               v interface{}
+               w bool
+       }{
+               {[]byte("foo"), true},
+               {[]errors.StackFrame{}, true},
+               {"line1", false},
+               {[...]byte{'x'}, false},
+               {[]string{}, false},
+       }
+       for _, test := range cases {
+               if g := isStackVal(test.v); g != test.w {
+                       t.Errorf("isStackVal(%#v) = %v want %v", test.v, g, test.w)
+               }
+       }
+}
+
+func TestWriteRawStack(t *testing.T) {
+       cases := []struct {
+               v interface{}
+               w string
+       }{
+               {[]byte("foo\nbar"), "foo\nbar\n"},
+               {[]errors.StackFrame{{Func: "foo", File: "f.go", Line: 1}}, "f.go:1 - foo\n"},
+               {1, ""}, // int is not a valid stack val
+       }
+
+       for _, test := range cases {
+               var buf bytes.Buffer
+               writeRawStack(&buf, test.v)
+               if g := buf.String(); g != test.w {
+                       t.Errorf("writeRawStack(%#v) = %q want %q", test.v, g, test.w)
+               }
+       }
+}
+
+func TestFormatKey(t *testing.T) {
+       examples := []struct {
+               key  interface{}
+               want string
+       }{
+               {"hello", "hello"},
+               {"hello world", "hello-world"},
+               {"", "?"},
+               {true, "true"},
+               {"a b\"c\nd;e\tf龜g", "a-b-c-d-e-f龜g"},
+       }
+
+       for i, ex := range examples {
+               t.Log("Example", i)
+               got := formatKey(ex.key)
+               if got != ex.want {
+                       t.Errorf("formatKey(%#v) = %q want %q", ex.key, got, ex.want)
+               }
+       }
+}
+
+func TestFormatValue(t *testing.T) {
+       examples := []struct {
+               value interface{}
+               want  string
+       }{
+               {"hello", "hello"},
+               {"hello world", `"hello world"`},
+               {1.5, "1.5"},
+               {true, "true"},
+               {errors.New("this is an error"), `"this is an error"`},
+               {[]byte{'a', 'b', 'c'}, `"[97 98 99]"`},
+               {bytes.NewBuffer([]byte{'a', 'b', 'c'}), "abc"},
+               {"a b\"c\nd;e\tf龜g", `"a b\"c\nd;e\tf龜g"`},
+       }
+
+       for i, ex := range examples {
+               t.Log("Example", i)
+               got := formatValue(ex.value)
+               if got != ex.want {
+                       t.Errorf("formatKey(%#v) = %q want %q", ex.value, got, ex.want)
+               }
+       }
+}
diff --git a/log/rotation/file.go b/log/rotation/file.go
new file mode 100644 (file)
index 0000000..4e14a10
--- /dev/null
@@ -0,0 +1,109 @@
+// Package rotation writes and rotates log files.
+package rotation
+
+import (
+       "bytes"
+       "os"
+       "strconv"
+)
+
+// A File is a log file with associated rotation files.
+// The rotation files are named after the base file
+// with a numeric suffix: base.1, base.2, and so on.
+// Calls to Write write data to the base file.
+// When the base file reaches the given size,
+// it is renamed to base.1
+// (and base.1 is renamed to base.2, and so on)
+// and a new base file is opened for subsequent writes.
+//
+// Any errors encountered while rotating files are ignored.
+// Only errors opening and writing the base file are reported.
+type File struct {
+       base string   // file name
+       size int64    // max size of f (limit on w)
+       n    int      // number of rotated files
+       buf  []byte   // partial line from last write
+       f    *os.File // current base file
+       w    int64    // bytes written to f
+}
+
+// Create creates a log writing to the named file
+// with mode 0644 (before umask),
+// appending to it if it already exists.
+// It will rotate to files name.1, name.2,
+// up to name.n.
+// The minimum value for n is 1;
+// lesser values will be taken as 1.
+func Create(name string, size, n int) *File {
+       return &File{
+               base: name,
+               size: int64(size),
+               n:    n,
+       }
+}
+
+var dropmsg = []byte("\nlog write error; some data dropped\n")
+
+// Write writes p to the log file f.
+// It writes only complete lines to the underlying file.
+// Incomplete lines are buffered in memory
+// and written once a NL is encountered.
+func (f *File) Write(p []byte) (n int, err error) {
+       f.buf = append(f.buf, p...)
+       n = len(p)
+       if i := bytes.LastIndexByte(f.buf, '\n'); i >= 0 {
+               _, err = f.write(f.buf[:i+1])
+               // Even if the write failed, discard the entire
+               // requested write payload. If we kept it around,
+               // then a failure to open the log file would
+               // cause us to accumulate data in memory
+               // without bound.
+               f.buf = f.buf[i+1:]
+               if err != nil {
+                       // If we recover and resume logging,
+                       // leave a message to indicate we dropped
+                       // some lines.
+                       f.buf = append(dropmsg, f.buf...)
+               }
+       }
+       return
+}
+
+// write writes the given data to f,
+// rotating files if necessary.
+func (f *File) write(p []byte) (int, error) {
+       // If p would increase the file over the
+       // max size, it is time to rotate.
+       if f.w+int64(len(p)) > f.size {
+               // best-effort; ignore errors
+               f.rotate()
+               f.f.Close()
+               f.f = nil
+               f.w = 0
+       }
+       if f.f == nil {
+               var err error
+               f.f, err = os.OpenFile(f.base, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) // #nosec
+               if err != nil {
+                       return 0, err
+               }
+               f.w, err = f.f.Seek(0, os.SEEK_END)
+               if err != nil {
+                       return 0, err
+               }
+       }
+       n, err := f.f.Write(p)
+       f.w += int64(n)
+       return n, err
+}
+
+func (f *File) rotate() {
+       for i := f.n - 1; i > 0; i-- {
+               os.Rename(f.name(i), f.name(i+1))
+       }
+       os.Rename(f.base, f.name(1))
+}
+
+func (f *File) name(i int) string {
+       return f.base + "." + strconv.Itoa(i)
+}
diff --git a/log/rotation/file_test.go b/log/rotation/file_test.go
new file mode 100644 (file)
index 0000000..b751c2b
--- /dev/null
@@ -0,0 +1,244 @@
+package rotation
+
+import (
+       "bytes"
+       "io/ioutil"
+       "os"
+       "testing"
+)
+
+func TestRotate(t *testing.T) {
+       defer os.Remove("x")
+       defer os.Remove("x.1")
+       defer os.Remove("x.2")
+       defer os.Remove("x.3") // just in case
+       os.Remove("x")
+       os.Remove("x.1")
+       os.Remove("x.2")
+       os.Remove("x.3") // just in case
+
+       f := Create("x", 1e6, 2)
+
+       touch("x")
+       f.rotate()
+       if !isRegular("x.1") {
+               t.Fatal("want rotated file x.1")
+       }
+       if isRegular("x.2") {
+               t.Fatal("want no rotated file x.2")
+       }
+       if isRegular("x.3") {
+               t.Fatal("want no rotated file x.3")
+       }
+
+       touch("x")
+       f.rotate()
+       if !isRegular("x.1") {
+               t.Fatal("want rotated file x.1")
+       }
+       if !isRegular("x.2") {
+               t.Fatal("want rotated file x.2")
+       }
+       if isRegular("x.3") {
+               t.Fatal("want no rotated file x.3")
+       }
+
+       touch("x")
+       f.rotate()
+       if !isRegular("x.1") {
+               t.Fatal("want rotated file x.1")
+       }
+       if !isRegular("x.2") {
+               t.Fatal("want rotated file x.2")
+       }
+       if isRegular("x.3") {
+               t.Fatal("want no rotated file x.3")
+       }
+}
+
+func TestRotate0(t *testing.T) {
+       defer os.Remove("x")
+       defer os.Remove("x.1")
+       defer os.Remove("x.2") // just in case
+       os.Remove("x")
+       os.Remove("x.1")
+       os.Remove("x.2") // just in case
+
+       f := Create("x", 1e6, 0)
+
+       touch("x")
+       f.rotate()
+       if !isRegular("x.1") {
+               t.Fatal("want rotated file x.1")
+       }
+       if isRegular("x.2") {
+               t.Fatal("want no rotated file x.2")
+       }
+
+       touch("x")
+       f.rotate()
+       if !isRegular("x.1") {
+               t.Fatal("want rotated file x.1")
+       }
+       if isRegular("x.2") {
+               t.Fatal("want no rotated file x.2")
+       }
+}
+
+func TestInternalWriteNoRotate(t *testing.T) {
+       defer os.Remove("x")
+       defer os.Remove("x.1") // just in case
+       os.Remove("x")
+       os.Remove("x.1") // just in case
+
+       f := &File{
+               base: "x",
+               n:    1,
+               size: 10,
+               w:    0,
+       }
+       b := []byte("abc\n")
+       n, err := f.write(b)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if n != len(b) {
+               t.Fatalf("write(%q) = %d want %d", b, n, len(b))
+       }
+       if isRegular("x.1") {
+               t.Fatal("want no rotated file x.1")
+       }
+       if f.w != int64(len(b)) {
+               t.Fatalf("f.w = %d want %d", f.w, len(b))
+       }
+}
+
+func TestInternalWriteRotate(t *testing.T) {
+       defer os.Remove("x")
+       defer os.Remove("x.1")
+       os.Remove("x")
+       os.Remove("x.1")
+
+       f0, err := os.Create("x")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       f := &File{
+               base: "x",
+               n:    1,
+               size: 10,
+               w:    9,
+               f:    f0,
+       }
+       b := []byte("abc\n")
+       n, err := f.write(b)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if n != len(b) {
+               t.Fatalf("write(%q) = %d want %d", b, n, 4)
+       }
+       if !isRegular("x.1") {
+               t.Fatal("want rotated file x.1")
+       }
+       if f.f == f0 {
+               t.Fatal("want new file object")
+       }
+       if f.w != int64(len(b)) {
+               t.Fatalf("f.w = %d want %d", f.w, len(b))
+       }
+}
+
+func TestWrite(t *testing.T) {
+       defer os.Remove("x")
+       defer os.Remove("x.1")
+       os.Remove("x")
+       os.Remove("x.1")
+       f := Create("x", 1e6, 1)
+
+       b0 := []byte("ab")
+       n, err := f.Write(b0)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if n != len(b0) {
+               t.Fatalf("n = %d want %d", n, len(b0))
+       }
+       if !bytes.Equal(f.buf, b0) {
+               t.Fatalf("buf = %q want %q", f.buf, b0)
+       }
+       if f.w != 0 {
+               t.Fatalf("w = %d want 0", f.w)
+       }
+
+       b1 := []byte("c\n")
+       n, err = f.Write(b1)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if n != len(b1) {
+               t.Fatalf("n = %d want %d", n, len(b1))
+       }
+       if !bytes.Equal(f.buf, nil) {
+               t.Fatalf("buf = %q want %q", f.buf, "")
+       }
+       if f.w != int64(len(b0)+len(b1)) {
+               t.Fatalf("w = %d want %d", f.w, len(b0)+len(b1))
+       }
+}
+
+func TestOpenFail(t *testing.T) {
+       f := Create("/tmp/improbable-test-path/second-level/x", 1e6, 1)
+       b := []byte("abc\nd")
+       want := append(dropmsg, 'd')
+       n, err := f.Write(b)
+       if err == nil {
+               t.Error("want error")
+       }
+       if n != len(b) {
+               t.Errorf("n = %d want %d", n, len(b))
+       }
+       if !bytes.Equal(f.buf, want) {
+               t.Fatalf("buf = %q want %q", f.buf, want)
+       }
+
+       n, err = f.Write(b)
+       if err == nil {
+               t.Error("want error")
+       }
+       if n != len(b) {
+               t.Errorf("n = %d want %d", n, len(b))
+       }
+       // don't accumulate multiple dropped-log messages
+       if !bytes.Equal(f.buf, want) {
+               t.Fatalf("buf = %q want %q", f.buf, want)
+       }
+}
+
+func TestAppend(t *testing.T) {
+       defer os.Remove("x")
+       b0 := []byte("abc\n")
+       b1 := []byte("def\n")
+       err := ioutil.WriteFile("x", b0, 0666)
+       if err != nil {
+               t.Fatal(err)
+       }
+       f := Create("x", 100, 1)
+       f.Write(b1)
+       if want := int64(len(b0) + len(b1)); f.w != want {
+               t.Fatalf("w = %d want %d", f.w, want)
+       }
+}
+
+func isRegular(name string) bool {
+       fi, err := os.Stat(name)
+       return err == nil && fi.Mode().IsRegular()
+}
+
+func touch(name string) {
+       f, _ := os.Create(name)
+       if f != nil {
+               f.Close()
+       }
+}
diff --git a/log/splunk/splunk.go b/log/splunk/splunk.go
new file mode 100644 (file)
index 0000000..bf9b494
--- /dev/null
@@ -0,0 +1,78 @@
+// Package splunk sends log data to a splunk server.
+package splunk
+
+import (
+       "io"
+       "net"
+       "time"
+)
+
+const (
+       // DialTimeout limits how long a write will block
+       // while dialing the splunk server. Assuming the
+       // connection stays open for a long time, this will
+       // happen only rarely.
+       DialTimeout = 50 * time.Millisecond
+
+       // WriteTimeout limits how long a write will block
+       // sending data on the network. It is deliberately
+       // very small, so that writes can only be satisfied
+       // by the local network buffers. It should never
+       // block waiting for a TCP ACK for an appreciable
+       // amount of time.
+       WriteTimeout = 100 * time.Microsecond
+)
+
+type splunk struct {
+       addr    string
+       conn    net.Conn
+       dropmsg []byte
+       err     error // last write error
+}
+
+// New creates a new writer that sends data
+// to the given TCP address.
+// It connects on the first call to Write,
+// and attempts to reconnect when necessary,
+// with a timeout of DialTimeout.
+//
+// Every write has a timeout of WriteTimeout.
+// If the write doesn't complete in that time,
+// the writer drops the unwritten data.
+// For every contiguous run of dropped data,
+// it writes dropmsg before resuming ordinary writes.
+// As long as the remote endpoint can keep up
+// with the averate data rate and the local
+// network buffers in the kernel and NIC are
+// big enough to hold traffic bursts, no data
+// will be lost.
+func New(addr string, dropmsg []byte) io.Writer {
+       return &splunk{
+               addr:    addr,
+               dropmsg: dropmsg,
+       }
+}
+
+func (s *splunk) Write(p []byte) (n int, err error) {
+       if s.conn == nil {
+               s.conn, err = net.DialTimeout("tcp", s.addr, DialTimeout)
+               if err != nil {
+                       return 0, err
+               }
+       }
+
+       if s.err != nil {
+               s.conn.SetDeadline(time.Now().Add(WriteTimeout))
+               _, s.err = s.conn.Write(s.dropmsg)
+       }
+       if s.err == nil {
+               s.conn.SetDeadline(time.Now().Add(WriteTimeout))
+               n, s.err = s.conn.Write(p)
+       }
+
+       if t, ok := s.err.(net.Error); s.err != nil && (!ok || !t.Temporary()) {
+               s.conn.Close()
+               s.conn = nil
+       }
+       return n, s.err
+}
diff --git a/log/stack.go b/log/stack.go
new file mode 100644 (file)
index 0000000..6eb7c55
--- /dev/null
@@ -0,0 +1,44 @@
+package log
+
+import (
+       "path/filepath"
+       "runtime"
+       "strconv"
+)
+
+var skipFunc = map[string]bool{
+       "chain/log.Printkv":            true,
+       "chain/log.Printf":             true,
+       "chain/log.Error":              true,
+       "chain/log.Fatalkv":            true,
+       "chain/log.RecoverAndLogError": true,
+}
+
+// SkipFunc removes the named function from stack traces
+// and at=[file:line] entries printed to the log output.
+// The provided name should be a fully-qualified function name
+// comprising the import path and identifier separated by a dot.
+// For example, chain/log.Printkv.
+// SkipFunc must not be called concurrently with any function
+// in this package (including itself).
+func SkipFunc(name string) {
+       skipFunc[name] = true
+}
+
+// caller returns a string containing filename and line number of
+// the deepest function invocation on the calling goroutine's stack,
+// after skipping functions in skipFunc.
+// If no stack information is available, it returns "?:?".
+func caller() string {
+       for i := 1; ; i++ {
+               // NOTE(kr): This is quadratic in the number of frames we
+               // ultimately have to skip. Consider using Callers instead.
+               pc, file, line, ok := runtime.Caller(i)
+               if !ok {
+                       return "?:?"
+               }
+               if !skipFunc[runtime.FuncForPC(pc).Name()] {
+                       return filepath.Base(file) + ":" + strconv.Itoa(line)
+               }
+       }
+}
diff --git a/math/checked/checked.go b/math/checked/checked.go
new file mode 100644 (file)
index 0000000..8280d81
--- /dev/null
@@ -0,0 +1,268 @@
+/*
+Package checked implements basic arithmetic operations
+with underflow and overflow checks.
+*/
+package checked
+
+import (
+       "errors"
+       "math"
+)
+
+var ErrOverflow = errors.New("arithmetic overflow")
+
+// AddInt64 returns a + b
+// with an integer overflow check.
+func AddInt64(a, b int64) (sum int64, ok bool) {
+       if (b > 0 && a > math.MaxInt64-b) ||
+               (b < 0 && a < math.MinInt64-b) {
+               return 0, false
+       }
+       return a + b, true
+}
+
+// SubInt64 returns a - b
+// with an integer overflow check.
+func SubInt64(a, b int64) (diff int64, ok bool) {
+       if (b > 0 && a < math.MinInt64+b) ||
+               (b < 0 && a > math.MaxInt64+b) {
+               return 0, false
+       }
+       return a - b, true
+}
+
+// MulInt64 returns a * b
+// with an integer overflow check.
+func MulInt64(a, b int64) (product int64, ok bool) {
+       if (a > 0 && b > 0 && a > math.MaxInt64/b) ||
+               (a > 0 && b <= 0 && b < math.MinInt64/a) ||
+               (a <= 0 && b > 0 && a < math.MinInt64/b) ||
+               (a < 0 && b <= 0 && b < math.MaxInt64/a) {
+               return 0, false
+       }
+       return a * b, true
+}
+
+// DivInt64 returns a / b
+// with an integer overflow check.
+func DivInt64(a, b int64) (quotient int64, ok bool) {
+       if b == 0 || (a == math.MinInt64 && b == -1) {
+               return 0, false
+       }
+       return a / b, true
+}
+
+// ModInt64 returns a % b
+// with an integer overflow check.
+func ModInt64(a, b int64) (remainder int64, ok bool) {
+       if b == 0 || (a == math.MinInt64 && b == -1) {
+               return 0, false
+       }
+       return a % b, true
+}
+
+// NegateInt64 returns -a
+// with an integer overflow check.
+func NegateInt64(a int64) (negated int64, ok bool) {
+       if a == math.MinInt64 {
+               return 0, false
+       }
+       return -a, true
+}
+
+// LshiftInt64 returns a << b
+// with an integer overflow check.
+func LshiftInt64(a, b int64) (result int64, ok bool) {
+       if b < 0 || b >= 64 {
+               return 0, false
+       }
+       if (a >= 0 && a > math.MaxInt64>>uint(b)) || (a < 0 && a < math.MinInt64>>uint(b)) {
+               return 0, false
+       }
+       return a << uint(b), true
+}
+
+// AddInt32 returns a + b
+// with an integer overflow check.
+func AddInt32(a, b int32) (sum int32, ok bool) {
+       if (b > 0 && a > math.MaxInt32-b) ||
+               (b < 0 && a < math.MinInt32-b) {
+               return 0, false
+       }
+       return a + b, true
+}
+
+// SubInt32 returns a - b
+// with an integer overflow check.
+func SubInt32(a, b int32) (diff int32, ok bool) {
+       if (b > 0 && a < math.MinInt32+b) ||
+               (b < 0 && a > math.MaxInt32+b) {
+               return 0, false
+       }
+       return a - b, true
+}
+
+// MulInt32 returns a * b
+// with an integer overflow check.
+func MulInt32(a, b int32) (product int32, ok bool) {
+       if (a > 0 && b > 0 && a > math.MaxInt32/b) ||
+               (a > 0 && b <= 0 && b < math.MinInt32/a) ||
+               (a <= 0 && b > 0 && a < math.MinInt32/b) ||
+               (a < 0 && b <= 0 && b < math.MaxInt32/a) {
+               return 0, false
+       }
+       return a * b, true
+}
+
+// DivInt32 returns a / b
+// with an integer overflow check.
+func DivInt32(a, b int32) (quotient int32, ok bool) {
+       if b == 0 || (a == math.MinInt32 && b == -1) {
+               return 0, false
+       }
+       return a / b, true
+}
+
+// ModInt32 returns a % b
+// with an integer overflow check.
+func ModInt32(a, b int32) (remainder int32, ok bool) {
+       if b == 0 || (a == math.MinInt32 && b == -1) {
+               return 0, false
+       }
+       return a % b, true
+}
+
+// NegateInt32 returns -a
+// with an integer overflow check.
+func NegateInt32(a int32) (negated int32, ok bool) {
+       if a == math.MinInt32 {
+               return 0, false
+       }
+       return -a, true
+}
+
+// LshiftInt32 returns a << b
+// with an integer overflow check.
+func LshiftInt32(a, b int32) (result int32, ok bool) {
+       if b < 0 || b >= 32 {
+               return 0, false
+       }
+       if (a >= 0 && a > math.MaxInt32>>uint(b)) || (a < 0 && a < math.MinInt32>>uint(b)) {
+               return 0, false
+       }
+       return a << uint(b), true
+}
+
+// AddUint64 returns a + b
+// with an integer overflow check.
+func AddUint64(a, b uint64) (sum uint64, ok bool) {
+       if math.MaxUint64-a < b {
+               return 0, false
+       }
+       return a + b, true
+}
+
+// SubUint64 returns a - b
+// with an integer overflow check.
+func SubUint64(a, b uint64) (diff uint64, ok bool) {
+       if a < b {
+               return 0, false
+       }
+       return a - b, true
+}
+
+// MulUint64 returns a * b
+// with an integer overflow check.
+func MulUint64(a, b uint64) (product uint64, ok bool) {
+       if b > 0 && a > math.MaxUint64/b {
+               return 0, false
+       }
+       return a * b, true
+}
+
+// DivUint64 returns a / b
+// with an integer overflow check.
+func DivUint64(a, b uint64) (quotient uint64, ok bool) {
+       if b == 0 {
+               return 0, false
+       }
+       return a / b, true
+}
+
+// ModUint64 returns a % b
+// with an integer overflow check.
+func ModUint64(a, b uint64) (remainder uint64, ok bool) {
+       if b == 0 {
+               return 0, false
+       }
+       return a % b, true
+}
+
+// LshiftUint64 returns a << b
+// with an integer overflow check.
+func LshiftUint64(a, b uint64) (result uint64, ok bool) {
+       if b >= 64 {
+               return 0, false
+       }
+       if a > math.MaxUint64>>uint(b) {
+               return 0, false
+       }
+       return a << uint(b), true
+}
+
+// AddUint32 returns a + b
+// with an integer overflow check.
+func AddUint32(a, b uint32) (sum uint32, ok bool) {
+       if math.MaxUint32-a < b {
+               return 0, false
+       }
+       return a + b, true
+}
+
+// SubUint32 returns a - b
+// with an integer overflow check.
+func SubUint32(a, b uint32) (diff uint32, ok bool) {
+       if a < b {
+               return 0, false
+       }
+       return a - b, true
+}
+
+// MulUint32 returns a * b
+// with an integer overflow check.
+func MulUint32(a, b uint32) (product uint32, ok bool) {
+       if b > 0 && a > math.MaxUint32/b {
+               return 0, false
+       }
+       return a * b, true
+}
+
+// DivUint32 returns a / b
+// with an integer overflow check.
+func DivUint32(a, b uint32) (quotient uint32, ok bool) {
+       if b == 0 {
+               return 0, false
+       }
+       return a / b, true
+}
+
+// ModUint32 returns a % b
+// with an integer overflow check.
+func ModUint32(a, b uint32) (remainder uint32, ok bool) {
+       if b == 0 {
+               return 0, false
+       }
+       return a % b, true
+}
+
+// LshiftUint32 returns a << b
+// with an integer overflow check.
+func LshiftUint32(a, b uint32) (result uint32, ok bool) {
+       if b >= 32 {
+               return 0, false
+       }
+       if a > math.MaxUint32>>uint(b) {
+               return 0, false
+       }
+       return a << uint(b), true
+}
diff --git a/math/checked/checked_test.go b/math/checked/checked_test.go
new file mode 100644 (file)
index 0000000..d9babb4
--- /dev/null
@@ -0,0 +1,232 @@
+package checked
+
+import (
+       "math"
+       "reflect"
+       "runtime"
+       "strings"
+       "testing"
+)
+
+func TestInt64(t *testing.T) {
+       cases := []struct {
+               f          func(a, b int64) (int64, bool)
+               a, b, want int64
+               wantOk     bool
+       }{
+               {AddInt64, 2, 3, 5, true},
+               {AddInt64, 2, -3, -1, true},
+               {AddInt64, -2, -3, -5, true},
+               {AddInt64, math.MaxInt64, 1, 0, false},
+               {AddInt64, math.MinInt64, math.MinInt64, 0, false},
+               {AddInt64, math.MinInt64, -1, 0, false},
+               {SubInt64, 3, 2, 1, true},
+               {SubInt64, 2, 3, -1, true},
+               {SubInt64, -2, -3, 1, true},
+               {SubInt64, math.MinInt64, 1, 0, false},
+               {SubInt64, -2, math.MaxInt64, 0, false},
+               {MulInt64, 2, 3, 6, true},
+               {MulInt64, -2, -3, 6, true},
+               {MulInt64, -2, 3, -6, true},
+               {MulInt64, math.MaxInt64, -1, math.MinInt64 + 1, true},
+               {MulInt64, math.MinInt64, 2, 0, false},
+               {MulInt64, math.MaxInt64, 2, 0, false},
+               {MulInt64, 2, math.MinInt64, 0, false},
+               {MulInt64, -2, math.MinInt64, 0, false},
+               {DivInt64, 2, 2, 1, true},
+               {DivInt64, -2, -2, 1, true},
+               {DivInt64, -2, 2, -1, true},
+               {DivInt64, 1, 0, 0, false},
+               {DivInt64, math.MinInt64, -1, 0, false},
+               {ModInt64, 3, 2, 1, true},
+               {ModInt64, -3, -2, -1, true},
+               {ModInt64, -3, 2, -1, true},
+               {ModInt64, 1, 0, 0, false},
+               {ModInt64, math.MinInt64, -1, 0, false},
+               {LshiftInt64, 1, 2, 4, true},
+               {LshiftInt64, -1, 2, -4, true},
+               {LshiftInt64, 1, 64, 0, false},
+               {LshiftInt64, 2, 63, 0, false},
+       }
+
+       for _, c := range cases {
+               got, gotOk := c.f(c.a, c.b)
+
+               if got != c.want {
+                       t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+               }
+
+               if gotOk != c.wantOk {
+                       t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+               }
+       }
+
+       negateCases := []struct {
+               a, want int64
+               wantOk  bool
+       }{
+               {1, -1, true},
+               {-1, 1, true},
+               {0, 0, true},
+               {math.MinInt64, 0, false},
+       }
+       for _, c := range negateCases {
+               got, gotOk := NegateInt64(c.a)
+
+               if got != c.want {
+                       t.Errorf("NegateInt64(%d) = %d want %d", c.a, got, c.want)
+               }
+
+               if gotOk != c.wantOk {
+                       t.Errorf("NegateInt64(%d) ok = %v want %v", c.a, gotOk, c.wantOk)
+               }
+       }
+}
+
+func TestUint64(t *testing.T) {
+       cases := []struct {
+               f          func(a, b uint64) (uint64, bool)
+               a, b, want uint64
+               wantOk     bool
+       }{
+               {AddUint64, 2, 3, 5, true},
+               {AddUint64, math.MaxUint64, 1, 0, false},
+               {SubUint64, 3, 2, 1, true},
+               {SubUint64, 2, 3, 0, false},
+               {MulUint64, 2, 3, 6, true},
+               {MulUint64, math.MaxUint64, 2, 0, false},
+               {DivUint64, 2, 2, 1, true},
+               {DivUint64, 1, 0, 0, false},
+               {ModUint64, 3, 2, 1, true},
+               {ModUint64, 1, 0, 0, false},
+               {LshiftUint64, 1, 2, 4, true},
+               {LshiftUint64, 1, 64, 0, false},
+               {LshiftUint64, 2, 63, 0, false},
+       }
+
+       for _, c := range cases {
+               got, gotOk := c.f(c.a, c.b)
+
+               if got != c.want {
+                       t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+               }
+
+               if gotOk != c.wantOk {
+                       t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+               }
+       }
+}
+
+func TestInt32(t *testing.T) {
+       cases := []struct {
+               f          func(a, b int32) (int32, bool)
+               a, b, want int32
+               wantOk     bool
+       }{
+               {AddInt32, 2, 3, 5, true},
+               {AddInt32, 2, -3, -1, true},
+               {AddInt32, -2, -3, -5, true},
+               {AddInt32, math.MaxInt32, 1, 0, false},
+               {AddInt32, math.MinInt32, math.MinInt32, 0, false},
+               {AddInt32, math.MinInt32, -1, 0, false},
+               {SubInt32, 3, 2, 1, true},
+               {SubInt32, 2, 3, -1, true},
+               {SubInt32, -2, -3, 1, true},
+               {SubInt32, math.MinInt32, 1, 0, false},
+               {SubInt32, -2, math.MaxInt32, 0, false},
+               {MulInt32, 2, 3, 6, true},
+               {MulInt32, -2, -3, 6, true},
+               {MulInt32, -2, 3, -6, true},
+               {MulInt32, math.MaxInt32, -1, math.MinInt32 + 1, true},
+               {MulInt32, math.MinInt32, 2, 0, false},
+               {MulInt32, math.MaxInt32, 2, 0, false},
+               {MulInt32, 2, math.MinInt32, 0, false},
+               {MulInt32, -2, math.MinInt32, 0, false},
+               {DivInt32, 2, 2, 1, true},
+               {DivInt32, -2, -2, 1, true},
+               {DivInt32, -2, 2, -1, true},
+               {DivInt32, 1, 0, 0, false},
+               {DivInt32, math.MinInt32, -1, 0, false},
+               {ModInt32, 3, 2, 1, true},
+               {ModInt32, -3, -2, -1, true},
+               {ModInt32, -3, 2, -1, true},
+               {ModInt32, 1, 0, 0, false},
+               {ModInt32, math.MinInt32, -1, 0, false},
+               {LshiftInt32, 1, 2, 4, true},
+               {LshiftInt32, -1, 2, -4, true},
+               {LshiftInt32, 1, 32, 0, false},
+               {LshiftInt32, 2, 31, 0, false},
+       }
+
+       for _, c := range cases {
+               got, gotOk := c.f(c.a, c.b)
+
+               if got != c.want {
+                       t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+               }
+
+               if gotOk != c.wantOk {
+                       t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+               }
+       }
+
+       negateCases := []struct {
+               a, want int32
+               wantOk  bool
+       }{
+               {1, -1, true},
+               {-1, 1, true},
+               {0, 0, true},
+               {math.MinInt32, 0, false},
+       }
+       for _, c := range negateCases {
+               got, gotOk := NegateInt32(c.a)
+
+               if got != c.want {
+                       t.Errorf("NegateInt32(%d) = %d want %d", c.a, got, c.want)
+               }
+
+               if gotOk != c.wantOk {
+                       t.Errorf("NegateInt32(%d) ok = %v want %v", c.a, gotOk, c.wantOk)
+               }
+       }
+}
+
+func TestUint32(t *testing.T) {
+       cases := []struct {
+               f          func(a, b uint32) (uint32, bool)
+               a, b, want uint32
+               wantOk     bool
+       }{
+               {AddUint32, 2, 3, 5, true},
+               {AddUint32, math.MaxUint32, 1, 0, false},
+               {SubUint32, 3, 2, 1, true},
+               {SubUint32, 2, 3, 0, false},
+               {MulUint32, 2, 3, 6, true},
+               {MulUint32, math.MaxUint32, 2, 0, false},
+               {DivUint32, 2, 2, 1, true},
+               {DivUint32, 1, 0, 0, false},
+               {ModUint32, 3, 2, 1, true},
+               {ModUint32, 1, 0, 0, false},
+               {LshiftUint32, 1, 2, 4, true},
+               {LshiftUint32, 1, 32, 0, false},
+               {LshiftUint32, 2, 31, 0, false},
+       }
+
+       for _, c := range cases {
+               got, gotOk := c.f(c.a, c.b)
+
+               if got != c.want {
+                       t.Errorf("%s(%d, %d) = %d want %d", fname(c.f), c.a, c.b, got, c.want)
+               }
+
+               if gotOk != c.wantOk {
+                       t.Errorf("%s(%d, %d) ok = %v want %v", fname(c.f), c.a, c.b, gotOk, c.wantOk)
+               }
+       }
+}
+
+func fname(f interface{}) string {
+       name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
+       return name[strings.IndexRune(name, '.')+1:]
+}