mirror of
https://github.com/MedUnes/go-kata.git
synced 2026-03-12 21:55:53 +07:00
feat(add solutions reference for challenge 01-concurrent-aggregator)
This commit is contained in:
32
.github/workflows/codeq.yaml
vendored
Normal file
32
.github/workflows/codeq.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ "go" ]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
76
.github/workflows/tests.yaml
vendored
Normal file
76
.github/workflows/tests.yaml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
check-latest: true
|
||||||
|
- name: Check Go formatting
|
||||||
|
run: |
|
||||||
|
UNFORMATTED_FILES=$(gofmt -l .)
|
||||||
|
if [ -n "$UNFORMATTED_FILES" ]; then
|
||||||
|
echo "::error::The following files are not formatted correctly:"
|
||||||
|
echo "$UNFORMATTED_FILES"
|
||||||
|
gofmt -d
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest, macos-latest ]
|
||||||
|
go: [ 1.25 ]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
go-build: ~/.cache/go-build
|
||||||
|
- os: macos-latest
|
||||||
|
go-build: /Users/runner/Library/Caches/go-build
|
||||||
|
name: ${{ matrix.os }} @ Go ${{ matrix.go }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
GO111MODULE: on
|
||||||
|
GOPROXY: https://proxy.golang.org
|
||||||
|
steps:
|
||||||
|
- name: Set up Go ${{ matrix.go }}
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ matrix.go-build }}
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
- name: Tests
|
||||||
|
run: |
|
||||||
|
go run gotest.tools/gotestsum@latest --format=testdox -- -covermode=atomic -coverprofile=coverage.txt ./...
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
flags: ${{ matrix.os }},go-${{ matrix.go }}
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
slug: medunes/go-kata
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1 +1,16 @@
|
|||||||
.idea
|
dist/
|
||||||
|
.vscode/
|
||||||
|
vendor/
|
||||||
|
pkg/
|
||||||
|
.idea
|
||||||
|
test/data
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
.glide/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
coverage.txt
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
.idea
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package concurrent_aggregator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/medunes/go-kata/01-context-cancellation-concurrency/01-concurrent-aggregator/order"
|
||||||
|
"github.com/medunes/go-kata/01-context-cancellation-concurrency/01-concurrent-aggregator/profile"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option func(ua *UserAggregator)
|
||||||
|
|
||||||
|
func WithLogger(l *slog.Logger) Option {
|
||||||
|
return func(ua *UserAggregator) {
|
||||||
|
ua.logger = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTimeout(timeout time.Duration) Option {
|
||||||
|
return func(ua *UserAggregator) {
|
||||||
|
ua.timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserAggregator struct {
|
||||||
|
orderService order.Service
|
||||||
|
profileService profile.Service
|
||||||
|
timeout time.Duration
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserAggregator(os order.Service, ps profile.Service, opts ...Option) *UserAggregator {
|
||||||
|
ua := &UserAggregator{
|
||||||
|
orderService: os,
|
||||||
|
profileService: ps,
|
||||||
|
logger: slog.Default(), // avoid nil panics, default writes to stderr
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(ua)
|
||||||
|
}
|
||||||
|
return ua
|
||||||
|
}
|
||||||
|
|
||||||
|
type AggregatedProfile struct {
|
||||||
|
Name string
|
||||||
|
Cost float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ua *UserAggregator) Aggregate(ctx context.Context, id int) ([]*AggregatedProfile, error) {
|
||||||
|
var (
|
||||||
|
localCtx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
pr *profile.Profile
|
||||||
|
or []*order.Order
|
||||||
|
au []*AggregatedProfile
|
||||||
|
g *errgroup.Group
|
||||||
|
)
|
||||||
|
if ua.timeout > 0 {
|
||||||
|
localCtx, cancel = context.WithTimeout(ctx, ua.timeout)
|
||||||
|
} else {
|
||||||
|
localCtx, cancel = context.WithCancel(ctx)
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
ua.logger.Info("starting aggregation", "user_id", id)
|
||||||
|
g, localCtx = errgroup.WithContext(localCtx)
|
||||||
|
g.Go(func() error {
|
||||||
|
var err error
|
||||||
|
pr, err = ua.profileService.Get(localCtx, id)
|
||||||
|
if err != nil {
|
||||||
|
ua.logger.Warn("failed to fetch profile", "user_id", id, "err", err)
|
||||||
|
return fmt.Errorf("profile fetch failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
g.Go(func() error {
|
||||||
|
var err error
|
||||||
|
or, err = ua.orderService.GetAll(localCtx, id)
|
||||||
|
if err != nil {
|
||||||
|
ua.logger.Warn("failed to fetch order", "user_id", id, "err", err)
|
||||||
|
return fmt.Errorf("order fetch failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
err := g.Wait()
|
||||||
|
if err != nil {
|
||||||
|
ua.logger.Warn("aggregator exited with error", "user_id", id, "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, o := range or {
|
||||||
|
if o.UserId == id {
|
||||||
|
au = append(au, &AggregatedProfile{pr.Name, o.Cost})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ua.logger.Info("aggregation complete successfully", "user_id", id, "count", len(au))
|
||||||
|
return au, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package concurrent_aggregator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/medunes/go-kata/01-context-cancellation-concurrency/01-concurrent-aggregator/order"
|
||||||
|
"github.com/medunes/go-kata/01-context-cancellation-concurrency/01-concurrent-aggregator/profile"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfileServiceMock struct {
|
||||||
|
simulatedDuration time.Duration
|
||||||
|
simulateError bool
|
||||||
|
simulatedProfiles []*profile.Profile
|
||||||
|
}
|
||||||
|
type OrderServiceMock struct {
|
||||||
|
simulatedDuration time.Duration
|
||||||
|
simulateError bool
|
||||||
|
simulatedOrders []*order.Order
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps *ProfileServiceMock) Get(ctx context.Context, id int) (*profile.Profile, error) {
|
||||||
|
|
||||||
|
fmt.Println("Simulating profile search..")
|
||||||
|
select {
|
||||||
|
case <-time.After(ps.simulatedDuration):
|
||||||
|
if ps.simulateError {
|
||||||
|
return nil, fmt.Errorf("simulated profile search error")
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
for _, p := range ps.simulatedProfiles {
|
||||||
|
if p.Id == id {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (os *OrderServiceMock) GetAll(ctx context.Context, userId int) ([]*order.Order, error) {
|
||||||
|
fmt.Println("Simulating orders search..")
|
||||||
|
var emptyOrders []*order.Order
|
||||||
|
select {
|
||||||
|
case <-time.After(os.simulatedDuration):
|
||||||
|
if os.simulateError {
|
||||||
|
return emptyOrders, fmt.Errorf("simulated orders search error")
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return emptyOrders, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
var userOrders []*order.Order
|
||||||
|
for _, o := range os.simulatedOrders {
|
||||||
|
if o.UserId == userId {
|
||||||
|
userOrders = append(userOrders, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userOrders, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
func TestAggregate(t *testing.T) {
|
||||||
|
type Input struct {
|
||||||
|
profileService *ProfileServiceMock
|
||||||
|
orderService *OrderServiceMock
|
||||||
|
searchedProfileId int
|
||||||
|
timeout time.Duration
|
||||||
|
parentContextDuration time.Duration
|
||||||
|
}
|
||||||
|
type Expected struct {
|
||||||
|
aggregatedProfiles []*AggregatedProfile
|
||||||
|
err bool
|
||||||
|
}
|
||||||
|
type TestCase struct {
|
||||||
|
name string
|
||||||
|
input Input
|
||||||
|
expected Expected
|
||||||
|
}
|
||||||
|
basicProfiles := []*profile.Profile{
|
||||||
|
{Id: 1, Name: "Alice"},
|
||||||
|
{Id: 2, Name: "Bob"},
|
||||||
|
{Id: 3, Name: "Charlie"},
|
||||||
|
{Id: 4, Name: "Dave"},
|
||||||
|
{Id: 5, Name: "Eva"},
|
||||||
|
}
|
||||||
|
//var emptyProfiles []*profile.Profile
|
||||||
|
//var emptyOrders []*order.Order
|
||||||
|
var emptyAggregateProfiles []*AggregatedProfile
|
||||||
|
testCases := []TestCase{
|
||||||
|
{
|
||||||
|
"no errors, profile service in time, order service in time, more than one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
10 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
20 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{2, 1, 20.6},
|
||||||
|
{3, 3, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
[]*AggregatedProfile{{"Alice", 100.0}, {"Alice", 20.6}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no errors, profile service in time, order service in time, one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
10 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
20 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 3, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
[]*AggregatedProfile{{"Alice", 100.0}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no errors, profile service in time, order service in time, no orders",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
10 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
20 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 2, 100.0},
|
||||||
|
{3, 3, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no errors, profile service in time, order service timeout, more than one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
10 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 1, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no errors, profile service in time, order service timeout, one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
10 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 2, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no errors, profile service in time, order service timeout, no orders",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
10 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 3, 100.0},
|
||||||
|
{3, 2, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no errors, profile service timeout, order service in time, more than one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
20 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 1, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no service timeout, no errors, more than one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
150 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 1, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
0 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
[]*AggregatedProfile{{"Alice", 100.0}, {"Alice", 30.79}},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no service timeout, profile error, more than one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
true,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
150 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 1, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
0 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no service timeout, order error, more than one order",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
70 * time.Millisecond,
|
||||||
|
false,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
100 * time.Millisecond,
|
||||||
|
true,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 1, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
0 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"no service timeout, profile error, order error, profile and order take same time",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
true,
|
||||||
|
basicProfiles,
|
||||||
|
},
|
||||||
|
&OrderServiceMock{
|
||||||
|
120 * time.Millisecond,
|
||||||
|
true,
|
||||||
|
[]*order.Order{
|
||||||
|
{1, 1, 100.0},
|
||||||
|
{3, 1, 30.79},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
0 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
emptyAggregateProfiles,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order service error propagates correctly",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{10 * time.Millisecond, false, basicProfiles},
|
||||||
|
&OrderServiceMock{10 * time.Millisecond, true, nil}, // Error here
|
||||||
|
1,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timeout error propagates correctly",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{200 * time.Millisecond, false, basicProfiles},
|
||||||
|
&OrderServiceMock{10 * time.Millisecond, false, nil},
|
||||||
|
1,
|
||||||
|
50 * time.Millisecond,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parent context cancellation propagates correctly",
|
||||||
|
Input{
|
||||||
|
&ProfileServiceMock{200 * time.Millisecond, false, basicProfiles},
|
||||||
|
&OrderServiceMock{10 * time.Millisecond, false, nil},
|
||||||
|
1,
|
||||||
|
500 * time.Millisecond,
|
||||||
|
100 * time.Millisecond,
|
||||||
|
},
|
||||||
|
Expected{
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if tc.input.parentContextDuration > 0 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, tc.input.parentContextDuration)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
spyLogger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||||
|
u := NewUserAggregator(
|
||||||
|
tc.input.orderService,
|
||||||
|
tc.input.profileService,
|
||||||
|
WithTimeout(tc.input.timeout),
|
||||||
|
WithLogger(spyLogger),
|
||||||
|
)
|
||||||
|
aggregatedProfiles, err := u.Aggregate(ctx, tc.input.searchedProfileId)
|
||||||
|
logOutput := buf.String()
|
||||||
|
if tc.expected.err {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, logOutput, "error")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, logOutput, "aggregation complete successfully")
|
||||||
|
assert.Contains(t, logOutput, "user_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected.aggregatedProfiles, aggregatedProfiles)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module concurrent-aggregator
|
|
||||||
|
|
||||||
go 1.25.0
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package order
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Order struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
Cost float64 `json:"cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
GetAll(ctx context.Context, userId int) ([]*Order, error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Get(ctx context.Context, id int) (*Profile, error)
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 medunes (medunes@protonmail.com)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
17
Makefile
Normal file
17
Makefile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
test:
|
||||||
|
go run gotest.tools/gotestsum@latest --format=testdox -- -covermode=atomic -coverprofile=coverage.txt ./...
|
||||||
|
benchmark:
|
||||||
|
go test -bench=. -benchmem ./...
|
||||||
|
format:
|
||||||
|
go fmt ./...
|
||||||
|
format-check:
|
||||||
|
@UNFORMATTED_FILES=$$(gofmt -l .); \
|
||||||
|
if [ -n "$$UNFORMATTED_FILES" ]; then \
|
||||||
|
echo "::error::The following files are not formatted correctly:"; \
|
||||||
|
echo "$$UNFORMATTED_FILES"; \
|
||||||
|
echo "--- Diff ---"; \
|
||||||
|
gofmt -d .; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: test format benchmark format-check
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
# 🥋 Go Katas 🥋
|
# 🥋 Go Katas 🥋
|
||||||
|
|
||||||
|
[](https://github.com/medunes/go-kata/actions/workflows/tests.yaml)
|
||||||
|
[](https://github.com/MedUnes/go-kata/actions/workflows/codeq.yaml)
|
||||||
|
[](https://codecov.io/gh/medunes/go-kata)
|
||||||
|
|
||||||
> "I fear not the man who has practiced 10,000 kicks once, but I fear the man who has practiced one kick 10,000 times."
|
> "I fear not the man who has practiced 10,000 kicks once, but I fear the man who has practiced one kick 10,000 times."
|
||||||
>
|
>
|
||||||
> (Bruce Lee)
|
> (Bruce Lee)
|
||||||
|
|
||||||
|
|
||||||
## What should it be?
|
## What should it be?
|
||||||
|
|
||||||
- Go is simple to learn, but nuanced to master. The difference between "working code" and "idiomatic code" often lies in details such as safety, memory efficiency, and concurrency control.
|
- Go is simple to learn, but nuanced to master. The difference between "working code" and "idiomatic code" often lies in details such as safety, memory efficiency, and concurrency control.
|
||||||
|
|||||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module github.com/medunes/go-kata
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
golang.org/x/sync v0.19.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
12
go.sum
Normal file
12
go.sum
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
Reference in New Issue
Block a user