<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      《Go 單元測試從入門到覆蓋率提升》(三)

       Go單元測試打樁框架

        Golang有常用的三個打樁框架:GoStub、GoMock、Monkey。

      1、GoStub

         GoStub 是一款輕量級的單元測試框架,接口友好,使用方式簡潔,能夠覆蓋多種常見測試場景:

      • 全局變量打樁:替換全局變量的值,方便測試不同狀態下的邏輯。

      • 函數打樁:為函數設置自定義的返回結果,模擬不同輸入輸出。

      • 過程打樁:當函數沒有返回值時(更像是過程),也可以通過打樁控制其行為。

      • 復合場景:可以將上述多種方式自由組合,滿足更復雜的測試需求。

        憑借這些特性,GoStub 非常適合在需要靈活 Mock 的單元測試中使用,尤其是在快速驗證邏輯、隔離外部依賴時效果明顯。

      GoStub安裝:go get github.com/prashantv/gostub

       ?、?為一個全局變量打樁(短暫修改這個變量的值)

      var counter = 200
      
      func TestStubExample(t *testing.T) {
          Convey("Simple Stub example", t, func() {
              // 驗證初始值
              So(counter, ShouldEqual, 200)
      
              // 執行stub操作
              stubs := gostub.Stub(&counter, 100)
              defer stubs.Reset() // 確保最后能恢復
      
              // 驗證stub后的值
              So(counter, should.Equal, 100) // 應該是100,不是200
      
              // 手動重置
              stubs.Reset()
      
              // 驗證恢復后的值
              So(counter, ShouldEqual, 200)
          })
      }

      image

       ?、?為一個函數打樁(讓函數返回固定的值)

      // GoStub/function_stub_test.go
      package gostub
      
      import (
          "testing"
      
          "github.com/prashantv/gostub"
          . "github.com/smartystreets/goconvey/convey"
      )
      
      // 給一個函數打樁
      func GetCurrentTime() int {
          return 1000 // 模擬返回當前的時間戳
      }
      
      // 使用該函數的業務邏輯
      func CalculateAge() int {
          birthTime := 500
          currentTime := GetCurrentTime()
          return currentTime - birthTime
      }
      
      // 用于打樁的函數變量
      var getCurrentTimeFunc = GetCurrentTime
      
      // 業務邏輯改寫為使用函數變量
      func CalculateAgeWithStub() int {
          birthTime := 500
          currentTime := getCurrentTimeFunc()
          return currentTime - birthTime
      }
      
      func TestFunctionStub(t *testing.T) {
          Convey("Function stub example", t, func() {
              // 正常情況下
              So(CalculateAgeWithStub(), ShouldEqual, 500)
      
              // 為函數打樁
              stubs := gostub.Stub(&getCurrentTimeFunc, func() int {
                  return 2000 // 模擬不同的當前時間
              })
              defer stubs.Reset()
      
              // 驗證打樁后的結果
              So(CalculateAgeWithStub(), ShouldEqual, 1500)
      
              // 恢復后再次驗證
              stubs.Reset()
              So(CalculateAgeWithStub(), ShouldEqual, 500)
          })
      }

      image

       ?、?為一個過程打樁

        在 GoStub 中,除了對變量和有返回值的函數進行打樁外,還支持對 過程(Procedure) 進行打樁。所謂“過程”,就是 沒有返回值的函數。在實際開發中,我們經常會把一些 資源清理、日志記錄、狀態更新 之類的邏輯寫成過程函數。

        對過程打樁的意義在于:我們可以臨時替換這些函數的行為,例如屏蔽真實的清理操作、只打印模擬日志,從而讓測試更可控,不會影響外部環境。

      // GoStub/simple_process_stub_test.go
      package gostub
      
      import (
          "testing"
      
          "github.com/prashantv/gostub"
          . "github.com/smartystreets/goconvey/convey"
      )
      
      // 要打樁的過程函數(無返回值)
      func PrintLog(msg string) {
          println("Real log:", msg)
      }
      
      // 業務函數
      func DoWork() {
          PrintLog("Starting work")
          // 做一些工作
          PrintLog("Work completed")
      }
      
      // 可打樁的函數變量
      var printLogFunc = PrintLog
      
      // 使用可打樁函數的業務版本
      func DoWorkWithStub() {
          printLogFunc("Starting work")
          // 做一些工作
          printLogFunc("Work completed")
      }
      
      func TestProcessStub(t *testing.T) {
          Convey("Simple process stub example", t, func() {
              // 標記變量
              called := false
      
              // 為過程函數打樁
              stubs := gostub.Stub(&printLogFunc, func(msg string) {
                  called = true
              })
              defer stubs.Reset()
      
              // 調用業務函數
              DoWorkWithStub()
      
              // 驗證樁函數被調用了
              So(called, ShouldBeTrue)
          })
      }

      image

       

       ?、?復雜組合場景

      // GoStub/multiple_stubs_test.go
      package gostub
      
      import (
          "testing"
          "github.com/prashantv/gostub"
          . "github.com/smartystreets/goconvey/convey"
      )
      
      var (
          name = "Alice"
          age  = 25
      )
      
      func GetCity() string {
          return "Beijing"
      }
      
      var getCityFunc = GetCity
      
      func GetUserInfo() string {
          return name + " is " + string(rune(age)) + " years old, lives in " + getCityFunc()
      }
      
      func TestMultipleStubs(t *testing.T) {
          Convey("Multiple stubs example", t, func() {
              // 使用一個stubs對象對多個目標打樁
              stubs := gostub.Stub(&name, "Bob")
              stubs.Stub(&age, 30)
              stubs.StubFunc(&getCityFunc, "Shanghai")
              defer stubs.Reset()
              
              // 驗證所有樁都生效了
              So(GetUserInfo(), ShouldEqual, "Bob is 0 years old, lives in Shanghai")
          })
      }
      //這個例子同時對兩個全局變量(name, age)和一個函數(getCityFunc)進行了打樁,使用同一個stubs對象管理,通過一次Reset()調用統一恢復。

       

      2、GoMock

         安裝:

      go get -u github.com/golang/mock/gomock
      go get -u github.com/golang/mock/mockgen
      

        在service層編寫單元測試時,通常需要對repo層進行mock。這是為了確保你的測試只關注service層本身的邏輯,而不是它所以來的外部組件(如數據庫、網絡等)。

        ?、?定義一個接口

      package db
      
      type Repository interface {
          Create(key string, value []byte) error
          Retrieve(key string) ([]byte, error)
          Update(key string, value []byte) error
          Delete(key string) error
      }

       ?、?生成mock類文件

        mockgen是GoMock提供的一個命令行工具,用來讀取接口定義,然后生成對應的mock文件。它有兩種模式:
      • 源文件模式(最常用)
      mockgen -source=./infra/db.go -destination=./mock/mock_repository.go -package=mock
      //去db.go找接口
      //去mock目錄下生成mock_repository.go
      //生成的包名叫mock
      • 反射模式
      mockgen database/sql/driver Conn,Driver
      // 表示要對database/sql/driver 包下的Conn和Driver接口生成mock

        接下來就可以生成mock_repository.go文件了,這是mockgen自動生成的,包含兩部分:

      // Automatically generated by MockGen. DO NOT EDIT!
      // Source: ./infra/db.go (interfaces: Repository)
      
      package mock
      
      import (
          gomock "github.com/golang/mock/gomock"
      )
      
      // MockRepository is a mock of Repository interface
      type MockRepository struct {
          ctrl     *gomock.Controller
          recorder *MockRepositoryMockRecorder
      }
      
      // MockRepositoryMockRecorder is the mock recorder for MockRepository
      type MockRepositoryMockRecorder struct {
          mock *MockRepository
      }
      
      // NewMockRepository creates a new mock instance
      func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
          mock := &MockRepository{ctrl: ctrl}
          mock.recorder = &MockRepositoryMockRecorder{mock}
          return mock
      }
      
      // EXPECT returns an object that allows the caller to indicate expected use
      func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
          return m.recorder
      }
      
      // Create mocks base method
      func (m *MockRepository) Create(key string, value []byte) error {
          ret := m.ctrl.Call(m, "Create", key, value)
          ret0, _ := ret[0].(error)
          return ret0
      }
      
      // Create indicates an expected call of Create
      func (mr *MockRepositoryMockRecorder) Create(key, value interface{}) *gomock.Call {
         return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), key, value)
      }
      
      // Retrieve mocks base method
      func (m *MockRepository) Retrieve(key string) ([]byte, error) {
         ret := m.ctrl.Call(m, "Retrieve", key)
         ret0, _ := ret[0].([]byte)
         ret1, _ := ret[1].(error)
         return ret0, ret1
      }
      
      // Retrieve indicates an expected call of Retrieve
      func (mr *MockRepositoryMockRecorder) Retrieve(key interface{}) *gomock.Call {
         return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockRepository)(nil).Retrieve), key)
      }
      
      // Update mocks base method
      func (m *MockRepository) Update(key string, value []byte) error {
         ret := m.ctrl.Call(m, "Update", key, value)
         ret0, _ := ret[0].(error)
         return ret0
      }
      
      // Update indicates an expected call of Update
      func (mr *MockRepositoryMockRecorder) Update(key, value interface{}) *gomock.Call {
         return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), key, value)
      }
      
      // Delete mocks base method
      func (m *MockRepository) Delete(key string) error {
         ret := m.ctrl.Call(m, "Delete", key)
         ret0, _ := ret[0].(error)
         return ret0
      }
      
      // Delete indicates an expected call of Delete
      func (mr *MockRepositoryMockRecorder) Delete(key interface{}) *gomock.Call {
         return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), key)
      }

        MYSQL.go編寫

      package MYSQL
      
      import db "GoExample/GoMock/infra"
      
      type MYSQL struct {
          DB db.Repository
      }
      
      func NewMySQL(db db.Repository) *MYSQL {
          return &MySQL{DB: db}
      }
      
      func (mysql *MySQL) CreateData(key string, value []byte) error {
          return mysql.DB.Retrieve(key, value)
      }
      
      func (mysql *MySQL) GetData(key string) ([]byte, error) {
         return mysql.DB.Retrieve(key)
      }
      
      func (mysql *MySQL) DeleteData(key string) error {
         return mysql.DB.Delete(key)
      }
      
      func (mysql *MySQL) UpdateData(key string, value []byte) error {
         return mysql.DB.Update(key, value)
      }

       測試用例MYSQL_test.go編寫

      package MYSQL
      
      import (
          "testing"
          "GoExample/GoMock/mock"
          "fmt"
          "github.com/golang/mock/gomock"
      )
      
      func TestMYSQL_CreateData(t *testing.T) {
          // 1.創建gomock控制器
          // 定義了mock對象的作用域和生命周期,以及期望
          ctr := gomock.NewController(t)
          //2. 結束時檢查期望有沒有滿足
          defer ctr.Finish()
      
          key := "Hello"
          value := []byte("Go")
      
          //3.生成一個假的數據庫對象
          mockRepo := mock_db.NewMockRepository(ctrl)
          //4.設定期望:若調用Create(”Hello", "Go"), 就返回nil
          mockRepo.EXPECT().Create(key, value).Return(nil)
          //5. 將假的repo對象注入到mySQL對象中(后續需要通過mySQL調用綁定的方法)
          mySQL := NewMYSQL(mockRepo)
          //6. 調用CreateData, 會轉發到mockRepo.Create
          err := mySQL.CreateData(key, value)
      
          if err != nil {
              //7.正常情況下不會打印,因為 err 應該是 nil
              fmt.Println(err)
          }
      }
      
      func TestMySQL_GetData(t *testing.T) {
          ctr := gomock.NewController(t)
          defer ctr.Finish()
          key := "Hello"
          value := []byte("Go")
          mockRepo := mock_db.NewMockRepository(ctr)
          // InOrder是期望下面的方法按順序調用,若調用順序不一致,就會觸發測試失敗
          gomock.InOrder(
              mockRepo.EXPECT().Retrieve(key).Return(value, nil),
          )
          mySQL := NewMySQL(mockRepo)
          bytes, err := mySQL.GetData(key)
          if err != nil {
              fmt.Println(err)
          } else {
              fmt.Println(string(bytes))
          }
      }
      
      func TestMySQL_UpdateData(t *testing.T) {
          ctr := gomock.NewController(t)
          defer ctr.Finish()
          var key string = "Hello"
          var value []byte = []byte("Go")
          mockRepository := mock_db.NewMockRepository(ctr)
          gomock.InOrder(
             mockRepository.EXPECT().Update(key, value).Return(nil),
          )
          mySQL := NewMySQL(mockRepository)
          err := mySQL.UpdateData(key, value)
          if err != nil {
              fmt.Println(err)
          }
      }
      
      func TestMySQL_DeleteData(t *testing.T) {
          ctr := gomock.NewController(t)
          defer ctr.Finish()
          var key string = "Hello"
          mockRepository := mock_db.NewMockRepository(ctr)
          gomock.InOrder(
              mockRepository.EXPECT().Delete(key).Return(nil),
          )
          mySQL := NewMySQL(mockRepository)
          err := mySQL.DeleteData(key)
          if err != nil {
              fmt.Println(err)
          }
      }

      3、Monkey

         前面提到,GoStub 非常適合處理全局變量、函數和過程的打樁,配合 GoMock 還能完成接口的替換。但是當我們遇到 結構體方法 時,問題就變得棘手了。

        在 Go 語言里,方法是與結構體綁定的,像 srv.GetUser(1) 這種調用,GoStub 并不能直接替換。如果項目里大量使用 面向對象風格(struct + 方法),就不得不額外抽象出接口,再通過接口去 mock。這種做法雖然可行,但會讓測試代碼和業務代碼之間產生一定的“距離”,降低了測試的直觀性和靈活性。

        為了填補這一空白,就有了另一類更“激進”的工具 —— Monkey 補?。∕onkey Patching)。Monkey 能夠在運行時動態替換函數或方法的實現,從而讓我們可以直接對結構體方法進行打樁,而無需額外抽象接口。當然,Monkey 的這種方式并不是沒有代價:它依賴底層的 unsafereflect 技術,雖然在測試階段能帶來極大便利,但在生產環境中需要謹慎使用。

        安裝命令:go get github.com/bouk/monkey

        ?。?)為一個函數打樁

      // Exec是infra層的一個操作函數:
      func Exec(cmd string, args ...string) (string, error) {
          cmdpath, err := exec.LookPath(cmd)
          if err != nil {
              fmt.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
              return "", infra.ErrExecLookPathFailed
          }
      
          var output []byte
          output, err = exec.Command(cmdpath, args...).CombinedOutput()
          if err != nil {
              fmt.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
              return "", infra.ErrExecCombinedOutputFailed
          }
          fmt.Println("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]")
          return string(output), nil
      }
      // 在這個函數中調用了庫函數exec.LoopPath和exec.Command,因此Exec函數的返回值和運行時
      // 的底層環境密切相關。若在被測函數中調用了Exec函數,應該根據用例的場景對Exec函數打樁
      // 具體的意思就是,打樁的是依賴,里面調用的兩個庫函數是依賴

       

      import (
          "testing"
          . "github.com/smartystreets/goconvey/convey"
          . "github.com/bouk/monkey"
          "infra/osencap"
      )
      
      const any = "any"
      
      func TestExec(t *testing.T) {
          Convey("test has digit", t, func() {
              Convey("for succ", func() {
                  outputExpect := "xxx-vethName100-yyy"
                  // 運行時打樁,將進程內所有對osencap.Exec的調用,都跳轉到這個匿名函數上
                  guard := Patch(
                   osencap.Exec,
                   func(_ string, _ ...string) (string, error) {
                       return outputExpect, nil
                   })
                  defer guard.Unpatch()
                  output, err := osencap.Exec(any, any)
                  So(output, ShouldEqual, outputExpect)
                  So(err, ShouldBeNil)
              })
          })
      }
      // Patch的第一個參數是:要被替換的目標函數”的函數標識符
      // 第二個參數是:替身函數,一般寫成匿名函數
      // guard.Unpatch() 取消本次補丁,恢復原實現
      // UnpatchAll() 一次性移除所有補?。ǖ鄶禃r候用defer guard.Unpatch()更安全)

      注意:

      • Monkey在進程級生效,并發/并行的用例可能互相影響
      • 這個補丁對進程內所有調用點生效,所以務必defer

         (2)為一個過程打樁

      // 當一個函數沒有返回值時,該函數一般稱為過程。
      func TestDestroyResource(t *testing.T) {
          called := false
          guard := Patch(DestroyResource, func(_ string) {
              called = true
          })
          defer guard.Unpatch()
      
          DestroyResource("abc") // 實際不會執行原邏輯
      
          if !called {
              t.Errorf("expected patched DestroyResource to be called")
          }
      }

       ?。?)為一個方法打樁

        假如有一個服務(如任務調度服務),不只跑一份,而是啟動了好幾個實例(進程),那么此時用Etcd做選舉,選出一個“Master”。Master負責把所有任務分配給各個實例,然后把分配的結果寫到Etcd。剩下的實例Node通過Watch功能實時監聽Etcd的任務分配結果,收到任務列表后,每個實例根據自己的instanceId過濾,只挑出屬于自己的任務去執行。

        現在我們需要給Etcd.Get()方法打樁,使得每個實例在輸入自己的instanceId時,會返回固定的任務列表。

      func (e *Etcd) Get(instanceId string) []string {
          // 本來這里應該去 Etcd 拿屬于 instanceId 的任務
          return []string{} // 真實情況依賴外部環境
      }
      
      var e *Etcd        //只是聲明一個指針變量,不需要真正賦值
      guard := PatchInstanceMethod(
       reflect.TypeOf(e),        // 表示etcd類型的方法
       "Get",                    //方法名
       func(_ *Etcd, _ string) []string {        //替身函數(簽名要一致)
        return []string{"task1", "task5", "task8"}
       })
      defer guard.Unpatch()

        (4)任意相同或不同的基本場景組合

      Px1
      defer UnpatchAll()
      Px2
      ...
      Pxn

        (5)樁中樁的一個案例

      type Movie strcut {
          Name string
          Type string
          Score int
      }
      
      //定義一個interface類型
      type Repository struct {
          // 傳進去一個空指針movie,希望返回的時候把movie填上內容,然后返回error
          // 但是真實的Retrieve要連數據庫,太重了。用GoMock雖然能攔截調用,但GoMock只能決定返回值
          // (比如只能返回nil),卻不能真正往movie里面填數據
          Retrieve(key string, movie *movie) error
      }    
      
      // ---------------------------------------------------------
      func TestDemo(t *testing.T) {
          Convey("test demo", t, func() {
              Convey("retrieve movie", func() {
                  ctrl := NewController(t)
                  defer ctrl.Finish()
                  mockRepo := mock_db.NewMockRepository(ctrl)
                  mockRepo.EXPECT().Retrieve(Any(), Any()).Return(nil)
                  Patch(redisrepo.GetInstance, func() Repository {
                      return mockRepo
                  })
                  defer UnpatchAll()
                  PatchInstanceMethod(reflect.TypeOf(mockRepo), "Retrieve", func(_ *mock_db.MockRepository, name string, movie *Movie) error {
                      movie = &Movie{Name: name, Type: "Love", Score: 95}
                      return nil
                  })
                  repo := redisrepo.GetInstance()
                  var movie *Movie
                  err := repo.Retrieve("Titanic", movie)
                  So(err, ShouldBeNil)
                  So(movie.Name, ShouldEqual, "Titanic")
                  So(movie.Type, ShouldEqual, "Love")
                  So(movie.Score, ShouldEqual, 95)
              })
              ...
          })
      }

      樁中樁的做法:

      • 第一層:GoMock,把Retrieve換成假的實現,讓它在調用時不會連接數據庫,但只能返回nil,沒法改movie
      • 第二層:Monkey Patch,把這個假的Retrieve方法本身替換掉。寫一個補丁函數,在里面手動改movie的值。
      整個流程:
      - 程序里調用repo.Retrieve( "Titanic", movie)
      - 實際走到GoMock的樁:但GoMock的樁又被Monkey Patch替換了
      - 最終執行的是你寫的補丁函數,它把movie填好,并返回nil
      - 測試代碼斷言movie的值是否符合預期

      為什么不能只用Monkey?
      - GoMock能mock接口,管理調用次數、順序,返回error
      - 只用Monkey 的話,不能校驗Retrieve方法到底被調用了幾次,參數是不是對的。
      mockRepo.EXPECT().Retrieve("Titanic", gomock.Any()).Return(nil).Times(2) 這里Times的意思是必須調用2次

      4、HTTPTest

      https://pkg.go.dev/net/http/httptest

        由于 Go 標準庫的強大支持,Go 可以很容易的進行 Web 開發。為此,Go 標準庫專門提供了 net/http/httptest 包專門用于進行 http Web 開發測試。

      var GetUserHost = "https://account.wps.cn"
      // 默認情況下,GetUser會調用https://account.wps.cn/p/auth/check這個真實的接口
      // 但在測試時不想依賴外部網絡,所以要“偽造”一個接口服務器
      func GetUser(wpssid, xff string) *User {
          url := fmt.Sprintf("%s/p/auth/check", GetUserHost)
          user := client.POST(url, ...)
          return user
      }
      
      func TestGetUser(t *testing.T) {
          // 用httptest.NewServer啟動了一個本地HTTP服務器
          // 它只實現一個接口POST /p/auth/check,并且返回一個固定的JSON(模擬線上接口的返回)
          svr := httptest.NewServer(func () http.HandlerFunc {
              r := gin.Default()
              r.POST("/p/auth/check", func(c *gin.Context) {
                  c.JSON(http.StatusOK, gin.H{
                      "result":      "ok",
                      "companyid":   1,
                      "nickname":    "test-account",
                      "account":     "123***789@test.com",
                      "phonenumber": "123***789",
                      "pic":         "https://pic.test.com",
                      "status":      "active",
                      "userid":      currentUserID,
                  })
              })
      
              return r.ServeHTTP
          })
      
          defer svr.Close()    //測試結束后關閉這個臨時服務器
          GetUserHost = svr.URL
      
          user := GetUser("test-wps-id", "")
          ...
      }

      5、如何理解Golang測試框架和打樁框架的關系

      測試框架是骨架:提供運行環境+斷言機制
      打樁框架是工具:幫你在測試環境中模擬依賴,制造可控場景
      它們是配合關系,而不是互相替代。
      若沒有測試框架,寫了樁也沒地方運行。
      若沒有打樁框架,你測試代碼可能跑不了(真實依賴很復雜)
      

       

      覆蓋率

      1、單元測試執行

      # 匹配當前目錄下*_test.go命令的文件,執行每一個測試函數
      go test -v
      
      # 執行 calc_test.go 文件中的所有測試函數
      go test -v calc_test.go calc.go
      
      # 指定特定的測試函數(其中:-count=1用于跳過緩存)
      go test -v -run TestAdd calc_test.go calc.go -count=1
      
      #調試單元測試文件。運行命令時,當前目錄應為項目根目錄。
      go test ./...     #運行所有包單元測試文件
      go test ${包名}     #運行指定包的單元測試文件
      go test ${指定路徑}    #運行指定路徑的單元測試文件

       

      2、生成單元測試覆蓋率

      go test -v -covermode=set -coverprofile=cover.out -coverpkg=./...
      
      其中,
      -covermode 有三種模式:
      ? set 語句是否執行,默認模式
      ? count 每個語句執行次數
      ? atomic 類似于count,表示并發程序中的精確技術
      -coverprofile是統計信息保存的文件。

       

      3、查看單元測試覆蓋率

      //(1)查看每個函數的覆蓋情況
      go tool cover -func=cover.out
      
      //(2)使用網頁方式
      go tool cover -html=cover.out

       

       
      posted @ 2025-09-27 09:41  筱倩  閱讀(33)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 免费无码AV一区二区波多野结衣 | 中年国产丰满熟女乱子正在播放| 久久这里只精品国产2| 亚洲日韩亚洲另类激情文学| 人妻系列中文字幕精品| 国产av无码专区亚洲av软件| 成人午夜免费无码视频在线观看| 免费人成再在线观看网站| 国产成人精品亚洲资源| 无码一区二区三区av在线播放| 亚洲情A成黄在线观看动漫尤物| 欧美激情a∨在线视频播放| 久久免费观看午夜成人网站| 九九热在线免费视频精品| 国产超碰无码最新上传| 亚洲精品亚洲人成人网| 亚洲精品一区二区区别| 人人色在线视频播放| 伊人天天久大香线蕉av色| 国产仑乱无码内谢| 午夜综合网| 99久久精品免费看国产电影| 国产在线乱子伦一区二区| 国产一区二区一卡二卡| P尤物久久99国产综合精品| 久久AV中文综合一区二区| 亚洲一区二区av免费| 国产福利酱国产一区二区| 男人的天堂av一二三区| 午夜福利一区二区在线看| 成人乱码一区二区三区av| 欧美三级中文字幕在线观看| 成人亚洲狠狠一二三四区| 亚洲国模精品一区二区| 亚洲一区二区三区小蜜桃| 国产熟睡乱子伦视频在线播放| 国产亚洲久久久久久久| 精品亚洲国产成人av| 亚洲激情av一区二区三区| 国产99视频精品免费观看9| 韩国主播av福利一区二区|