Ensure that no pointer handles leak during the test (#712)

This change makes sure that pointer handles are correctly cleaned up
during tests.

(cherry picked from commit e28cce87c7)
This commit is contained in:
lhchavez 2020-12-10 05:35:40 -08:00 committed by lhchavez
parent 5e09ac76e8
commit 0602194535
9 changed files with 80 additions and 34 deletions

View File

@ -55,38 +55,36 @@ func Clone(url string, path string, options *CloneOptions) (*Repository, error)
//export remoteCreateCallback //export remoteCreateCallback
func remoteCreateCallback( func remoteCreateCallback(
cremote unsafe.Pointer, out **C.git_remote,
crepo unsafe.Pointer, crepo *C.git_repository,
cname, curl *C.char, cname, curl *C.char,
payload unsafe.Pointer, handle unsafe.Pointer,
) C.int { ) C.int {
name := C.GoString(cname) name := C.GoString(cname)
url := C.GoString(curl) url := C.GoString(curl)
repo := newRepositoryFromC((*C.git_repository)(crepo)) repo := newRepositoryFromC(crepo)
// We don't own this repository, so make sure we don't try to free it repo.weak = true
runtime.SetFinalizer(repo, nil) defer repo.Free()
data, ok := pointerHandles.Get(payload).(*cloneCallbackData) data, ok := pointerHandles.Get(handle).(*cloneCallbackData)
if !ok { if !ok {
panic("invalid remote create callback") panic("invalid remote create callback")
} }
remote, ret := data.options.RemoteCreateCallback(repo, name, url) remote, ret := data.options.RemoteCreateCallback(repo, name, url)
// clear finalizer as the calling C function will
// free the remote itself
runtime.SetFinalizer(remote, nil)
if ret < 0 { if ret < 0 {
*data.errorTarget = errors.New(ErrorCode(ret).String()) *data.errorTarget = errors.New(ErrorCode(ret).String())
return C.int(ErrorCodeUser) return C.int(ErrorCodeUser)
} }
if remote == nil { if remote == nil {
panic("no remote created by callback") panic("no remote created by callback")
} }
cptr := (**C.git_remote)(cremote) *out = remote.ptr
*cptr = remote.ptr
// clear finalizer as the calling C function will
// free the remote itself
runtime.SetFinalizer(remote, nil)
return C.int(ErrorCodeOK) return C.int(ErrorCodeOK)
} }

View File

@ -74,4 +74,5 @@ func TestCloneWithCallback(t *testing.T) {
if err != nil || remote == nil { if err != nil || remote == nil {
t.Fatal("Remote was not created properly") t.Fatal("Remote was not created properly")
} }
defer remote.Free()
} }

View File

