丰富单元测试

This commit is contained in:
liwei committed 2020-08-11 22:29:36 +08:00
1 parent dd47b52952
commit df2dbca475
6 files changed
+345 -113

No files matched your search

+34 -18
View File
@@ -36,10 +36,9 @@ type Options struct {
} }
type FileInfo struct { type FileInfo struct {
Name string Name string
Path string Path string
Size int64 Size int64
Downloaded int64
} }
type Process interface { type Process interface {
@@ -53,25 +52,42 @@ type Controller struct {
Files map[string]*os.File Files map[string]*os.File
} }
func (c *Controller) Touch(name string, size int64) error { func NewController() *Controller {
return &Controller{Files: make(map[string]*os.File)}
}
func (c *Controller) Touch(name string, size int64) (file *os.File, err error) {
if size > 0 { if size > 0 {
return os.Truncate(name, size) err := os.Truncate(name, size)
if err != nil {
return nil, err
}
file, err = os.OpenFile(name, os.O_RDWR, 0666)
} else {
file, err = os.Create(name)
} }
_, err := os.Create(name) if err == nil {
return err c.Files[name] = file
}
return
}
func (c *Controller) Open(name string) (file *os.File, err error) {
file, err = os.OpenFile(name, os.O_RDWR, 0666)
if err == nil {
c.Files[name] = file
}
return
} }
func (c *Controller) Write(name string, offset int64, buf []byte) (int, error) { func (c *Controller) Write(name string, offset int64, buf []byte) (int, error) {
file, ok := c.Files[name] return c.Files[name].WriteAt(buf, offset)
if !ok { }
var err error
file, err = os.OpenFile(name, os.O_RDWR, 0666) func (c *Controller) Close(name string) error {
if err != nil { err := c.Files[name].Close()
return 0, err delete(c.Files, name)
} return err
c.Files[name] = file
}
return file.WriteAt(buf, offset)
} }
func (c *Controller) ContextDialer() (proxy.Dialer, error) { func (c *Controller) ContextDialer() (proxy.Dialer, error) {
+1 -2
View File
@@ -5,7 +5,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/monkeyWie/gopeed/download/common" "github.com/monkeyWie/gopeed/download/common"
"net/url" "net/url"
"os"
"strings" "strings"
) )
@@ -26,7 +25,7 @@ type Downloader struct {
} }
func NewDownloader(fetchers ...common.Fetcher) *Downloader { func NewDownloader(fetchers ...common.Fetcher) *Downloader {
d := &Downloader{Controller: &common.Controller{Files: make(map[string]*os.File)}} d := &Downloader{Controller: common.NewController()}
d.fetches = make(map[string]common.Fetcher) d.fetches = make(map[string]common.Fetcher)
for _, f := range fetchers { for _, f := range fetchers {
for _, p := range f.Protocols() { for _, p := range f.Protocols() {
+26 -7
View File
@@ -2,7 +2,6 @@ package http
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"github.com/monkeyWie/gopeed/download/common" "github.com/monkeyWie/gopeed/download/common"
"github.com/monkeyWie/gopeed/download/http/model" "github.com/monkeyWie/gopeed/download/http/model"
@@ -16,10 +15,27 @@ import (
"time" "time"
) )
type RequestError struct {
Code int
Msg string
}
func NewRequestError(code int, msg string) *RequestError {
return &RequestError{Code: code, Msg: msg}
}
func (re *RequestError) Error() string {
return fmt.Sprintf("http request fail,code:%d", re.Code)
}
type Fetcher struct { type Fetcher struct {
*common.BaseFetcher *common.BaseFetcher
} }
func NewFetcher() *Fetcher {
return &Fetcher{BaseFetcher: &common.BaseFetcher{}}
}
func (h *Fetcher) Protocols() []string { func (h *Fetcher) Protocols() []string {
return []string{"HTTP", "HTTPS"} return []string{"HTTP", "HTTPS"}
} }
@@ -36,7 +52,8 @@ func (h *Fetcher) Resolve(req *common.Request) (*common.Resource, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer httpResp.Body.Close() // 拿到响应头就关闭,不用加defer
httpResp.Body.Close()
res := &common.Resource{ res := &common.Resource{
Req: req, Req: req,
Range: false, Range: false,
@@ -57,13 +74,15 @@ func (h *Fetcher) Resolve(req *common.Request) (*common.Resource, error) {
} else if common.HttpCodeOK == httpResp.StatusCode { } else if common.HttpCodeOK == httpResp.StatusCode {
// 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码 // 返回200响应码,不支持断点下载,通过Content-Length头获取文件大小,获取不到的话可能是chunked编码
contentLength := httpResp.Header.Get(common.HttpHeaderContentLength) contentLength := httpResp.Header.Get(common.HttpHeaderContentLength)
parse, err := strconv.ParseInt(contentLength, 10, 64) if contentLength != "" {
if err != nil { parse, err := strconv.ParseInt(contentLength, 10, 64)
return nil, err if err != nil {
return nil, err
}
res.Size = parse
} }
res.Size = parse
} else { } else {
return nil, errors.New("http request fail") return nil, NewRequestError(httpResp.StatusCode, httpResp.Status)
} }
file := &common.FileInfo{ file := &common.FileInfo{
Size: res.Size, Size: res.Size,
+195 -73
View File
@@ -1,41 +1,171 @@
package http package http
import ( import (
"encoding/json" "crypto/md5"
"fmt" "encoding/hex"
"github.com/monkeyWie/gopeed/download/common" "github.com/monkeyWie/gopeed/download/common"
"golang.org/x/net/proxy"
"io" "io"
"io/ioutil"
"math/rand" "math/rand"
"net" "net"
"net/http" "net/http"
"os" "os"
"reflect"
"testing" "testing"
"time" "time"
) )
func startMockServer() net.Listener { const testDir = "./"
const buildSize = 200 * 1024 * 1024
const buildName = "build.data"
const buildFile = testDir + buildName
const downloadName = "download.data"
const downloadFile = testDir + downloadName
func TestFetcher_Resolve(t *testing.T) {
testResolve(startTestFileServer, &common.Resource{
Size: buildSize,
Range: true,
Files: []*common.FileInfo{
{
Name: buildName,
Size: buildSize,
},
},
}, t)
testResolve(startTestChunkedServer, &common.Resource{
Size: 0,
Range: false,
Files: []*common.FileInfo{
{
Name: buildName,
Size: 0,
},
},
}, t)
}
func testResolve(startTestServer func() net.Listener, want *common.Resource, t *testing.T) {
listener := startTestServer()
defer listener.Close()
fetcher := NewFetcher()
res, err := fetcher.Resolve(&common.Request{
URL: "http://" + listener.Addr().String() + "/" + buildName,
})
if err != nil {
t.Fatal(err)
}
res.Req = nil
if !reflect.DeepEqual(want, res) {
t.Errorf("Resolve error = %v, want %v", res, want)
}
}
func TestFetcher_DownloadNormal(t *testing.T) {
listener := startTestFileServer()
defer listener.Close()
// 正常下载
downloadNormal(listener, 1, t)
downloadNormal(listener, 5, t)
downloadNormal(listener, 8, t)
downloadNormal(listener, 16, t)
}
func TestFetcher_DownloadContinue(t *testing.T) {
listener := startTestFileServer()
defer listener.Close()
// 暂停继续
//downloadContinue(listener, 1, t)
downloadContinue(listener, 5, t)
//downloadContinue(listener, 8, t)
//downloadContinue(listener, 16, t)
}
func TestFetcher_DownloadChunked(t *testing.T) {
listener := startTestChunkedServer()
defer listener.Close()
// chunked编码下载
downloadNormal(listener, 1, t)
downloadContinue(listener, 1, t)
}
func TestFetcher_DownloadRetry(t *testing.T) {
listener := startTestRetryServer()
defer listener.Close()
// chunked编码下载
downloadNormal(listener, 1, t)
}
func startTestFileServer() net.Listener {
return startTestServer(func() http.Handler {
return http.FileServer(http.Dir(testDir))
})
}
func startTestChunkedServer() net.Listener {
return startTestServer(func() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+buildName, func(writer http.ResponseWriter, request *http.Request) {
file, err := os.Open(buildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux
})
}
func startTestRetryServer() net.Listener {
counter := 0
return startTestServer(func() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+buildName, func(writer http.ResponseWriter, request *http.Request) {
counter++
if counter != 1 && counter < 5 {
writer.WriteHeader(500)
return
}
file, err := os.Open(buildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux
})
}
func startTestServer(serverHandle func() http.Handler) net.Listener {
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
panic(err) panic(err)
} }
file, err := os.Create("./test.data") file, err := os.Create(buildFile)
if err != nil { if err != nil {
panic(err) panic(err)
} }
buf := make([]byte, 1024) //随机生成一个文件
for i := 0; i < 20*1024; i++ { l := int64(8192)
buf := make([]byte, l)
size := int64(0)
for {
_, err := rand.Read(buf) _, err := rand.Read(buf)
if err != nil { if err != nil {
panic(err) panic(err)
} }
file.WriteAt(buf, int64(i*1024)) if size+l >= buildSize {
file.WriteAt(buf[0:buildSize-size], size)
break
}
file.WriteAt(buf, size)
size += l
} }
server := &http.Server{}
server.Handler = serverHandle()
go server.Serve(listener)
go func() {
http.Serve(listener, http.FileServer(http.Dir("./")))
}()
return &delFileListener{ return &delFileListener{
File: file, File: file,
Listener: listener, Listener: listener,
@@ -48,93 +178,85 @@ type delFileListener struct {
} }
func (c *delFileListener) Close() error { func (c *delFileListener) Close() error {
c.File.Close() defer func() {
err := os.Remove(c.File.Name()) c.File.Close()
if err != nil { os.Remove(c.File.Name())
fmt.Println(err) os.Remove(downloadFile)
return err }()
}
return c.Listener.Close() return c.Listener.Close()
} }
func TestFetcher_Resolve(t *testing.T) { func downloadReady(listener net.Listener, connections int, t *testing.T) common.Process {
listener := startMockServer() fetcher := NewFetcher()
defer listener.Close() fetcher.InitCtl(common.NewController())
fetcher := &Fetcher{} res, err := fetcher.Resolve(&common.Request{
resolve, err := fetcher.Resolve(&common.Request{ URL: "http://" + listener.Addr().String() + "/" + buildName,
URL: "http://" + listener.Addr().String() + "/test.data",
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
buf, err := json.Marshal(resolve) process, err := fetcher.Create(res, &common.Options{
Name: downloadName,
Path: testDir,
Connections: connections,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
fmt.Printf("%s\n", string(buf)) return process
} }
func Test2(t *testing.T) { func downloadNormal(listener net.Listener, connections int, t *testing.T) {
socks5, err := proxy.SOCKS5("tcp", "127.0.0.1:1080", nil, nil) process := downloadReady(listener, connections, t)
err := process.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
conn, err := socks5.Dial("tcp", "www.baidu.com:80") want := fileMd5(buildFile)
if err != nil { got := fileMd5(downloadFile)
t.Fatal(err) if want != got {
t.Errorf("Download error = %v, want %v", got, want)
} }
conn.Write([]byte("GET"))
} }
func Test3(t *testing.T) { func downloadContinue(listener net.Listener, connections int, t *testing.T) {
var u User process := downloadReady(listener, connections, t)
fmt.Println((&u).Say1())
fmt.Println(u.Say2())
}
func Test4(t *testing.T) {
resp, err := http.Get("http://192.168.200.163:8088/go1.12.7.linux-amd64.tar.gz")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
go func() { go func() {
time.Sleep(time.Second * 3) err := process.Start()
err2 := resp.Body.Close() if err != nil {
if err2 != nil { t.Fatal(err)
fmt.Println("close err2:" + err2.Error())
} }
}() }()
_, err = io.Copy(ioutil.Discard, resp.Body) time.Sleep(time.Millisecond * 100)
if err != nil { if err := process.Pause(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
time.Sleep(time.Millisecond * 200)
if err := process.Continue(); err != nil {
t.Fatal(err)
}
want := fileMd5(buildFile)
got := fileMd5(downloadFile)
if want != got {
t.Errorf("Download error = %v, want %v", got, want)
}
} }
func Test5(t *testing.T) { func fileMd5(filePath string) string {
name := "E:\\test\\gopeed\\http\\test.txt" file, err := os.Open(filePath)
err := os.Truncate(name, 10)
if err != nil { if err != nil {
t.Fatal(err) panic(err)
} }
file, err := os.OpenFile(name, os.O_RDWR, 0666) // Tell the program to call the following function when the current function returns
if err != nil { defer file.Close()
t.Fatal(err)
} // Open a new hash interface to write to
_, err = file.WriteAt([]byte("test"), 5) hash := md5.New()
if err != nil {
t.Fatal(err) // Copy the file in the hash interface and check for any error
if _, err := io.Copy(hash, file); err != nil {
return ""
} }
} return hex.EncodeToString(hash.Sum(nil))
type User struct {
name string
}
func (u *User) Say1() string {
return "hello1 " + u.name
}
func (u User) Say2() string {
return "hello2 " + u.name
} }
+3 -3
View File
@@ -1,11 +1,11 @@
package model package model
import ( import (
common2 "github.com/monkeyWie/gopeed/download/common" "github.com/monkeyWie/gopeed/download/common"
) )
type Chunk struct { type Chunk struct {
Status common2.Status Status common.Status
Begin int64 Begin int64
End int64 End int64
Downloaded int64 Downloaded int64
@@ -13,7 +13,7 @@ type Chunk struct {
func NewChunk(begin int64, end int64) *Chunk { func NewChunk(begin int64, end int64) *Chunk {
return &Chunk{ return &Chunk{
Status: common2.DownloadStatusReady, Status: common.DownloadStatusReady,
Begin: begin, Begin: begin,
End: end, End: end,
} }
+86 -10
View File
@@ -7,6 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"path/filepath" "path/filepath"
"sync"
) )
type Process struct { type Process struct {
@@ -17,6 +18,8 @@ type Process struct {
status common.Status status common.Status
clients []*http.Response clients []*http.Response
chunks []*model.Chunk chunks []*model.Chunk
pauseLock *sync.Mutex
} }
func NewProcess(fetcher *Fetcher, res *common.Resource, opts *common.Options) *Process { func NewProcess(fetcher *Fetcher, res *common.Resource, opts *common.Options) *Process {
@@ -25,6 +28,8 @@ func NewProcess(fetcher *Fetcher, res *common.Resource, opts *common.Options) *P
res: res, res: res,
opts: opts, opts: opts,
status: common.DownloadStatusReady, status: common.DownloadStatusReady,
pauseLock: &sync.Mutex{},
} }
} }
@@ -32,11 +37,14 @@ func (p *Process) Start() error {
ctl := p.fetcher.GetCtl() ctl := p.fetcher.GetCtl()
// 创建文件 // 创建文件
name := p.name() name := p.name()
err := ctl.Touch(name, p.res.Size) _, err := ctl.Touch(name, p.res.Size)
if err != nil { if err != nil {
return err return err
} }
if p.res.Range && p.res.Size > 0 { defer p.close()
p.status = common.DownloadStatusStart
var fetchErr error
if p.res.Range {
// 每个连接平均需要下载的分块大小 // 每个连接平均需要下载的分块大小
var ( var (
chunkSize = p.res.Size / int64(p.opts.Connections) chunkSize = p.res.Size / int64(p.opts.Connections)
@@ -62,12 +70,27 @@ func (p *Process) Start() error {
errCh <- p.fetchChunk(i, name, chunk) errCh <- p.fetchChunk(i, name, chunk)
}(i) }(i)
} }
return p.wait(errCh) fetchErr = p.wait(errCh)
} else {
// 只支持单连接下载
p.chunks = make([]*model.Chunk, 1)
p.clients = make([]*http.Response, 1)
p.chunks[0] = model.NewChunk(0, 0)
fetchErr = p.fetchChunk(0, name, p.chunks[0])
} }
return nil if p.status == common.DownloadStatusPause {
return nil
}
return fetchErr
} }
func (p *Process) Pause() error { func (p *Process) Pause() error {
p.pauseLock.Lock()
defer p.pauseLock.Unlock()
if common.DownloadStatusStart != p.status {
return nil
}
p.status = common.DownloadStatusPause
if len(p.clients) > 0 { if len(p.clients) > 0 {
for _, client := range p.clients { for _, client := range p.clients {
client.Body.Close() client.Body.Close()
@@ -77,10 +100,28 @@ func (p *Process) Pause() error {
} }
func (p *Process) Continue() error { func (p *Process) Continue() error {
if func() bool {
p.pauseLock.Lock()
defer p.pauseLock.Unlock()
if common.DownloadStatusPause != p.status && common.DownloadStatusError != p.status {
return true
}
p.status = common.DownloadStatusStart
return false
}() {
return nil
}
var ( var (
ctl = p.fetcher.GetCtl()
name = p.name() name = p.name()
errCh = make(chan error) errCh = make(chan error)
) )
_, err := ctl.Open(name)
if err != nil {
return err
}
defer ctl.Close(name)
for i := 0; i < p.opts.Connections; i++ { for i := 0; i < p.opts.Connections; i++ {
go func(i int) { go func(i int) {
errCh <- p.fetchChunk(i, name, p.chunks[i]) errCh <- p.fetchChunk(i, name, p.chunks[i])
@@ -93,6 +134,10 @@ func (p *Process) Delete() error {
panic("implement me") panic("implement me")
} }
func (p *Process) close() error {
return p.fetcher.GetCtl().Close(p.name())
}
func (p *Process) name() string { func (p *Process) name() string {
// 创建文件 // 创建文件
var filename = p.opts.Name var filename = p.opts.Name
@@ -120,6 +165,14 @@ func (p *Process) wait(errCh <-chan error) error {
return nil return nil
} }
type action int
const (
actionDirect action = iota
actionContinue
actionReturn
)
func (p *Process) fetchChunk(index int, name string, chunk *model.Chunk) (err error) { func (p *Process) fetchChunk(index int, name string, chunk *model.Chunk) (err error) {
httpReq, err := BuildRequest(p.res.Req) httpReq, err := BuildRequest(p.res.Req)
if err != nil { if err != nil {
@@ -135,14 +188,37 @@ func (p *Process) fetchChunk(index int, name string, chunk *model.Chunk) (err er
resp *http.Response resp *http.Response
retry bool retry bool
) )
httpReq.Header.Set(common.HttpHeaderRange, if p.res.Range {
fmt.Sprintf(common.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End)) httpReq.Header.Set(common.HttpHeaderRange,
resp, err = client.Do(httpReq) fmt.Sprintf(common.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
if err != nil { } else {
// 连接失败,直接重试 chunk.Downloaded = 0
}
var act action
act, err = func() (action, error) {
p.pauseLock.Lock()
defer p.pauseLock.Unlock()
if p.status == common.DownloadStatusPause {
return actionReturn, nil
}
resp, err = client.Do(httpReq)
if err != nil {
// 连接失败,直接重试
return actionContinue, err
}
if resp.StatusCode != common.HttpCodeOK && resp.StatusCode != common.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status)
return actionContinue, err
}
p.clients[index] = resp
return actionDirect, nil
}()
fmt.Printf("action:%d\n", act)
if act == actionContinue {
continue continue
} else if act == actionReturn {
return nil
} }
p.clients[index] = resp
retry, err = func() (bool, error) { retry, err = func() (bool, error) {
defer resp.Body.Close() defer resp.Body.Close()
for { for {