Merge pull request #7860 from dcbaker/wip/2020-10/rust-module

Add a rust module
pull/8163/head
Jussi Pakkanen 4 years ago committed by GitHub
commit c9d9dacdbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CODEOWNERS
  2. 1
      docs/markdown/Reference-manual.md
  3. 35
      docs/markdown/Rust-module.md
  4. 1
      docs/markdown/_Sidebar.md
  5. 4
      docs/markdown/snippets/rust_test_format_support.md
  6. 4
      docs/markdown/snippets/unstable-rust-module.md
  7. 11
      docs/sitemap.txt
  8. 45
      docs/theme/extra/templates/navbar_links.html
  9. 5
      mesonbuild/backend/backends.py
  10. 19
      mesonbuild/interpreter.py
  11. 137
      mesonbuild/modules/unstable_rust.py
  12. 73
      mesonbuild/mtest.py
  13. 1
      run_mypy.py
  14. 43
      test cases/rust/9 unit tests/meson.build
  15. 24
      test cases/rust/9 unit tests/test.rs
  16. 11
      test cases/rust/9 unit tests/test2.rs

@ -2,6 +2,7 @@
/mesonbuild/modules/pkgconfig.py @xclaesse
/mesonbuild/modules/cmake.py @mensinda
/mesonbuild/modules/unstable_external_project.py @xclaesse
/mesonbuild/modules/unstable_rust.py @dcbaker
/mesonbuild/ast/ @mensinda
/mesonbuild/cmake/ @mensinda
/mesonbuild/compilers/ @dcbaker

