chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,865 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user