From 7b380befa57a00e162117dec9c13c01e5810d1e5 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 20 Nov 2024 11:24:08 +0100 Subject: [PATCH] rust: add rust.doctest Signed-off-by: Paolo Bonzini --- docs/markdown/Rust-module.md | 25 ++++++++ mesonbuild/modules/rust.py | 80 ++++++++++++++++++++++-- test cases/rust/9 unit tests/doctest1.rs | 2 +- test cases/rust/9 unit tests/meson.build | 13 ++-- 4 files changed, 107 insertions(+), 13 deletions(-) diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md index ee095e9d6da1..afac67c27846 100644 --- a/docs/markdown/Rust-module.md +++ b/docs/markdown/Rust-module.md @@ -41,6 +41,31 @@ It also takes the following keyword arguments: This function also accepts all of the keyword arguments accepted by the [[test]] function except `protocol`, it will set that automatically. +### doctest() + +```meson +rustmod.doctest(name, target, ...) +``` + +This function creates a new `test()` target from an existing rust +based library target. The test will use `rustdoc` to extract and run +the doctests that are included in `target`'s sources. + +This function takes two positional arguments, the first is the name of the +test and the second is the library or executable that is the rust based target. +It also takes the following keyword arguments: + +- `dependencies`: a list of test-only Dependencies +- `link_with`: a list of additional build Targets to link with +- `rust_args`: a list of extra arguments passed to the Rust compiler + +The target is linked automatically into the doctests. + +This function also accepts all of the keyword arguments accepted by the +[[test]] function except `protocol`, it will set that automatically. +However, arguments are limited to strings that do not contain spaces +due to limitations of `rustdoc`. + ### bindgen() This function wraps bindgen to simplify creating rust bindings around C diff --git a/mesonbuild/modules/rust.py b/mesonbuild/modules/rust.py index 191694cffcc8..49691d4e0ea9 100644 --- a/mesonbuild/modules/rust.py +++ b/mesonbuild/modules/rust.py @@ -11,15 +11,17 @@ from . import ExtensionModule, ModuleReturnValue, ModuleInfo from .. import mesonlib, mlog from ..build import (BothLibraries, BuildTarget, CustomTargetIndex, Executable, ExtractedObjects, GeneratedList, - CustomTarget, InvalidArguments, Jar, StructuredSources, SharedLibrary) + CustomTarget, InvalidArguments, Jar, StructuredSources, SharedLibrary, StaticLibrary) from ..compilers.compilers import are_asserts_disabled, lang_suffixes +from ..compilers.rust import RustCompiler from ..interpreter.type_checking import ( - DEPENDENCIES_KW, LINK_WITH_KW, SHARED_LIB_KWS, TEST_KWS, OUTPUT_KW, + DEPENDENCIES_KW, LINK_WITH_KW, SHARED_LIB_KWS, TEST_KWS, TEST_KWS_NO_ARGS, OUTPUT_KW, INCLUDE_DIRECTORIES, SOURCES_VARARGS, NoneType, in_set_validator ) from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, typed_kwargs, typed_pos_args, noPosargs, permittedKwargs -from ..mesonlib import File -from ..programs import ExternalProgram +from ..interpreter.interpreterobjects import Doctest +from ..mesonlib import File, MesonException, PerMachine +from ..programs import ExternalProgram, NonExistingExternalProgram if T.TYPE_CHECKING: from . import ModuleState @@ -44,6 +46,7 @@ class FuncRustTest(TypedDict, _kwargs.BaseTest, T.Generic[ArgsType]): rust_args: T.List[str] FuncTest = FuncRustTest[_kwargs.TestArgs] + FuncDoctest = FuncRustTest[str] class FuncBindgen(TypedDict): @@ -69,18 +72,25 @@ class FuncBindgen(TypedDict): KwargInfo('is_parallel', bool, default=False), ] +def no_spaces_validator(arg: T.Optional[T.Union[str, T.List]]) -> T.Optional[str]: + if any((len(x.split()) > 1 for x in arg)): + return 'must not contain spaces due to limitations of rustdoc' + return None + class RustModule(ExtensionModule): """A module that holds helper functions for rust.""" INFO = ModuleInfo('rust', '0.57.0', stabilized='1.0.0') + rustdoc: PerMachine[T.Optional[ExternalProgram]] = PerMachine(None, None) def __init__(self, interpreter: Interpreter) -> None: super().__init__(interpreter) self._bindgen_bin: T.Optional[T.Union[ExternalProgram, Executable, OverrideProgram]] = None self.methods.update({ 'test': self.test, + 'doctest': self.doctest, 'bindgen': self.bindgen, 'proc_macro': self.proc_macro, }) @@ -199,6 +209,68 @@ def test(self, state: ModuleState, args: T.Tuple[str, BuildTarget], kwargs: Func return ModuleReturnValue(None, [new_target, test]) + @FeatureNew('rust.doctest', '1.7.0') + @typed_pos_args('rust.doctest', str, BuildTarget) + @typed_kwargs( + 'rust.doctest', + *TEST_KWS_NO_ARGS, + DEPENDENCIES_KW, + LINK_WITH_KW, + *RUST_TEST_KWS, + KwargInfo( + 'args', + ContainerTypeInfo(list, str), + listify=True, + default=[], + validator=no_spaces_validator, + ), + ) + def doctest(self, state: ModuleState, args: T.Tuple[str, T.Union[SharedLibrary, StaticLibrary]], kwargs: FuncDoctest) -> ModuleReturnValue: + name, base_target = args + + # Link the base target's crate into the tests + kwargs['link_with'].append(base_target) + kwargs['depends'].append(base_target) + workdir = kwargs['workdir'] + kwargs['workdir'] = None + new_target, tkwargs = self.test_common('doctest', state, args, kwargs) + + # added automatically by rustdoc; keep things simple + tkwargs['args'].remove('--test') + + # --test-args= is "parsed" simply via the Rust function split_whitespace(). + # This means no quoting nightmares (pfew) but it also means no spaces. + # Unfortunately it's pretty hard at this point to accept e.g. CustomTarget, + # because their paths may not be known. This is not a big deal because the + # user does not control the test harness, so make things easy and allow + # strings only. + if tkwargs['args']: + tkwargs['args'] = ['--test-args=' + ' '.join(T.cast(T.Sequence[str], tkwargs['args']))] + if workdir: + tkwargs['args'].append('--test-run-directory=' + workdir) + + if self.rustdoc[base_target.for_machine] is None: + rustc = base_target.compilers['rust'] + assert isinstance(rustc, RustCompiler) + rustdoc = rustc.get_rustdoc(state.environment) + if rustdoc: + self.rustdoc[base_target.for_machine] = ExternalProgram(rustdoc.get_exe()) + else: + self.rustdoc[base_target.for_machine] = NonExistingExternalProgram() + + rustdoc_prog = self.rustdoc[base_target.for_machine] + if not rustdoc_prog.found(): + raise MesonException(f'could not find rustdoc for {base_target.for_machine} machine') + + doctests: Doctest = self.interpreter.make_test( + self.interpreter.current_node, (name, rustdoc_prog), tkwargs, Doctest) + + # Note that the new_target is intentionally not returned, as it + # is only reached via the base_target and never built by "ninja" + doctests.target = new_target + base_target.doctests = doctests + return ModuleReturnValue(None, [doctests]) + @noPosargs @typed_kwargs( 'rust.bindgen', diff --git a/test cases/rust/9 unit tests/doctest1.rs b/test cases/rust/9 unit tests/doctest1.rs index d270f7d67047..da42792b8be0 100644 --- a/test cases/rust/9 unit tests/doctest1.rs +++ b/test cases/rust/9 unit tests/doctest1.rs @@ -7,6 +7,6 @@ /// ```ignore /// this one will be skipped /// ``` -fn my_func() +pub fn my_func() { } diff --git a/test cases/rust/9 unit tests/meson.build b/test cases/rust/9 unit tests/meson.build index aa9da679693e..7c5fa8cb037c 100644 --- a/test cases/rust/9 unit tests/meson.build +++ b/test cases/rust/9 unit tests/meson.build @@ -1,4 +1,4 @@ -project('rust unit tests', 'rust', meson_version: '>=1.2.0') +project('rust unit tests', 'rust', meson_version: '>=1.7.0') t = executable( 'rust_test', @@ -31,14 +31,12 @@ test( suite : ['foo'], ) +rust = import('rust') + rustdoc = find_program('rustdoc', required: false) if rustdoc.found() - # rustdoc is invoked mostly like rustc. This is a simple example - # where it is easy enough to invoke it by hand. - test( - 'rust doctest', - rustdoc, - args : ['--test', '--crate-name', 'doctest1', '--crate-type', 'lib', files('doctest1.rs')], + doclib = static_library('rust_doc_lib', ['doctest1.rs'], build_by_default : false) + rust.doctest('rust doctests', doclib, protocol : 'rust', suite : ['doctests'], ) @@ -46,7 +44,6 @@ endif exe = executable('rust_exe', ['test2.rs', 'test.rs'], build_by_default : false) -rust = import('rust') rust.test('rust_test_from_exe', exe, should_fail : true) lib = static_library('rust_static', ['test.rs'], build_by_default : false, rust_crate_type : 'lib')