test(utils): 添加事件驱动模型测试

- 在 sqrt_test.go 中添加了 fastSqr1 测试函数,用于测试事件驱动模型
- 新增了 Event 和 Uint32AsyncEvent 类型用于测试
- 更新了 go.work、go.mod 和
This commit is contained in:
2025-08-05 16:10:18 +08:00
parent bbbec5dff0
commit cd7583ba05
32 changed files with 1789 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
package cart
import (
"context"
"fmt"
"strings"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/factory-request-reply/events"
"github.com/badu/bus/test_scenarios/factory-request-reply/inventory"
"github.com/badu/bus/test_scenarios/factory-request-reply/prices"
)
type ServiceImpl struct {
sb *strings.Builder
}
func NewService(sb *strings.Builder) ServiceImpl {
result := ServiceImpl{sb: sb}
return result
}
func (s *ServiceImpl) AddProductToCart(ctx context.Context, productID string) error {
inventoryClientRequest := events.NewInventoryGRPCClientRequestEvent()
bus.Pub(inventoryClientRequest)
inventoryClientRequest.WaitReply()
pricesClientRequest := events.NewPricesGRPCClientRequestEvent()
bus.Pub(pricesClientRequest)
pricesClientRequest.WaitReply()
defer inventoryClientRequest.Conn.Close() // close GRPC connection when done
stockResponse, err := inventoryClientRequest.Client.GetStockForProduct(ctx, &inventory.ProductIDRequest{ID: productID})
if err != nil {
return err
}
defer pricesClientRequest.Conn.Close() // close GRPC connection when done
priceResponse, err := pricesClientRequest.Client.GetPricesForProduct(ctx, &prices.ProductIDRequest{ID: productID})
if err != nil {
return err
}
s.sb.WriteString(fmt.Sprintf("stock %0.2fpcs @ price %0.2f$\n", stockResponse.Stock, priceResponse.Price))
return nil
}

View File

@@ -0,0 +1,56 @@
package events
import (
"sync"
"github.com/badu/bus/test_scenarios/factory-request-reply/inventory"
"github.com/badu/bus/test_scenarios/factory-request-reply/prices"
)
type InventoryGRPCClientRequestEvent struct {
wg sync.WaitGroup
Conn Closer // should be *grpc.ClientConn, but we're avoiding the import
Client inventory.ServiceClient
}
func NewInventoryGRPCClientRequestEvent() *InventoryGRPCClientRequestEvent {
result := InventoryGRPCClientRequestEvent{}
result.wg.Add(1)
return &result
}
func (i *InventoryGRPCClientRequestEvent) Async() bool {
return true // this one is async
}
func (i *InventoryGRPCClientRequestEvent) WaitReply() {
i.wg.Wait()
}
func (i *InventoryGRPCClientRequestEvent) Reply() {
i.wg.Done()
}
type PricesGRPCClientRequestEvent struct {
wg sync.WaitGroup
Conn Closer // should be *grpc.ClientConn, but we're avoiding the import
Client prices.ServiceClient
}
func NewPricesGRPCClientRequestEvent() *PricesGRPCClientRequestEvent {
result := PricesGRPCClientRequestEvent{}
result.wg.Add(1)
return &result
}
func (p *PricesGRPCClientRequestEvent) WaitReply() {
p.wg.Wait()
}
func (p *PricesGRPCClientRequestEvent) Reply() {
p.wg.Done()
}
type Closer interface {
Close() error
}

View File

@@ -0,0 +1,17 @@
package inventory
import (
"context"
)
type ServiceClient interface {
GetStockForProduct(ctx context.Context, in *ProductIDRequest) (*ProductStockResponse, error) // , opts ...grpc.CallOption) (*ProductStockResponse, error)
}
type ProductIDRequest struct {
ID string
}
type ProductStockResponse struct {
Stock float64
}

View File

@@ -0,0 +1,18 @@
package inventory
import (
"context"
)
type ServiceImpl struct {
}
func NewService() ServiceImpl {
result := ServiceImpl{}
return result
}
func (s *ServiceImpl) GetStockForProduct(ctx context.Context, productID string) {
}

