Commit 00b2a30f by Ian Lance Taylor

libgo: update to final Go 1.8 release

    
    Along with the update this fixes a problem that was always present but
    only showed up with the new reflect test.  When a program used a
    **unsafe.Pointer and stored the value in an interface type, the
    generated type descriptor pointed to the GC data for *unsafe.Pointer.
    It did that by name, but we were not generating a variable with the
    right name.
    
    Reviewed-on: https://go-review.googlesource.com/37144

From-SVN: r245535
parent 4bcd6597
c3935e1f20ad5b1d4c41150f11fb266913c04df7 893f0e4a707c6f10eb14842b18954486042f0fb3
The first line of this file holds the git revision number of the last The first line of this file holds the git revision number of the last
merge done from the gofrontend repository. merge done from the gofrontend repository.
2a5f65a98ca483aad2dd74dc2636a7baecc59cf2 cd6b6202dd1559b3ac63179b45f1833fcfbe7eca
The first line of this file holds the git revision number of the The first line of this file holds the git revision number of the
last merge done from the master library sources. last merge done from the master library sources.
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
// clean remove object files // clean remove object files
// doc show documentation for package or symbol // doc show documentation for package or symbol
// env print Go environment information // env print Go environment information
// bug print information for bug reports // bug start a bug report
// fix run go tool fix on packages // fix run go tool fix on packages
// fmt run gofmt on package sources // fmt run gofmt on package sources
// generate generate Go files by processing source // generate generate Go files by processing source
...@@ -324,15 +324,14 @@ ...@@ -324,15 +324,14 @@
// each named variable on its own line. // each named variable on its own line.
// //
// //
// Print information for bug reports // Start a bug report
// //
// Usage: // Usage:
// //
// go bug // go bug
// //
// Bug prints information that helps file effective bug reports. // Bug opens the default browser and starts a new bug report.
// // The report includes useful system information.
// Bugs may be reported at https://golang.org/issue/new.
// //
// //
// Run go tool fix on packages // Run go tool fix on packages
......
...@@ -428,7 +428,7 @@ func downloadPackage(p *Package) error { ...@@ -428,7 +428,7 @@ func downloadPackage(p *Package) error {
return fmt.Errorf("cannot download, $GOPATH not set. For more details see: 'go help gopath'") return fmt.Errorf("cannot download, $GOPATH not set. For more details see: 'go help gopath'")
} }
// Guard against people setting GOPATH=$GOROOT. // Guard against people setting GOPATH=$GOROOT.
if list[0] == goroot { if filepath.Clean(list[0]) == filepath.Clean(goroot) {
return fmt.Errorf("cannot download, $GOPATH must not be set to $GOROOT. For more details see: 'go help gopath'") return fmt.Errorf("cannot download, $GOPATH must not be set to $GOROOT. For more details see: 'go help gopath'")
} }
if _, err := os.Stat(filepath.Join(list[0], "src/cmd/go/alldocs.go")); err == nil { if _, err := os.Stat(filepath.Join(list[0], "src/cmd/go/alldocs.go")); err == nil {
......
...@@ -1683,173 +1683,111 @@ func homeEnvName() string { ...@@ -1683,173 +1683,111 @@ func homeEnvName() string {
} }
} }
// Test go env missing GOPATH shows default. func TestDefaultGOPATH(t *testing.T) {
func TestMissingGOPATHEnvShowsDefault(t *testing.T) {
tg := testgo(t) tg := testgo(t)
defer tg.cleanup() defer tg.cleanup()
tg.parallel() tg.parallel()
tg.setenv("GOPATH", "") tg.tempDir("home/go")
tg.run("env", "GOPATH") tg.setenv(homeEnvName(), tg.path("home"))
want := filepath.Join(os.Getenv(homeEnvName()), "go") tg.run("env", "GOPATH")
got := strings.TrimSpace(tg.getStdout()) tg.grepStdout(regexp.QuoteMeta(tg.path("home/go")), "want GOPATH=$HOME/go")
if got != want {
t.Errorf("got %q; want %q", got, want)
}
}
// Test go get missing GOPATH causes go get to warn if directory doesn't exist.
func TestMissingGOPATHGetWarnsIfNotExists(t *testing.T) {
testenv.MustHaveExternalNetwork(t)
if _, err := exec.LookPath("git"); err != nil {
t.Skip("skipping because git binary not found")
}
tg := testgo(t)
defer tg.cleanup()
// setenv variables for test and defer deleting temporary home directory.
tg.setenv("GOPATH", "")
tmp, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("could not create tmp home: %v", err)
}
defer os.RemoveAll(tmp)
tg.setenv(homeEnvName(), tmp)
tg.run("get", "-v", "github.com/golang/example/hello") tg.setenv("GOROOT", tg.path("home/go"))
tg.run("env", "GOPATH")
tg.grepStdoutNot(".", "want unset GOPATH because GOROOT=$HOME/go")
want := fmt.Sprintf("created GOPATH=%s; see 'go help gopath'", filepath.Join(tmp, "go")) tg.setenv("GOROOT", tg.path("home/go")+"/")
got := strings.TrimSpace(tg.getStderr()) tg.run("env", "GOPATH")
if !strings.Contains(got, want) { tg.grepStdoutNot(".", "want unset GOPATH because GOROOT=$HOME/go/")
t.Errorf("got %q; want %q", got, want)
}
} }
// Test go get missing GOPATH causes no warning if directory exists. func TestDefaultGOPATHGet(t *testing.T) {
func TestMissingGOPATHGetDoesntWarnIfExists(t *testing.T) {
testenv.MustHaveExternalNetwork(t) testenv.MustHaveExternalNetwork(t)
if _, err := exec.LookPath("git"); err != nil {
t.Skip("skipping because git binary not found")
}
tg := testgo(t) tg := testgo(t)
defer tg.cleanup() defer tg.cleanup()
// setenv variables for test and defer resetting them.
tg.setenv("GOPATH", "") tg.setenv("GOPATH", "")
tmp, err := ioutil.TempDir("", "") tg.tempDir("home")
if err != nil { tg.setenv(homeEnvName(), tg.path("home"))
t.Fatalf("could not create tmp home: %v", err)
}
defer os.RemoveAll(tmp)
if err := os.Mkdir(filepath.Join(tmp, "go"), 0777); err != nil {
t.Fatalf("could not create $HOME/go: %v", err)
}
tg.setenv(homeEnvName(), tmp) // warn for creating directory
tg.run("get", "-v", "github.com/golang/example/hello")
tg.grepStderr("created GOPATH="+regexp.QuoteMeta(tg.path("home/go"))+"; see 'go help gopath'", "did not create GOPATH")
// no warning if directory already exists
tg.must(os.RemoveAll(tg.path("home/go")))
tg.tempDir("home/go")
tg.run("get", "github.com/golang/example/hello") tg.run("get", "github.com/golang/example/hello")
tg.grepStderrNot(".", "expected no output on standard error")
got := strings.TrimSpace(tg.getStderr()) // error if $HOME/go is a file
if got != "" { tg.must(os.RemoveAll(tg.path("home/go")))
t.Errorf("got %q; wants empty", got) tg.tempFile("home/go", "")
} tg.runFail("get", "github.com/golang/example/hello")
} tg.grepStderr(`mkdir .*[/\\]go: .*(not a directory|cannot find the path)`, "expected error because $HOME/go is a file")
// Test go get missing GOPATH fails if pointed file is not a directory.
func TestMissingGOPATHGetFailsIfItsNotDirectory(t *testing.T) {
testenv.MustHaveExternalNetwork(t)
tg := testgo(t)
defer tg.cleanup()
// setenv variables for test and defer resetting them.
tg.setenv("GOPATH", "")
tmp, err := ioutil.TempDir("", "")
if err != nil {
t.Fatalf("could not create tmp home: %v", err)
}
defer os.RemoveAll(tmp)
path := filepath.Join(tmp, "go")
if err := ioutil.WriteFile(path, nil, 0777); err != nil {
t.Fatalf("could not create GOPATH at %s: %v", path, err)
}
tg.setenv(homeEnvName(), tmp)
const pkg = "github.com/golang/example/hello"
tg.runFail("get", pkg)
msg := "not a directory"
if runtime.GOOS == "windows" {
msg = "The system cannot find the path specified."
}
want := fmt.Sprintf("package %s: mkdir %s: %s", pkg, filepath.Join(tmp, "go"), msg)
got := strings.TrimSpace(tg.getStderr())
if got != want {
t.Errorf("got %q; wants %q", got, want)
}
} }
// Test go install of missing package when missing GOPATH fails and shows default GOPATH. func TestDefaultGOPATHPrintedSearchList(t *testing.T) {
func TestMissingGOPATHInstallMissingPackageFailsAndShowsDefault(t *testing.T) {
tg := testgo(t) tg := testgo(t)
defer tg.cleanup() defer tg.cleanup()
// setenv variables for test and defer resetting them.
tg.setenv("GOPATH", "") tg.setenv("GOPATH", "")
tmp, err := ioutil.TempDir("", "") tg.tempDir("home")
if err != nil { tg.setenv(homeEnvName(), tg.path("home"))
t.Fatalf("could not create tmp home: %v", err)
}
defer os.RemoveAll(tmp)
if err := os.Mkdir(filepath.Join(tmp, "go"), 0777); err != nil {
t.Fatalf("could not create $HOME/go: %v", err)
}
tg.setenv(homeEnvName(), tmp)
const pkg = "github.com/golang/example/hello"
tg.runFail("install", pkg)
pkgPath := filepath.Join(strings.Split(pkg, "/")...)
want := fmt.Sprintf("can't load package: package %s: cannot find package \"%s\" in any of:", pkg, pkg) +
fmt.Sprintf("\n\t%s (from $GOROOT)", filepath.Join(runtime.GOROOT(), "src", pkgPath)) +
fmt.Sprintf("\n\t%s (from $GOPATH)", filepath.Join(tmp, "go", "src", pkgPath))
got := strings.TrimSpace(tg.getStderr()) tg.runFail("install", "github.com/golang/example/hello")
if got != want { tg.grepStderr(regexp.QuoteMeta(tg.path("home/go/src/github.com/golang/example/hello"))+`.*from \$GOPATH`, "expected default GOPATH")
t.Errorf("got %q; wants %q", got, want)
}
} }
// Issue 4186. go get cannot be used to download packages to $GOROOT. // Issue 4186. go get cannot be used to download packages to $GOROOT.
// Test that without GOPATH set, go get should fail. // Test that without GOPATH set, go get should fail.
func TestWithoutGOPATHGoGetFails(t *testing.T) { func TestGoGetIntoGOROOT(t *testing.T) {
testenv.MustHaveExternalNetwork(t) testenv.MustHaveExternalNetwork(t)
tg := testgo(t) tg := testgo(t)
defer tg.cleanup() defer tg.cleanup()
tg.parallel() tg.parallel()
tg.tempDir("src") tg.tempDir("src")
tg.setenv("GOPATH", "")
// Fails because GOROOT=GOPATH
tg.setenv("GOPATH", tg.path("."))
tg.setenv("GOROOT", tg.path(".")) tg.setenv("GOROOT", tg.path("."))
tg.runFail("get", "-d", "golang.org/x/codereview/cmd/hgpatch") tg.runFail("get", "-d", "github.com/golang/example/hello")
} tg.grepStderr("warning: GOPATH set to GOROOT", "go should detect GOPATH=GOROOT")
tg.grepStderr(`\$GOPATH must not be set to \$GOROOT`, "go should detect GOPATH=GOROOT")
// Test that with GOPATH=$GOROOT, go get should fail. // Fails because GOROOT=GOPATH after cleaning.
func TestWithGOPATHEqualsGOROOTGoGetFails(t *testing.T) { tg.setenv("GOPATH", tg.path(".")+"/")
testenv.MustHaveExternalNetwork(t) tg.setenv("GOROOT", tg.path("."))
tg.runFail("get", "-d", "github.com/golang/example/hello")
tg.grepStderr("warning: GOPATH set to GOROOT", "go should detect GOPATH=GOROOT")
tg.grepStderr(`\$GOPATH must not be set to \$GOROOT`, "go should detect GOPATH=GOROOT")
tg := testgo(t)
defer tg.cleanup()
tg.parallel()
tg.tempDir("src")
tg.setenv("GOPATH", tg.path(".")) tg.setenv("GOPATH", tg.path("."))
tg.setenv("GOROOT", tg.path(".")) tg.setenv("GOROOT", tg.path(".")+"/")
tg.runFail("get", "-d", "golang.org/x/codereview/cmd/hgpatch") tg.runFail("get", "-d", "github.com/golang/example/hello")
tg.grepStderr("warning: GOPATH set to GOROOT", "go should detect GOPATH=GOROOT")
tg.grepStderr(`\$GOPATH must not be set to \$GOROOT`, "go should detect GOPATH=GOROOT")
// Fails because GOROOT=$HOME/go so default GOPATH unset.
tg.tempDir("home/go")
tg.setenv(homeEnvName(), tg.path("home"))
tg.setenv("GOPATH", "")
tg.setenv("GOROOT", tg.path("home/go"))
tg.runFail("get", "-d", "github.com/golang/example/hello")
tg.grepStderr(`\$GOPATH not set`, "expected GOPATH not set")
tg.setenv(homeEnvName(), tg.path("home")+"/")
tg.setenv("GOPATH", "")
tg.setenv("GOROOT", tg.path("home/go"))
tg.runFail("get", "-d", "github.com/golang/example/hello")
tg.grepStderr(`\$GOPATH not set`, "expected GOPATH not set")
tg.setenv(homeEnvName(), tg.path("home"))
tg.setenv("GOPATH", "")
tg.setenv("GOROOT", tg.path("home/go")+"/")
tg.runFail("get", "-d", "github.com/golang/example/hello")
tg.grepStderr(`\$GOPATH not set`, "expected GOPATH not set")
} }
func TestLdflagsArgumentsWithSpacesIssue3941(t *testing.T) { func TestLdflagsArgumentsWithSpacesIssue3941(t *testing.T) {
...@@ -3744,6 +3682,13 @@ func TestMatchesOnlySubtestParallelIsOK(t *testing.T) { ...@@ -3744,6 +3682,13 @@ func TestMatchesOnlySubtestParallelIsOK(t *testing.T) {
tg.grepBoth(okPattern, "go test did not say ok") tg.grepBoth(okPattern, "go test did not say ok")
} }
// Issue 18845
func TestBenchTimeout(t *testing.T) {
tg := testgo(t)
defer tg.cleanup()
tg.run("test", "-bench", ".", "-timeout", "750ms", "testdata/timeoutbench_test.go")
}
func TestLinkXImportPathEscape(t *testing.T) { func TestLinkXImportPathEscape(t *testing.T) {
// golang.org/issue/16710 // golang.org/issue/16710
tg := testgo(t) tg := testgo(t)
......
...@@ -136,7 +136,7 @@ func main() { ...@@ -136,7 +136,7 @@ func main() {
// Diagnose common mistake: GOPATH==GOROOT. // Diagnose common mistake: GOPATH==GOROOT.
// This setting is equivalent to not setting GOPATH at all, // This setting is equivalent to not setting GOPATH at all,
// which is not what most people want when they do it. // which is not what most people want when they do it.
if gopath := buildContext.GOPATH; gopath == runtime.GOROOT() { if gopath := buildContext.GOPATH; filepath.Clean(gopath) == filepath.Clean(runtime.GOROOT()) {
fmt.Fprintf(os.Stderr, "warning: GOPATH set to GOROOT (%s) has no effect\n", gopath) fmt.Fprintf(os.Stderr, "warning: GOPATH set to GOROOT (%s) has no effect\n", gopath)
} else { } else {
for _, p := range filepath.SplitList(gopath) { for _, p := range filepath.SplitList(gopath) {
......
...@@ -7,8 +7,8 @@ package x509 ...@@ -7,8 +7,8 @@ package x509
// Possible certificate files; stop after finding one. // Possible certificate files; stop after finding one.
var certFiles = []string{ var certFiles = []string{
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6
"/etc/ssl/ca-bundle.pem", // OpenSUSE "/etc/ssl/ca-bundle.pem", // OpenSUSE
"/etc/pki/tls/cacert.pem", // OpenELEC "/etc/pki/tls/cacert.pem", // OpenELEC
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
} }
...@@ -35,15 +35,12 @@ func ctxDriverExec(ctx context.Context, execer driver.Execer, query string, nvda ...@@ -35,15 +35,12 @@ func ctxDriverExec(ctx context.Context, execer driver.Execer, query string, nvda
return nil, err return nil, err
} }
resi, err := execer.Exec(query, dargs)
if err == nil {
select { select {
default: default:
case <-ctx.Done(): case <-ctx.Done():
return resi, ctx.Err() return nil, ctx.Err()
}
} }
return resi, err return execer.Exec(query, dargs)
} }
func ctxDriverQuery(ctx context.Context, queryer driver.Queryer, query string, nvdargs []driver.NamedValue) (driver.Rows, error) { func ctxDriverQuery(ctx context.Context, queryer driver.Queryer, query string, nvdargs []driver.NamedValue) (driver.Rows, error) {
...@@ -56,16 +53,12 @@ func ctxDriverQuery(ctx context.Context, queryer driver.Queryer, query string, n ...@@ -56,16 +53,12 @@ func ctxDriverQuery(ctx context.Context, queryer driver.Queryer, query string, n
return nil, err return nil, err
} }
rowsi, err := queryer.Query(query, dargs)
if err == nil {
select { select {
default: default:
case <-ctx.Done(): case <-ctx.Done():
rowsi.Close()
return nil, ctx.Err() return nil, ctx.Err()
} }
} return queryer.Query(query, dargs)
return rowsi, err
} }
func ctxDriverStmtExec(ctx context.Context, si driver.Stmt, nvdargs []driver.NamedValue) (driver.Result, error) { func ctxDriverStmtExec(ctx context.Context, si driver.Stmt, nvdargs []driver.NamedValue) (driver.Result, error) {
...@@ -77,15 +70,12 @@ func ctxDriverStmtExec(ctx context.Context, si driver.Stmt, nvdargs []driver.Nam ...@@ -77,15 +70,12 @@ func ctxDriverStmtExec(ctx context.Context, si driver.Stmt, nvdargs []driver.Nam
return nil, err return nil, err
} }
resi, err := si.Exec(dargs)
if err == nil {
select { select {
default: default:
case <-ctx.Done(): case <-ctx.Done():
return resi, ctx.Err() return nil, ctx.Err()
}
} }
return resi, err return si.Exec(dargs)
} }
func ctxDriverStmtQuery(ctx context.Context, si driver.Stmt, nvdargs []driver.NamedValue) (driver.Rows, error) { func ctxDriverStmtQuery(ctx context.Context, si driver.Stmt, nvdargs []driver.NamedValue) (driver.Rows, error) {
...@@ -97,16 +87,12 @@ func ctxDriverStmtQuery(ctx context.Context, si driver.Stmt, nvdargs []driver.Na ...@@ -97,16 +87,12 @@ func ctxDriverStmtQuery(ctx context.Context, si driver.Stmt, nvdargs []driver.Na
return nil, err return nil, err
} }
rowsi, err := si.Query(dargs)
if err == nil {
select { select {
default: default:
case <-ctx.Done(): case <-ctx.Done():
rowsi.Close()
return nil, ctx.Err() return nil, ctx.Err()
} }
} return si.Query(dargs)
return rowsi, err
} }
var errLevelNotSupported = errors.New("sql: selected isolation level is not supported") var errLevelNotSupported = errors.New("sql: selected isolation level is not supported")
......
...@@ -305,7 +305,8 @@ type DB struct { ...@@ -305,7 +305,8 @@ type DB struct {
mu sync.Mutex // protects following fields mu sync.Mutex // protects following fields
freeConn []*driverConn freeConn []*driverConn
connRequests []chan connRequest connRequests map[uint64]chan connRequest
nextRequest uint64 // Next key to use in connRequests.
numOpen int // number of opened and pending open connections numOpen int // number of opened and pending open connections
// Used to signal the need for new connections // Used to signal the need for new connections
// a goroutine running connectionOpener() reads on this chan and // a goroutine running connectionOpener() reads on this chan and
...@@ -576,6 +577,7 @@ func Open(driverName, dataSourceName string) (*DB, error) { ...@@ -576,6 +577,7 @@ func Open(driverName, dataSourceName string) (*DB, error) {
dsn: dataSourceName, dsn: dataSourceName,
openerCh: make(chan struct{}, connectionRequestQueueSize), openerCh: make(chan struct{}, connectionRequestQueueSize),
lastPut: make(map[*driverConn]string), lastPut: make(map[*driverConn]string),
connRequests: make(map[uint64]chan connRequest),
} }
go db.connectionOpener() go db.connectionOpener()
return db, nil return db, nil
...@@ -881,6 +883,14 @@ type connRequest struct { ...@@ -881,6 +883,14 @@ type connRequest struct {
var errDBClosed = errors.New("sql: database is closed") var errDBClosed = errors.New("sql: database is closed")
// nextRequestKeyLocked returns the next connection request key.
// It is assumed that nextRequest will not overflow.
func (db *DB) nextRequestKeyLocked() uint64 {
next := db.nextRequest
db.nextRequest++
return next
}
// conn returns a newly-opened or cached *driverConn. // conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) { func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
db.mu.Lock() db.mu.Lock()
...@@ -918,12 +928,25 @@ func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn ...@@ -918,12 +928,25 @@ func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn
// Make the connRequest channel. It's buffered so that the // Make the connRequest channel. It's buffered so that the
// connectionOpener doesn't block while waiting for the req to be read. // connectionOpener doesn't block while waiting for the req to be read.
req := make(chan connRequest, 1) req := make(chan connRequest, 1)
db.connRequests = append(db.connRequests, req) reqKey := db.nextRequestKeyLocked()
db.connRequests[reqKey] = req
db.mu.Unlock() db.mu.Unlock()
// Timeout the connection request with the context. // Timeout the connection request with the context.
select { select {
case <-ctx.Done(): case <-ctx.Done():
// Remove the connection request and ensure no value has been sent
// on it after removing.
db.mu.Lock()
delete(db.connRequests, reqKey)
db.mu.Unlock()
select {
default:
case ret, ok := <-req:
if ok {
db.putConn(ret.conn, ret.err)
}
}
return nil, ctx.Err() return nil, ctx.Err()
case ret, ok := <-req: case ret, ok := <-req:
if !ok { if !ok {
...@@ -1044,12 +1067,12 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool { ...@@ -1044,12 +1067,12 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
return false return false
} }
if c := len(db.connRequests); c > 0 { if c := len(db.connRequests); c > 0 {
req := db.connRequests[0] var req chan connRequest
// This copy is O(n) but in practice faster than a linked list. var reqKey uint64
// TODO: consider compacting it down less often and for reqKey, req = range db.connRequests {
// moving the base instead? break
copy(db.connRequests, db.connRequests[1:]) }
db.connRequests = db.connRequests[:c-1] delete(db.connRequests, reqKey) // Remove from pending requests.
if err == nil { if err == nil {
dc.inUse = true dc.inUse = true
} }
...@@ -2071,14 +2094,21 @@ type Rows struct { ...@@ -2071,14 +2094,21 @@ type Rows struct {
dc *driverConn // owned; must call releaseConn when closed to release dc *driverConn // owned; must call releaseConn when closed to release
releaseConn func(error) releaseConn func(error)
rowsi driver.Rows rowsi driver.Rows
// closed value is 1 when the Rows is closed.
// Use atomic operations on value when checking value.
closed int32
cancel func() // called when Rows is closed, may be nil. cancel func() // called when Rows is closed, may be nil.
lastcols []driver.Value
lasterr error // non-nil only if closed is true
closeStmt *driverStmt // if non-nil, statement to Close on close closeStmt *driverStmt // if non-nil, statement to Close on close
// closemu prevents Rows from closing while there
// is an active streaming result. It is held for read during non-close operations
// and exclusively during close.
//
// closemu guards lasterr and closed.
closemu sync.RWMutex
closed bool
lasterr error // non-nil only if closed is true
// lastcols is only used in Scan, Next, and NextResultSet which are expected
// not not be called concurrently.
lastcols []driver.Value
} }
func (rs *Rows) initContextClose(ctx context.Context) { func (rs *Rows) initContextClose(ctx context.Context) {
...@@ -2089,7 +2119,7 @@ func (rs *Rows) initContextClose(ctx context.Context) { ...@@ -2089,7 +2119,7 @@ func (rs *Rows) initContextClose(ctx context.Context) {
// awaitDone blocks until the rows are closed or the context canceled. // awaitDone blocks until the rows are closed or the context canceled.
func (rs *Rows) awaitDone(ctx context.Context) { func (rs *Rows) awaitDone(ctx context.Context) {
<-ctx.Done() <-ctx.Done()
rs.Close() rs.close(ctx.Err())
} }
// Next prepares the next result row for reading with the Scan method. It // Next prepares the next result row for reading with the Scan method. It
...@@ -2099,8 +2129,19 @@ func (rs *Rows) awaitDone(ctx context.Context) { ...@@ -2099,8 +2129,19 @@ func (rs *Rows) awaitDone(ctx context.Context) {
// //
// Every call to Scan, even the first one, must be preceded by a call to Next. // Every call to Scan, even the first one, must be preceded by a call to Next.
func (rs *Rows) Next() bool { func (rs *Rows) Next() bool {
if rs.isClosed() { var doClose, ok bool
return false withLock(rs.closemu.RLocker(), func() {
doClose, ok = rs.nextLocked()
})
if doClose {
rs.Close()
}
return ok
}
func (rs *Rows) nextLocked() (doClose, ok bool) {
if rs.closed {
return false, false
} }
if rs.lastcols == nil { if rs.lastcols == nil {
rs.lastcols = make([]driver.Value, len(rs.rowsi.Columns())) rs.lastcols = make([]driver.Value, len(rs.rowsi.Columns()))
...@@ -2109,23 +2150,21 @@ func (rs *Rows) Next() bool { ...@@ -2109,23 +2150,21 @@ func (rs *Rows) Next() bool {
if rs.lasterr != nil { if rs.lasterr != nil {
// Close the connection if there is a driver error. // Close the connection if there is a driver error.
if rs.lasterr != io.EOF { if rs.lasterr != io.EOF {
rs.Close() return true, false
return false
} }
nextResultSet, ok := rs.rowsi.(driver.RowsNextResultSet) nextResultSet, ok := rs.rowsi.(driver.RowsNextResultSet)
if !ok { if !ok {
rs.Close() return true, false
return false
} }
// The driver is at the end of the current result set. // The driver is at the end of the current result set.
// Test to see if there is another result set after the current one. // Test to see if there is another result set after the current one.
// Only close Rows if there is no further result sets to read. // Only close Rows if there is no further result sets to read.
if !nextResultSet.HasNextResultSet() { if !nextResultSet.HasNextResultSet() {
rs.Close() doClose = true
} }
return false return doClose, false
} }
return true return false, true
} }
// NextResultSet prepares the next result set for reading. It returns true if // NextResultSet prepares the next result set for reading. It returns true if
...@@ -2137,18 +2176,28 @@ func (rs *Rows) Next() bool { ...@@ -2137,18 +2176,28 @@ func (rs *Rows) Next() bool {
// scanning. If there are further result sets they may not have rows in the result // scanning. If there are further result sets they may not have rows in the result
// set. // set.
func (rs *Rows) NextResultSet() bool { func (rs *Rows) NextResultSet() bool {
if rs.isClosed() { var doClose bool
defer func() {
if doClose {
rs.Close()
}
}()
rs.closemu.RLock()
defer rs.closemu.RUnlock()
if rs.closed {
return false return false
} }
rs.lastcols = nil rs.lastcols = nil
nextResultSet, ok := rs.rowsi.(driver.RowsNextResultSet) nextResultSet, ok := rs.rowsi.(driver.RowsNextResultSet)
if !ok { if !ok {
rs.Close() doClose = true
return false return false
} }
rs.lasterr = nextResultSet.NextResultSet() rs.lasterr = nextResultSet.NextResultSet()
if rs.lasterr != nil { if rs.lasterr != nil {
rs.Close() doClose = true
return false return false
} }
return true return true
...@@ -2157,6 +2206,8 @@ func (rs *Rows) NextResultSet() bool { ...@@ -2157,6 +2206,8 @@ func (rs *Rows) NextResultSet() bool {
// Err returns the error, if any, that was encountered during iteration. // Err returns the error, if any, that was encountered during iteration.
// Err may be called after an explicit or implicit Close. // Err may be called after an explicit or implicit Close.
func (rs *Rows) Err() error { func (rs *Rows) Err() error {
rs.closemu.RLock()
defer rs.closemu.RUnlock()
if rs.lasterr == io.EOF { if rs.lasterr == io.EOF {
return nil return nil
} }
...@@ -2167,7 +2218,9 @@ func (rs *Rows) Err() error { ...@@ -2167,7 +2218,9 @@ func (rs *Rows) Err() error {
// Columns returns an error if the rows are closed, or if the rows // Columns returns an error if the rows are closed, or if the rows
// are from QueryRow and there was a deferred error. // are from QueryRow and there was a deferred error.
func (rs *Rows) Columns() ([]string, error) { func (rs *Rows) Columns() ([]string, error) {
if rs.isClosed() { rs.closemu.RLock()
defer rs.closemu.RUnlock()
if rs.closed {
return nil, errors.New("sql: Rows are closed") return nil, errors.New("sql: Rows are closed")
} }
if rs.rowsi == nil { if rs.rowsi == nil {
...@@ -2179,7 +2232,9 @@ func (rs *Rows) Columns() ([]string, error) { ...@@ -2179,7 +2232,9 @@ func (rs *Rows) Columns() ([]string, error) {
// ColumnTypes returns column information such as column type, length, // ColumnTypes returns column information such as column type, length,
// and nullable. Some information may not be available from some drivers. // and nullable. Some information may not be available from some drivers.
func (rs *Rows) ColumnTypes() ([]*ColumnType, error) { func (rs *Rows) ColumnTypes() ([]*ColumnType, error) {
if rs.isClosed() { rs.closemu.RLock()
defer rs.closemu.RUnlock()
if rs.closed {
return nil, errors.New("sql: Rows are closed") return nil, errors.New("sql: Rows are closed")
} }
if rs.rowsi == nil { if rs.rowsi == nil {
...@@ -2329,9 +2384,13 @@ func rowsColumnInfoSetup(rowsi driver.Rows) []*ColumnType { ...@@ -2329,9 +2384,13 @@ func rowsColumnInfoSetup(rowsi driver.Rows) []*ColumnType {
// For scanning into *bool, the source may be true, false, 1, 0, or // For scanning into *bool, the source may be true, false, 1, 0, or
// string inputs parseable by strconv.ParseBool. // string inputs parseable by strconv.ParseBool.
func (rs *Rows) Scan(dest ...interface{}) error { func (rs *Rows) Scan(dest ...interface{}) error {
if rs.isClosed() { rs.closemu.RLock()
if rs.closed {
rs.closemu.RUnlock()
return errors.New("sql: Rows are closed") return errors.New("sql: Rows are closed")
} }
rs.closemu.RUnlock()
if rs.lastcols == nil { if rs.lastcols == nil {
return errors.New("sql: Scan called without calling Next") return errors.New("sql: Scan called without calling Next")
} }
...@@ -2351,20 +2410,28 @@ func (rs *Rows) Scan(dest ...interface{}) error { ...@@ -2351,20 +2410,28 @@ func (rs *Rows) Scan(dest ...interface{}) error {
// hook throug a test only mutex. // hook throug a test only mutex.
var rowsCloseHook = func() func(*Rows, *error) { return nil } var rowsCloseHook = func() func(*Rows, *error) { return nil }
func (rs *Rows) isClosed() bool {
return atomic.LoadInt32(&rs.closed) != 0
}
// Close closes the Rows, preventing further enumeration. If Next is called // Close closes the Rows, preventing further enumeration. If Next is called
// and returns false and there are no further result sets, // and returns false and there are no further result sets,
// the Rows are closed automatically and it will suffice to check the // the Rows are closed automatically and it will suffice to check the
// result of Err. Close is idempotent and does not affect the result of Err. // result of Err. Close is idempotent and does not affect the result of Err.
func (rs *Rows) Close() error { func (rs *Rows) Close() error {
if !atomic.CompareAndSwapInt32(&rs.closed, 0, 1) { return rs.close(nil)
}
func (rs *Rows) close(err error) error {
rs.closemu.Lock()
defer rs.closemu.Unlock()
if rs.closed {
return nil return nil
} }
rs.closed = true
if rs.lasterr == nil {
rs.lasterr = err
}
err := rs.rowsi.Close() err = rs.rowsi.Close()
if fn := rowsCloseHook(); fn != nil { if fn := rowsCloseHook(); fn != nil {
fn(rs, &err) fn(rs, &err)
} }
......
...@@ -153,8 +153,13 @@ func closeDB(t testing.TB, db *DB) { ...@@ -153,8 +153,13 @@ func closeDB(t testing.TB, db *DB) {
if err != nil { if err != nil {
t.Fatalf("error closing DB: %v", err) t.Fatalf("error closing DB: %v", err)
} }
if count := db.numOpenConns(); count != 0 {
t.Fatalf("%d connections still open after closing DB", count) var numOpen int
if !waitCondition(5*time.Second, 5*time.Millisecond, func() bool {
numOpen = db.numOpenConns()
return numOpen == 0
}) {
t.Fatalf("%d connections still open after closing DB", numOpen)
} }
} }
...@@ -276,6 +281,7 @@ func TestQuery(t *testing.T) { ...@@ -276,6 +281,7 @@ func TestQuery(t *testing.T) {
} }
} }
// TestQueryContext tests canceling the context while scanning the rows.
func TestQueryContext(t *testing.T) { func TestQueryContext(t *testing.T) {
db := newTestDB(t, "people") db := newTestDB(t, "people")
defer closeDB(t, db) defer closeDB(t, db)
...@@ -297,7 +303,7 @@ func TestQueryContext(t *testing.T) { ...@@ -297,7 +303,7 @@ func TestQueryContext(t *testing.T) {
for rows.Next() { for rows.Next() {
if index == 2 { if index == 2 {
cancel() cancel()
time.Sleep(10 * time.Millisecond) waitForRowsClose(t, rows, 5*time.Second)
} }
var r row var r row
err = rows.Scan(&r.age, &r.name) err = rows.Scan(&r.age, &r.name)
...@@ -313,9 +319,13 @@ func TestQueryContext(t *testing.T) { ...@@ -313,9 +319,13 @@ func TestQueryContext(t *testing.T) {
got = append(got, r) got = append(got, r)
index++ index++
} }
err = rows.Err() select {
if err != nil { case <-ctx.Done():
t.Fatalf("Err: %v", err) if err := ctx.Err(); err != context.Canceled {
t.Fatalf("context err = %v; want context.Canceled")
}
default:
t.Fatalf("context err = nil; want context.Canceled")
} }
want := []row{ want := []row{
{age: 1, name: "Alice"}, {age: 1, name: "Alice"},
...@@ -327,6 +337,7 @@ func TestQueryContext(t *testing.T) { ...@@ -327,6 +337,7 @@ func TestQueryContext(t *testing.T) {
// And verify that the final rows.Next() call, which hit EOF, // And verify that the final rows.Next() call, which hit EOF,
// also closed the rows connection. // also closed the rows connection.
waitForRowsClose(t, rows, 5*time.Second)
waitForFree(t, db, 5*time.Second, 1) waitForFree(t, db, 5*time.Second, 1)
if prepares := numPrepares(t, db) - prepares0; prepares != 1 { if prepares := numPrepares(t, db) - prepares0; prepares != 1 {
t.Errorf("executed %d Prepare statements; want 1", prepares) t.Errorf("executed %d Prepare statements; want 1", prepares)
...@@ -356,12 +367,27 @@ func waitForFree(t *testing.T, db *DB, maxWait time.Duration, want int) { ...@@ -356,12 +367,27 @@ func waitForFree(t *testing.T, db *DB, maxWait time.Duration, want int) {
} }
} }
func waitForRowsClose(t *testing.T, rows *Rows, maxWait time.Duration) {
if !waitCondition(maxWait, 5*time.Millisecond, func() bool {
rows.closemu.RLock()
defer rows.closemu.RUnlock()
return rows.closed
}) {
t.Fatal("failed to close rows")
}
}
// TestQueryContextWait ensures that rows and all internal statements are closed when
// a query context is closed during execution.
func TestQueryContextWait(t *testing.T) { func TestQueryContextWait(t *testing.T) {
db := newTestDB(t, "people") db := newTestDB(t, "people")
defer closeDB(t, db) defer closeDB(t, db)
prepares0 := numPrepares(t, db) prepares0 := numPrepares(t, db)
ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*15) // TODO(kardianos): convert this from using a timeout to using an explicit
// cancel when the query signals that is is "executing" the query.
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
// This will trigger the *fakeConn.Prepare method which will take time // This will trigger the *fakeConn.Prepare method which will take time
// performing the query. The ctxDriverPrepare func will check the context // performing the query. The ctxDriverPrepare func will check the context
...@@ -374,10 +400,15 @@ func TestQueryContextWait(t *testing.T) { ...@@ -374,10 +400,15 @@ func TestQueryContextWait(t *testing.T) {
// Verify closed rows connection after error condition. // Verify closed rows connection after error condition.
waitForFree(t, db, 5*time.Second, 1) waitForFree(t, db, 5*time.Second, 1)
if prepares := numPrepares(t, db) - prepares0; prepares != 1 { if prepares := numPrepares(t, db) - prepares0; prepares != 1 {
t.Errorf("executed %d Prepare statements; want 1", prepares) // TODO(kardianos): if the context timeouts before the db.QueryContext
// executes this check may fail. After adjusting how the context
// is canceled above revert this back to a Fatal error.
t.Logf("executed %d Prepare statements; want 1", prepares)
} }
} }
// TestTxContextWait tests the transaction behavior when the tx context is canceled
// during execution of the query.
func TestTxContextWait(t *testing.T) { func TestTxContextWait(t *testing.T) {
db := newTestDB(t, "people") db := newTestDB(t, "people")
defer closeDB(t, db) defer closeDB(t, db)
...@@ -386,6 +417,10 @@ func TestTxContextWait(t *testing.T) { ...@@ -386,6 +417,10 @@ func TestTxContextWait(t *testing.T) {
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, nil)
if err != nil { if err != nil {
// Guard against the context being canceled before BeginTx completes.
if err == context.DeadlineExceeded {
t.Skip("tx context canceled prior to first use")
}
t.Fatal(err) t.Fatal(err)
} }
...@@ -398,12 +433,6 @@ func TestTxContextWait(t *testing.T) { ...@@ -398,12 +433,6 @@ func TestTxContextWait(t *testing.T) {
} }
waitForFree(t, db, 5*time.Second, 0) waitForFree(t, db, 5*time.Second, 0)
// Ensure the dropped connection allows more connections to be made.
// Checked on DB Close.
waitCondition(5*time.Second, 5*time.Millisecond, func() bool {
return db.numOpenConns() == 0
})
} }
func TestMultiResultSetQuery(t *testing.T) { func TestMultiResultSetQuery(t *testing.T) {
...@@ -527,6 +556,63 @@ func TestQueryNamedArg(t *testing.T) { ...@@ -527,6 +556,63 @@ func TestQueryNamedArg(t *testing.T) {
} }
} }
func TestPoolExhaustOnCancel(t *testing.T) {
if testing.Short() {
t.Skip("long test")
}
db := newTestDB(t, "people")
defer closeDB(t, db)
max := 3
db.SetMaxOpenConns(max)
// First saturate the connection pool.
// Then start new requests for a connection that is cancelled after it is requested.
var saturate, saturateDone sync.WaitGroup
saturate.Add(max)
saturateDone.Add(max)
for i := 0; i < max; i++ {
go func() {
saturate.Done()
rows, err := db.Query("WAIT|500ms|SELECT|people|name,photo|")
if err != nil {
t.Fatalf("Query: %v", err)
}
rows.Close()
saturateDone.Done()
}()
}
saturate.Wait()
// Now cancel the request while it is waiting.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
for i := 0; i < max; i++ {
ctxReq, cancelReq := context.WithCancel(ctx)
go func() {
time.Sleep(time.Millisecond * 100)
cancelReq()
}()
err := db.PingContext(ctxReq)
if err != context.Canceled {
t.Fatalf("PingContext (Exhaust): %v", err)
}
}
saturateDone.Wait()
// Now try to open a normal connection.
err := db.PingContext(ctx)
if err != nil {
t.Fatalf("PingContext (Normal): %v", err)
}
}
func TestByteOwnership(t *testing.T) { func TestByteOwnership(t *testing.T) {
db := newTestDB(t, "people") db := newTestDB(t, "people")
defer closeDB(t, db) defer closeDB(t, db)
...@@ -2677,7 +2763,6 @@ func TestIssue18429(t *testing.T) { ...@@ -2677,7 +2763,6 @@ func TestIssue18429(t *testing.T) {
}() }()
} }
wg.Wait() wg.Wait()
time.Sleep(milliWait * 3 * time.Millisecond)
} }
// TestIssue18719 closes the context right before use. The sql.driverConn // TestIssue18719 closes the context right before use. The sql.driverConn
...@@ -2720,14 +2805,8 @@ func TestIssue18719(t *testing.T) { ...@@ -2720,14 +2805,8 @@ func TestIssue18719(t *testing.T) {
// Do not explicitly rollback. The rollback will happen from the // Do not explicitly rollback. The rollback will happen from the
// canceled context. // canceled context.
// Wait for connections to return to pool. cancel()
var numOpen int waitForRowsClose(t, rows, 5*time.Second)
if !waitCondition(5*time.Second, 5*time.Millisecond, func() bool {
numOpen = db.numOpenConns()
return numOpen == 0
}) {
t.Fatalf("open conns after hitting EOF = %d; want 0", numOpen)
}
} }
func TestConcurrency(t *testing.T) { func TestConcurrency(t *testing.T) {
......
...@@ -775,6 +775,20 @@ func (p *printer) marshalSimple(typ reflect.Type, val reflect.Value) (string, [] ...@@ -775,6 +775,20 @@ func (p *printer) marshalSimple(typ reflect.Type, val reflect.Value) (string, []
var ddBytes = []byte("--") var ddBytes = []byte("--")
// indirect drills into interfaces and pointers, returning the pointed-at value.
// If it encounters a nil interface or pointer, indirect returns that nil value.
// This can turn into an infinite loop given a cyclic chain,
// but it matches the Go 1 behavior.
func indirect(vf reflect.Value) reflect.Value {
for vf.Kind() == reflect.Interface || vf.Kind() == reflect.Ptr {
if vf.IsNil() {
return vf
}
vf = vf.Elem()
}
return vf
}
func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error { func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {
s := parentStack{p: p} s := parentStack{p: p}
for i := range tinfo.fields { for i := range tinfo.fields {
...@@ -816,17 +830,9 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error { ...@@ -816,17 +830,9 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {
continue continue
} }
} }
// Drill into interfaces and pointers.
// This can turn into an infinite loop given a cyclic chain,
// but it matches the Go 1 behavior.
for vf.Kind() == reflect.Interface || vf.Kind() == reflect.Ptr {
if vf.IsNil() {
return nil
}
vf = vf.Elem()
}
var scratch [64]byte var scratch [64]byte
vf = indirect(vf)
switch vf.Kind() { switch vf.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if err := emit(p, strconv.AppendInt(scratch[:0], vf.Int(), 10)); err != nil { if err := emit(p, strconv.AppendInt(scratch[:0], vf.Int(), 10)); err != nil {
...@@ -861,6 +867,7 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error { ...@@ -861,6 +867,7 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {
if err := s.trim(finfo.parents); err != nil { if err := s.trim(finfo.parents); err != nil {
return err return err
} }
vf = indirect(vf)
k := vf.Kind() k := vf.Kind()
if !(k == reflect.String || k == reflect.Slice && vf.Type().Elem().Kind() == reflect.Uint8) { if !(k == reflect.String || k == reflect.Slice && vf.Type().Elem().Kind() == reflect.Uint8) {
return fmt.Errorf("xml: bad type for comment field of %s", val.Type()) return fmt.Errorf("xml: bad type for comment field of %s", val.Type())
...@@ -901,6 +908,7 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error { ...@@ -901,6 +908,7 @@ func (p *printer) marshalStruct(tinfo *typeInfo, val reflect.Value) error {
continue continue
case fInnerXml: case fInnerXml:
vf = indirect(vf)
iface := vf.Interface() iface := vf.Interface()
switch raw := iface.(type) { switch raw := iface.(type) {
case []byte: case []byte:
......
...@@ -386,6 +386,140 @@ func ifaceptr(x interface{}) interface{} { ...@@ -386,6 +386,140 @@ func ifaceptr(x interface{}) interface{} {
return &x return &x
} }
func stringptr(x string) *string {
return &x
}
type T1 struct{}
type T2 struct{}
type T3 struct{}
type IndirComment struct {
T1 T1
Comment *string `xml:",comment"`
T2 T2
}
type DirectComment struct {
T1 T1
Comment string `xml:",comment"`
T2 T2
}
type IfaceComment struct {
T1 T1
Comment interface{} `xml:",comment"`
T2 T2
}
type IndirChardata struct {
T1 T1
Chardata *string `xml:",chardata"`
T2 T2
}
type DirectChardata struct {
T1 T1
Chardata string `xml:",chardata"`
T2 T2
}
type IfaceChardata struct {
T1 T1
Chardata interface{} `xml:",chardata"`
T2 T2
}
type IndirCDATA struct {
T1 T1
CDATA *string `xml:",cdata"`
T2 T2
}
type DirectCDATA struct {
T1 T1
CDATA string `xml:",cdata"`
T2 T2
}
type IfaceCDATA struct {
T1 T1
CDATA interface{} `xml:",cdata"`
T2 T2
}
type IndirInnerXML struct {
T1 T1
InnerXML *string `xml:",innerxml"`
T2 T2
}
type DirectInnerXML struct {
T1 T1
InnerXML string `xml:",innerxml"`
T2 T2
}
type IfaceInnerXML struct {
T1 T1
InnerXML interface{} `xml:",innerxml"`
T2 T2
}
type IndirElement struct {
T1 T1
Element *string
T2 T2
}
type DirectElement struct {
T1 T1
Element string
T2 T2
}
type IfaceElement struct {
T1 T1
Element interface{}
T2 T2
}
type IndirOmitEmpty struct {
T1 T1
OmitEmpty *string `xml:",omitempty"`
T2 T2
}
type DirectOmitEmpty struct {
T1 T1
OmitEmpty string `xml:",omitempty"`
T2 T2
}
type IfaceOmitEmpty struct {
T1 T1
OmitEmpty interface{} `xml:",omitempty"`
T2 T2
}
type IndirAny struct {
T1 T1
Any *string `xml:",any"`
T2 T2
}
type DirectAny struct {
T1 T1
Any string `xml:",any"`
T2 T2
}
type IfaceAny struct {
T1 T1
Any interface{} `xml:",any"`
T2 T2
}
var ( var (
nameAttr = "Sarah" nameAttr = "Sarah"
ageAttr = uint(12) ageAttr = uint(12)
...@@ -401,7 +535,9 @@ var marshalTests = []struct { ...@@ -401,7 +535,9 @@ var marshalTests = []struct {
Value interface{} Value interface{}
ExpectXML string ExpectXML string
MarshalOnly bool MarshalOnly bool
MarshalError string
UnmarshalOnly bool UnmarshalOnly bool
UnmarshalError string
}{ }{
// Test nil marshals to nothing // Test nil marshals to nothing
{Value: nil, ExpectXML: ``, MarshalOnly: true}, {Value: nil, ExpectXML: ``, MarshalOnly: true},
...@@ -1133,6 +1269,382 @@ var marshalTests = []struct { ...@@ -1133,6 +1269,382 @@ var marshalTests = []struct {
ExpectXML: `<NestedAndCData><A><B></B><B></B></A><![CDATA[test]]></NestedAndCData>`, ExpectXML: `<NestedAndCData><A><B></B><B></B></A><![CDATA[test]]></NestedAndCData>`,
Value: &NestedAndCData{AB: make([]string, 2), CDATA: "test"}, Value: &NestedAndCData{AB: make([]string, 2), CDATA: "test"},
}, },
// Test pointer indirection in various kinds of fields.
// https://golang.org/issue/19063
{
ExpectXML: `<IndirComment><T1></T1><!--hi--><T2></T2></IndirComment>`,
Value: &IndirComment{Comment: stringptr("hi")},
MarshalOnly: true,
},
{
ExpectXML: `<IndirComment><T1></T1><T2></T2></IndirComment>`,
Value: &IndirComment{Comment: stringptr("")},
MarshalOnly: true,
},
{
ExpectXML: `<IndirComment><T1></T1><T2></T2></IndirComment>`,
Value: &IndirComment{Comment: nil},
MarshalError: "xml: bad type for comment field of xml.IndirComment",
},
{
ExpectXML: `<IndirComment><T1></T1><!--hi--><T2></T2></IndirComment>`,
Value: &IndirComment{Comment: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceComment><T1></T1><!--hi--><T2></T2></IfaceComment>`,
Value: &IfaceComment{Comment: "hi"},
MarshalOnly: true,
},
{
ExpectXML: `<IfaceComment><T1></T1><!--hi--><T2></T2></IfaceComment>`,
Value: &IfaceComment{Comment: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceComment><T1></T1><T2></T2></IfaceComment>`,
Value: &IfaceComment{Comment: nil},
MarshalError: "xml: bad type for comment field of xml.IfaceComment",
},
{
ExpectXML: `<IfaceComment><T1></T1><T2></T2></IfaceComment>`,
Value: &IfaceComment{Comment: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectComment><T1></T1><!--hi--><T2></T2></DirectComment>`,
Value: &DirectComment{Comment: string("hi")},
},
{
ExpectXML: `<DirectComment><T1></T1><T2></T2></DirectComment>`,
Value: &DirectComment{Comment: string("")},
},
{
ExpectXML: `<IndirChardata><T1></T1>hi<T2></T2></IndirChardata>`,
Value: &IndirChardata{Chardata: stringptr("hi")},
},
{
ExpectXML: `<IndirChardata><T1></T1><![CDATA[hi]]><T2></T2></IndirChardata>`,
Value: &IndirChardata{Chardata: stringptr("hi")},
UnmarshalOnly: true, // marshals without CDATA
},
{
ExpectXML: `<IndirChardata><T1></T1><T2></T2></IndirChardata>`,
Value: &IndirChardata{Chardata: stringptr("")},
},
{
ExpectXML: `<IndirChardata><T1></T1><T2></T2></IndirChardata>`,
Value: &IndirChardata{Chardata: nil},
MarshalOnly: true, // unmarshal leaves Chardata=stringptr("")
},
{
ExpectXML: `<IfaceChardata><T1></T1>hi<T2></T2></IfaceChardata>`,
Value: &IfaceChardata{Chardata: string("hi")},
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<IfaceChardata><T1></T1><![CDATA[hi]]><T2></T2></IfaceChardata>`,
Value: &IfaceChardata{Chardata: string("hi")},
UnmarshalOnly: true, // marshals without CDATA
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<IfaceChardata><T1></T1><T2></T2></IfaceChardata>`,
Value: &IfaceChardata{Chardata: string("")},
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<IfaceChardata><T1></T1><T2></T2></IfaceChardata>`,
Value: &IfaceChardata{Chardata: nil},
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<DirectChardata><T1></T1>hi<T2></T2></DirectChardata>`,
Value: &DirectChardata{Chardata: string("hi")},
},
{
ExpectXML: `<DirectChardata><T1></T1><![CDATA[hi]]><T2></T2></DirectChardata>`,
Value: &DirectChardata{Chardata: string("hi")},
UnmarshalOnly: true, // marshals without CDATA
},
{
ExpectXML: `<DirectChardata><T1></T1><T2></T2></DirectChardata>`,
Value: &DirectChardata{Chardata: string("")},
},
{
ExpectXML: `<IndirCDATA><T1></T1><![CDATA[hi]]><T2></T2></IndirCDATA>`,
Value: &IndirCDATA{CDATA: stringptr("hi")},
},
{
ExpectXML: `<IndirCDATA><T1></T1>hi<T2></T2></IndirCDATA>`,
Value: &IndirCDATA{CDATA: stringptr("hi")},
UnmarshalOnly: true, // marshals with CDATA
},
{
ExpectXML: `<IndirCDATA><T1></T1><T2></T2></IndirCDATA>`,
Value: &IndirCDATA{CDATA: stringptr("")},
},
{
ExpectXML: `<IndirCDATA><T1></T1><T2></T2></IndirCDATA>`,
Value: &IndirCDATA{CDATA: nil},
MarshalOnly: true, // unmarshal leaves CDATA=stringptr("")
},
{
ExpectXML: `<IfaceCDATA><T1></T1><![CDATA[hi]]><T2></T2></IfaceCDATA>`,
Value: &IfaceCDATA{CDATA: string("hi")},
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<IfaceCDATA><T1></T1>hi<T2></T2></IfaceCDATA>`,
Value: &IfaceCDATA{CDATA: string("hi")},
UnmarshalOnly: true, // marshals with CDATA
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<IfaceCDATA><T1></T1><T2></T2></IfaceCDATA>`,
Value: &IfaceCDATA{CDATA: string("")},
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<IfaceCDATA><T1></T1><T2></T2></IfaceCDATA>`,
Value: &IfaceCDATA{CDATA: nil},
UnmarshalError: "cannot unmarshal into interface {}",
},
{
ExpectXML: `<DirectCDATA><T1></T1><![CDATA[hi]]><T2></T2></DirectCDATA>`,
Value: &DirectCDATA{CDATA: string("hi")},
},
{
ExpectXML: `<DirectCDATA><T1></T1>hi<T2></T2></DirectCDATA>`,
Value: &DirectCDATA{CDATA: string("hi")},
UnmarshalOnly: true, // marshals with CDATA
},
{
ExpectXML: `<DirectCDATA><T1></T1><T2></T2></DirectCDATA>`,
Value: &DirectCDATA{CDATA: string("")},
},
{
ExpectXML: `<IndirInnerXML><T1></T1><hi/><T2></T2></IndirInnerXML>`,
Value: &IndirInnerXML{InnerXML: stringptr("<hi/>")},
MarshalOnly: true,
},
{
ExpectXML: `<IndirInnerXML><T1></T1><T2></T2></IndirInnerXML>`,
Value: &IndirInnerXML{InnerXML: stringptr("")},
MarshalOnly: true,
},
{
ExpectXML: `<IndirInnerXML><T1></T1><T2></T2></IndirInnerXML>`,
Value: &IndirInnerXML{InnerXML: nil},
},
{
ExpectXML: `<IndirInnerXML><T1></T1><hi/><T2></T2></IndirInnerXML>`,
Value: &IndirInnerXML{InnerXML: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceInnerXML><T1></T1><hi/><T2></T2></IfaceInnerXML>`,
Value: &IfaceInnerXML{InnerXML: "<hi/>"},
MarshalOnly: true,
},
{
ExpectXML: `<IfaceInnerXML><T1></T1><hi/><T2></T2></IfaceInnerXML>`,
Value: &IfaceInnerXML{InnerXML: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceInnerXML><T1></T1><T2></T2></IfaceInnerXML>`,
Value: &IfaceInnerXML{InnerXML: nil},
},
{
ExpectXML: `<IfaceInnerXML><T1></T1><T2></T2></IfaceInnerXML>`,
Value: &IfaceInnerXML{InnerXML: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectInnerXML><T1></T1><hi/><T2></T2></DirectInnerXML>`,
Value: &DirectInnerXML{InnerXML: string("<hi/>")},
MarshalOnly: true,
},
{
ExpectXML: `<DirectInnerXML><T1></T1><hi/><T2></T2></DirectInnerXML>`,
Value: &DirectInnerXML{InnerXML: string("<T1></T1><hi/><T2></T2>")},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectInnerXML><T1></T1><T2></T2></DirectInnerXML>`,
Value: &DirectInnerXML{InnerXML: string("")},
MarshalOnly: true,
},
{
ExpectXML: `<DirectInnerXML><T1></T1><T2></T2></DirectInnerXML>`,
Value: &DirectInnerXML{InnerXML: string("<T1></T1><T2></T2>")},
UnmarshalOnly: true,
},
{
ExpectXML: `<IndirElement><T1></T1><Element>hi</Element><T2></T2></IndirElement>`,
Value: &IndirElement{Element: stringptr("hi")},
},
{
ExpectXML: `<IndirElement><T1></T1><Element></Element><T2></T2></IndirElement>`,
Value: &IndirElement{Element: stringptr("")},
},
{
ExpectXML: `<IndirElement><T1></T1><T2></T2></IndirElement>`,
Value: &IndirElement{Element: nil},
},
{
ExpectXML: `<IfaceElement><T1></T1><Element>hi</Element><T2></T2></IfaceElement>`,
Value: &IfaceElement{Element: "hi"},
MarshalOnly: true,
},
{
ExpectXML: `<IfaceElement><T1></T1><Element>hi</Element><T2></T2></IfaceElement>`,
Value: &IfaceElement{Element: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceElement><T1></T1><T2></T2></IfaceElement>`,
Value: &IfaceElement{Element: nil},
},
{
ExpectXML: `<IfaceElement><T1></T1><T2></T2></IfaceElement>`,
Value: &IfaceElement{Element: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectElement><T1></T1><Element>hi</Element><T2></T2></DirectElement>`,
Value: &DirectElement{Element: string("hi")},
},
{
ExpectXML: `<DirectElement><T1></T1><Element></Element><T2></T2></DirectElement>`,
Value: &DirectElement{Element: string("")},
},
{
ExpectXML: `<IndirOmitEmpty><T1></T1><OmitEmpty>hi</OmitEmpty><T2></T2></IndirOmitEmpty>`,
Value: &IndirOmitEmpty{OmitEmpty: stringptr("hi")},
},
{
// Note: Changed in Go 1.8 to include <OmitEmpty> element (because x.OmitEmpty != nil).
ExpectXML: `<IndirOmitEmpty><T1></T1><OmitEmpty></OmitEmpty><T2></T2></IndirOmitEmpty>`,
Value: &IndirOmitEmpty{OmitEmpty: stringptr("")},
MarshalOnly: true,
},
{
ExpectXML: `<IndirOmitEmpty><T1></T1><OmitEmpty></OmitEmpty><T2></T2></IndirOmitEmpty>`,
Value: &IndirOmitEmpty{OmitEmpty: stringptr("")},
UnmarshalOnly: true,
},
{
ExpectXML: `<IndirOmitEmpty><T1></T1><T2></T2></IndirOmitEmpty>`,
Value: &IndirOmitEmpty{OmitEmpty: nil},
},
{
ExpectXML: `<IfaceOmitEmpty><T1></T1><OmitEmpty>hi</OmitEmpty><T2></T2></IfaceOmitEmpty>`,
Value: &IfaceOmitEmpty{OmitEmpty: "hi"},
MarshalOnly: true,
},
{
ExpectXML: `<IfaceOmitEmpty><T1></T1><OmitEmpty>hi</OmitEmpty><T2></T2></IfaceOmitEmpty>`,
Value: &IfaceOmitEmpty{OmitEmpty: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceOmitEmpty><T1></T1><T2></T2></IfaceOmitEmpty>`,
Value: &IfaceOmitEmpty{OmitEmpty: nil},
},
{
ExpectXML: `<IfaceOmitEmpty><T1></T1><T2></T2></IfaceOmitEmpty>`,
Value: &IfaceOmitEmpty{OmitEmpty: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectOmitEmpty><T1></T1><OmitEmpty>hi</OmitEmpty><T2></T2></DirectOmitEmpty>`,
Value: &DirectOmitEmpty{OmitEmpty: string("hi")},
},
{
ExpectXML: `<DirectOmitEmpty><T1></T1><T2></T2></DirectOmitEmpty>`,
Value: &DirectOmitEmpty{OmitEmpty: string("")},
},
{
ExpectXML: `<IndirAny><T1></T1><Any>hi</Any><T2></T2></IndirAny>`,
Value: &IndirAny{Any: stringptr("hi")},
},
{
ExpectXML: `<IndirAny><T1></T1><Any></Any><T2></T2></IndirAny>`,
Value: &IndirAny{Any: stringptr("")},
},
{
ExpectXML: `<IndirAny><T1></T1><T2></T2></IndirAny>`,
Value: &IndirAny{Any: nil},
},
{
ExpectXML: `<IfaceAny><T1></T1><Any>hi</Any><T2></T2></IfaceAny>`,
Value: &IfaceAny{Any: "hi"},
MarshalOnly: true,
},
{
ExpectXML: `<IfaceAny><T1></T1><Any>hi</Any><T2></T2></IfaceAny>`,
Value: &IfaceAny{Any: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceAny><T1></T1><T2></T2></IfaceAny>`,
Value: &IfaceAny{Any: nil},
},
{
ExpectXML: `<IfaceAny><T1></T1><T2></T2></IfaceAny>`,
Value: &IfaceAny{Any: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectAny><T1></T1><Any>hi</Any><T2></T2></DirectAny>`,
Value: &DirectAny{Any: string("hi")},
},
{
ExpectXML: `<DirectAny><T1></T1><Any></Any><T2></T2></DirectAny>`,
Value: &DirectAny{Any: string("")},
},
{
ExpectXML: `<IndirFoo><T1></T1><Foo>hi</Foo><T2></T2></IndirFoo>`,
Value: &IndirAny{Any: stringptr("hi")},
UnmarshalOnly: true,
},
{
ExpectXML: `<IndirFoo><T1></T1><Foo></Foo><T2></T2></IndirFoo>`,
Value: &IndirAny{Any: stringptr("")},
UnmarshalOnly: true,
},
{
ExpectXML: `<IndirFoo><T1></T1><T2></T2></IndirFoo>`,
Value: &IndirAny{Any: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceFoo><T1></T1><Foo>hi</Foo><T2></T2></IfaceFoo>`,
Value: &IfaceAny{Any: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceFoo><T1></T1><T2></T2></IfaceFoo>`,
Value: &IfaceAny{Any: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<IfaceFoo><T1></T1><T2></T2></IfaceFoo>`,
Value: &IfaceAny{Any: nil},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectFoo><T1></T1><Foo>hi</Foo><T2></T2></DirectFoo>`,
Value: &DirectAny{Any: string("hi")},
UnmarshalOnly: true,
},
{
ExpectXML: `<DirectFoo><T1></T1><Foo></Foo><T2></T2></DirectFoo>`,
Value: &DirectAny{Any: string("")},
UnmarshalOnly: true,
},
} }
func TestMarshal(t *testing.T) { func TestMarshal(t *testing.T) {
...@@ -1142,9 +1654,19 @@ func TestMarshal(t *testing.T) { ...@@ -1142,9 +1654,19 @@ func TestMarshal(t *testing.T) {
} }
data, err := Marshal(test.Value) data, err := Marshal(test.Value)
if err != nil { if err != nil {
if test.MarshalError == "" {
t.Errorf("#%d: marshal(%#v): %s", idx, test.Value, err) t.Errorf("#%d: marshal(%#v): %s", idx, test.Value, err)
continue continue
} }
if !strings.Contains(err.Error(), test.MarshalError) {
t.Errorf("#%d: marshal(%#v): %s, want %q", idx, test.Value, err, test.MarshalError)
}
continue
}
if test.MarshalError != "" {
t.Errorf("#%d: Marshal succeeded, want error %q", idx, test.MarshalError)
continue
}
if got, want := string(data), test.ExpectXML; got != want { if got, want := string(data), test.ExpectXML; got != want {
if strings.Contains(want, "\n") { if strings.Contains(want, "\n") {
t.Errorf("#%d: marshal(%#v):\nHAVE:\n%s\nWANT:\n%s", idx, test.Value, got, want) t.Errorf("#%d: marshal(%#v):\nHAVE:\n%s\nWANT:\n%s", idx, test.Value, got, want)
...@@ -1268,8 +1790,16 @@ func TestUnmarshal(t *testing.T) { ...@@ -1268,8 +1790,16 @@ func TestUnmarshal(t *testing.T) {
} }
if err != nil { if err != nil {
t.Errorf("#%d: unexpected error: %#v", i, err) if test.UnmarshalError == "" {
} else if got, want := dest, test.Value; !reflect.DeepEqual(got, want) { t.Errorf("#%d: unmarshal(%#v): %s", i, test.ExpectXML, err)
continue
}
if !strings.Contains(err.Error(), test.UnmarshalError) {
t.Errorf("#%d: unmarshal(%#v): %s, want %q", i, test.ExpectXML, err, test.UnmarshalError)
}
continue
}
if got, want := dest, test.Value; !reflect.DeepEqual(got, want) {
t.Errorf("#%d: unmarshal(%q):\nhave %#v\nwant %#v", i, test.ExpectXML, got, want) t.Errorf("#%d: unmarshal(%q):\nhave %#v\nwant %#v", i, test.ExpectXML, got, want)
} }
} }
......
...@@ -266,7 +266,7 @@ func defaultGOPATH() string { ...@@ -266,7 +266,7 @@ func defaultGOPATH() string {
} }
if home := os.Getenv(env); home != "" { if home := os.Getenv(env); home != "" {
def := filepath.Join(home, "go") def := filepath.Join(home, "go")
if def == runtime.GOROOT() { if filepath.Clean(def) == filepath.Clean(runtime.GOROOT()) {
// Don't set the default GOPATH to GOROOT, // Don't set the default GOPATH to GOROOT,
// as that will trigger warnings from the go tool. // as that will trigger warnings from the go tool.
return "" return ""
......
...@@ -2481,17 +2481,24 @@ func TestNumMethodOnDDD(t *testing.T) { ...@@ -2481,17 +2481,24 @@ func TestNumMethodOnDDD(t *testing.T) {
} }
func TestPtrTo(t *testing.T) { func TestPtrTo(t *testing.T) {
// This block of code means that the ptrToThis field of the
// reflect data for *unsafe.Pointer is non zero, see
// https://golang.org/issue/19003
var x unsafe.Pointer
var y = &x
var z = &y
var i int var i int
typ := TypeOf(i) typ := TypeOf(z)
for i = 0; i < 100; i++ { for i = 0; i < 100; i++ {
typ = PtrTo(typ) typ = PtrTo(typ)
} }
for i = 0; i < 100; i++ { for i = 0; i < 100; i++ {
typ = typ.Elem() typ = typ.Elem()
} }
if typ != TypeOf(i) { if typ != TypeOf(z) {
t.Errorf("after 100 PtrTo and Elem, have %s, want %s", typ, TypeOf(i)) t.Errorf("after 100 PtrTo and Elem, have %s, want %s", typ, TypeOf(z))
} }
} }
......
...@@ -1174,6 +1174,7 @@ func (t *rtype) ptrTo() *rtype { ...@@ -1174,6 +1174,7 @@ func (t *rtype) ptrTo() *rtype {
pp := *prototype pp := *prototype
pp.string = &s pp.string = &s
pp.ptrToThis = nil
// For the type structures linked into the binary, the // For the type structures linked into the binary, the
// compiler provides a good hash of the string. // compiler provides a good hash of the string.
......
...@@ -61,7 +61,7 @@ static void* cpuHogDriver(void* arg __attribute__ ((unused))) { ...@@ -61,7 +61,7 @@ static void* cpuHogDriver(void* arg __attribute__ ((unused))) {
return 0; return 0;
} }
void runCPUHogThread() { void runCPUHogThread(void) {
pthread_t tid; pthread_t tid;
pthread_create(&tid, 0, cpuHogDriver, 0); pthread_create(&tid, 0, cpuHogDriver, 0);
} }
......
...@@ -15,16 +15,16 @@ package main ...@@ -15,16 +15,16 @@ package main
char *p; char *p;
static int f3() { static int f3(void) {
*p = 0; *p = 0;
return 0; return 0;
} }
static int f2() { static int f2(void) {
return f3(); return f3();
} }
static int f1() { static int f1(void) {
return f2(); return f2();
} }
......
...@@ -821,6 +821,7 @@ func (m *M) Run() int { ...@@ -821,6 +821,7 @@ func (m *M) Run() int {
haveExamples = len(m.examples) > 0 haveExamples = len(m.examples) > 0
testRan, testOk := runTests(m.deps.MatchString, m.tests) testRan, testOk := runTests(m.deps.MatchString, m.tests)
exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples) exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples)
stopAlarm()
if !testRan && !exampleRan && *matchBenchmarks == "" { if !testRan && !exampleRan && *matchBenchmarks == "" {
fmt.Fprintln(os.Stderr, "testing: warning: no tests to run") fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
} }
......
...@@ -85,6 +85,12 @@ static const String preflection_string = ...@@ -85,6 +85,12 @@ static const String preflection_string =
sizeof PREFLECTION - 1, sizeof PREFLECTION - 1,
}; };
extern const uintptr pointer_unsafe_Pointer_gc[]
__asm__ (GOSYM_PREFIX "__go_td_pN14_unsafe.Pointer$gc");
const uintptr pointer_unsafe_Pointer_gc[] __attribute__((aligned(4))) =
{sizeof(void*), GC_APTR, 0, GC_END};
const struct __go_ptr_type pointer_unsafe_Pointer = const struct __go_ptr_type pointer_unsafe_Pointer =
{ {
/* __common */ /* __common */
...@@ -104,7 +110,7 @@ const struct __go_ptr_type pointer_unsafe_Pointer = ...@@ -104,7 +110,7 @@ const struct __go_ptr_type pointer_unsafe_Pointer =
/* __equalfn */ /* __equalfn */
&runtime_pointerequal_descriptor, &runtime_pointerequal_descriptor,
/* __gc */ /* __gc */
unsafe_Pointer_gc, pointer_unsafe_Pointer_gc,
/* __reflection */ /* __reflection */
&preflection_string, &preflection_string,
/* __uncommon */ /* __uncommon */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment