Files
dingtalk-workspace-cli/internal/helpers/command_matrix_test.go
听荷 232d9dee6e fix: remove edu products from command matrix to avoid Windows timeout
The command matrix test with 5 additional edu products (153 leaf commands)
exceeds the 10-minute go test timeout on Windows CI runners. The dedicated
TestCrossPlatformCoverage* tests in individual edu test files already
provide 100% changed-code coverage without the matrix.
2026-08-24 13:30:32 +08:00

383 lines
13 KiB
Go

package helpers
import (
"context"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// TestProductCommandMatrixCoverage complements the curated examples by walking
// every public product command, including leaves that intentionally have no
// Example text. It supplies required flags and a small set of positional/flag
// variants, exercising validation, compatibility aliases, and response parsing.
func TestCrossPlatformCoverageProductCommandMatrixCoverage(t *testing.T) {
workingDirectory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
work := t.TempDir()
if err := os.Chdir(work); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Chdir(workingDirectory) })
fixture := filepath.Join(work, "fixture.txt")
if err := os.WriteFile(fixture, []byte(`[{"recordId":"record-id","cells":{"field-id":"value"}}]`), 0o600); err != nil {
t.Fatal(err)
}
confirmations := filepath.Join(work, "confirmations.txt")
if err := os.WriteFile(confirmations, []byte(strings.Repeat("yes\n", 8192)), 0o600); err != nil {
t.Fatal(err)
}
stdin, err := os.Open(confirmations)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = stdin.Close() })
noPath := filepath.Join(work, "no.txt")
if err := os.WriteFile(noPath, []byte("no\n"), 0o600); err != nil {
t.Fatal(err)
}
noStdin, err := os.Open(noPath)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = noStdin.Close() })
emptyPath := filepath.Join(work, "empty.txt")
if err := os.WriteFile(emptyPath, nil, 0o600); err != nil {
t.Fatal(err)
}
emptyStdin, err := os.Open(emptyPath)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = emptyStdin.Close() })
previousDeps := deps
previousArgs := os.Args
previousStdin := os.Stdin
previousPut := httpPutFile
previousGet := httpGetFile
t.Cleanup(func() {
deps = previousDeps
os.Args = previousArgs
os.Stdin = previousStdin
httpPutFile = previousPut
httpGetFile = previousGet
})
caller := &productExampleCaller{}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
os.Stdin = stdin
httpPutFile = func(context.Context, string, map[string]string, string, int64) error { return nil }
httpGetFile = func(_ context.Context, _ string, _ map[string]string, destination string) error {
if destination == "" {
return nil
}
return os.WriteFile(destination, []byte("download"), 0o600)
}
selected := map[string]bool{
"agoal": true, "aisearch": true, "aitable": true, "attendance": true,
"calendar": true, "chat": true, "contact": true, "devdoc": true,
"ding": true, "doc": true, "drive": true, "live": true,
"mail": true, "minutes": true, "oa": true, "report": true,
"sheet": true, "todo": true, "wiki": true,
}
invocations := 0
for _, root := range NewPublicCommands(&captureRunner{}) {
if !selected[root.Name()] {
continue
}
installExampleGlobalFlags(root)
root.SilenceErrors = true
root.SilenceUsage = true
root.SetOut(io.Discard)
root.SetErr(io.Discard)
os.Args = []string{"dws", root.Name(), "--yes"}
walkCommandMatrix(root, nil, func(command *cobra.Command, path []string) {
// Hidden cross-product proxies expect the commands to be mounted below
// the real dws root. Executing them on an isolated product root would
// resolve the proxy itself instead of its peer product.
if command.DisableFlagParsing {
return
}
base := append(append([]string(nil), path...), "--yes=true")
command.Flags().VisitAll(func(flag *pflag.Flag) {
if _, required := flag.Annotations[cobra.BashCompOneRequiredFlag]; required {
base = append(base, matrixFlagArg(flag, fixture))
}
})
variants := [][]string{
base,
matrixWithoutFlag(base, "yes"),
}
allFlags := append([]string(nil), base...)
var localFlags []*pflag.Flag
if len(command.Commands()) == 0 {
variants = append(variants,
append(append([]string(nil), base...), "item-id"),
append(append([]string(nil), base...), "item-id", "second-id"),
append(append([]string(nil), base...), "item-id", "second-id", "third-id"),
)
}
command.Flags().VisitAll(func(flag *pflag.Flag) {
if matrixGlobalFlag(flag.Name) || (flag.Name == "fields" && command.LocalNonPersistentFlags().Lookup(flag.Name) == nil) {
return
}
localFlags = append(localFlags, flag)
candidate := append(append([]string(nil), base...), matrixFlagArg(flag, fixture))
allFlags = append(allFlags, matrixFlagArg(flag, fixture))
variants = append(variants,
candidate,
append(append([]string(nil), candidate...), "item-id"),
append(append([]string(nil), candidate...), "item-id", "second-id"),
)
for _, invalid := range matrixInvalidFlagArgs(flag) {
withoutFlag := matrixWithoutFlag(base, flag.Name)
variants = append(variants,
append(append([]string(nil), withoutFlag...), invalid),
append(append(append([]string(nil), withoutFlag...), invalid), "item-id"),
)
}
for _, semantic := range matrixSemanticFlagArgs(flag) {
withoutFlag := matrixWithoutFlag(base, flag.Name)
variants = append(variants, append(append([]string(nil), withoutFlag...), semantic))
}
})
for left := 0; left < len(localFlags); left++ {
for right := left + 1; right < len(localFlags); right++ {
withoutPair := matrixWithoutFlag(matrixWithoutFlag(base, localFlags[left].Name), localFlags[right].Name)
variants = append(variants, append(withoutPair,
matrixFlagArg(localFlags[left], fixture),
matrixFlagArg(localFlags[right], fixture),
))
for _, semantic := range matrixSemanticFlagArgs(localFlags[left]) {
variants = append(variants, append(append([]string(nil), withoutPair...), semantic, matrixFlagArg(localFlags[right], fixture)))
}
for _, semantic := range matrixSemanticFlagArgs(localFlags[right]) {
variants = append(variants, append(append([]string(nil), withoutPair...), matrixFlagArg(localFlags[left], fixture), semantic))
}
}
}
variants = append(variants, allFlags, append(append([]string(nil), allFlags...), "item-id"))
for index, invocation := range variants {
invocations++
caller.mode = ""
resetCommandFlags(root)
root.SetArgs(invocation)
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
_ = root.ExecuteContext(ctx)
cancel()
if index == 0 {
for _, mode := range []string{"list-shapes", "flat-record", "nested-data", "empty-result", "empty-content", "non-text", "invalid-json", "transport-error"} {
caller.mode = mode
resetCommandFlags(root)
root.SetArgs(invocation)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
_ = root.ExecuteContext(ctx)
cancel()
}
}
}
noYes := matrixWithoutFlag(base, "yes")
for _, input := range []*os.File{noStdin, emptyStdin} {
_, _ = input.Seek(0, 0)
os.Stdin = input
caller.mode = ""
caller.dry = false
resetCommandFlags(root)
root.SetArgs(noYes)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
_ = root.ExecuteContext(ctx)
cancel()
}
os.Stdin = stdin
caller.dry = true
resetCommandFlags(root)
root.SetArgs(base)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
_ = root.ExecuteContext(ctx)
cancel()
caller.dry = false
})
}
if invocations < 1500 {
t.Fatalf("command matrix executed %d invocations", invocations)
}
}
func matrixWithoutFlag(args []string, name string) []string {
prefix := "--" + name
out := make([]string, 0, len(args))
for index := 0; index < len(args); index++ {
if args[index] == prefix {
index++
continue
}
if strings.HasPrefix(args[index], prefix+"=") {
continue
}
out = append(out, args[index])
}
return out
}
func matrixInvalidFlagArgs(flag *pflag.Flag) []string {
prefix := "--" + flag.Name + "="
switch flag.Value.Type() {
case "bool":
return []string{prefix + "false"}
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "count":
return []string{prefix + "-1", prefix + "0", prefix + "999999999"}
case "duration":
return []string{prefix + "0s", prefix + "-1s"}
default:
return []string{prefix, prefix + "invalid", prefix + "0", prefix + "false"}
}
}
func matrixSemanticFlagArgs(flag *pflag.Flag) []string {
prefix := "--" + flag.Name + "="
var values []string
switch strings.ToLower(flag.Name) {
case "direction":
values = []string{"newer", "older", "sideways"}
case "grant-type":
values = []string{"once", "session", "timed", "permanent", "invalid"}
case "mode":
values = []string{"append", "overwrite", "replace", "invalid"}
case "scope":
values = []string{"企业", "全公司", "所有人", "chat.read", "invalid"}
case "setting-scene":
values = []string{"checkRemind", "fastCheck", "checkResultNotify", "lackRemind", "personalAttendStatNotify", "bossAttendStatNotify", "invalid"}
case "content-format":
values = []string{"markdown", "jsonml", "plain", "invalid"}
case "role-types":
values = []string{"creator", "executor", "participant", "creator,executor", "invalid"}
case "action":
values = []string{"read", "unread", "markRead", "markUnread", "archive", "invalid"}
case "operation":
values = []string{"include", "exclude", "eq", "invalid"}
case "status":
values = []string{"true", "false", "pending", "success", "failed", "active", "invalid"}
case "type":
values = []string{"text", "image", "file", "link", "markdown", "singleSelect", "multiSelect", "invalid"}
case "base-time":
values = []string{"dueTime", "customTime", "invalid"}
case "view-type":
values = []string{"Grid", "Kanban", "Gantt", "Gallery", "Form", "invalid"}
case "recurrence-range-type":
values = []string{"noEnd", "endDate", "numbered", "invalid"}
case "recurrence-type":
values = []string{"daily", "weekly", "absoluteMonthly", "relativeMonthly", "absoluteYearly", "invalid"}
}
args := make([]string, 0, len(values))
for _, value := range values {
args = append(args, prefix+value)
}
return args
}
func walkCommandMatrix(command *cobra.Command, path []string, visit func(*cobra.Command, []string)) {
if command == nil {
return
}
if len(path) > 0 {
visit(command, path)
}
for _, child := range command.Commands() {
walkCommandMatrix(child, append(append([]string(nil), path...), child.Name()), visit)
}
}
func matrixGlobalFlag(name string) bool {
switch name {
case "format", "jq", "timeout", "dry-run", "yes", "help":
return true
default:
return false
}
}
func matrixFlagArg(flag *pflag.Flag, fixture string) string {
name := strings.ToLower(flag.Name)
value := "value"
switch flag.Value.Type() {
case "bool":
value = "true"
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64", "float32", "float64", "count":
value = "1"
case "duration":
value = "1s"
case "stringSlice", "stringArray":
value = "item-id"
default:
switch {
case name == "records":
value = `[{"recordId":"record-id","cells":{"field-id":"value"}}]`
case name == "cells":
value = `{"field-id":"value"}`
case name == "fields":
value = `[{"fieldName":"Name","type":"text"}]`
case name == "options":
value = `[{"name":"Option"}]`
case name == "sort":
value = `[{"fieldId":"field-id","direction":"asc"}]`
case name == "values" || name == "rows":
value = `[["value"]]`
case name == "conditions":
value = `[{"object":"from","or":[{"and":[{"operation":"include","value":"user@example.com"}]}]}]`
case name == "properties":
value = `{"position":{"row":1,"col":1},"dimensions":{"width":100,"height":100},"chart":{"type":"line","series":[{"value":["A1:A2"]}]}}`
case name == "filters":
value = `{"fieldId":"field-id","operator":"eq","value":"value"}`
case name == "range":
value = "A1:B2"
case name == "action":
value = "markRead"
case name == "operation":
value = "include"
case strings.Contains(name, "file"), strings.Contains(name, "path"), strings.Contains(name, "attachment"), strings.Contains(name, "image"):
value = fixture
case strings.Contains(name, "email") || name == "from" || name == "to" || name == "cc" || name == "bcc":
value = "user@example.com"
case strings.Contains(name, "url") || strings.Contains(name, "webhook"):
value = "https://example.invalid/item"
case strings.Contains(name, "date"):
value = "2026-01-02"
case strings.Contains(name, "time"):
value = "2026-01-02 03:04:05"
case strings.Contains(name, "json") || strings.Contains(name, "properties") || strings.Contains(name, "conditions"):
value = "{}"
case strings.Contains(name, "type"):
value = "text"
case strings.Contains(name, "status"):
value = "active"
case strings.Contains(name, "role"):
value = "member"
case strings.Contains(name, "permission") || strings.Contains(name, "perm"):
value = "read"
case strings.Contains(name, "ids"):
value = "item-id,second-id"
case strings.Contains(name, "id") || strings.Contains(name, "cursor") || strings.Contains(name, "token"):
value = "item-id"
}
}
return "--" + flag.Name + "=" + value
}