View File

@@ -0,0 +1,81 @@
package factory_request_reply
import (
"context"
"strings"
"testing"
"time"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/factory-request-reply/cart"
"github.com/badu/bus/test_scenarios/factory-request-reply/events"
"github.com/badu/bus/test_scenarios/factory-request-reply/inventory"
"github.com/badu/bus/test_scenarios/factory-request-reply/prices"
)
var sb strings.Builder
type pricesClientStub struct{}
func (s *pricesClientStub) GetPricesForProduct(ctx context.Context, in *prices.ProductIDRequest) (*prices.ProductPriceResponse, error) {
return &prices.ProductPriceResponse{Price: 10.30}, nil
}
type fakeCloser struct {
}
func (f *fakeCloser) Close() error {
return nil
}
func OnPricesGRPCClientStubRequest(e *events.PricesGRPCClientRequestEvent) {
sb.WriteString("OnPricesGRPCClientStubRequest\n")
e.Client = &pricesClientStub{}
e.Conn = &fakeCloser{}
<-time.After(300 * time.Millisecond)
e.Reply()
}
type inventoryClientStub struct{}
func (s *inventoryClientStub) GetStockForProduct(ctx context.Context, in *inventory.ProductIDRequest) (*inventory.ProductStockResponse, error) {
return &inventory.ProductStockResponse{Stock: 200}, nil
}
func OnInventoryGRPCClientStubRequest(e *events.InventoryGRPCClientRequestEvent) {
sb.WriteString("OnInventoryGRPCClientStubRequest\n")
e.Client = &inventoryClientStub{}
e.Conn = &fakeCloser{}
<-time.After(300 * time.Millisecond)
e.Reply()
}
func TestGRPCClientStub(t *testing.T) {
cartSvc := cart.NewService(&sb)
bus.Sub(OnInventoryGRPCClientStubRequest)
bus.Sub(OnPricesGRPCClientStubRequest)
err := cartSvc.AddProductToCart(context.Background(), "1")
if err != nil {
t.Fatalf("error adding product to cart : %#v", err)
}
err = cartSvc.AddProductToCart(context.Background(), "2")
if err != nil {
t.Fatalf("error adding product to cart : %#v", err)
}
const expecting = "OnInventoryGRPCClientStubRequest\n" +
"OnPricesGRPCClientStubRequest\n" +
"stock 200.00pcs @ price 10.30$\n" +
"OnInventoryGRPCClientStubRequest\n" +
"OnPricesGRPCClientStubRequest\n" +
"stock 200.00pcs @ price 10.30$\n"
got := sb.String()
if got != expecting {
t.Fatalf("expecting :\n%s but got : \n%s", expecting, got)
}
}

View File

@@ -0,0 +1,17 @@
package prices
import (
"context"
)
type ServiceClient interface {
GetPricesForProduct(ctx context.Context, in *ProductIDRequest) (*ProductPriceResponse, error) // , opts ...grpc.CallOption) (*ProductPriceResponse, error)
}
type ProductIDRequest struct {
ID string
}
type ProductPriceResponse struct {
Price float64
}

View File

@@ -0,0 +1,18 @@
package prices
import (
"context"
)
type ServiceImpl struct {
}
func NewService() ServiceImpl {
result := ServiceImpl{}
return result
}
func (s *ServiceImpl) GetPricesForProduct(ctx context.Context, productID string) {
}

View File

