net/rpc 当 运行 使用 'count' 标志测试多次时,服务器保持注册状态

net/rpc server stay registered when running test more than once with the 'count' flag

该程序创建一个 rpc 服务器和一个客户端,并通过 rpc 接口公开几个方法。
几个测试函数分别测试其中一种方法。

第一个测试函数注册rpc服务器:

    RPCServer := new(RPCServer)
    rpc.Register(RPCServer)
    rpc.HandleHTTP()

使用一个通道,每个函数等待服务器发出它正在 运行ning 的信号,当客户端完成时,向服务器发出关闭信号。

在测试函数中:

    // start the server
    ch := make(chan bool, 1)
    go RunRPCServer(ch)

    // wait for the server to start
    <-ch

    ...Do Some Work...

    // close the server
    ch <- true
    close(ch)

在服务器函数中:

    listener, err := net.Listen("tcp4", "")
    if err != nil {
        log.Fatal("Could not open a listener port: ", err.Error())
    }
    wg := sync.WaitGroup{}
        wg.Add(1)
        // Start the server
        rpcServer := http.Server{}
        go func() {
            go rpcServer.Serve(listener)
            // signal that the server is running
            ch <- true
            // wait for the command to close the server
            <-ch
            <-ch
            // shutdown server and exit
            if err := rpcServer.Shutdown(context.Background()); err != nil {
                log.Printf("HTTP server Shutdown: %v\n", err)
            }
            listener.Close()
            wg.Done()
    }()
    wg.Wait()

当 运行测试一次,甚至多次时,一切正常:

go test -count 1 ./src/rpcserver/
go test -count 1 ./src/rpcserver/

但是,当运行宁

go test -count 2 ./src/rpcserver/

第二个运行启动时返回错误:

--- FAIL: TestRPCServer (0.00s)
panic: http: multiple registrations for /goRPC [recovered]
panic: http: multiple registrations for /goRPC

不知道为什么

go test -count 2 ./src/rpcserver/

与以下行为不完全相同:

go test -count 1 ./src/rpcserver/
go test -count 1 ./src/rpcserver/

?

为了在连续 运行 次测试之间注销服务器,可以做些什么?

编辑: 这是说明问题的完整最小示例:

server.go:

package rpcserver

import (
    "context"
    "log"
    "net"
    "net/http"
    "net/rpc"
)

type RPCServer int

// RPC: only methods that comply with the following scheme are exported:
// func (t *T) MethodName(argType T1, replyType *T2) error

// Since RPC-exposed methods must takes an argument.
type EmptyArg struct{}

// Expose several methods via the RPC interface:
func (RpcServer *RPCServer) Method_1(emptyArg EmptyArg, reply *string) error {

    *reply = "This is Method_1"
    return nil
}

func (RpcServer *RPCServer) Method_2(emptyArg EmptyArg, reply *string) error {

    *reply = "This is Method_2"
    return nil
}

func (RpcServer *RPCServer) Method_3(emptyArg EmptyArg, reply *string) error {

    *reply = "This is Method_3"
    return nil
}

// should be called only once per process
func RegisterRPCMethods() {

    // Register the rpc methods
    RpcServer := new(RPCServer)
    rpc.Register(RpcServer)
    rpc.HandleHTTP()
}

func RunRPCServer(ch chan struct{}) {

    // Open a listener port
    listener, err := net.Listen("tcp4", "127.0.0.1:38659")
    if err != nil {
        log.Fatal("listen error:", err)
    }

    // Print some data
    log.Println("Server running")
    log.Println("network:", listener.Addr().Network())

    // Start the server
    rpcServer := http.Server{}
    go func() {
        go rpcServer.Serve(listener)
        // signal that the server is running
        ch <- struct{}{}
        // wait for the command to close the server
        <-ch
        // shutdown server and exit
        if err := rpcServer.Shutdown(context.Background()); err != nil {
            log.Printf("HTTP server Shutdown: %v\n", err)
        }
        listener.Close()
        // signal that the server is closed
        ch <- struct{}{}
    }()
}

server_test.go:


package rpcserver

import (
    "net/rpc"
    "testing"
)

func TestMethod_1(t *testing.T) {

    // call once.
    // (test functions are executed in-order)
    RegisterRPCMethods()

    // start the server
    ch := make(chan struct{})
    go RunRPCServer(ch)

    // wait for the server to start
    <-ch

    // Dial to the rpc server
    client, err := rpc.DialHTTP("tcp", "127.0.0.1:38659")
    if err != nil {
        t.Errorf("Could not dial %s: %s", "127.0.0.1:38659", err.Error())
    }
    // the called function allready asserting type.
    reply := ""
    err = client.Call("RPCServer.Method_1", EmptyArg{}, &reply)
    if err != nil {
        t.Error(err)
    }

    // close the server
    ch <- struct{}{}
    // wait for the server to close
    <-ch
    close(ch)
}

