"""网站构建脚本的自检:用人造包检验通用性。 人造包刻意与本仓库的 guan 包结构不同,覆盖各种「源码大改」后可能出现的形态: 包一(mypkg) * 包名不同(mypkg)、目录布局不同(/src/mypkg) * 元数据写在 pyproject.toml 的 [project] 表里(不是 setup.cfg) * 函数说明用 docstring,而不是定义上方的注释 * 带类型注解、async 函数、@deprecated 装饰器 * 有嵌套子包(mypkg.sub.extra) * 有私有函数(下划线开头)与显式 __all__ * 有一个语法错误的文件(构建不能中断) 包二(edge)—— 各种边角情形 * 空文件、带 BOM 的文件、CRLF 换行的文件、GBK 编码的文件 * 文件名不是合法标识符(my-module.py) * 函数内部再定义函数(不应出现在 API 顶层) * 类里的 property 与 async 方法 * `from . import helper` 形式的相对导入 * README 里带本地图片、表格、任务列表 * README 与源码里出现 {{version}} 这类模板占位符(不能被替换掉) 包三(empty)—— 什么都没有 * 没有任何函数时,「API 参考」页应当自动消失 用法: python website/selftest.py """ from __future__ import annotations import json import re import shutil import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import build_site # noqa: E402 import extract # noqa: E402 import i18n # noqa: E402 import md_render # noqa: E402 FAILURES: list[str] = [] def check(condition: bool, message: str) -> None: if condition: print(f" ok {message}") else: print(f" FAIL {message}") FAILURES.append(message) # -------------------------------------------------------------------------- # 包一:结构与人造包完全不同 # -------------------------------------------------------------------------- PYPROJECT = """\ [build-system] requires = ["setuptools>=42"] build-backend = "setuptools.build_meta" [project] name = "mypkg" version = "9.9.9" description = "A synthetic package for testing" requires-python = ">=3.10" authors = [{name = "Tester", email = "tester@example.com"}] [project.urls] Homepage = "https://example.com/mypkg" Repository = "https://github.com/example/mypkg" [tool.setuptools] packages = ["mypkg", "mypkg.sub"] """ INIT = '''\ """A synthetic package.""" from .core import * from .sub import * from .extra_only import special __all__ = ["greet", "VERSION", "special"] VERSION = "9.9.9" ''' CORE = '''\ """Core helpers of the synthetic package.""" def greet(name: str = "world") -> str: """Return a greeting for *name*. This description comes from a docstring, not from a comment above. """ return f"hello {name}" def _hidden_helper(x: int) -> int: """Not exported because of the leading underscore.""" return x async def fetch(url: str) -> str: """An async function.""" return url @deprecated def old_api(): """Deprecated on purpose.""" return 1 def table(): """Return a literal table.""" return {"alpha": 1, "beta": 2} def use_greet(): """Call another function through the package namespace.""" import mypkg return mypkg.greet("there") ''' EXTRA_ONLY = '''\ def special(): """Only exported through an explicit import.""" return 42 ''' SUB_INIT = '''\ """A subpackage.""" from .extra import * ''' SUB_EXTRA = '''\ def sub_function(x): """A function living in a nested subpackage.""" return x SUB_CONSTANTS = { "one": 1, "two": 2, } ''' BROKEN = '''\ def broken( ''' def make_package(root: Path) -> Path: """在 root 下造出人造包,返回包源码目录。""" package = root / "src" / "mypkg" (package / "sub").mkdir(parents=True) (root / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8") (package / "__init__.py").write_text(INIT, encoding="utf-8") (package / "core.py").write_text(CORE, encoding="utf-8") (package / "extra_only.py").write_text(EXTRA_ONLY, encoding="utf-8") (package / "broken.py").write_text(BROKEN, encoding="utf-8") (package / "sub" / "__init__.py").write_text(SUB_INIT, encoding="utf-8") (package / "sub" / "extra.py").write_text(SUB_EXTRA, encoding="utf-8") (root / "README.md").write_text( "# mypkg\n\nA synthetic package.\n\n+ one\n+ two\n", encoding="utf-8" ) (root / "INSTALLATION.md").write_text( "## Installation\n\n```bash\npip install --upgrade mypkg\n```\n", encoding="utf-8", ) return package # -------------------------------------------------------------------------- # 包二:边角情形 # -------------------------------------------------------------------------- EDGE_PYPROJECT = """\ [project] name = "edge" version = "0.1" description = "Edge cases" """ EDGE_INIT = '''\ """Edge case package.""" import os import json as js from . import helper from .plain import plain_function ''' EDGE_PLAIN = '''\ """Plain module.""" def plain_function(): """A plain function.""" return 1 def outer(): """A function with a nested function inside.""" def inner(): return 2 return inner() def tricky(): """模板占位符 {{version}} 不能被替换; 必须被转义。""" return 1 def plain(): """Module-level function named plain.""" return 0 def use_widget(): """Call a class of the same package, to exercise cross references.""" from .cls import Widget return Widget() class Container: """A class whose method has the same name as a module-level function.""" def plain(self): """Method named plain.""" return 0 ''' EDGE_CLS = '''\ """A class with fancy members.""" class Widget: """A widget.""" kind = "widget" @property def size(self) -> int: """The size.""" return 1 async def refresh(self) -> None: """Refresh the widget.""" def plain(self): """A plain method.""" return 3 ''' def make_edge_package(root: Path) -> Path: """造出各种边角情形的包。""" package = root / "src" / "edge" package.mkdir(parents=True) (root / "pyproject.toml").write_text(EDGE_PYPROJECT, encoding="utf-8") (package / "__init__.py").write_text(EDGE_INIT, encoding="utf-8") (package / "plain.py").write_text(EDGE_PLAIN, encoding="utf-8") (package / "cls.py").write_text(EDGE_CLS, encoding="utf-8") (package / "helper.py").write_text("", encoding="utf-8") # 空文件 # 带 BOM 的文件 (package / "bom.py").write_text( '"""With BOM."""\n\n\ndef bom_func():\n """BOM function."""\n return 1\n', encoding="utf-8-sig", ) # CRLF 换行 (package / "crlf.py").write_bytes( b'"""CRLF module."""\r\n\r\n\r\ndef crlf_func():\r\n """CRLF function."""\r\n return 1\r\n' ) # GBK 编码 (package / "gbk.py").write_bytes( '"""GBK 模块。"""\n\n\ndef gbk_func():\n """中文说明。"""\n return 1\n'.encode( "gbk" ) ) # 文件名不是合法标识符 (package / "my-module.py").write_text( '"""Odd name."""\n\n\ndef odd_func():\n """Odd function."""\n return 1\n', encoding="utf-8", ) # README:本地图片 + 表格 + 任务列表 + 模板占位符 (root / "docs").mkdir() (root / "docs" / "logo.png").write_bytes(b"\x89PNG\r\n\x1a\n") (root / "README.md").write_text( "# Edge\n\n" "![logo](docs/logo.png)\n\n" "版本号是 {{version}},不要被替换掉。\n\n" "| 列 | 值 |\n|---|---|\n| a | 1 |\n| b |\n\n" "- [ ] 待办\n- [x] 完成\n", encoding="utf-8", ) return package # -------------------------------------------------------------------------- # 包四:元数据的各种退化情形 # -------------------------------------------------------------------------- META_INIT = '''\ """Meta package.""" __version__ = "7.7.7" def only(): """Only one function.""" return 1 ''' def make_meta_package(root: Path) -> Path: """没有可读元数据时,应当退回到目录名与 __init__.py 里的 __version__。""" package = root / "src" / "meta" package.mkdir(parents=True) (package / "__init__.py").write_text(META_INIT, encoding="utf-8") # 故意写坏的 pyproject.toml,不应让构建崩溃 (root / "pyproject.toml").write_text( "[project\nname = broken\n", encoding="utf-8" ) return package # -------------------------------------------------------------------------- # 包五:setup.cfg 里用 attr: 写版本号 # -------------------------------------------------------------------------- ATTR_INIT = '''\ """Attr package.""" __version__ = "5.6.7" def only(): """Only one function.""" return 1 ''' def make_attr_package(root: Path) -> Path: package = root / "src" / "attrpkg" package.mkdir(parents=True) (package / "__init__.py").write_text(ATTR_INIT, encoding="utf-8") (root / "setup.cfg").write_text( "[metadata]\n" "name = attrpkg\n" "version = attr: attrpkg.__version__\n" "description = Version read from the package itself\n", encoding="utf-8", ) (root / "README.md").write_text("# attrpkg\n", encoding="utf-8") return package # -------------------------------------------------------------------------- # 包六:只有类,没有模块级函数 # -------------------------------------------------------------------------- CLASS_ONLY_INIT = '''\ """Only classes.""" from .thing import Thing ''' def make_class_only_package(root: Path) -> Path: package = root / "src" / "classonly" package.mkdir(parents=True) (package / "__init__.py").write_text(CLASS_ONLY_INIT, encoding="utf-8") (package / "thing.py").write_text( '"""A thing."""\n' "\n" "\n" "class Thing:\n" ' """A thing."""\n' "\n" " def go(self):\n" ' """Go."""\n' " return 1\n", encoding="utf-8", ) (root / "README.md").write_text("# classonly\n", encoding="utf-8") return package # -------------------------------------------------------------------------- # 包三:空包 # -------------------------------------------------------------------------- def make_empty_package(root: Path) -> Path: package = root / "src" / "empty" package.mkdir(parents=True) (root / "pyproject.toml").write_text( '[project]\nname = "empty"\nversion = "0"\ndescription = "Nothing here"\n', encoding="utf-8", ) (package / "__init__.py").write_text('"""Nothing here."""\n', encoding="utf-8") (root / "README.md").write_text("# Empty\n", encoding="utf-8") return package def check_meta(data: dict, output: Path) -> None: api = json.loads((output / "api.json").read_text(encoding="utf-8")) print("\n元数据退化") check(api["package"]["name"] == "meta", "元数据坏掉时退回目录名") check(api["package"]["version"] == "7.7.7", "版本退回 __init__.py 里的 __version__") check((output / "index.html").exists(), "元数据坏掉时网站照常生成") check(not build_site.verify_output(output), "生成的页面没有断链") # -------------------------------------------------------------------------- # 检查项 # -------------------------------------------------------------------------- def build(package: Path, tmp: Path, name: str) -> tuple[dict, Path]: output = tmp / name data = build_site.generate( source_dir=package, output=output, readme_path=package.parent.parent / "README.md", installation_path=package.parent.parent / "INSTALLATION.md", # 人造包不带上仓库里真实的 tutorials/,免得混入无关内容 tutorials_dir=package.parent.parent / "没有这个目录", quiet=True, ) return data, output def check_basic(data: dict, output: Path) -> None: api = json.loads((output / "api.json").read_text(encoding="utf-8")) print("\n包信息") check(api["package"]["name"] == "mypkg", "包名来自元数据") check(api["package"]["version"] == "9.9.9", "版本来自元数据") check(api["package"]["import_name"] == "mypkg", "导入名来自包目录") check(api["package"]["requires_python"] == ">=3.10", "requires-python 正确解析") check( api["package"]["urls"].get("Homepage") == "https://example.com/mypkg", "[project.urls] 表正确解析", ) check(api["package"]["author"] == "Tester", "PEP 621 的 authors 正确解析") check(api["package"]["author_email"] == "tester@example.com", "作者邮箱正确解析") print("\n模块发现") names = [m["name"] for m in api["modules"]] check("core" in names, "识别到 core 模块") check("sub.extra" in names, "识别到嵌套子包模块 sub.extra") check("mypkg" in names, "包入口被识别为模块") check("broken" in names, "语法错误的文件仍被列出") print("\n函数提取") functions = { f["name"]: f for module in api["modules"] for f in module["functions"] } check("greet" in functions, "识别到函数 greet") check( "Return a greeting" in functions["greet"]["description"], "docstring 被用作函数说明(源码没有注释时)", ) check( "This description comes from a docstring" in functions["greet"]["docstring"], "完整 docstring 保留在数据里", ) check(functions["greet"]["params"][0]["annotation"] == "str", "参数类型注解解析") check( functions["greet"]["params"][0]["default"] in ('"world"', "'world'"), "默认值解析", ) check(functions["greet"]["returns"] == "str", "返回类型解析") check(functions["fetch"]["is_async"], "async 函数被标记") check(functions["old_api"]["deprecated"], "@deprecated 装饰器被识别") check("_hidden_helper" in functions, "私有函数仍被提取(只是不导出)") check( functions["_hidden_helper"]["exported"] is False, "下划线开头的函数不被标记为导出", ) check(functions["greet"]["exported"] is True, "导出的函数被标记") print("\n导出关系") exports = api["exports"] check("greet" in exports, "from .core import * 的名字被导出") check("sub_function" in exports, "子包的名字被导出") check("special" in exports, "显式 from .extra_only import special 被导出") check("_hidden_helper" not in exports, "私有名字不在导出列表里") print("\n数据表") tables = [table for module in api["modules"] for table in module["data"]] table_names = {t["name"] for t in tables} check("table" in table_names, "函数返回的字面量表被识别") check("SUB_CONSTANTS" in table_names, "模块级字面量表被识别") check(any(t["origin"] == "module" for t in tables), "模块级表被标注来源") print("\n容错") broken = next(m for m in api["modules"] if m["name"] == "broken") check(bool(broken["error"]), "语法错误被记录而不是抛异常") print("\n页面生成") for filename in ("index.html", "api.html", "about.html", "installation.html"): check((output / filename).exists(), f"生成了 {filename}") check(not (output / "modules.html").exists(), "不再生成模块页") check(not (output / "data.html").exists(), "不再生成数据页") installation = (output / "installation.html").read_text(encoding="utf-8") check( "pip install --upgrade mypkg" in installation, "安装页用真实的包名渲染 INSTALLATION.md", ) index = (output / "index.html").read_text(encoding="utf-8") check("mypkg" in index, "首页出现包名") check("guan." not in index, "首页没有残留旧包名") check("import guan" not in index, "首页没有残留旧包的导入写法") check("pip install --upgrade mypkg" in index, "安装命令用真实的包名") check('href="installation.html"' in index, "首页有指向安装页的链接") api_html = (output / "api.html").read_text(encoding="utf-8") check("mypkg.greet" in api_html, "交叉引用使用真实的导入名") check("guan." not in api_html, "API 页没有残留旧包名") check(not build_site.verify_output(output), "生成的页面没有断链") def check_edge(data: dict, output: Path) -> None: api = json.loads((output / "api.json").read_text(encoding="utf-8")) names = [m["name"] for m in api["modules"]] print("\n各种编码与文件形态") check("helper" in names, "空文件也被识别为模块") check("bom" in names, "带 BOM 的文件被识别") check("crlf" in names, "CRLF 换行的文件被识别") check("gbk" in names, "GBK 编码的文件被识别") check("my-module" in names, "文件名不是标识符时也能识别") functions = { f["name"]: f for module in api["modules"] for f in module["functions"] } check("bom_func" in functions, "BOM 文件里的函数被解析") check("crlf_func" in functions, "CRLF 文件里的函数被解析") check("gbk_func" in functions, "GBK 文件里的函数被解析") check( functions.get("gbk_func", {}).get("description") == "中文说明。", "GBK 文件里的中文说明没有乱码", ) check("odd_func" in functions, "特殊文件名的模块里的函数被解析") check("inner" not in functions, "函数内部的嵌套函数不出现在 API 顶层") print("\n类") classes = { c["name"]: c for module in api["modules"] for c in module["classes"] } check("Widget" in classes, "识别到类") widget = classes.get("Widget", {}) method_names = {m["name"] for m in widget.get("methods", [])} check("size" in method_names, "property 被当作方法记录") check("plain" in method_names, "普通方法被记录") check( any(m["name"] == "refresh" and m["is_async"] for m in widget.get("methods", [])), "类里的 async 方法被标记", ) check("kind" in widget.get("attributes", []), "类属性被记录") print("\n相对导入") check("helper" in api["exports"], "from . import helper 被识别为导出") check("os" not in api["exports"], "第三方库(import os)不会被当成导出") check("js" not in api["exports"], "第三方库的别名(import json as js)不会被当成导出") print("\nREADME 与资源") about = (output / "about.html").read_text(encoding="utf-8") check("{{version}}" in about, "README 里的 {{version}} 没有被模板替换掉") check((output / "docs" / "logo.png").exists(), "README 引用的本地图片被复制过来") check("docs/logo.png" in about, "图片的相对路径被保留") check("" in about, "表格缺失的单元格被补齐") check("checkbox" in about, "任务列表被正确渲染") check(not build_site.verify_output(output), "生成的页面没有断链") print("\n源码里的模板占位符") api_html = (output / "api.html").read_text(encoding="utf-8") check("{{version}}" in api_html, "源码里的 {{version}} 没有被替换掉") check( "" not in api_html, "docstring 里的 HTML 被转义,不会被当成标签执行", ) check("<script>" in api_html, "转义后的 HTML 实体出现在页面上") print("\n同名函数") duplicated = [ module for module in api["modules"] if any(f["name"] == "plain_function" for f in module["functions"]) ] check(len(duplicated) >= 1, "同名函数被分别记录") ids = re.findall(r'id="([^"]+)"', api_html) check(len(ids) == len(set(ids)), "页面里的 id 没有重复") check("plain.Container.plain" in ids, "同名方法与函数各有各的锚点") check("plain.plain" in ids, "模块级函数的锚点没有被方法覆盖") check("cls.Widget" in ids, "类本身也有锚点") print("\n交叉参考") use_widget = next( f for module in api["modules"] for f in module["functions"] if f["name"] == "use_widget" ) check("Widget" in use_widget["calls"], "直接 import 进来的包内函数也被认出来") check('' in api_html, "交叉参考指向类自己的锚点") check(not build_site.verify_output(output), "生成的页面没有断链") def check_i18n(data: dict, output: Path) -> None: print("\n中英文界面") check(not i18n.missing_keys(), "两种语言的文案都写全了") for filename in ("index.html", "api.html", "about.html", "installation.html"): if not (output / filename).exists(): continue # 中文页面里就没有的(比如没有 INSTALLATION.md),英文也不该有 check( (output / "en" / filename).exists(), f"生成了 en/{filename}", ) zh_index = (output / "index.html").read_text(encoding="utf-8") en_index = (output / "en" / "index.html").read_text(encoding="utf-8") check('' in zh_index, "中文页面声明了语言") check('' in en_index, "英文页面声明了语言") check(">首页<" in zh_index, "中文页面用中文导航") check(">Home<" in en_index, "英文页面用英文导航") check("模块一览" in zh_index, "中文页面的界面文字是中文") check("模块一览" not in en_index, "英文页面的界面文字是英文") check(">Modules<" in en_index, "英文页面有对应的英文标题") print("\n语言切换") check('href="en/index.html"' in zh_index, "中文页面链到英文页面") check('href="../index.html"' in en_index, "英文页面链回中文页面") zh_api = (output / "api.html").read_text(encoding="utf-8") en_api = (output / "en" / "api.html").read_text(encoding="utf-8") check('href="en/api.html"' in zh_api, "API 页也能切到另一种语言") check('href="../api.html"' in en_api, "英文 API 页能切回中文") print("\n文件内容保持原样") # README、源码里的说明不翻译:两种语言的页面里都应当原样出现 zh_about = (output / "about.html").read_text(encoding="utf-8") en_about = (output / "en" / "about.html").read_text(encoding="utf-8") check("版本号是 {{version}}" in zh_about, "中文页面里是 README 原文") check("版本号是 {{version}}" in en_about, "英文页面里也是 README 原文") check( "A plain function." in en_api, "英文页面里保留源码里的英文说明", ) check("The size." in en_api, "英文页面里保留 docstring 原文") print("\n两种语言的链接都是通的") check(not build_site.verify_output(output), "生成的页面没有断链") check((output / "en" / "static" / "style.css").exists(), "英文目录里有自己的样式") check((output / "en" / "docs" / "logo.png").exists(), "英文目录里也有 README 的图片") def check_cli(tmp: Path) -> None: print("\n命令行与异常处理") try: build_site.generate(source_dir=tmp / "does-not-exist", output=tmp / "nope") check(False, "不存在的源码目录应当报错") except SystemExit: check(True, "不存在的源码目录会给出明确报错而不是崩溃") except Exception as error: # noqa: BLE001 check(False, f"不存在的源码目录应当报 SystemExit,实际是 {error!r}") try: build_site.generate(source_dir=tmp / "not-a-dir.txt", output=tmp / "nope2") check(False, "指向文件的源码路径应当报错") except SystemExit: check(True, "源码路径不是目录时会给出明确报错") except Exception as error: # noqa: BLE001 check(False, f"源码路径不是目录时应当报 SystemExit,实际是 {error!r}") print("\n断链检查本身有效") broken = tmp / "broken-site" broken.mkdir(parents=True, exist_ok=True) (broken / "index.html").write_text( 'xy', encoding="utf-8" ) problems = build_site.verify_output(broken) check(len(problems) == 2, "断链与失效锚点都能被查出来") def check_empty(data: dict, output: Path) -> None: print("\n没有内容时的页面取舍") check(not (output / "api.html").exists(), "没有函数时不生成 API 参考页") check(not (output / "modules.html").exists(), "不再生成模块页") check(not (output / "data.html").exists(), "不再生成数据页") check((output / "index.html").exists(), "首页仍然生成") check( not (output / "installation.html").exists(), "没有 INSTALLATION.md 时不生成安装页", ) index = (output / "index.html").read_text(encoding="utf-8") check("api.html" not in index, "首页没有链到不存在的 API 页") check( "installation.html" not in index, "首页没有链到不存在的安装页", ) check("modules.html" not in index, "首页没有链到已经不存在的模块页") check("data.html" not in index, "首页没有链到已经不存在的数据页") check(not build_site.verify_output(output), "生成的页面没有断链") def check_attr(data: dict, output: Path) -> None: api = json.loads((output / "api.json").read_text(encoding="utf-8")) print("\n版本号的 attr: 写法") check(api["package"]["version"] == "5.6.7", "attr: 形式的版本号被解析出来") check( api["package"]["description"] == "Version read from the package itself", "setup.cfg 里的描述被正确读取", ) check(not build_site.verify_output(output), "生成的页面没有断链") def check_class_only(data: dict, output: Path) -> None: print("\n只有类的时候") check((output / "api.html").exists(), "只有类时也会生成 API 参考页") api_html = (output / "api.html").read_text(encoding="utf-8") check("thing.Thing.go" in api_html, "类的方法有正确的锚点") check(not build_site.verify_output(output), "生成的页面没有断链") TUTORIALS_README = """\ # Tutorials Some examples. ## 一、日常场景 / Daily scenarios | File | 说明 | Requirements | | --- | --- | --- | | `01_hello.py` | 打个招呼 | numpy | """ TUTORIALS_SCRIPT = """\ # 1. 打个招呼 # # 来源 / Source: https://example.com/post # import numpy as np print(np.zeros(3)) """ def make_tutorials(root: Path) -> Path: """造一个 tutorials/ 目录。""" directory = root / "tutorials" (directory / "daily_scenarios").mkdir(parents=True, exist_ok=True) (directory / "README.md").write_text(TUTORIALS_README, encoding="utf-8") (directory / "daily_scenarios" / "01_hello.py").write_text( TUTORIALS_SCRIPT, encoding="utf-8" ) return directory def check_tutorials(tmp: Path) -> None: print("\n教程页") tutorials_dir = make_tutorials(tmp / "t") package = make_package(tmp / "t") output = tmp / "t" / "site-t" data = build_site.generate( source_dir=package, output=output, readme_path=tmp / "t" / "README.md", installation_path=tmp / "t" / "INSTALLATION.md", tutorials_dir=tutorials_dir, quiet=True, ) check((output / "tutorials.html").exists(), "有 tutorials/ 时生成教程页") html = (output / "tutorials.html").read_text(encoding="utf-8") check("一、日常场景" in html, "分组标题来自 README 的章节") check("打个招呼" in html, "脚本标题与说明被收进来") check("np.zeros(3)" in html, "脚本源码被收进来") check( "href=\"tutorials/daily_scenarios/01_hello.py\"" in html, "页面给出脚本的下载地址", ) check( (output / "tutorials" / "daily_scenarios" / "01_hello.py").exists(), "脚本被复制到站点里", ) api = json.loads((output / "api.json").read_text(encoding="utf-8")) check(bool(api.get("tutorials", {}).get("files")), "api.json 里带上教程索引") check( "code" not in api["tutorials"]["groups"][0]["files"][0], "api.json 里不重复存整段源码", ) check(data["tutorials"]["count"] == 1, "教程数量统计正确") check(not build_site.verify_output(output), "生成的页面没有断链") def check_markdown() -> None: print("\nMarkdown 渲染") html = md_render.render("- a\n1. b\n") check("