Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix data race and optimize performance #183

Merged
merged 7 commits into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ language: go
matrix:
include:
- go: 1.9.x
env: PKG='./...'
env: GO111MODULE=on
- go: 1.12.x
env: PKG='./...' COVER='-coverprofile=coverage.txt -covermode=count'
env: GO111MODULE=on COVER='-coverprofile=coverage.txt -covermode=atomic'

install:
- go get -t -v $PKG
- go get -t -v ./...

script:
- go test $COVER $PKG
- go test -race $COVER ./...

after_success:
- bash <(curl -s https://codecov.io/bash)
29 changes: 29 additions & 0 deletions i18n/bundle_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package i18n

import (
"fmt"
"reflect"
"testing"

Expand Down Expand Up @@ -31,6 +32,34 @@ var everythingMessage = MustNewMessage(map[string]string{
"other": "other translation",
})

func TestConcurrentAccess(t *testing.T) {
bundle := NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
bundle.MustParseMessageFileBytes([]byte(`
# Comment
hello = "world"
`), "en.toml")

count := 10
errch := make(chan error, count)
for i := 0; i < count; i++ {
go func() {
localized := NewLocalizer(bundle, "en").MustLocalize(&LocalizeConfig{MessageID: "hello"})
if localized != "world" {
errch <- fmt.Errorf(`expected "world"; got %q`, localized)
} else {
errch <- nil
}
}()
}

for i := 0; i < count; i++ {
if err := <-errch; err != nil {
t.Fatal(err)
}
}
}

func TestPseudoLanguage(t *testing.T) {
bundle := NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
Expand Down
33 changes: 13 additions & 20 deletions i18n/message_template.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package i18n

import (
"bytes"
"fmt"

"text/template"
Expand All @@ -19,12 +18,12 @@ type MessageTemplate struct {
// NewMessageTemplate returns a new message template.
func NewMessageTemplate(m *Message) *MessageTemplate {
pluralTemplates := map[plural.Form]*internal.Template{}
setPluralTemplate(pluralTemplates, plural.Zero, m.Zero)
setPluralTemplate(pluralTemplates, plural.One, m.One)
setPluralTemplate(pluralTemplates, plural.Two, m.Two)
setPluralTemplate(pluralTemplates, plural.Few, m.Few)
setPluralTemplate(pluralTemplates, plural.Many, m.Many)
setPluralTemplate(pluralTemplates, plural.Other, m.Other)
setPluralTemplate(pluralTemplates, plural.Zero, m.Zero, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.One, m.One, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Two, m.Two, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Few, m.Few, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Many, m.Many, m.LeftDelim, m.RightDelim)
setPluralTemplate(pluralTemplates, plural.Other, m.Other, m.LeftDelim, m.RightDelim)
if len(pluralTemplates) == 0 {
return nil
}
Expand All @@ -34,9 +33,13 @@ func NewMessageTemplate(m *Message) *MessageTemplate {
}
}

func setPluralTemplate(pluralTemplates map[plural.Form]*internal.Template, pluralForm plural.Form, src string) {
func setPluralTemplate(pluralTemplates map[plural.Form]*internal.Template, pluralForm plural.Form, src, leftDelim, rightDelim string) {
if src != "" {
pluralTemplates[pluralForm] = &internal.Template{Src: src}
pluralTemplates[pluralForm] = &internal.Template{
Src: src,
LeftDelim: leftDelim,
RightDelim: rightDelim,
}
}
}

Expand All @@ -58,15 +61,5 @@ func (mt *MessageTemplate) Execute(pluralForm plural.Form, data interface{}, fun
messageID: mt.Message.ID,
}
}
if err := t.Parse(mt.LeftDelim, mt.RightDelim, funcs); err != nil {
return "", err
}
if t.Template == nil {
return t.Src, nil
}
var buf bytes.Buffer
if err := t.Template.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
return t.Execute(funcs, data)
}
51 changes: 38 additions & 13 deletions internal/template.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
package internal

import (
"bytes"
"strings"
"sync"
gotemplate "text/template"
)

// Template stores the template for a string.
type Template struct {
Src string
Template *gotemplate.Template
ParseErr *error
Src string
LeftDelim string
RightDelim string

parseOnce sync.Once
parsedTemplate *gotemplate.Template
parseError error
}

func (t *Template) Parse(leftDelim, rightDelim string, funcs gotemplate.FuncMap) error {
if t.ParseErr == nil {
if strings.Contains(t.Src, leftDelim) {
gt, err := gotemplate.New("").Funcs(funcs).Delims(leftDelim, rightDelim).Parse(t.Src)
t.Template = gt
t.ParseErr = &err
} else {
t.ParseErr = new(error)
}
func (t *Template) Execute(funcs gotemplate.FuncMap, data interface{}) (string, error) {
leftDelim := t.LeftDelim
if leftDelim == "" {
leftDelim = "{{"
}
if !strings.Contains(t.Src, leftDelim) {
// Fast path to avoid parsing a template that has no actions.
return t.Src, nil
}

var gt *gotemplate.Template
var err error
if funcs == nil {
t.parseOnce.Do(func() {
// If funcs is nil, then we only need to parse this template once.
t.parsedTemplate, t.parseError = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Parse(t.Src)
})
gt, err = t.parsedTemplate, t.parseError
} else {
gt, err = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Funcs(funcs).Parse(t.Src)
}

if err != nil {
return "", err
}
var buf bytes.Buffer
if err := gt.Execute(&buf, data); err != nil {
return "", err
}
return *t.ParseErr
return buf.String(), nil
}
103 changes: 63 additions & 40 deletions internal/template_test.go
Original file line number Diff line number Diff line change
@@ -1,54 +1,77 @@
package internal

import (
"bytes"
"fmt"
"testing"
"text/template"
)

func TestParse(t *testing.T) {
tmpl := &Template{Src: "hello"}
if err := tmpl.Parse("", "", nil); err != nil {
t.Fatal(err)
}
if tmpl.ParseErr == nil {
t.Fatal("expected non-nil parse error")
}
if tmpl.Template == nil {
t.Fatal("expected non-nil template")
func TestExecute(t *testing.T) {
tests := []struct {
template *Template
funcs template.FuncMap
data interface{}
result string
err string
noallocs bool
}{
{
template: &Template{
Src: "hello",
},
result: "hello",
noallocs: true,
},
{
template: &Template{
Src: "hello {{.Noun}}",
},
data: map[string]string{
"Noun": "world",
},
result: "hello world",
},
{
template: &Template{
Src: "hello {{world}}",
},
funcs: template.FuncMap{
"world": func() string {
return "world"
},
},
result: "hello world",
},
{
template: &Template{
Src: "hello {{",
},
err: "template: :1: unexpected unclosed action in command",
noallocs: true,
},
}
}

func TestParseError(t *testing.T) {
expectedErr := fmt.Errorf("expected error")
tmpl := &Template{ParseErr: &expectedErr}
if err := tmpl.Parse("", "", nil); err != expectedErr {
t.Fatalf("expected %#v; got %#v", expectedErr, err)
for _, test := range tests {
t.Run(test.template.Src, func(t *testing.T) {
result, err := test.template.Execute(test.funcs, test.data)
if actual := str(err); actual != test.err {
t.Errorf("expected err %q; got %q", test.err, actual)
}
if result != test.result {
t.Errorf("expected result %q; got %q", test.result, result)
}
allocs := testing.AllocsPerRun(10, func() {
_, _ = test.template.Execute(test.funcs, test.data)
})
if test.noallocs && allocs > 0 {
t.Errorf("expected no allocations; got %f", allocs)
}
})
}
}

func TestParseWithFunc(t *testing.T) {
tmpl := &Template{Src: "{{foo}}"}
funcs := template.FuncMap{
"foo": func() string {
return "bar"
},
}
if err := tmpl.Parse("", "", funcs); err != nil {
t.Fatal(err)
}
if tmpl.ParseErr == nil {
t.Fatal("expected non-nil parse error")
}
if tmpl.Template == nil {
t.Fatal("expected non-nil template")
}
var buf bytes.Buffer
if tmpl.Template.Execute(&buf, nil) != nil {
t.Fatal("expected nil template execute error")
}
if buf.String() != "bar" {
t.Fatalf("expected bar; got %s", buf.String())
func str(err error) string {
if err == nil {
return ""
}
return err.Error()
}