Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fddace8df | ||
|
|
076d77da8e |
@@ -95,6 +95,7 @@ jobs:
|
|||||||
typeof filename === 'string' &&
|
typeof filename === 'string' &&
|
||||||
(
|
(
|
||||||
filename.startsWith('.github/workflows/') ||
|
filename.startsWith('.github/workflows/') ||
|
||||||
|
filename.startsWith('scripts/ci/') ||
|
||||||
filename.startsWith('scripts/policy/') ||
|
filename.startsWith('scripts/policy/') ||
|
||||||
filename.startsWith('scripts/release/') ||
|
filename.startsWith('scripts/release/') ||
|
||||||
filename === 'test/fixtures/cli-interface-baseline.txt' ||
|
filename === 'test/fixtures/cli-interface-baseline.txt' ||
|
||||||
|
|||||||
+22
-29
@@ -130,11 +130,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Verify Test Package Plan
|
||||||
|
if: steps.classify.outputs.changelog_only != 'true'
|
||||||
|
run: make test-plan
|
||||||
|
|
||||||
- name: Format Check
|
- name: Format Check
|
||||||
if: steps.classify.outputs.changelog_only != 'true'
|
if: steps.classify.outputs.changelog_only != 'true'
|
||||||
run: |
|
run: make format-check
|
||||||
unformatted="$(find cmd internal test scripts/policy -name '*.go' -print0 | xargs -0r gofmt -l)"
|
|
||||||
test -z "$unformatted" || (printf '%s\n' "$unformatted" && exit 1)
|
|
||||||
|
|
||||||
- name: Go Vet
|
- name: Go Vet
|
||||||
if: steps.classify.outputs.changelog_only != 'true'
|
if: steps.classify.outputs.changelog_only != 'true'
|
||||||
@@ -167,38 +169,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
||||||
- name: Install archive tooling
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y zip unzip
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: ${{ matrix.shard == 'remaining' }}
|
if: ${{ matrix.shard == 'remaining' }}
|
||||||
run: make build
|
run: make build
|
||||||
|
|
||||||
- name: Test shard with Race Detection
|
- name: Test shard with Race Detection
|
||||||
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
DWS_PACKAGE_VERSION: 0.0.0-test
|
||||||
TEST_SHARD: ${{ matrix.shard }}
|
TEST_SHARD: ${{ matrix.shard }}
|
||||||
run: |
|
run: |
|
||||||
case "$TEST_SHARD" in
|
set -euo pipefail
|
||||||
app)
|
package_output="$(./scripts/ci/test-packages.sh list "$TEST_SHARD")"
|
||||||
packages=(./internal/app/...)
|
test -n "$package_output"
|
||||||
;;
|
mapfile -t packages <<< "$package_output"
|
||||||
generators)
|
|
||||||
packages=(./internal/generator/...)
|
|
||||||
;;
|
|
||||||
helpers)
|
|
||||||
packages=(./internal/helpers/...)
|
|
||||||
;;
|
|
||||||
remaining)
|
|
||||||
mapfile -t packages < <(
|
|
||||||
go list ./cmd/... ./internal/... |
|
|
||||||
grep -Ev '/internal/(app|generator|helpers)(/|$)'
|
|
||||||
)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf 'unknown test shard: %s\n' "$TEST_SHARD" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
test "${#packages[@]}" -gt 0
|
test "${#packages[@]}" -gt 0
|
||||||
go test -v -race -count=1 -timeout=10m "${packages[@]}"
|
go test -v -race -count=1 -timeout=10m "${packages[@]}"
|
||||||
|
|
||||||
@@ -207,7 +191,7 @@ jobs:
|
|||||||
needs: lint
|
needs: lint
|
||||||
if: ${{ needs.lint.outputs.changelog_only != 'true' }}
|
if: ${{ needs.lint.outputs.changelog_only != 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository
|
- name: Check out repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -221,7 +205,16 @@ jobs:
|
|||||||
run: sudo apt-get update && sudo apt-get install -y zip unzip
|
run: sudo apt-get update && sudo apt-get install -y zip unzip
|
||||||
|
|
||||||
- name: Test release scripts
|
- name: Test release scripts
|
||||||
run: go test -v -count=1 -timeout=5m ./test/scripts
|
shell: bash
|
||||||
|
env:
|
||||||
|
DWS_PACKAGE_VERSION: 0.0.0-test
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
package_output="$(./scripts/ci/test-packages.sh list release-scripts)"
|
||||||
|
test -n "$package_output"
|
||||||
|
mapfile -t packages <<< "$package_output"
|
||||||
|
test "${#packages[@]}" -gt 0
|
||||||
|
go test -v -count=1 -timeout=10m "${packages[@]}"
|
||||||
|
|
||||||
test-cross-platform:
|
test-cross-platform:
|
||||||
name: Test (cross-platform compile)
|
name: Test (cross-platform compile)
|
||||||
|
|||||||
+1
-1
@@ -36,8 +36,8 @@ Common repository checks already used here include:
|
|||||||
./scripts/policy/check-open-source-assets.sh
|
./scripts/policy/check-open-source-assets.sh
|
||||||
go test ./...
|
go test ./...
|
||||||
make test
|
make test
|
||||||
|
make test-plan
|
||||||
make lint
|
make lint
|
||||||
bash test/scripts/run_all_tests.sh --jobs 8
|
|
||||||
./scripts/policy/check-generated-drift.sh
|
./scripts/policy/check-generated-drift.sh
|
||||||
./scripts/policy/check-command-surface.sh --strict
|
./scripts/policy/check-command-surface.sh --strict
|
||||||
./scripts/release/verify-package-managers.sh
|
./scripts/release/verify-package-managers.sh
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
GO ?= go
|
GO ?= go
|
||||||
|
DWS_PACKAGE_VERSION ?= 0.0.0-test
|
||||||
REMOTE ?=
|
REMOTE ?=
|
||||||
PUBLISH ?= 0
|
PUBLISH ?= 0
|
||||||
YES ?= 0
|
YES ?= 0
|
||||||
DWS_POLICY_TMPDIR ?= $(CURDIR)/.worktrees/policy-tmp
|
DWS_POLICY_TMPDIR ?= $(CURDIR)/.worktrees/policy-tmp
|
||||||
POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go
|
POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go
|
||||||
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
|
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
|
||||||
|
GO_SOURCE_LIST = git ls-files -z --cached --others --exclude-standard -- '*.go'
|
||||||
|
|
||||||
.PHONY: all help build rebuild test lint fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
.PHONY: all help build rebuild test test-plan lint format-check fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||||
|
|
||||||
all: setup-hooks fmt lint build test rebuild
|
all: setup-hooks fmt lint build test rebuild
|
||||||
|
|
||||||
@@ -14,8 +16,10 @@ help:
|
|||||||
@printf "Available targets:\n"
|
@printf "Available targets:\n"
|
||||||
@printf " make build - Build the dws CLI binary\n"
|
@printf " make build - Build the dws CLI binary\n"
|
||||||
@printf " make test - Run the Go test suite\n"
|
@printf " make test - Run the Go test suite\n"
|
||||||
@printf " make lint - Run formatting checks and golangci-lint when available\n"
|
@printf " make test-plan - Verify every default Go package belongs to one CI test shard\n"
|
||||||
@printf " make fmt - Format Go source files\n"
|
@printf " make lint - Run formatting checks, go vet, and staticcheck\n"
|
||||||
|
@printf " make format-check - Check all repository Go source files with gofmt\n"
|
||||||
|
@printf " make fmt - Format all repository Go source files\n"
|
||||||
@printf " make policy - Check the built dws plus open-source and Schema policies\n"
|
@printf " make policy - Check the built dws plus open-source and Schema policies\n"
|
||||||
@printf " make interface-integrity - Check historical commands and help contracts still work\n"
|
@printf " make interface-integrity - Check historical commands and help contracts still work\n"
|
||||||
@printf " make authoritative-interface-integrity BASE_REF=<ref> - Check the Git-owned PR merge-base\n"
|
@printf " make authoritative-interface-integrity BASE_REF=<ref> - Check the Git-owned PR merge-base\n"
|
||||||
@@ -45,13 +49,32 @@ rebuild:
|
|||||||
@./scripts/dev/build.sh
|
@./scripts/dev/build.sh
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@./test/scripts/run_all_tests.sh --timeout 5m
|
@DWS_PACKAGE_VERSION="$(DWS_PACKAGE_VERSION)" $(GO) test -count=1 -timeout=10m ./...
|
||||||
|
|
||||||
|
test-plan:
|
||||||
|
@./scripts/ci/test-packages.sh verify
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@./scripts/dev/lint.sh
|
@./scripts/dev/lint.sh
|
||||||
|
|
||||||
|
format-check:
|
||||||
|
@set -eu; \
|
||||||
|
go_files="$$(mktemp "$${TMPDIR:-/tmp}/dws-go-files.XXXXXX")"; \
|
||||||
|
trap 'rm -f "$$go_files"' EXIT HUP INT TERM; \
|
||||||
|
$(GO_SOURCE_LIST) > "$$go_files"; \
|
||||||
|
unformatted="$$(xargs -0 sh -c 'if [ "$$#" -gt 0 ]; then exec gofmt -l -- "$$@"; fi' sh < "$$go_files")"; \
|
||||||
|
if [ -n "$$unformatted" ]; then \
|
||||||
|
printf '%s\n' "$$unformatted"; \
|
||||||
|
printf '%s\n' "Go files are not formatted. Run 'make fmt'." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@find cmd internal test scripts/policy -name '*.go' -print0 2>/dev/null | xargs -0r gofmt -w
|
@set -eu; \
|
||||||
|
go_files="$$(mktemp "$${TMPDIR:-/tmp}/dws-go-files.XXXXXX")"; \
|
||||||
|
trap 'rm -f "$$go_files"' EXIT HUP INT TERM; \
|
||||||
|
$(GO_SOURCE_LIST) > "$$go_files"; \
|
||||||
|
xargs -0 sh -c 'if [ "$$#" -gt 0 ]; then exec gofmt -w -- "$$@"; fi' sh < "$$go_files"
|
||||||
|
|
||||||
policy:
|
policy:
|
||||||
@mkdir -p "$(POLICY_GOTMPDIR)"
|
@mkdir -p "$(POLICY_GOTMPDIR)"
|
||||||
|
|||||||
@@ -31,6 +31,31 @@ import (
|
|||||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type consumeReadyWriter struct {
|
||||||
|
ready chan struct{}
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConsumeReadyWriter() *consumeReadyWriter {
|
||||||
|
return &consumeReadyWriter{ready: make(chan struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *consumeReadyWriter) Write(p []byte) (int, error) {
|
||||||
|
if bytes.Contains(p, []byte("[event] ready ")) {
|
||||||
|
w.once.Do(func() { close(w.ready) })
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForConsumerReady(t *testing.T, ready <-chan struct{}) {
|
||||||
|
t.Helper()
|
||||||
|
select {
|
||||||
|
case <-ready:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("consumer did not report ready")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestIntegration_HelloPushdownFiltersAtBus verifies the Hello-time
|
// TestIntegration_HelloPushdownFiltersAtBus verifies the Hello-time
|
||||||
// event_types pushdown contract (plan §4 unsung superpower): a consumer
|
// event_types pushdown contract (plan §4 unsung superpower): a consumer
|
||||||
// subscribing to "im.*" must NOT receive "approval.*" events even when
|
// subscribing to "im.*" must NOT receive "approval.*" events even when
|
||||||
@@ -49,40 +74,50 @@ func TestIntegration_HelloPushdownFiltersAtBus(t *testing.T) {
|
|||||||
defer func() { cancel(); <-runDone }()
|
defer func() { cancel(); <-runDone }()
|
||||||
|
|
||||||
var imBuf, approvalBuf bytes.Buffer
|
var imBuf, approvalBuf bytes.Buffer
|
||||||
var wg sync.WaitGroup
|
imReady := newConsumeReadyWriter()
|
||||||
wg.Add(2)
|
approvalReady := newConsumeReadyWriter()
|
||||||
|
consumeCtx, cancelConsumers := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancelConsumers()
|
||||||
|
consumeDone := make(chan error, 2)
|
||||||
|
|
||||||
// Consumer A: im.* only
|
// Consumer A: im.* only
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
consumeDone <- Run(consumeCtx, Config{
|
||||||
_ = Run(context.Background(), Config{
|
|
||||||
WorkDir: dir,
|
WorkDir: dir,
|
||||||
IPCEndpoint: sock,
|
IPCEndpoint: sock,
|
||||||
ClientID: "ding_test",
|
ClientID: "ding_test",
|
||||||
Stdout: &imBuf,
|
Stdout: &imBuf,
|
||||||
Stderr: io.Discard,
|
Stderr: imReady,
|
||||||
EventTypes: []string{"im.*"},
|
EventTypes: []string{"im.*"},
|
||||||
MaxEvents: 3, // 3 im events expected
|
MaxEvents: 3, // 3 im events expected
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
// Consumer B: approval.* only
|
// Consumer B: approval.* only
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
consumeDone <- Run(consumeCtx, Config{
|
||||||
_ = Run(context.Background(), Config{
|
|
||||||
WorkDir: dir,
|
WorkDir: dir,
|
||||||
IPCEndpoint: sock,
|
IPCEndpoint: sock,
|
||||||
ClientID: "ding_test",
|
ClientID: "ding_test",
|
||||||
Stdout: &approvalBuf,
|
Stdout: &approvalBuf,
|
||||||
Stderr: io.Discard,
|
Stderr: approvalReady,
|
||||||
EventTypes: []string{"approval.*"},
|
EventTypes: []string{"approval.*"},
|
||||||
MaxEvents: 2, // 2 approval events expected
|
MaxEvents: 2, // 2 approval events expected
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Give both consumers time to Hello + register.
|
waitForConsumerReady(t, imReady.ready)
|
||||||
time.Sleep(200 * time.Millisecond)
|
waitForConsumerReady(t, approvalReady.ready)
|
||||||
close(trigger)
|
close(trigger)
|
||||||
wg.Wait()
|
for range 2 {
|
||||||
|
select {
|
||||||
|
case err := <-consumeDone:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("consume failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-consumeCtx.Done():
|
||||||
|
t.Fatalf("consumers did not finish: %v", consumeCtx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Verify consumer A got exactly the 3 im.* events.
|
// Verify consumer A got exactly the 3 im.* events.
|
||||||
imLines := nonEmptyLines(imBuf.String())
|
imLines := nonEmptyLines(imBuf.String())
|
||||||
@@ -130,23 +165,32 @@ func TestIntegration_FilterRegexNarrowsFurther(t *testing.T) {
|
|||||||
defer func() { cancel(); <-runDone }()
|
defer func() { cancel(); <-runDone }()
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
consumeDone := make(chan struct{})
|
ready := newConsumeReadyWriter()
|
||||||
|
consumeCtx, cancelConsumer := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancelConsumer()
|
||||||
|
consumeDone := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(consumeDone)
|
consumeDone <- Run(consumeCtx, Config{
|
||||||
_ = Run(context.Background(), Config{
|
|
||||||
WorkDir: dir,
|
WorkDir: dir,
|
||||||
IPCEndpoint: sock,
|
IPCEndpoint: sock,
|
||||||
ClientID: "ding_test",
|
ClientID: "ding_test",
|
||||||
Stdout: &buf,
|
Stdout: &buf,
|
||||||
Stderr: io.Discard,
|
Stderr: ready,
|
||||||
EventTypes: []string{"im.*"},
|
EventTypes: []string{"im.*"},
|
||||||
Filter: `\.at_v1$`, // only at_v1 events
|
Filter: `\.at_v1$`, // only at_v1 events
|
||||||
MaxEvents: 1,
|
MaxEvents: 1,
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
time.Sleep(150 * time.Millisecond)
|
waitForConsumerReady(t, ready.ready)
|
||||||
close(trigger)
|
close(trigger)
|
||||||
<-consumeDone
|
select {
|
||||||
|
case err := <-consumeDone:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("consume failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-consumeCtx.Done():
|
||||||
|
t.Fatalf("consumer did not finish: %v", consumeCtx.Err())
|
||||||
|
}
|
||||||
|
|
||||||
lines := nonEmptyLines(buf.String())
|
lines := nonEmptyLines(buf.String())
|
||||||
if len(lines) != 1 {
|
if len(lines) != 1 {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
// 2. 渐进式退避(1s/2s/5s/10s/15s/30s/30s...,可自定义)
|
// 2. 渐进式退避(1s/2s/5s/10s/15s/30s/30s...,可自定义)
|
||||||
// 3. 默认 5 分钟超时,超时返回 jobId 让用户用 `... get --job-id <ID>` 续等
|
// 3. 默认 5 分钟超时,超时返回 jobId 让用户用 `... get --job-id <ID>` 续等
|
||||||
// 4. 状态机统一:PROCESSING/PENDING → SUCCESS → 拿 downloadUrl
|
// 4. 状态机统一:PROCESSING/PENDING → SUCCESS → 拿 downloadUrl
|
||||||
// ↓
|
// ↓
|
||||||
// FAILED → 返回 error
|
// FAILED → 返回 error
|
||||||
// 5. 可选 PUT 下载:传入 OutputPath 时本包负责 HTTP GET 落盘
|
// 5. 可选 PUT 下载:传入 OutputPath 时本包负责 HTTP GET 落盘
|
||||||
package asynctask
|
package asynctask
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ func TestSubmit_ContextCancel(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
submitFn := func(ctx context.Context) (string, error) { return "job-x", nil }
|
submitFn := func(ctx context.Context) (string, error) { return "job-x", nil }
|
||||||
queryFn := func(ctx context.Context, jobID string) (QueryResult, error) { return QueryResult{Status: StatusProcessing}, nil }
|
queryFn := func(ctx context.Context, jobID string) (QueryResult, error) {
|
||||||
|
return QueryResult{Status: StatusProcessing}, nil
|
||||||
|
}
|
||||||
_, err := Submit(ctx, submitFn, queryFn, Options{Backoff: testBackoff, Timeout: 1 * time.Second})
|
_, err := Submit(ctx, submitFn, queryFn, Options{Backoff: testBackoff, Timeout: 1 * time.Second})
|
||||||
if !errors.Is(err, context.Canceled) {
|
if !errors.Is(err, context.Canceled) {
|
||||||
t.Fatalf("expect ctx.Canceled, got %v", err)
|
t.Fatalf("expect ctx.Canceled, got %v", err)
|
||||||
|
|||||||
+6
-3
@@ -3,9 +3,11 @@
|
|||||||
These repo-local entrypoints are the supported shell entrypoints for building, testing, packaging, and policy checks.
|
These repo-local entrypoints are the supported shell entrypoints for building, testing, packaging, and policy checks.
|
||||||
|
|
||||||
- `make build`: build the `dws` CLI from `cmd`
|
- `make build`: build the `dws` CLI from `cmd`
|
||||||
- `make test`: run `go test ./...`
|
- `make test`: run every default package returned by `go list ./...`
|
||||||
- `make lint`: run formatting checks and required `golangci-lint`
|
- `make test-plan`: verify every default Go package belongs to exactly one CI test shard
|
||||||
- `make fmt`: format Go source files under `cmd/`, `internal/`, and `test/`
|
- `make lint`: run the repository-wide format check, `go vet`, and required `staticcheck`
|
||||||
|
- `make format-check`: check every tracked or non-ignored untracked repository Go source file with `gofmt`
|
||||||
|
- `make fmt`: format every tracked or non-ignored untracked repository Go source file
|
||||||
- `make policy`: reuse the current `dws` binary and run the complete policy suite (`make build` first)
|
- `make policy`: reuse the current `dws` binary and run the complete policy suite (`make build` first)
|
||||||
- `make package`: build all release artifacts locally
|
- `make package`: build all release artifacts locally
|
||||||
- `make release-pre VERSION=vX.Y.Z-beta.N`: validate a prerelease (`PUBLISH=1` pushes its tag)
|
- `make release-pre VERSION=vX.Y.Z-beta.N`: validate a prerelease (`PUBLISH=1` pushes its tag)
|
||||||
@@ -15,6 +17,7 @@ Script groups:
|
|||||||
|
|
||||||
- Root installers: `./scripts/install.sh`, `./scripts/install.ps1`, `./scripts/install-skills.sh`
|
- Root installers: `./scripts/install.sh`, `./scripts/install.ps1`, `./scripts/install-skills.sh`
|
||||||
- Product convenience installers: `./scripts/install-devapp.sh`, `./scripts/install-devapp.ps1`, `./scripts/install-event.sh`
|
- Product convenience installers: `./scripts/install-devapp.sh`, `./scripts/install-devapp.ps1`, `./scripts/install-event.sh`
|
||||||
|
- CI package plan: `./scripts/ci/test-packages.sh`
|
||||||
- Dev helpers: `./scripts/dev/build.sh`, `./scripts/dev/lint.sh`, `./scripts/dev/ci-local.sh`, `./scripts/dev/run-mock-e2e.sh`, `./scripts/dev/coverage.sh`
|
- Dev helpers: `./scripts/dev/build.sh`, `./scripts/dev/lint.sh`, `./scripts/dev/ci-local.sh`, `./scripts/dev/run-mock-e2e.sh`, `./scripts/dev/coverage.sh`
|
||||||
- Policy checks: `./scripts/policy/check-schema-command-registry.sh`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-command-compatibility.sh --base-ref <main-ref> --stable-ref <latest-GA-tag>`, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh`
|
- Policy checks: `./scripts/policy/check-schema-command-registry.sh`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-command-compatibility.sh --base-ref <main-ref> --stable-ref <latest-GA-tag>`, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh`
|
||||||
- Policy runtime files: set `DWS_POLICY_TMPDIR` to override the default `.worktrees/policy-tmp` workspace
|
- Policy runtime files: set `DWS_POLICY_TMPDIR` to override the default `.worktrees/policy-tmp` workspace
|
||||||
|
|||||||
Executable
+121
@@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||||
|
MODULE="$(cd "$ROOT" && go list -m)"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf '%s\n' \
|
||||||
|
"usage: $0 list <app|generators|helpers|remaining|release-scripts>" \
|
||||||
|
" $0 verify" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
list_shard() {
|
||||||
|
shard="$1"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
case "$shard" in
|
||||||
|
app)
|
||||||
|
go list ./internal/app/...
|
||||||
|
;;
|
||||||
|
generators)
|
||||||
|
go list ./internal/generator/...
|
||||||
|
;;
|
||||||
|
helpers)
|
||||||
|
go list ./internal/helpers/...
|
||||||
|
;;
|
||||||
|
release-scripts)
|
||||||
|
go list ./test/scripts/...
|
||||||
|
;;
|
||||||
|
remaining)
|
||||||
|
all_packages="$(go list ./...)"
|
||||||
|
printf '%s\n' "$all_packages" | while IFS= read -r package; do
|
||||||
|
case "$package" in
|
||||||
|
"$MODULE/internal/app"|"$MODULE/internal/app/"*) ;;
|
||||||
|
"$MODULE/internal/generator"|"$MODULE/internal/generator/"*) ;;
|
||||||
|
"$MODULE/internal/helpers"|"$MODULE/internal/helpers/"*) ;;
|
||||||
|
"$MODULE/test/scripts"|"$MODULE/test/scripts/"*) ;;
|
||||||
|
*) printf '%s\n' "$package" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'unknown test package shard: %s\n' "$shard" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_plan() {
|
||||||
|
workdir="$(mktemp -d "${TMPDIR:-/tmp}/dws-test-packages.XXXXXX")"
|
||||||
|
trap 'rm -rf "$workdir"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
expected="$workdir/expected"
|
||||||
|
assigned="$workdir/assigned"
|
||||||
|
unique="$workdir/unique"
|
||||||
|
duplicates="$workdir/duplicates"
|
||||||
|
missing="$workdir/missing"
|
||||||
|
unexpected="$workdir/unexpected"
|
||||||
|
all_packages="$workdir/all-packages"
|
||||||
|
|
||||||
|
cd "$ROOT"
|
||||||
|
go list ./... > "$all_packages"
|
||||||
|
LC_ALL=C sort -u "$all_packages" > "$expected"
|
||||||
|
: > "$assigned"
|
||||||
|
|
||||||
|
for shard in app generators helpers remaining release-scripts; do
|
||||||
|
shard_packages="$workdir/$shard"
|
||||||
|
unsorted_shard_packages="$workdir/$shard.unsorted"
|
||||||
|
list_shard "$shard" > "$unsorted_shard_packages"
|
||||||
|
LC_ALL=C sort "$unsorted_shard_packages" > "$shard_packages"
|
||||||
|
if [ ! -s "$shard_packages" ]; then
|
||||||
|
printf 'test package shard is empty: %s\n' "$shard" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cat "$shard_packages" >> "$assigned"
|
||||||
|
done
|
||||||
|
|
||||||
|
LC_ALL=C sort "$assigned" -o "$assigned"
|
||||||
|
uniq -d "$assigned" > "$duplicates"
|
||||||
|
LC_ALL=C sort -u "$assigned" > "$unique"
|
||||||
|
comm -23 "$expected" "$unique" > "$missing"
|
||||||
|
comm -13 "$expected" "$unique" > "$unexpected"
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
if [ -s "$duplicates" ]; then
|
||||||
|
printf '%s\n' 'test packages assigned to more than one shard:' >&2
|
||||||
|
sed 's/^/ /' "$duplicates" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
if [ -s "$missing" ]; then
|
||||||
|
printf '%s\n' 'default Go packages missing from the CI test plan:' >&2
|
||||||
|
sed 's/^/ /' "$missing" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
if [ -s "$unexpected" ]; then
|
||||||
|
printf '%s\n' 'CI test plan contains packages outside go list ./...:' >&2
|
||||||
|
sed 's/^/ /' "$unexpected" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
if [ "$failed" -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
package_count="$(wc -l < "$expected" | tr -d ' ')"
|
||||||
|
printf 'test package plan covers %s default packages exactly once\n' "$package_count"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
list)
|
||||||
|
[ "$#" -eq 2 ] || usage
|
||||||
|
list_shard "$2"
|
||||||
|
;;
|
||||||
|
verify)
|
||||||
|
[ "$#" -eq 1 ] || usage
|
||||||
|
verify_plan
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -11,7 +11,7 @@ run() {
|
|||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
run ./scripts/dev/lint.sh
|
run ./scripts/dev/lint.sh
|
||||||
run go test ./...
|
run make test
|
||||||
COVERAGE_PROFILE="$(mktemp "${TMPDIR:-/tmp}/dws-coverage.XXXXXX")"
|
COVERAGE_PROFILE="$(mktemp "${TMPDIR:-/tmp}/dws-coverage.XXXXXX")"
|
||||||
run ./scripts/dev/coverage.sh "$COVERAGE_PROFILE"
|
run ./scripts/dev/coverage.sh "$COVERAGE_PROFILE"
|
||||||
rm -f "$COVERAGE_PROFILE"
|
rm -f "$COVERAGE_PROFILE"
|
||||||
|
|||||||
+5
-6
@@ -1,13 +1,12 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
# ── Format check ──────────────────────────────────────────
|
# ── Format check ──────────────────────────────────────────
|
||||||
unformatted="$(find cmd internal test -name '*.go' -print0 2>/dev/null | xargs -0r gofmt -l)"
|
echo "Running gofmt check..."
|
||||||
if [ -n "$unformatted" ]; then
|
make format-check
|
||||||
echo "$unformatted"
|
|
||||||
echo "Go files are not formatted. Run 'make fmt'." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── go vet (built-in) ────────────────────────────────────
|
# ── go vet (built-in) ────────────────────────────────────
|
||||||
echo "Running go vet..."
|
echo "Running go vet..."
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -eu
|
|||||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||||
|
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
go test ./test/mock_mcp/... \
|
DWS_PACKAGE_VERSION="${DWS_PACKAGE_VERSION:-0.0.0-test}" \
|
||||||
./test/integration/discovery/... \
|
go test -count=1 -timeout=5m \
|
||||||
./test/integration/extensions/... \
|
./test/mock_mcp/... \
|
||||||
./test/integration/recovery/...
|
./test/integration/...
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ type docsFlagCase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDWSDocsCommandTreeCoverage(t *testing.T) {
|
func TestDWSDocsCommandTreeCoverage(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
docPaths, _, err := parseDWSDocsCompatibility()
|
docPaths, _, err := parseDWSDocsCompatibility()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
|
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
|
||||||
@@ -45,8 +43,6 @@ func TestDWSDocsCommandTreeCoverage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDWSDocsLocalFlagsCoverage(t *testing.T) {
|
func TestDWSDocsLocalFlagsCoverage(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
docPaths, docFlags, err := parseDWSDocsCompatibility()
|
docPaths, docFlags, err := parseDWSDocsCompatibility()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
|
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import (
|
|||||||
// and their availability depends on the test environment's fixture data.
|
// and their availability depends on the test environment's fixture data.
|
||||||
|
|
||||||
func TestHiddenMCPHelpIsReachable(t *testing.T) {
|
func TestHiddenMCPHelpIsReachable(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cmd := app.NewRootCommand()
|
cmd := app.NewRootCommand()
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
cmd.SetOut(&out)
|
cmd.SetOut(&out)
|
||||||
@@ -33,8 +31,6 @@ func TestHiddenMCPHelpIsReachable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSkillCommandIsRegistered(t *testing.T) {
|
func TestSkillCommandIsRegistered(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := app.NewRootCommand()
|
root := app.NewRootCommand()
|
||||||
|
|
||||||
var found bool
|
var found bool
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
// Set an empty catalog fixture so that EnvironmentLoader does not
|
// Set an empty catalog fixture so that EnvironmentLoader does not
|
||||||
// attempt live discovery (which would hang on unreachable MCP endpoints).
|
// attempt live discovery (which would hang on unreachable MCP endpoints).
|
||||||
|
// Tests that construct app root commands must remain serial because root
|
||||||
|
// construction initializes process-wide helper dependencies.
|
||||||
absFixture, _ := filepath.Abs("testdata/empty_catalog.json")
|
absFixture, _ := filepath.Abs("testdata/empty_catalog.json")
|
||||||
os.Setenv(cli.CatalogFixtureEnv, absFixture)
|
os.Setenv(cli.CatalogFixtureEnv, absFixture)
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestRootHelpHidesRecoveredModules(t *testing.T) {
|
func TestRootHelpHidesRecoveredModules(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cmd := app.NewRootCommand()
|
cmd := app.NewRootCommand()
|
||||||
var out strings.Builder
|
var out strings.Builder
|
||||||
cmd.SetOut(&out)
|
cmd.SetOut(&out)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func TestAIBehaviorPolicyProtectsEnforcementInputs(t *testing.T) {
|
|||||||
|
|
||||||
workflow := string(data)
|
workflow := string(data)
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
|
"filename.startsWith('scripts/ci/')",
|
||||||
"filename.startsWith('scripts/policy/')",
|
"filename.startsWith('scripts/policy/')",
|
||||||
"filename === 'test/fixtures/cli-interface-baseline.txt'",
|
"filename === 'test/fixtures/cli-interface-baseline.txt'",
|
||||||
"previous_filename",
|
"previous_filename",
|
||||||
|
|||||||
@@ -421,6 +421,10 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
|||||||
`COVERAGE_ENFORCE_OVERALL: "false"`,
|
`COVERAGE_ENFORCE_OVERALL: "false"`,
|
||||||
`COVERAGE_OVERALL_TOLERANCE: "0"`,
|
`COVERAGE_OVERALL_TOLERANCE: "0"`,
|
||||||
`COVERAGE_ADDITIONAL_DIFF_PROFILE=coverage-shortcut.txt`,
|
`COVERAGE_ADDITIONAL_DIFF_PROFILE=coverage-shortcut.txt`,
|
||||||
|
"run: make test-plan",
|
||||||
|
"run: make format-check",
|
||||||
|
"./scripts/ci/test-packages.sh list",
|
||||||
|
`package_output="$(./scripts/ci/test-packages.sh list`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(admission, want) {
|
if !strings.Contains(admission, want) {
|
||||||
t.Errorf("Code Admission workflow missing contract %q", want)
|
t.Errorf("Code Admission workflow missing contract %q", want)
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package scripts_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCITestPackagePlanCoversDefaultPackagesExactlyOnce(t *testing.T) {
|
||||||
|
root := testPackagePlanRoot(t)
|
||||||
|
output := runTestPackagePlan(t, root, "verify")
|
||||||
|
if !strings.Contains(output, "default packages exactly once") {
|
||||||
|
t.Fatalf("verify output = %q, want coverage summary", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCITestPackagePlanRoutesPublicTestSuites(t *testing.T) {
|
||||||
|
root := testPackagePlanRoot(t)
|
||||||
|
remaining := strings.Fields(runTestPackagePlan(t, root, "list", "remaining"))
|
||||||
|
releaseScripts := strings.Fields(runTestPackagePlan(t, root, "list", "release-scripts"))
|
||||||
|
|
||||||
|
for _, suffix := range []string{
|
||||||
|
"/test/cli",
|
||||||
|
"/test/contract",
|
||||||
|
"/test/integration/extensions",
|
||||||
|
"/test/mock_mcp",
|
||||||
|
"/test/smoke",
|
||||||
|
"/test/unit",
|
||||||
|
} {
|
||||||
|
if !containsPackageSuffix(remaining, suffix) {
|
||||||
|
t.Errorf("remaining shard does not contain package ending in %q", suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if containsPackageSuffix(remaining, "/test/scripts") {
|
||||||
|
t.Error("remaining shard unexpectedly contains /test/scripts")
|
||||||
|
}
|
||||||
|
if !containsPackageSuffix(releaseScripts, "/test/scripts") {
|
||||||
|
t.Error("release-scripts shard does not contain /test/scripts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCITestPackagePlanFailsClosedWhenGoListFails(t *testing.T) {
|
||||||
|
root := testPackagePlanRoot(t)
|
||||||
|
fakeBin := t.TempDir()
|
||||||
|
fakeGo := filepath.Join(fakeBin, "go")
|
||||||
|
err := os.WriteFile(fakeGo, []byte(`#!/bin/sh
|
||||||
|
if [ "$1" = "list" ] && [ "$2" = "-m" ]; then
|
||||||
|
printf '%s\n' 'github.com/DingTalk-Real-AI/dingtalk-workspace-cli'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf '%s\n' 'injected go list failure' >&2
|
||||||
|
exit 42
|
||||||
|
`), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write fake go: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
script := filepath.Join(root, "scripts", "ci", "test-packages.sh")
|
||||||
|
for _, args := range [][]string{{"list", "remaining"}, {"verify"}} {
|
||||||
|
cmd := exec.Command("sh", append([]string{script}, args...)...)
|
||||||
|
cmd.Dir = root
|
||||||
|
cmd.Env = []string{
|
||||||
|
"PATH=" + fakeBin + string(os.PathListSeparator) + os.Getenv("PATH"),
|
||||||
|
"TMPDIR=" + t.TempDir(),
|
||||||
|
}
|
||||||
|
output, runErr := cmd.CombinedOutput()
|
||||||
|
if runErr == nil {
|
||||||
|
t.Fatalf("%s unexpectedly succeeded with failing go list:\n%s", strings.Join(args, " "), output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), "injected go list failure") {
|
||||||
|
t.Fatalf("%s failure output = %q, want injected failure", strings.Join(args, " "), output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPackagePlanRoot(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve repository root: %v", err)
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestPackagePlan(t *testing.T, root string, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
script := filepath.Join(root, "scripts", "ci", "test-packages.sh")
|
||||||
|
cmd := exec.Command("sh", append([]string{script}, args...)...)
|
||||||
|
cmd.Dir = root
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s %s failed: %v\n%s", script, strings.Join(args, " "), err, output)
|
||||||
|
}
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsPackageSuffix(packages []string, suffix string) bool {
|
||||||
|
for _, packagePath := range packages {
|
||||||
|
if strings.HasSuffix(packagePath, suffix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# run_all_tests.sh — Run all test suites in parallel and print a summary.
|
|
||||||
# Usage: bash test/scripts/run_all_tests.sh [--verbose] [--timeout <duration>] [--jobs <n>]
|
|
||||||
#
|
|
||||||
# Options:
|
|
||||||
# --verbose Print full test output for every suite (default: only on failure)
|
|
||||||
# --timeout <dur> Per-suite go test timeout (default: 120s)
|
|
||||||
# --jobs <n> Max parallel suites (default: number of CPU cores)
|
|
||||||
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
||||||
VERBOSE=false
|
|
||||||
TIMEOUT="120s"
|
|
||||||
MAX_JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
|
|
||||||
|
|
||||||
# ── Parse flags ──────────────────────────────────────────────────────────────
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--verbose) VERBOSE=true; shift ;;
|
|
||||||
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
||||||
--jobs) MAX_JOBS="$2"; shift 2 ;;
|
|
||||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# ── Colour helpers ────────────────────────────────────────────────────────────
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
BOLD='\033[1m'
|
|
||||||
RESET='\033[0m'
|
|
||||||
|
|
||||||
# ── Test suite definitions ────────────────────────────────────────────────────
|
|
||||||
# Each entry: "label|package_path"
|
|
||||||
declare -a SUITES=(
|
|
||||||
"cli|./test/cli/..."
|
|
||||||
"contract|./test/contract/..."
|
|
||||||
"integration/extensions|./test/integration/extensions/..."
|
|
||||||
"integration/recovery|./test/integration/recovery/..."
|
|
||||||
"mock_mcp|./test/mock_mcp/..."
|
|
||||||
"scripts|./test/scripts/..."
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Temporary workspace ───────────────────────────────────────────────────────
|
|
||||||
LOG_DIR="$(mktemp -d)"
|
|
||||||
trap 'rm -rf "$LOG_DIR"' EXIT
|
|
||||||
|
|
||||||
# ── Worker: run one suite, write result files ─────────────────────────────────
|
|
||||||
# Result files written to $LOG_DIR/<safe_label>/{status,output}
|
|
||||||
run_suite_worker() {
|
|
||||||
local label="$1"
|
|
||||||
local pkg="$2"
|
|
||||||
local safe_label="${label//\//_}"
|
|
||||||
local suite_dir="$LOG_DIR/$safe_label"
|
|
||||||
mkdir -p "$suite_dir"
|
|
||||||
|
|
||||||
# Check whether the package directory actually exists
|
|
||||||
local pkg_dir="${pkg//\.\//}"
|
|
||||||
pkg_dir="${pkg_dir%/...}"
|
|
||||||
if [[ ! -d "$REPO_ROOT/$pkg_dir" ]]; then
|
|
||||||
echo "skip" > "$suite_dir/status"
|
|
||||||
echo "(directory not found)" > "$suite_dir/output"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local exit_code=0
|
|
||||||
(
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
go test -timeout "$TIMEOUT" -count=1 "$pkg" 2>&1
|
|
||||||
) > "$suite_dir/output" 2>&1 || exit_code=$?
|
|
||||||
|
|
||||||
if [[ $exit_code -eq 0 ]]; then
|
|
||||||
echo "pass" > "$suite_dir/status"
|
|
||||||
elif grep -q "matched no packages\|no packages to test\|\[no test files\]" "$suite_dir/output" 2>/dev/null; then
|
|
||||||
echo "skip" > "$suite_dir/status"
|
|
||||||
echo "(no test files found)" > "$suite_dir/output"
|
|
||||||
else
|
|
||||||
echo "fail" > "$suite_dir/status"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
export -f run_suite_worker
|
|
||||||
export REPO_ROOT TIMEOUT LOG_DIR
|
|
||||||
|
|
||||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
printf "${BOLD}╔══════════════════════════════════════════════════════════════╗${RESET}\n"
|
|
||||||
printf "${BOLD}║ dws — Full Test Suite Runner ║${RESET}\n"
|
|
||||||
printf "${BOLD}╚══════════════════════════════════════════════════════════════╝${RESET}\n"
|
|
||||||
echo ""
|
|
||||||
printf "Repo root : %s\n" "$REPO_ROOT"
|
|
||||||
printf "Timeout : %s per suite\n" "$TIMEOUT"
|
|
||||||
printf "Parallel : %s jobs\n" "$MAX_JOBS"
|
|
||||||
printf "Verbose : %s\n" "$VERBOSE"
|
|
||||||
echo ""
|
|
||||||
printf "${BOLD}Launching %d suites in parallel…${RESET}\n" "${#SUITES[@]}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
START_TIME=$(date +%s)
|
|
||||||
|
|
||||||
# ── Dispatch all suites concurrently with a job-slot semaphore ────────────────
|
|
||||||
declare -a PIDS=()
|
|
||||||
declare -a PID_LABELS=()
|
|
||||||
active_jobs=0
|
|
||||||
|
|
||||||
for suite in "${SUITES[@]}"; do
|
|
||||||
label="${suite%%|*}"
|
|
||||||
pkg="${suite##*|}"
|
|
||||||
|
|
||||||
printf " ${CYAN}START${RESET} %s\n" "$label"
|
|
||||||
|
|
||||||
run_suite_worker "$label" "$pkg" &
|
|
||||||
PIDS+=($!)
|
|
||||||
PID_LABELS+=("$label")
|
|
||||||
active_jobs=$((active_jobs + 1))
|
|
||||||
|
|
||||||
# Throttle: wait for one slot to free up when at capacity
|
|
||||||
if [[ $active_jobs -ge $MAX_JOBS ]]; then
|
|
||||||
wait "${PIDS[$((${#PIDS[@]} - MAX_JOBS))]}" 2>/dev/null || true
|
|
||||||
active_jobs=$((active_jobs - 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Wait for all remaining background jobs
|
|
||||||
for pid in "${PIDS[@]}"; do
|
|
||||||
wait "$pid" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
|
|
||||||
END_TIME=$(date +%s)
|
|
||||||
ELAPSED=$((END_TIME - START_TIME))
|
|
||||||
|
|
||||||
# ── Collect results in original suite order ───────────────────────────────────
|
|
||||||
TOTAL=0
|
|
||||||
PASSED=0
|
|
||||||
FAILED=0
|
|
||||||
SKIPPED=0
|
|
||||||
|
|
||||||
declare -a FAILED_SUITES=()
|
|
||||||
declare -a PASSED_SUITES=()
|
|
||||||
declare -a SKIPPED_SUITES=()
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
printf "${BOLD}Results:${RESET}\n"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
for suite in "${SUITES[@]}"; do
|
|
||||||
label="${suite%%|*}"
|
|
||||||
safe_label="${label//\//_}"
|
|
||||||
suite_dir="$LOG_DIR/$safe_label"
|
|
||||||
status_file="$suite_dir/status"
|
|
||||||
output_file="$suite_dir/output"
|
|
||||||
|
|
||||||
TOTAL=$((TOTAL + 1))
|
|
||||||
|
|
||||||
if [[ ! -f "$status_file" ]]; then
|
|
||||||
# Worker never wrote a status — treat as failure
|
|
||||||
printf " ${RED}FAIL${RESET} %s (worker did not complete)\n" "$label"
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
FAILED_SUITES+=("$label")
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
status="$(cat "$status_file")"
|
|
||||||
|
|
||||||
case "$status" in
|
|
||||||
pass)
|
|
||||||
PASSED=$((PASSED + 1))
|
|
||||||
PASSED_SUITES+=("$label")
|
|
||||||
printf " ${GREEN}PASS${RESET} %s\n" "$label"
|
|
||||||
if $VERBOSE; then
|
|
||||||
sed 's/^/ /' "$output_file"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
skip)
|
|
||||||
SKIPPED=$((SKIPPED + 1))
|
|
||||||
SKIPPED_SUITES+=("$label")
|
|
||||||
printf " ${YELLOW}SKIP${RESET} %s %s\n" "$label" "$(cat "$output_file")"
|
|
||||||
;;
|
|
||||||
fail)
|
|
||||||
FAILED=$((FAILED + 1))
|
|
||||||
FAILED_SUITES+=("$label")
|
|
||||||
printf " ${RED}FAIL${RESET} %s\n" "$label"
|
|
||||||
echo " ── output ──────────────────────────────────────────────────────"
|
|
||||||
sed 's/^/ /' "$output_file"
|
|
||||||
echo " ────────────────────────────────────────────────────────────────"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
||||||
echo ""
|
|
||||||
printf "${BOLD}══════════════════════════════════════════════════════════════${RESET}\n"
|
|
||||||
printf "${BOLD} Summary (elapsed: %ds, parallel: %s jobs)${RESET}\n" "$ELAPSED" "$MAX_JOBS"
|
|
||||||
printf "${BOLD}══════════════════════════════════════════════════════════════${RESET}\n"
|
|
||||||
printf " Total : %d\n" "$TOTAL"
|
|
||||||
printf " ${GREEN}Passed : %d${RESET}\n" "$PASSED"
|
|
||||||
printf " ${RED}Failed : %d${RESET}\n" "$FAILED"
|
|
||||||
printf " ${YELLOW}Skipped : %d${RESET}\n" "$SKIPPED"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [[ ${#FAILED_SUITES[@]} -gt 0 ]]; then
|
|
||||||
printf "${RED}${BOLD}Failed suites:${RESET}\n"
|
|
||||||
for suite in "${FAILED_SUITES[@]}"; do
|
|
||||||
printf " ${RED}✗${RESET} %s\n" "$suite"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#SKIPPED_SUITES[@]} -gt 0 ]]; then
|
|
||||||
printf "${YELLOW}Skipped suites:${RESET}\n"
|
|
||||||
for suite in "${SKIPPED_SUITES[@]}"; do
|
|
||||||
printf " ${YELLOW}–${RESET} %s\n" "$suite"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#PASSED_SUITES[@]} -gt 0 ]]; then
|
|
||||||
printf "${GREEN}Passed suites:${RESET}\n"
|
|
||||||
for suite in "${PASSED_SUITES[@]}"; do
|
|
||||||
printf " ${GREEN}✓${RESET} %s\n" "$suite"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Exit code ─────────────────────────────────────────────────────────────────
|
|
||||||
if [[ $FAILED -gt 0 ]]; then
|
|
||||||
printf "${RED}${BOLD}Result: FAILED${RESET}\n\n"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
printf "${GREEN}${BOLD}Result: ALL PASSED${RESET}\n\n"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user