Skip to content

Commit 9f8fe30

Browse files
Verify Examples (pyscf#2379)
* add verify_examples * make executable * minor fix relating to cwd * add doc string and usage example
1 parent 8215a21 commit 9f8fe30

File tree

1 file changed

+257
-0
lines changed

1 file changed

+257
-0
lines changed

tools/verify_examples.py

+257
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#!/usr/bin/env python
2+
# Copyright 2014-2024 The PySCF Developers. All Rights Reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""
17+
Verify Examples
18+
===============
19+
20+
Author: Matthew R. Hennefarth
21+
22+
Script used to automatically run and verify PySCF example codes terminate
23+
successfully. For any job that does not terminate normally, the stderr of the
24+
example will be printed to the output. This script will exit with 0 only if all
25+
examples terminate normally.
26+
27+
Initially introduced in [PR 2379](https://github.com/pyscf/pyscf/pull/2379).
28+
29+
Usage
30+
-------------
31+
32+
From the main pyscf repository directory, the tests can be run as
33+
```sh
34+
./tools/verify_examples.py examples
35+
```
36+
This will run all example files (which can be very long). To run only a subset
37+
of examples, provide instead a path to a subdirectory. For example, to run only
38+
the example files in `pyscf/examples/gto` the command
39+
```sh
40+
./tools/verify_examples.py examples/gto
41+
```
42+
It is also possible to run the examples in parallel using the `-j` or `--jobs`
43+
flag (this is similar to make). As an example, to run the jobs in parallel over
44+
4 threads,
45+
```sh
46+
./tools/verify_examples.py -j 8
47+
```
48+
Note that the environmental variable such as `OMP_NUM_THREADS` should be set to
49+
an appropriate value such that number of jobs * OMP_NUM_THREADS does not exceed
50+
the maximum number of cores on the computer.
51+
52+
"""
53+
54+
import os
55+
import sys
56+
import time
57+
import subprocess
58+
import argparse
59+
import logging
60+
61+
import multiprocessing as mp
62+
from glob import glob
63+
from enum import Enum
64+
65+
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
66+
67+
logger = logging.getLogger()
68+
69+
70+
class StdOutFilter(logging.Filter):
71+
def filter(self, record):
72+
return record.levelno < logging.ERROR
73+
74+
75+
stdout_handler = logging.StreamHandler(sys.stdout)
76+
stdout_handler.setLevel(logging.INFO)
77+
stdout_handler.addFilter(StdOutFilter())
78+
79+
stderr_handler = logging.StreamHandler(sys.stderr)
80+
stderr_handler.setLevel(logging.ERROR)
81+
82+
logger.handlers = []
83+
logger.addHandler(stdout_handler)
84+
logger.addHandler(stderr_handler)
85+
86+
87+
class ANSIColors(Enum):
88+
RESET = "\033[0m"
89+
RED = "\033[31m"
90+
GREEN = "\033[32m"
91+
92+
93+
def colorize(text, color):
94+
if sys.stdout.isatty():
95+
return f"\033[{color.value}{text}{ANSIColors.RESET.value}"
96+
else:
97+
return text
98+
99+
100+
class Status(Enum):
101+
OK = colorize("ok", ANSIColors.GREEN)
102+
FAIL = colorize("FAILED", ANSIColors.RED)
103+
104+
105+
def get_path(p):
106+
if not os.path.isdir(p):
107+
raise ValueError("Path does not point to directory")
108+
109+
if os.path.basename(p) == "examples":
110+
return p
111+
112+
if os.path.isdir(os.path.join(p, "examples")):
113+
return os.path.join(p, "examples")
114+
115+
return p
116+
117+
118+
class ExampleResults:
119+
def __init__(self):
120+
self.common_prefix = ""
121+
self.failed_examples = []
122+
self.passed = 0
123+
self.failed = 0
124+
self.filtered = 0
125+
self.time = 0.0
126+
self.status = Status.OK
127+
128+
129+
def run_example(progress, nexamples, example, failed_examples, common_prefix):
130+
idx, lock = progress
131+
132+
status = Status.OK
133+
directory = os.path.dirname(example)
134+
try:
135+
subprocess.run(
136+
["python3", os.path.basename(example)],
137+
cwd=directory,
138+
capture_output=False,
139+
stderr=subprocess.PIPE,
140+
stdout=subprocess.DEVNULL,
141+
check=True,
142+
text=True,
143+
)
144+
except subprocess.CalledProcessError as e:
145+
status = Status.FAIL
146+
failed_examples.append((example, e.stderr))
147+
148+
with lock:
149+
idx.value += 1
150+
percent = int(100 * (idx.value) / nexamples)
151+
152+
message = (
153+
f"[{percent:3}%]: {os.path.relpath(example, common_prefix)} ... {status.value}"
154+
)
155+
logger.info(message)
156+
157+
158+
def run_examples(example_path, num_threads):
159+
examples = [
160+
y for x in os.walk(example_path) for y in glob(os.path.join(x[0], "*.py"))
161+
]
162+
# remove symlinks?
163+
# examples = list(set([os.path.realpath(e) for e in examples]))
164+
165+
examples = sorted(examples, key=lambda e: e.split("/"))
166+
167+
results = ExampleResults()
168+
results.common_prefix = os.path.dirname(os.path.commonpath(examples))
169+
results.filtered = 0
170+
171+
with mp.Manager() as manager:
172+
failed_examples = manager.list()
173+
progress = (manager.Value("i", 0), manager.Lock())
174+
175+
logger.info("")
176+
logger.info(f"running {len(examples)} examples")
177+
tic = time.perf_counter()
178+
with mp.Pool(num_threads) as pool:
179+
pool.starmap(
180+
run_example,
181+
[
182+
(
183+
progress,
184+
len(examples),
185+
example,
186+
failed_examples,
187+
results.common_prefix,
188+
)
189+
for example in examples
190+
],
191+
)
192+
results.time = time.perf_counter() - tic
193+
results.failed_examples = list(failed_examples)
194+
195+
results.failed = len(results.failed_examples)
196+
results.passed = len(examples) - results.failed
197+
results.status = Status.FAIL if results.failed else Status.OK
198+
199+
return results
200+
201+
202+
def log_failures(results):
203+
logger.info("")
204+
logger.info("failures: ")
205+
logger.info("")
206+
207+
for e, msg in results.failed_examples:
208+
logger.info(f"---- {os.path.relpath(e, results.common_prefix)} stderr ----")
209+
logger.info(msg)
210+
211+
logger.info("")
212+
logger.info("failures:")
213+
for e, _ in results.failed_examples:
214+
logger.info(f" {os.path.relpath(e, results.common_prefix)}")
215+
216+
217+
def main():
218+
parser = argparse.ArgumentParser(description="Verify pyscf examples")
219+
parser.add_argument(
220+
"path",
221+
type=str,
222+
default="examples",
223+
help="Path to examples directory (default: ./)",
224+
)
225+
parser.add_argument(
226+
"-j",
227+
"--jobs",
228+
type=int,
229+
default=1,
230+
help="Number of parallel threads (default: 1)",
231+
)
232+
args = parser.parse_args()
233+
234+
example_path = get_path(args.path)
235+
236+
results = run_examples(example_path, args.jobs)
237+
238+
if results.status is Status.FAIL:
239+
log_failures(results)
240+
241+
logger.info("")
242+
logger.info(
243+
f"example results: {results.status.value}. {results.passed} passed; {results.failed} failed; {results.filtered} filtered out; finished in {results.time:.2f}s"
244+
)
245+
logger.info("")
246+
247+
if results.status is Status.OK:
248+
sys.exit(0)
249+
else:
250+
logger.error(
251+
f"{ANSIColors.RED.value}error{ANSIColors.RESET.value}: examples failed"
252+
)
253+
sys.exit(1)
254+
255+
256+
if __name__ == "__main__":
257+
main()

0 commit comments

Comments
 (0)