func TestMethod_2(t *testing.T) {

    // start the server
    ch := make(chan struct{})
    go RunRPCServer(ch)

    // wait for the server to start
    <-ch

    // Dial to the rpc server
    client, err := rpc.DialHTTP("tcp", "127.0.0.1:38659")
    if err != nil {
        t.Errorf("Could not dial %s: %s", "127.0.0.1:38659", err.Error())
    }
    // the called function allready asserting type.
    reply := ""
    err = client.Call("RPCServer.Method_2", EmptyArg{}, &reply)
    if err != nil {
        t.Error(err)
    }

    // close the server
    ch <- struct{}{}
    // wait for the server to close
    <-ch
    close(ch)
}

func TestMethod_3(t *testing.T) {

    // start the server
    ch := make(chan struct{})
    go RunRPCServer(ch)

    // wait for the server to start
    <-ch

    // Dial to the rpc server
    client, err := rpc.DialHTTP("tcp", "127.0.0.1:38659")
    if err != nil {
        t.Errorf("Could not dial %s: %s", "127.0.0.1:38659", err.Error())
    }
    // the called function allready asserting type.
    reply := ""
    err = client.Call("RPCServer.Method_3", EmptyArg{}, &reply)
    if err != nil {
        t.Error(err)
    }

    // close the server
    ch <- struct{}{}
    // wait for the server to close
    <-ch
    close(ch)
}

当运行宁:

go test -v -count 1 ./src/server/...

输出符合预期:

=== RUN TestMethod_1
2022/05/11 10:59:49 Server running
2022/05/11 10:59:49 network: tcp
--- PASS: TestMethod_1 (0.00s)
=== RUN TestMethod_2
2022/05/11 10:59:49 Server running
2022/05/11 10:59:49 network: tcp
--- PASS: TestMethod_2 (0.00s)
=== RUN TestMethod_3
2022/05/11 10:59:49 Server running
2022/05/11 10:59:49 network: tcp
--- PASS: TestMethod_3 (0.00s)
PASS
ok lsfrpc/src/server 0.008s

而当运行宁

go test -v -count 1 ./src/server/...
go test -v -count 1 ./src/server/...

一切正常(上面的输出,两次)

然而,当 运行ning:

go test -v -count 2 ./src/server/...

第二个开头有错误运行:

=== RUN TestMethod_1
2022/05/11 10:59:52 Server running
2022/05/11 10:59:52 network: tcp
--- PASS: TestMethod_1 (0.00s)
=== RUN TestMethod_2
2022/05/11 10:59:52 Server running
2022/05/11 10:59:52 network: tcp
--- PASS: TestMethod_2 (0.00s)
=== RUN TestMethod_3
2022/05/11 10:59:52 Server running
2022/05/11 10:59:52 network: tcp
--- PASS: TestMethod_3 (0.00s)
=== RUN TestMethod_1
--- FAIL: TestMethod_1 (0.00s)
panic: http: multiple registrations for /goRPC [recovered]
panic: http: multiple registrations for /goRPC

goroutine 63 [running]:
testing.tRunner.func1.2({0x7b9f20, 0xc0002927f0})
/home/sivsha01/go/src/testing/testing.go:1389 +0x24e
testing.tRunner.func1()
/home/sivsha01/go/src/testing/testing.go:1392 +0x39f
panic({0x7b9f20, 0xc0002927f0})
/home/sivsha01/go/src/runtime/panic.go:838 +0x207
net/http.(*ServeMux).Handle(0xb2e160, {0x83449c, 0x8}, {0x8dc3c0?, 0xc000198000})

和我上面描述的一样。这对我来说似乎是一种错误的行为。

重现问题的简化版本:

package rpcserver

import (
    "net/http"
    "os"
    "testing"
)

func TestRegister(t *testing.T) {
    t.Logf("pid: %d", os.Getpid())

    register()
}

func register() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
}

当您 运行 使用 go test -count 2 -v 进行测试时,您会发现 2 个测试 运行 在同一个过程中进行。正如您的评论中所述:RegisterRPCMethods() 每个进程只应调用一次

您可以使用 sync.Once 来确保只调用一次:

package rpcserver

import (
    "net/http"
    "os"
    "sync"
    "testing"
)

func TestRegister(t *testing.T) {
    t.Logf("pid: %d", os.Getpid())

    register()
}

var registerOnce sync.Once

func register() {
    registerOnce.Do(func() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})
    })
}

此外,您的测试将不再依赖于测试的执行顺序。而且所有的测试都可以单独运行