fix: align schema aliases and one-of coverage
This commit is contained in:
1 parent
ddad2f648c
commit
eec64bdf35
7 files changed
+269
-40
No files matched your search
@@ -80,6 +80,47 @@ func TestSchemaCoversVisibleProductLeafCommands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaHidesChatFileUploadCompatibilityAliases(t *testing.T) {
|
||||
root := NewRootCommand()
|
||||
var out, errOut bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&errOut)
|
||||
root.SetArgs([]string{"schema", "chat.upload_conversation_file", "--format", "json"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("schema Execute() error = %v\nstderr:\n%s", err, errOut.String())
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Parameters map[string]json.RawMessage `json:"parameters"`
|
||||
Constraints struct {
|
||||
RequireOneOf [][]string `json:"require_one_of"`
|
||||
} `json:"constraints"`
|
||||
}
|
||||
if err := json.Unmarshal(out.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("json.Unmarshal() error = %v\noutput:\n%s", err, out.String())
|
||||
}
|
||||
for _, name := range []string{"conversation-id", "id", "file-path"} {
|
||||
if _, ok := payload.Parameters[name]; ok {
|
||||
t.Fatalf("compatibility alias %q leaked into schema parameters", name)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"group", "user", "open-dingtalk-id", "file", "url"} {
|
||||
if _, ok := payload.Parameters[name]; !ok {
|
||||
t.Fatalf("canonical parameter %q missing from schema", name)
|
||||
}
|
||||
}
|
||||
wantGroups := map[string]bool{
|
||||
"group,user,open-dingtalk-id": true,
|
||||
"file,url": true,
|
||||
}
|
||||
for _, group := range payload.Constraints.RequireOneOf {
|
||||
delete(wantGroups, strings.Join(group, ","))
|
||||
}
|
||||
if len(wantGroups) != 0 {
|
||||
t.Fatalf("require_one_of missing groups: %#v", wantGroups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaProductIDsMatchRootHelpServices(t *testing.T) {
|
||||
root := NewRootCommand()
|
||||
helpProducts := map[string]bool{}
|
||||
|
||||
@@ -164,12 +164,12 @@ func newChatFileUploadCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
preferLegacyLeaf(cmd)
|
||||
cmd.Flags().String("group", "", "群聊 openConversationId(群聊时使用)")
|
||||
cmd.Flags().String("conversation-id", "", "--group 的别名")
|
||||
cmd.Flags().String("id", "", "--group 的别名")
|
||||
addChatFileHiddenStringFlag(cmd, "conversation-id", "--group 的别名")
|
||||
addChatFileHiddenStringFlag(cmd, "id", "--group 的别名")
|
||||
cmd.Flags().String("user", "", "单聊对方 userId(单聊时使用)")
|
||||
cmd.Flags().String("open-dingtalk-id", "", "单聊对方 openDingTalkId(单聊时使用)")
|
||||
cmd.Flags().String("file", "", "本地文件路径(与 --url 二选一)")
|
||||
cmd.Flags().String("file-path", "", "--file 的别名")
|
||||
addChatFileHiddenStringFlag(cmd, "file-path", "--file 的别名")
|
||||
cmd.Flags().String("url", "", "远程文件 URL(与 --file 二选一,服务端代传)")
|
||||
cmd.Flags().String("file-name", "", "文件名(可选)")
|
||||
cmd.Flags().String("md5", "", "文件 MD5(可选,本地不传自动计算)")
|
||||
@@ -189,6 +189,11 @@ func newChatFileUploadCommand(runner executor.Runner) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addChatFileHiddenStringFlag(cmd *cobra.Command, name, usage string) {
|
||||
cmd.Flags().String(name, "", usage)
|
||||
_ = cmd.Flags().MarkHidden(name)
|
||||
}
|
||||
|
||||
func chatConversationTargetArgs(cmd *cobra.Command) (map[string]any, error) {
|
||||
group := strings.TrimSpace(firstNonEmptyFlag(cmd, "group", "conversation-id", "id"))
|
||||
user := strings.TrimSpace(firstNonEmptyFlag(cmd, "user"))
|
||||
|
||||
@@ -3,6 +3,8 @@ package helpers
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -15,6 +17,41 @@ type captureRunner struct {
|
||||
last executor.Invocation
|
||||
}
|
||||
|
||||
func TestChatFileUploadCompatibilityAliasesRemainCallable(t *testing.T) {
|
||||
for _, name := range []string{"conversation-id", "id", "file-path"} {
|
||||
cmd := newChatFileUploadCommand(&captureRunner{})
|
||||
flag := cmd.Flags().Lookup(name)
|
||||
if flag == nil || !flag.Hidden {
|
||||
t.Fatalf("compatibility flag --%s must remain registered and hidden", name)
|
||||
}
|
||||
}
|
||||
|
||||
runner := &captureRunner{}
|
||||
cmd := newChatFileUploadCommand(runner)
|
||||
cmd.SetArgs([]string{
|
||||
"--conversation-id", "cid-alias",
|
||||
"--url", "https://example.com/report.pdf",
|
||||
"--file-name", "report.pdf",
|
||||
})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("conversation alias Execute() error = %v", err)
|
||||
}
|
||||
if got := runner.last.Params["openConversationId"]; got != "cid-alias" {
|
||||
t.Fatalf("openConversationId = %#v, want cid-alias", got)
|
||||
}
|
||||
|
||||
fixture := filepath.Join(t.TempDir(), "report.txt")
|
||||
if err := os.WriteFile(fixture, []byte("schema alias test\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
dryRun := newChatFileUploadCommand(&captureRunner{})
|
||||
dryRun.Flags().Bool("dry-run", false, "dry run")
|
||||
dryRun.SetArgs([]string{"--id", "cid-id-alias", "--file-path", fixture, "--dry-run"})
|
||||
if err := dryRun.Execute(); err != nil {
|
||||
t.Fatalf("id/file-path alias Execute() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *captureRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) {
|
||||
r.last = invocation
|
||||
return executor.Result{Invocation: invocation}, nil
|
||||
|
||||
@@ -1066,6 +1066,8 @@ func newDocBlockInsertCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("content-format", "", i18n.T("输入格式: element / jsonml"))
|
||||
cmd.Flags().String("parent-block", "", i18n.T("父容器块 ID(JSONML 场景透传)"))
|
||||
addDocJSONMLControlFlags(cmd)
|
||||
annotateFlagEnum(cmd, "type", "paragraph", "heading", "blockquote", "callout", "code", "orderedList", "unorderedList", "columns", "divider")
|
||||
annotateFlagExample(cmd, "type", "divider")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"text", "heading", "element", "type"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
@@ -1130,6 +1132,8 @@ func newDocBlockUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().Int("level", 1, i18n.T("标题级别"))
|
||||
cmd.Flags().String("content-format", "", i18n.T("输入格式: element / jsonml"))
|
||||
addDocJSONMLControlFlags(cmd)
|
||||
annotateFlagEnum(cmd, "type", "paragraph", "heading", "blockquote", "callout", "code", "orderedList", "unorderedList", "columns", "divider")
|
||||
annotateFlagExample(cmd, "type", "divider")
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{"text", "heading", "element", "type"}}, nil)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1111,6 +1111,7 @@ func newSheetCondFormatCreateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象")
|
||||
cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象")
|
||||
annotateFlagExample(cmd, "ranges", `["A1:B2"]`)
|
||||
annotateFlagExample(cmd, "condition", `{"numberCondition":{"operator":"greater","value1":"80"}}`)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1132,6 +1133,7 @@ func newSheetCondFormatUpdateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd.Flags().String("cell-style", "", "单元格样式 JSON 对象")
|
||||
cmd.Flags().String("data-bar-style", "", "数据条样式 JSON 对象")
|
||||
annotateFlagExample(cmd, "ranges", `["A1:B2"]`)
|
||||
annotateFlagExample(cmd, "condition", `{"numberCondition":{"operator":"greater","value1":"80"}}`)
|
||||
annotateFlagConstraints(cmd, nil, [][]string{{
|
||||
"ranges", "condition", "cell-style", "data-bar-style",
|
||||
}}, nil)
|
||||
@@ -2102,6 +2104,17 @@ func bindSheetStyleFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().String("font-weights-json", "", "字体粗细二维 JSON 数组")
|
||||
cmd.Flags().String("word-wrap", "", "换行方式")
|
||||
cmd.Flags().String("number-format", "", "数字格式 code")
|
||||
|
||||
annotateFlagEnum(cmd, "h-align", "left", "center", "right", "general")
|
||||
annotateFlagEnum(cmd, "v-align", "top", "middle", "bottom")
|
||||
annotateFlagEnum(cmd, "font-weight", "bold", "normal")
|
||||
annotateFlagEnum(cmd, "word-wrap", "overflow", "clip", "autoWrap")
|
||||
annotateFlagExample(cmd, "bg-colors-json", `[["#FFFFFF","#FFFFFF"],["#FFFFFF","#FFFFFF"]]`)
|
||||
annotateFlagExample(cmd, "font-sizes-json", `[[12,12],[12,12]]`)
|
||||
annotateFlagExample(cmd, "h-aligns-json", `[["left","left"],["left","left"]]`)
|
||||
annotateFlagExample(cmd, "v-aligns-json", `[["top","top"],["top","top"]]`)
|
||||
annotateFlagExample(cmd, "font-colors-json", `[["#000000","#000000"],["#000000","#000000"]]`)
|
||||
annotateFlagExample(cmd, "font-weights-json", `[["normal","normal"],["normal","normal"]]`)
|
||||
}
|
||||
|
||||
func sheetReadStyleSpec(cmd *cobra.Command) *sheetStyleSpec {
|
||||
|
||||
@@ -414,6 +414,10 @@ agentCode 配置:
|
||||
chmodCmd.Flags().BoolVar(&recommend, "recommend", false, "使用推荐 scope 集合批量授权;执行授权需 --yes")
|
||||
|
||||
cli.AnnotateRuntimeFlagEnum(chmodCmd, "grant-type", "once", "session", "permanent")
|
||||
cli.AnnotateRuntimeFlagExample(chmodCmd, "product", "calendar")
|
||||
cli.AnnotateRuntimeFlagExample(chmodCmd, "products", "calendar,aitable")
|
||||
cli.AnnotateRuntimeFlagExample(chmodCmd, "domain", "calendar")
|
||||
cli.AnnotateRuntimeFlagExample(chmodCmd, "domains", "calendar,chat")
|
||||
cli.AnnotateRuntimePositionals(chmodCmd, cli.RuntimeSchemaPositional{
|
||||
Name: "scope",
|
||||
Type: "array",
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
|
||||
The script treats `dws schema --all --format json` as the command inventory, expands
|
||||
each leaf schema, synthesizes flag values from parameter metadata, and executes
|
||||
the runnable CLI path with `--dry-run --yes --format json`.
|
||||
the runnable CLI path with `--dry-run --yes --format json`. Every available
|
||||
combination of `require_one_of` branches is exercised independently.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import concurrent.futures
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -26,6 +28,7 @@ from typing import Any
|
||||
@dataclass
|
||||
class SmokeResult:
|
||||
canonical_path: str
|
||||
case_name: str
|
||||
cli_path: str
|
||||
command: list[str]
|
||||
status: str
|
||||
@@ -74,13 +77,13 @@ def kebab_to_words(value: str) -> str:
|
||||
|
||||
|
||||
def scalar_value(flag: str, param: dict[str, Any], canonical_path: str) -> str:
|
||||
if param.get("example") not in (None, ""):
|
||||
return str(param["example"])
|
||||
|
||||
enum = param.get("enum") or []
|
||||
if enum:
|
||||
return str(enum[0])
|
||||
|
||||
if param.get("example") not in (None, ""):
|
||||
return str(param["example"])
|
||||
|
||||
value_format = str(param.get("format", "")).lower()
|
||||
if value_format in {"numeric-id", "positive-integer"}:
|
||||
return "1"
|
||||
@@ -182,6 +185,9 @@ def value_for(flag: str, param: dict[str, Any], canonical_path: str) -> str | No
|
||||
if canonical_path == "sheet.range_batch_set_style" and flag == "batch":
|
||||
return str(Path(tempfile.gettempdir()) / "dws-schema-smoke-style.json")
|
||||
|
||||
if param.get("example") not in (None, ""):
|
||||
return str(param["example"])
|
||||
|
||||
typ = str(param.get("type", "string")).lower()
|
||||
prop = str(param.get("property", ""))
|
||||
haystack = " ".join([kebab_to_words(flag), kebab_to_words(prop)])
|
||||
@@ -223,7 +229,43 @@ def constraint_groups(leaf: dict[str, Any], name: str) -> list[list[str]]:
|
||||
return [[str(item) for item in group] for group in groups if group]
|
||||
|
||||
|
||||
def selected_inputs(leaf: dict[str, Any], include_optional: bool) -> tuple[set[str], set[str]]:
|
||||
def available_one_of_groups(leaf: dict[str, Any]) -> list[list[str]]:
|
||||
params = leaf.get("parameters") or {}
|
||||
positional_names = {
|
||||
str(item.get("name"))
|
||||
for item in (leaf.get("positionals") or [])
|
||||
if item.get("name")
|
||||
}
|
||||
available = set(params) | positional_names
|
||||
return [
|
||||
[name for name in group if name in available]
|
||||
for group in constraint_groups(leaf, "require_one_of")
|
||||
if any(name in available for name in group)
|
||||
]
|
||||
|
||||
|
||||
def one_of_cases(leaf: dict[str, Any]) -> list[tuple[str, ...]]:
|
||||
groups = available_one_of_groups(leaf)
|
||||
if not groups:
|
||||
return [()]
|
||||
cases: list[tuple[str, ...]] = []
|
||||
seen: set[tuple[str, ...]] = set()
|
||||
mutually_exclusive = constraint_groups(leaf, "mutually_exclusive")
|
||||
for choices in itertools.product(*groups):
|
||||
selected = set(choices)
|
||||
if any(sum(name in selected for name in group) > 1 for group in mutually_exclusive):
|
||||
continue
|
||||
if choices not in seen:
|
||||
seen.add(choices)
|
||||
cases.append(choices)
|
||||
return cases or [()]
|
||||
|
||||
|
||||
def selected_inputs(
|
||||
leaf: dict[str, Any],
|
||||
include_optional: bool,
|
||||
one_of_choices: tuple[str, ...] = (),
|
||||
) -> tuple[set[str], set[str]]:
|
||||
params = leaf.get("parameters") or {}
|
||||
positionals = {
|
||||
str(item.get("name")): item
|
||||
@@ -251,12 +293,14 @@ def selected_inputs(leaf: dict[str, Any], include_optional: bool) -> tuple[set[s
|
||||
return True
|
||||
return False
|
||||
|
||||
for group in constraint_groups(leaf, "require_one_of"):
|
||||
if any(is_selected(name) for name in group):
|
||||
continue
|
||||
one_of_groups = available_one_of_groups(leaf)
|
||||
choices = one_of_choices or tuple(group[0] for group in one_of_groups)
|
||||
preferred = set(choices)
|
||||
for group, choice in zip(one_of_groups, choices):
|
||||
for name in group:
|
||||
if select(name):
|
||||
break
|
||||
selected_params.discard(name)
|
||||
selected_positionals.discard(name)
|
||||
select(choice)
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
@@ -270,20 +314,30 @@ def selected_inputs(leaf: dict[str, Any], include_optional: bool) -> tuple[set[s
|
||||
|
||||
for group in constraint_groups(leaf, "mutually_exclusive"):
|
||||
selected = [name for name in group if is_selected(name)]
|
||||
for name in selected[1:]:
|
||||
keep = next((name for name in selected if name in preferred), selected[0] if selected else "")
|
||||
for name in selected:
|
||||
if name == keep:
|
||||
continue
|
||||
selected_params.discard(name)
|
||||
selected_positionals.discard(name)
|
||||
|
||||
return selected_params, selected_positionals
|
||||
|
||||
|
||||
def build_command(binary: str, leaf: dict[str, Any], include_optional: bool) -> list[str]:
|
||||
def build_command(
|
||||
binary: str,
|
||||
leaf: dict[str, Any],
|
||||
include_optional: bool,
|
||||
one_of_choices: tuple[str, ...] = (),
|
||||
) -> list[str]:
|
||||
cli_path = str(leaf["cli_path"])
|
||||
canonical_path = str(leaf.get("canonical_path", ""))
|
||||
extra_flags = smoke_extra_flags(canonical_path)
|
||||
argv = [binary, *shlex.split(cli_path)]
|
||||
params = leaf.get("parameters") or {}
|
||||
selected_params, selected_positionals = selected_inputs(leaf, include_optional)
|
||||
selected_params, selected_positionals = selected_inputs(
|
||||
leaf, include_optional, one_of_choices
|
||||
)
|
||||
|
||||
positionals = sorted(
|
||||
(leaf.get("positionals") or []),
|
||||
@@ -326,24 +380,18 @@ def help_flags(binary: str, cwd: Path, cli_path: str, timeout: int) -> set[str]:
|
||||
return set(re.findall(r"--([a-zA-Z0-9][a-zA-Z0-9_.-]*)", proc.stdout))
|
||||
|
||||
|
||||
def run_one(binary: str, cwd: Path, path: str, timeout: int, include_optional: bool) -> SmokeResult:
|
||||
def run_smoke_case(
|
||||
binary: str,
|
||||
cwd: Path,
|
||||
path: str,
|
||||
leaf: dict[str, Any],
|
||||
timeout: int,
|
||||
include_optional: bool,
|
||||
choices: tuple[str, ...],
|
||||
) -> SmokeResult:
|
||||
case_name = "default" if not choices else "one_of:" + "+".join(choices)
|
||||
try:
|
||||
leaf = run_json([binary, "schema", path, "--format", "json"], cwd, timeout)
|
||||
cli_path = str(leaf.get("cli_path", ""))
|
||||
declared = set((leaf.get("parameters") or {}).keys())
|
||||
missing = sorted(declared - help_flags(binary, cwd, cli_path, timeout))
|
||||
if missing:
|
||||
return SmokeResult(
|
||||
canonical_path=path,
|
||||
cli_path=cli_path,
|
||||
command=[binary, *shlex.split(cli_path), "--help"],
|
||||
status="schema_flag_mismatch",
|
||||
exit_code=3,
|
||||
stdout="",
|
||||
stderr="",
|
||||
error="schema parameters missing from command help: " + ", ".join(missing),
|
||||
)
|
||||
command = build_command(binary, leaf, include_optional)
|
||||
command = build_command(binary, leaf, include_optional, choices)
|
||||
proc = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
@@ -355,6 +403,7 @@ def run_one(binary: str, cwd: Path, path: str, timeout: int, include_optional: b
|
||||
status = "pass" if proc.returncode == 0 else "fail"
|
||||
return SmokeResult(
|
||||
canonical_path=path,
|
||||
case_name=case_name,
|
||||
cli_path=str(leaf.get("cli_path", "")),
|
||||
command=command,
|
||||
status=status,
|
||||
@@ -365,6 +414,7 @@ def run_one(binary: str, cwd: Path, path: str, timeout: int, include_optional: b
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return SmokeResult(
|
||||
canonical_path=path,
|
||||
case_name=case_name,
|
||||
cli_path="",
|
||||
command=exc.cmd if isinstance(exc.cmd, list) else [str(exc.cmd)],
|
||||
status="timeout",
|
||||
@@ -376,6 +426,7 @@ def run_one(binary: str, cwd: Path, path: str, timeout: int, include_optional: b
|
||||
except Exception as exc: # noqa: BLE001 - record all smoke failures.
|
||||
return SmokeResult(
|
||||
canonical_path=path,
|
||||
case_name=case_name,
|
||||
cli_path="",
|
||||
command=[],
|
||||
status="error",
|
||||
@@ -386,9 +437,79 @@ def run_one(binary: str, cwd: Path, path: str, timeout: int, include_optional: b
|
||||
)
|
||||
|
||||
|
||||
def run_one(
|
||||
binary: str,
|
||||
cwd: Path,
|
||||
path: str,
|
||||
timeout: int,
|
||||
include_optional: bool,
|
||||
) -> list[SmokeResult]:
|
||||
try:
|
||||
leaf = run_json([binary, "schema", path, "--format", "json"], cwd, timeout)
|
||||
cli_path = str(leaf.get("cli_path", ""))
|
||||
declared = set((leaf.get("parameters") or {}).keys())
|
||||
missing = sorted(declared - help_flags(binary, cwd, cli_path, timeout))
|
||||
if missing:
|
||||
return [
|
||||
SmokeResult(
|
||||
canonical_path=path,
|
||||
case_name="schema",
|
||||
cli_path=cli_path,
|
||||
command=[binary, *shlex.split(cli_path), "--help"],
|
||||
status="schema_flag_mismatch",
|
||||
exit_code=3,
|
||||
stdout="",
|
||||
stderr="",
|
||||
error="schema parameters missing from command help: "
|
||||
+ ", ".join(missing),
|
||||
)
|
||||
]
|
||||
return [
|
||||
run_smoke_case(
|
||||
binary,
|
||||
cwd,
|
||||
path,
|
||||
leaf,
|
||||
timeout,
|
||||
include_optional,
|
||||
choices,
|
||||
)
|
||||
for choices in one_of_cases(leaf)
|
||||
]
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return [
|
||||
SmokeResult(
|
||||
canonical_path=path,
|
||||
case_name="schema",
|
||||
cli_path="",
|
||||
command=exc.cmd if isinstance(exc.cmd, list) else [str(exc.cmd)],
|
||||
status="timeout",
|
||||
exit_code=124,
|
||||
stdout=exc.stdout or "",
|
||||
stderr=exc.stderr or "",
|
||||
error=str(exc),
|
||||
)
|
||||
]
|
||||
except Exception as exc: # noqa: BLE001 - record all smoke failures.
|
||||
return [
|
||||
SmokeResult(
|
||||
canonical_path=path,
|
||||
case_name="schema",
|
||||
cli_path="",
|
||||
command=[],
|
||||
status="error",
|
||||
exit_code=1,
|
||||
stdout="",
|
||||
stderr="",
|
||||
error=str(exc),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def result_to_dict(result: SmokeResult) -> dict[str, Any]:
|
||||
return {
|
||||
"canonical_path": result.canonical_path,
|
||||
"case": result.case_name,
|
||||
"cli_path": result.cli_path,
|
||||
"command": result.command,
|
||||
"status": result.status,
|
||||
@@ -399,7 +520,7 @@ def result_to_dict(result: SmokeResult) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def write_markdown(results: list[SmokeResult], output: Path) -> None:
|
||||
def write_markdown(results: list[SmokeResult], output: Path, tool_count: int) -> None:
|
||||
counts: dict[str, int] = {}
|
||||
for result in results:
|
||||
counts[result.status] = counts.get(result.status, 0) + 1
|
||||
@@ -408,7 +529,8 @@ def write_markdown(results: list[SmokeResult], output: Path) -> None:
|
||||
"# Schema Command Smoke Results",
|
||||
"",
|
||||
f"- Generated: {datetime.now(timezone.utc).isoformat()}",
|
||||
f"- Total: {len(results)}",
|
||||
f"- Tools: {tool_count}",
|
||||
f"- Cases: {len(results)}",
|
||||
]
|
||||
for status in sorted(counts):
|
||||
lines.append(f"- {status}: {counts[status]}")
|
||||
@@ -418,8 +540,8 @@ def write_markdown(results: list[SmokeResult], output: Path) -> None:
|
||||
if not failures:
|
||||
lines.append("No failures.")
|
||||
else:
|
||||
lines.append("| canonical_path | cli_path | status | exit | issue |")
|
||||
lines.append("| --- | --- | --- | --- | --- |")
|
||||
lines.append("| canonical_path | case | cli_path | status | exit | issue |")
|
||||
lines.append("| --- | --- | --- | --- | --- | --- |")
|
||||
for result in failures:
|
||||
issue = result.error or result.stderr.strip() or result.stdout.strip()
|
||||
issue = issue.replace("\n", "<br>")
|
||||
@@ -430,6 +552,7 @@ def write_markdown(results: list[SmokeResult], output: Path) -> None:
|
||||
+ " | ".join(
|
||||
[
|
||||
f"`{result.canonical_path}`",
|
||||
f"`{result.case_name}`",
|
||||
f"`{result.cli_path}`",
|
||||
result.status,
|
||||
str(result.exit_code),
|
||||
@@ -469,14 +592,15 @@ def main() -> int:
|
||||
paths = [tool["canonical_path"] for product in listing["products"] for tool in product["tools"]]
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=args.jobs) as executor:
|
||||
results = list(
|
||||
result_groups = list(
|
||||
executor.map(
|
||||
lambda p: run_one(args.binary, cwd, p, args.timeout, args.include_optional),
|
||||
paths,
|
||||
)
|
||||
)
|
||||
results = [result for group in result_groups for result in group]
|
||||
|
||||
results.sort(key=lambda item: item.canonical_path)
|
||||
results.sort(key=lambda item: (item.canonical_path, item.case_name))
|
||||
|
||||
jsonl_path = Path(args.output_jsonl)
|
||||
jsonl_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -486,9 +610,10 @@ def main() -> int:
|
||||
|
||||
md_path = Path(args.output_md)
|
||||
md_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_markdown(results, md_path)
|
||||
write_markdown(results, md_path, len(paths))
|
||||
|
||||
failures = [result for result in results if result.status != "pass"]
|
||||
print(f"tools={len(paths)}")
|
||||
print(f"total={len(results)}")
|
||||
print(f"failures={len(failures)}")
|
||||
print(f"jsonl={jsonl_path}")
|
||||
|
||||
Reference in new issue
Block a user