@@ -0,0 +1,37 @@
package audit
import (
"fmt"
"strings"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/fire-and-forget/events"
)
type ServiceImpl struct {
sb *strings.Builder
}
func NewAuditService(sb *strings.Builder) ServiceImpl {
result := ServiceImpl{sb: sb}
bus.Sub(result.OnUserRegisteredEvent)
bus.SubCancel(result.OnSMSRequestEvent)
bus.SubCancel(result.OnSMSSentEvent)
return result
}
// OnUserRegisteredEvent is classic event handler
func (s *ServiceImpl) OnUserRegisteredEvent(event events.UserRegisteredEvent) {
// we can save audit data here
}
// OnSMSRequestEvent is a pub-unsub type, we have to return 'false' to continue listening for this kind of events
func (s *ServiceImpl) OnSMSRequestEvent(event events.SMSRequestEvent) bool {
return false
}
// OnSMSSentEvent is a pub-unsub type where we give up on listening after receiving first message
func (s *ServiceImpl) OnSMSSentEvent(event events.SMSSentEvent) bool {
s.sb.WriteString(fmt.Sprintf("audit event : an sms was %s sent to %s with message %s\n", event.Status, event.Request.Number, event.Request.Message))
return true // after first event, audit will give up listening for events
}

View File

@@ -0,0 +1,36 @@
package events
type UserRegisteredEvent struct {
UserName string
Phone string
}
func (e UserRegisteredEvent) Async() bool {
return true
}
type SMSRequestEvent struct {
Number string
Message string
}
func (e SMSRequestEvent) Async() bool {
return true
}
type SMSSentEvent struct {
Request SMSRequestEvent
Status string
}
func (e SMSSentEvent) Async() bool {
return true
}
type DummyEvent struct {
AlteredAsync bool
}
func (e *DummyEvent) Async() bool {
return e.AlteredAsync
}

View File

@@ -0,0 +1,49 @@
package fire_and_forget
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/fire-and-forget/audit"
"github.com/badu/bus/test_scenarios/fire-and-forget/events"
"github.com/badu/bus/test_scenarios/fire-and-forget/notifications"
"github.com/badu/bus/test_scenarios/fire-and-forget/users"
)
func OnDummyEvent(event *events.DummyEvent) {
fmt.Println("dummy event async ?", event.Async())
}
func TestUserRegistration(t *testing.T) {
var sb strings.Builder
userSvc := users.NewService(&sb)
notifications.NewSmsService(&sb)
notifications.NewEmailService(&sb)
audit.NewAuditService(&sb)
bus.Sub(OnDummyEvent)
userSvc.RegisterUser(context.Background(), "Badu", "+40742222222")
<-time.After(500 * time.Millisecond)
userSvc.RegisterUser(context.Background(), "Adina", "+40743333333")
<-time.After(500 * time.Millisecond)
const expecting = "user Badu has registered - sending welcome email message\n" +
"sms sent requested for number +40742222222 with message Badu your user account was created. Check your email for instructions\n" +
"audit event : an sms was successfully sent sent to +40742222222 with message Badu your user account was created. Check your email for instructions\n" +
"user Adina has registered - sending welcome email message\n" +
"sms sent requested for number +40743333333 with message Adina your user account was created. Check your email for instructions\n"
got := sb.String()
if got != expecting {
t.Fatalf("expecting :\n%s but got : \n%s", expecting, got)
}
}

View File

@@ -0,0 +1,27 @@
package notifications
import (
"fmt"
"strings"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/fire-and-forget/events"
)
type EmailServiceImpl struct {
sb *strings.Builder
}
func NewEmailService(sb *strings.Builder) EmailServiceImpl {
result := EmailServiceImpl{sb: sb}
bus.Sub(result.OnUserRegisteredEvent)
return result
}
func (s *EmailServiceImpl) OnUserRegisteredEvent(e events.UserRegisteredEvent) {
s.sb.WriteString(fmt.Sprintf("user %s has registered - sending welcome email message\n", e.UserName))
bus.Pub(events.SMSRequestEvent{
Number: e.Phone,
Message: fmt.Sprintf("%s your user account was created. Check your email for instructions", e.UserName),
})
}

View File

@@ -0,0 +1,27 @@
package notifications
import (
"fmt"
"strings"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/fire-and-forget/events"
)
type SmsServiceImpl struct {
sb *strings.Builder
}
func NewSmsService(sb *strings.Builder) SmsServiceImpl {
result := SmsServiceImpl{sb: sb}
bus.Sub(result.OnSMSSendRequest)
return result
}
func (s *SmsServiceImpl) OnSMSSendRequest(event events.SMSRequestEvent) {
s.sb.WriteString(fmt.Sprintf("sms sent requested for number %s with message %s\n", event.Number, event.Message))
bus.Pub(events.SMSSentEvent{
Request: event,
Status: "successfully sent",
})
}

