Skip to content

Commit 48d6834

Browse files
psiddhfacebook-github-bot
authored andcommitted
Write Linux CMake pytest test-run reports
Summary: Use the upstream schema-1.0 reporter only when the runner supplies its report directory. Fail preflight explicitly on missing dependencies, unsupported schema/pytest, invalid job identity, or environment capture errors. Keep unopted arguments, coverage, and C++ test ordering unchanged. AI-assisted implementation. Reviewed By: shoumikhin Differential Revision: D129280827
1 parent 0fc43f6 commit 48d6834

3 files changed

Lines changed: 209 additions & 1 deletion

File tree

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
import shlex
9+
import subprocess
10+
import sys
11+
import tempfile
12+
import unittest
13+
from pathlib import Path
14+
15+
16+
ROOT = Path(__file__).resolve().parents[3]
17+
HELPER = ROOT / ".ci/scripts/torchci-report-args.sh"
18+
PROBE = """
19+
import os
20+
import sys
21+
from types import ModuleType, SimpleNamespace
22+
23+
sys.modules['pytest'] = SimpleNamespace(__version__=os.environ['PROBE_PYTEST'])
24+
for name in ('torch', 'torch.testing', 'torch.testing._internal',
25+
'torch.testing._internal.torchci'):
26+
sys.modules[name] = ModuleType(name)
27+
torchci = sys.modules['torch.testing._internal.torchci']
28+
def capture():
29+
os.environ['TORCHCI_POLLUTION'] = 'child-only'
30+
if os.environ.get('PROBE_CAPTURE_ERROR'):
31+
raise RuntimeError('capture failed')
32+
print('CAPTURE')
33+
torchci.environment = SimpleNamespace(capture=capture)
34+
torchci.report = SimpleNamespace(SCHEMA_VERSION=os.environ['PROBE_SCHEMA'])
35+
if not os.environ.get('PROBE_MISSING_PLUGIN'):
36+
torchci.plugin = ModuleType('plugin')
37+
exec(sys.argv[2])
38+
"""
39+
40+
41+
class TorchciReportArgumentsTest(unittest.TestCase):
42+
def run_helper(self, extra=None, command=None):
43+
with tempfile.TemporaryDirectory(dir=ROOT, prefix="torchci-test-") as tmp:
44+
probe = Path(tmp) / "probe.py"
45+
probe.write_text(PROBE)
46+
env = dict(os.environ)
47+
for name in (
48+
"RUNNER_TEST_RUN_REPORTS_DIR",
49+
"JOB_ID",
50+
"GITHUB_REPOSITORY",
51+
"TORCHCI_PYTHON",
52+
"PYTHON_EXECUTABLE",
53+
"TORCHCI_POLLUTION",
54+
"PYTHONOPTIMIZE",
55+
):
56+
env.pop(name, None)
57+
env.update(
58+
PROBE_PYTEST="7.3.0",
59+
PROBE_SCHEMA="1.0",
60+
TORCHCI_PYTHON=f"{sys.executable} -B {probe}",
61+
)
62+
env.update(extra or {})
63+
command = command or (
64+
"printf 'ARG <%s>\\n' "
65+
'${TORCHCI_REPORT_ARGS[@]+"${TORCHCI_REPORT_ARGS[@]}"}; '
66+
'printf "POLLUTION <%s>\\n" "${TORCHCI_POLLUTION:-}"'
67+
)
68+
return subprocess.run(
69+
[
70+
"bash",
71+
"-c",
72+
f"set -eu; source {shlex.quote(str(HELPER))} smoke; {command}",
73+
],
74+
env=env,
75+
capture_output=True,
76+
text=True,
77+
)
78+
79+
def opted_env(self, **extra):
80+
return {
81+
"RUNNER_TEST_RUN_REPORTS_DIR": "/tmp/torchci reports",
82+
"JOB_ID": "51234567890",
83+
"GITHUB_REPOSITORY": "pytorch/executorch",
84+
**extra,
85+
}
86+
87+
def test_unopted_run_does_not_start_an_interpreter(self):
88+
for directory in (None, ""):
89+
with self.subTest(directory=directory):
90+
extra = {"TORCHCI_PYTHON": "/missing/python"}
91+
if directory is not None:
92+
extra["RUNNER_TEST_RUN_REPORTS_DIR"] = directory
93+
result = self.run_helper(extra)
94+
self.assertEqual(result.returncode, 0, result.stderr)
95+
self.assertNotIn("CAPTURE", result.stdout)
96+
self.assertNotIn("torchci.plugin", result.stdout)
97+
98+
def test_valid_preflight_preserves_argument_boundaries_and_parent_environment(self):
99+
result = self.run_helper(self.opted_env())
100+
self.assertEqual(result.returncode, 0, result.stderr)
101+
self.assertEqual(
102+
result.stdout.splitlines(),
103+
[
104+
"CAPTURE",
105+
"ARG <-p>",
106+
"ARG <torch.testing._internal.torchci.plugin>",
107+
"ARG <--torchci-report-prefix=/tmp/torchci reports/smoke>",
108+
"POLLUTION <>",
109+
],
110+
)
111+
112+
def test_invalid_preflight_stops_before_the_next_command(self):
113+
cases = (
114+
({"PROBE_PYTEST": "7.2.0"}, "pytest >= 7.3"),
115+
({"PROBE_SCHEMA": "2.0"}, "schema 1.0"),
116+
({"JOB_ID": ""}, "positive JOB_ID"),
117+
({"JOB_ID": "0"}, "positive JOB_ID"),
118+
({"JOB_ID": "-1"}, "positive JOB_ID"),
119+
({"JOB_ID": "abc"}, "positive JOB_ID"),
120+
({"GITHUB_REPOSITORY": ""}, "GITHUB_REPOSITORY"),
121+
({"PROBE_MISSING_PLUGIN": "1"}, "plugin"),
122+
({"PROBE_CAPTURE_ERROR": "1"}, "capture failed"),
123+
)
124+
for extra, message in cases:
125+
with self.subTest(extra=extra):
126+
result = self.run_helper(self.opted_env(**extra), "echo TESTS_STARTED")
127+
self.assertNotEqual(result.returncode, 0)
128+
self.assertIn(message, result.stderr)
129+
self.assertNotIn("TESTS_STARTED", result.stdout)
130+
131+
def test_python_optimization_cannot_bypass_preflight(self):
132+
result = self.run_helper(self.opted_env(JOB_ID="0", PYTHONOPTIMIZE="1"))
133+
self.assertNotEqual(result.returncode, 0)
134+
self.assertIn("positive JOB_ID", result.stderr)
135+
result = self.run_helper(self.opted_env(PROBE_SCHEMA="2.0", PYTHONOPTIMIZE="1"))
136+
self.assertNotEqual(result.returncode, 0)
137+
self.assertIn("schema 1.0", result.stderr)
138+
139+
def test_linux_invocation_preserves_coverage_and_cpp_failure_order(self):
140+
script = (ROOT / ".ci/scripts/unittest-linux-cmake.sh").read_text()
141+
invocation = script.split("# Run pytest with coverage\n", 1)[1]
142+
for status in (0, 1):
143+
with self.subTest(status=status):
144+
command = (
145+
"pytest() { printf 'PYTEST'; printf ' <%s>' \"$@\"; "
146+
f"printf '\\n'; return {status}; }}; "
147+
"test() { echo CPP; }; "
148+
+ invocation.replace("test/run_oss_cpp_tests.sh", "test")
149+
)
150+
result = self.run_helper(self.opted_env(), command)
151+
self.assertEqual(result.returncode, status, result.stderr)
152+
self.assertIn(
153+
"PYTEST <-n> <auto> <--cov=./> <--cov-report=xml> <-p> "
154+
"<torch.testing._internal.torchci.plugin> "
155+
"<--torchci-report-prefix=/tmp/torchci reports/smoke>",
156+
result.stdout,
157+
)
158+
self.assertEqual("CPP" in result.stdout, status == 0)
159+
160+
def test_unopted_linux_invocation_has_no_reporting_arguments(self):
161+
script = (ROOT / ".ci/scripts/unittest-linux-cmake.sh").read_text()
162+
invocation = script.split("# Run pytest with coverage\n", 1)[1]
163+
command = (
164+
'pytest() { printf "ARG <%s>\\n" "$@"; }; '
165+
"test() { :; }; " + invocation.replace("test/run_oss_cpp_tests.sh", "test")
166+
)
167+
result = self.run_helper({"TORCHCI_PYTHON": "/missing/python"}, command)
168+
self.assertEqual(result.returncode, 0, result.stderr)
169+
self.assertEqual(
170+
result.stdout.splitlines(),
171+
["ARG <-n>", "ARG <auto>", "ARG <--cov=./>", "ARG <--cov-report=xml>"],
172+
)

