Skip to content

Commit

Permalink
rust: add rust.doctest
Browse files Browse the repository at this point in the history
Signed-off-by: Paolo Bonzini <[email protected]>
  • Loading branch information
bonzini committed Dec 19, 2024
1 parent 53ba077 commit c7f3350
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 13 deletions.
25 changes: 25 additions & 0 deletions docs/markdown/Rust-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 77 additions & 4 deletions mesonbuild/modules/rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,25 @@
from __future__ import annotations
import itertools
import os
import re
import typing as T

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, 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
Expand All @@ -44,6 +47,7 @@ class FuncRustTest(_kwargs.BaseTest, T.Generic[ArgsType]):
rust_args: T.List[str]

FuncTest = FuncRustTest[_kwargs.TestArgs]
FuncDoctest = FuncRustTest[str]

class FuncBindgen(TypedDict):

Expand All @@ -69,18 +73,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((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):

"""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,
})
Expand Down Expand Up @@ -199,6 +210,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',
Expand Down
2 changes: 1 addition & 1 deletion test cases/rust/9 unit tests/doctest1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
/// ```ignore
/// this one will be skipped
/// ```
fn my_func()
pub fn my_func()
{
}
13 changes: 5 additions & 8 deletions test cases/rust/9 unit tests/meson.build
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -31,22 +31,19 @@ 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'],
)
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')
Expand Down

0 comments on commit c7f3350

Please sign in to comment.