Skip to content

Commit c9ca4c1

Browse files
Add tests and workflow for package build / testing (#22)
Signed-off-by: Luca Della Vedova <[email protected]>
1 parent 1d890ff commit c9ca4c1

File tree

9 files changed

+260
-5
lines changed

9 files changed

+260
-5
lines changed
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
inputs:
2+
codecov_token:
3+
description: Codecov secret token
4+
required: true
5+
6+
runs:
7+
using: composite
8+
steps:
9+
- uses: colcon/ci/.github/workflows/pytest.yaml@main
10+
env:
11+
CODECOV_TOKEN: ${{ inputs.codecov_token }}

.github/workflows/ci.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ on:
88

99
jobs:
1010
pytest:
11-
uses: colcon/ci/.github/workflows/pytest.yaml@main
11+
uses: ./.github/workflows/pytest.yaml
12+
with:
13+
prerun-step: 'cargo install cargo-ament-build'
1214
secrets:
1315
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/pytest.yaml

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Forked from colcon/ci to allow cargo install step, ref https://github.com/colcon/ci/pull/33
2+
# TODO(anyone) revisit if a better solution pops up from the upstream PR
3+
---
4+
name: Run tests
5+
6+
on: # yamllint disable-line rule:truthy
7+
workflow_call:
8+
inputs:
9+
codecov:
10+
description: 'run codecov action after testing'
11+
default: true
12+
required: false
13+
type: boolean
14+
matrix-filter:
15+
description: 'jq filter string indicating which configuration(s)
16+
should be included'
17+
default: '.'
18+
required: false
19+
type: string
20+
repository:
21+
description: 'repository to test if different from current'
22+
default: ''
23+
required: false
24+
type: string
25+
setup-repository:
26+
description: 'repository used during job setup'
27+
default: 'colcon/ci'
28+
required: false
29+
type: string
30+
prerun-step:
31+
description: 'instruction to run before the testing'
32+
default: ''
33+
required: false
34+
type: string
35+
secrets:
36+
CODECOV_TOKEN:
37+
description: 'token to use when running codecov action after testing'
38+
required: false
39+
40+
jobs:
41+
setup:
42+
runs-on: ubuntu-latest
43+
outputs:
44+
strategy: ${{ steps.load.outputs.strategy }}
45+
steps:
46+
- uses: actions/checkout@v4
47+
with:
48+
repository: ${{ inputs.setup-repository }}
49+
- id: load
50+
run: |
51+
strategy=$(jq -c -M '${{ inputs.matrix-filter }}' strategy.json)
52+
echo "strategy=${strategy}" >> $GITHUB_OUTPUT
53+
54+
pytest:
55+
needs: [setup]
56+
strategy: ${{ fromJson(needs.setup.outputs.strategy) }}
57+
runs-on: ${{ matrix.os }}
58+
59+
steps:
60+
- uses: actions/checkout@v4
61+
with:
62+
repository: ${{ inputs.repository }}
63+
- run: bash -c "${{ inputs.prerun-step }}"
64+
if: ${{ inputs.prerun-step != ''}}
65+
- uses: actions/setup-python@v5
66+
with:
67+
python-version: ${{ matrix.python }}
68+
- uses: actions/checkout@v4
69+
with:
70+
repository: ${{ inputs.setup-repository }}
71+
path: ./.github-ci-action-repo
72+
- uses: ./.github-ci-action-repo
73+
- uses: codecov/codecov-action@v4
74+
env:
75+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
76+
if: ${{ inputs.codecov }}

colcon_ros_cargo/task/ament_cargo/build.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ def write_cargo_config_toml(package_paths):
104104
config_dir = Path.cwd() / '.cargo'
105105
config_dir.mkdir(exist_ok=True)
106106
cargo_config_toml_out = config_dir / 'config.toml'
107-
cargo_config_toml_out.unlink(missing_ok=True)
108-
toml.dump(content, cargo_config_toml_out.open('w'))
107+
with cargo_config_toml_out.open('w') as toml_file:
108+
toml.dump(content, toml_file)
109109

110110

111111
def find_installed_cargo_packages(env):
@@ -118,8 +118,8 @@ def find_installed_cargo_packages(env):
118118
prefix_for_package = {}
119119
ament_prefix_path_var = env.get('AMENT_PREFIX_PATH')
120120
if ament_prefix_path_var is None:
121-
logger.warn('AMENT_PREFIX_PATH is empty. '
122-
'You probably intended to source a ROS installation.')
121+
logger.warning('AMENT_PREFIX_PATH is empty. '
122+
'You probably intended to source a ROS installation.')
123123
prefixes = []
124124
else:
125125
prefixes = ament_prefix_path_var.split(os.pathsep)

test/rust-sample-package/Cargo.toml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "rust-sample-package"
3+
version = "0.1.0"
4+
authors = ["Test McTestface <[email protected]>"]
5+
edition = "2018"
6+
7+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8+
9+
[dependencies]

test/rust-sample-package/package.xml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<package format="3">
2+
<name>rust-sample-package</name>
3+
<version>0.0.1</version>
4+
<description>A sample package for testing</description>
5+
<maintainer email="[email protected]">Test McTestface</maintainer>
6+
<license>Apache License 2.0</license>
7+
8+
<export>
9+
<build_type>ament_cargo</build_type>
10+
</export>
11+
</package>

test/rust-sample-package/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/// Failing doctest example
2+
/// ```
3+
/// invalid_syntax
4+
/// ```
5+
pub struct Type;

test/rust-sample-package/src/main.rs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
4+
5+
#[cfg(test)]
6+
mod tests {
7+
8+
#[test]
9+
fn ok() -> Result<(), ()> {
10+
Ok(())
11+
}
12+
13+
#[test]
14+
fn err() -> Result<(), ()> {
15+
Err(())
16+
}
17+
}

test/test_build.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Copyright 2024 Open Source Robotics Foundation, Inc.
2+
# Licensed under the Apache License, Version 2.0
3+
4+
import asyncio
5+
import os
6+
from pathlib import Path
7+
import shutil
8+
import tempfile
9+
from types import SimpleNamespace
10+
import xml.etree.ElementTree as eTree
11+
12+
from colcon_core.event_handler.console_direct import ConsoleDirectEventHandler
13+
from colcon_core.package_descriptor import PackageDescriptor
14+
from colcon_core.subprocess import new_event_loop
15+
from colcon_core.task import TaskContext
16+
from colcon_ros_cargo.package_identification.ament_cargo import AmentCargoPackageIdentification # noqa: E501
17+
from colcon_ros_cargo.task.ament_cargo.build import AmentCargoBuildTask
18+
from colcon_ros_cargo.task.ament_cargo.test import AmentCargoTestTask
19+
import pytest
20+
21+
TEST_PACKAGE_NAME = 'rust-sample-package'
22+
23+
test_project_path = Path(__file__).parent / TEST_PACKAGE_NAME
24+
25+
26+
@pytest.fixture(autouse=True)
27+
def monkey_patch_put_event_into_queue(monkeypatch):
28+
event_handler = ConsoleDirectEventHandler()
29+
monkeypatch.setattr(
30+
TaskContext,
31+
'put_event_into_queue',
32+
lambda self, event: event_handler((event, 'cargo')),
33+
)
34+
35+
36+
def test_package_identification():
37+
cpi = AmentCargoPackageIdentification()
38+
desc = PackageDescriptor(test_project_path)
39+
cpi.identify(desc)
40+
assert desc.type == 'ament_cargo'
41+
assert desc.name == TEST_PACKAGE_NAME
42+
43+
44+
@pytest.mark.skipif(
45+
not shutil.which('cargo'),
46+
reason='Rust must be installed to run this test')
47+
def test_build_and_test_package():
48+
event_loop = new_event_loop()
49+
asyncio.set_event_loop(event_loop)
50+
51+
try:
52+
cpi = AmentCargoPackageIdentification()
53+
package = PackageDescriptor(test_project_path)
54+
cpi.identify(package)
55+
56+
with tempfile.TemporaryDirectory() as tmpdir:
57+
tmpdir = Path(tmpdir)
58+
# TODO(luca) Also test clean build and cargo args
59+
context = TaskContext(pkg=package,
60+
args=SimpleNamespace(
61+
path=str(test_project_path),
62+
build_base=str(tmpdir / 'build'),
63+
install_base=str(tmpdir / 'install'),
64+
clean_build=None,
65+
cargo_args=None,
66+
lookup_in_workspace=None,
67+
),
68+
dependencies={}
69+
)
70+
71+
task = AmentCargoBuildTask()
72+
task.set_context(context=context)
73+
74+
src_base = test_project_path / 'src'
75+
76+
source_files_before = set(src_base.rglob('*'))
77+
rc = event_loop.run_until_complete(task.build())
78+
assert not rc
79+
source_files_after = set(src_base.rglob('*'))
80+
assert source_files_before == source_files_after
81+
82+
# Make sure the binary is compiled
83+
install_base = Path(task.context.args.install_base)
84+
app_name = TEST_PACKAGE_NAME
85+
executable = TEST_PACKAGE_NAME
86+
# Executable in windows have a .exe extension
87+
if os.name == 'nt':
88+
executable += '.exe'
89+
assert (install_base / 'lib' / app_name / executable).is_file()
90+
91+
# Now compile tests
92+
task = AmentCargoTestTask()
93+
task.set_context(context=context)
94+
95+
# Expect tests to have failed but return code will still be 0
96+
# since testing run succeeded
97+
rc = event_loop.run_until_complete(task.test())
98+
assert not rc
99+
build_base = Path(task.context.args.build_base)
100+
101+
# Make sure the testing files are built
102+
assert (build_base / 'debug' / 'deps').is_dir()
103+
assert len(os.listdir(build_base / 'debug' / 'deps')) > 0
104+
result_file_path = build_base / 'cargo_test.xml'
105+
assert result_file_path.is_file()
106+
check_result_file(result_file_path)
107+
108+
finally:
109+
event_loop.close()
110+
111+
112+
# Check the testing result file, expect cargo test and doc test to fail
113+
# but fmt to succeed
114+
def check_result_file(path):
115+
tree = eTree.parse(path)
116+
root = tree.getroot()
117+
testsuite = root.find('testsuite')
118+
assert testsuite is not None
119+
unit_result = testsuite.find("testcase[@name='unit']")
120+
assert unit_result is not None
121+
assert unit_result.find('failure') is not None
122+
fmt_result = testsuite.find("testcase[@name='fmt']")
123+
assert fmt_result is not None
124+
assert fmt_result.find('failure') is None

0 commit comments

Comments
 (0)