Files
dingtalk-workspace-cli/test/cli/docs_compatibility_test.go
T

333 lines
7.6 KiB
Go

package cli_test
import (
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/spf13/cobra"
)
type docsFlagCase struct {
CommandPath string
Flag string
Source string
}
func TestDWSDocsCommandTreeCoverage(t *testing.T) {
docPaths, _, err := parseDWSDocsCompatibility()
if err != nil {
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
}
if len(docPaths) == 0 {
t.Skip("no command paths parsed from docs/dws (directory may not exist)")
}
index := buildCommandIndex(app.NewSchemaSourceRootCommand())
missing := make([]string, 0)
for _, path := range docPaths {
if _, ok := index[path]; ok {
continue
}
missing = append(missing, path)
}
if len(missing) > 0 {
sort.Strings(missing)
t.Fatalf("docs command paths missing in CLI (%d): %v", len(missing), headStrings(missing, 30))
}
}
func TestDWSDocsLocalFlagsCoverage(t *testing.T) {
docPaths, docFlags, err := parseDWSDocsCompatibility()
if err != nil {
t.Fatalf("parseDWSDocsCompatibility() error = %v", err)
}
if len(docFlags) == 0 {
t.Skip("no local flags parsed from docs/dws (directory may not exist)")
}
docLeafSet := leafCommandSet(docPaths)
index := buildCommandIndex(app.NewSchemaSourceRootCommand())
missing := make([]string, 0)
fallbackMatched := 0
explicitMatched := 0
for _, tc := range docFlags {
if _, isLeaf := docLeafSet[tc.CommandPath]; !isLeaf {
continue
}
cmd, ok := index[tc.CommandPath]
if !ok {
missing = append(missing, tc.CommandPath+" "+tc.Flag+" ("+tc.Source+")")
continue
}
if commandHasFlag(cmd, tc.Flag) {
explicitMatched++
continue
}
if commandHasJSONFallback(cmd) {
fallbackMatched++
continue
}
missing = append(missing, tc.CommandPath+" "+tc.Flag+" ("+tc.Source+")")
}
if len(missing) > 0 {
sort.Strings(missing)
t.Fatalf("docs local flags missing in CLI (%d): %v", len(missing), headStrings(missing, 40))
}
t.Logf("docs local flags checked=%d explicit=%d fallback_json_params=%d", len(docFlags), explicitMatched, fallbackMatched)
}
func parseDWSDocsCompatibility() ([]string, []docsFlagCase, error) {
skip := map[string]struct{}{
"README.md": {},
"quick-reference.md": {},
"scenario-guide.md": {},
"range-audit.md": {},
}
files, err := filepath.Glob(filepath.Join("..", "..", "docs", "dws", "*.md"))
if err != nil {
return nil, nil, err
}
commandSet := make(map[string]struct{})
flagCases := make([]docsFlagCase, 0)
flagSet := make(map[string]struct{})
for _, file := range files {
name := filepath.Base(file)
if _, ok := skip[name]; ok {
continue
}
paths, flags, parseErr := parseOneDocsFile(file)
if parseErr != nil {
return nil, nil, parseErr
}
for _, path := range paths {
commandSet[path] = struct{}{}
}
for _, fc := range flags {
key := fc.CommandPath + "|" + fc.Flag
if _, ok := flagSet[key]; ok {
continue
}
flagSet[key] = struct{}{}
flagCases = append(flagCases, fc)
}
}
paths := make([]string, 0, len(commandSet))
for path := range commandSet {
paths = append(paths, path)
}
sort.Strings(paths)
sort.Slice(flagCases, func(i, j int) bool {
if flagCases[i].CommandPath != flagCases[j].CommandPath {
return flagCases[i].CommandPath < flagCases[j].CommandPath
}
return flagCases[i].Flag < flagCases[j].Flag
})
return paths, flagCases, nil
}
func parseOneDocsFile(path string) ([]string, []docsFlagCase, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, err
}
lines := strings.Split(string(data), "\n")
paths := make([]string, 0)
flags := make([]docsFlagCase, 0)
pathSet := make(map[string]struct{})
flagRegex := regexp.MustCompile(`--[a-zA-Z0-9][a-zA-Z0-9-]*`)
inTree := false
currentCommand := ""
inLocalParams := false
inLocalTable := false
for idx, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "## 命令树") {
inTree = true
continue
}
if inTree && strings.HasPrefix(trimmed, "## ") {
inTree = false
}
if inTree {
if cmdPath, ok := extractInlineCode(trimmed); ok && strings.HasPrefix(cmdPath, "dws ") {
if _, exists := pathSet[cmdPath]; !exists {
pathSet[cmdPath] = struct{}{}
paths = append(paths, cmdPath)
}
}
}
if strings.HasPrefix(trimmed, "### `") {
currentCommand = ""
inLocalParams = false
inLocalTable = false
if cmdPath, ok := extractHeadingCommand(trimmed); ok {
currentCommand = cmdPath
}
continue
}
if currentCommand == "" {
continue
}
if strings.HasPrefix(trimmed, "本地参数:") {
inLocalParams = true
inLocalTable = false
continue
}
if inLocalParams && strings.Contains(trimmed, "无本地参数") {
inLocalParams = false
inLocalTable = false
continue
}
if !inLocalParams {
continue
}
if strings.HasPrefix(trimmed, "| 参数 |") {
inLocalTable = true
continue
}
if !inLocalTable {
continue
}
if strings.HasPrefix(trimmed, "| ---") {
continue
}
if !strings.HasPrefix(trimmed, "|") {
inLocalParams = false
inLocalTable = false
continue
}
cells := parseMarkdownRow(trimmed)
if len(cells) == 0 {
continue
}
for _, flag := range flagRegex.FindAllString(cells[0], -1) {
flags = append(flags, docsFlagCase{
CommandPath: currentCommand,
Flag: flag,
Source: filepath.Base(path) + ":" + strconv.Itoa(idx+1),
})
}
}
return paths, flags, nil
}
func extractInlineCode(line string) (string, bool) {
if !strings.Contains(line, "`") {
return "", false
}
start := strings.IndexByte(line, '`')
if start < 0 {
return "", false
}
end := strings.IndexByte(line[start+1:], '`')
if end < 0 {
return "", false
}
value := strings.TrimSpace(line[start+1 : start+1+end])
if value == "" {
return "", false
}
return value, true
}
func extractHeadingCommand(line string) (string, bool) {
if !strings.HasPrefix(line, "### `") {
return "", false
}
value, ok := extractInlineCode(line)
if !ok {
return "", false
}
if !strings.HasPrefix(value, "dws ") {
return "", false
}
return value, true
}
func parseMarkdownRow(row string) []string {
raw := strings.TrimSpace(row)
raw = strings.TrimPrefix(raw, "|")
raw = strings.TrimSuffix(raw, "|")
parts := strings.Split(raw, "|")
out := make([]string, 0, len(parts))
for _, part := range parts {
out = append(out, strings.TrimSpace(part))
}
return out
}
func buildCommandIndex(root *cobra.Command) map[string]*cobra.Command {
index := make(map[string]*cobra.Command)
var walk func(*cobra.Command)
walk = func(cmd *cobra.Command) {
for _, child := range cmd.Commands() {
path := "dws " + strings.TrimPrefix(child.CommandPath(), "dws ")
index[path] = child
walk(child)
}
}
walk(root)
return index
}
func commandHasFlag(cmd *cobra.Command, longFlag string) bool {
name := strings.TrimSpace(strings.TrimPrefix(longFlag, "--"))
if name == "" {
return false
}
return cmd.LocalFlags().Lookup(name) != nil || cmd.InheritedFlags().Lookup(name) != nil
}
func commandHasJSONFallback(cmd *cobra.Command) bool {
return commandHasFlag(cmd, "--json") && commandHasFlag(cmd, "--params")
}
func headStrings(values []string, max int) []string {
if len(values) <= max {
return values
}
return values[:max]
}
func leafCommandSet(paths []string) map[string]struct{} {
leaf := make(map[string]struct{}, len(paths))
for _, path := range paths {
leaf[path] = struct{}{}
}
for _, path := range paths {
prefix := path + " "
for _, other := range paths {
if path == other {
continue
}
if strings.HasPrefix(other, prefix) {
delete(leaf, path)
break
}
}
}
return leaf
}