@ -1745,6 +1745,7 @@ test(..., env: nomalloc, ...)
to record the outcome of the test).
- `tap`: [Test Anything Protocol](https://www.testanything.org/).
- `gtest` *(since 0.55.0)*: for Google Tests.
- `rust` *(since 0.56.0)*: for native rust tests
- `priority` *(since 0.52.0)*:specifies the priority of a test. Tests with a
higher priority are *started* before tests with a lower priority.

@ -0,0 +1,35 @@
---
short-description: Rust language integration module
authors:
- name: Dylan Baker
email: dylan@pnwbakers.com
years: [2020]
...
# Unstable Rust module
*(new in 0.57.0)*
**Note** Unstable modules make no backwards compatible API guarantees.
The rust module provides helper to integrate rust code into meson. The goal
is to make using rust in meson more pleasant, while still remaining mesonic,
this means that it attempts to make rust work more like meson, rather than
meson work more like rust.
## Functions
### test(name: string, target: library | executable, dependencies: []Dependency)
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 adding the `--test`
argument to the compilation, then creates a new test target which calls that
executable, using the rust test protocol.
This accepts all of the keyword arguments as the
[`test`](Reference-manual.md#test) function except `protocol`, it will set
that automatically.
Additional, test only dependencies may be passed via the dependencies
argument.

@ -12,3 +12,4 @@
* [gnome](Gnome-module.md)
* [i18n](i18n-module.md)
* [pkgconfig](Pkgconfig-module.md)
* [rust](Rust-module.md)

@ -0,0 +1,4 @@
## Meson test() now accepts `protocol : 'rust'`
This allows native rust tests to be run and parsed by meson, simply set the
protocol to `rust` and meson takes care of the rest.

@ -0,0 +1,4 @@
## Untable Rust module
A new unstable module has been added to make using rust with meson easier.
Currently it adds a single function to ease defining rust tests.

@ -34,24 +34,25 @@ index.md
Disabler.md
Modules.md
CMake-module.md
Cuda-module.md
Dlang-module.md
External-Project-module.md
Fs-module.md
Gnome-module.md
Hotdoc-module.md
i18n-module.md
Icestorm-module.md
Keyval-module.md
Pkgconfig-module.md
Python-module.md
Python-3-module.md
Python-module.md
Qt4-module.md
Qt5-module.md
RPM-module.md
Rust-module.md
Simd-module.md
SourceSet-module.md
Windows-module.md
Cuda-module.md
Keyval-module.md
External-Project-module.md
i18n-module.md
Java.md
Vala.md
D.md

@ -5,28 +5,29 @@
Modules <span class="caret"></span>
</a>
<ul class="dropdown-menu" id="modules-menu">
@for tup in ( \
("CMake-module.html","CMake"), \
("Cuda-module.html","CUDA"), \
("Dlang-module.html","Dlang"), \
("Fs-module.html","Filesystem"), \
("Gnome-module.html","GNOME"), \
("Hotdoc-module.html","Hotdoc"), \
("i18n-module.html","i18n"), \
("Icestorm-module.html","Icestorm"), \
("Keyval-module.html","Keyval"), \
("Pkgconfig-module.html","Pkgconfig"), \
("Python-module.html","Python"), \
("Python-3-module.html","Python 3"), \
("Qt4-module.html","Qt4"), \
("Qt5-module.html","Qt5"), \
("RPM-module.html","RPM"), \
("SourceSet-module.html","SourceSet"), \
("Windows-module.html","Windows")):
<li>
<a href="@tup[0]">@tup[1]</a>
</li>
@end
@for tup in [ \
("CMake-module.html","CMake"), \
("Cuda-module.html","CUDA"), \
("Dlang-module.html","Dlang"), \
("Fs-module.html","Filesystem"), \
("Gnome-module.html","GNOME"), \
("Hotdoc-module.html","Hotdoc"), \
("Icestorm-module.html","Icestorm"), \
("Keyval-module.html","Keyval"), \
("Pkgconfig-module.html","Pkgconfig"), \
("Python-3-module.html","Python 3"), \
("Python-module.html","Python"), \
("Qt4-module.html","Qt4"), \
("Qt5-module.html","Qt5"), \
("RPM-module.html","RPM"), \
("Rust-module.html","Rust"), \
("SourceSet-module.html","SourceSet"), \
("Windows-module.html","Windows"), \
("i18n-module.html","i18n")]:
<li>
<a href="@tup[0]">@tup[1]</a>
</li>
@end
</ul>
</li>
\

@ -46,6 +46,7 @@ class TestProtocol(enum.Enum):
EXITCODE = 0
TAP = 1
GTEST = 2
RUST = 3
@classmethod
def from_str(cls, string: str) -> 'TestProtocol':
@ -55,6 +56,8 @@ class TestProtocol(enum.Enum):
return cls.TAP
elif string == 'gtest':
return cls.GTEST
elif string == 'rust':
return cls.RUST
raise MesonException('unknown test format {}'.format(string))
def __str__(self) -> str:
@ -62,6 +65,8 @@ class TestProtocol(enum.Enum):
return 'exitcode'
elif self is self.GTEST:
return 'gtest'
elif self is self.RUST:
return 'rust'
return 'tap'

@ -2593,6 +2593,8 @@ class Interpreter(InterpreterBase):
self.process_new_values(v.sources[0])
elif isinstance(v, InstallDir):
self.build.install_dirs.append(v)
elif isinstance(v, Test):
self.build.tests.append(v)
elif hasattr(v, 'held_object'):
pass
elif isinstance(v, (int, str, bool, Disabler)):
@ -4120,7 +4122,7 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
env = env.held_object
return env
def add_test(self, node, args, kwargs, is_base_test):
def make_test(self, node: mparser.BaseNode, args: T.List, kwargs: T.Dict[str, T.Any]):
if len(args) != 2:
raise InterpreterException('test expects 2 arguments, {} given'.format(len(args)))
name = args[0]
@ -4159,8 +4161,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
if not isinstance(timeout, int):
raise InterpreterException('Timeout must be an integer.')
protocol = kwargs.get('protocol', 'exitcode')
if protocol not in {'exitcode', 'tap', 'gtest'}:
raise InterpreterException('Protocol must be "exitcode", "tap", or "gtest".')
if protocol not in {'exitcode', 'tap', 'gtest', 'rust'}:
raise InterpreterException('Protocol must be one of "exitcode", "tap", "gtest", or "rust".')
suite = []
prj = self.subproject if self.is_subproject() else self.build.project_name
for s in mesonlib.stringlistify(kwargs.get('suite', '')):
@ -4174,14 +4176,17 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
priority = kwargs.get('priority', 0)
if not isinstance(priority, int):
raise InterpreterException('Keyword argument priority must be an integer.')
t = Test(name, prj, suite, exe.held_object, depends, par, cmd_args,
env, should_fail, timeout, workdir, protocol, priority)
return Test(name, prj, suite, exe.held_object, depends, par, cmd_args,
env, should_fail, timeout, workdir, protocol, priority)
def add_test(self, node: mparser.BaseNode, args: T.List, kwargs: T.Dict[str, T.Any], is_base_test: bool):
t = self.make_test(node, args, kwargs)
if is_base_test:
self.build.tests.append(t)
mlog.debug('Adding test', mlog.bold(name, True))
mlog.debug('Adding test', mlog.bold(t.name, True))
else:
self.build.benchmarks.append(t)
mlog.debug('Adding benchmark', mlog.bold(name, True))
mlog.debug('Adding benchmark', mlog.bold(t.name, True))
@FeatureNewKwargs('install_headers', '0.47.0', ['install_mode'])
@permittedKwargs(permitted_kwargs['install_headers'])

@ -0,0 +1,137 @@
# Copyright © 2020 Intel Corporation
# 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.
import typing as T
from . import ExtensionModule, ModuleReturnValue
from .. import mlog
from ..build import BuildTarget, Executable, InvalidArguments
from ..dependencies import Dependency, ExternalLibrary
from ..interpreter import ExecutableHolder, permitted_kwargs
from ..interpreterbase import InterpreterException, permittedKwargs, FeatureNew
from ..mesonlib import stringlistify, unholder, listify
if T.TYPE_CHECKING:
from ..interpreter import ModuleState, Interpreter
class RustModule(ExtensionModule):
"""A module that holds helper functions for rust."""
@FeatureNew('rust module', '0.57.0')
def __init__(self, interpreter: 'Interpreter') -> None:
super().__init__(interpreter)
@permittedKwargs(permitted_kwargs['test'] | {'dependencies'} ^ {'protocol'})
def test(self, state: 'ModuleState', args: T.List, kwargs: T.Dict[str, T.Any]) -> ModuleReturnValue:
"""Generate a rust test target from a given rust target.
Rust puts it's unitests inside it's main source files, unlike most
languages that put them in external files. This means that normally
you have to define two seperate targets with basically the same
arguments to get tests:
```meson
rust_lib_sources = [...]
rust_lib = static_library(
'rust_lib',
rust_lib_sources,
)
rust_lib_test = executable(
'rust_lib_test',
rust_lib_sources,
rust_args : ['--test'],
)
test(
'rust_lib_test',
rust_lib_test,
protocol : 'rust',
)
```
This is all fine, but not very DRY. This method makes it much easier
to define rust tests:
```meson
rust = import('unstable-rust')
rust_lib = static_library(
'rust_lib',
[sources],
)
rust.test('rust_lib_test', rust_lib)
```
"""
if len(args) != 2:
raise InterpreterException('rustmod.test() takes exactly 2 positional arguments')
name: str = args[0]
if not isinstance(name, str):
raise InterpreterException('First positional argument to rustmod.test() must be a string')
base_target: BuildTarget = unholder(args[1])
if not isinstance(base_target, BuildTarget):
raise InterpreterException('Second positional argument to rustmod.test() must be a library or executable')
if not base_target.get_using_rustc():
raise InterpreterException('Second positional argument to rustmod.test() must be a rust based target')
extra_args = stringlistify(kwargs.get('args', []))
# Delete any arguments we don't want passed
if '--test' in extra_args:
mlog.warning('Do not add --test to rustmod.test arguments')
extra_args.remove('--test')
if '--format' in extra_args:
mlog.warning('Do not add --format to rustmod.test arguments')
i = extra_args.index('--format')
# Also delete the argument to --format
del extra_args[i + 1]
del extra_args[i]
for i, a in enumerate(extra_args):
if a.startswith('--format='):
del extra_args[i]
break
dependencies = unholder(listify(kwargs.get('dependencies', [])))
for d in dependencies:
if not isinstance(d, (Dependency, ExternalLibrary)):
raise InvalidArguments('dependencies must be a dependency or external library')
kwargs['args'] = extra_args + ['--test', '--format', 'pretty']
kwargs['protocol'] = 'rust'
new_target_kwargs = base_target.kwargs.copy()
# Don't mutate the shallow copied list, instead replace it with a new
# one
new_target_kwargs['rust_args'] = new_target_kwargs.get('rust_args', []) + ['--test']
new_target_kwargs['install'] = False
new_target_kwargs['dependencies'] = new_target_kwargs.get('dependencies', []) + dependencies
new_target = Executable(
name, base_target.subdir, state.subproject,
base_target.for_machine, base_target.sources,
base_target.objects, base_target.environment,
new_target_kwargs
)
e = ExecutableHolder(new_target, self.interpreter)
test = self.interpreter.make_test(
self.interpreter.current_node, [name, e], kwargs)
return ModuleReturnValue([], [e, test])
def initialize(*args: T.List, **kwargs: T.Dict) -> RustModule:
return RustModule(*args, **kwargs) # type: ignore

@ -595,17 +595,17 @@ class JunitBuilder(TestLogger):
'testsuite',
name=suitename,
tests=str(len(test.results)),
errors=str(sum(1 for r in test.results if r in
errors=str(sum(1 for r in test.results.values() if r in
{TestResult.INTERRUPT, TestResult.ERROR})),
failures=str(sum(1 for r in test.results if r in
failures=str(sum(1 for r in test.results.values() if r in
{TestResult.FAIL, TestResult.UNEXPECTEDPASS, TestResult.TIMEOUT})),
skipped=str(sum(1 for r in test.results if r is TestResult.SKIP)),
skipped=str(sum(1 for r in test.results.values() if r is TestResult.SKIP)),
)
for i, result in enumerate(test.results):
for i, result in test.results.items():
# Both name and classname are required. Set them both to the
# number of the test in a TAP test, as TAP doesn't give names.
testcase = et.SubElement(suite, 'testcase', name=str(i), classname=str(i))
testcase = et.SubElement(suite, 'testcase', name=i, classname=i)
if result is TestResult.SKIP:
et.SubElement(testcase, 'skipped')
elif result is TestResult.ERROR:
@ -666,6 +666,28 @@ class JunitBuilder(TestLogger):
tree.write(f, encoding='utf-8', xml_declaration=True)
def parse_rust_test(stdout: str) -> T.Dict[str, TestResult]:
"""Parse the output of rust tests."""
res = {} # type; T.Dict[str, TestResult]
def parse_res(res: str) -> TestResult:
if res == 'ok':
return TestResult.OK
elif res == 'ignored':
return TestResult.SKIP
elif res == 'FAILED':
return TestResult.FAIL
raise MesonException('Unsupported output from rust test: {}'.format(res))
for line in stdout.splitlines():
if line.startswith('test ') and not line.startswith('test result'):
_, name, _, result = line.split(' ')
name = name.replace('::', '.')
res[name] = parse_res(result)
return res
class TestRun:
TEST_NUM = 0
@ -675,7 +697,7 @@ class TestRun:
self.test = test
self._num = None # type: T.Optional[int]
self.name = name
self.results = list() # type: T.List[TestResult]
self.results: T.Dict[str, TestResult] = {}
self.returncode = 0
self.starttime = None # type: T.Optional[float]
self.duration = None # type: T.Optional[float]
@ -713,23 +735,23 @@ class TestRun:
res = TestResult.EXPECTEDFAIL if bool(returncode) else TestResult.UNEXPECTEDPASS
else:
res = TestResult.FAIL if bool(returncode) else TestResult.OK
self.complete(res, [], returncode, stdo, stde, cmd, **kwargs)
self.complete(res, {}, returncode, stdo, stde, cmd, **kwargs)
def complete_tap(self, returncode: int, stdo: str, stde: str, cmd: T.List[str]) -> None:
res = None # type: T.Optional[TestResult]
results = [] # type: T.List[TestResult]
results = {} # type: T.Dict[str, TestResult]
failed = False
for i in TAPParser(io.StringIO(stdo)).parse():
for n, i in enumerate(TAPParser(io.StringIO(stdo)).parse()):
if isinstance(i, TAPParser.Bailout):
results.append(TestResult.ERROR)
results[str(n)] = TestResult.ERROR
failed = True
elif isinstance(i, TAPParser.Test):
results.append(i.result)
results[str(n)] = i.result
if i.result not in {TestResult.OK, TestResult.EXPECTEDFAIL, TestResult.SKIP}:
failed = True
elif isinstance(i, TAPParser.Error):
results.append(TestResult.ERROR)
results[str(n)] = TestResult.ERROR
stde += '\nTAP parsing error: ' + i.message
failed = True
@ -739,7 +761,7 @@ class TestRun:
if res is None:
# Now determine the overall result of the test based on the outcome of the subcases
if all(t is TestResult.SKIP for t in results):
if all(t is TestResult.SKIP for t in results.values()):
# This includes the case where num_tests is zero
res = TestResult.SKIP
elif self.should_fail:
@ -749,6 +771,21 @@ class TestRun:
self.complete(res, results, returncode, stdo, stde, cmd)
def complete_rust(self, returncode: int, stdo: str, stde: str, cmd: T.List[str]) -> None:
results = parse_rust_test(stdo)
failed = TestResult.FAIL in results.values()
# Now determine the overall result of the test based on the outcome of the subcases
if all(t is TestResult.SKIP for t in results.values()):
# This includes the case where num_tests is zero
res = TestResult.SKIP
elif self.should_fail:
res = TestResult.EXPECTEDFAIL if failed else TestResult.UNEXPECTEDPASS
else:
res = TestResult.FAIL if failed else TestResult.OK
self.complete(res, results, returncode, stdo, stde, cmd)
@property
def num(self) -> int:
if self._num is None:
@ -756,13 +793,13 @@ class TestRun:
self._num = TestRun.TEST_NUM
return self._num
def complete(self, res: TestResult, results: T.List[TestResult],
def complete(self, res: TestResult, results: T.Dict[str, TestResult],
returncode: int,
stdo: T.Optional[str], stde: T.Optional[str],
cmd: T.List[str], *, junit: T.Optional[et.ElementTree] = None) -> None:
assert isinstance(res, TestResult)
self.res = res
self.results = results
self.results = results # May be empty
self.returncode = returncode
self.duration = time.time() - self.starttime
self.stdo = stdo
@ -906,7 +943,7 @@ class SingleTestRunner:
self.runobj.start()
if cmd is None:
skip_stdout = 'Not run because can not execute cross compiled binaries.'
self.runobj.complete(TestResult.SKIP, [], GNU_SKIP_RETURNCODE, skip_stdout, None, None)
self.runobj.complete(TestResult.SKIP, {}, GNU_SKIP_RETURNCODE, skip_stdout, None, None)
else:
wrap = TestHarness.get_wrapper(self.options)
if self.options.gdb:
@ -1063,12 +1100,14 @@ class SingleTestRunner:
stdo = ""
stde = additional_error
if result:
self.runobj.complete(result, [], returncode, stdo, stde, cmd)
self.runobj.complete(result, {}, returncode, stdo, stde, cmd)
else:
if self.test.protocol is TestProtocol.EXITCODE:
self.runobj.complete_exitcode(returncode, stdo, stde, cmd)
elif self.test.protocol is TestProtocol.GTEST:
self.runobj.complete_gtest(returncode, stdo, stde, cmd)
elif self.test.protocol is TestProtocol.RUST:
return self.runobj.complete_rust(returncode, stdo, stde, cmd)
else:
if self.options.verbose:
print(stdo, end='')

@ -29,6 +29,7 @@ modules = [
'mesonbuild/mintro.py',
'mesonbuild/mlog.py',
'mesonbuild/modules/fs.py',
'mesonbuild/modules/unstable_rust.py',
'mesonbuild/mparser.py',
'mesonbuild/msetup.py',
'mesonbuild/mtest.py',

@ -0,0 +1,43 @@
project('rust unit tests', 'rust')
t = executable(
'rust_test',
['test.rs'],
rust_args : ['--test'],
)
test(
'rust test (should fail)',
t,
protocol : 'rust',
suite : ['foo'],
should_fail : true,
)
test(
'rust test (should pass)',
t,
args : ['--skip', 'test_add_intentional_fail'],
protocol : 'rust',
suite : ['foo'],
)
test(
'rust test (should skip)',
t,
args : ['--skip', 'test_add'],
protocol : 'rust',
suite : ['foo'],
)
exe = executable('rust_exe', ['test2.rs', 'test.rs'])
rust = import('unstable-rust')
rust.test('rust_test_from_exe', exe, should_fail : true)
lib = static_library('rust_static', ['test.rs'])
rust.test('rust_test_from_static', lib, args: ['--skip', 'test_add_intentional_fail'])
lib = shared_library('rust_shared', ['test.rs'])
rust.test('rust_test_from_shared', lib, args: ['--skip', 'test_add_intentional_fail'])

@ -0,0 +1,24 @@
pub fn add(a: i32, b: i32) -> i32 {
return a + b;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_add_intentional_fail() {
assert_eq!(add(1, 2), 5);
}
#[test]
#[ignore]
fn test_add_intentional_fail2() {
assert_eq!(add(1, 7), 5);
}
}

@ -0,0 +1,11 @@
mod test;
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let first = args[1].parse::<i32>().expect("Invliad value for first argument.");
let second = args[2].parse::<i32>().expect("Invliad value for second argument.");
let new = test::add(first, second);
println!("New value: {}", new);
}
Loading…
Cancel
Save