summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaolo Bonzini <pbonzini@redhat.com>2025-02-13 16:42:09 +0100
committerDylan Baker <dylan@pnwbakers.com>2025-04-02 08:44:37 -0700
commitbf8d4927238a40dc0dca584e91988f456a970bbb (patch)
tree742a1e206d52a8c542208a2b706e4f5e6d300e20
parent00dc6fa4dfee880a3c3c17f658e6d0b98a93bab6 (diff)
downloadmeson-bf8d4927238a40dc0dca584e91988f456a970bbb.tar.gz
rust: add rust.doctest
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
-rw-r--r--docs/markdown/Rust-module.md27
-rw-r--r--mesonbuild/modules/rust.py79
-rw-r--r--test cases/rust/9 unit tests/doctest1.rs2
-rw-r--r--test cases/rust/9 unit tests/meson.build13
4 files changed, 108 insertions, 13 deletions
diff --git a/docs/markdown/Rust-module.md b/docs/markdown/Rust-module.md
index ee095e9d6..6e8202a6f 100644
--- a/docs/markdown/Rust-module.md
+++ b/docs/markdown/Rust-module.md
@@ -24,6 +24,8 @@ like Meson, rather than Meson work more like rust.
rustmod.test(name, target, ...)
```
+*Since 1.8.0*
+
This function creates a new rust unittest target from an existing rust
based target, which may be a library or executable. It does this by
copying the sources and arguments passed to the original target and
@@ -41,6 +43,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 7e599505d..fdb18261e 100644
--- a/mesonbuild/modules/rust.py
+++ b/mesonbuild/modules/rust.py
@@ -4,6 +4,7 @@
from __future__ import annotations
import itertools
import os
+import re
import typing as T
from mesonbuild.interpreterbase.decorators import FeatureNew
@@ -11,15 +12,16 @@ from mesonbuild.interpreterbase.decorators import FeatureNew
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_for_subproject, lang_suffixes
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
@@ -45,6 +47,7 @@ if T.TYPE_CHECKING:
rust_args: T.List[str]
FuncTest = FuncRustTest[_kwargs.TestArgs]
+ FuncDoctest = FuncRustTest[str]
class FuncBindgen(TypedDict):
@@ -70,6 +73,11 @@ RUST_TEST_KWS: T.List[KwargInfo] = [
KwargInfo('is_parallel', bool, default=False),
]
+def no_spaces_validator(arg: T.Optional[T.Union[str, T.List]]) -> T.Optional[str]:
+ if any(bool(re.search(r'\s', x)) for x in arg):
+ return 'must not contain spaces due to limitations of rustdoc'
+ return None
+
class RustModule(ExtensionModule):
@@ -77,6 +85,7 @@ class RustModule(ExtensionModule):
INFO = ModuleInfo('rust', '0.57.0', stabilized='1.0.0')
_bindgen_rust_target: T.Optional[str]
+ rustdoc: PerMachine[T.Optional[ExternalProgram]] = PerMachine(None, None)
def __init__(self, interpreter: Interpreter) -> None:
super().__init__(interpreter)
@@ -89,6 +98,7 @@ class RustModule(ExtensionModule):
self._bindgen_set_std = False
self.methods.update({
'test': self.test,
+ 'doctest': self.doctest,
'bindgen': self.bindgen,
'proc_macro': self.proc_macro,
})
@@ -207,6 +217,67 @@ class RustModule(ExtensionModule):
return ModuleReturnValue(None, [new_target, test])
+ @FeatureNew('rust.doctest', '1.8.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 = T.cast('RustCompiler', base_target.compilers['rust'])
+ 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 d270f7d67..da42792b8 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 4d04ee892..0fa2fa80b 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.8.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_abi: 'c')