View File

@@ -0,0 +1,25 @@
package users
import (
"context"
"strings"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/fire-and-forget/events"
)
type ServiceImpl struct {
sb *strings.Builder
c int
}
func NewService(sb *strings.Builder) ServiceImpl {
result := ServiceImpl{sb: sb}
return result
}
func (s *ServiceImpl) RegisterUser(ctx context.Context, name, phone string) {
s.c++
bus.Pub(events.UserRegisteredEvent{UserName: name, Phone: phone})
bus.Pub(&events.DummyEvent{AlteredAsync: s.c%2 == 0}) // nobody listens on this one
}

View File

@@ -0,0 +1,18 @@
package events
type RequestEvent[T any] struct {
Payload T
Callback func() (*T, error)
Done chan struct{}
}
func NewRequestEvent[T any](payload T) *RequestEvent[T] {
return &RequestEvent[T]{
Payload: payload,
Done: make(chan struct{}),
}
}
func (i *RequestEvent[T]) Async() bool {
return true // this one is async
}

View File

@@ -0,0 +1,59 @@
package request_reply_callback
import (
"context"
"strings"
"testing"
"github.com/badu/bus/test_scenarios/request-reply-callback/orders"
)
func TestRequestReplyCallback(t *testing.T) {
var sb strings.Builder
orders.NewRepository(&sb)
svc := orders.NewService(&sb)
ctx := context.Background()
newOrder0, err := svc.RegisterOrder(ctx, []int{1, 2, 3})
if err != nil {
t.Fatalf("error creating order : %#v", err)
}
t.Logf("new order #0 : %#v", newOrder0)
newOrder1, err := svc.RegisterOrder(ctx, []int{4, 5, 6})
if err != nil {
t.Fatalf("error creating order : %#v", err)
}
t.Logf("new order #1 : %#v", newOrder1)
newOrder2, err := svc.RegisterOrder(ctx, []int{7, 8, 9})
if err != nil {
t.Fatalf("error creating order : %#v", err)
}
t.Logf("new order #2 : %#v", newOrder2)
stat0, err := svc.GetOrderStatus(ctx, newOrder0.OrderID)
if err != nil {
t.Fatalf("error getting order status : %#v", err)
}
t.Logf("order #0 status : %s", stat0.Status)
stat1, err := svc.GetOrderStatus(ctx, newOrder1.OrderID)
if err != nil {
t.Fatalf("error getting order status : %#v", err)
}
t.Logf("order #1 status : %s", stat1.Status)
stat2, err := svc.GetOrderStatus(ctx, newOrder2.OrderID)
if err != nil {
t.Fatalf("error getting order status : %#v", err)
}
t.Logf("order #2 status : %s", stat2.Status)
t.Logf("%s", sb.String())
}

View File

@@ -0,0 +1,57 @@
package orders
import (
"strings"
"time"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/request-reply-callback/events"
)
type Order struct {
OrderID int
ProductIDs []int
}
type OrderStatus struct {
OrderID int
Status string
}
type RepositoryImpl struct {
sb *strings.Builder
calls int
}
func NewRepository(sb *strings.Builder) RepositoryImpl {
result := RepositoryImpl{sb: sb}
bus.Sub(result.onCreateOrder)
bus.Sub(result.onGetOrderStatus)
return result
}
func (r *RepositoryImpl) onCreateOrder(event *events.RequestEvent[Order]) {
defer func() { r.calls++ }()
<-time.After(500 * time.Millisecond) // simulate heavy database call
event.Callback = func() (*Order, error) {
return &Order{OrderID: r.calls, ProductIDs: event.Payload.ProductIDs}, nil
}
close(event.Done)
}
func (r *RepositoryImpl) onGetOrderStatus(event *events.RequestEvent[OrderStatus]) {
<-time.After(300 * time.Millisecond) // simulate heavy database call
event.Callback = func() (*OrderStatus, error) {
status := "in_progress"
if event.Payload.OrderID == 3 {
status = "cancelled"
}
return &OrderStatus{OrderID: event.Payload.OrderID, Status: status}, nil
}
close(event.Done)
}

