已开启
test: add pytest and cargo test #39
3stone创建于 6月8日
test: add pytest and cargo test #39
已开启
3stone创建于 6月8日
共 11 个文件变更+7492-7
@@ -0,0 +1,213 @@
1+#!/usr/bin/env python3
2+"""Run cargo test and write per-test results to CSV.
3+ 
4+Default behavior:
5+ python3 ci_project/cargo_test_to_csv.py
6+ 
7+Useful examples:
8+ python3 ci_project/cargo_test_to_csv.py -- -p daft-core --lib
9+ python3 ci_project/cargo_test_to_csv.py --output /tmp/results.csv -- --workspace
10+ 
11+The script requests libtest JSON output from nightly Rust and falls back to parsing
12+standard cargo test text lines when JSON test events are not available.
13+"""
14+ 
15+from __future__ import annotations
16+ 
17+import argparse
18+import csv
19+import json
20+import re
21+import subprocess
22+import sys
23+from pathlib import Path
24+ 
25+TEXT_TEST_RE = re.compile(r"^test (?P<name>.+?) \.\.\. (?P<result>ok|FAILED|ignored)$")
26+JSON_EVENTS = {"ok", "failed", "ignored"}
27+ 
28+ 
29+def split_module_and_name(full_name: str) -> tuple[str, str]:
30+ full_name = full_name.strip()
31+ if "::" not in full_name:
32+ return "", full_name
33+ module, name = full_name.rsplit("::", 1)
34+ return module, name
35+ 
36+ 
37+def normalize_result(result: str) -> str:
38+ result = result.strip()
39+ if result == "FAILED":
40+ return "failed"
41+ return result.lower()
42+ 
43+ 
44+def parse_json_event(line: str) -> tuple[str, str, str] | None:
45+ try:
46+ event = json.loads(line)
47+ except json.JSONDecodeError:
48+ return None
49+ 
50+ if event.get("type") != "test":
51+ return None
52+ 
53+ result = event.get("event")
54+ name = event.get("name")
55+ if result not in JSON_EVENTS or not isinstance(name, str):
56+ return None
57+ 
58+ module, short_name = split_module_and_name(name)
59+ return module, short_name, result
60+ 
61+ 
62+def parse_text_event(line: str) -> tuple[str, str, str] | None:
63+ match = TEXT_TEST_RE.match(line.strip())
64+ if not match:
65+ return None
66+ 
67+ module, short_name = split_module_and_name(match.group("name"))
68+ return module, short_name, normalize_result(match.group("result"))
69+ 
70+ 
71+ 
72+def csv_name(module: str, name: str) -> str:
73+ if module:
74+ return f"{module}::{name}"
75+ return name
76+ 
77+ 
78+def write_csv(path: Path, rows: list[tuple[str, str, str]]) -> None:
79+ path.parent.mkdir(parents=True, exist_ok=True)
80+ output_rows = sorted(
81+ ((csv_name(module, name), result) for module, name, result in rows),
82+ key=lambda row: row[0],
83+ reverse=True,
84+ )
85+ with path.open("w", newline="", encoding="utf-8") as file:
86+ writer = csv.writer(file)
87+ writer.writerow(["name", "result"])
88+ writer.writerows(output_rows)
89+ 
90+ 
91+def build_command(cargo_args: list[str], skip_tests: list[str]) -> list[str]:
92+ command = [
93+ "cargo",
94+ "test",
95+ *cargo_args,
96+ "--",
97+ "--format",
98+ "json",
99+ "-Z",
100+ "unstable-options",
101+ ]
102+ for test_name in skip_tests:
103+ command.extend(["--skip", test_name])
104+ return command
105+ 
106+ 
107+def main() -> int:
108+ parser = argparse.ArgumentParser(
109+ description="Run cargo test and write module,name,result rows to CSV."
110+ )
111+ parser.add_argument(
112+ "--output",
113+ default="target/cargo-test-results.csv",
114+ help="CSV output path. Defaults to target/cargo-test-results.csv.",
115+ )
116+ parser.add_argument(
117+ "--no-stream",
118+ action="store_true",
119+ help="Do not mirror cargo test output to stdout while parsing.",
120+ )
121+ parser.add_argument(
122+ "--log-output",
123+ help="Optional path for the combined cargo test stdout/stderr log.",
124+ )
125+ parser.add_argument(
126+ "--skip-test",
127+ action="append",
128+ default=[],
129+ help="Test name pattern to pass to libtest as --skip. Can be repeated.",
130+ )
131+ parser.add_argument(
132+ "--exit-code-output",
133+ help="Optional path to write cargo test's raw process exit code.",
134+ )
135+ parser.add_argument(
136+ "--ignore-cargo-exit-code",
137+ action="store_true",
138+ help="Return 0 after writing the CSV, even if cargo test returned non-zero.",
139+ )
140+ parser.add_argument(
141+ "cargo_args",
142+ nargs=argparse.REMAINDER,
143+ help="Arguments passed to cargo test. Prefix them with --.",
144+ )
145+ args = parser.parse_args()
146+ 
147+ cargo_args = args.cargo_args
148+ if cargo_args and cargo_args[0] == "--":
149+ cargo_args = cargo_args[1:]
150+ 
151+ command = build_command(cargo_args, args.skip_test)
152+ print("running:", " ".join(command), flush=True)
153+ 
154+ process = subprocess.Popen(
155+ command,
156+ stdout=subprocess.PIPE,
157+ stderr=subprocess.STDOUT,
158+ text=True,
159+ encoding="utf-8",
160+ errors="replace",
161+ bufsize=1,
162+ )
163+ 
164+ json_rows: list[tuple[str, str, str]] = []
165+ text_rows: list[tuple[str, str, str]] = []
166+ log_file = None
167+ if args.log_output:
168+ log_path = Path(args.log_output)
169+ log_path.parent.mkdir(parents=True, exist_ok=True)
170+ log_file = log_path.open("w", encoding="utf-8")
171+ 
172+ assert process.stdout is not None
173+ 
174+ for line in process.stdout:
175+ if log_file is not None:
176+ log_file.write(line)
177+ if not args.no_stream:
178+ print(line, end="")
179+ 
180+ json_event = parse_json_event(line)
181+ if json_event is not None:
182+ json_rows.append(json_event)
183+ continue
184+ 
185+ text_event = parse_text_event(line)
186+ if text_event is not None:
187+ text_rows.append(text_event)
188+ 
189+ return_code = process.wait()
190+ if log_file is not None:
191+ log_file.close()
192+ if args.exit_code_output:
193+ exit_code_path = Path(args.exit_code_output)
194+ exit_code_path.parent.mkdir(parents=True, exist_ok=True)
195+ exit_code_path.write_text(f"{return_code}\n", encoding="utf-8")
196+ rows = json_rows if json_rows else text_rows
197+ output = Path(args.output)
198+ write_csv(output, rows)
199+ 
200+ print(f"wrote {len(rows)} test result rows to {output}", flush=True)
201+ if not rows:
202+ print(
203+ "warning: no test result rows were parsed; check cargo output for build or harness errors.",
204+ file=sys.stderr,
205+ flush=True,
206+ )
207+ if args.ignore_cargo_exit_code:
208+ return 0
209+ return return_code
210+ 
211+ 
212+if __name__ == "__main__":
213+ raise SystemExit(main())
@@ -0,0 +1,85 @@
1+#!/usr/bin/env python3
2+"""Compare two cargo test result CSV files.
3+ 
4+Print tests that were ok in origin_csv and failed in current_csv.
5+Returns 1 when any regression is found, otherwise 0.
6+ 
7+Supported CSV formats:
8+ name,result
9+ module,name,result
10+"""
11+ 
12+from __future__ import annotations
13+ 
14+import argparse
15+import csv
16+import sys
17+from pathlib import Path
18+ 
19+SUCCESS = "ok"
20+FAILURE = "failed"
21+ 
22+ 
23+def full_name(row: dict[str, str]) -> str:
24+ name = (row.get("name") or "").strip()
25+ module = (row.get("module") or "").strip()
26+ if module and "::" not in name:
27+ return f"{module}::{name}"
28+ return name
29+ 
30+ 
31+def read_results(path: Path) -> dict[str, str]:
32+ with path.open(newline="", encoding="utf-8") as file:
33+ reader = csv.DictReader(file)
34+ if reader.fieldnames is None:
35+ raise ValueError(f"{path} is empty or missing a CSV header")
36+ 
37+ required = {"name", "result"}
38+ missing = required - set(reader.fieldnames)
39+ if missing:
40+ missing_fields = ", ".join(sorted(missing))
41+ raise ValueError(f"{path} is missing required column(s): {missing_fields}")
42+ 
43+ results: dict[str, str] = {}
44+ for row in reader:
45+ name = full_name(row)
46+ result = (row.get("result") or "").strip().lower()
47+ if not name:
48+ continue
49+ results[name] = result
50+ return results
51+ 
52+ 
53+def main() -> int:
54+ parser = argparse.ArgumentParser(
55+ description="Print tests that changed from ok in origin_csv to failed in current_csv."
56+ )
57+ parser.add_argument("origin_csv", help="Baseline CSV path")
58+ parser.add_argument("current_csv", help="Current CSV path")
59+ args = parser.parse_args()
60+ 
61+ try:
62+ origin = read_results(Path(args.origin_csv))
63+ current = read_results(Path(args.current_csv))
64+ except OSError as error:
65+ print(f"error: {error}", file=sys.stderr)
66+ return 2
67+ except ValueError as error:
68+ print(f"error: {error}", file=sys.stderr)
69+ return 2
70+ 
71+ regressions = sorted(
72+ name
73+ for name, origin_result in origin.items()
74+ if origin_result == SUCCESS and current.get(name) == FAILURE
75+ )
76+ 
77+ print(f"Found {len(regressions)} regression(s):")
78+ for name in regressions:
79+ print(name)
80+ 
81+ return 1 if regressions else 0
82+ 
83+ 
84+if __name__ == "__main__":
85+ raise SystemExit(main())
@@ -0,0 +1,83 @@
1+#!/usr/bin/env python3
2+"""Compare pytest result CSV files and print tests that stopped passing."""
3+ 
4+from __future__ import annotations
5+ 
6+import argparse
7+import csv
8+import sys
9+from pathlib import Path
10+ 
11+SUCCESS = "ok"
12+ 
13+ 
14+def full_name(row: dict[str, str]) -> str:
15+ name = (row.get("name") or "").strip()
16+ module = (row.get("module") or "").strip()
17+ if module and "::" not in name:
18+ return f"{module}::{name}"
19+ return name
20+ 
21+ 
22+def read_results(path: Path) -> dict[str, str]:
23+ with path.open(newline="", encoding="utf-8") as file:
24+ reader = csv.DictReader(file)
25+ if reader.fieldnames is None:
26+ raise ValueError(f"{path} is empty or missing a CSV header")
27+ missing = {"name", "result"} - set(reader.fieldnames)
28+ if missing:
29+ missing_fields = ", ".join(sorted(missing))
30+ raise ValueError(f"{path} is missing required column(s): {missing_fields}")
31+ 
32+ results: dict[str, str] = {}
33+ for row in reader:
34+ name = full_name(row)
35+ result = (row.get("result") or "").strip().lower()
36+ if name:
37+ results[name] = result
38+ return results
39+ 
40+ 
41+def find_degraded_tests(origin_csv: Path, current_csv: Path) -> list[str]:
42+ origin = read_results(origin_csv)
43+ current = read_results(current_csv)
44+ return sorted(
45+ name
46+ for name, origin_result in origin.items()
47+ if origin_result == SUCCESS and current.get(name) != SUCCESS
48+ )
49+ 
50+ 
51+def main() -> int:
52+ parser = argparse.ArgumentParser(
53+ description="Print tests that are ok in origin_csv but not ok in current_csv."
54+ )
55+ parser.add_argument("origin_csv", help="Baseline CSV path")
56+ parser.add_argument("current_csv", help="Current CSV path")
57+ parser.add_argument("--degraded-output", help="Optional path to write degraded test names")
58+ args = parser.parse_args()
59+ 
60+ try:
61+ degraded_tests = find_degraded_tests(Path(args.origin_csv), Path(args.current_csv))
62+ except OSError as error:
63+ print(f"error: {error}", file=sys.stderr)
64+ return 2
65+ except ValueError as error:
66+ print(f"error: {error}", file=sys.stderr)
67+ return 2
68+ 
69+ 
70+ print(f"Found {len(degraded_tests)} degraded test(s):")
71+ for name in degraded_tests:
72+ print(name)
73+ 
74+ if args.degraded_output:
75+ output = Path(args.degraded_output)
76+ output.parent.mkdir(parents=True, exist_ok=True)
77+ output.write_text("\n".join(degraded_tests) + ("\n" if degraded_tests else ""), encoding="utf-8")
78+ 
79+ return 1 if degraded_tests else 0
80+ 
81+ 
82+if __name__ == "__main__":
83+ raise SystemExit(main())
@@ -0,0 +1,72 @@
1+from __future__ import annotations
2+ 
3+import csv
4+import os
5+from pathlib import Path
6+from typing import Any
7+ 
8+RESULT_OK = "ok"
9+RESULT_IGNORED = "ignored"
10+RESULT_FAILED = "failed"
11+RESULT_RANK = {RESULT_OK: 0, RESULT_IGNORED: 1, RESULT_FAILED: 2}
12+ 
13+ 
14+def _split_nodeid(nodeid: str) -> tuple[str, str]:
15+ path_part, *name_parts = nodeid.split("::")
16+ module = os.environ.get("PYTEST_CSV_MODULE_OVERRIDE")
17+ if not module:
18+ module = path_part[:-3] if path_part.endswith(".py") else path_part
19+ module = module.replace("/", ".").replace("\\", ".")
20+ name = "::".join(name_parts) if name_parts else path_part
21+ return module, name
22+ 
23+ 
24+def _merge_result(old: str | None, new: str) -> str:
25+ if old is None:
26+ return new
27+ return new if RESULT_RANK[new] > RESULT_RANK[old] else old
28+ 
29+ 
30+class CsvResultPlugin:
31+ def __init__(self, output: str) -> None:
32+ self.output = output
33+ self.results: dict[str, tuple[str, str, str]] = {}
34+ 
35+ def pytest_collectreport(self, report: Any) -> None:
36+ if report.failed:
37+ module, name = _split_nodeid(report.nodeid)
38+ self.results[report.nodeid] = (module, name, RESULT_FAILED)
39+ 
40+ def pytest_runtest_logreport(self, report: Any) -> None:
41+ if report.when not in {"setup", "call", "teardown"}:
42+ return
43+ 
44+ if report.failed:
45+ result = RESULT_FAILED
46+ elif report.skipped:
47+ result = RESULT_IGNORED
48+ elif report.when == "call" and report.passed:
49+ result = RESULT_OK
50+ else:
51+ return
52+ 
53+ module, name = _split_nodeid(report.nodeid)
54+ previous = self.results.get(report.nodeid)
55+ merged = _merge_result(previous[2] if previous else None, result)
56+ self.results[report.nodeid] = (module, name, merged)
57+ 
58+ def pytest_sessionfinish(self, session: Any, exitstatus: int) -> None:
59+ path = Path(self.output)
60+ path.parent.mkdir(parents=True, exist_ok=True)
61+ rows = sorted(self.results.values(), key=lambda row: (row[0], row[1]))
62+ with path.open("w", newline="", encoding="utf-8") as file:
63+ writer = csv.writer(file)
64+ writer.writerow(["module", "name", "result"])
65+ writer.writerows(rows)
66+ print(f"wrote {len(rows)} pytest result rows to {path}", flush=True)
67+ 
68+ 
69+def pytest_configure(config: Any) -> None:
70+ output = os.environ.get("PYTEST_CSV_OUTPUT")
71+ if output:
72+ config.pluginmanager.register(CsvResultPlugin(output), "pytest-csv-result-plugin")
@@ -0,0 +1,169 @@
1+#!/usr/bin/env python3
2+"""Run Daft pytest tests and write module/name/result rows to CSV."""
3+ 
4+from __future__ import annotations
5+ 
6+import argparse
7+import csv
8+import os
9+import subprocess
10+import sys
11+from pathlib import Path
12+from tempfile import TemporaryDirectory
13+ 
14+REPO_DIR = Path(__file__).resolve().parents[1]
15+TESTS_DIR = REPO_DIR / "tests"
16+ 
17+ 
18+def latest_wheel(wheel_dir: Path) -> Path:
19+ wheels = sorted(wheel_dir.glob("*.whl"), key=lambda path: path.stat().st_mtime, reverse=True)
20+ if not wheels:
21+ raise FileNotFoundError(f"no .whl files found in {wheel_dir}")
22+ return wheels[0]
23+ 
24+ 
25+def install_wheel(wheel: Path) -> None:
26+ command = [sys.executable, "-m", "pip", "install", "--force-reinstall", "--no-deps", str(wheel)]
27+ print("installing wheel:", " ".join(command), flush=True)
28+ subprocess.run(command, check=True)
29+ 
30+ 
31+def normalize_pytest_args(pytest_args: list[str]) -> list[str]:
32+ if pytest_args and pytest_args[0] == "--":
33+ pytest_args = pytest_args[1:]
34+ return pytest_args
35+ 
36+ 
37+def module_name_for_root_test_file(path: Path) -> str:
38+ stem = path.stem
39+ return stem[5:] if stem.startswith("test_") else stem
40+ 
41+ 
42+def test_targets() -> list[tuple[Path, str]]:
43+ directory_targets = [
44+ (path, path.name)
45+ for path in TESTS_DIR.iterdir()
46+ if path.is_dir() and not path.name.startswith(".") and not path.name.startswith("__")
47+ ]
48+ file_targets = [
49+ (path, module_name_for_root_test_file(path))
50+ for path in TESTS_DIR.glob("test*.py")
51+ if path.is_file()
52+ ]
53+ return sorted([*directory_targets, *file_targets], key=lambda item: (item[1], str(item[0])))
54+ 
55+ 
56+def base_env(args: argparse.Namespace, output: Path, module_override: str | None = None) -> dict[str, str]:
57+ env = os.environ.copy()
58+ env["DAFT_RUNNER"] = args.runner
59+ env["PYTEST_CSV_OUTPUT"] = str(output)
60+ env["PYTHONPATH"] = f"{REPO_DIR}{os.pathsep}{env.get('PYTHONPATH', '')}" if env.get("PYTHONPATH") else str(REPO_DIR)
61+ if module_override:
62+ env["PYTEST_CSV_MODULE_OVERRIDE"] = module_override
63+ else:
64+ env.pop("PYTEST_CSV_MODULE_OVERRIDE", None)
65+ return env
66+ 
67+ 
68+def pytest_command(args: argparse.Namespace, pytest_args: list[str]) -> list[str]:
69+ command = [sys.executable, "-m", "pytest", "-p", "ci_project.pytest_csv_plugin", *pytest_args]
70+ if not args.no_importlib:
71+ command.insert(3, "--import-mode=importlib")
72+ return command
73+ 
74+ 
75+def run_one_pytest(args: argparse.Namespace, pytest_args: list[str], output: Path, module_override: str | None = None) -> int:
76+ command = pytest_command(args, pytest_args)
77+ print("running pytest:", " ".join(command), flush=True)
78+ completed = subprocess.run(command, cwd=REPO_DIR, env=base_env(args, output, module_override))
79+ return completed.returncode
80+ 
81+ 
82+def read_result_rows(path: Path) -> list[list[str]]:
83+ if not path.exists():
84+ return []
85+ with path.open(newline="", encoding="utf-8") as file:
86+ reader = csv.reader(file)
87+ header = next(reader, None)
88+ if header != ["module", "name", "result"]:
89+ raise ValueError(f"unexpected CSV header in {path}: {header}")
90+ return [row for row in reader]
91+ 
92+ 
93+def write_result_rows(path: Path, rows: list[list[str]]) -> None:
94+ path.parent.mkdir(parents=True, exist_ok=True)
95+ with path.open("w", newline="", encoding="utf-8") as file:
96+ writer = csv.writer(file)
97+ writer.writerow(["module", "name", "result"])
98+ writer.writerows(sorted(rows, key=lambda row: (row[0], row[1], row[2])))
99+ print(f"wrote {len(rows)} pytest result rows to {path}", flush=True)
100+ 
101+ 
102+def run_by_test_dir(args: argparse.Namespace, current_csv: Path, exit_code_output: Path) -> int:
103+ rows: list[list[str]] = []
104+ exit_codes: list[int] = []
105+ targets = test_targets()
106+ 
107+ if not targets:
108+ write_result_rows(current_csv, rows)
109+ exit_code_output.write_text("5\n", encoding="utf-8")
110+ print(f"no test targets found under {TESTS_DIR}", file=sys.stderr)
111+ return 5 if args.fail_on_pytest_error else 0
112+ 
113+ with TemporaryDirectory(prefix="daft-pytest-csv-") as tmpdir_text:
114+ tmpdir = Path(tmpdir_text)
115+ for test_target, module in targets:
116+ module_csv = tmpdir / f"{module}.csv"
117+ exit_code = run_one_pytest(args, [str(test_target.relative_to(REPO_DIR))], module_csv, module)
118+ target_rows = read_result_rows(module_csv)
119+ rows.extend(target_rows)
120+ exit_codes.append(0 if exit_code == 5 and not target_rows else exit_code)
121+ 
122+ write_result_rows(current_csv, rows)
123+ combined_exit_code = next((code for code in exit_codes if code != 0), 0)
124+ exit_code_output.write_text(f"{combined_exit_code}\n", encoding="utf-8")
125+ return combined_exit_code if args.fail_on_pytest_error else 0
126+ 
127+ 
128+def run_pytest_to_csv(args: argparse.Namespace) -> int:
129+ pytest_args = normalize_pytest_args(args.pytest_args)
130+ 
131+ if args.install_wheel or args.wheel:
132+ wheel = Path(args.wheel).resolve() if args.wheel else latest_wheel((REPO_DIR / args.wheel_dir).resolve())
133+ install_wheel(wheel)
134+ 
135+ current_csv = Path(args.current_csv)
136+ exit_code_output = Path(args.pytest_exit_code_output)
137+ current_csv.parent.mkdir(parents=True, exist_ok=True)
138+ exit_code_output.parent.mkdir(parents=True, exist_ok=True)
139+ 
140+ if args.by_test_dir or not pytest_args:
141+ return run_by_test_dir(args, current_csv, exit_code_output)
142+ 
143+ exit_code = run_one_pytest(args, pytest_args, current_csv)
144+ exit_code_output.write_text(f"{exit_code}\n", encoding="utf-8")
145+ return exit_code if args.fail_on_pytest_error else 0
146+ 
147+ 
148+def build_parser() -> argparse.ArgumentParser:
149+ parser = argparse.ArgumentParser(description="Run pytest and write a module/name/result CSV.")
150+ parser.add_argument("--current-csv", "--output", "-o", default="target/pytest-results.csv", help="Current pytest result CSV path")
151+ parser.add_argument("--pytest-exit-code-output", default="target/pytest-exit-code.txt", help="Path to write pytest's raw exit code")
152+ parser.add_argument("--by-test-dir", action="store_true", help="Run pytest once for each first-level directory and root test*.py file under tests/")
153+ parser.add_argument("--wheel", help="Wheel file to install before testing")
154+ parser.add_argument("--wheel-dir", default="target/wheels", help="Directory used to find the newest wheel")
155+ parser.add_argument("--install-wheel", action="store_true", help="Install the newest wheel before running pytest")
156+ parser.add_argument("--no-install-wheel", action="store_true", help=argparse.SUPPRESS)
157+ parser.add_argument("--runner", default="native", choices=("native", "ray"), help="DAFT_RUNNER value for pytest")
158+ parser.add_argument("--no-importlib", action="store_true", help="Do not add --import-mode=importlib to pytest")
159+ parser.add_argument("--fail-on-pytest-error", action="store_true", help="Return pytest's raw exit code instead of always returning 0")
160+ parser.add_argument("pytest_args", nargs=argparse.REMAINDER, help="Arguments passed to pytest. Prefix with -- when needed.")
161+ return parser
162+ 
163+ 
164+def main() -> int:
165+ return run_pytest_to_csv(build_parser().parse_args())
166+ 
167+ 
168+if __name__ == "__main__":
169+ raise SystemExit(main())
@@ -0,0 +1,96 @@
1+#!/usr/bin/env bash
2+set -u
3+ 
4+SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
5+REPO_DIR="$(dirname "$SCRIPT_PATH")/.."
6+CONDA_ENV="git-bot"
7+OUTPUT="cargo-test-results.csv"
8+LOG_OUTPUT="cargo-test-output.log"
9+EXIT_CODE_OUTPUT="cargo-test-exit-code.txt"
10+SKIP_HF_TEST=1
11+IGNORE_CARGO_EXIT_CODE=1
12+export DAFT_DASHBOARD_SKIP_BUILD="${DAFT_DASHBOARD_SKIP_BUILD:-1}"
13+ 
14+usage() {
15+ cat <<'USAGE'
16+Usage:
17+ ci_project/run_cargo_test_to_csv.sh [--output PATH] [--log-output PATH] [--exit-code-output PATH] [--include-hf-test] [--fail-on-cargo-error] [cargo test args...]
18+ 
19+Examples:
20+ ci_project/run_cargo_test_to_csv.sh
21+ ci_project/run_cargo_test_to_csv.sh --output /tmp/results.csv -p daft-core --lib
22+ ci_project/run_cargo_test_to_csv.sh --include-hf-test --workspace
23+ 
24+The CSV columns are: name,result
25+By default this script returns 0 after writing CSV and records cargo's raw exit code separately.
26+USAGE
27+}
28+ 
29+while [[ $# -gt 0 ]]; do
30+ case "$1" in
31+ --output)
32+ if [[ $# -lt 2 ]]; then
33+ echo "--output requires a path" >&2
34+ exit 2
35+ fi
36+ OUTPUT="$2"
37+ shift 2
38+ ;;
39+ --log-output)
40+ if [[ $# -lt 2 ]]; then
41+ echo "--log-output requires a path" >&2
42+ exit 2
43+ fi
44+ LOG_OUTPUT="$2"
45+ shift 2
46+ ;;
47+ --exit-code-output)
48+ if [[ $# -lt 2 ]]; then
49+ echo "--exit-code-output requires a path" >&2
50+ exit 2
51+ fi
52+ EXIT_CODE_OUTPUT="$2"
53+ shift 2
54+ ;;
55+ --include-hf-test)
56+ SKIP_HF_TEST=0
57+ shift
58+ ;;
59+ --fail-on-cargo-error)
60+ IGNORE_CARGO_EXIT_CODE=0
61+ shift
62+ ;;
63+ --help|-h)
64+ usage
65+ exit 0
66+ ;;
67+ *)
68+ break
69+ ;;
70+ esac
71+done
72+ 
73+cd "$REPO_DIR"
74+ 
75+if command -v conda >/dev/null 2>&1; then
76+ CONDA_BASE="$(conda info --base 2>/dev/null || true)"
77+ if [[ -n "$CONDA_BASE" && -f "$CONDA_BASE/etc/profile.d/conda.sh" ]]; then
78+ # shellcheck disable=SC1090
79+ source "$CONDA_BASE/etc/profile.d/conda.sh"
80+ conda activate "$CONDA_ENV" || true
81+ fi
82+fi
83+ 
84+if [[ $# -eq 0 ]]; then
85+ set -- --workspace --no-fail-fast
86+fi
87+ 
88+SCRIPT_ARGS=(--output "$OUTPUT" --log-output "$LOG_OUTPUT" --exit-code-output "$EXIT_CODE_OUTPUT")
89+if [[ "$IGNORE_CARGO_EXIT_CODE" -eq 1 ]]; then
90+ SCRIPT_ARGS+=(--ignore-cargo-exit-code)
91+fi
92+if [[ "$SKIP_HF_TEST" -eq 1 ]]; then
93+ SCRIPT_ARGS+=(--skip-test test_full_get_from_hf)
94+fi
95+ 
96+python3 ci_project/cargo_test_to_csv.py "${SCRIPT_ARGS[@]}" -- "$@"
@@ -0,0 +1,114 @@
1+#!/usr/bin/env bash
2+set -u
3+ 
4+SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
5+REPO_DIR="$(dirname "$SCRIPT_PATH")/.."
6+OUTPUT="target/pytest-results.csv"
7+LOG_OUTPUT="target/pytest-output.log"
8+EXIT_CODE_OUTPUT="target/pytest-exit-code.txt"
9+IGNORE_PYTEST_EXIT_CODE=1
10+BY_TEST_DIR=1
11+ 
12+usage() {
13+ cat <<'USAGE'
14+Usage:
15+ ci_project/run_pytest_to_csv.sh [--output PATH] [--log-output PATH] [--exit-code-output PATH] [--fail-on-pytest-error] [pytest args...]
16+ 
17+Examples:
18+ ci_project/run_pytest_to_csv.sh
19+ ci_project/run_pytest_to_csv.sh --output /tmp/pytest-results.csv
20+ ci_project/run_pytest_to_csv.sh --single tests/dataframe/test_select.py
21+ 
22+With no pytest args, this script runs pytest once for each first-level directory under tests/.
23+For a directory such as tests/dataframe, CSV rows use module=dataframe.
24+The CSV columns are: module,name,result
25+Results use: ok, failed, ignored
26+By default this script returns 0 after writing CSV and records pytest's raw exit code separately.
27+USAGE
28+}
29+ 
30+while [[ $# -gt 0 ]]; do
31+ case "$1" in
32+ --output|-o|--current-csv)
33+ if [[ $# -lt 2 ]]; then
34+ echo "--output requires a path" >&2
35+ exit 2
36+ fi
37+ OUTPUT="$2"
38+ shift 2
39+ ;;
40+ --log-output)
41+ if [[ $# -lt 2 ]]; then
42+ echo "--log-output requires a path" >&2
43+ exit 2
44+ fi
45+ LOG_OUTPUT="$2"
46+ shift 2
47+ ;;
48+ --exit-code-output|--pytest-exit-code-output)
49+ if [[ $# -lt 2 ]]; then
50+ echo "--exit-code-output requires a path" >&2
51+ exit 2
52+ fi
53+ EXIT_CODE_OUTPUT="$2"
54+ shift 2
55+ ;;
56+ --fail-on-pytest-error)
57+ IGNORE_PYTEST_EXIT_CODE=0
58+ shift
59+ ;;
60+ --single)
61+ BY_TEST_DIR=0
62+ shift
63+ ;;
64+ --by-test-dir)
65+ BY_TEST_DIR=1
66+ shift
67+ ;;
68+ --help|-h)
69+ usage
70+ exit 0
71+ ;;
72+ *)
73+ break
74+ ;;
75+ esac
76+done
77+ 
78+cd "$REPO_DIR"
79+ 
80+if [[ -f .venv/bin/activate ]]; then
81+ # shellcheck disable=SC1091
82+ source .venv/bin/activate
83+fi
84+ 
85+if ! command -v pytest >/dev/null 2>&1; then
86+ echo "pytest not found; activate the expected Python environment first" >&2
87+ exit 2
88+fi
89+ 
90+mkdir -p target
91+ 
92+PYTEST_TO_CSV_ARGS=(
93+ --current-csv "$OUTPUT"
94+ --pytest-exit-code-output "$EXIT_CODE_OUTPUT"
95+ --runner native
96+)
97+ 
98+if [[ "$BY_TEST_DIR" -eq 1 ]]; then
99+ PYTEST_TO_CSV_ARGS+=(--by-test-dir)
100+fi
101+ 
102+if [[ $# -gt 0 ]]; then
103+ PYTEST_TO_CSV_ARGS+=(-- "$@")
104+fi
105+ 
106+set +e
107+python ci_project/pytest_to_csv.py "${PYTEST_TO_CSV_ARGS[@]}" 2>&1 | tee "$LOG_OUTPUT"
108+PYTEST_EXIT=${PIPESTATUS[0]}
109+set -e
110+ 
111+if [[ "$IGNORE_PYTEST_EXIT_CODE" -eq 1 ]]; then
112+ exit 0
113+fi
114+exit "$PYTEST_EXIT"
@@ -69,7 +69,8 @@ if ! command -v uv &> /dev/null; then
69 echo "export UV_CACHE_DIR=/home/uv" >> ~/.bashrc69 echo "export UV_CACHE_DIR=/home/uv" >> ~/.bashrc
70 fi70 fi
71 curl -LsSf https://astral.sh/uv/install.sh | sh71 curl -LsSf https://astral.sh/uv/install.sh | sh
72- source ~/.bashrc72+ source "$HOME/.local/bin/env"
73+ 
73 if ! command -v uv &> /dev/null; then74 if ! command -v uv &> /dev/null; then
74 echo "错误: uv 安装失败,请手动安装后重试。"75 echo "错误: uv 安装失败,请手动安装后重试。"
75 exit 176 exit 1
@@ -79,12 +80,12 @@ else
79 echo "√ uv 已安装"80 echo "√ uv 已安装"
80fi81fi
81 82 
82-# 5. 检查 CMake83+# 5. 检查 make
83-if ! command -v cmake &> /dev/null; then84+if ! command -v make &> /dev/null; then
84- echo "错误: CMake 未安装。请先安装 CMake (如 sudo yum install cmake 或从官网下载)。"85+ echo "错误: Make 未安装。请先安装 Make "
85 exit 186 exit 1
86fi87fi
87-echo "√ CMake 已安装"88+echo "√ Make 已安装"
88 89 
89# 6. 设置环境变量(解决字体下载网络问题)90# 6. 设置环境变量(解决字体下载网络问题)
90export NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=191export NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1
@@ -140,4 +141,4 @@ else
140 echo "未能自动定位 wheel 文件,请查看上述 make 输出中的路径。"141 echo "未能自动定位 wheel 文件,请查看上述 make 输出中的路径。"
141fi142fi
142 143 
143-echo "========================================"144+echo "========================================"
@@ -1,3 +1,3 @@
1unstable_features = true1unstable_features = true
2group_imports = "StdExternalCrate"2group_imports = "StdExternalCrate"
3-imports_granularity = "Crate"3+imports_granularity = "Crate"