lib9p

Go 9P library.
Log | Files | Refs | LICENSE

commit f6789a34b394239544179747522339eca903913b
parent 6eb207b31e815193514fe458de7ef118741621f5
Author: Matsuda Kenji <info@mtkn.jp>
Date:   Tue, 26 Dec 2023 09:52:41 +0900

add test for listener and speaker goroutines

Diffstat:
Mauth_test.go | 49+++++++++++++++++++++++++++++++++----------------
Mfcall_test.go | 2+-
Mreq.go | 10+++++++---
Mserver.go | 11+++++------
Aserver2_test.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 143 insertions(+), 26 deletions(-)

diff --git a/auth_test.go b/auth_test.go @@ -23,7 +23,6 @@ func TestAuth(t *testing.T) { asr.Close() asw.Close() }() - authDone := make(chan struct{}) conn.S.Auth = func(ctx context.Context, respc chan<- *lib9p.Req) chan<- *lib9p.Req { authc := make(chan *lib9p.Req) go func() { @@ -44,7 +43,7 @@ func TestAuth(t *testing.T) { W: acw, R: acr, } - runAuth(ctx, t, r.Afid.File.(*lib9p.AuthFile), asr, asw, authDone) + runAuth(ctx, t, r.Afid.File.(*lib9p.AuthFile), asr, asw) r.Ofcall = &lib9p.RAuth{Tag: ifcall.Tag, Aqid: aqid} respc <- r } @@ -69,15 +68,14 @@ func TestAuth(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = conn.C.Write(ctx, 0, 0, 0, 5, []byte("kenji")) + _, err = conn.C.Write(ctx, 0, 0, 0, 8, []byte("password")) if err != nil { t.Fatal(err) } - _, err = conn.C.Write(ctx, 0, 0, 0, 8, []byte("password")) + _, err = conn.C.Read(ctx, 0, 0, 0, 1024) if err != nil { t.Fatal(err) } - <-authDone _, err = conn.C.Attach(ctx, 0, 1, 0, "kenji", "") if err != nil { t.Fatal(err) @@ -89,20 +87,39 @@ func TestAuth(t *testing.T) { } // Dumb state machine... -func runAuth(ctx context.Context, t *testing.T, afile *lib9p.AuthFile, r io.Reader, w io.Writer, authDone chan<- struct{}) { +func runAuth(ctx context.Context, t *testing.T, afile *lib9p.AuthFile, r io.Reader, w io.Writer) { go func() { buf := make([]byte, 10) - r.Read(buf) - t.Logf("read username: %s", string(buf)) - r.Read(buf) - t.Logf("read password: %s", string(buf)) - afile.AuthOK = true - close(authDone) - t.Log("authenticated") + uname := "kenji" + password := "password" + state := 0 for { - _, err := r.Read(buf) - if err != nil { - return + switch state { + case 0: // start + if afile.Uname != uname { + state = 2 + break + } + n, err := r.Read(buf) + if err != nil { + t.Log(err) + break + } + if string(buf[:n]) == password { + state = 1 + break + } + case 1: // accepted + afile.AuthOK = true + go func() { for { r.Read(buf) } }() + for { + w.Write([]byte("ok")) + } + case 2: // unknown user + go func() { for { r.Read(buf) } }() + for { + w.Write([]byte("unknown user")) + } } } }() diff --git a/fcall_test.go b/fcall_test.go @@ -34,7 +34,7 @@ func TestFcallParse(t *testing.T) { t.Errorf("unmarshal: %v", err) continue } - t.Log(m) + //t.Log(m) if !bytes.Equal(m.marshal(), mb) { t.Errorf("data mismatch: %v, %v != %v", m, m.marshal(), mb) } diff --git a/req.go b/req.go @@ -7,8 +7,8 @@ import ( // Req represents each requests. type Req struct { - Tag uint16 - Srv *Server + Tag uint16 + Srv *Server // Ifcall is incomming 9P message Ifcall Msg // Ofcall is response message to Ifcall. @@ -21,7 +21,7 @@ type Req struct { Cancel context.CancelFunc // Done is used by time consuming goroutines to check whether the request // is canceled. - Done <-chan struct{} + Done <-chan struct{} // listenErr is any error encountered while waiting for new 9P message. listenErr error // speakErrChan is used to report any error encountered while sending @@ -57,6 +57,10 @@ func newReqPool() *ReqPool { // Add allocates a Req with the specified tag in ReqPool rp. // It returns (nil, ErrDupTag) if there is already a Req with the specified tag. func (rp *ReqPool) add(tag uint16) (*Req, error) { + return reqPoolAdd(rp, tag) +} + +var reqPoolAdd = func(rp *ReqPool, tag uint16) (*Req, error) { rp.lock.Lock() defer rp.lock.Unlock() if _, ok := rp.m[tag]; ok { diff --git a/server.go b/server.go @@ -109,9 +109,8 @@ func (s *Server) setMSize(mSize uint32) { } // runListener runs the listener goroutine. -// Listener goroutine reads 9P messages from r and allocats Req for each -// of them, and sends it to the returned chan of *Req. -// It reports any error to the returned chan of error. +// Listener goroutine reads 9P messages from s.r by calling getReq +// and allocats Req for each of them, and sends it to the server's listenChan. func (s *Server) runListener(ctx context.Context) { rc := make(chan *Req) s.listenChan = rc @@ -129,7 +128,7 @@ func (s *Server) runListener(ctx context.Context) { // runSpeaker runs the speaker goroutine. // Speaker goroutine wait for reply Requests from the returned channel, -// and marshalls each of them into 9P messages and writes it to w. +// and marshalls each of them into 9P messages and writes it to s.w. func (s *Server) runSpeaker(ctx context.Context) { rc := make(chan *Req) s.speakChan = rc @@ -153,10 +152,10 @@ func (s *Server) runSpeaker(ctx context.Context) { }() } -// GetReq reads 9P message from r, allocates Req and returns it. +// GetReq reads 9P message from s.r, allocates Req and returns it. // Any error it encounters is embedded into the Req struct. // This function is called only by the server's listener goroutine, -// and does not need to lock r. +// and does not need to lock s.r. // The argument ctx is used to set the request's context. func (s *Server) getReq(ctx context.Context) *Req { ifcall, err := RecvMsg(s.r) diff --git a/server2_test.go b/server2_test.go @@ -0,0 +1,97 @@ +package lib9p + +import ( + "bytes" + "context" + "io" + "os" + "reflect" + "testing" +) + +// TestRunListener tests if the listener goroutine receives 9P messages +// and send them through the server's listenChan channel. +// It also checks whether the listener goroutine returns by canceling +// ctx, by checking if the server's listenChan is closed. +func TestRunListener(t *testing.T) { + tFile, err := os.Open("testdata/test_Tmsg.dat") + if err != nil { + t.Fatalf("open file: %v", err) + } + defer tFile.Close() + tFile2, err := os.Open("testdata/test_Tmsg.dat") + if err != nil { + t.Fatalf("open file: %v", err) + } + defer tFile2.Close() + s := &Server{ + rPool: newReqPool(), + r: tFile, + } + oldReqPoolAdd := reqPoolAdd + defer func() { reqPoolAdd = oldReqPoolAdd }() + reqPoolAdd = func(*ReqPool, uint16) (*Req, error) { return &Req{}, nil } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + s.runListener(ctx) + for { + want, err := RecvMsg(tFile2) + if err == io.EOF { + break + } else if err != nil { + t.Fatalf("recvmsg: %v", err) + } + r := <-s.listenChan + if r.listenErr != nil { + t.Fatalf("listenErr: %v", r.listenErr) + } + got := r.Ifcall + if !reflect.DeepEqual(want, got) { + t.Errorf("listener modified message:\n\twant: %v\n\tgot: %v", + want, got) + } + } + cancel() + if _, ok := <-s.listenChan; ok { + t.Error("listenChan not closed") + } +} + +// TestRunSpeaker tests if the speaker goroutine receives 9P messages +// and send them through the server's speakChan channel. +func TestRunSpeaker(t *testing.T) { + rFile, err := os.Open("testdata/test_Rmsg.dat") + if err != nil { + t.Fatalf("open file: %v", err) + } + defer rFile.Close() + r, w := io.Pipe() + s := &Server{w: w} + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + s.runSpeaker(ctx) + for { + want, err := readMsg(rFile) + if err == io.EOF { + break + } else if err != nil { + t.Fatalf("readmsg: %v", err) + } + msg, err := unmarshal(want) + if err != nil { + t.Fatalf("unmarshal %v", err) + } + s.speakChan <- &Req{Ofcall: msg, speakErrChan: make(chan error)} + got := make([]byte, len(want)) + _, err = io.ReadFull(r, got) + if err != nil { + t.Fatalf("readfull: %v", err) + } + if !bytes.Equal(want, got) { + t.Errorf("speaker modified message:\n\twant: %v\n\tgot: %v", + want, got) + } + } +} + +func TestGetReq(t *testing.T) {}