Skip to content

Commit 777e053

Browse files
RyodoTanakaclaude
andcommitted
feat: add ltest verb and CI-friendly output
Add `colcon ltest` (alias `lt`): colcon test with the live per-package board, a readable summary built from JUnit (tests/failures/errors/skipped, failing test cases with messages, JUnit locations), a non-zero exit on any failure, an isolated ROS_DOMAIN_ID per run (on by default), and options --no-isolated-domain, --no-user-site, --fail-on-skip PATTERN, --retest-failed. Add CI mode to lbuild and ltest: used when stdout is not a TTY, with --ci, or when CI/GITHUB_ACTIONS is set. It prints a stable per-package log, GitHub Actions groups and ::error annotations, and writes a Markdown summary to $GITHUB_STEP_SUMMARY. Terminal output of lbuild/lclean is unchanged. Unregistered CI handlers fall back to the live board, so a shadowing installation can no longer make --event-handlers fail with a KeyError. Add pytest tests under test/ and run them in CI. Bump to 0.2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6498da1 commit 777e053

18 files changed

Lines changed: 2276 additions & 16 deletions

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@ jobs:
2525
run: |
2626
python -m pip install colcon-core
2727
python -m pip install dist/*.whl
28-
colcon --help | grep -E "lbuild|lclean"
28+
colcon --help | grep -E "lbuild|lclean|ltest"
29+
- name: Unit tests
30+
run: |
31+
python -m pip install colcon-core pytest
32+
python -m pytest -q test

‎CHANGELOG.rst‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22
Changelog for package colcon-live-tools
33
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44

5+
0.2.0 (2026-10-09)
6+
------------------
7+
* Added ``ltest`` / ``lt`` verb: ``colcon test`` with the same live per-package
8+
board, a readable result summary (tests/failures/errors/skipped per package,
9+
failing test cases with messages, JUnit locations), a non-zero exit code on any
10+
failure, and an isolated ``ROS_DOMAIN_ID`` for the run (``--isolated-domain``,
11+
on by default; ``--no-isolated-domain`` to turn off).
12+
* ``ltest`` options: ``--no-user-site`` (``PYTHONNOUSERSITE=1`` for the test
13+
processes), ``--fail-on-skip PATTERN`` (a skip whose reason matches counts as a
14+
failure), ``--retest-failed`` (only packages that failed last time).
15+
* CI mode for ``lbuild`` and ``ltest``: enabled when stdout is not a TTY, with
16+
``--ci``, or when ``CI``/``GITHUB_ACTIONS`` is set. Prints a stable per-package
17+
log instead of the live board; on GitHub Actions uses ``::group::`` blocks,
18+
``::error`` annotations and writes a Markdown summary to
19+
``$GITHUB_STEP_SUMMARY``.
20+
* ``lbuild`` / ``lclean`` behavior on a terminal is unchanged.
21+
* Added ``test/`` (pytest) covering the JUnit summary, CI mode, GitHub
22+
workflow commands, step summary, fail-on-skip, domain isolation and the
23+
``ltest`` verb.
24+
525
0.1.1 (2026-09-23)
626
------------------
727
* No functional changes.

‎README.md‎

Lines changed: 181 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ This package adds three things:
5555
3. **`lclean` / `lc` verb** — a `catkin clean` equivalent. Shows whichever of the
5656
`build`/`install`/`log` directories actually exist and asks for confirmation before
5757
removing them, unless `-y`/`--yes` is passed.
58+
4. **`ltest` / `lt` verb** — `colcon test` with the same live board, a readable result
59+
summary (tests / failures / errors / skipped per package, the failing test cases with
60+
their messages, and where the JUnit files are), a non-zero exit code on any failure,
61+
and an isolated `ROS_DOMAIN_ID` for the run.
62+
5. **CI mode** (`lbuild` and `ltest`) — when stdout is not a terminal, or `--ci`, or the
63+
`CI`/`GITHUB_ACTIONS` environment variable is set, the live board is replaced by a
64+
stable per-package log: GitHub Actions groups, `::error` annotations, and a Markdown
65+
summary in `$GITHUB_STEP_SUMMARY`.
5866

5967
The only dependency is `colcon-core` — no `rich`, no `curses`.
6068

@@ -112,6 +120,110 @@ colcon lclean --build-base b2 --install-base i2 --log-base l2 # non-default dir
112120
Only removes directories that actually exist; nothing else is touched (no per-package
113121
granularity, no build "profiles" the way `catkin_tools` has).
114122

123+
### `colcon ltest` / `colcon lt`
124+
125+
`colcon lt` (long form `colcon ltest`) accepts every argument `colcon test` does, including
126+
`--packages-select`, `--packages-up-to`, `--retest-until-pass` and `--abort-on-error`.
127+
128+
```bash
129+
colcon lt --packages-select my_pkg # live board, then a summary
130+
colcon lt --retest-failed # only the packages that failed last time
131+
colcon lt --fail-on-skip 'requires hardware' # a skip whose reason matches counts as a failure
132+
colcon lt --no-isolated-domain --no-user-site # keep the caller's ROS environment as is
133+
```
134+
135+
| Option | Default | What it does |
136+
|---|---|---|
137+
| exit code | non-zero on any failure | A failing test case makes the run fail even if `colcon test` itself returned 0 (the same as `colcon test --return-code-on-test-failure`, but always on). |
138+
| `--isolated-domain` / `--no-isolated-domain` | **on** | Picks a random `ROS_DOMAIN_ID` whose DDS ports are free on this host, and sets `ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST`. See below for why. |
139+
| `--no-user-site` | off | Sets `PYTHONNOUSERSITE=1` for the test processes, so packages in `~/.local` cannot shadow the workspace. colcon itself is not affected. |
140+
| `--fail-on-skip PATTERN` | none | Regular expression matched against each skipped test case's skip reason. A match is counted as a failure (repeatable). |
141+
| `--retest-failed` | off | Only tests the packages that failed in the last `ltest` run, recorded in `build/.ltest-last-failed.json`. Combined with `--packages-select` it narrows further. Packages that were not run keep their recorded state. |
142+
| `--ci` | off | Forces CI mode (see below). |
143+
144+
After the run, a summary is printed:
145+
146+
```
147+
Test time summary (wall time 6.13s, ROS_DOMAIN_ID=217, ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST)
148+
Package Tests Failures Errors Skipped Result
149+
oedo_response_curve 147 0 0 18 PASS
150+
oedo_foo 40 1 0 0 FAIL
151+
total 187 1 0 18
152+
153+
1 package(s) failed: oedo_foo
154+
----- oedo_foo: failures -----
155+
FAILED CurveTest.bad
156+
Expected: 1
157+
Which is: 2
158+
at test/curve_test.cpp:42
159+
junit: .../build/oedo_foo/test_results/oedo_foo/gtest.xml
160+
```
161+
162+
The JUnit files are read from `build/<package>/` (or `--test-result-base`), and only files
163+
written by this run are counted, so results left over from an earlier run do not show up.
164+
A package with no JUnit file and exit code 0 is reported as `NO JUNIT`, not as a failure.
165+
166+
**Why the isolated domain is on by default.** On a machine where the same LAN also has
167+
live robot nodes, a test that publishes or subscribes on the default domain can talk to
168+
them. Picking a free domain and keeping discovery on loopback stops test traffic from
169+
leaving the process group that colcon started. Use `--no-isolated-domain` when a test
170+
really needs to reach another machine, or in an environment you fully control. The domain
171+
choice is a heuristic: it sees DDS ports bound on this host, not participants on other
172+
machines (which loopback discovery does not reach anyway).
173+
174+
**What `--no-user-site` does and does not do.** It sets the variable in the environment
175+
of the whole `colcon test` run, so every Python child process started by a test (not only
176+
`pytest`) runs without the user site-packages. There is no way to narrow it to `pytest`
177+
alone. The `colcon` process itself is already running and is not affected.
178+
179+
### CI mode (GitHub Actions and other CI)
180+
181+
CI mode is chosen per run and applies to `colcon lbuild`/`lb` and `colcon ltest`/`lt`:
182+
183+
- automatically when stdout is not a terminal (a pipe or a log file),
184+
- or when `CI` or `GITHUB_ACTIONS` is set to a value other than `0`/`false`/`no`/`off`,
185+
- or when `--ci` is passed.
186+
187+
In CI mode there is no cursor movement. Each package's output is printed as one block when
188+
its job ends (so parallel packages never interleave), preceded by `Starting >>> <pkg>` and
189+
followed by `Finished <<< <pkg> [time]` or `Failed <<< <pkg> ...`. On GitHub Actions:
190+
191+
- each block is a `::group::` (collapsed in the log),
192+
- a failed package gets an `::error` annotation, with `file=`/`line=` when a compiler
193+
diagnostic was seen,
194+
- a failing test case gets an `::error` annotation with its file and line when JUnit
195+
provides them,
196+
- the summary is appended to `$GITHUB_STEP_SUMMARY` as Markdown tables.
197+
198+
Outside GitHub Actions the groups and annotations are replaced by plain
199+
`===== <pkg>: OK ... =====` lines, and no workflow commands are printed.
200+
201+
A workflow example:
202+
203+
```yaml
204+
name: CI
205+
on: [push, pull_request]
206+
jobs:
207+
build-and-test:
208+
runs-on: ubuntu-24.04
209+
steps:
210+
- uses: actions/checkout@v4
211+
- name: Install ROS 2 and colcon
212+
run: |
213+
# install ROS 2 Jazzy and colcon-common-extensions as your project already does
214+
python3 -m pip install --break-system-packages colcon-live-tools
215+
- name: Build
216+
run: |
217+
source /opt/ros/jazzy/setup.bash
218+
colcon lb --packages-select my_pkg
219+
- name: Test
220+
run: |
221+
source /opt/ros/jazzy/setup.bash
222+
colcon lt --packages-select my_pkg --fail-on-skip 'requires hardware'
223+
```
224+
225+
`colcon lt` exits non-zero when a test fails, so the step fails without further configuration.
226+
115227
### Trying it out without touching your own workspace
116228

117229
```bash
@@ -139,11 +251,8 @@ reports for your environment is a reliable reference.)
139251

140252
### Non-TTY environments (CI, output redirected to a file)
141253

142-
When `sys.stdout.isatty()` is false, no live redraw happens (redrawing a log file has no
143-
effect). One-line `Starting >>> pkg` / `Finished <<< pkg [1.23s]` logging still comes
144-
from colcon's own `console_start_end` handler (on by default) as usual, so
145-
`live_status` doesn't duplicate it. The final build-time table and failure tails are
146-
always printed — TTY or not, colored or not.
254+
Non-TTY output (a pipe, a log file, or CI) uses CI mode, described above. The final table
255+
and the failure tails are printed in every mode, plain text when not on a terminal.
147256

148257
## Reading the output
149258

@@ -281,6 +390,12 @@ colcon lbuild
281390
引数の説明のみ。`colcon build` 本来の引数の説明は英語のまま)。
282391
3. **`lclean` / `lc` コマンド(colcon の verb)** -- `catkin clean` 相当。`build`/`install`/`log`
283392
ディレクトリのうち実際に存在するものだけを表示し、`-y`/`--yes` を渡さない限り削除前に確認を求める。
393+
4. **`ltest` / `lt` コマンド(colcon の verb)** -- `colcon test` にパッケージごとのライブ表示を付け、
394+
見やすい結果要約(パッケージごとの tests/failures/errors/skipped、失敗したテストケースとメッセージ、
395+
junit の場所)を出す。失敗があれば非0で終了し、実行ごとに ROS_DOMAIN_ID を分離する。
396+
5. **CI モード**(`lbuild` と `ltest`) -- 標準出力が端末でない場合、`--ci` を渡した場合、
397+
または `CI`/`GITHUB_ACTIONS` 環境変数が設定されている場合、ライブ表示をやめてパッケージごとの
398+
安定したログにする(GitHub Actions のグループと `::error` 注記、`$GITHUB_STEP_SUMMARY` への Markdown 要約)。
284399

285400
依存は `colcon-core` のみ。`rich` や `curses` などは使わない。
286401

@@ -338,6 +453,65 @@ colcon lclean --build-base b2 --install-base i2 --log-base l2 # ディレクト
338453
実在するディレクトリだけを削除する(パッケージ単位の削除や、`catkin_tools` にあるような
339454
ビルドプロファイルの概念はない)。
340455

456+
### `colcon ltest` / `colcon lt`
457+
458+
`colcon lt`(正式名 `colcon ltest`)は `colcon test` が受け付ける引数をすべて受け付ける
459+
(`--packages-select`、`--packages-up-to`、`--retest-until-pass`、`--abort-on-error` など)。
460+
461+
```bash
462+
colcon lt --packages-select my_pkg # ライブ表示のあと要約を出す
463+
colcon lt --retest-failed # 前回失敗したパッケージだけ回す
464+
colcon lt --fail-on-skip 'requires hardware' # スキップ理由が一致したものを失敗扱い
465+
colcon lt --no-isolated-domain --no-user-site # 呼び出し元の ROS 環境のまま実行
466+
```
467+
468+
| オプション | 既定 | 働き |
469+
|---|---|---|
470+
| 終了コード | 失敗があれば非0 | `colcon test` 自体が 0 を返しても、失敗したテストケースがあれば非0(`--return-code-on-test-failure` と同じだが常に有効) |
471+
| `--isolated-domain` / `--no-isolated-domain` | **有効** | この機で DDS ポートの空いている乱数の `ROS_DOMAIN_ID` を選び、`ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST` を設定する。理由は後述 |
472+
| `--no-user-site` | 無効 | テストのプロセスに `PYTHONNOUSERSITE=1` を渡し、`~/.local` のパッケージがワークスペースを隠さないようにする。colcon 本体には影響しない |
473+
| `--fail-on-skip PATTERN` | なし | スキップされたテストケースの理由に対する正規表現。一致したものは失敗として数える(複数指定可) |
474+
| `--retest-failed` | 無効 | 前回の `ltest` 実行で失敗したパッケージだけを回す(`build/.ltest-last-failed.json` に記録)。`--packages-select` と併用すると両方で絞り込む。今回実行されなかったパッケージの記録は残す |
475+
| `--ci` | 無効 | CI モードを強制する(後述) |
476+
477+
実行後は次のような要約を出す(上の英語版と同じ形式)。junit は `build/<パッケージ>/`(または
478+
`--test-result-base`)から読み、今回の実行で書かれたファイルだけを数えるので、以前の結果が混ざらない。
479+
junit が無く終了コードが 0 のパッケージは失敗ではなく `NO JUNIT` と表示する。
480+
481+
**分離ドメインを既定で有効にする理由。** 同じ LAN に実機の ROS ノードがいる環境では、既定ドメインで
482+
publish/subscribe する試験が実機と混線しうる(建機の制御系では危険)。空いている乱数のドメインを選び、
483+
検出を loopback に限ることで、試験の通信が colcon を起動した側の外へ出ないようにする。試験が本当に
484+
別マシンへ届く必要がある場合や、完全に管理された環境では `--no-isolated-domain` を使う。ドメインの
485+
空き判定は「この機で DDS ポートが塞がっていないか」のヒューリスティックで、別マシンの参加者は見えない
486+
(そもそも loopback の検出では届かない)。
487+
488+
**`--no-user-site` の効く範囲。** 変数は `colcon test` 全体の環境に設定されるため、試験が起動する Python の
489+
子プロセス(`pytest` だけでなくすべて)が ユーザー site-packages なしで動く。`pytest` だけに絞る手段は無い。
490+
すでに動いている `colcon` プロセス本体には影響しない。
491+
492+
### CI モード(GitHub Actions など)
493+
494+
CI モードは実行ごとに決まり、`colcon lbuild`/`lb` と `colcon ltest`/`lt` の両方に効く:
495+
496+
- 標準出力が端末でない場合(パイプ、ログファイル)は自動で有効、
497+
- `CI` または `GITHUB_ACTIONS` が `0`/`false`/`no`/`off` 以外に設定されている場合も有効、
498+
- `--ci` を渡した場合も有効。
499+
500+
CI モードではカーソル移動をしない。各パッケージの出力は、そのジョブが終わった時点で 1 ブロックとして
501+
出す(並列のパッケージ同士が混ざらない)。前に `Starting >>> <パッケージ>`、後ろに
502+
`Finished <<< <パッケージ> [時間]` または `Failed <<< ...` を付ける。GitHub Actions では:
503+
504+
- 各ブロックは `::group::`(ログ上で折りたたまれる)、
505+
- 失敗したパッケージには `::error` 注記(コンパイラの診断を見ていれば `file=`/`line=` 付き)、
506+
- junit が行番号を持つ失敗したテストケースには、そのファイルと行の `::error` 注記、
507+
- 要約を Markdown の表で `$GITHUB_STEP_SUMMARY` に追記する。
508+
509+
GitHub Actions 以外では、グループと注記の代わりに `===== <パッケージ>: OK ... =====` の行を出し、
510+
ワークフローコマンドは出さない。
511+
512+
ワークフローの例は上の英語版を参照(`colcon lb` でビルド、`colcon lt --fail-on-skip '...'` で試験。
513+
`colcon lt` は失敗があると非0で終わるので、追加の設定なしでステップが失敗する)。
514+
341515
### 自分のワークスペースを使わずに試す
342516

343517
```bash
@@ -363,11 +537,8 @@ colcon build --event-handlers status- summary- console_start_end- console_stderr
363537

364538
### 非TTY環境(CI・ログファイル出力など)
365539

366-
`sys.stdout.isatty()` が False の場合、ライブ描画は行わない(ログファイルに描画しても意味がないため)。
367-
`Starting >>> pkg` / `Finished <<< pkg [1.23s]` のような1行ログは、有効になっている
368-
`console_start_end` ハンドラ(colcon 標準、デフォルト有効)がそのまま出力するので、
369-
`live_status` 側で重複して出すことはしない。ビルド終了時のテーブルと失敗ログの末尾は
370-
TTY かどうかに関わらず常に出力される。
540+
非TTY の出力(パイプ、ログファイル、CI)は、上の CI モードになる。終了時のテーブルと失敗ログの末尾は
541+
どのモードでも出力され、端末でない場合はプレーンテキストになる。
371542

372543
## 出力の読み方
373544

‎colcon_live_tools/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright 2026 OEDO Dynamics Inc.
22
# Licensed under the Apache License, Version 2.0
33

4-
__version__ = '0.1.1'
4+
__version__ = '0.2.0'

0 commit comments

Comments
 (0)