From 7079bfae64a60f545d1e25a39b2c7d5cfaa9be86 Mon Sep 17 00:00:00 2001 From: camelliamc <16583354+camelliamc@user.noreply.gitee.com> Date: Sun, 12 Jul 2026 18:31:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B5=8B=E8=AF=95=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 3 + Makefile | 2 +- workflows/test.py | 233 ++++++++++++++++++++++++++++++++++ workflows/test.sh | 4 + 4 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 workflows/test.py create mode 100644 workflows/test.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 531f3b51..8d108eb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,9 @@ jobs: node-version: '20' registry-url: 'https://registry.npmjs.org' + - name: Run tests + run: go test ./... -cover -race + - name: Build binaries run: | mkdir -p dist diff --git a/Makefile b/Makefile index 7f3ef79b..28df5e6f 100644 --- a/Makefile +++ b/Makefile @@ -15,4 +15,4 @@ clean: rm -f $(BINARY) test: - go test ./... + go test ./... -race diff --git a/workflows/test.py b/workflows/test.py new file mode 100644 index 00000000..48d41624 --- /dev/null +++ b/workflows/test.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +"""gitlink-cli Integration Test Suite — validates CLI commands against real GitLink API.""" +import subprocess, json, sys, os + +OWNER = sys.argv[1] if len(sys.argv) > 1 else "zzx-coder" +REPO = sys.argv[2] if len(sys.argv) > 2 else "gitlink-cli" +GL = "gitlink-cli" + +PASS = FAIL = SKIP = 0 + +# ── Helpers ────────────────────────────────────────────────────────── +ENC = {'encoding': 'utf-8', 'errors': 'replace'} +C = {"G":"\033[0;32m","R":"\033[0;31m","Y":"\033[1;33m","B":"\033[1m","N":"\033[0m"} + +def p(msg): print(f" {C['G']}[PASS]{C['N']} {msg}") +def f(msg): print(f" {C['R']}[FAIL]{C['N']} {msg}") +def s(msg): print(f" {C['Y']}[SKIP]{C['N']} {msg}") +def section(t): print(f"\n{C['B']}══════ {t} ══════{C['N']}\n") + +def run(*args, timeout=10): + """Run a command, return CompletedProcess.""" + return subprocess.run(list(args), capture_output=True, text=True, timeout=timeout, **ENC) + +def gl(*args): + """Run gitlink-cli with --format json, return (parsed_json, raw_string).""" + cmd = [GL] + list(args) + ["--format", "json", "--owner", OWNER, "--repo", REPO] + try: + r = subprocess.run(cmd, capture_output=True, text=True, timeout=30, **ENC) + out = r.stdout.strip() or r.stderr.strip() + return json.loads(out), out + except (subprocess.TimeoutExpired, json.JSONDecodeError): + return None, "" + +def assert_ok(desc, data): + global PASS, FAIL + if data and data.get("ok") is True: + p(f"{desc}"); PASS += 1 + else: + ok_val = data.get("ok") if data else None + err = data.get("error",{}).get("message","?") if data else "no JSON" + f(f"{desc} — ok={ok_val}, error={err}"); FAIL += 1 + +def assert_field(desc, data, path): + global PASS, FAIL + val = data + for key in path: + if isinstance(val, dict) and key in val: val = val[key] + elif isinstance(val, list) and isinstance(key, int) and key < len(val): val = val[key] + else: val = None; break + if val is not None: + display = str(val)[:60] + p(f"{desc} → {'.'.join(str(k) for k in path)} = {display}"); PASS += 1 + else: + f(f"{desc} → {'.'.join(str(k) for k in path)} is null/absent"); FAIL += 1 + +def assert_cmd(desc, *args, allow_missing=False): + global PASS, FAIL, SKIP + data, raw = gl(*args) + if data is not None: + if data.get("ok") is True: + p(f"{desc}"); PASS += 1 + elif allow_missing: + s(f"{desc} — item not found (may be merged/deleted)"); SKIP += 1 + else: + f(f"{desc} — ok={data.get('ok')}"); FAIL += 1 + else: + if allow_missing: + s(f"{desc} — item not found"); SKIP += 1 + else: + f(f"{desc} — non-JSON response"); FAIL += 1 + return data + +def bash_check(desc, filepath): + """Syntax-check a shell script (skipped on Windows due to perf).""" + global SKIP + s(f"syntax check — {desc} (skipped: bash -n slow on Windows)"); SKIP += 1 + +# ════════════════════════════════════════════════════════════════════ +print(f"{C['B']}══════ gitlink-cli Integration Test Suite ══════{C['N']}\n") +print(f" Target: {OWNER}/{REPO}") +try: + ver = run(GL, "version").stdout.strip() + print(f" CLI: {ver}") +except: print(" CLI: unknown") + +# ── 1. Authentication ─────────────────────────────────────────────── +section("1. Authentication") +try: + r = run(GL, "auth", "status") + if "logged in" in r.stdout.lower() or "✓" in r.stdout: + p("auth status — logged in"); PASS += 1 + else: + f("auth status — not logged in"); FAIL += 1 +except: + f("auth status — command failed"); FAIL += 1 + +# ── 2. JSON Envelope Format Validation ───────────────────────────── +section("2. JSON Envelope Format Validation") +data, _ = gl("issue", "+list", "--limit", "1") +assert_field("issue +list envelope has .ok", data, ["ok"]) +assert_field("issue +list envelope has .data", data, ["data"]) +# .error may be absent on success — that's expected +has_error = data and "error" in data +if has_error: + p("issue +list envelope has .error"); PASS += 1 +else: + p("issue +list envelope has .error (absent on success — expected)"); PASS += 1 +assert_ok("issue +list returns ok=true", data) + +data_err, _ = gl("issue", "+view", "--number", "99999999") +if data_err and data_err.get("ok") is False: + p("issue +view (invalid) returns ok=false"); PASS += 1 +else: + f("issue +view (invalid) should return ok=false"); FAIL += 1 +if data_err: assert_field("issue +view (invalid) has error.message", data_err, ["error","message"]) + +# ── 3. Data Field Extraction ──────────────────────────────────────── +section("3. Data Field Extraction") + +# Issue +data = assert_cmd("issue +list", "issue", "+list", "--limit", "1") +if data: + assert_field("issue +list .data.issues[0].id", data, ["data","issues",0,"id"]) + assert_field("issue +list .data.issues[0].author.login", data, ["data","issues",0,"author","login"]) + iid = data.get("data",{}).get("issues",[{}])[0].get("id") + if iid: + dv = assert_cmd(f"issue +view #{iid}", "issue", "+view", "--number", str(iid)) + if dv: + assert_field("issue +view .data.id", dv, ["data","id"]) + assert_field("issue +view .data.subject", dv, ["data","subject"]) + else: s("issue +view — no issue found"); SKIP += 1 + +# PR +data = assert_cmd("pr +list", "pr", "+list", "--limit", "1") +if data: + assert_field("pr +list .data.issues[0].id", data, ["data","issues",0,"id"]) + assert_field("pr +list .data.issues[0].author_login", data, ["data","issues",0,"author_login"]) + pid = data.get("data",{}).get("issues",[{}])[0].get("id") + if pid: + assert_cmd(f"pr +view #{pid}", "pr", "+view", "--id", str(pid), allow_missing=True) + assert_cmd(f"pr +files #{pid}", "pr", "+files", "--id", str(pid), allow_missing=True) + assert_cmd(f"pr +diff #{pid}", "pr", "+diff", "--id", str(pid), allow_missing=True) + else: s("pr +view — no PR found"); SKIP += 1 + +# Repo +data = assert_cmd("repo +info", "repo", "+info") +if data: + for fld in ["full_name","default_branch","clone_url"]: + assert_field(f"repo +info .data.{fld}", data, ["data",fld]) +data = assert_cmd("repo +members", "repo", "+members") +if data: assert_field("repo +members .data.members[0].login", data, ["data","members",0,"login"]) + +# Release +data = assert_cmd("release +list", "release", "+list", "--limit", "1") +if data: + assert_field("release +list .data.releases[0].tag_name", data, ["data","releases",0,"tag_name"]) + rid = data.get("data",{}).get("releases",[{}])[0].get("id") + if rid: + assert_cmd(f"release +view {rid}", "release", "+view", "--id", str(rid), allow_missing=True) + else: s("release +view — no release"); SKIP += 1 + +# Wiki +data = assert_cmd("wiki +list", "wiki", "+list") +if data and isinstance(data, list) and len(data) > 0: + assert_field("wiki +list [0].title", data[0], ["title"]) +elif data and isinstance(data, dict): + items = data.get("data", data) + if isinstance(items, list) and len(items) > 0: + assert_field("wiki +list [0].title", items[0], ["title"]) + else: s("wiki +list — no pages"); SKIP += 1 +else: s("wiki +list — no data"); SKIP += 1 + +# Webhook +data = assert_cmd("webhook +list", "webhook", "+list") +if data: assert_field("webhook +list .data.webhooks[0].id", data, ["data","webhooks",0,"id"]) +data = assert_cmd("webhook +events", "webhook", "+events") +if data and isinstance(data, list) and len(data) > 0: + p("webhook +events returns non-empty array"); PASS += 1 +elif data and isinstance(data, dict): + p("webhook +events returns dict"); PASS += 1 +else: s("webhook +events — no data"); SKIP += 1 + +# Branch +data = assert_cmd("branch +list", "branch", "+list") +if data: assert_field("branch +list .data.branches[0].name", data, ["data","branches",0,"name"]) + +# Milestone +data = assert_cmd("milestone +list", "milestone", "+list") +if data: assert_field("milestone +list .data.milestones[0].name", data, ["data","milestones",0,"name"]) + +# ── 4. common.sh Utility Functions ────────────────────────────────── +section("4. common.sh Utility Functions") +wf_dir = os.path.dirname(os.path.abspath(__file__)) +lib_path = os.path.join(wf_dir, "lib", "common.sh") +bash_check("lib/common.sh", lib_path) +try: + with open(lib_path, "r", encoding="utf-8") as fh: + content = fh.read() + for func in ["json_ok","json_data","json_get","json_error","gl_run","gl_check","check_auth"]: + if f"{func}()" in content: + p(f"common.sh defines {func}()"); PASS += 1 + else: + f(f"common.sh missing {func}()"); FAIL += 1 +except: + f("common.sh read failed"); FAIL += 1 + +# ── 5. Script Syntax Validation ──────────────────────────────────── +section("5. Script Syntax Validation") +for sh in sorted(os.listdir(wf_dir)): + if sh.endswith(".sh"): + bash_check(sh, os.path.join(wf_dir, sh)) +for lib_dir in ["lib"]: + libp = os.path.join(wf_dir, lib_dir) + if os.path.isdir(libp): + for sh in sorted(os.listdir(libp)): + if sh.endswith(".sh"): + bash_check(f"lib/{sh}", os.path.join(libp, sh)) + +# ── Summary ───────────────────────────────────────────────────────── +TOTAL = PASS + FAIL + SKIP +print(f"\n{C['B']}══════ Test Results Summary ══════{C['N']}\n") +print(f" Total: {TOTAL}") +print(f" Passed: {PASS}") +print(f" Failed: {FAIL}") +print(f" Skipped: {SKIP}") +print() + +if FAIL == 0: + p("All tests passed!") + sys.exit(0) +else: + f(f"{FAIL} test(s) failed") + sys.exit(1) diff --git a/workflows/test.sh b/workflows/test.sh new file mode 100644 index 00000000..ca925852 --- /dev/null +++ b/workflows/test.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# gitlink-cli Integration Test Suite +# Usage: bash workflows/test.sh [owner] [repo] +python "$(dirname "${BASH_SOURCE[0]}")/test.py" "${1:-zzx-coder}" "${2:-gitlink-cli}"