Skip to content

Commit d921d8a

Browse files
authored
Merge branch 'development' into pyrealsense2-wheel-script
2 parents 6a8f934 + c4bb712 commit d921d8a

23 files changed

Lines changed: 749 additions & 715 deletions

.github/skills/pytest-infra.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -139,43 +139,48 @@ When migrating a legacy `test-*.py` to `pytest-*.py`:
139139

140140
## Handling `on_fail=test.ABORT`
141141

142-
The legacy framework supported `with test.closure('Name', on_fail=test.ABORT):`if that closure failed, all subsequent closures were skipped. In pytest, use the **`pytest-dependency`** plugin (already in `requirements.txt` and `plugins.py`).
142+
The legacy framework supported `with test.closure('Name', on_fail=test.ABORT):`if that closure failed, all subsequent closures were skipped. In pytest, use a **module-level state dict** with `pytest.skip()`.
143143

144-
**Pattern**: mark the prerequisite test with `@pytest.mark.dependency(scope='module')`, and each dependent test with `@pytest.mark.dependency(scope='module', depends=["prerequisite_name"])`. If the prerequisite fails or is skipped, all dependents are automatically skipped.
144+
> **Why not `pytest-dependency`?** The plugin requires exact test-name matching, but `pytest_generate_tests` (used by `device_each`) appends parametrized suffixes like `[D455-1234567890]`. The plugin cannot match `"test_foo"` against `"test_foo[D455-1234567890]"` — no regex, glob, or prefix support exists. The module-state pattern is zero-dependency and works regardless of parametrization.
145+
146+
**Pattern**: the prerequisite test sets a flag in a module-level dict on success. Dependent tests check the flag and `pytest.skip()` if missing.
145147

146148
```python
147-
# Prerequisite test — asserts (hard fail if condition not met), registers as a dependency
148-
@pytest.mark.dependency(scope='module')
149+
_module_state = {}
150+
149151
def test_advanced_mode_support(test_device_wrapped):
150152
"""Prerequisite: camera must be in advanced mode."""
151153
dev, ctx = test_device_wrapped
152154
assert rs.rs400_advanced_mode(dev).is_enabled()
155+
_module_state['am_ok'] = True
153156

154-
# Dependent test — skipped automatically if test_advanced_mode_support failed/was skipped
155-
@pytest.mark.dependency(scope='module', depends=["test_advanced_mode_support"])
156157
def test_set_depth_control(test_device_wrapped):
158+
if not _module_state.get('am_ok'):
159+
pytest.skip("prerequisite test_advanced_mode_support failed")
157160
dev, ctx = test_device_wrapped
158161
...
159162
```
160163

161-
**`scope='module'`**: limits dependency resolution to the current test file, so identically-named tests in other files do not interfere.
162-
163-
**Parametrized tests**: when both the prerequisite and dependent tests share the same parametrization (e.g., `device_each`), `pytest-dependency` automatically matches per-parameter — `test_set_depth_control[D455-SN]` is only skipped if `test_advanced_mode_support[D455-SN]` specifically failed, not if a different device's run failed.
164-
165-
**Chain of ABORTs**: if a file has multiple `on_fail=test.ABORT` closures in sequence, list all prerequisite names in `depends=`:
164+
**Chain of ABORTs**: if a file has multiple prerequisite tests in sequence, each sets its own flag. Dependents check the deepest prerequisite (which implicitly requires all prior ones to have passed):
166165

167166
```python
168-
@pytest.mark.dependency(scope='module')
167+
_module_state = {}
168+
169169
def test_advanced_mode_support(...): # first ABORT
170170
assert ...
171+
_module_state['am_ok'] = True
171172

172-
@pytest.mark.dependency(scope='module', depends=["test_advanced_mode_support"])
173173
def test_visual_preset_support(...): # second ABORT
174+
if not _module_state.get('am_ok'):
175+
pytest.skip("prerequisite test_advanced_mode_support failed")
174176
assert ...
177+
_module_state['preset_ok'] = True
175178

176-
# Everything after the second ABORT depends on both
177-
@pytest.mark.dependency(scope='module', depends=["test_advanced_mode_support", "test_visual_preset_support"])
179+
# Everything after the second ABORT checks only 'preset_ok'
180+
# (preset_ok being set implies am_ok was set too)
178181
def test_set_depth_control(...):
182+
if not _module_state.get('preset_ok'):
183+
pytest.skip("prerequisite test_visual_preset_support failed")
179184
...
180185
```
181186