@ -1,13 +1,33 @@
package git package git
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"reflect"
"testing" "testing"
"time" "time"
) )
func TestMain(m *testing.M) {
ret := m.Run()
// Ensure that we are not leaking any pointer handles.
pointerHandles.Lock()
if len(pointerHandles.handles) > 0 {
for h, ptr := range pointerHandles.handles {
fmt.Printf("%016p: %v %+v\n", h, reflect.TypeOf(ptr), ptr)
}
panic("pointer handle list not empty")
}
pointerHandles.Unlock()
Shutdown()
os.Exit(ret)
}
func cleanupTestRepo(t *testing.T, r *Repository) { func cleanupTestRepo(t *testing.T, r *Repository) {
var err error var err error
if r.IsBare() { if r.IsBare() {

View File

@ -77,14 +77,14 @@ func (o *Object) Type() ObjectType {
return ret return ret
} }
// Owner returns a weak reference to the repository which owns this // Owner returns a weak reference to the repository which owns this object.
// object. This won't keep the underlying repository alive. // This won't keep the underlying repository alive, but it should still be
// Freed.
func (o *Object) Owner() *Repository { func (o *Object) Owner() *Repository {
ret := &Repository{ repo := newRepositoryFromC(C.git_object_owner(o.ptr))
ptr: C.git_object_owner(o.ptr),
}
runtime.KeepAlive(o) runtime.KeepAlive(o)
return ret repo.weak = true
return repo
} }
func dupObject(obj *Object, kind ObjectType) (*C.git_object, error) { func dupObject(obj *Object, kind ObjectType) (*C.git_object, error) {

View File

@ -14,15 +14,18 @@ func TestRemotePush(t *testing.T) {
remote, err := localRepo.Remotes.Create("test_push", repo.Path()) remote, err := localRepo.Remotes.Create("test_push", repo.Path())
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
seedTestRepo(t, localRepo) seedTestRepo(t, localRepo)
err = remote.Push([]string{"refs/heads/master"}, nil) err = remote.Push([]string{"refs/heads/master"}, nil)
checkFatal(t, err) checkFatal(t, err)
_, err = localRepo.References.Lookup("refs/remotes/test_push/master") ref, err := localRepo.References.Lookup("refs/remotes/test_push/master")
checkFatal(t, err) checkFatal(t, err)
defer ref.Free()
_, err = repo.References.Lookup("refs/heads/master") ref, err = repo.References.Lookup("refs/heads/master")
checkFatal(t, err) checkFatal(t, err)
defer ref.Free()
} }

View File

@ -293,12 +293,14 @@ func (v *Reference) Peel(t ObjectType) (*Object, error) {
return allocObject(cobj, v.repo), nil return allocObject(cobj, v.repo), nil
} }
// Owner returns a weak reference to the repository which owns this // Owner returns a weak reference to the repository which owns this reference.
// reference. // This won't keep the underlying repository alive, but it should still be
// Freed.
func (v *Reference) Owner() *Repository { func (v *Reference) Owner() *Repository {
return &Repository{ repo := newRepositoryFromC(C.git_reference_owner(v.ptr))
ptr: C.git_reference_owner(v.ptr), runtime.KeepAlive(v)
} repo.weak = true
return repo
} }
// Cmp compares v to ref2. It returns 0 on equality, otherwise a // Cmp compares v to ref2. It returns 0 on equality, otherwise a

View File

@ -432,10 +432,12 @@ func RemoteIsValidName(name string) bool {
return C.git_remote_is_valid_name(cname) == 1 return C.git_remote_is_valid_name(cname) == 1
} }
// Free releases the resources of the Remote.
func (r *Remote) Free() { func (r *Remote) Free() {
runtime.SetFinalizer(r, nil) runtime.SetFinalizer(r, nil)
C.git_remote_free(r.ptr) C.git_remote_free(r.ptr)
r.ptr = nil r.ptr = nil
r.repo = nil
} }
type RemoteCollection struct { type RemoteCollection struct {

View File

@ -23,9 +23,9 @@ func TestListRemotes(t *testing.T) {
repo := createTestRepo(t) repo := createTestRepo(t)
defer cleanupTestRepo(t, repo) defer cleanupTestRepo(t, repo)
_, err := repo.Remotes.Create("test", "git://foo/bar") remote, err := repo.Remotes.Create("test", "git://foo/bar")
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
expected := []string{ expected := []string{
"test", "test",
@ -53,6 +53,7 @@ func TestCertificateCheck(t *testing.T) {
remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository") remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
options := FetchOptions{ options := FetchOptions{
RemoteCallbacks: RemoteCallbacks{ RemoteCallbacks: RemoteCallbacks{
@ -73,6 +74,7 @@ func TestRemoteConnect(t *testing.T) {
remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository") remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
err = remote.ConnectFetch(nil, nil, nil) err = remote.ConnectFetch(nil, nil, nil)
checkFatal(t, err) checkFatal(t, err)
@ -85,6 +87,7 @@ func TestRemoteLs(t *testing.T) {
remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository") remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
err = remote.ConnectFetch(nil, nil, nil) err = remote.ConnectFetch(nil, nil, nil)
checkFatal(t, err) checkFatal(t, err)
@ -104,6 +107,7 @@ func TestRemoteLsFiltering(t *testing.T) {
remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository") remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
err = remote.ConnectFetch(nil, nil, nil) err = remote.ConnectFetch(nil, nil, nil)
checkFatal(t, err) checkFatal(t, err)
@ -136,11 +140,13 @@ func TestRemotePruneRefs(t *testing.T) {
err = config.SetBool("remote.origin.prune", true) err = config.SetBool("remote.origin.prune", true)
checkFatal(t, err) checkFatal(t, err)
_, err = repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository") remote, err := repo.Remotes.Create("origin", "https://github.com/libgit2/TestGitRepository")
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
remote, err := repo.Remotes.Lookup("origin") remote, err = repo.Remotes.Lookup("origin")
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
if !remote.PruneRefs() { if !remote.PruneRefs() {
t.Fatal("Expected remote to be configured to prune references") t.Fatal("Expected remote to be configured to prune references")
@ -159,6 +165,7 @@ func TestRemotePrune(t *testing.T) {
remoteRef, err := remoteRepo.CreateBranch("test-prune", commit, true) remoteRef, err := remoteRepo.CreateBranch("test-prune", commit, true)
checkFatal(t, err) checkFatal(t, err)
defer remoteRef.Free()
repo := createTestRepo(t) repo := createTestRepo(t)
defer cleanupTestRepo(t, repo) defer cleanupTestRepo(t, repo)
@ -170,12 +177,14 @@ func TestRemotePrune(t *testing.T) {
remoteUrl := fmt.Sprintf("file://%s", remoteRepo.Workdir()) remoteUrl := fmt.Sprintf("file://%s", remoteRepo.Workdir())
remote, err := repo.Remotes.Create("origin", remoteUrl) remote, err := repo.Remotes.Create("origin", remoteUrl)
checkFatal(t, err) checkFatal(t, err)
defer remote.Free()
err = remote.Fetch([]string{"test-prune"}, nil, "") err = remote.Fetch([]string{"test-prune"}, nil, "")
checkFatal(t, err) checkFatal(t, err)
_, err = repo.References.Create("refs/remotes/origin/test-prune", head, true, "remote reference") ref, err := repo.References.Create("refs/remotes/origin/test-prune", head, true, "remote reference")
checkFatal(t, err) checkFatal(t, err)
defer ref.Free()
err = remoteRef.Delete() err = remoteRef.Delete()
checkFatal(t, err) checkFatal(t, err)
@ -185,6 +194,7 @@ func TestRemotePrune(t *testing.T) {
rr, err := repo.Remotes.Lookup("origin") rr, err := repo.Remotes.Lookup("origin")
checkFatal(t, err) checkFatal(t, err)
defer rr.Free()
err = rr.ConnectFetch(nil, nil, nil) err = rr.ConnectFetch(nil, nil, nil)
checkFatal(t, err) checkFatal(t, err)
@ -192,8 +202,9 @@ func TestRemotePrune(t *testing.T) {
err = rr.Prune(nil) err = rr.Prune(nil)
checkFatal(t, err) checkFatal(t, err)
_, err = repo.References.Lookup("refs/remotes/origin/test-prune") ref, err = repo.References.Lookup("refs/remotes/origin/test-prune")
if err == nil { if err == nil {
ref.Free()
t.Fatal("Expected error getting a pruned reference") t.Fatal("Expected error getting a pruned reference")
} }
} }

View File

@ -35,6 +35,10 @@ type Repository struct {
// Stashes represents the collection of stashes and can be used to // Stashes represents the collection of stashes and can be used to
// save, apply and iterate over stash states in this repository. // save, apply and iterate over stash states in this repository.
Stashes StashCollection Stashes StashCollection
// weak indicates that a repository is a weak pointer and should not be
// freed.
weak bool
} }
func newRepositoryFromC(ptr *C.git_repository) *Repository { func newRepositoryFromC(ptr *C.git_repository) *Repository {
@ -136,8 +140,13 @@ func (v *Repository) SetRefdb(refdb *Refdb) {
} }
func (v *Repository) Free() { func (v *Repository) Free() {
ptr := v.ptr
v.ptr = nil
runtime.SetFinalizer(v, nil) runtime.SetFinalizer(v, nil)
C.git_repository_free(v.ptr) if v.weak {
return
}
C.git_repository_free(ptr)
} }
func (v *Repository) Config() (*Config, error) { func (v *Repository) Config() (*Config, error) {