866 lines
27 KiB
Python
866 lines
27 KiB
Python
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import typing
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger.propagate = False
|
|
|
|
formatter = logging.Formatter("%(message)s")
|
|
|
|
# add stdout for all logs
|
|
handler_stdout = logging.StreamHandler(stream=sys.stdout)
|
|
handler_stdout.setLevel(logging.DEBUG)
|
|
handler_stdout.setFormatter(formatter)
|
|
|
|
# add stderr for bad code-block
|
|
handler_stderr = logging.StreamHandler(stream=sys.stderr)
|
|
handler_stderr.setLevel(logging.WARNING)
|
|
handler_stderr.setFormatter(formatter)
|
|
|
|
logger.addHandler(handler_stdout)
|
|
logger.addHandler(handler_stderr)
|
|
|
|
|
|
RUN_ON_DEVICE = 'cpu'
|
|
ENV_KEY_TEST_CAPACITY = 'SAMPLE_CODE_TEST_CAPACITY'
|
|
API_DEV_SPEC_FN = 'paddle/fluid/API_DEV.spec'
|
|
API_PR_SPEC_FN = 'paddle/fluid/API_PR.spec'
|
|
API_DIFF_SPEC_FN = 'dev_pr_diff_api.spec'
|
|
TEST_TIMEOUT = 15
|
|
|
|
PAT_API_SPEC_MEMBER = re.compile(r'\((paddle[^,]+)\W*document\W*([0-9a-z]{32})')
|
|
# insert ArgSpec for changing the API's type annotation can trigger the CI
|
|
PAT_API_SPEC_SIGNATURE = re.compile(
|
|
r'^(paddle[^,]+)\s+\((ArgSpec.*),.*document\W*([0-9a-z]{32})'
|
|
)
|
|
|
|
|
|
class Result:
|
|
# name/key for result
|
|
name: str = ''
|
|
|
|
# default value
|
|
default: bool = False
|
|
|
|
# is failed result or not
|
|
is_fail: bool = False
|
|
|
|
# logging
|
|
logger: typing.Callable = logger.info
|
|
|
|
# logging print order(not logging level, just for convenient)
|
|
order: int = 0
|
|
|
|
@classmethod
|
|
def msg(cls, count: int, env: set) -> str:
|
|
"""Message for logging with api `count` and running `env`."""
|
|
raise NotImplementedError
|
|
|
|
|
|
class MetaResult(type):
|
|
"""A meta class to record `Result` subclasses."""
|
|
|
|
__slots__ = ()
|
|
|
|
# hold result cls
|
|
__cls_map = {}
|
|
|
|
# result added order
|
|
__order = 0
|
|
|
|
def __new__(
|
|
mcs,
|
|
name: str,
|
|
bases: tuple[type, ...],
|
|
namespace: dict[str, typing.Any],
|
|
) -> type:
|
|
cls = super().__new__(mcs, name, bases, namespace)
|
|
if issubclass(cls, Result):
|
|
# set cls order as added to Meta
|
|
cls.order = mcs.__order
|
|
mcs.__order += 1
|
|
|
|
# put cls into Meta's map
|
|
mcs.__cls_map[namespace.get('name')] = cls
|
|
|
|
return cls
|
|
|
|
@classmethod
|
|
def get(mcs, name: str) -> type:
|
|
return mcs.__cls_map.get(name)
|
|
|
|
@classmethod
|
|
def cls_map(mcs) -> dict[str, Result]:
|
|
return mcs.__cls_map
|
|
|
|
|
|
class RPassed(Result, metaclass=MetaResult):
|
|
name = 'passed'
|
|
is_fail = False
|
|
|
|
@classmethod
|
|
def msg(cls, count, env):
|
|
return f">>> {count} sample codes ran success in env: {env}"
|
|
|
|
|
|
class RSkipped(Result, metaclass=MetaResult):
|
|
name = 'skipped'
|
|
is_fail = False
|
|
logger = logger.warning
|
|
|
|
@classmethod
|
|
def msg(cls, count, env):
|
|
return f">>> {count} sample codes skipped in env: {env}"
|
|
|
|
|
|
class RFailed(Result, metaclass=MetaResult):
|
|
name = 'failed'
|
|
is_fail = True
|
|
logger = logger.error
|
|
|
|
@classmethod
|
|
def msg(cls, count, env):
|
|
return f">>> {count} sample codes ran failed in env: {env}"
|
|
|
|
|
|
class RNoCode(Result, metaclass=MetaResult):
|
|
name = 'nocode'
|
|
is_fail = True
|
|
logger = logger.error
|
|
|
|
@classmethod
|
|
def msg(cls, count, env):
|
|
return f">>> {count} apis don't have sample codes or could not run test in env: {env}"
|
|
|
|
|
|
class RTimeout(Result, metaclass=MetaResult):
|
|
name = 'timeout'
|
|
is_fail = True
|
|
logger = logger.error
|
|
|
|
@classmethod
|
|
def msg(cls, count, env):
|
|
return f">>> {count} sample codes ran timeout or error in env: {env}"
|
|
|
|
|
|
class RBadStatement(Result, metaclass=MetaResult):
|
|
name = 'badstatement'
|
|
is_fail = True
|
|
logger = logger.error
|
|
|
|
@classmethod
|
|
def msg(cls, count, env):
|
|
return (
|
|
f">>> {count} bad statements detected in sample codes in env: {env}"
|
|
)
|
|
|
|
|
|
class TestResult:
|
|
name: str = ""
|
|
time: float = float('inf')
|
|
test_msg: str = ""
|
|
extra_info: str = ""
|
|
|
|
# there should be only one result be True.
|
|
__unique_state: Result = None
|
|
|
|
def __init__(self, **kwargs) -> None:
|
|
# set all attr from metaclass
|
|
for result_name, result_cls in MetaResult.cls_map().items():
|
|
setattr(self, result_name, result_cls.default)
|
|
|
|
# overwrite attr from kwargs
|
|
for name, value in kwargs.items():
|
|
# check attr name
|
|
if not (hasattr(self, name) or name in MetaResult.cls_map()):
|
|
raise KeyError(f'`{name}` is not a valid result type.')
|
|
|
|
setattr(self, name, value)
|
|
|
|
if name in MetaResult.cls_map() and value:
|
|
if self.__unique_state is not None:
|
|
logger.warning('Only one result state should be True.')
|
|
|
|
self.__unique_state = MetaResult.get(name)
|
|
|
|
if self.__unique_state is None:
|
|
logger.warning('Default result will be set to FAILED!')
|
|
setattr(self, RFailed.name, True)
|
|
self.__unique_state = RFailed
|
|
|
|
@property
|
|
def state(self) -> Result:
|
|
return self.__unique_state
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.name}, running time: {self.time:.3f}s'
|
|
|
|
|
|
class DocTester:
|
|
"""A DocTester can be used to test the codeblock from the API's docstring.
|
|
|
|
Attributes:
|
|
|
|
style(str): `style` should be in {'google', 'freeform'}.
|
|
`google`, codeblock in `Example(s):` section of docstring.
|
|
`freeform`, all codeblocks in docstring wrapped with PS1(>>> ) and PS2(... ).
|
|
**CAUTION** no matter `.. code-block:: pycon` used or not,
|
|
the docstring in PS1(>>> ) and PS2(... ) should be considered as codeblock.
|
|
target(str): `target` should be in {'docstring', 'codeblock'}.
|
|
`docstring`, the test target is a docstring with optional description, `Args:`, `Returns:`, `Examples:` and so on.
|
|
`codeblock`, the codeblock extracted by `extract_code_blocks_from_docstr` from the docstring, and the pure codeblock is the docstring to test.
|
|
If we use `.. code-block:: pycon` wrapping the codeblock, the target should be `codeblock` instead of `docstring`.
|
|
Because the `doctest` and `xdoctest` do NOT care the `.. code-block:: pycon` directive.
|
|
If the `style` is set to `google` and `target` is set to `codeblock`, we should implement/overwrite `ensemble_docstring` method,
|
|
where ensemble the codeblock into a docstring with a `Examples:` and some indents as least.
|
|
directives(list[str]): `DocTester` hold the default directives, we can/should replace them with method `convert_directive`.
|
|
For example:
|
|
``` text
|
|
# doctest: +SKIP
|
|
# doctest: +REQUIRES(env:CPU)
|
|
# doctest: +REQUIRES(env:GPU)
|
|
# doctest: +REQUIRES(env:XPU)
|
|
# doctest: +REQUIRES(env:DISTRIBUTED)
|
|
# doctest: +REQUIRES(env:GPU, env:XPU)
|
|
```
|
|
"""
|
|
|
|
style = 'google'
|
|
target = 'docstring'
|
|
directives = None
|
|
|
|
def ensemble_docstring(self, codeblock: str) -> str:
|
|
"""Ensemble a cleaned codeblock into a docstring.
|
|
|
|
For example, we can add `Example:` before the code block and some indents, which makes it a `google` style docstring.
|
|
Otherwise, a codeblock is just a `freeform` style docstring.
|
|
|
|
Args:
|
|
codeblock(str): a str of codeblock and its outputs.
|
|
|
|
Returns:
|
|
a docstring for test.
|
|
"""
|
|
if self.style == 'google':
|
|
return 'Examples:\n' + '\n'.join(
|
|
[' ' + line for line in codeblock.splitlines()]
|
|
)
|
|
|
|
return codeblock
|
|
|
|
def convert_directive(self, docstring: str) -> str:
|
|
"""Convert the standard directive from default DocTester into the doctester's style:
|
|
|
|
For example:
|
|
From: # doctest: +SKIP
|
|
To: # xdoctest: +SKIP
|
|
|
|
Args:
|
|
docstring(str): the raw docstring
|
|
|
|
Returns:
|
|
a docstring with directives converted.
|
|
"""
|
|
return docstring
|
|
|
|
def prepare(self, test_capacity: set) -> None:
|
|
"""Something before run the test.
|
|
|
|
Xdoctest need to set the `os.environ` according to the test capacity,
|
|
which `+REQUIRES` used to match the test required environment.
|
|
|
|
Legacy sample code processor do NOT need.
|
|
|
|
Args:
|
|
test_capacity(set): the test capacity, like `cpu`, `gpu` and so on.
|
|
"""
|
|
pass
|
|
|
|
def run(self, api_name: str, docstring: str) -> list[TestResult]:
|
|
"""Extract codeblocks from docstring, and run the test.
|
|
Run only one docstring at a time.
|
|
|
|
Args:
|
|
api_name(str): api name
|
|
docstring(str): docstring.
|
|
|
|
Returns:
|
|
list[TestResult]: test results. because one docstring may extract more than one code examples, so return a list.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def print_summary(
|
|
self, test_results: list[TestResult], whl_error: list[str]
|
|
) -> None:
|
|
"""Post process test results and print test summary.
|
|
|
|
There are some `required not match` in legacy test processor, but NOT exist in Xdoctest.
|
|
When using the legacy processor, we can set test result to `skipped=True` and store the `not match` information in `extra_info`,
|
|
then logging the `not match` in `print_summary`.
|
|
|
|
Args:
|
|
test_results(list[TestResult]): test results generated from doctester.
|
|
whl_error(list[str]): wheel error when we extract apis from module.
|
|
"""
|
|
pass
|
|
|
|
|
|
def get_api_md5(path):
|
|
"""
|
|
read the api spec file, and scratch the md5sum value of every api's docstring.
|
|
|
|
Args:
|
|
path: the api spec file. ATTENTION the path relative
|
|
|
|
Returns:
|
|
api_md5(dict): key is the api's real fullname, value is the md5sum.
|
|
"""
|
|
api_md5 = {}
|
|
API_spec = os.path.abspath(os.path.join(os.getcwd(), "..", path))
|
|
if not os.path.isfile(API_spec):
|
|
return api_md5
|
|
|
|
with open(API_spec) as f:
|
|
for line in f:
|
|
mo = PAT_API_SPEC_MEMBER.search(line)
|
|
|
|
if mo:
|
|
api_md5[mo.group(1)] = mo.group(2)
|
|
else:
|
|
mo = PAT_API_SPEC_SIGNATURE.search(line)
|
|
api_md5[mo.group(1)] = f'{mo.group(2)}, {mo.group(3)}'
|
|
|
|
return api_md5
|
|
|
|
|
|
def get_incrementapi(
|
|
api_dev_spec_fn: str | None = None,
|
|
api_pr_spec_fn: str | None = None,
|
|
api_diff_spec_fn: str | None = None,
|
|
) -> None:
|
|
'''
|
|
this function will get the apis that difference between API_DEV.spec and API_PR.spec.
|
|
'''
|
|
global API_DEV_SPEC_FN, API_PR_SPEC_FN, API_DIFF_SPEC_FN # readonly
|
|
|
|
dev_api = get_api_md5(
|
|
API_DEV_SPEC_FN if api_dev_spec_fn is None else api_dev_spec_fn
|
|
)
|
|
pr_api = get_api_md5(
|
|
API_PR_SPEC_FN if api_pr_spec_fn is None else api_pr_spec_fn
|
|
)
|
|
with open(
|
|
API_DIFF_SPEC_FN if api_diff_spec_fn is None else api_diff_spec_fn, 'w'
|
|
) as f:
|
|
for key in pr_api:
|
|
if key in dev_api:
|
|
if dev_api[key] != pr_api[key]:
|
|
logger.debug(
|
|
"%s in dev is %s, different from pr's %s",
|
|
key,
|
|
dev_api[key],
|
|
pr_api[key],
|
|
)
|
|
f.write(key)
|
|
f.write('\n')
|
|
else:
|
|
logger.debug("%s is not in dev", key)
|
|
f.write(key)
|
|
f.write('\n')
|
|
|
|
|
|
def get_full_api_by_walk():
|
|
"""
|
|
get all the apis
|
|
"""
|
|
global API_DIFF_SPEC_FN # readonly
|
|
from print_signatures import get_all_api
|
|
|
|
apilist = get_all_api()
|
|
with open(API_DIFF_SPEC_FN, 'w') as f:
|
|
f.write("\n".join([ai[0] for ai in apilist]))
|
|
|
|
|
|
def get_full_api_from_pr_spec():
|
|
"""
|
|
get all the apis
|
|
"""
|
|
global API_PR_SPEC_FN, API_DIFF_SPEC_FN # readonly
|
|
pr_api = get_api_md5(API_PR_SPEC_FN)
|
|
if len(pr_api):
|
|
with open(API_DIFF_SPEC_FN, 'w') as f:
|
|
f.write("\n".join(pr_api.keys()))
|
|
else:
|
|
get_full_api_by_walk()
|
|
|
|
|
|
def extract_code_blocks_from_docstr(docstr, google_style=True):
|
|
"""
|
|
extract code-blocks from the given docstring.
|
|
DON'T include the multiline-string definition in code-blocks.
|
|
The *Examples* section must be the last.
|
|
Args:
|
|
docstr(str): docstring
|
|
google_style(bool): if not use google_style, the code blocks will be extracted from all the parts of docstring.
|
|
Return:
|
|
code_blocks: A list of code-blocks, indent removed.
|
|
element {'name': the code-block's name, 'id': sequence id.
|
|
'codes': codes, 'in_examples': bool, code block in `Examples` or not,}
|
|
"""
|
|
code_blocks = []
|
|
|
|
mo = re.search(r"Examples?:", docstr)
|
|
|
|
if google_style and mo is None:
|
|
return code_blocks
|
|
|
|
example_start = len(docstr) if mo is None else mo.start()
|
|
docstr_describe = docstr[:example_start].splitlines()
|
|
docstr_examples = docstr[example_start:].splitlines()
|
|
|
|
docstr_list = []
|
|
if google_style:
|
|
example_lineno = 0
|
|
docstr_list = docstr_examples
|
|
else:
|
|
example_lineno = len(docstr_describe)
|
|
docstr_list = docstr_describe + docstr_examples
|
|
|
|
lastlineindex = len(docstr_list) - 1
|
|
|
|
cb_start_pat = re.compile(r"code-block::\s*(python|python-console|pycon)")
|
|
cb_param_pat = re.compile(r"^\s*:(\w+):\s*(\S*)\s*$")
|
|
|
|
cb_info = {}
|
|
cb_info['cb_started'] = False
|
|
cb_info['cb_cur'] = []
|
|
cb_info['cb_cur_indent'] = -1
|
|
cb_info['cb_cur_name'] = None
|
|
cb_info['cb_cur_seq_id'] = 0
|
|
|
|
def _cb_started():
|
|
# nonlocal cb_started, cb_cur_name, cb_cur_seq_id
|
|
cb_info['cb_started'] = True
|
|
cb_info['cb_cur_seq_id'] += 1
|
|
cb_info['cb_cur_name'] = None
|
|
|
|
def _append_code_block(in_examples):
|
|
# nonlocal code_blocks, cb_cur, cb_cur_name, cb_cur_seq_id
|
|
code_blocks.append(
|
|
{
|
|
'codes': inspect.cleandoc("\n" + "\n".join(cb_info['cb_cur'])),
|
|
'name': cb_info['cb_cur_name'],
|
|
'id': cb_info['cb_cur_seq_id'],
|
|
'in_examples': in_examples,
|
|
}
|
|
)
|
|
|
|
for lineno, linecont in enumerate(docstr_list):
|
|
if re.search(cb_start_pat, linecont):
|
|
if not cb_info['cb_started']:
|
|
_cb_started()
|
|
continue
|
|
else:
|
|
# cur block end
|
|
if len(cb_info['cb_cur']):
|
|
_append_code_block(lineno > example_lineno)
|
|
_cb_started() # another block started
|
|
cb_info['cb_cur_indent'] = -1
|
|
cb_info['cb_cur'] = []
|
|
else:
|
|
if cb_info['cb_started']:
|
|
# handle the code-block directive's options
|
|
mo_p = cb_param_pat.match(linecont)
|
|
if mo_p:
|
|
if mo_p.group(1) == 'name':
|
|
cb_info['cb_cur_name'] = mo_p.group(2)
|
|
continue
|
|
# docstring end
|
|
if lineno == lastlineindex:
|
|
mo = re.search(r"\S", linecont)
|
|
if (
|
|
mo is not None
|
|
and cb_info['cb_cur_indent'] <= mo.start()
|
|
):
|
|
cb_info['cb_cur'].append(linecont)
|
|
if len(cb_info['cb_cur']):
|
|
_append_code_block(lineno > example_lineno)
|
|
break
|
|
# check indent for cur block start and end.
|
|
if cb_info['cb_cur_indent'] < 0:
|
|
mo = re.search(r"\S", linecont)
|
|
if mo is None:
|
|
continue
|
|
# find the first non empty line
|
|
cb_info['cb_cur_indent'] = mo.start()
|
|
cb_info['cb_cur'].append(linecont)
|
|
else:
|
|
mo = re.search(r"\S", linecont)
|
|
if mo is None:
|
|
cb_info['cb_cur'].append(linecont)
|
|
continue
|
|
if cb_info['cb_cur_indent'] <= mo.start():
|
|
cb_info['cb_cur'].append(linecont)
|
|
else:
|
|
if linecont[mo.start()] == '#':
|
|
continue
|
|
else:
|
|
# block end
|
|
if len(cb_info['cb_cur']):
|
|
_append_code_block(lineno > example_lineno)
|
|
cb_info['cb_started'] = False
|
|
cb_info['cb_cur_indent'] = -1
|
|
cb_info['cb_cur'] = []
|
|
return code_blocks
|
|
|
|
|
|
def log_exit(arg=None):
|
|
if arg:
|
|
_logger = logger.warning
|
|
else:
|
|
_logger = logger.info
|
|
|
|
_logger("----------------End of the Check--------------------")
|
|
|
|
sys.exit(arg)
|
|
|
|
|
|
def init_logger(debug=True, log_file=None):
|
|
"""
|
|
init logger level and file handler
|
|
"""
|
|
if debug:
|
|
logger.setLevel(logging.DEBUG)
|
|
else:
|
|
logger.setLevel(logging.INFO)
|
|
|
|
if log_file is not None:
|
|
logfHandler = logging.FileHandler(log_file)
|
|
logfHandler.setFormatter(
|
|
logging.Formatter(
|
|
"%(asctime)s - %(funcName)s:%(lineno)d - %(levelname)s - %(message)s"
|
|
)
|
|
)
|
|
logger.addHandler(logfHandler)
|
|
|
|
|
|
def check_test_mode(mode="cpu", gpu_id=0):
|
|
"""
|
|
check test mode in {cpu, gpu}
|
|
"""
|
|
if mode == "gpu":
|
|
logger.info("using GPU_ID %d", gpu_id)
|
|
|
|
elif mode == "cpu":
|
|
logger.info("using CPU")
|
|
|
|
else:
|
|
logger.error(
|
|
"Unrecognized argument:%s, 'cpu' or 'gpu' is desired.", mode
|
|
)
|
|
log_exit("Invalid arguments")
|
|
|
|
return mode
|
|
|
|
|
|
def get_test_capacity(run_on_device="cpu"):
|
|
"""
|
|
collect capacities and set to sample_code_test_capacity
|
|
"""
|
|
sample_code_test_capacity = set()
|
|
if ENV_KEY_TEST_CAPACITY in os.environ:
|
|
for env_value in os.environ[ENV_KEY_TEST_CAPACITY].split(','):
|
|
if env_value:
|
|
sample_code_test_capacity.add(env_value.strip().lower())
|
|
|
|
if 'cpu' not in sample_code_test_capacity:
|
|
sample_code_test_capacity.add('cpu')
|
|
|
|
if run_on_device:
|
|
sample_code_test_capacity.add(run_on_device)
|
|
|
|
logger.info("Sample code test capacity: %s", sample_code_test_capacity)
|
|
|
|
return sample_code_test_capacity
|
|
|
|
|
|
def get_docstring(
|
|
full_test: bool = False,
|
|
filter_api: typing.Callable[[str], bool] | None = None,
|
|
apis: list[tuple[str, str]] | None = None,
|
|
) -> tuple[dict[str, str], list[str]]:
|
|
'''
|
|
this function will get the docstring for test.
|
|
|
|
Args:
|
|
full_test, get all api
|
|
filter_api, a function that filter api, if `True` then skip add to `docstrings_to_test`.
|
|
apis, checking apis with ((line, api), (line, api), ...) like (("paddle.abs", "paddle.abs"), ("paddle.sin", "paddle.sin"), ...).
|
|
Do NOT use `full_test` and `apis` at the same time.
|
|
'''
|
|
import paddle
|
|
import paddle.static.quantization # noqa: F401
|
|
|
|
docstrings_to_test = {}
|
|
whl_error = []
|
|
|
|
if apis is None or not apis:
|
|
# get api from spec
|
|
if full_test:
|
|
get_full_api_from_pr_spec()
|
|
else:
|
|
get_incrementapi()
|
|
|
|
with open(API_DIFF_SPEC_FN) as f:
|
|
apis = [(line, line.replace('\n', '')) for line in f]
|
|
|
|
for line, api in apis:
|
|
if filter_api is not None and filter_api(api.strip()):
|
|
continue
|
|
|
|
try:
|
|
api_obj = eval(api)
|
|
except AttributeError:
|
|
whl_error.append(api)
|
|
continue
|
|
except SyntaxError:
|
|
logger.warning('line:%s, api:%s', line, api)
|
|
# paddle.Tensor.<lambda>
|
|
continue
|
|
if hasattr(api_obj, '__doc__') and api_obj.__doc__:
|
|
docstrings_to_test[api] = api_obj.__doc__
|
|
|
|
if len(docstrings_to_test) == 0 and len(whl_error) == 0:
|
|
logger.warning("-----API_PR.spec is the same as API_DEV.spec-----")
|
|
log_exit(0)
|
|
|
|
if len(docstrings_to_test) > 50:
|
|
logger.info("::group::API_PR is diff from API_DEV")
|
|
logger.info(docstrings_to_test.keys())
|
|
logger.info("::endgroup::")
|
|
else:
|
|
logger.info(
|
|
"API_PR is diff from API_DEV: %s", docstrings_to_test.keys()
|
|
)
|
|
|
|
logger.info("Total api: %s", len(docstrings_to_test.keys()))
|
|
|
|
return docstrings_to_test, whl_error
|
|
|
|
|
|
def check_old_style(docstrings_to_test: dict[str, str]):
|
|
old_style_apis = []
|
|
for api_name, raw_docstring in docstrings_to_test.items():
|
|
for codeblock in extract_code_blocks_from_docstr(
|
|
raw_docstring, google_style=False
|
|
):
|
|
old_style = True
|
|
|
|
for line in codeblock['codes'].splitlines():
|
|
if line.strip().startswith('>>>'):
|
|
old_style = False
|
|
break
|
|
|
|
if old_style:
|
|
codeblock_name = codeblock['name']
|
|
codeblock_id = codeblock['id']
|
|
|
|
docstring_name = f'{api_name}:{codeblock_name or codeblock_id}'
|
|
|
|
old_style_apis.append(docstring_name)
|
|
|
|
if old_style_apis:
|
|
logger.warning(
|
|
">>> %d apis use plain sample code style.",
|
|
len(old_style_apis),
|
|
)
|
|
logger.warning('=======================')
|
|
logger.warning('\n'.join(old_style_apis))
|
|
logger.warning('=======================')
|
|
logger.warning(">>> Check Failed!")
|
|
logger.warning(
|
|
">>> DEPRECATION: Please do not use plain sample code style."
|
|
)
|
|
logger.warning(
|
|
">>> For more information: https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/dev_guides/style_guide_and_references/code_example_writing_specification_cn.html "
|
|
)
|
|
log_exit(1)
|
|
|
|
|
|
def exec_gen_doc():
|
|
result = True
|
|
cmd = ["bash", "document_preview.sh"]
|
|
logger.info("----exec gen_doc----")
|
|
start_time = time.time()
|
|
subprc = subprocess.Popen(
|
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
)
|
|
output, error = subprc.communicate()
|
|
msg = "".join(output.decode(encoding='utf-8'))
|
|
err = "".join(error.decode(encoding='utf-8'))
|
|
end_time = time.time()
|
|
|
|
if subprc.returncode != 0:
|
|
logger.info("----gen_doc msg----")
|
|
logger.info(msg)
|
|
logger.error("----gen_doc error msg----")
|
|
logger.error(err)
|
|
logger.error("----exec gen_doc failed----")
|
|
result = False
|
|
else:
|
|
logger.info("----gen_doc msg----")
|
|
logger.info(msg)
|
|
logger.info("----exec gen_doc success----")
|
|
|
|
for fn in [
|
|
'/docs/en/develop/index_en.html',
|
|
'/docs/zh/develop/index_cn.html',
|
|
]:
|
|
if os.path.exists(fn):
|
|
logger.info('%s exists.', fn)
|
|
else:
|
|
logger.error('%s not exists.', fn)
|
|
|
|
# msg is the returned code execution report
|
|
return result, msg, end_time - start_time
|
|
|
|
|
|
def get_test_results(
|
|
doctester: DocTester, docstrings_to_test: dict[str, str]
|
|
) -> list[TestResult]:
|
|
"""Get test results from doctester with docstrings to test."""
|
|
_test_style = (
|
|
doctester.style
|
|
if doctester.style in {'google', 'freeform'}
|
|
else 'google'
|
|
)
|
|
google_style = _test_style == 'google'
|
|
|
|
test_results = []
|
|
for api_name, raw_docstring in docstrings_to_test.items():
|
|
docstrings_extracted = []
|
|
if doctester.target == 'codeblock':
|
|
# if the target is `codeblock`, we may extract more than one codeblocks from docstring.
|
|
for codeblock in extract_code_blocks_from_docstr(
|
|
raw_docstring, google_style=google_style
|
|
):
|
|
codeblock_name = codeblock['name']
|
|
codeblock_id = codeblock['id']
|
|
docstring = doctester.ensemble_docstring(
|
|
codeblock=codeblock['codes']
|
|
)
|
|
docstring_name = f'{api_name}:{codeblock_name or codeblock_id}'
|
|
|
|
docstrings_extracted.append(
|
|
{'name': docstring_name, 'docstring': docstring}
|
|
)
|
|
else:
|
|
docstrings_extracted.append(
|
|
{'name': api_name, 'docstring': raw_docstring}
|
|
)
|
|
|
|
for doc_extracted in docstrings_extracted:
|
|
# run doctester for one docstring at a time.
|
|
test_results.extend(
|
|
doctester.run(
|
|
api_name=doc_extracted['name'],
|
|
docstring=doctester.convert_directive(
|
|
doc_extracted['docstring']
|
|
),
|
|
)
|
|
)
|
|
|
|
return test_results
|
|
|
|
|
|
def run_doctest(args, doctester: DocTester):
|
|
# init logger
|
|
init_logger(debug=args.debug, log_file=args.logf)
|
|
|
|
logger.info("----------------Codeblock Check Start--------------------")
|
|
|
|
logger.info(">>> Check test mode ...")
|
|
run_on_device = check_test_mode(mode=args.mode, gpu_id=args.gpu_id)
|
|
|
|
logger.info(">>> Get test capacity ...")
|
|
sample_code_test_capacity = get_test_capacity(run_on_device)
|
|
|
|
logger.info(">>> Get docstring from api ...")
|
|
docstrings_to_test, whl_error = get_docstring(full_test=args.full_test)
|
|
|
|
logger.info(">>> Checking plain sample code style before Paddle 2.5 ...")
|
|
check_old_style(docstrings_to_test)
|
|
|
|
logger.info(">>> Prepare doctester ...")
|
|
doctester.prepare(sample_code_test_capacity)
|
|
|
|
logger.info(">>> Running doctester ...")
|
|
test_results = get_test_results(doctester, docstrings_to_test)
|
|
|
|
logger.info(">>> Print summary ...")
|
|
doctester.print_summary(test_results, whl_error)
|
|
|
|
if args.mode == "cpu":
|
|
# As cpu mode is also run with the GPU whl, so skip it in gpu mode.
|
|
exec_gen_doc()
|
|
|
|
|
|
def parse_args():
|
|
"""
|
|
Parse input arguments
|
|
"""
|
|
parser = argparse.ArgumentParser(description='run Sample Code Test')
|
|
parser.add_argument('--debug', dest='debug', action="store_true")
|
|
parser.add_argument('--full-test', dest='full_test', action="store_true")
|
|
parser.add_argument(
|
|
'--mode', dest='mode', type=str, default='cpu', help='run on device'
|
|
)
|
|
parser.add_argument(
|
|
'--build-doc',
|
|
dest='build_doc',
|
|
action='store_true',
|
|
help='build doc if need.',
|
|
)
|
|
parser.add_argument(
|
|
'--gpu_id',
|
|
dest='gpu_id',
|
|
type=int,
|
|
default=0,
|
|
help='GPU device id to use [0]',
|
|
)
|
|
parser.add_argument(
|
|
'--logf', dest='logf', type=str, default=None, help='file for logging'
|
|
)
|
|
parser.add_argument(
|
|
'--threads',
|
|
dest='threads',
|
|
type=int,
|
|
default=0,
|
|
help='sub processes number',
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
return args
|