From 6015f1d7d1e6fff74ab4fcdb4f2bf120dbb4f6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Wed, 4 Sep 2024 14:19:54 +0000 Subject: [PATCH] add member_status (#4) Co-authored-by: daniel.w Reviewed-on: https://code.30cm.net/digimon/app-cloudep-permission-server/pulls/4 --- .golangci.yaml | 9 ++ Makefile | 1 + go.mod | 4 +- internal/domain/errors.go | 26 +++-- internal/domain/repository/error.go | 6 + internal/domain/repository/member_status.go | 16 +++ internal/domain/usecase/bitmap.go | 16 +++ internal/domain/usecase/permission_tree.go | 67 ++++++----- internal/repository/member_status.go | 104 +++++++++++++++++ internal/usecase/bitmap.go | 117 ++++++++++++++++++++ internal/usecase/bitmap_benchmark_test.go | 69 ++++++++++++ internal/usecase/bitmap_test.go | 78 +++++++++++++ 12 files changed, 466 insertions(+), 47 deletions(-) create mode 100644 internal/domain/repository/member_status.go create mode 100644 internal/domain/usecase/bitmap.go create mode 100644 internal/repository/member_status.go create mode 100644 internal/usecase/bitmap.go create mode 100644 internal/usecase/bitmap_benchmark_test.go create mode 100644 internal/usecase/bitmap_test.go diff --git a/.golangci.yaml b/.golangci.yaml index 5518484..3f78cd3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -117,6 +117,15 @@ issues: - gocognit - contextcheck + exclude-dirs: + - internal/model + - internal/mock + + exclude-files: + - .*_test.go + + + linters-settings: gci: sections: diff --git a/Makefile b/Makefile index 1cad38c..777c00b 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ test: # 進行測試 fmt: # 格式優化 $(GOFMT) -w $(GOFILES) goimports -w ./ + golangci-lint run .PHONY: gen-rpc gen-rpc: # 建立 rpc code diff --git a/go.mod b/go.mod index 8c17682..d04251f 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ go 1.22.3 require ( code.30cm.net/digimon/library-go/errors v1.0.1 + code.30cm.net/digimon/library-go/utils/invited_code v1.0.2 code.30cm.net/digimon/library-go/validator v1.0.0 - code.30cm.net/wanderland/library-go/errors v1.0.1 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.9.0 github.com/zeromicro/go-zero v1.7.0 go.uber.org/mock v0.4.0 google.golang.org/grpc v1.65.0 @@ -53,6 +54,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect diff --git a/internal/domain/errors.go b/internal/domain/errors.go index aef56d1..14bb3ba 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -3,8 +3,8 @@ package domain import ( mts "app-cloudep-permission-server/internal/lib/metric" - ers "code.30cm.net/wanderland/library-go/errors" - "code.30cm.net/wanderland/library-go/errors/code" + ers "code.30cm.net/digimon/library-go/errors" + "code.30cm.net/digimon/library-go/errors/code" ) // 12 represents Scope @@ -32,52 +32,58 @@ const ( ) // TokenUnexpectedSigningErr 30001 Token 簽名錯誤 -func TokenUnexpectedSigningErr(msg string) *ers.Err { +func TokenUnexpectedSigningErr(msg string) *ers.LibError { mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg) } // TokenTokenValidateErr 30002 Token 驗證錯誤 -func TokenTokenValidateErr(msg string) *ers.Err { +func TokenTokenValidateErr(msg string) *ers.LibError { mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg) } // TokenClaimError 30003 Token 驗證錯誤 -func TokenClaimError(msg string) *ers.Err { +func TokenClaimError(msg string) *ers.LibError { mts.AppErrorMetrics.AddFailure("token", "token_claim_error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg) } // RedisDelError 30020 Redis 刪除錯誤 -func RedisDelError(msg string) *ers.Err { +func RedisDelError(msg string) *ers.LibError { // 看需要建立哪些 Metrics mts.AppErrorMetrics.AddFailure("redis", "del_error") + return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg) } // RedisPipLineError 30021 Redis PipLine 錯誤 -func RedisPipLineError(msg string) *ers.Err { +func RedisPipLineError(msg string) *ers.LibError { // 看需要建立哪些 Metrics mts.AppErrorMetrics.AddFailure("redis", "pip_line_error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg) } // RedisError 30022 Redis 錯誤 -func RedisError(msg string) *ers.Err { +func RedisError(msg string) *ers.LibError { // 看需要建立哪些 Metrics mts.AppErrorMetrics.AddFailure("redis", "error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg) } // PermissionNotFoundError 30030 權限錯誤 -func PermissionNotFoundError(msg string) *ers.Err { +func PermissionNotFoundError(msg string) *ers.LibError { // 看需要建立哪些 Metrics return ers.NewErr(code.CloudEPPermission, code.Forbidden, PermissionNotFoundCode, msg) } // PermissionGetDataError 30031 解析權限時錯誤 -func PermissionGetDataError(msg string) *ers.Err { +func PermissionGetDataError(msg string) *ers.LibError { // 看需要建立哪些 Metrics return ers.NewErr(code.CloudEPPermission, code.InvalidFormat, PermissionGetDataErrorCode, msg) } diff --git a/internal/domain/repository/error.go b/internal/domain/repository/error.go index cf590af..82bdda6 100644 --- a/internal/domain/repository/error.go +++ b/internal/domain/repository/error.go @@ -23,18 +23,21 @@ const ( // TokenUnexpectedSigningErr 30001 Token 簽名錯誤 func TokenUnexpectedSigningErr(msg string) *ers.LibError { mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg) } // TokenTokenValidateErr 30002 Token 驗證錯誤 func TokenTokenValidateErr(msg string) *ers.LibError { mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg) } // TokenClaimError 30003 Token 驗證錯誤 func TokenClaimError(msg string) *ers.LibError { mts.AppErrorMetrics.AddFailure("token", "token_claim_error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg) } @@ -42,6 +45,7 @@ func TokenClaimError(msg string) *ers.LibError { func RedisDelError(msg string) *ers.LibError { // 看需要建立哪些 Metrics mts.AppErrorMetrics.AddFailure("redis", "del_error") + return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg) } @@ -49,6 +53,7 @@ func RedisDelError(msg string) *ers.LibError { func RedisPipLineError(msg string) *ers.LibError { // 看需要建立哪些 Metrics mts.AppErrorMetrics.AddFailure("redis", "pip_line_error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg) } @@ -56,5 +61,6 @@ func RedisPipLineError(msg string) *ers.LibError { func RedisError(msg string) *ers.LibError { // 看需要建立哪些 Metrics mts.AppErrorMetrics.AddFailure("redis", "error") + return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg) } diff --git a/internal/domain/repository/member_status.go b/internal/domain/repository/member_status.go new file mode 100644 index 0000000..74de198 --- /dev/null +++ b/internal/domain/repository/member_status.go @@ -0,0 +1,16 @@ +package repository + +import "context" + +// MemberOnlineStatusRepository 會員上限狀態,使用Bitmap +type MemberOnlineStatusRepository interface { + SetMemberOnline(ctx context.Context, uid string) (bool, error) + SetMemberOffline(ctx context.Context, uid string) (bool, error) + IsMemberOnline(ctx context.Context, uid string) (bool, error) + QueryMemberOnlineList(ctx context.Context, uids []string) ([]MemberOnlineStatusResp, error) +} + +type MemberOnlineStatusResp struct { + UID string + Status bool +} diff --git a/internal/domain/usecase/bitmap.go b/internal/domain/usecase/bitmap.go new file mode 100644 index 0000000..05379b8 --- /dev/null +++ b/internal/domain/usecase/bitmap.go @@ -0,0 +1,16 @@ +package usecase + +type BitMapUseCase interface { + // SetTrue 設定該 Bit 狀態為 true + SetTrue(bitPos uint32) + // SetFalse 設定該Bit 狀態為 false + SetFalse(bitPos uint32) + // IsTrue 確認是否為真 + IsTrue(bitPos uint32) bool + // Reset 重設 BitMap + Reset() + // ByteSize 最大 Byte 數 + ByteSize() int + // BitSize 最大 Byte * 8 + BitSize() int +} diff --git a/internal/domain/usecase/permission_tree.go b/internal/domain/usecase/permission_tree.go index f67f436..7b13a5d 100644 --- a/internal/domain/usecase/permission_tree.go +++ b/internal/domain/usecase/permission_tree.go @@ -1,38 +1,33 @@ package usecase -import ( - "ark-permission/internal/domain" - "ark-permission/internal/entity" -) - -// PermissionTreeManager 定義一組操作權限樹的接口 -// 這個名稱說明它是專門負責管理和操作權限樹的管理器 -type PermissionTreeManager interface { - // AddPermission 將一個新的權限節點插入到樹中 - // key 是父節點的ID,value 是要插入的 Permission 資料 - // 此方法應該能處理節點是否存在於父節點下的情況 - AddPermission(parentID int64, permission entity.Permission) error - // FindPermissionByID 根據權限 ID 查詢樹中的某個節點 - // 如果節點存在,返回對應的 Permission 資料,否則返回 nil - FindPermissionByID(permissionID int64) (*Permission, error) - // GetAllParentPermissionIDs 根據傳入的 permissions 列表 - // 找出每個權限的完整父節點權限 ID 路徑 - // 例如,如果 B 的父權限是 A,並且給了 B 權限,則返回 A 和 B 的權限 ID - GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error) - // GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態 - // 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態 - GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error) - // GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態 - // 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態 - GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions -} - -type Permission struct { - ID int64 `json:"-"` - Name string `json:"name"` - HTTPMethod string `json:"http_method"` - HTTPPath string `json:"http_path"` - Parent *Permission `json:"-"` - Children []*Permission `json:"children"` - PathIDs []int64 `json:"-"` // full path id -} +// // PermissionTreeManager 定義一組操作權限樹的接口 +// // 這個名稱說明它是專門負責管理和操作權限樹的管理器 +// type PermissionTreeManager interface { +// // AddPermission 將一個新的權限節點插入到樹中 +// // key 是父節點的ID,value 是要插入的 Permission 資料 +// // 此方法應該能處理節點是否存在於父節點下的情況 +// AddPermission(parentID int64, permission entity.Permission) error +// // FindPermissionByID 根據權限 ID 查詢樹中的某個節點 +// // 如果節點存在,返回對應的 Permission 資料,否則返回 nil +// FindPermissionByID(permissionID int64) (*Permission, error) +// // GetAllParentPermissionIDs 根據傳入的 permissions 列表 +// // 找出每個權限的完整父節點權限 ID 路徑 +// // 例如,如果 B 的父權限是 A,並且給了 B 權限,則返回 A 和 B 的權限 ID +// GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error) +// // GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態 +// // 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態 +// GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error) +// // GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態 +// // 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態 +// GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions +// } +// +// type Permission struct { +// ID int64 `json:"-"` +// Name string `json:"name"` +// HTTPMethod string `json:"http_method"` +// HTTPPath string `json:"http_path"` +// Parent *Permission `json:"-"` +// Children []*Permission `json:"children"` +// PathIDs []int64 `json:"-"` // full path id +// } diff --git a/internal/repository/member_status.go b/internal/repository/member_status.go new file mode 100644 index 0000000..adcf99e --- /dev/null +++ b/internal/repository/member_status.go @@ -0,0 +1,104 @@ +package repository + +import ( + "app-cloudep-permission-server/internal/domain/repository" + "code.30cm.net/digimon/library-go/utils/invited_code" + "context" + "fmt" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +type MemberOnlineStatusRepositoryParam struct { + Store *redis.Redis `name:"redis"` +} + +type memberOnlineStatusRepository struct { + store *redis.Redis +} + +// 使用 UID 計算 Bitmap 的 offset +func (t *memberOnlineStatusRepository) uidToOffset(uid string) (int64, error) { + converter := invited_code.MustConverter(10, invited_code.DefaultCodeLen, invited_code.ConvertTable) + + // 將 UID 轉換為整數型,並減去基礎 UID (1000000) + uidInt, err := converter.DecodeFromCode(uid) + if err != nil { + return 0, fmt.Errorf("invalid UID: %w", err) + } + // 以 1000000 作為基準 + baseUID := invited_code.InitAutoId + if uidInt < int64(baseUID) { + return 0, fmt.Errorf("UID smaller than base: %d", baseUID) + } + + return uidInt - int64(baseUID), nil +} + +func (t *memberOnlineStatusRepository) SetMemberOnline(ctx context.Context, uid string) (bool, error) { + offset, err := t.uidToOffset(uid) + if err != nil { + return false, err + } + + // 使用 SET BIT 設置對應的位為 1 + _, err = t.store.SetBit("member_status", offset, 1) + if err != nil { + return false, fmt.Errorf("failed to set member online: %w", err) + } + + return true, nil +} + +func (t *memberOnlineStatusRepository) SetMemberOffline(ctx context.Context, uid string) (bool, error) { + offset, err := t.uidToOffset(uid) + if err != nil { + return false, err + } + + // 使用 SET BIT 設置對應的位為 1 + _, err = t.store.SetBit("member_status", offset, 0) + if err != nil { + return false, fmt.Errorf("failed to set member offline: %w", err) + } + + return true, nil +} + +func (t *memberOnlineStatusRepository) IsMemberOnline(ctx context.Context, uid string) (bool, error) { + offset, err := t.uidToOffset(uid) + if err != nil { + return false, err + } + + // 使用 GET BIT 獲取對應的位,1 表示在線,0 表示離線 + status, err := t.store.GetBit("member_status", offset) + if err != nil { + return false, fmt.Errorf("failed to get member status: %w", err) + } + + return status == 1, nil +} + +func (t *memberOnlineStatusRepository) QueryMemberOnlineList(ctx context.Context, uids []string) ([]repository.MemberOnlineStatusResp, error) { + statusMap := make([]repository.MemberOnlineStatusResp, 0, len(uids)) + + // 遍歷所有用戶的 UID,查詢他們的在線狀態 + for _, uid := range uids { + isOnline, err := t.IsMemberOnline(ctx, uid) + if err != nil { + return nil, fmt.Errorf("failed to query member status for UID %s: %w", uid, err) + } + statusMap = append(statusMap, repository.MemberOnlineStatusResp{ + UID: uid, + Status: isOnline, + }) + } + + return statusMap, nil +} + +func NewMemberOnlineStatusRepository(param MemberOnlineStatusRepositoryParam) repository.MemberOnlineStatusRepository { + return &memberOnlineStatusRepository{ + store: param.Store, + } +} diff --git a/internal/usecase/bitmap.go b/internal/usecase/bitmap.go new file mode 100644 index 0000000..6660472 --- /dev/null +++ b/internal/usecase/bitmap.go @@ -0,0 +1,117 @@ +package usecase + +// Bitmap 基礎結構 +// Bitmap 是一個位圖結構,使用 byte slice 來表示大量的位(bit)。 +// 每個 byte 由 8 個位組成,因此可以高效地管理大量的開關狀態(true/false)。 +// Bitmap 的優點在於它能節省空間,尤其是在需要大量布爾值的場合。 +// 缺點是,如果需要動態擴充 Bitmap 的大小,會導致效率下降,因為需要重新分配和移動內存。 +// 因此,最好在初始化時就規劃好所需的位數大小,避免在之後頻繁擴充。 + +type Bitmap []byte + +// MakeBitmapWithBitSize 通過指定的 bit 數創建一個新的 Bitmap。 +// 參數 nBits 表示所需的位(bit)數。 +// 如果指定的位數少於 64,則默認將 Bitmap 初始化為 64 位(這是最低的限制)。 +// 此外,位數(nBits)會被自動調整為 8 的倍數,以適配 byte 的長度(每 8 位為一個 byte)。 +// 返回值是一個 Bitmap(byte slice),其大小根據位數確定。 +func MakeBitmapWithBitSize(nBits int) Bitmap { + // 如果指定的位數少於 64,則設置為 64 位(8 個 byte) + if nBits < 64 { + nBits = 64 + } + // 計算需要的 byte 數,確保每 8 位為一個 byte,並調整 nBits 以達到 8 的倍數 + return MustBitMap((nBits + 7) / 8) +} + +// MustBitMap 根據指定的 byte 數創建一個 Bitmap(byte slice)。 +// 參數 nBytes 表示所需的 byte 數。 +// 返回值是一個長度為 nBytes 的 Bitmap。 +func MustBitMap(nBytes int) Bitmap { + // 使用 make 函數創建一個 byte slice,大小為 nBytes。 + return make([]byte, nBytes) +} + +// SetTrue 設置指定位置的 bit 為 true(1)。 +// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。 +// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 1。 +func (b Bitmap) SetTrue(bitPos uint32) { + // |= 是一種位運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位或運算(bitwise OR),並將結果賦值 + b[bitPos/8] |= 1 << (bitPos % 8) +} + +// SetFalse 設置指定位置的 bit 為 false(0)。 +// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。 +// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 0。 +func (b Bitmap) SetFalse(bitPos uint32) { + // 取出對應 byte,使用位與和取反運算將對應的 bit 設置為 0 + // 假設我們有以下情況: + + // • b[1](即 b[bitPos/8])是 10101111(十進制 175)。 + // • bitPos = 10,也就是我們想清除第 10 位的值。 + // + // 操作步驟: + // + // 1. bitPos/8 = 1:所以我們要修改 b[1] 這個 byte。 + // 2. bitPos % 8 = 2:表示我們要清除的位是這個 byte 中的第 3 位(從右數起第 3 位)。 + // 3. 1 << (bitPos % 8) = 1 << 2 = 00000100:生成位掩碼 00000100。 + // 4. 取反:^(1 << 2) = ^00000100 = 11111011,這樣的掩碼表示除了第 3 位,其他位都是 1。 + // 5. 位與運算:10101111 & 11111011 = 10101011,結果將第 3 位清除,其餘位保持不變。即,b[1] 變成了 10101011(十進制 171)。 + // &= 是一種 位與運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位與運算(bitwise AND),然後將結果賦值給左邊的變數。 + b[bitPos/8] &= ^(1 << (bitPos % 8)) +} + +// IsTrue 檢查指定位置的 bit 是否為 true(1)。 +// 參數 bitPos 是要檢查的位的位置(以 0 為基準的位索引)。 +// 如果該 bit 是 1,則返回 true;否則返回 false。 +func (b Bitmap) IsTrue(bitPos uint32) bool { + /* + 這一行程式碼 b[bitPos/8]&(1<<(bitPos%8)) != 0 是用來檢查 指定位(bit) 是否為 true(1), + 它的核心是位運算。讓我們逐步拆解並解釋這一行程式碼: + + 1. 背景知識: + + • 位運算 是在二進制層面操作數字。每個 byte 有 8 個位(bit),所以位圖結構是以 byte 來表示位的集合。 + • b 是一個 Bitmap 結構,也就是 []byte,即 byte 的切片。 + • bitPos 是一個 uint32 類型的變數,表示我們想要檢查的位(bit)在整個位圖中的索引(位置)。 + 3. 完整流程舉例: + 假設: + • b = []byte{0b10101010, 0b01010101} (即二進制表示的兩個 byte,分別是 10101010 和 01010101)。 + • bitPos = 10(我們要檢查第 10 位是否為 1)。 + 操作順序: + + 1. 計算 bitPos/8 = 10/8 = 1,所以我們要檢查的是第二個 byte:0b01010101。 + 2. 計算 bitPos%8 = 10%8 = 2,所以我們要檢查的是該 byte 中的第 3 位(從右數起第 3 位)。 + 3. 位移:1 << 2 = 00000100。 + 4. 位與:0b01010101 & 0b00000100 = 0b00000100(因為該 byte 的第 3 位是 1,結果不等於 0)。 + 5. 判斷結果:結果不等於 0,因此第 10 位是 1(true)。 + + 4. 總結: + + • b[bitPos/8]&(1<<(bitPos%8)) != 0 是一個經典的位操作,用來檢查位圖中某一個位是否為 1。 + • bitPos/8 找到對應的 byte,bitPos % 8 找到該位在這個 byte 中的具體位置。 + • 最後的位與運算和比較用來確定該位的狀態是 true(1)還是 false(0)。 + */ + return b[bitPos/8]&(1<<(bitPos%8)) != 0 +} + +// Reset 重置 Bitmap,使所有的 bit 都設置為 false(0)。 +// 這個操作會將整個 Bitmap 的所有 byte 都設置為 0,從而達到重置的效果。 +func (b Bitmap) Reset() { + // 迭代 Bitmap 中的每個 byte,並將其設置為 0 + for i := range b { + b[i] = 0 + } +} + +// ByteSize 返回 Bitmap 的 byte 長度。 +// 這個函數返回 Bitmap 目前占用的 byte 數量,該值等於 Bitmap 的長度(slice 的長度)。 +func (b Bitmap) ByteSize() int { + return len(b) +} + +// BitSize 返回 Bitmap 的位(bit)長度。 +// 這個函數通過將 byte 長度乘以 8 來計算 Bitmap 中的總位數。 +func (b Bitmap) BitSize() int { + // 每個 byte 包含 8 個 bit,因此將 byte 長度乘以 8 + return len(b) * 8 +} diff --git a/internal/usecase/bitmap_benchmark_test.go b/internal/usecase/bitmap_benchmark_test.go new file mode 100644 index 0000000..03545b2 --- /dev/null +++ b/internal/usecase/bitmap_benchmark_test.go @@ -0,0 +1,69 @@ +package usecase + +import "testing" + +// 基準測試 SetTrue 函數,測試在不同大小的 Bitmap 上設置位元為 true 的效能 +func BenchmarkBitmapSetTrue(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + bitmap.SetTrue(uint32(i % 1000000)) // 設置位元為 true + } +} + +// 基準測試 SetFalse 函數,測試在不同大小的 Bitmap 上清除位元為 false 的效能 +func BenchmarkBitmapSetFalse(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + bitmap.SetFalse(uint32(i % 1000000)) // 清除位元為 false + } +} + +// 基準測試 IsTrue 函數,測試在不同大小的 Bitmap 上檢查位元狀態的效能 +func BenchmarkBitmapIsTrue(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + _ = bitmap.IsTrue(uint32(i % 1000000)) // 檢查位元是否為 true + } +} + +// 基準測試 Reset 函數,測試重置不同大小的 Bitmap 的效能 +func BenchmarkBitmapReset(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + bitmap.Reset() // 重置 Bitmap + } +} + +// 基準測試 BitSize 函數,測試返回位圖的 bit 長度的效能 +func BenchmarkBitmapBitSize(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + _ = bitmap.BitSize(0) // 測試返回位圖的 bit 長度 + } +} + +// 基準測試 ByteSize 函數,測試返回位圖的 byte 長度的效能 +func BenchmarkBitmapByteSize(b *testing.B) { + // 以 10^6 位元作為基準測試的 Bitmap 大小 + bitmap := MakeBitmapWithBitSize(1000000) + b.ResetTimer() // 重設計時器,排除初始化的時間 + + for i := 0; i < b.N; i++ { + _ = bitmap.ByteSize(0) // 測試返回位圖的 byte 長度 + } +} diff --git a/internal/usecase/bitmap_test.go b/internal/usecase/bitmap_test.go new file mode 100644 index 0000000..544cf45 --- /dev/null +++ b/internal/usecase/bitmap_test.go @@ -0,0 +1,78 @@ +package usecase + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBitmap_SetTrueAndIsTrue(t *testing.T) { + tests := []struct { + name string + bitPos uint32 + expected bool + }{ + {"Set bit 0 to true", 0, true}, + {"Set bit 1 to true", 1, true}, + {"Set bit 63 to true", 63, true}, + {"Set bit 64 to true", 64, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + bitmap.SetTrue(tt.bitPos) + result := bitmap.IsTrue(tt.bitPos) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBitmap_SetFalse(t *testing.T) { + tests := []struct { + name string + bitPos uint32 + expected bool + }{ + {"Set bit 0 to false", 0, false}, + {"Set bit 1 to false", 1, false}, + {"Set bit 63 to false", 63, false}, + {"Set bit 64 to false", 64, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + bitmap.SetTrue(tt.bitPos) // 先設置該 bit 為 true + bitmap.SetFalse(tt.bitPos) // 然後設置該 bit 為 false + result := bitmap.IsTrue(tt.bitPos) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestBitmap_Reset(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + bitmap.SetTrue(0) + bitmap.SetTrue(64) + + // 確認 bit 0 和 bit 64 是 true + assert.True(t, bitmap.IsTrue(0)) + assert.True(t, bitmap.IsTrue(64)) + + bitmap.Reset() // 重置位圖 + + // 確認所有的位都已經重置為 false + assert.False(t, bitmap.IsTrue(0)) + assert.False(t, bitmap.IsTrue(64)) +} + +func TestBitmap_ByteSize(t *testing.T) { + bitmap := MakeBitmapWithBitSize(64) // 初始化一個 64 位的 Bitmap + assert.Equal(t, 8, bitmap.ByteSize(0)) // 64 位應該佔用 8 個 byte +} + +func TestBitmap_BitSize(t *testing.T) { + bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap + assert.Equal(t, 128, bitmap.BitSize(0)) // 128 位應該有 128 個 bit +}