From e440c057b543d3f01bf165e1a10c2e47bbaf8adb Mon Sep 17 00:00:00 2001 From: windyboy Date: Sun, 16 Nov 2025 10:18:40 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Enhance=20Ginkgo=20unit=20test=20ex?= =?UTF-8?q?ecution=20by=20specifying=20directories=20for=20tests=20in=20Ma?= =?UTF-8?q?kefile=20and=20Taskfile.=20Update=20README=20to=20reflect=20cha?= =?UTF-8?q?nges=20in=20test=20command=20usage=20and=20clarify=20the=20stru?= =?UTF-8?q?cture=20of=20unit=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 2 +- README.md | 4 +-- Taskfile.yml | 2 +- cmd/seed-telegrams/main.go | 64 +++++++++++++++++++++++++++++++------- 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 9e34367..f963d94 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ run-local: ## Run receiver directly via go run test: ## Run unit tests (Ginkgo, verbose) @command -v ginkgo >/dev/null || (echo "Please install ginkgo (go install github.com/onsi/ginkgo/v2/ginkgo@latest)"; exit 1) @echo "Running Ginkgo unit test suites (verbose)..." - @ginkgo -r -v ./... + @ginkgo -r -v ./cmd ./internal .PHONY: test-int test-int: ## Run integration tests (requires Docker) diff --git a/README.md b/README.md index 9acf299..9b58466 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ Dependencies are managed using Google Wire. To add a new dependency: The project keeps tests close to the code that they exercise: -- **Domain/adapter/app unit tests** live under `internal/**` and cover parsing, validation, orchestration, and adapters. Run them all with `task test` (or `make test`), which now uses the Ginkgo CLI to run unit test suites in verbose mode (`ginkgo -r -v ./...`). +- **Domain/adapter/app unit tests** live under `internal/**` and cover parsing, validation, orchestration, and adapters. Run them all with `task test` (or `make test`), which now uses the Ginkgo CLI to run unit test suites in verbose mode (`ginkgo -r -v ./cmd ./internal`). - **Integration tests** under `test/integration` spin up disposable TimescaleDB and NATS JetStream instances (via `testcontainers-go`) and execute a full ingestion flow. Use `task test-int` after ensuring Docker is running. - **Coverage goals** are tracked via `task coverage`, which produces both a coverage profile and an HTML report under `coverage/coverage.html`. @@ -330,7 +330,7 @@ The project keeps tests close to the code that they exercise: | Generate coverage html | `make coverage` | `task coverage` | | Lint (golangci-lint) | `make lint` | `task lint` | -> Integration tests need Docker available on the host. Ginkgo-based unit tests or lint targets require the respective binaries (`go install github.com/onsi/ginkgo/v2/ginkgo@latest`, [golangci-lint install guide](https://golangci-lint.run/)). Use `task install-test` to bootstrap Ginkgo tooling before running `task test`, `task test-all`, or their `make` equivalents. `make test` / `task test` run Ginkgo in verbose mode (`-v`), showing each spec for easier debugging. +> Integration tests need Docker available on the host. Ginkgo-based unit tests or lint targets require the respective binaries (`go install github.com/onsi/ginkgo/v2/ginkgo@latest`, [golangci-lint install guide](https://golangci-lint.run/)). Use `task install-test` to bootstrap Ginkgo tooling before running `task test`, `task test-all`, or their `make` equivalents. `make test` / `task test` run Ginkgo in verbose mode (`-v`) over `./cmd` and `./internal`, showing each spec for easier debugging. ## Message Flow diff --git a/Taskfile.yml b/Taskfile.yml index b52560b..eb74482 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -63,7 +63,7 @@ tasks: exit 1 fi - echo "Running Ginkgo unit test suites (verbose)..." - - ginkgo -r -v ./... + - ginkgo -r -v ./cmd ./internal test-int: desc: Run integration tests (requires Docker) diff --git a/cmd/seed-telegrams/main.go b/cmd/seed-telegrams/main.go index d85dd06..253866c 100644 --- a/cmd/seed-telegrams/main.go +++ b/cmd/seed-telegrams/main.go @@ -76,8 +76,30 @@ func main() { for i := 0; i < *count; i++ { cat := categories[rand.Intn(len(categories))] - payload := buildTelegram(cat) - payload.Status = statuses[rand.Intn(len(statuses))] + payload, intentionallyInvalid := buildTelegram(cat) + + // 当状态为 random 时,根据报文是否合法来倾向选择 parsed 或 body_error + if strings.ToLower(*status) == "random" { + if intentionallyInvalid { + // 故意非法的报文:大概率标记为 body_error + if rand.Intn(100) < 80 { + payload.Status = "body_error" + } else { + payload.Status = statusValues[rand.Intn(len(statusValues))] + } + } else { + // 合法报文:大概率标记为 parsed + if rand.Intn(100) < 70 { + payload.Status = "parsed" + } else { + payload.Status = statusValues[rand.Intn(len(statusValues))] + } + } + } else { + // 非 random 模式下沿用原有逻辑 + payload.Status = statuses[rand.Intn(len(statuses))] + } + payload.ErrorReason = *errorReason payload.Metadata = map[string]string{ "message_id": payload.MessageID, @@ -140,7 +162,7 @@ type telegram struct { Metadata map[string]string `json:"metadata"` } -func buildTelegram(category string) *telegram { +func buildTelegram(category string) (*telegram, bool) { now := time.Now().UTC() messageID := fmt.Sprintf("%s%04d", category, rand.Intn(9000)+1000) headerTime := now.Format("020304") @@ -149,7 +171,7 @@ func buildTelegram(category string) *telegram { originLine := originatorLines[rand.Intn(len(originatorLines))] originator := originators[rand.Intn(len(originators))] - body := buildBody(category) + body, intentionallyInvalid := buildBody(category) content := strings.Join([]string{ fmt.Sprintf("ZCZC %s %s", messageID, headerTime), @@ -166,10 +188,10 @@ func buildTelegram(category string) *telegram { Category: category, Content: content, ReceivedAt: now, - } + }, intentionallyInvalid } -func buildBody(category string) string { +func buildBody(category string) (string, bool) { flight := fmt.Sprintf("%s%04d", []string{"CCA", "SWA", "DLH", "AAL", "JAE"}[rand.Intn(5)], rand.Intn(9000)+1000) dep := airports[rand.Intn(len(airports))] arr := airports[rand.Intn(len(airports))] @@ -179,15 +201,23 @@ func buildBody(category string) string { switch category { case "ARR": if rand.Intn(2) == 0 { - return fmt.Sprintf("(ARR-%s-%s-%s%s)", flight, dep, arr, arrTime) + return fmt.Sprintf("(ARR-%s-%s-%s%s)", flight, dep, arr, arrTime), false } - return fmt.Sprintf("(ARR-%s/%s-%s-%s%s)", flight, randomSSR(), dep, arr, arrTime) + // 70% 使用简单 SSR(合法),30% 使用复杂 SSR(当前正则下非法) + if rand.Intn(100) < 30 { + return fmt.Sprintf("(ARR-%s/%s-%s-%s%s)", flight, randomComplexSSR(), dep, arr, arrTime), true + } + return fmt.Sprintf("(ARR-%s/%s-%s-%s%s)", flight, randomSimpleSSR(), dep, arr, arrTime), false case "DEP": - return fmt.Sprintf("(DEP-%s/%s-%s%s-%s)", flight, randomSSR(), dep, depTime, arr) + // 70% 使用简单 SSR(合法),30% 使用复杂 SSR(当前正则下非法) + if rand.Intn(100) < 30 { + return fmt.Sprintf("(DEP-%s/%s-%s%s-%s)", flight, randomComplexSSR(), dep, depTime, arr), true + } + return fmt.Sprintf("(DEP-%s/%s-%s%s-%s)", flight, randomSimpleSSR(), dep, depTime, arr), false case "CNL": - return fmt.Sprintf("(CNL-%s-%s-%s)", flight, dep, arr) + return fmt.Sprintf("(CNL-%s-%s-%s)", flight, dep, arr), false case "DLA": - return fmt.Sprintf("(DLA-%s-%s%s-%s)", flight, dep, depTime, arr) + return fmt.Sprintf("(DLA-%s-%s%s-%s)", flight, dep, depTime, arr), false default: // FPL return fmt.Sprintf(`(FPL-%s-IS -%s/H @@ -208,7 +238,7 @@ func buildBody(category string) string { arrTime, randomAirportPair(), randomOtherInfo(), - ) + ), false } } @@ -216,6 +246,16 @@ func randomSSR() string { return []string{"A0132", "A5633", "SXIRPZJWY/LB101", "SHID/C"}[rand.Intn(4)] } +// 与当前 ARR/DEP 正则匹配的简单 SSR +func randomSimpleSSR() string { + return []string{"A0132", "A5633"}[rand.Intn(2)] +} + +// 故意构造为当前 ARR/DEP 正则无法解析的复杂 SSR +func randomComplexSSR() string { + return []string{"SXIRPZJWY/LB101", "SHID/C"}[rand.Intn(2)] +} + func randomAircraft() string { return []string{"A332", "B788", "MA60", "A359"}[rand.Intn(4)] }