mirror of
https://github.com/singchia/frontier.git
synced 2026-04-22 16:07:04 +08:00
Feat/container multi os (#98)
* Add comprehensive in-process test framework Add unit tests for exchange layer, E2E integration tests, security tests (race + fuzz), and Go benchmark tests replacing the old shell-script-based bench programs. All tests run in-process without requiring an external frontier process. Suppress klog and armorigo log noise in all test files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update build configs, Dockerfiles and dependencies Update Makefile with new targets, consolidate frontier_all.yaml config, bump base image versions in Dockerfiles, and update go.mod/go.sum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Revert etc/frontier_all.yaml to previous version Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
|
||||
|
||||
<skills_system priority="1">
|
||||
|
||||
## Available Skills
|
||||
|
||||
<!-- SKILLS_TABLE_START -->
|
||||
<usage>
|
||||
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
|
||||
|
||||
How to use skills:
|
||||
- Invoke: `npx openskills read <skill-name>` (run in your shell)
|
||||
- For multiple: `npx openskills read skill-one,skill-two`
|
||||
- The skill content will load with detailed instructions on how to complete the task
|
||||
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
|
||||
|
||||
Usage notes:
|
||||
- Only use skills listed in <available_skills> below
|
||||
- Do not invoke a skill that is already loaded in your context
|
||||
- Each skill invocation is stateless
|
||||
</usage>
|
||||
|
||||
<available_skills>
|
||||
|
||||
<skill>
|
||||
<name>algorithmic-art</name>
|
||||
<description>Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>brand-guidelines</name>
|
||||
<description>Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>canvas-design</name>
|
||||
<description>Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>doc-coauthoring</name>
|
||||
<description>Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>docx</name>
|
||||
<description>"Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of \"Word doc\", \"word document\", \".docx\", or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a \"report\", \"memo\", \"letter\", \"template\", or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>frontend-design</name>
|
||||
<description>Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>internal-comms</name>
|
||||
<description>A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>mcp-builder</name>
|
||||
<description>Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>pdf</name>
|
||||
<description>Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>pptx</name>
|
||||
<description>"Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill."</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>skill-creator</name>
|
||||
<description>Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>slack-gif-creator</name>
|
||||
<description>Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>template</name>
|
||||
<description>Replace with description of the skill and when Claude should use it.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>theme-factory</name>
|
||||
<description>Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>web-artifacts-builder</name>
|
||||
<description>Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>webapp-testing</name>
|
||||
<description>Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
<skill>
|
||||
<name>xlsx</name>
|
||||
<description>"Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved."</description>
|
||||
<location>global</location>
|
||||
</skill>
|
||||
|
||||
</available_skills>
|
||||
<!-- SKILLS_TABLE_END -->
|
||||
|
||||
</skills_system>
|
||||
@@ -52,6 +52,12 @@ help:
|
||||
@echo " make frontier-windows-amd64 - Build frontier for Windows amd64"
|
||||
@echo " make frontier-windows-arm64 - Build frontier for Windows arm64"
|
||||
@echo ""
|
||||
@echo "Docker images (REGISTRY=, VERSION=):"
|
||||
@echo " make image-frontier - Build frontier image (current platform)"
|
||||
@echo " make image-frontier-linux - Build frontier image for linux/amd64"
|
||||
@echo " make image-frontlas - Build frontlas image (current platform)"
|
||||
@echo " make image-frontlas-linux - Build frontlas image for linux/amd64"
|
||||
@echo ""
|
||||
@echo "Other targets:"
|
||||
@echo " make clean - Clean local build artifacts"
|
||||
@echo " make clean-dist - Clean cross-compilation artifacts"
|
||||
@@ -419,10 +425,18 @@ uninstall-systemd:
|
||||
image-frontier:
|
||||
docker buildx build -t ${REGISTRY}/frontier:${VERSION} -f images/Dockerfile.frontier .
|
||||
|
||||
.PHONY: image-frontier-linux
|
||||
image-frontier-linux:
|
||||
docker buildx build --platform linux/amd64 -t ${REGISTRY}/frontier:${VERSION} -f images/Dockerfile.frontier .
|
||||
|
||||
.PHONY: image-frontlas
|
||||
image-frontlas:
|
||||
docker buildx build -t ${REGISTRY}/frontlas:${VERSION} -f images/Dockerfile.frontlas .
|
||||
|
||||
.PHONY: image-frontlas-linux
|
||||
image-frontlas-linux:
|
||||
docker buildx build --platform linux/amd64 -t ${REGISTRY}/frontlas:${VERSION} -f images/Dockerfile.frontlas .
|
||||
|
||||
.PHONY: image-gen-api
|
||||
image-gen-api:
|
||||
docker buildx build -t image-gen-api:${VERSION} -f images/Dockerfile.controlplane-api .
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -21,5 +21,5 @@ redis:
|
||||
mode: standalone
|
||||
standalone:
|
||||
network: tcp
|
||||
addr: redis:6379
|
||||
addr: 127.0.0.1:6379
|
||||
db: 0
|
||||
|
||||
@@ -12,7 +12,7 @@ require (
|
||||
github.com/nats-io/nats.go v1.33.1
|
||||
github.com/nsqio/go-nsq v1.1.0
|
||||
github.com/rabbitmq/amqp091-go v1.9.0
|
||||
github.com/singchia/geminio v1.2.2-rc.2
|
||||
github.com/singchia/geminio v1.2.3-rc.1
|
||||
github.com/singchia/go-timer/v2 v2.2.1
|
||||
github.com/singchia/joy4 v0.0.0-20240621074108-53a2b0132ec6
|
||||
github.com/soheilhy/cmux v0.1.5
|
||||
|
||||
@@ -146,8 +146,8 @@ github.com/redis/go-redis/v9 v9.5.5 h1:51VEyMF8eOO+NUHFm8fpg+IOc1xFuFOhxs3R+kPu1
|
||||
github.com/redis/go-redis/v9 v9.5.5/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/singchia/geminio v1.2.2-rc.2 h1:3cAb2GrgxCp1tQwy0ZwLR2b0HsjwY4NfAMO28QEQm+M=
|
||||
github.com/singchia/geminio v1.2.2-rc.2/go.mod h1:b6bld5o0aofg/kuAdc5uAnaTJYvd6YaJxYDtH9b+NzY=
|
||||
github.com/singchia/geminio v1.2.3-rc.1 h1:UbMMsxNe5i7vUFNjKmtj0XryqUV9HNgRZ6HSPOrTvGg=
|
||||
github.com/singchia/geminio v1.2.3-rc.1/go.mod h1:b6bld5o0aofg/kuAdc5uAnaTJYvd6YaJxYDtH9b+NzY=
|
||||
github.com/singchia/go-timer/v2 v2.0.3/go.mod h1:PgkEQc6io8slCUiT5rHzWKU4/P2HXHWk3WWfijZXAf4=
|
||||
github.com/singchia/go-timer/v2 v2.2.1 h1:gJucmL99fkuNzGk2AfNPFpa1X3/4+aGO21KkjFAG624=
|
||||
github.com/singchia/go-timer/v2 v2.2.1/go.mod h1:PgkEQc6io8slCUiT5rHzWKU4/P2HXHWk3WWfijZXAf4=
|
||||
|
||||
@@ -15,7 +15,7 @@ ARG CGO_ENABLED=1
|
||||
# Linux: Enable CGO (native build in Linux container)
|
||||
# Windows/macOS: Disable CGO (cross-compilation from Linux)
|
||||
ENV GO111MODULE=on \
|
||||
GOPROXY=https://goproxy.io,direct
|
||||
GOPROXY=https://goproxy.cn,https://goproxy.io,https://proxy.golang.org,direct
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM golang:1.18-alpine
|
||||
RUN apk add --no-cache curl unzip protoc protobuf-dev
|
||||
|
||||
ENV GO111MODULE=on \
|
||||
GOPROXY=https://goproxy.io,direct
|
||||
GOPROXY=https://goproxy.cn,https://goproxy.io,https://proxy.golang.org,direct
|
||||
|
||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \
|
||||
&& go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest \
|
||||
|
||||
@@ -6,7 +6,7 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GO111MODULE=on \
|
||||
GOPROXY=https://goproxy.io,direct
|
||||
GOPROXY=https://goproxy.cn,https://goproxy.io,https://proxy.golang.org,direct
|
||||
|
||||
WORKDIR /go/src/github.com/singchia/frontier
|
||||
RUN --mount=type=bind,readwrite,target=/go/src/github.com/singchia/frontier \
|
||||
|
||||
@@ -6,7 +6,7 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GO111MODULE=on \
|
||||
GOPROXY=https://goproxy.io,direct
|
||||
GOPROXY=https://goproxy.cn,https://goproxy.io,https://proxy.golang.org,direct
|
||||
|
||||
WORKDIR /go/src/github.com/singchia/frontier
|
||||
RUN --mount=type=bind,readwrite,target=/go/src/github.com/singchia/frontier \
|
||||
|
||||
@@ -6,7 +6,7 @@ ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GO111MODULE=on \
|
||||
GOPROXY=https://goproxy.io,direct
|
||||
GOPROXY=https://goproxy.cn,https://goproxy.io,https://proxy.golang.org,direct
|
||||
|
||||
WORKDIR /go/src/github.com/singchia/frontier
|
||||
RUN --mount=type=bind,readwrite,target=/go/src/github.com/singchia/frontier \
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jumboframes/armorigo/log"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
gconfig "github.com/singchia/frontier/pkg/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/edgebound"
|
||||
"github.com/singchia/frontier/pkg/frontier/mq"
|
||||
"github.com/singchia/frontier/pkg/frontier/repo"
|
||||
"github.com/singchia/frontier/pkg/frontier/servicebound"
|
||||
"github.com/singchia/geminio"
|
||||
"github.com/singchia/go-timer/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
klog.InitFlags(nil)
|
||||
flag.Set("v", "0")
|
||||
flag.Set("logtostderr", "false")
|
||||
flag.Set("stderrthreshold", "FATAL")
|
||||
|
||||
log.SetLevel(log.LevelFatal)
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
const (
|
||||
testNetwork = "tcp"
|
||||
edgeboundAddr = "127.0.0.1:13300"
|
||||
serviceboundAddr = "127.0.0.1:13301"
|
||||
)
|
||||
|
||||
// exchangeHarness starts an in-process exchange + edgebound + servicebound.
|
||||
type exchangeHarness struct {
|
||||
eb interface{ Close() error }
|
||||
sb interface{ Close() error }
|
||||
r interface{ Close() error }
|
||||
mqm interface{ Close() error }
|
||||
tmr timer.Timer
|
||||
}
|
||||
|
||||
func newHarness(t *testing.T) *exchangeHarness {
|
||||
t.Helper()
|
||||
conf := &config.Configuration{
|
||||
Edgebound: config.Edgebound{
|
||||
Listen: gconfig.Listen{Network: testNetwork, Addr: edgeboundAddr},
|
||||
EdgeIDAllocWhenNoIDServiceOn: true,
|
||||
},
|
||||
Servicebound: config.Servicebound{
|
||||
Listen: gconfig.Listen{Network: testNetwork, Addr: serviceboundAddr},
|
||||
},
|
||||
}
|
||||
r, err := repo.NewRepo(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
mqm, err := mq.NewMQM(conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
tmr := timer.NewTimer()
|
||||
ex := NewExchange(conf, mqm)
|
||||
|
||||
sb, err := servicebound.NewServicebound(conf, r, nil, ex, mqm, tmr)
|
||||
require.NoError(t, err)
|
||||
|
||||
eb, err := edgebound.NewEdgebound(conf, r, nil, ex, tmr)
|
||||
require.NoError(t, err)
|
||||
|
||||
go sb.Serve()
|
||||
go eb.Serve()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
h := &exchangeHarness{eb: eb, sb: sb, r: r, mqm: mqm, tmr: tmr}
|
||||
t.Cleanup(func() {
|
||||
eb.Close()
|
||||
sb.Close()
|
||||
r.Close()
|
||||
mqm.Close()
|
||||
tmr.Close()
|
||||
})
|
||||
return h
|
||||
}
|
||||
|
||||
func edgeDial() edge.Dialer {
|
||||
return func() (net.Conn, error) { return net.Dial(testNetwork, edgeboundAddr) }
|
||||
}
|
||||
func svcDial() service.Dialer {
|
||||
return func() (net.Conn, error) { return net.Dial(testNetwork, serviceboundAddr) }
|
||||
}
|
||||
|
||||
// UNIT-EXCH-001: RPC from Edge forwarded to Service
|
||||
func TestExchangeForwardRPCToService(t *testing.T) {
|
||||
newHarness(t)
|
||||
|
||||
svc, err := service.NewService(svcDial(), service.OptionServiceName("rpc-svc"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
require.NoError(t, svc.Register(context.TODO(), "echo", func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
}))
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
e, err := edge.NewEdge(edgeDial())
|
||||
require.NoError(t, err)
|
||||
defer e.Close()
|
||||
|
||||
req := e.NewRequest([]byte("ping"))
|
||||
resp, err := e.Call(context.TODO(), "echo", req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("ping"), resp.Data())
|
||||
}
|
||||
|
||||
// UNIT-EXCH-002: Message from Edge forwarded to Service via topic
|
||||
func TestExchangeForwardMessageToService(t *testing.T) {
|
||||
newHarness(t)
|
||||
|
||||
const topic = "news"
|
||||
svc, err := service.NewService(svcDial(),
|
||||
service.OptionServiceName("msg-svc"),
|
||||
service.OptionServiceReceiveTopics([]string{topic}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
|
||||
received := make(chan []byte, 1)
|
||||
go func() {
|
||||
msg, err := svc.Receive(context.TODO())
|
||||
if err == nil {
|
||||
received <- msg.Data()
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
e, err := edge.NewEdge(edgeDial())
|
||||
require.NoError(t, err)
|
||||
defer e.Close()
|
||||
|
||||
msg := e.NewMessage([]byte("headline"))
|
||||
require.NoError(t, e.Publish(context.TODO(), topic, msg))
|
||||
|
||||
select {
|
||||
case data := <-received:
|
||||
assert.Equal(t, []byte("headline"), data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out")
|
||||
}
|
||||
}
|
||||
|
||||
// UNIT-EXCH-003: RPC from Service forwarded to specific Edge
|
||||
func TestExchangeForwardRPCToEdge(t *testing.T) {
|
||||
newHarness(t)
|
||||
|
||||
e, err := edge.NewEdge(edgeDial())
|
||||
require.NoError(t, err)
|
||||
defer e.Close()
|
||||
require.NoError(t, e.Register(context.TODO(), "greet", func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData([]byte("hello-from-edge"))
|
||||
}))
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
svc, err := service.NewService(svcDial(), service.OptionServiceName("rpc-caller"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
|
||||
req := svc.NewRequest([]byte(""))
|
||||
resp, err := svc.Call(context.TODO(), e.EdgeID(), "greet", req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("hello-from-edge"), resp.Data())
|
||||
}
|
||||
|
||||
// UNIT-EXCH-004: Message from Service delivered to specific Edge
|
||||
func TestExchangeForwardMessageToEdge(t *testing.T) {
|
||||
newHarness(t)
|
||||
|
||||
e, err := edge.NewEdge(edgeDial())
|
||||
require.NoError(t, err)
|
||||
defer e.Close()
|
||||
|
||||
received := make(chan []byte, 1)
|
||||
go func() {
|
||||
msg, err := e.Receive(context.TODO())
|
||||
if err == nil {
|
||||
received <- msg.Data()
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
svc, err := service.NewService(svcDial(), service.OptionServiceName("msg-pub"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
|
||||
msg := svc.NewMessage([]byte("push-to-edge"))
|
||||
require.NoError(t, svc.Publish(context.TODO(), e.EdgeID(), msg))
|
||||
|
||||
select {
|
||||
case data := <-received:
|
||||
assert.Equal(t, []byte("push-to-edge"), data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out")
|
||||
}
|
||||
}
|
||||
|
||||
// UNIT-EXCH-005: Stream opened from Edge transparently forwarded to Service
|
||||
func TestExchangeStreamToService(t *testing.T) {
|
||||
newHarness(t)
|
||||
|
||||
accepted := make(chan geminio.Stream, 1)
|
||||
svc, err := service.NewService(svcDial(), service.OptionServiceName("stream-svc"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
go func() {
|
||||
if st, err := svc.AcceptStream(); err == nil {
|
||||
accepted <- st
|
||||
}
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
e, err := edge.NewEdge(edgeDial())
|
||||
require.NoError(t, err)
|
||||
defer e.Close()
|
||||
|
||||
st, err := e.OpenStream("stream-svc")
|
||||
require.NoError(t, err)
|
||||
defer st.Close()
|
||||
|
||||
select {
|
||||
case serverSt := <-accepted:
|
||||
assert.NotNil(t, serverSt)
|
||||
serverSt.Close()
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for stream on service side")
|
||||
}
|
||||
}
|
||||
|
||||
// UNIT-EXCH-006: Stream opened from Service transparently forwarded to Edge
|
||||
func TestExchangeStreamToEdge(t *testing.T) {
|
||||
newHarness(t)
|
||||
|
||||
accepted := make(chan geminio.Stream, 1)
|
||||
e, err := edge.NewEdge(edgeDial())
|
||||
require.NoError(t, err)
|
||||
defer e.Close()
|
||||
go func() {
|
||||
if st, err := e.AcceptStream(); err == nil {
|
||||
accepted <- st
|
||||
}
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
svc, err := service.NewService(svcDial(), service.OptionServiceName("stream-opener"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
|
||||
st, err := svc.OpenStream(context.TODO(), e.EdgeID())
|
||||
require.NoError(t, err)
|
||||
defer st.Close()
|
||||
|
||||
select {
|
||||
case edgeSt := <-accepted:
|
||||
assert.NotNil(t, edgeSt)
|
||||
edgeSt.Close()
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for stream on edge side")
|
||||
}
|
||||
}
|
||||
|
||||
// UNIT-EXCH-007: Edge online/offline events are forwarded to Service via control RPCs
|
||||
func TestExchangeEdgeOnlineOffline(t *testing.T) {
|
||||
newHarness(t)
|
||||
|
||||
onlineCh := make(chan uint64, 1)
|
||||
offlineCh := make(chan uint64, 1)
|
||||
|
||||
svc, err := service.NewService(svcDial(), service.OptionServiceName("event-watcher"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
|
||||
require.NoError(t, svc.RegisterEdgeOnline(context.TODO(), func(edgeID uint64, meta []byte, addr net.Addr) error {
|
||||
onlineCh <- edgeID
|
||||
return nil
|
||||
}))
|
||||
require.NoError(t, svc.RegisterEdgeOffline(context.TODO(), func(edgeID uint64, meta []byte, addr net.Addr) error {
|
||||
offlineCh <- edgeID
|
||||
return nil
|
||||
}))
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
// connect then disconnect an edge
|
||||
e, err := edge.NewEdge(edgeDial())
|
||||
require.NoError(t, err)
|
||||
edgeID := e.EdgeID()
|
||||
|
||||
select {
|
||||
case id := <-onlineCh:
|
||||
assert.Equal(t, edgeID, id)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for EdgeOnline event")
|
||||
}
|
||||
|
||||
e.Close()
|
||||
|
||||
select {
|
||||
case id := <-offlineCh:
|
||||
assert.Equal(t, edgeID, id)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for EdgeOffline event")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
# Frontier 测试计划
|
||||
|
||||
**文档版本:** 1.1
|
||||
**创建日期:** 2026-04-01
|
||||
**测试执行:** Claude Code
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [项目概述与测试范围](#一项目概述与测试范围)
|
||||
2. [测试分类与编号规则](#二测试分类与编号规则)
|
||||
3. [单元测试](#三单元测试)
|
||||
4. [基准测试](#四基准测试)
|
||||
5. [端到端测试](#五端到端测试)
|
||||
6. [安全测试](#六安全测试)
|
||||
7. [测试覆盖矩阵](#七测试覆盖矩阵)
|
||||
8. [执行命令速查](#八执行命令速查)
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概述与测试范围
|
||||
|
||||
### 架构简述
|
||||
|
||||
Frontier 是一个面向边缘节点的反向代理与消息总线,核心数据流如下:
|
||||
|
||||
```
|
||||
Edge (边缘节点)
|
||||
│ TCP/TLS
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ Frontier │
|
||||
│ ┌───────────┐ ┌────────────┐ │
|
||||
│ │ Edgebound │ │Servicebound│ │
|
||||
│ └─────┬─────┘ └─────┬──────┘ │
|
||||
│ └──────┬────────┘ │
|
||||
│ ┌────▼────┐ │
|
||||
│ │Exchange │ │
|
||||
│ └─────────┘ │
|
||||
└─────────────────────────────────┘
|
||||
│ TCP/TLS
|
||||
▼
|
||||
Service (业务服务)
|
||||
```
|
||||
|
||||
**核心能力(测试重点):**
|
||||
- **Edgebound**:接受 Edge 接入,管理 Edge 连接生命周期
|
||||
- **Servicebound**:接受 Service 接入,管理 Service 注册与路由
|
||||
- **Exchange**:Edge ↔ Service 之间的 RPC 转发、消息转发、Stream 透传
|
||||
- **Repo(DAO)**:内存数据库(buntdb / sqlite)存储 Edge/Service 元数据
|
||||
|
||||
### 测试范围
|
||||
|
||||
| 包含 | 不包含 |
|
||||
|------|--------|
|
||||
| frontier 核心数据面(edgebound / servicebound / exchange) | frontlas(集群控制面) |
|
||||
| Repo DAO(membuntdb / memsqlite) | Kubernetes Operator |
|
||||
| 配置加载(config) | MQ 外部依赖集成(Kafka/NATS/NSQ 等) |
|
||||
| 基准测试(bench / batch) | 控制面 REST/gRPC API |
|
||||
|
||||
---
|
||||
|
||||
## 二、测试分类与编号规则
|
||||
|
||||
### 2.1 测试类别
|
||||
|
||||
| 类别编码 | 类别名称 | 测试工具 | 目录 |
|
||||
|---------|---------|---------|------|
|
||||
| UNIT | 单元测试 | `go test` | `pkg/frontier/...` |
|
||||
| BENCH | 基准测试 | `go test -bench` / 独立二进制 | `test/bench/`, `test/batch/` |
|
||||
| E2E | 端到端测试 | `go test` + 本地 frontier 实例 | `test/e2e/`(待创建)|
|
||||
| SEC | 安全测试 | `go test -race` / `go test -fuzz` | `test/security/`(待创建)|
|
||||
|
||||
### 2.2 编号规则
|
||||
|
||||
格式:`[类别]-[模块]-[序号]`
|
||||
|
||||
| 缩写 | 模块 |
|
||||
|------|------|
|
||||
| EDGE | Edgebound |
|
||||
| SVC | Servicebound |
|
||||
| EXCH | Exchange |
|
||||
| REPO | Repo/DAO |
|
||||
| CONF | Config |
|
||||
| CONN | 连接管理 |
|
||||
| RPC | RPC 转发 |
|
||||
| MSG | 消息转发 |
|
||||
| STRM | Stream 透传 |
|
||||
|
||||
---
|
||||
|
||||
## 三、单元测试
|
||||
|
||||
### 3.1 已有测试(`pkg/`)
|
||||
|
||||
| 编号 | 测试名称 | 文件 | 验证点 |
|
||||
|------|---------|------|--------|
|
||||
| UNIT-CONF-001 | TestGenDefaultConfig | `pkg/frontier/config/config_test.go` | 默认配置序列化到 YAML |
|
||||
| UNIT-CONF-002 | TestGenAllConfig | `pkg/frontier/config/config_test.go` | 完整配置序列化到 YAML |
|
||||
| UNIT-EDGE-001 | TestEdgeManager | `pkg/frontier/edgebound/edge_manager_test.go` | Edge 接入→在线→断开完整流程 |
|
||||
| UNIT-EDGE-002 | TestEdgeManagerStream | `pkg/frontier/edgebound/edge_dataplane_test.go` | Edge 批量创建 Stream(1000条) |
|
||||
| UNIT-SVC-001 | TestServiceManager | `pkg/frontier/servicebound/service_manager_test.go` | Service 接入→在线→断开完整流程 |
|
||||
| UNIT-REPO-001 | TestListEdges(buntdb) | `pkg/frontier/repo/dao/membuntdb/dao_edge_test.go` | Edge 列表按地址前缀/时间范围查询 |
|
||||
| UNIT-REPO-002 | TestListEdgeRPCs(buntdb) | `pkg/frontier/repo/dao/membuntdb/dao_edge_test.go` | EdgeRPC 多条件查询 |
|
||||
| UNIT-REPO-003 | TestListServices(buntdb) | `pkg/frontier/repo/dao/membuntdb/dao_service_test.go` | Service 列表查询及分页 |
|
||||
| UNIT-REPO-004 | TestDeleteService(buntdb) | `pkg/frontier/repo/dao/membuntdb/dao_service_test.go` | Service 删除后数量校验 |
|
||||
| UNIT-REPO-005 | TestListServiceRPCs(buntdb) | `pkg/frontier/repo/dao/membuntdb/dao_service_test.go` | ServiceRPC 按 ID/时间查询 |
|
||||
| UNIT-REPO-006 | TestListServiceTopics(buntdb) | `pkg/frontier/repo/dao/membuntdb/dao_service_test.go` | ServiceTopic 多条件查询 |
|
||||
| UNIT-REPO-007 | TestCreateEdge(sqlite) | `pkg/frontier/repo/dao/memsqlite/dao_edge_test.go` | Edge 写入 sqlite |
|
||||
| UNIT-REPO-008 | TestCountEdges(sqlite) | `pkg/frontier/repo/dao/memsqlite/dao_edge_test.go` | 批量写入后计数校验(10000条)|
|
||||
| UNIT-REPO-009 | BenchmarkCreateEdge(sqlite) | `pkg/frontier/repo/dao/memsqlite/dao_edge_test.go` | Edge 写入并发性能基线 |
|
||||
| UNIT-REPO-010 | BenchmarkGetEdge(sqlite) | `pkg/frontier/repo/dao/memsqlite/dao_edge_test.go` | Edge 读取并发性能基线 |
|
||||
| UNIT-REPO-011 | BenchmarkListEdges(sqlite) | `pkg/frontier/repo/dao/memsqlite/dao_edge_test.go` | 10万条数据分页查询性能 |
|
||||
| UNIT-REPO-012 | TestListServices(sqlite) | `pkg/frontier/repo/dao/memsqlite/dao_service_test.go` | Service 与 RPC/Topic 联合查询 |
|
||||
|
||||
### 3.2 待补充测试
|
||||
|
||||
| 编号 | 建议测试名称 | 目标文件 | 验证点 |
|
||||
|------|------------|---------|--------|
|
||||
| UNIT-EXCH-001 | TestExchangeForwardRPCToService | `pkg/frontier/exchange/` | RPC 从 Edge 转发到 Service 全流程 |
|
||||
| UNIT-EXCH-002 | TestExchangeForwardMessageToService | `pkg/frontier/exchange/` | 消息从 Edge 转发到 Service 全流程 |
|
||||
| UNIT-EXCH-003 | TestExchangeForwardRPCToEdge | `pkg/frontier/exchange/` | RPC 从 Service 转发到指定 Edge |
|
||||
| UNIT-EXCH-004 | TestExchangeForwardMessageToEdge | `pkg/frontier/exchange/` | 消息从 Service 投递到指定 Edge |
|
||||
| UNIT-EXCH-005 | TestExchangeStreamToService | `pkg/frontier/exchange/` | Stream 从 Edge 透传到 Service |
|
||||
| UNIT-EXCH-006 | TestExchangeStreamToEdge | `pkg/frontier/exchange/` | Stream 从 Service 透传到 Edge |
|
||||
| UNIT-EXCH-007 | TestExchangeEdgeOnlineOffline | `pkg/frontier/exchange/` | Edge 上下线事件通知 Service |
|
||||
| UNIT-EDGE-003 | TestEdgeManagerMultiple | `pkg/frontier/edgebound/` | 多 Edge 并发接入,ID 分配唯一性 |
|
||||
| UNIT-SVC-002 | TestServiceManagerRouting | `pkg/frontier/servicebound/` | 按 RPC/Topic/Name 查找 Service |
|
||||
|
||||
---
|
||||
|
||||
## 四、基准测试
|
||||
|
||||
基准测试为**独立二进制**,需先启动一个本地 frontier 实例,再运行对应客户端程序。
|
||||
|
||||
### 4.1 已有基准测试
|
||||
|
||||
| 编号 | 测试方法 | 文件 | 场景 |
|
||||
|------|---------|------|------|
|
||||
| BENCH-CALL-001 | `BenchmarkEdgeCallService` | `test/bench/benchmark_test.go` | Edge 端通过 Frontier 调用 Service 的 RPC 的并发吞吐 (QPS) |
|
||||
| BENCH-PUB-001 | `BenchmarkEdgePublishMessage` | `test/bench/benchmark_test.go` | Edge 端通过 Frontier 发布消息的并发吞吐 (QPS) |
|
||||
| BENCH-OPEN-001 | `BenchmarkEdgeOpenStream` | `test/bench/benchmark_test.go` | Edge 端通过 Frontier 打开并关闭 Stream 的并发吞吐 (QPS) |
|
||||
| BENCH-EDGE-001 | `edges` (独立二进制) | `test/batch/edges/edges.go` | 大规模边缘节点长连接模拟 |
|
||||
|
||||
### 4.2 待补充基准测试
|
||||
|
||||
| 编号 | 建议测试名称 | 目录 | 场景 |
|
||||
|------|------------|------|------|
|
||||
| BENCH-CONN-001 | `BenchmarkConnect` | `test/bench/benchmark_test.go` | 测量每秒可接入 Edge 连接数(TPS) |
|
||||
| BENCH-STRM-001 | `BenchmarkStreamTransfer` | `test/bench/benchmark_test.go` | Stream 双向数据传输带��测试 |
|
||||
|
||||
### 4.3 基准测试执行方式
|
||||
|
||||
```bash
|
||||
# 运行所有性能基准测试并打印内存分配情况
|
||||
go test -bench=. -benchmem -v ./test/bench/...
|
||||
|
||||
# 运行特定模块基准测试并设定测试时间(如 10s)
|
||||
go test -bench=BenchmarkEdgeCallService -benchtime=10s ./test/bench/...
|
||||
|
||||
# 大规模连接模拟(独立二进制)
|
||||
cd test/batch/edges && make
|
||||
./edges --address 127.0.0.1:30011 --count 10000 --nseconds 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、端到端测试
|
||||
|
||||
E2E 测试在进程内启动 frontier(不依赖外部进程),验证 Edge → Frontier → Service 的完整链路。
|
||||
|
||||
### 5.1 测试目录结构(待创建)
|
||||
|
||||
```
|
||||
test/e2e/
|
||||
├── main_test.go # TestMain:启动/停止嵌入式 frontier
|
||||
├── helper.go # 公共 dialer、frontier 启动工具函数
|
||||
├── conn_test.go # 连接生命周期测试
|
||||
├── rpc_test.go # RPC 转发测试
|
||||
├── message_test.go # 消息转发测试
|
||||
└── stream_test.go # Stream 透传测试
|
||||
```
|
||||
|
||||
### 5.2 E2E 测试用例
|
||||
|
||||
#### 连接管理(CONN)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| E2E-CONN-001 | TestEdgeConnect | Edge 成功接入 frontier,edgeID 非零 |
|
||||
| E2E-CONN-002 | TestEdgeConnectAndClose | Edge 正常关闭,frontier 侧资源清理完毕 |
|
||||
| E2E-CONN-003 | TestEdgeConnectWithMeta | Edge 携带 meta 接入,Service 侧通过 `EdgeOnline` 回调获得正确 meta |
|
||||
| E2E-CONN-004 | TestMultiEdgeConnect | 100 个 Edge 并发接入,全部成功且 edgeID 唯一 |
|
||||
| E2E-CONN-005 | TestServiceConnect | Service 成功接入并注册 RPC/Topic |
|
||||
| E2E-CONN-006 | TestServiceConnectAndClose | Service 下线后,Edge 侧 RPC 调用返回 `ErrServiceNotOnline` |
|
||||
|
||||
#### RPC 转发(RPC)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| E2E-RPC-001 | TestEdgeCallService | Edge 通过 frontier 调用 Service 注册的 RPC,返回正确响应 |
|
||||
| E2E-RPC-002 | TestServiceCallEdge | Service 通过 frontier 调用 Edge 注册的 RPC,指定 edgeID |
|
||||
| E2E-RPC-003 | TestRPCEdgeIDCarry | Service 调用 Edge RPC 时,frontier 正确在 Custom 字段附加 edgeID |
|
||||
| E2E-RPC-004 | TestRPCTargetEdgeOffline | 目标 Edge 已下线时,Service 调用返回 `ErrEdgeNotOnline` |
|
||||
| E2E-RPC-005 | TestRPCTargetRPCNotFound | Service 调用不存在的 RPC 方法时,Edge 返回错误 |
|
||||
| E2E-RPC-006 | TestRPCConcurrent | 10 个 Edge 同时调用 Service RPC,无错误,响应数据一致 |
|
||||
|
||||
#### 消息转发(MSG)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| E2E-MSG-001 | TestEdgePublishToService | Edge Publish 消息,Service 通过已注册 Topic 正确 Receive |
|
||||
| E2E-MSG-002 | TestServicePublishToEdge | Service Publish 消息到指定 edgeID,Edge 正确 Receive |
|
||||
| E2E-MSG-003 | TestMessageTopicRoute | 多个 Service 注册不同 Topic,Edge 消息按 Topic 路由到正确 Service |
|
||||
| E2E-MSG-004 | TestMessageTopicNotFound | Edge 发布不存在 Topic 的消息,返回 `ErrTopicNotOnline` |
|
||||
| E2E-MSG-005 | TestMessageConcurrent | 10 个 Edge 并发 Publish,消息不丢失,数量一致 |
|
||||
|
||||
#### Stream 透传(STRM)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| E2E-STRM-001 | TestEdgeOpenStreamToService | Edge OpenStream 到指定 Service,Service AcceptStream 收到 |
|
||||
| E2E-STRM-002 | TestServiceOpenStreamToEdge | Service OpenStream 到指定 edgeID,Edge AcceptStream 收到 |
|
||||
| E2E-STRM-003 | TestStreamRawDataForward | Stream 内 Raw IO 双向传输,数据内容完整一致 |
|
||||
| E2E-STRM-004 | TestStreamMessageForward | Stream 内 Message 双向转发,数据内容完整一致 |
|
||||
| E2E-STRM-005 | TestStreamRPCForward | Stream 内 RPC 双向调用,返回值正确 |
|
||||
| E2E-STRM-006 | TestStreamClose | Stream 一端关闭,另一端收到 EOF,资源正确清理 |
|
||||
| E2E-STRM-007 | TestStreamTargetEdgeOffline | 目标 Edge 不在线时,Service OpenStream 返回错误 |
|
||||
|
||||
#### 资源管理(RES)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| E2E-RES-001 | TestResourceCleanupOnEdgeClose | Edge 关闭后,Repo 中 Edge 及其 RPC 记录被删除 |
|
||||
| E2E-RES-002 | TestResourceCleanupOnServiceClose | Service 关闭后,Repo 中 Service 及其 RPC/Topic 记录被删除 |
|
||||
| E2E-RES-003 | TestGoroutineNoLeak | 100 次 Edge 接入/断开循环后,goroutine 数量回落到基线 |
|
||||
|
||||
### 5.3 E2E 执行方式
|
||||
|
||||
```bash
|
||||
# 运行所有 E2E 测试
|
||||
go test -v -timeout 5m ./test/e2e/
|
||||
|
||||
# 带竞态检测
|
||||
go test -race -v -timeout 5m ./test/e2e/
|
||||
|
||||
# 运行单个用例
|
||||
go test -v -run TestEdgeCallService ./test/e2e/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、安全测试
|
||||
|
||||
### 6.1 测试目录结构(待创建)
|
||||
|
||||
```
|
||||
test/security/
|
||||
├── main_test.go
|
||||
├── input_test.go # 输入合法性验证
|
||||
├── boundary_test.go # 边界值测试
|
||||
├── race_test.go # 并发竞态测试
|
||||
└── fuzz_test.go # 模糊测试
|
||||
```
|
||||
|
||||
### 6.2 安全测试用例
|
||||
|
||||
#### 输入合法性(INPUT)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| SEC-INPUT-001 | TestLargePayloadRPC | RPC 请求携带 64MB payload,frontier 不崩溃,返回正常错误或正常响应 |
|
||||
| SEC-INPUT-002 | TestEmptyPayloadRPC | RPC/消息 payload 为空(nil / 0字节),frontier 正常处理 |
|
||||
| SEC-INPUT-003 | TestSpecialCharactersMeta | Edge meta 包含特殊字符(换行、空字节、Unicode),frontier 正常接受 |
|
||||
| SEC-INPUT-004 | TestNilMessageData | Edge 发送 nil data 的消息,frontier 不 panic |
|
||||
|
||||
#### 边界值(BOUND)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| SEC-BOUND-001 | TestMaxEdgeConnections | 超大量 Edge 并发接入(如 65535),系统不崩溃,超出限制时返回可预期错误 |
|
||||
| SEC-BOUND-002 | TestMaxStreamsPerEdge | 单个 Edge 打开 10000 个 Stream,frontier 不崩溃,资源可释放 |
|
||||
| SEC-BOUND-003 | TestEdgeIDOverflow | edgeID 为 0 / MaxUint64 等边界值,frontier 正确拒绝或处理 |
|
||||
|
||||
#### 并发竞态(RACE)
|
||||
|
||||
| 编号 | 测试名称 | 执行方式 | 验证点 |
|
||||
|------|---------|---------|--------|
|
||||
| SEC-RACE-001 | TestRaceEdgeConnectClose | `-race` | 并发 Connect 和 Close,无 data race |
|
||||
| SEC-RACE-002 | TestRaceMultipleEdgeClose | `-race` | 同一 Edge 被多个 goroutine 并发 Close,无 panic / data race |
|
||||
| SEC-RACE-003 | TestRaceServiceRegisterUnregister | `-race` | Service 并发注册/注销 RPC,无 data race |
|
||||
| SEC-RACE-004 | TestRaceForwardAndClose | `-race` | Edge 正在转发 RPC 时同时 Close,无 panic |
|
||||
|
||||
#### 模糊测试(FUZZ,Go 1.18+)
|
||||
|
||||
| 编号 | 测试名称 | 验证点 |
|
||||
|------|---------|--------|
|
||||
| SEC-FUZZ-001 | FuzzEdgeMeta | 随机 meta 字节序列作为 Edge 接入 meta,frontier 不 panic |
|
||||
| SEC-FUZZ-002 | FuzzRPCPayload | 随机 payload 通过 RPC 调用,frontier 不 panic |
|
||||
| SEC-FUZZ-003 | FuzzMessagePayload | 随机 payload 通过 Publish 发送,frontier 不 panic |
|
||||
|
||||
### 6.3 安全测试执行方式
|
||||
|
||||
```bash
|
||||
# 带竞态检测运行安全测试
|
||||
go test -race -v ./test/security/
|
||||
|
||||
# 运行 fuzzing(至少跑 60 秒)
|
||||
go test -fuzz=FuzzEdgeMeta -fuzztime=60s ./test/security/
|
||||
go test -fuzz=FuzzRPCPayload -fuzztime=60s ./test/security/
|
||||
go test -fuzz=FuzzMessagePayload -fuzztime=60s ./test/security/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、测试覆盖矩阵
|
||||
|
||||
| 功能模块 | 单元测试 | 基准测试 | E2E测试 | 安全测试 |
|
||||
|---------|:-------:|:-------:|:------:|:-------:|
|
||||
| Edgebound(连接接入)| ✅ 已有 | ✅ 已有 | 🔲 待建 | 🔲 待建 |
|
||||
| Servicebound(连接接入)| ✅ 已有 | ✅ 已有 | 🔲 待建 | 🔲 待建 |
|
||||
| Exchange RPC 转发 | 🔲 待建 | ✅ 已有 | 🔲 待建 | 🔲 待建 |
|
||||
| Exchange 消息转发 | 🔲 待建 | ✅ 已有 | 🔲 待建 | 🔲 待建 |
|
||||
| Exchange Stream 透传 | 🔲 待建 | ✅ 已有 | 🔲 待建 | 🔲 待建 |
|
||||
| Exchange 上下线通知 | 🔲 待建 | — | 🔲 待建 | — |
|
||||
| Repo / DAO(buntdb)| ✅ 已有 | — | — | — |
|
||||
| Repo / DAO(sqlite)| ✅ 已有 | ✅ 已有 | — | — |
|
||||
| Config 加载 | ✅ 已有 | — | — | — |
|
||||
| 竞态安全 | — | — | — | 🔲 待建 |
|
||||
| 边界/模糊 | — | — | — | 🔲 待建 |
|
||||
|
||||
---
|
||||
|
||||
## 八、执行命令速查
|
||||
|
||||
```bash
|
||||
# ── 单元测试 ──────────────────────────────────────────────
|
||||
# 运行所有单元测试
|
||||
go test ./pkg/frontier/...
|
||||
|
||||
# 带竞态检测
|
||||
go test -race ./pkg/frontier/...
|
||||
|
||||
# 带覆盖率
|
||||
go test -coverprofile=coverage.out ./pkg/frontier/...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# ── 基准测试(go test bench 方式)───────────────
|
||||
go test -bench=. -benchmem -v ./test/bench/...
|
||||
|
||||
# 大规模连接模拟
|
||||
cd test/batch/edges && make && ./edges --count 10000 --nseconds 30
|
||||
|
||||
# ── E2E 测试(待创建)────────────────────────────────────
|
||||
go test -v -timeout 5m ./test/e2e/
|
||||
go test -race -v -timeout 5m ./test/e2e/
|
||||
|
||||
# ── 安全测试(待创建)────────────────────────────────────
|
||||
go test -race -v ./test/security/
|
||||
go test -fuzz=FuzzEdgeMeta -fuzztime=60s ./test/security/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### 文档更新记录
|
||||
|
||||
| 日期 | 版本 | 修改内容 |
|
||||
|-----|------|---------|
|
||||
| 2026-04-01 | 1.0 | 初始版本 |
|
||||
| 2026-04-01 | 1.1 | 去除 frontlas / Operator,聚焦 frontier 数据面;细化 E2E 和安全测试用例 |
|
||||
@@ -0,0 +1,338 @@
|
||||
package bench
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jumboframes/armorigo/log"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
gconfig "github.com/singchia/frontier/pkg/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/edgebound"
|
||||
"github.com/singchia/frontier/pkg/frontier/exchange"
|
||||
"github.com/singchia/frontier/pkg/frontier/mq"
|
||||
"github.com/singchia/frontier/pkg/frontier/repo"
|
||||
"github.com/singchia/frontier/pkg/frontier/servicebound"
|
||||
"github.com/singchia/geminio"
|
||||
"github.com/singchia/go-timer/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Set klog to only show fatal errors
|
||||
klog.InitFlags(nil)
|
||||
flag.Set("v", "0")
|
||||
flag.Set("logtostderr", "false")
|
||||
flag.Set("alsologtostderr", "false")
|
||||
flag.Set("stderrthreshold", "FATAL")
|
||||
|
||||
// Set armorigo log to only show fatal errors
|
||||
log.SetLevel(log.LevelFatal)
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
var benchPortCounter int32 = 15000
|
||||
|
||||
// benchFrontier holds the in-process frontier addresses.
|
||||
type benchFrontier struct {
|
||||
edgeAddr string
|
||||
svcAddr string
|
||||
}
|
||||
|
||||
// allocatePorts allocates two consecutive ports for a benchmark
|
||||
func allocatePorts() (edgeAddr, svcAddr string) {
|
||||
port := atomic.AddInt32(&benchPortCounter, 20) // Use 20-port spacing to avoid conflicts
|
||||
edgeAddr = fmt.Sprintf("127.0.0.1:%d", port-19)
|
||||
svcAddr = fmt.Sprintf("127.0.0.1:%d", port-18)
|
||||
return
|
||||
}
|
||||
|
||||
// startFrontier spins up an in-process frontier and
|
||||
// registers b.Cleanup to shut it down.
|
||||
func startFrontier(b *testing.B) *benchFrontier {
|
||||
b.Helper()
|
||||
|
||||
edgeAddr, svcAddr := allocatePorts()
|
||||
|
||||
conf := &config.Configuration{
|
||||
Edgebound: config.Edgebound{
|
||||
Listen: gconfig.Listen{Network: "tcp", Addr: edgeAddr},
|
||||
EdgeIDAllocWhenNoIDServiceOn: true,
|
||||
},
|
||||
Servicebound: config.Servicebound{
|
||||
Listen: gconfig.Listen{Network: "tcp", Addr: svcAddr},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := repo.NewRepo(conf)
|
||||
require.NoError(b, err)
|
||||
mqm, err := mq.NewMQM(conf)
|
||||
require.NoError(b, err)
|
||||
tmr := timer.NewTimer()
|
||||
ex := exchange.NewExchange(conf, mqm)
|
||||
|
||||
sb, err := servicebound.NewServicebound(conf, r, nil, ex, mqm, tmr)
|
||||
require.NoError(b, err)
|
||||
eb, err := edgebound.NewEdgebound(conf, r, nil, ex, tmr)
|
||||
require.NoError(b, err)
|
||||
|
||||
go sb.Serve()
|
||||
go eb.Serve()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
b.Cleanup(func() {
|
||||
eb.Close()
|
||||
sb.Close()
|
||||
r.Close()
|
||||
mqm.Close()
|
||||
tmr.Close()
|
||||
})
|
||||
|
||||
return &benchFrontier{edgeAddr: edgeAddr, svcAddr: svcAddr}
|
||||
}
|
||||
|
||||
// dialEdge opens a new Edge connection and registers cleanup.
|
||||
func (f *benchFrontier) dialEdge(b *testing.B, opts ...edge.EdgeOption) edge.Edge {
|
||||
b.Helper()
|
||||
dialer := func() (net.Conn, error) { return net.Dial("tcp", f.edgeAddr) }
|
||||
e, err := edge.NewEdge(dialer, opts...)
|
||||
require.NoError(b, err)
|
||||
b.Cleanup(func() { e.Close() })
|
||||
return e
|
||||
}
|
||||
|
||||
// dialService opens a new Service connection and registers cleanup.
|
||||
func (f *benchFrontier) dialService(b *testing.B, name string, opts ...service.ServiceOption) service.Service {
|
||||
b.Helper()
|
||||
dialer := func() (net.Conn, error) { return net.Dial("tcp", f.svcAddr) }
|
||||
opts = append([]service.ServiceOption{service.OptionServiceName(name)}, opts...)
|
||||
svc, err := service.NewService(dialer, opts...)
|
||||
require.NoError(b, err)
|
||||
b.Cleanup(func() { svc.Close() })
|
||||
return svc
|
||||
}
|
||||
|
||||
// BENCH-CALL-001: Edge → Frontier → Service RPC 吞吐 (QPS)
|
||||
func BenchmarkEdgeCallService(b *testing.B) {
|
||||
f := startFrontier(b)
|
||||
|
||||
svc := f.dialService(b, "bench-rpc-svc")
|
||||
require.NoError(b, svc.Register(context.TODO(), "echo",
|
||||
func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
},
|
||||
))
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// verify echo works before benchmark
|
||||
e0 := f.dialEdge(b)
|
||||
req0 := e0.NewRequest([]byte("test"))
|
||||
_, err := e0.Call(context.TODO(), "echo", req0)
|
||||
require.NoError(b, err, "pre-bench verification failed")
|
||||
|
||||
payload := []byte("ping")
|
||||
|
||||
// pre-create edges to avoid timing issues with RPC routing
|
||||
const numWorkers = 10
|
||||
edges := make([]edge.Edge, numWorkers)
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
edges[i] = f.dialEdge(b)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
e := edges[i%numWorkers]
|
||||
i++
|
||||
req := e.NewRequest(payload)
|
||||
if _, err := e.Call(context.TODO(), "echo", req); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.StopTimer()
|
||||
|
||||
qps := float64(b.N) / b.Elapsed().Seconds()
|
||||
b.ReportMetric(qps, "qps")
|
||||
}
|
||||
|
||||
// BENCH-CALL-002: Service → Frontier → Edge RPC 吞吐 (QPS)
|
||||
func BenchmarkServiceCallEdge(b *testing.B) {
|
||||
f := startFrontier(b)
|
||||
|
||||
e := f.dialEdge(b)
|
||||
require.NoError(b, e.Register(context.TODO(), "echo",
|
||||
func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
},
|
||||
))
|
||||
edgeID := e.EdgeID()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
s0 := f.dialService(b, "bench-verify")
|
||||
req0 := s0.NewRequest([]byte("test"))
|
||||
_, err := s0.Call(context.TODO(), edgeID, "echo", req0)
|
||||
require.NoError(b, err, "pre-bench verification failed")
|
||||
|
||||
payload := []byte("pong")
|
||||
|
||||
const numWorkers = 10
|
||||
svcs := make([]service.Service, numWorkers)
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
svcs[i] = f.dialService(b, fmt.Sprintf("bench-caller-%d", i))
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
svc := svcs[i%numWorkers]
|
||||
i++
|
||||
req := svc.NewRequest(payload)
|
||||
if _, err := svc.Call(context.TODO(), edgeID, "echo", req); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
b.StopTimer()
|
||||
|
||||
qps := float64(b.N) / b.Elapsed().Seconds()
|
||||
b.ReportMetric(qps, "qps")
|
||||
}
|
||||
|
||||
// BENCH-MSG-001: Edge → Frontier → Service 消息吞吐 (QPS)
|
||||
func BenchmarkEdgePublishMessage(b *testing.B) {
|
||||
f := startFrontier(b)
|
||||
|
||||
svc := f.dialService(b, "bench-msg-svc", service.OptionServiceReceiveTopics([]string{"bench-topic"}))
|
||||
go func() {
|
||||
for {
|
||||
msg, err := svc.Receive(context.TODO())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
payload := []byte("message")
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
e := f.dialEdge(b)
|
||||
for pb.Next() {
|
||||
msg := e.NewMessage(payload)
|
||||
e.Publish(context.TODO(), "bench-topic", msg)
|
||||
}
|
||||
})
|
||||
b.StopTimer()
|
||||
|
||||
qps := float64(b.N) / b.Elapsed().Seconds()
|
||||
b.ReportMetric(qps, "qps")
|
||||
}
|
||||
|
||||
// BENCH-STRM-001: Edge → Frontier → Service 流建立吞吐 (QPS)
|
||||
// Note: This benchmark may occasionally panic in geminio when run repeatedly
|
||||
// due to a race condition in stream cleanup. Run with -count=1 if issues occur.
|
||||
func BenchmarkEdgeOpenStream(b *testing.B) {
|
||||
f := startFrontier(b)
|
||||
|
||||
svc := f.dialService(b, "bench-stream-svc")
|
||||
go func() {
|
||||
for {
|
||||
st, err := svc.AcceptStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go st.Close()
|
||||
}
|
||||
}()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
e := f.dialEdge(b)
|
||||
for pb.Next() {
|
||||
st, err := e.OpenStream("bench-stream-svc")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
st.Close()
|
||||
}
|
||||
})
|
||||
b.StopTimer()
|
||||
|
||||
qps := float64(b.N) / b.Elapsed().Seconds()
|
||||
b.ReportMetric(qps, "qps")
|
||||
}
|
||||
|
||||
// BENCH-CONN-001: Edge 连接建立与断开吞吐 (QPS / TPS)
|
||||
func BenchmarkEdgeConnectDisconnect(b *testing.B) {
|
||||
// Skip this in parallel runs because it exhausts ports
|
||||
if !testing.Short() {
|
||||
b.Skip("Skipping connect/disconnect benchmark in non-short mode to avoid port exhaustion")
|
||||
}
|
||||
|
||||
edgeAddr, svcAddr := allocatePorts()
|
||||
|
||||
conf := &config.Configuration{
|
||||
Edgebound: config.Edgebound{
|
||||
Listen: gconfig.Listen{Network: "tcp", Addr: edgeAddr},
|
||||
EdgeIDAllocWhenNoIDServiceOn: true,
|
||||
},
|
||||
Servicebound: config.Servicebound{
|
||||
Listen: gconfig.Listen{Network: "tcp", Addr: svcAddr},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := repo.NewRepo(conf)
|
||||
require.NoError(b, err)
|
||||
mqm, err := mq.NewMQM(conf)
|
||||
require.NoError(b, err)
|
||||
tmr := timer.NewTimer()
|
||||
ex := exchange.NewExchange(conf, mqm)
|
||||
|
||||
sb, err := servicebound.NewServicebound(conf, r, nil, ex, mqm, tmr)
|
||||
require.NoError(b, err)
|
||||
eb, err := edgebound.NewEdgebound(conf, r, nil, ex, tmr)
|
||||
require.NoError(b, err)
|
||||
|
||||
go sb.Serve()
|
||||
go eb.Serve()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
b.Cleanup(func() {
|
||||
eb.Close()
|
||||
sb.Close()
|
||||
r.Close()
|
||||
mqm.Close()
|
||||
tmr.Close()
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
dialer := func() (net.Conn, error) { return net.Dial("tcp", edgeAddr) }
|
||||
for pb.Next() {
|
||||
e, err := edge.NewEdge(dialer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
e.Close()
|
||||
}
|
||||
})
|
||||
b.StopTimer()
|
||||
|
||||
qps := float64(b.N) / b.Elapsed().Seconds()
|
||||
b.ReportMetric(qps, "qps")
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// E2E-CONN-001
|
||||
func TestEdgeConnect(t *testing.T) {
|
||||
e := newEdge(t)
|
||||
assert.NotZero(t, e.EdgeID())
|
||||
}
|
||||
|
||||
// E2E-CONN-002
|
||||
func TestEdgeConnectAndClose(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
e, err := edge.NewEdge(edgeDialer())
|
||||
require.NoError(t, err)
|
||||
go func() {
|
||||
defer close(done)
|
||||
e.Close()
|
||||
}()
|
||||
waitTimeout(t, done, 3*time.Second)
|
||||
}
|
||||
|
||||
// E2E-CONN-003: Edge carries meta, Service receives it via EdgeOnline callback
|
||||
func TestEdgeConnectWithMeta(t *testing.T) {
|
||||
meta := []byte("hello-frontier")
|
||||
gotMeta := make(chan []byte, 1)
|
||||
|
||||
svc := newService(t,
|
||||
service.OptionServiceName("meta-checker"),
|
||||
service.OptionServiceReceiveTopics([]string{}),
|
||||
)
|
||||
err := svc.RegisterEdgeOnline(context.Background(), func(edgeID uint64, m []byte, addr net.Addr) error {
|
||||
gotMeta <- m
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
_ = newEdge(t, edge.OptionEdgeMeta(meta))
|
||||
|
||||
select {
|
||||
case m := <-gotMeta:
|
||||
assert.Equal(t, meta, m)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for EdgeOnline callback")
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-CONN-004: 100 edges connect concurrently, all succeed with unique IDs
|
||||
func TestMultiEdgeConnect(t *testing.T) {
|
||||
const n = 100
|
||||
ids := make(chan uint64, n)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e := newEdge(t)
|
||||
ids <- e.EdgeID()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(ids)
|
||||
|
||||
seen := make(map[uint64]struct{}, n)
|
||||
for id := range ids {
|
||||
assert.NotZero(t, id)
|
||||
_, dup := seen[id]
|
||||
assert.False(t, dup, "duplicate edgeID: %d", id)
|
||||
seen[id] = struct{}{}
|
||||
}
|
||||
assert.Len(t, seen, n)
|
||||
}
|
||||
|
||||
// E2E-CONN-005: Service connects and registers successfully
|
||||
func TestServiceConnect(t *testing.T) {
|
||||
svc := newService(t, service.OptionServiceName("my-service"))
|
||||
assert.NotNil(t, svc)
|
||||
}
|
||||
|
||||
// E2E-CONN-006: After Service disconnects, Edge RPC call returns an error
|
||||
func TestServiceConnectAndClose(t *testing.T) {
|
||||
svc, err := service.NewService(serviceDialer(),
|
||||
service.OptionServiceName("gone-service"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
svc.Close()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
req := e.NewRequest([]byte("ping"))
|
||||
_, err = e.Call(context.Background(), "anything", req)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jumboframes/armorigo/log"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
gconfig "github.com/singchia/frontier/pkg/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/edgebound"
|
||||
"github.com/singchia/frontier/pkg/frontier/exchange"
|
||||
"github.com/singchia/frontier/pkg/frontier/mq"
|
||||
"github.com/singchia/frontier/pkg/frontier/repo"
|
||||
"github.com/singchia/frontier/pkg/frontier/servicebound"
|
||||
"github.com/singchia/go-timer/v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
klog.InitFlags(nil)
|
||||
flag.Set("v", "0")
|
||||
flag.Set("logtostderr", "false")
|
||||
flag.Set("stderrthreshold", "FATAL")
|
||||
|
||||
log.SetLevel(log.LevelFatal)
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
const (
|
||||
edgeboundAddr = "127.0.0.1:13100"
|
||||
serviceboundAddr = "127.0.0.1:13101"
|
||||
network = "tcp"
|
||||
)
|
||||
|
||||
// TestMain starts one shared frontier instance for the whole test binary.
|
||||
func TestMain(m *testing.M) {
|
||||
conf := &config.Configuration{
|
||||
Edgebound: config.Edgebound{
|
||||
Listen: gconfig.Listen{Network: network, Addr: edgeboundAddr},
|
||||
EdgeIDAllocWhenNoIDServiceOn: true,
|
||||
},
|
||||
Servicebound: config.Servicebound{
|
||||
Listen: gconfig.Listen{Network: network, Addr: serviceboundAddr},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := repo.NewRepo(conf)
|
||||
if err != nil {
|
||||
panic("new repo: " + err.Error())
|
||||
}
|
||||
mqm, err := mq.NewMQM(conf)
|
||||
if err != nil {
|
||||
panic("new mqm: " + err.Error())
|
||||
}
|
||||
tmr := timer.NewTimer()
|
||||
ex := exchange.NewExchange(conf, mqm)
|
||||
|
||||
sb, err := servicebound.NewServicebound(conf, r, nil, ex, mqm, tmr)
|
||||
if err != nil {
|
||||
panic("new servicebound: " + err.Error())
|
||||
}
|
||||
eb, err := edgebound.NewEdgebound(conf, r, nil, ex, tmr)
|
||||
if err != nil {
|
||||
panic("new edgebound: " + err.Error())
|
||||
}
|
||||
|
||||
go sb.Serve()
|
||||
go eb.Serve()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
eb.Close()
|
||||
sb.Close()
|
||||
r.Close()
|
||||
mqm.Close()
|
||||
tmr.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// edgeDialer returns a Dialer that connects to the edgebound.
|
||||
func edgeDialer() edge.Dialer {
|
||||
return func() (net.Conn, error) {
|
||||
return net.Dial(network, edgeboundAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// serviceDialer returns a Dialer that connects to the servicebound.
|
||||
func serviceDialer() service.Dialer {
|
||||
return func() (net.Conn, error) {
|
||||
return net.Dial(network, serviceboundAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// newEdge creates an Edge connected to the shared test frontier.
|
||||
func newEdge(t *testing.T, opts ...edge.EdgeOption) edge.Edge {
|
||||
t.Helper()
|
||||
e, err := edge.NewEdge(edgeDialer(), opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("new edge: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { e.Close() })
|
||||
return e
|
||||
}
|
||||
|
||||
// newService creates a Service connected to the shared test frontier.
|
||||
func newService(t *testing.T, opts ...service.ServiceOption) service.Service {
|
||||
t.Helper()
|
||||
svc, err := service.NewService(serviceDialer(), opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("new service: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { svc.Close() })
|
||||
return svc
|
||||
}
|
||||
|
||||
// waitTimeout waits for done to be closed, failing the test if deadline is exceeded.
|
||||
func waitTimeout(t *testing.T, done <-chan struct{}, d time.Duration) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(d):
|
||||
t.Fatal("timed out waiting")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// E2E-MSG-001: Edge publishes to a topic, Service registered on that topic receives it
|
||||
func TestEdgePublishToService(t *testing.T) {
|
||||
const topic = "news"
|
||||
received := make(chan []byte, 1)
|
||||
|
||||
svc := newService(t,
|
||||
service.OptionServiceName("subscriber"),
|
||||
service.OptionServiceReceiveTopics([]string{topic}),
|
||||
)
|
||||
go func() {
|
||||
msg, err := svc.Receive(context.TODO())
|
||||
if err == nil {
|
||||
received <- msg.Data()
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
payload := []byte("breaking-news")
|
||||
msg := e.NewMessage(payload)
|
||||
err := e.Publish(context.TODO(), topic, msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case data := <-received:
|
||||
assert.Equal(t, payload, data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for message")
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-MSG-002: Service publishes a message to a specific edgeID, Edge receives it
|
||||
func TestServicePublishToEdge(t *testing.T) {
|
||||
received := make(chan []byte, 1)
|
||||
|
||||
e := newEdge(t)
|
||||
go func() {
|
||||
msg, err := e.Receive(context.TODO())
|
||||
if err == nil {
|
||||
received <- msg.Data()
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
svc := newService(t, service.OptionServiceName("publisher"))
|
||||
payload := []byte("hello-edge")
|
||||
msg := svc.NewMessage(payload)
|
||||
err := svc.Publish(context.TODO(), e.EdgeID(), msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case data := <-received:
|
||||
assert.Equal(t, payload, data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for message")
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-MSG-003: Multiple services on different topics; messages route correctly
|
||||
func TestMessageTopicRoute(t *testing.T) {
|
||||
topics := []string{"topic-a", "topic-b"}
|
||||
receivedA := make(chan []byte, 1)
|
||||
receivedB := make(chan []byte, 1)
|
||||
|
||||
svcA := newService(t,
|
||||
service.OptionServiceName("svc-a"),
|
||||
service.OptionServiceReceiveTopics([]string{topics[0]}),
|
||||
)
|
||||
svcB := newService(t,
|
||||
service.OptionServiceName("svc-b"),
|
||||
service.OptionServiceReceiveTopics([]string{topics[1]}),
|
||||
)
|
||||
go func() {
|
||||
if msg, err := svcA.Receive(context.TODO()); err == nil {
|
||||
receivedA <- msg.Data()
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
if msg, err := svcB.Receive(context.TODO()); err == nil {
|
||||
receivedB <- msg.Data()
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
msgA := e.NewMessage([]byte("for-a"))
|
||||
msgB := e.NewMessage([]byte("for-b"))
|
||||
require.NoError(t, e.Publish(context.TODO(), topics[0], msgA))
|
||||
require.NoError(t, e.Publish(context.TODO(), topics[1], msgB))
|
||||
|
||||
for _, ch := range []struct {
|
||||
ch chan []byte
|
||||
want string
|
||||
timeout time.Duration
|
||||
}{
|
||||
{receivedA, "for-a", 3 * time.Second},
|
||||
{receivedB, "for-b", 3 * time.Second},
|
||||
} {
|
||||
select {
|
||||
case data := <-ch.ch:
|
||||
assert.Equal(t, []byte(ch.want), data)
|
||||
case <-time.After(ch.timeout):
|
||||
t.Fatalf("timed out waiting for message on topic")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-MSG-004: Edge publishes to a topic with no subscriber => error
|
||||
func TestMessageTopicNotFound(t *testing.T) {
|
||||
e := newEdge(t)
|
||||
msg := e.NewMessage([]byte("orphan"))
|
||||
err := e.Publish(context.TODO(), "no-such-topic", msg)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// E2E-MSG-005: 10 edges publish concurrently, service receives all messages
|
||||
func TestMessageConcurrent(t *testing.T) {
|
||||
const (
|
||||
topic = "concurrent-topic"
|
||||
workers = 10
|
||||
)
|
||||
var mu sync.Mutex
|
||||
received := 0
|
||||
allDone := make(chan struct{})
|
||||
|
||||
svc := newService(t,
|
||||
service.OptionServiceName("concurrent-sub"),
|
||||
service.OptionServiceReceiveTopics([]string{topic}),
|
||||
)
|
||||
go func() {
|
||||
for {
|
||||
msg, err := svc.Receive(context.TODO())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg.Done()
|
||||
mu.Lock()
|
||||
received++
|
||||
if received == workers {
|
||||
close(allDone)
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e := newEdge(t)
|
||||
msg := e.NewMessage([]byte("concurrent"))
|
||||
if err := e.Publish(context.TODO(), topic, msg); err != nil {
|
||||
t.Errorf("publish error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
select {
|
||||
case <-allDone:
|
||||
mu.Lock()
|
||||
assert.Equal(t, workers, received)
|
||||
mu.Unlock()
|
||||
case <-time.After(5 * time.Second):
|
||||
mu.Lock()
|
||||
t.Fatalf("timed out: only received %d/%d messages", received, workers)
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
"github.com/singchia/geminio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// E2E-RES-001: After edge closes, frontier side resources are cleaned up (no panic/hang)
|
||||
func TestResourceCleanupOnEdgeClose(t *testing.T) {
|
||||
e, err := edge.NewEdge(edgeDialer())
|
||||
require.NoError(t, err)
|
||||
|
||||
// open a few streams to ensure there is something to clean up
|
||||
svc := newService(t, service.OptionServiceName("cleanup-sink"))
|
||||
go func() {
|
||||
for {
|
||||
st, err := svc.AcceptStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
st.Close()
|
||||
}
|
||||
}()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
st, err := e.OpenStream("cleanup-sink")
|
||||
if err == nil {
|
||||
st.Close()
|
||||
}
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
// close the edge — frontier must not panic or deadlock
|
||||
e.Close()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// E2E-RES-002: After service closes, subsequent edge RPC calls return an error
|
||||
func TestResourceCleanupOnServiceClose(t *testing.T) {
|
||||
// start a service, register an RPC, then close it
|
||||
svc, err := service.NewService(serviceDialer(), service.OptionServiceName("gone-svc"))
|
||||
require.NoError(t, err)
|
||||
err = svc.Register(context.TODO(), "probe", func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
svc.Close()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// now an edge should get an error calling the gone service
|
||||
e := newEdge(t)
|
||||
req := e.NewRequest([]byte("hello"))
|
||||
_, err = e.Call(context.TODO(), "probe", req)
|
||||
assert.Error(t, err, "expected error after service closed")
|
||||
}
|
||||
|
||||
// E2E-RES-003: goroutine count does not grow unboundedly after repeated edge connect/close
|
||||
func TestGoroutineNoLeak(t *testing.T) {
|
||||
// let the frontier settle, then record baseline
|
||||
runtime.GC()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
baseline := runtime.NumGoroutine()
|
||||
|
||||
const iterations = 30
|
||||
for i := 0; i < iterations; i++ {
|
||||
e, err := edge.NewEdge(edgeDialer())
|
||||
require.NoError(t, err)
|
||||
e.Close()
|
||||
}
|
||||
|
||||
// allow goroutines to wind down
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
runtime.GC()
|
||||
|
||||
after := runtime.NumGoroutine()
|
||||
// Leak threshold: must not grow by more than iterations goroutines above baseline
|
||||
assert.Less(t, after, baseline+iterations,
|
||||
"possible goroutine leak: baseline=%d after=%d", baseline, after)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
"github.com/singchia/geminio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// E2E-RPC-001: Edge calls a method registered by Service via frontier
|
||||
func TestEdgeCallService(t *testing.T) {
|
||||
svc := newService(t, service.OptionServiceName("echo-service"))
|
||||
err := svc.Register(context.TODO(), "echo", func(ctx context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// give servicebound time to index the RPC
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
payload := []byte("hello")
|
||||
req := e.NewRequest(payload)
|
||||
resp, err := e.Call(context.TODO(), "echo", req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, payload, resp.Data())
|
||||
}
|
||||
|
||||
// E2E-RPC-002: Service calls a method registered by Edge via frontier (specifying edgeID)
|
||||
func TestServiceCallEdge(t *testing.T) {
|
||||
e := newEdge(t)
|
||||
err := e.Register(context.TODO(), "ping", func(ctx context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData([]byte("pong"))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
svc := newService(t, service.OptionServiceName("caller"))
|
||||
req := svc.NewRequest([]byte(""))
|
||||
resp, err := svc.Call(context.TODO(), e.EdgeID(), "ping", req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("pong"), resp.Data())
|
||||
}
|
||||
|
||||
// E2E-RPC-003: RPC not found on edge returns an error (no matching RPC registered)
|
||||
func TestRPCTargetRPCNotFound(t *testing.T) {
|
||||
svc := newService(t, service.OptionServiceName("noop-service"))
|
||||
// register a method so the service itself is reachable
|
||||
_ = svc.Register(context.TODO(), "placeholder", func(_ context.Context, req geminio.Request, resp geminio.Response) {})
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
// call a method the edge never registered
|
||||
req := e.NewRequest([]byte("x"))
|
||||
_, err := e.Call(context.TODO(), "nonexistent-method", req)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// E2E-RPC-004: Service calls edge that is already offline => ErrEdgeNotOnline
|
||||
func TestRPCTargetEdgeOffline(t *testing.T) {
|
||||
// create an edge then close it immediately (without t.Cleanup so we control timing)
|
||||
offlineEdge, err := edge.NewEdge(edgeDialer())
|
||||
require.NoError(t, err)
|
||||
offlineID := offlineEdge.EdgeID()
|
||||
offlineEdge.Close()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
svc := newService(t, service.OptionServiceName("caller2"))
|
||||
req := svc.NewRequest([]byte("data"))
|
||||
_, err = svc.Call(context.TODO(), offlineID, "any-method", req)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// E2E-RPC-005: 10 edges concurrently call the same Service RPC, all succeed
|
||||
func TestRPCConcurrent(t *testing.T) {
|
||||
svc := newService(t, service.OptionServiceName("concurrent-echo"))
|
||||
err := svc.Register(context.TODO(), "echo", func(ctx context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create all edges first and wait for them to be indexed before calling
|
||||
const n = 10
|
||||
edges := make([]edge.Edge, n)
|
||||
for i := 0; i < n; i++ {
|
||||
edges[i] = newEdge(t)
|
||||
}
|
||||
// give frontier time to propagate the RPC registration to all edges
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
errs := make(chan error, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
e := edges[i]
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
payload := []byte("concurrent")
|
||||
req := e.NewRequest(payload)
|
||||
resp, err := e.Call(context.TODO(), "echo", req)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
if string(resp.Data()) != string(payload) {
|
||||
errs <- assert.AnError
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
for err := range errs {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
"github.com/singchia/geminio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// E2E-STRM-001: Edge opens a stream to Service, Service accepts it
|
||||
func TestEdgeOpenStreamToService(t *testing.T) {
|
||||
accepted := make(chan geminio.Stream, 1)
|
||||
svc := newService(t, service.OptionServiceName("stream-service"))
|
||||
go func() {
|
||||
st, err := svc.AcceptStream()
|
||||
if err == nil {
|
||||
accepted <- st
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
st, err := e.OpenStream("stream-service")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { st.Close() })
|
||||
|
||||
select {
|
||||
case serverSt := <-accepted:
|
||||
assert.NotNil(t, serverSt)
|
||||
serverSt.Close()
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for AcceptStream")
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-STRM-002: Service opens a stream to Edge, Edge accepts it
|
||||
func TestServiceOpenStreamToEdge(t *testing.T) {
|
||||
accepted := make(chan geminio.Stream, 1)
|
||||
e := newEdge(t)
|
||||
go func() {
|
||||
st, err := e.AcceptStream()
|
||||
if err == nil {
|
||||
accepted <- st
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
svc := newService(t, service.OptionServiceName("stream-opener"))
|
||||
st, err := svc.OpenStream(context.TODO(), e.EdgeID())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { st.Close() })
|
||||
|
||||
select {
|
||||
case edgeSt := <-accepted:
|
||||
assert.NotNil(t, edgeSt)
|
||||
edgeSt.Close()
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for AcceptStream on edge")
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-STRM-003: Raw IO forwarded bidirectionally through the stream
|
||||
func TestStreamRawDataForward(t *testing.T) {
|
||||
serverRead := make(chan []byte, 1)
|
||||
clientRead := make(chan []byte, 1)
|
||||
|
||||
svc := newService(t, service.OptionServiceName("raw-echo"))
|
||||
go func() {
|
||||
st, err := svc.AcceptStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer st.Close()
|
||||
buf := make([]byte, 64)
|
||||
n, _ := st.Read(buf)
|
||||
serverRead <- buf[:n]
|
||||
st.Write([]byte("server-reply"))
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
st, err := e.OpenStream("raw-echo")
|
||||
require.NoError(t, err)
|
||||
defer st.Close()
|
||||
|
||||
_, err = st.Write([]byte("client-hello"))
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := make([]byte, 64)
|
||||
go func() {
|
||||
n, _ := st.Read(buf)
|
||||
clientRead <- buf[:n]
|
||||
}()
|
||||
|
||||
select {
|
||||
case data := <-serverRead:
|
||||
assert.Equal(t, []byte("client-hello"), data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for server read")
|
||||
}
|
||||
select {
|
||||
case data := <-clientRead:
|
||||
assert.Equal(t, []byte("server-reply"), data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for client read")
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-STRM-004: Message forwarded bidirectionally inside a stream
|
||||
func TestStreamMessageForward(t *testing.T) {
|
||||
const streamTopic = "stream-topic"
|
||||
serverReceived := make(chan []byte, 1)
|
||||
clientReceived := make(chan []byte, 1)
|
||||
|
||||
svc := newService(t, service.OptionServiceName("msg-echo"))
|
||||
go func() {
|
||||
st, err := svc.AcceptStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer st.Close()
|
||||
// receive from edge
|
||||
msg, err := st.Receive(context.TODO())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
serverReceived <- msg.Data()
|
||||
msg.Done()
|
||||
// reply back
|
||||
reply := st.NewMessage([]byte("svc-msg-reply"))
|
||||
_ = st.Publish(context.TODO(), reply)
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
st, err := e.OpenStream("msg-echo")
|
||||
require.NoError(t, err)
|
||||
defer st.Close()
|
||||
|
||||
go func() {
|
||||
msg, err := st.Receive(context.TODO())
|
||||
if err == nil {
|
||||
clientReceived <- msg.Data()
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
edgeMsg := st.NewMessage([]byte("edge-msg"))
|
||||
err = st.Publish(context.TODO(), edgeMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case data := <-serverReceived:
|
||||
assert.Equal(t, []byte("edge-msg"), data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for server message")
|
||||
}
|
||||
select {
|
||||
case data := <-clientReceived:
|
||||
assert.Equal(t, []byte("svc-msg-reply"), data)
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("timed out waiting for client message")
|
||||
}
|
||||
}
|
||||
|
||||
// E2E-STRM-005: RPC forwarded bidirectionally inside a stream
|
||||
func TestStreamRPCForward(t *testing.T) {
|
||||
svc := newService(t, service.OptionServiceName("rpc-echo"))
|
||||
go func() {
|
||||
st, err := svc.AcceptStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer st.Close()
|
||||
_ = st.Register(context.TODO(), "echo", func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
})
|
||||
// keep the stream alive while the test runs
|
||||
time.Sleep(3 * time.Second)
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
st, err := e.OpenStream("rpc-echo")
|
||||
require.NoError(t, err)
|
||||
defer st.Close()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
payload := []byte("rpc-payload")
|
||||
req := st.NewRequest(payload)
|
||||
resp, err := st.Call(context.TODO(), "echo", req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, payload, resp.Data())
|
||||
}
|
||||
|
||||
// E2E-STRM-006: Stream Close does not panic and can be called multiple times safely.
|
||||
func TestStreamClose(t *testing.T) {
|
||||
svc := newService(t, service.OptionServiceName("close-test"))
|
||||
go func() {
|
||||
for {
|
||||
st, err := svc.AcceptStream()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
st.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
e := newEdge(t)
|
||||
st, err := e.OpenStream("close-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Close must not panic, even when called multiple times
|
||||
assert.NotPanics(t, func() { st.Close() })
|
||||
assert.NotPanics(t, func() { st.Close() })
|
||||
}
|
||||
|
||||
// E2E-STRM-007: Service opens a stream to an offline edge; the stream is returned
|
||||
// but immediately closed by frontier (edge not found), so subsequent IO fails.
|
||||
func TestStreamTargetEdgeOffline(t *testing.T) {
|
||||
offlineEdge, err := edge.NewEdge(edgeDialer())
|
||||
require.NoError(t, err)
|
||||
offlineID := offlineEdge.EdgeID()
|
||||
offlineEdge.Close()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
svc := newService(t, service.OptionServiceName("opener"))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
st, err := svc.OpenStream(ctx, offlineID)
|
||||
// frontier may return an error immediately, or return a stream that is
|
||||
// already closed — either way IO must fail.
|
||||
if err != nil {
|
||||
return // expected: direct error
|
||||
}
|
||||
defer st.Close()
|
||||
// if a stream was returned, a write or receive must fail
|
||||
_, writeErr := st.Write([]byte("probe"))
|
||||
recvCtx, recvCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer recvCancel()
|
||||
_, recvErr := st.Receive(recvCtx)
|
||||
if writeErr == nil && recvErr == nil {
|
||||
t.Error("expected IO on stream to dead edge to fail, but both succeeded")
|
||||
}
|
||||
}
|
||||
Executable
+274
@@ -0,0 +1,274 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Frontier Comprehensive Test Suite Runner — see `test/run_tests.sh -h`
|
||||
|
||||
# Benchmark -benchtime defaults
|
||||
: "${BENCH_TIME_EACH:=3s}"
|
||||
: "${BENCH_TIME_ALL:=10s}"
|
||||
# go test -timeout: applies to the whole process (default: ~10m, too short for bench=. + long benchtime)
|
||||
: "${BENCH_GO_TEST_TIMEOUT:=30m}"
|
||||
|
||||
OUTPUT_FILE=""
|
||||
run_unit=false
|
||||
run_bench=false
|
||||
run_e2e=false
|
||||
run_security=false
|
||||
run_race=false
|
||||
run_cover=false
|
||||
any_category=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o|--output)
|
||||
if [[ -z "${2:-}" ]]; then
|
||||
echo "Error: $1 requires a file path" >&2
|
||||
exit 1
|
||||
fi
|
||||
OUTPUT_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
cat <<'EOF'
|
||||
Usage: test/run_tests.sh [options] [--category ...]
|
||||
|
||||
Options:
|
||||
-o, --output FILE Write full output to FILE (overwrite; also shown on terminal)
|
||||
-h, --help Show this help
|
||||
|
||||
Categories (combine multiple; omit all to run everything):
|
||||
--unit Unit tests (exchange): go test ./... -short
|
||||
--bench Benchmarks under test/bench
|
||||
--e2e End-to-end tests under test/e2e
|
||||
--security Security tests under test/security (race, fuzz)
|
||||
--race Race detector on unit tests
|
||||
--cover Coverage (coverage.out, coverage.html)
|
||||
--all Explicitly run all categories
|
||||
|
||||
Examples:
|
||||
test/run_tests.sh --unit
|
||||
test/run_tests.sh --e2e --security
|
||||
test/run_tests.sh -o run.log --bench
|
||||
|
||||
Env (benchmark section only):
|
||||
BENCH_TIME_EACH Per-benchmark -benchtime (default: 3s)
|
||||
BENCH_TIME_ALL Final bench=. -benchtime (default: 10s)
|
||||
BENCH_GO_TEST_TIMEOUT go test -timeout for benchmarks (default: 30m; 0 = no limit)
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--unit)
|
||||
run_unit=true
|
||||
any_category=true
|
||||
shift
|
||||
;;
|
||||
--bench)
|
||||
run_bench=true
|
||||
any_category=true
|
||||
shift
|
||||
;;
|
||||
--e2e)
|
||||
run_e2e=true
|
||||
any_category=true
|
||||
shift
|
||||
;;
|
||||
--security)
|
||||
run_security=true
|
||||
any_category=true
|
||||
shift
|
||||
;;
|
||||
--race)
|
||||
run_race=true
|
||||
any_category=true
|
||||
shift
|
||||
;;
|
||||
--cover|--coverage)
|
||||
run_cover=true
|
||||
any_category=true
|
||||
shift
|
||||
;;
|
||||
--all)
|
||||
run_unit=true
|
||||
run_bench=true
|
||||
run_e2e=true
|
||||
run_security=true
|
||||
run_race=true
|
||||
run_cover=true
|
||||
any_category=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1 (try -h)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! $any_category; then
|
||||
run_unit=true
|
||||
run_bench=true
|
||||
run_e2e=true
|
||||
run_security=true
|
||||
run_race=true
|
||||
run_cover=true
|
||||
fi
|
||||
|
||||
# Resolve relative log path to invocation cwd (before cd to project root)
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
if [[ "$OUTPUT_FILE" != /* ]]; then
|
||||
OUTPUT_FILE="$(pwd)/$OUTPUT_FILE"
|
||||
fi
|
||||
mkdir -p "$(dirname "$OUTPUT_FILE")"
|
||||
exec > >(tee "$OUTPUT_FILE") 2>&1
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
echo "==================================="
|
||||
echo "Frontier Comprehensive Test Suite"
|
||||
echo "==================================="
|
||||
if [[ -n "$OUTPUT_FILE" ]]; then
|
||||
echo "Full output also logged to: $OUTPUT_FILE"
|
||||
fi
|
||||
echo "Categories:"
|
||||
$run_unit && echo " - unit"
|
||||
$run_bench && echo " - bench"
|
||||
$run_e2e && echo " - e2e"
|
||||
$run_security && echo " - security (race, fuzz)"
|
||||
$run_race && echo " - race"
|
||||
$run_cover && echo " - cover"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print section headers
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}===================================${NC}"
|
||||
echo -e "${YELLOW}$1${NC}"
|
||||
echo -e "${YELLOW}===================================${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Change to project root
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies..."
|
||||
go mod download
|
||||
|
||||
if $run_unit; then
|
||||
print_header "Running Unit Tests (Exchange)"
|
||||
go test -v ./pkg/frontier/exchange/... -short -count=1 2>&1 | tail -50 || true
|
||||
fi
|
||||
|
||||
if $run_bench; then
|
||||
print_header "BENCHMARK TESTS"
|
||||
|
||||
echo "Running RPC Call Benchmarks..."
|
||||
go test -timeout="${BENCH_GO_TEST_TIMEOUT}" -bench=BenchmarkEdgeCall -benchmem -benchtime="${BENCH_TIME_EACH}" ./test/bench/... 2>&1 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Service Call Benchmarks..."
|
||||
go test -timeout="${BENCH_GO_TEST_TIMEOUT}" -bench=BenchmarkServiceCall -benchmem -benchtime="${BENCH_TIME_EACH}" ./test/bench/... 2>&1 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Message Publishing Benchmarks..."
|
||||
go test -timeout="${BENCH_GO_TEST_TIMEOUT}" -bench=BenchmarkEdgePublish -benchmem -benchtime="${BENCH_TIME_EACH}" ./test/bench/... 2>&1 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Stream Open Benchmarks..."
|
||||
go test -timeout="${BENCH_GO_TEST_TIMEOUT}" -bench=BenchmarkEdgeOpen -benchmem -benchtime="${BENCH_TIME_EACH}" ./test/bench/... 2>&1 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Edge Connect/Disconnect Benchmarks..."
|
||||
go test -timeout="${BENCH_GO_TEST_TIMEOUT}" -bench=BenchmarkEdgeConnect -benchmem -benchtime="${BENCH_TIME_EACH}" ./test/bench/... 2>&1 || true
|
||||
|
||||
print_header "Running All Benchmarks (${BENCH_TIME_ALL} each)..."
|
||||
go test -timeout="${BENCH_GO_TEST_TIMEOUT}" -bench=. -benchmem -benchtime="${BENCH_TIME_ALL}" ./test/bench/... 2>&1 || true
|
||||
fi
|
||||
|
||||
if $run_e2e; then
|
||||
print_header "E2E INTEGRATION TESTS"
|
||||
|
||||
echo "Running Connection Tests..."
|
||||
go test -v -run TestConn ./test/e2e/... -count=1 2>&1 | tail -100 || true
|
||||
|
||||
echo ""
|
||||
echo "Running RPC Tests..."
|
||||
go test -v -run TestRPC ./test/e2e/... -count=1 2>&1 | tail -100 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Message Tests..."
|
||||
go test -v -run TestMessage ./test/e2e/... -count=1 2>&1 | tail -100 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Stream Tests..."
|
||||
go test -v -run TestStream ./test/e2e/... -count=1 2>&1 | tail -100 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Resource Cleanup Tests..."
|
||||
go test -v -run TestResourceCleanup ./test/e2e/... -count=1 2>&1 | tail -50 || true
|
||||
|
||||
print_header "Running All E2E Tests..."
|
||||
go test -v ./test/e2e/... -count=1 -timeout=10m 2>&1 | tail -100 || true
|
||||
fi
|
||||
|
||||
if $run_security; then
|
||||
print_header "SECURITY TESTS (Race & Fuzz)"
|
||||
|
||||
echo "Running Race Condition Tests..."
|
||||
go test -v -race ./test/security/... -count=1 -timeout=5m 2>&1 | tail -100 || true
|
||||
|
||||
echo ""
|
||||
echo "Running Fuzz Tests..."
|
||||
if go version | grep -qE 'go1\.(1[89]|2[0-9]|[3-9][0-9])'; then
|
||||
go test -v -run TestFuzz ./test/security/... -count=1 2>&1 | tail -50 || true
|
||||
echo ""
|
||||
echo "Running native fuzz (30 seconds)..."
|
||||
go test -fuzz=Fuzz -fuzztime=30s ./test/security/... 2>&1 || true
|
||||
else
|
||||
echo "Go version doesn't support fuzzing natively (requires Go 1.18+), skipping..."
|
||||
fi
|
||||
|
||||
print_header "Running All Security Tests..."
|
||||
go test -v ./test/security/... -count=1 -timeout=10m 2>&1 | tail -100 || true
|
||||
fi
|
||||
|
||||
if $run_race; then
|
||||
print_header "RACE DETECTION TESTS"
|
||||
|
||||
echo "Running unit tests with race detector..."
|
||||
go test -race -short ./pkg/frontier/exchange/... 2>&1 | tail -100 || true
|
||||
fi
|
||||
|
||||
if $run_cover; then
|
||||
print_header "CODE COVERAGE"
|
||||
|
||||
echo "Generating coverage report..."
|
||||
go test -coverprofile=coverage.out ./pkg/frontier/... ./test/e2e/... 2>&1 || true
|
||||
go tool cover -func=coverage.out | tail -30 || true
|
||||
|
||||
if command -v go &> /dev/null; then
|
||||
go tool cover -html=coverage.out -o coverage.html 2>&1 || true
|
||||
echo "Coverage report generated: coverage.html"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_header "TEST SUMMARY"
|
||||
|
||||
echo -e "${GREEN}Test execution completed!${NC}"
|
||||
echo ""
|
||||
echo "Test categories executed:"
|
||||
$run_unit && echo " - Unit Tests (Exchange)"
|
||||
$run_bench && echo " - Benchmark Tests"
|
||||
$run_e2e && echo " - E2E Integration Tests"
|
||||
$run_security && echo " - Security Tests (Race, Fuzz)"
|
||||
$run_race && echo " - Race Detection Tests"
|
||||
$run_cover && echo " - Code Coverage"
|
||||
echo ""
|
||||
echo "Check the output above for any failures or issues."
|
||||
echo ""
|
||||
@@ -0,0 +1,88 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
"github.com/singchia/geminio"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// SEC-FUZZ-001: Random bytes as Edge meta must not crash frontier
|
||||
func FuzzEdgeMeta(f *testing.F) {
|
||||
// seed corpus
|
||||
f.Add([]byte("normal-meta"))
|
||||
f.Add([]byte{0x00})
|
||||
f.Add([]byte{0xff, 0xfe, 0xfd})
|
||||
f.Add([]byte("line1\nline2"))
|
||||
|
||||
f.Fuzz(func(t *testing.T, meta []byte) {
|
||||
e, err := edge.NewEdge(testEdgeDial, edge.OptionEdgeMeta(meta))
|
||||
if err != nil {
|
||||
return // connection refused or rejected is acceptable
|
||||
}
|
||||
e.Close()
|
||||
})
|
||||
}
|
||||
|
||||
// SEC-FUZZ-002: Random bytes as RPC payload must not crash frontier
|
||||
func FuzzRPCPayload(f *testing.F) {
|
||||
f.Add([]byte("hello"))
|
||||
f.Add([]byte{})
|
||||
f.Add([]byte{0x00, 0xff})
|
||||
|
||||
// set up a long-lived service that echoes RPCs
|
||||
svc, err := service.NewService(testSvcDial, service.OptionServiceName("fuzz-rpc-svc"))
|
||||
require.NoError(f, err)
|
||||
f.Cleanup(func() { svc.Close() })
|
||||
require.NoError(f, svc.Register(context.TODO(), "fuzz", func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
}))
|
||||
|
||||
f.Fuzz(func(t *testing.T, payload []byte) {
|
||||
e, err := edge.NewEdge(testEdgeDial)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer e.Close()
|
||||
req := e.NewRequest(payload)
|
||||
_, _ = e.Call(context.TODO(), "fuzz", req)
|
||||
})
|
||||
}
|
||||
|
||||
// SEC-FUZZ-003: Random bytes as Publish payload must not crash frontier
|
||||
func FuzzMessagePayload(f *testing.F) {
|
||||
f.Add([]byte("msg"))
|
||||
f.Add([]byte{})
|
||||
f.Add([]byte{0x00, 0x01, 0x02})
|
||||
|
||||
const topic = "fuzz-topic"
|
||||
svc, err := service.NewService(testSvcDial,
|
||||
service.OptionServiceName("fuzz-msg-svc"),
|
||||
service.OptionServiceReceiveTopics([]string{topic}),
|
||||
)
|
||||
require.NoError(f, err)
|
||||
f.Cleanup(func() { svc.Close() })
|
||||
// drain received messages silently
|
||||
go func() {
|
||||
for {
|
||||
msg, err := svc.Receive(context.TODO())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
f.Fuzz(func(t *testing.T, payload []byte) {
|
||||
e, err := edge.NewEdge(testEdgeDial)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer e.Close()
|
||||
msg := e.NewMessage(payload)
|
||||
_ = e.Publish(context.TODO(), topic, msg)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jumboframes/armorigo/log"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
gconfig "github.com/singchia/frontier/pkg/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/config"
|
||||
"github.com/singchia/frontier/pkg/frontier/edgebound"
|
||||
"github.com/singchia/frontier/pkg/frontier/exchange"
|
||||
"github.com/singchia/frontier/pkg/frontier/mq"
|
||||
"github.com/singchia/frontier/pkg/frontier/repo"
|
||||
"github.com/singchia/frontier/pkg/frontier/servicebound"
|
||||
"github.com/singchia/go-timer/v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
klog.InitFlags(nil)
|
||||
flag.Set("v", "0")
|
||||
flag.Set("logtostderr", "false")
|
||||
flag.Set("stderrthreshold", "FATAL")
|
||||
|
||||
log.SetLevel(log.LevelFatal)
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
const (
|
||||
edgeboundAddr = "127.0.0.1:13200"
|
||||
serviceboundAddr = "127.0.0.1:13201"
|
||||
testNetwork = "tcp"
|
||||
)
|
||||
|
||||
var (
|
||||
testEdgeDial edge.Dialer
|
||||
testSvcDial service.Dialer
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
conf := &config.Configuration{
|
||||
Edgebound: config.Edgebound{
|
||||
Listen: gconfig.Listen{Network: testNetwork, Addr: edgeboundAddr},
|
||||
EdgeIDAllocWhenNoIDServiceOn: true,
|
||||
},
|
||||
Servicebound: config.Servicebound{
|
||||
Listen: gconfig.Listen{Network: testNetwork, Addr: serviceboundAddr},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := repo.NewRepo(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mqm, err := mq.NewMQM(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tmr := timer.NewTimer()
|
||||
ex := exchange.NewExchange(conf, mqm)
|
||||
|
||||
sb, err := servicebound.NewServicebound(conf, r, nil, ex, mqm, tmr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
eb, err := edgebound.NewEdgebound(conf, r, nil, ex, tmr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go sb.Serve()
|
||||
go eb.Serve()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
|
||||
testEdgeDial = func() (net.Conn, error) {
|
||||
return net.Dial(testNetwork, edgeboundAddr)
|
||||
}
|
||||
testSvcDial = func() (net.Conn, error) {
|
||||
return net.Dial(testNetwork, serviceboundAddr)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
eb.Close()
|
||||
sb.Close()
|
||||
r.Close()
|
||||
mqm.Close()
|
||||
tmr.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/singchia/frontier/api/dataplane/v1/edge"
|
||||
"github.com/singchia/frontier/api/dataplane/v1/service"
|
||||
"github.com/singchia/geminio"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// SEC-RACE-001: Concurrent Connect and Close on many edges — run with -race
|
||||
func TestRaceEdgeConnectClose(t *testing.T) {
|
||||
const n = 50
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e, err := edge.NewEdge(testEdgeDial)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
e.Close()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// SEC-RACE-002: Same edge closed concurrently from multiple goroutines — must not panic
|
||||
func TestRaceMultipleEdgeClose(t *testing.T) {
|
||||
e, err := edge.NewEdge(testEdgeDial)
|
||||
require.NoError(t, err)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const closers = 10
|
||||
wg.Add(closers)
|
||||
for i := 0; i < closers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e.Close()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// SEC-RACE-003: Service concurrently registers and the edge concurrently calls — no data race
|
||||
func TestRaceServiceRegisterAndCall(t *testing.T) {
|
||||
svc, err := service.NewService(testSvcDial, service.OptionServiceName("race-svc"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
|
||||
e, err := edge.NewEdge(testEdgeDial)
|
||||
require.NoError(t, err)
|
||||
defer e.Close()
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const workers = 10
|
||||
|
||||
// goroutines registering RPCs
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
method := "method"
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = svc.Register(context.TODO(), method, func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
resp.SetData(req.Data())
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// goroutines calling RPC from edge simultaneously
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
req := e.NewRequest([]byte("race"))
|
||||
_, _ = e.Call(context.TODO(), "method", req)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// SEC-RACE-004: Edge closes while its RPC is being forwarded — must not panic
|
||||
func TestRaceForwardAndClose(t *testing.T) {
|
||||
svc, err := service.NewService(testSvcDial, service.OptionServiceName("slow-svc"))
|
||||
require.NoError(t, err)
|
||||
defer svc.Close()
|
||||
|
||||
// slow handler to ensure forwarding is in-flight when edge closes
|
||||
err = svc.Register(context.TODO(), "slow", func(_ context.Context, req geminio.Request, resp geminio.Response) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
resp.SetData(req.Data())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
e, err := edge.NewEdge(testEdgeDial)
|
||||
require.NoError(t, err)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
req := e.NewRequest([]byte("x"))
|
||||
_, _ = e.Call(context.TODO(), "slow", req)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
e.Close()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user