zoukankan      html  css  js  c++  java
  • Go测试,功能测试,性能测试,测试辅助,go test 工具,高级测试,IO相关测试,黑盒测试,HTTP测试,进程测试

    go命令教程: http://wiki.jikexueyuan.com/project/go-command-tutorial/0.5.html

    Go测试 

    第一个测试 “Hello Test!”

    首先,在我们$GOPATH/src目录下创建hello目录,作为本文涉及到的所有示例代码的根目录。

    然后,新建名为hello.go的文件,定义一个函数hello() ,功能是返回一个由若干单词拼接成句子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package hello
     
    func hello() string {
     words := []string{"hello", "func", "in", "package", "hello"}
     wl := len(words)
     
     sentence := ""
     for key, word := range words {
      sentence += word
      if key < wl-1 {
       sentence += " "
      } else {
       sentence += "."
      }
     }
     return sentence
    }

    接着,新建名为hello_test.go的文件,填入如下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package hello
     
    import (
     "fmt"
     "testing"
    )
     
    func TestHello(t *testing.T) {
     got := hello()
     expect := "hello func in package hello."
     
     if got != expect {
      t.Errorf("got [%s] expected [%s]", got, expect)
     }
    }
     
    func BenchmarkHello(b *testing.B) {
     for i := 0; i < b.N; i++ {
      hello()
     }
    }
     
    func ExampleHello() {
     hl := hello()
     fmt.Println(hl)
     // Output: hello func in package hello.
    }

    最后,打开终端,进入hello目录,输入go test命令并回车,可以看到如下输出:

    1
    2
    PASS
    ok  hello 0.007s

    编写测试代码

    Golang的测试代码位于某个包的源代码中名称以_test.go结尾的源文件里,测试代码包含测试函数、测试辅助代码和示例函数;测试函数有以Test开头的功能测试函数和以Benchmark开头的性能测试函数两种,测试辅助代码是为测试函数服务的公共函数、初始化函数、测试数据等,示例函数则是以Example开头的说明被测试函数用法的函数。

    大部分情况下,测试代码是作为某个包的一部分,意味着它可以访问包中不可导出的元素。但在有需要的时候(如避免循环依赖)也可以修改测试文件的包名,如package hello的测试文件,包名可以设为package hello_test。

    功能测试函数

    功能测试函数需要接收*testing.T类型的单一参数t,testing.T 类型用来管理测试状态和支持格式化的测试日志。测试日志在测试执行过程中积累起来,完成后输出到标准错误输出。

    下面是从Go标准库摘抄的 testing.T类型的常用方法的用法:

    测试函数中的某条测试用例执行结果与预期不符时,调用t.Error()t.Errorf()方法记录日志并标记测试失败

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # /usr/local/go/src/bytes/compare_test.go
    func TestCompareIdenticalSlice(t *testing.T) {
     var b = []byte("Hello Gophers!")
     if Compare(b, b) != 0 {
      t.Error("b != b")
     }
     if Compare(b, b[:1]) != 1 {
      t.Error("b > b[:1] failed")
     }
    }

    使用t.Fatal()t.Fatalf()方法,在某条测试用例失败后就跳出该测试函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # /usr/local/go/src/bytes/reader_test.go
    func TestReadAfterBigSeek(t *testing.T) {
     r := NewReader([]byte("0123456789"))
     if _, err := r.Seek(1<<31+5, os.SEEK_SET); err != nil {
      t.Fatal(err)
     }
     if n, err := r.Read(make([]byte, 10)); n != 0 || err != io.EOF {
      t.Errorf("Read = %d, %v; want 0, EOF", n, err)
     }
    }

    使用t.Skip()t.Skipf()方法,跳过某条测试用例的执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # /usr/local/go/src/archive/zip/zip_test.go
    func TestZip64(t *testing.T) {
     if testing.Short() {
      t.Skip("slow test; skipping")
     }
     const size = 1 << 32 // before the "END " part
     buf := testZip64(t, size)
     testZip64DirectoryRecordLength(buf, t)
    }

    执行测试用例的过程中通过t.Log()t.Logf()记录日志

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # /usr/local/go/src/regexp/exec_test.go
    func TestFowler(t *testing.T) {
     files, err := filepath.Glob("testdata/*.dat")
     if err != nil {
      t.Fatal(err)
     }
     for _, file := range files {
      t.Log(file)
      testFowler(t, file)
     }
    }

    使用t.Parallel()标记需要并发执行的测试函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # /usr/local/go/src/runtime/stack_test.go
    func TestStackGrowth(t *testing.T) {
     t.Parallel()
     var wg sync.WaitGroup
     
     // in a normal goroutine
     wg.Add(1)
     go func() {
      defer wg.Done()
      growStack()
     }()
     wg.Wait()
     
     // ...
    }

    性能测试函数

    性能测试函数需要接收*testing.B类型的单一参数b,性能测试函数中需要循环b.N次调用被测函数。testing.B 类型用来管理测试时间和迭代运行次数,也支持和testing.T相同的方式管理测试状态和格式化的测试日志,不一样的是testing.B的日志总是会输出。

    下面是从Go标准库摘抄的 testing.B类型的常用方法的用法:

    在函数中调用t.ReportAllocs() ,启用内存使用分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # /usr/local/go/src/bufio/bufio_test.go
    func BenchmarkWriterFlush(b *testing.B) {
     b.ReportAllocs()
     bw := NewWriter(ioutil.Discard)
     str := strings.Repeat("x", 50)
     for i := 0; i < b.N; i++ {
      bw.WriteString(str)
      bw.Flush()
     }
    }

    通过 b.StopTimer() b.ResetTimer() b.StartTimer()来停止、重置、启动 时间经过和内存分配计数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # /usr/local/go/src/fmt/scan_test.go
    func BenchmarkScanInts(b *testing.B) {
     b.ResetTimer()
     ints := makeInts(intCount)
     var r RecursiveInt
     for i := b.N - 1; i >= 0; i-- {
      buf := bytes.NewBuffer(ints)
      b.StartTimer()
      scanInts(&r, buf)
      b.StopTimer()
     }
    }

    调用b.SetBytes()记录在一个操作中处理的字节数

    1
    2
    3
    4
    5
    6
    7
    # /usr/local/go/src/testing/benchmark.go
    func BenchmarkFields(b *testing.B) {
     b.SetBytes(int64(len(fieldsInput)))
     for i := 0; i < b.N; i++ {
      Fields(fieldsInput)
     }
    }

    通过b.RunParallel()方法和 *testing.PB类型的Next()方法来并发执行被测对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # /usr/local/go/src/sync/atomic/value_test.go
    func BenchmarkValueRead(b *testing.B) {
     var v Value
     v.Store(new(int))
     b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
       x := v.Load().(*int)
       if *x != 0 {
        b.Fatalf("wrong value: got %v, want 0", *x)
       }
      }
     })
    }

    测试辅助代码

    测试辅助代码是编写测试代码过程中因代码重用和代码质量考虑而产生的。主要包括如下方面:

    引入依赖的外部包,如每个测试文件都需要的 testing 包等:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # /usr/local/go/src/log/log_test.go:
    import (
     "bytes"
     "fmt"
     "os"
     "regexp"
     "strings"
     "testing"
     "time"
    )

    定义多次用到的常量和变量,测试用例数据等:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # /usr/local/go/src/log/log_test.go:
    const (
     Rdate   = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]`
     Rtime   = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`
     Rmicroseconds = `.[0-9][0-9][0-9][0-9][0-9][0-9]`
     Rline   = `(57|59):` // must update if the calls to l.Printf / l.Print below move
     Rlongfile  = `.*/[A-Za-z0-9_-]+.go:` + Rline
     Rshortfile = `[A-Za-z0-9_-]+.go:` + Rline
    )
     
    // ...
     
    var tests = []tester{
     // individual pieces:
     {0, "", ""},
     {0, "XXX", "XXX"},
     {Ldate, "", Rdate + " "},
     {Ltime, "", Rtime + " "},
     {Ltime | Lmicroseconds, "", Rtime + Rmicroseconds + " "},
     {Lmicroseconds, "", Rtime + Rmicroseconds + " "}, // microsec implies time
     {Llongfile, "", Rlongfile + " "},
     {Lshortfile, "", Rshortfile + " "},
     {Llongfile | Lshortfile, "", Rshortfile + " "}, // shortfile overrides longfile
     // everything at once:
     {Ldate | Ltime | Lmicroseconds | Llongfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rlongfile + " "},
     {Ldate | Ltime | Lmicroseconds | Lshortfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rshortfile + " "},
    }

    和普通的Golang源代码一样,测试代码中也能定义init函数,init函数会在引入外部包、定义常量、声明变量之后被自动调用,可以在init函数里编写测试相关的初始化代码。

    1
    2
    3
    4
    5
    6
    7
    8
    # /usr/local/go/src/bytes/buffer_test.go
    func init() {
     testBytes = make([]byte, N)
     for i := 0; i < N; i++ {
      testBytes[i] = 'a' + byte(i%26)
     }
     data = string(testBytes)
    }

    封装测试专用的公共函数,抽象测试专用的结构体等:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # /usr/local/go/src/log/log_test.go:
    type tester struct {
     flag int
     prefix string
     pattern string // regexp that log output must match; we add ^ and expected_text$ always
    }
     
    // ...
     
    func testPrint(t *testing.T, flag int, prefix string, pattern string, useFormat bool) {
     // ...
    }

    示例函数

    示例函数无需接收参数,但需要使用注释的 Output: 标记说明示例函数的输出值,未指定Output:标记或输出值为空的示例函数不会被执行。

    示例函数需要归属于某个 包/函数/类型/类型 的方法,具体命名规则如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func Example() { ... }  # 包的示例函数
    func ExampleF() { ... }  # 函数F的示例函数
    func ExampleT() { ... }  # 类型T的示例函数
    func ExampleT_M() { ... } # 类型T的M方法的示例函数
     
    # 多示例函数 需要跟下划线加小写字母开头的后缀
    func Example_suffix() { ... }
    func ExampleF_suffix() { ... }
    func ExampleT_suffix() { ... }
    func ExampleT_M_suffix() { ... }

    go doc 工具会解析示例函数的函数体作为对应 包/函数/类型/类型的方法 的用法。

    测试函数的相关说明,可以通过go help testfunc来查看帮助文档。

    使用 go test 工具

    Golang中通过命令行工具go test来执行测试代码,打开shell终端,进入需要测试的包所在的目录执行 go test,或者直接执行go test $pkg_name_in_gopath即可对指定的包执行测试。

    通过形如go test github.com/tabalt/...的命令可以执行$GOPATH/github.com/tabalt/目录下所有的项目的测试。go test std命令则可以执行Golang标准库的所有测试。

    如果想查看执行了哪些测试函数及函数的执行结果,可以使用-v参数:

    1
    2
    3
    4
    5
    6
    7
    [tabalt@localhost hello] go test -v
    === RUN TestHello
    --- PASS: TestHello (0.00s)
    === RUN ExampleHello
    --- PASS: ExampleHello (0.00s)
    PASS
    ok  hello 0.006s

    假设我们有很多功能测试函数,但某次测试只想执行其中的某一些,可以通过-run参数,使用正则表达式来匹配要执行的功能测试函数名。如下面指定参数后,功能测试函数TestHello不会执行到。

    1
    2
    3
    [tabalt@localhost hello] go test -v -run=xxx
    PASS
    ok  hello 0.006s

    性能测试函数默认并不会执行,需要添加-bench参数,并指定匹配性能测试函数名的正则表达式;例如,想要执行某个包中所有的性能测试函数可以添加参数-bench . 或 -bench=.。

    1
    2
    3
    4
    [tabalt@localhost hello] go test -bench=.
    PASS
    BenchmarkHello-8  2000000   657 ns/op
    ok  hello 1.993s

    想要查看性能测试时的内存情况,可以再添加参数-benchmem:

    1
    2
    3
    4
    [tabalt@localhost hello] go test -bench=. -benchmem
    PASS
    BenchmarkHello-8  2000000   666 ns/op   208 B/op   9 allocs/op
    ok  hello 2.014s

    参数-cover可以用来查看我们编写的测试对代码的覆盖率:

    1
     

    详细的覆盖率信息,可以通过-coverprofile输出到文件,并使用go tool cover来查看,用法请参考go tool cover -help 

    更多go test命令的参数及用法,可以通过go help testflag来查看帮助文档。

    高级测试技术

    IO相关测试

    testing/iotest包中实现了常用的出错的Reader和Writer,可供我们在io相关的测试中使用。主要有:

    触发数据错误dataErrReader,通过DataErrReader()函数创建

    读取一半内容的halfReader,通过HalfReader()函数创建

    读取一个byte的oneByteReader,通过OneByteReader()函数创建

    触发超时错误的timeoutReader,通过TimeoutReader()函数创建

    写入指定位数内容后停止的truncateWriter,通过TruncateWriter()函数创建

    读取时记录日志的readLogger,通过NewReadLogger()函数创建

    写入时记录日志的writeLogger,通过NewWriteLogger()函数创建

    黑盒测试

    testing/quick包实现了帮助黑盒测试的实用函数 Check和CheckEqual。

    Check函数的第1个参数是要测试的只返回bool值的黑盒函数f,Check会为f的每个参数设置任意值并多次调用,如果f返回false,Check函数会返回错误值 *CheckError。Check函数的第2个参数 可以指定一个quick.Config类型的config,传nil则会默认使用quick.defaultConfig。quick.Config结构体包含了测试运行的选项。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # /usr/local/go/src/math/big/int_test.go
    func checkMul(a, b []byte) bool {
     var x, y, z1 Int
     x.SetBytes(a)
     y.SetBytes(b)
     z1.Mul(&x, &y)
     
     var z2 Int
     z2.SetBytes(mulBytes(a, b))
     
     return z1.Cmp(&z2) == 0
    }
     
    func TestMul(t *testing.T) {
     if err := quick.Check(checkMul, nil); err != nil {
      t.Error(err)
     }
    }

    CheckEqual函数是比较给定的两个黑盒函数是否相等,函数原型如下:

    1
    func CheckEqual(f, g interface{}, config *Config) (err error)

    HTTP测试

    net/http/httptest包提供了HTTP相关代码的工具,我们的测试代码中可以创建一个临时的httptest.Server来测试发送HTTP请求的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
     fmt.Fprintln(w, "Hello, client")
    }))
    defer ts.Close()
     
    res, err := http.Get(ts.URL)
    if err != nil {
     log.Fatal(err)
    }
     
    greeting, err := ioutil.ReadAll(res.Body)
    res.Body.Close()
    if err != nil {
     log.Fatal(err)
    }
     
    fmt.Printf("%s", greeting)

    还可以创建一个应答的记录器httptest.ResponseRecorder来检测应答的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    handler := func(w http.ResponseWriter, r *http.Request) {
     http.Error(w, "something failed", http.StatusInternalServerError)
    }
     
    req, err := http.NewRequest("GET", "http://example.com/foo", nil)
    if err != nil {
     log.Fatal(err)
    }
     
    w := httptest.NewRecorder()
    handler(w, req)
     
    fmt.Printf("%d - %s", w.Code, w.Body.String())

    测试进程操作行为

    当我们被测函数有操作进程的行为,可以将被测程序作为一个子进程执行测试。下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //被测试的进程退出函数
    func Crasher() {
     fmt.Println("Going down in flames!")
     os.Exit(1)
    }
     
    //测试进程退出函数的测试函数
    func TestCrasher(t *testing.T) {
     if os.Getenv("BE_CRASHER") == "1" {
      Crasher()
      return
     }
     cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
     cmd.Env = append(os.Environ(), "BE_CRASHER=1")
     err := cmd.Run()
     if e, ok := err.(*exec.ExitError); ok && !e.Success() {
      return
     }
     t.Fatalf("process ran with err %v, want exit status 1", err)
    }

    1.报错的写法:

    1. go test -v -run="Test_main" -args "-c 1"
    2. go test -v -run="Test_main" -args "-c=1"
    3. go test -v -run="Test_main" -args -c=1

    总之,args后面的自定义参数不能带减号-

    2.正确的写法,上面“-c”去掉减号。

    3.但是代码里面不能用如下传统方法:

    1. flag.IntVar(&cmd, "c", 0, cmdhelp)
    2. flag.Parse()

    4.目前我的做法:

    1. for _, arg := range flag.Args() {
    2. arglist := strings.Split(arg, "=")
    3. if len(arglist) != 2 {
    4. arglist = strings.Split(arg, " ")
    5. }
    6. if len(arglist) != 2 {
    7. fmt.Printf(" unknown arg:%v ", arg)
    8. continue
    9. }
    10. arg0 := strings.TrimSpace(arglist[0])
    11. if arg0 == "c" || arg0 == "C" {
    12. cmd = stringToInt1(strings.TrimSpace(arglist[1]))
    13. }
    14. }
    15. if cmd == 0 {
    16. fmt.Printf(`
    17. Usage:
    18. go test -v -run="Test_main" -args "c 1"
    19. or
    20. go test -v -run="Test_main" -args c=1
    21. `)
    22. return
    23. }

    依据:

    1. go test -v -run="Test_main" -args "c 1" a=1 d=3 f="3e"
    2. === RUN Test_main
    3. os.Args:[**test.exe -test.v=true -test.run=Test_main c 1 a=1 d=3 f=3e]
    4. flag.Args():[c 1 a=1 d=3 f=3e]

    Golang测试包

    golang自带了测试包(testing),直接可以进行单元测试、性能分析、输出结果验证等。简单看着官方文档试了试,总结一下:

    目录结构和命令

    使用golang的测试包,需要遵循简单的目录结构

    测试代码放在待测试代码的目录下(一个包内),以_test.go结尾,例如如下目录结构,MyTest目录下有待测试的代码文件MyTest.go和测试代码MyTest_test.go

    .
    |-- bin
    |   `-- main
    |-- pkg
    |   `-- darwin_amd64
    |       `-- MyTest.a
    `-- src
        |-- MyTest
        |   |-- MyTest.go
        |   `-- MyTest_test.go
        `-- main
            `-- main.go

    直接在MyTest目录下执行go test命令即可,go test包含很多选项,可以参考Golang手册相关部分

    基本的测试函数

    待测试的MyTest.go源码如下:

    package MyTest
    
    import (
        "fmt"
    )
    
    type MyStruct struct {
        name string
    }
    
    func GetFieldValue(x *MyStruct) string {
        value := x.name
        return value
    }
    
    func SetFieldValue(x *MyStruct, value string) {
        fmt.Println("SetFieldValue()")
        x.name = value
    }

    对于MyTest_test.go,首先自然是要导入golang的测试包

    package MyTest
    import testing

    基本的测试函数以Test开头,后面接的字符串,第一个字符必须是数字或者大写,例如:

    func TestMytest(t *testing.T) {
        var st MyStruct
        SetFieldValue(&st, "hello")
        val := GetFieldValue(&st)
    
        if val != "hello" {
            t.Error("Set Field")
        }
    }

    T是testing包里定义的一个结构体,其包含了名为common的接口,提供了很多格式化输出的功能,golang提供了自动检查与调用测试函数的机制,测试函数的执行逻辑则需要编写者自行完成。

    执行go test 结果为:

    Call SetFieldValue()
    PASS
    ok  	/go/src/MyTest 0.004s

    如果写成Testmytest,运行时会直接忽略掉该函数。得到的结果仍然为Pass,但是不会打印“Call SetFieldValue()”,也就是说测试函数实际没有执行。
    修改一下判断条件:

    func TestMytest(t *testing.T) {
        var st MyStruct
        SetFieldValue(&st, "hello")
        val := GetFieldValue(&st)
    
        if val != "world" {
            t.Error("Set Field")
        }
    }

    这个测试用例不会通过, 而Error函数的入参就是测试不通过时打印的信息:

    SetFieldValue()
    --- FAIL: TestMytest (0.00s)
    	MyTest_test.go:15: Set Field
    FAIL
    exit status 1
    FAIL	_/Users/ronghuihe/Documents/code/golang/my/go/src/MyTest 0.004s

    这并不会中断测试程序,这个函数后面的部分仍然会执行,其他的测试函数也会执行。

    性能分析函数

    testing包还自带了性能分析功能,可评估代码执行性能。性能分析函数也可以放到前面的xxx_test.go文件内,命名以Benchmark开头,如:

    func BenchmarkGetFieldValue(b *testing.B) {
        var st MyStruct
        SetFieldValue(&st, "hello")
    
        for i := 0; i < b.N; i++ {
            GetFieldValue(&st)
        }
    }

    其中b.N的值在执行过程中会自动调整,使得循环可以执行足够多次,以便得到较为准确的单次结果:
    性能分析加-bench参数执行,即go test -bench . (不能漏了最后这个点,它表示执行所有的性能测试函数)

    BenchmarkGetFieldValue	SetFieldValue()
    SetFieldValue()
    SetFieldValue()
    SetFieldValue()
    SetFieldValue()
    SetFieldValue()
    2000000000	         0.55 ns/op

    每次循环大约需要0.55ns。这里SetFieldValue()打印了多次,是因为BenchmarkGetFieldValue被调用了多次。
    分析testing包的源码benchmark.go里的launch函数和runN函数,可以看到golang会自行调节性能分析函数的调用次数。每次执行runN都会执行一次性能测试函数,而后根据运行时间,会确定后续的内部循环执行次数b.N,直到总的运行时间达到go test -benchtime指定的时间(如果没指定默认为1s)

    注意统计时间时,执行的是使用者编写的性能分析函数,如上例中的BenchmarkGetFieldValue,而最后输出的结果,表达的是for循环里函数的性能,因此for循环之前的代码执行时间很长的话,可能导致统计误差比较大,如果需要剔除for之前代码的影响,可以在for循环之前调用ResetTimer()接口重置本次统计的时间值:

    func BenchmarkGetFieldValue(b *testing.B) {
        var st MyStruct
        SetFieldValue(&st, "hello")
    
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            GetFieldValue(&st)
        }
    }
  • 相关阅读:
    [20180808]exists and not exists.txt
    [20180806]tune2fs调整保留块百分比.txt
    [20180730]exadata与行链接.txt
    [20180801]insert导致死锁.txt
    [20180718]拷贝数据文件从dg库.txt
    [20180713]关于hash join 测试中一个疑问.txt
    [20180705]关于hash join 2.txt
    virtualbox 中ubantu虚拟机范文win7文件夹
    myeclipse10安装findbugs
    win7共享文件夹给局域网
  • 原文地址:https://www.cnblogs.com/timssd/p/9019305.html
Copyright © 2011-2022 走看看