View File

@@ -0,0 +1,35 @@
package orders
import (
"context"
"fmt"
"strings"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/request-reply-callback/events"
)
type ServiceImpl struct {
sb *strings.Builder
}
func NewService(sb *strings.Builder) ServiceImpl {
result := ServiceImpl{sb: sb}
return result
}
func (s *ServiceImpl) RegisterOrder(ctx context.Context, productIDs []int) (*Order, error) {
event := events.NewRequestEvent[Order](Order{ProductIDs: productIDs})
s.sb.WriteString(fmt.Sprintf("dispatching event typed %T\n", event))
bus.Pub(event)
<-event.Done // wait for "reply"
return event.Callback() // return the callback, which is containing the actual result
}
func (s *ServiceImpl) GetOrderStatus(ctx context.Context, orderID int) (*OrderStatus, error) {
event := events.NewRequestEvent[OrderStatus](OrderStatus{OrderID: orderID})
s.sb.WriteString(fmt.Sprintf("dispatching event typed %T\n", event))
bus.Pub(event)
<-event.Done // wait for "reply"
return event.Callback() // return the callback, which is containing the actual result
}

View File

@@ -0,0 +1,33 @@
package events
import (
"context"
)
type EventState struct {
Ctx context.Context
Done chan struct{} `json:"-"`
Error error
}
func NewEventState(ctx context.Context) *EventState {
return &EventState{
Ctx: ctx,
Done: make(chan struct{}),
}
}
func (s *EventState) Close() {
s.Error = s.Ctx.Err()
close(s.Done)
}
type NewOrder struct {
ID int
}
type CreateOrderEvent struct {
NewOrder *NewOrder
ProductIDs []int
State *EventState
}

View File

@@ -0,0 +1,25 @@
package request_reply
import (
"context"
"testing"
"time"
"github.com/badu/bus/test_scenarios/request-reply-with-cancellation/orders"
)
func TestRequestReplyWithCancellation(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
svc := orders.NewService()
orders.NewRepository()
response, err := svc.CreateOrder(ctx, []int{1, 2, 3})
switch err {
default:
t.Fatalf("error : it supposed to timeout, but it responded %#v and the error is %#v", response, err)
case context.DeadlineExceeded:
// what we were expecting
}
cancel()
}

View File

@@ -0,0 +1,41 @@
package orders
import (
"time"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/request-reply-with-cancellation/events"
)
type Order struct {
ID int
ProductIDs []int
}
type RepositoryImpl struct {
calls int
}
func NewRepository() RepositoryImpl {
result := RepositoryImpl{}
bus.Sub(result.OnCreateOrder)
return result
}
func (r *RepositoryImpl) OnCreateOrder(event events.CreateOrderEvent) {
defer func() {
r.calls++
}()
for {
select {
case <-time.After(4 * time.Second):
event.NewOrder = &events.NewOrder{ID: r.calls}
event.State.Close()
return
case <-event.State.Ctx.Done():
event.State.Close()
return
}
}
}

View File

@@ -0,0 +1,28 @@
package orders
import (
"context"
"github.com/badu/bus"
"github.com/badu/bus/test_scenarios/request-reply-with-cancellation/events"
)
type ServiceImpl struct {
}
func NewService() ServiceImpl {
result := ServiceImpl{}
return result
}
func (s *ServiceImpl) CreateOrder(ctx context.Context, productIDs []int) (*Order, error) {
event := events.CreateOrderEvent{State: events.NewEventState(ctx), ProductIDs: productIDs, NewOrder: &events.NewOrder{}}
bus.Pub(event)
<-event.State.Done
if event.NewOrder != nil && event.State.Error == nil {
return &Order{ID: event.NewOrder.ID, ProductIDs: productIDs}, nil
}
return nil, event.State.Error
}