aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDylan Baker <dylan@pnwbakers.com>2020-10-16 12:37:30 -0700
committerDylan Baker <dylan@pnwbakers.com>2021-01-05 10:23:41 -0800
commitd89ec98b4763cda13da0ae22515c27f4dfe5c1b9 (patch)
tree77c7d63e029c3e3a0cb369124a257a6f21ba763c
parent07ff9c61fed420af33f9d1a561512ff2c6cd21d2 (diff)
downloadmeson-d89ec98b4763cda13da0ae22515c27f4dfe5c1b9.zip
meson-d89ec98b4763cda13da0ae22515c27f4dfe5c1b9.tar.gz
meson-d89ec98b4763cda13da0ae22515c27f4dfe5c1b9.tar.bz2
mtest: Add support for rust unit tests
Rust has it's own built in unit test format, which is invoked by compiling a rust executable with the `--test` flag to rustc. The tests are then run by simply invoking that binary. They output a custom test format, which this patch adds parsing support for. This means that we can report each subtest in the junit we generate correctly, which should be helpful for orchestration systems like gitlab and jenkins which can parse junit XML.
-rw-r--r--docs/markdown/Reference-manual.md1
-rw-r--r--docs/markdown/snippets/rust_test_format_support.md4
-rw-r--r--mesonbuild/backend/backends.py5
-rw-r--r--mesonbuild/interpreter.py4
-rw-r--r--mesonbuild/mtest.py39
-rw-r--r--test cases/rust/9 unit tests/meson.build32
-rw-r--r--test cases/rust/9 unit tests/test.rs24
7 files changed, 107 insertions, 2 deletions
diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md
index 924047c..525c3da 100644
--- a/docs/markdown/Reference-manual.md
+++ b/docs/markdown/Reference-manual.md
@@ -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.
diff --git a/docs/markdown/snippets/rust_test_format_support.md b/docs/markdown/snippets/rust_test_format_support.md
new file mode 100644
index 0000000..69e9aa1
--- /dev/null
+++ b/docs/markdown/snippets/rust_test_format_support.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.
diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py
index ec3aca6..9bb870c 100644
--- a/mesonbuild/backend/backends.py
+++ b/mesonbuild/backend/backends.py
@@ -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'
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
index c20c205..0c6bb99 100644
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -4159,8 +4159,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', '')):
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
index c31ad1a..9db271e 100644
--- a/mesonbuild/mtest.py
+++ b/mesonbuild/mtest.py
@@ -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
@@ -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:
@@ -1069,6 +1106,8 @@ class SingleTestRunner:
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='')
diff --git a/test cases/rust/9 unit tests/meson.build b/test cases/rust/9 unit tests/meson.build
new file mode 100644
index 0000000..44fd6b6
--- /dev/null
+++ b/test cases/rust/9 unit tests/meson.build
@@ -0,0 +1,32 @@
+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'],
+)
diff --git a/test cases/rust/9 unit tests/test.rs b/test cases/rust/9 unit tests/test.rs
new file mode 100644
index 0000000..0225be5
--- /dev/null
+++ b/test cases/rust/9 unit tests/test.rs
@@ -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);
+ }
+}