‎.ci/scripts/torchci-report-args.sh‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env bash
2+
# shellcheck disable=SC2034
3+
# Copyright (c) Meta Platforms, Inc. and affiliates.
4+
# All rights reserved.
5+
#
6+
# This source code is licensed under the BSD-style license found in the
7+
# LICENSE file in the root directory of this source tree.
8+
9+
TORCHCI_REPORT_ARGS=()
10+
if [[ -n "${RUNNER_TEST_RUN_REPORTS_DIR:-}" ]]; then
11+
# The interpreter may include a launcher such as conda run.
12+
# shellcheck disable=SC2086
13+
${TORCHCI_PYTHON:-${PYTHON_EXECUTABLE:-python}} -c '
14+
import os
15+
import pytest
16+
from torch.testing._internal.torchci import environment, plugin, report
17+
18+
if report.SCHEMA_VERSION != "1.0":
19+
raise RuntimeError("torchci requires report schema 1.0")
20+
if tuple(map(int, pytest.__version__.split(".")[:2])) < (7, 3):
21+
raise RuntimeError("torchci requires pytest >= 7.3")
22+
job_id = os.environ.get("JOB_ID", "")
23+
if not job_id.isdigit() or int(job_id) <= 0:
24+
raise RuntimeError("torchci requires a positive JOB_ID")
25+
if not os.environ.get("GITHUB_REPOSITORY"):
26+
raise RuntimeError("torchci requires GITHUB_REPOSITORY")
27+
environment.capture()
28+
' || return 1
29+
TORCHCI_REPORT_ARGS=(
30+
-p torch.testing._internal.torchci.plugin
31+
"--torchci-report-prefix=${RUNNER_TEST_RUN_REPORTS_DIR}/${1:-pytest}"
32+
)
33+
fi

‎.ci/scripts/unittest-linux-cmake.sh‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ fi
3131

3232
bash .ci/scripts/tests/vulkan/test_vulkan_cmake_package.sh
3333

34+
source .ci/scripts/torchci-report-args.sh unittest-linux-cmake
35+
3436
# Run pytest with coverage
35-
pytest -n auto --cov=./ --cov-report=xml
37+
pytest -n auto --cov=./ --cov-report=xml \
38+
${TORCHCI_REPORT_ARGS[@]+"${TORCHCI_REPORT_ARGS[@]}"}
3639
# Run gtest
3740
LLVM_PROFDATA=llvm-profdata-12 LLVM_COV=llvm-cov-12 \
3841
test/run_oss_cpp_tests.sh

0 commit comments

Comments
 (0)