common/viewer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3022,7 +3022,7 @@ namespace rs2
30223022
std::string excl_icon_str = rsutils::string::from() << textual_icons::exclamation_triangle
30233023
<< " The following changes will take effect only after restarting the application";
30243024
ImGui::Text( "%s", excl_icon_str.c_str() );
3025-
bool allow_partial_device = temp_cfg.get_nested< bool >( "context.partial-device-allowed", false );
3025+
bool allow_partial_device = temp_cfg.get_nested< bool >( "context.partial-device-allowed", true );
30263026
if( ImGui::Checkbox( "Allow partial device initialization", &allow_partial_device ) )
30273027
{
30283028
temp_cfg.set_nested( "context.partial-device-allowed", allow_partial_device );

examples/enhanced-depth-range/live_minz_compare.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@
4141
ir2 = profile.get_stream(rs.stream.infrared, 2).as_video_stream_profile()
4242
calib = Calibration.from_sdk(ir1.get_intrinsics(), ir1.get_extrinsics_to(ir2))
4343

44+
# Meters per Z16 unit (RS2_OPTION_DEPTH_UNITS). Typical D4xx is 0.001 (raw
45+
# Z16 == mm), but high-accuracy presets and SR300 use other values, so we
46+
# must scale raw values by this factor to get true millimetres.
47+
try:
48+
depth_scale = profile.get_device().first_depth_sensor().get_depth_scale()
49+
except Exception:
50+
depth_scale = 0.001
51+
print(f"Depth scale: {depth_scale} m/unit ({depth_scale * 1000:.4f} mm/unit)")
52+
4453
# ── 3. Construct the improver ───────────────────────────────────────────
4554
improver = DepthRangeImprover(calib)
4655
print(f"MinZ threshold: {improver.min_z_threshold_mm} mm")
@@ -81,7 +90,8 @@ def colorize_depth_mm(depth_mm: np.ndarray) -> np.ndarray:
8190
f = pipeline.wait_for_frames()
8291
ir_left = np.asanyarray(f.get_infrared_frame(1).get_data())
8392
ir_right = np.asanyarray(f.get_infrared_frame(2).get_data())
84-
depth_hw = np.asanyarray(f.get_depth_frame().get_data())
93+
depth_hw = (np.asanyarray(f.get_depth_frame().get_data())
94+
* depth_scale * 1000.0).astype(np.uint16)
8595

8696
depth_imp = improver.process(ir_left, ir_right, depth_hw)
8797

examples/enhanced-depth-range/range_depth.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <csignal>
2727
#include <cstdint>
2828
#include <cstdio>
29+
#include <cstring>
2930
#include <vector>
3031

3132
static volatile std::sig_atomic_t g_stop = 0;
@@ -49,6 +50,14 @@ int main() {
4950
auto calib = rs_depth::Calibration::from_sdk(ir1.get_intrinsics(),
5051
ir1.get_extrinsics_to(ir2));
5152

53+
// Meters per Z16 unit (RS2_OPTION_DEPTH_UNITS). Typical D4xx = 0.001 (raw
54+
// Z16 == mm), but high-accuracy presets and SR300 use other values — scale
55+
// raw values to mm before passing to the improver and the MinZ comparison.
56+
const float depth_scale = profile.get_device()
57+
.first<rs2::depth_sensor>()
58+
.get_depth_scale();
59+
const float depth_to_mm = depth_scale * 1000.0f;
60+
5261
// ── 3. Construct the improver (auto threshold = focal × baseline / 105)
5362
rs_depth::DepthRangeImprover improver(calib);
5463
const int T = calib.min_z_threshold_mm();
@@ -57,6 +66,7 @@ int main() {
5766

5867
// ── 4. Stream + improve + live status line ────────────────────────
5968
std::vector<uint16_t> depth_imp(N);
69+
std::vector<uint16_t> depth_mm(N);
6070
long long hw_total = 0, imp_total = 0, rescued_total = 0;
6171
int frames_seen = 0;
6272

@@ -68,7 +78,20 @@ int main() {
6878

6979
const uint8_t* ir_left = static_cast<const uint8_t*>(ir_l.get_data());
7080
const uint8_t* ir_right = static_cast<const uint8_t*>(ir_r.get_data());
71-
const uint16_t* depth_hw = static_cast<const uint16_t*>(depth.get_data());
81+
const uint16_t* raw_z16 = static_cast<const uint16_t*>(depth.get_data());
82+
83+
// Convert raw Z16 → uint16 mm using the camera's depth_scale.
84+
const uint16_t* depth_hw = depth_mm.data();
85+
if (depth_to_mm == 1.0f) {
86+
std::memcpy(depth_mm.data(), raw_z16, N * sizeof(uint16_t));
87+
} else {
88+
for (int i = 0; i < N; ++i) {
89+
float mm = static_cast<float>(raw_z16[i]) * depth_to_mm;
90+
depth_mm[i] = (mm > 65535.0f) ? 65535
91+
: (mm < 0.0f) ? 0
92+
: static_cast<uint16_t>(mm);
93+
}
94+
}
7295

7396
// Mirrors Python's FrameMetadata.from_rs2_frameset — extracts width,
7497
// height, exposure/gain/laser-power/temperature for IR + depth + color.

examples/enhanced-depth-range/range_depth.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@
3434
ir2 = profile.get_stream(rs.stream.infrared, 2).as_video_stream_profile()
3535
calib = Calibration.from_sdk(ir1.get_intrinsics(), ir1.get_extrinsics_to(ir2))
3636

37+
# Meters per Z16 unit. Typical D4xx = 0.001 (raw Z16 == mm), but high-accuracy
38+
# presets and SR300 use other values — scale raw values to mm before comparing
39+
# against MinZ threshold (which is in mm).
40+
try:
41+
depth_scale = profile.get_device().first_depth_sensor().get_depth_scale()
42+
except Exception:
43+
depth_scale = 0.001
44+
3745
# ── 3. Construct the improver (auto threshold = focal × baseline / 105) ─
3846
improver = DepthRangeImprover(calib)
3947
T = improver.min_z_threshold_mm
@@ -49,7 +57,8 @@
4957
f = pipeline.wait_for_frames()
5058
ir_left = np.asanyarray(f.get_infrared_frame(1).get_data())
5159
ir_right = np.asanyarray(f.get_infrared_frame(2).get_data())
52-
depth_hw = np.asanyarray(f.get_depth_frame().get_data())
60+
depth_hw = (np.asanyarray(f.get_depth_frame().get_data())
61+
* depth_scale * 1000.0).astype(np.uint16)
5362

5463
depth_imp = improver.process(ir_left, ir_right, depth_hw)
5564

src/dds/rsdds-device-factory.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ rsdds_device_factory::rsdds_device_factory( std::shared_ptr< context > const & c
157157
<< domain_id << "; cannot create '" << participant_name << "'" );
158158
}
159159
size_t device_initialization_timeout = dds_settings.nested( "device-initialization-timeout-ms" ).default_value< size_t >( 5000 );
160-
bool partial_capabilities_allowed = ctx->get_settings().nested( "partial-device-allowed" ).default_value< bool >( false );
160+
bool partial_capabilities_allowed = ctx->get_settings().nested( "partial-device-allowed" ).default_value< bool >( true );
161161
_watcher_singleton = domain.device_watcher.instance( _participant, device_initialization_timeout, partial_capabilities_allowed );
162162
_subscription = _watcher_singleton->subscribe(
163163
[liveliness = std::weak_ptr< context >( ctx ),

src/ds/ds-private.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ namespace librealsense
122122
bool is_partial_device_allowed( const std::shared_ptr< context > & ctx )
123123
{
124124
auto settings = ctx->get_settings();
125-
return settings.nested( "partial-device-allowed" ).default_value< bool >( false );
125+
return settings.nested( "partial-device-allowed" ).default_value< bool >( true );
126126
}
127127
} // librealsense::ds
128128
} // namespace librealsense

unit-tests/conftest.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,13 @@ def pytest_configure(config):
240240
setup_test_logging(config)
241241

242242
# Enable LibRS debug logging if --rslog (once, globally)
243+
# log_to_console writes directly to stderr from C++. Pytest's default fd-level
244+
# capture swallows it, so we downgrade to sys-level capture (Python only) which
245+
# lets C++ stderr through while still capturing Python stdout/stderr.
243246
if rs and config.getoption("--rslog", default=False):
244247
rs.log_to_console(rs.log_severity.debug)
248+
if config.option.capture == 'fd':
249+
config.option.capture = 'sys'
245250

246251
# Test discovery defaults (replaces pytest.ini which is .gitignored)
247252
config.addinivalue_line("python_files", "pytest-*.py")
@@ -579,7 +584,7 @@ def test_context(request, module_device_setup):
579584
if not rs:
580585
pytest.skip("pyrealsense2 not available")
581586

582-
ctx = rs.context()
587+
ctx = rs.context({"device-mask":0xfe}) # Intel only (no platform camera when testing locally)
583588

584589
if module_device_setup and len(list(ctx.devices)) == 0:
585590
pytest.fail("No devices visible in context after device setup")

0 commit comments

Comments
 (0)