mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 10:36:35 +00:00
fix(scripts): scroll_speeds.py opens the panel with the service's options
--measure and --demo built RGBMatrixOptions from a private copy of the display service's builder that had drifted: gpio_slowdown came from display.hardware (default 2) instead of display.runtime (default 3), and rp1_rio, panel_type, disable_hardware_pulsing, inverse_colors, pixel_mapper_config and orientation were skipped, with different defaults (hardware_mapping "regular", pwm_bits 11). A panel needing a high slowdown was measured -- or garbled -- in a setup the service never drives. The option filling in DisplayManager._setup_matrix moves, unchanged, into DisplayManager.apply_matrix_options(options, config), which _setup_matrix calls and the script reuses (overriding only limit_refresh_rate_hz for --measure). The script now loads the whole config rather than the hardware block. Tests pin the script's options to the service's attribute for attribute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+37
-42
@@ -47,52 +47,47 @@ from src.common import scroll_config # noqa: E402
|
|||||||
CONFIG = Path(__file__).resolve().parent.parent / "config" / "config.json"
|
CONFIG = Path(__file__).resolve().parent.parent / "config" / "config.json"
|
||||||
|
|
||||||
|
|
||||||
def load_hardware():
|
def load_config():
|
||||||
|
"""The whole config.json, or {} when it is missing or unreadable."""
|
||||||
try:
|
try:
|
||||||
with open(CONFIG, encoding="utf-8") as handle:
|
with open(CONFIG, encoding="utf-8") as handle:
|
||||||
return (json.load(handle).get("display") or {}).get("hardware") or {}
|
config = json.load(handle)
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
return {}
|
return {}
|
||||||
|
return config if isinstance(config, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
def build_options(hardware, refresh_override=None):
|
def hardware_of(config):
|
||||||
from rgbmatrix import RGBMatrixOptions
|
return (config.get("display") or {}).get("hardware") or {}
|
||||||
|
|
||||||
o = RGBMatrixOptions()
|
|
||||||
from src.display_geometry import (
|
|
||||||
DEFAULT_CHAIN_LENGTH, DEFAULT_COLS, DEFAULT_PARALLEL, DEFAULT_ROWS,
|
|
||||||
)
|
|
||||||
o.rows = int(hardware.get("rows", DEFAULT_ROWS))
|
|
||||||
o.cols = int(hardware.get("cols", DEFAULT_COLS))
|
|
||||||
o.chain_length = int(hardware.get("chain_length", DEFAULT_CHAIN_LENGTH))
|
|
||||||
o.parallel = int(hardware.get("parallel", DEFAULT_PARALLEL))
|
|
||||||
o.brightness = int(hardware.get("brightness", 80))
|
|
||||||
o.hardware_mapping = hardware.get("hardware_mapping", "regular")
|
|
||||||
o.pwm_bits = int(hardware.get("pwm_bits", 11))
|
|
||||||
o.pwm_dither_bits = int(hardware.get("pwm_dither_bits", 0))
|
|
||||||
o.pwm_lsb_nanoseconds = int(hardware.get("pwm_lsb_nanoseconds", 130))
|
|
||||||
o.led_rgb_sequence = hardware.get("led_rgb_sequence", "RGB")
|
|
||||||
o.scan_mode = int(hardware.get("scan_mode", 0))
|
|
||||||
o.row_address_type = int(hardware.get("row_address_type", 0))
|
|
||||||
o.multiplexing = int(hardware.get("multiplexing", 0))
|
|
||||||
o.gpio_slowdown = int(hardware.get("gpio_slowdown", 2))
|
|
||||||
o.limit_refresh_rate_hz = (
|
|
||||||
int(refresh_override) if refresh_override is not None
|
|
||||||
else int(hardware.get("limit_refresh_rate_hz", 0))
|
|
||||||
)
|
|
||||||
return o
|
|
||||||
|
|
||||||
|
|
||||||
def open_matrix(hardware, refresh_override=None):
|
def build_options(config, refresh_override=None):
|
||||||
|
"""The matrix options the display service would use for this config.
|
||||||
|
|
||||||
|
Built by DisplayManager.apply_matrix_options, not a copy of it, so the
|
||||||
|
measurement and the demo drive the panel exactly as the service does
|
||||||
|
(runtime gpio_slowdown, rp1_rio, panel_type, orientation, defaults).
|
||||||
|
``refresh_override`` replaces limit_refresh_rate_hz; 0 means uncapped.
|
||||||
|
"""
|
||||||
|
from src.display_manager import DisplayManager, RGBMatrixOptions
|
||||||
|
|
||||||
|
options = DisplayManager.apply_matrix_options(RGBMatrixOptions(), config)
|
||||||
|
if refresh_override is not None:
|
||||||
|
options.limit_refresh_rate_hz = int(refresh_override)
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def open_matrix(config, refresh_override=None):
|
||||||
"""Construct the matrix, or explain why it will not open."""
|
"""Construct the matrix, or explain why it will not open."""
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
sys.exit("this needs root for GPIO access - rerun with sudo")
|
sys.exit("this needs root for GPIO access - rerun with sudo")
|
||||||
try:
|
try:
|
||||||
from rgbmatrix import RGBMatrix
|
from src.display_manager import RGBMatrix
|
||||||
except ImportError:
|
except ImportError as exc:
|
||||||
sys.exit("rgbmatrix is not installed on this machine")
|
sys.exit("could not load the display stack ({}); is rgbmatrix "
|
||||||
|
"installed on this machine?".format(exc))
|
||||||
try:
|
try:
|
||||||
return RGBMatrix(options=build_options(hardware, refresh_override))
|
return RGBMatrix(options=build_options(config, refresh_override))
|
||||||
except Exception as exc: # pragma: no cover - hardware dependent
|
except Exception as exc: # pragma: no cover - hardware dependent
|
||||||
sys.exit(
|
sys.exit(
|
||||||
"could not open the panel ({}).\n"
|
"could not open the panel ({}).\n"
|
||||||
@@ -101,7 +96,7 @@ def open_matrix(hardware, refresh_override=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def measure_refresh(hardware, seconds=6.0):
|
def measure_refresh(config, seconds=6.0):
|
||||||
"""Actual refresh rate, by running uncapped and timing the swaps.
|
"""Actual refresh rate, by running uncapped and timing the swaps.
|
||||||
|
|
||||||
SwapOnVSync blocks until the panel's next refresh, so an unthrottled loop
|
SwapOnVSync blocks until the panel's next refresh, so an unthrottled loop
|
||||||
@@ -109,7 +104,7 @@ def measure_refresh(hardware, seconds=6.0):
|
|||||||
chain will really give you, as opposed to whatever limit_refresh_rate_hz
|
chain will really give you, as opposed to whatever limit_refresh_rate_hz
|
||||||
optimistically asks for.
|
optimistically asks for.
|
||||||
"""
|
"""
|
||||||
matrix = open_matrix(hardware, refresh_override=0)
|
matrix = open_matrix(config, refresh_override=0)
|
||||||
canvas = matrix.CreateFrameCanvas()
|
canvas = matrix.CreateFrameCanvas()
|
||||||
canvas = matrix.SwapOnVSync(canvas) # discard the first, it includes setup
|
canvas = matrix.SwapOnVSync(canvas) # discard the first, it includes setup
|
||||||
frames = 0
|
frames = 0
|
||||||
@@ -122,17 +117,17 @@ def measure_refresh(hardware, seconds=6.0):
|
|||||||
return measured
|
return measured
|
||||||
|
|
||||||
|
|
||||||
def demo(hardware, target, seconds):
|
def demo(config, target, seconds):
|
||||||
"""Scroll text at the crisp speed nearest `target`."""
|
"""Scroll text at the crisp speed nearest `target`."""
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
from src.common.font_layout import load_truetype
|
from src.common.font_layout import load_truetype
|
||||||
|
|
||||||
hz = float(hardware.get("limit_refresh_rate_hz") or scroll_config.DEFAULT_REFRESH_HZ)
|
hz = scroll_config.refresh_hz_from_config(config)
|
||||||
choice = scroll_config.solve_crisp(target, hz)
|
choice = scroll_config.solve_crisp(target, hz)
|
||||||
print("asked for {:.0f} px/s -> {}".format(target, choice.describe()))
|
print("asked for {:.0f} px/s -> {}".format(target, choice.describe()))
|
||||||
|
|
||||||
matrix = open_matrix(hardware)
|
matrix = open_matrix(config)
|
||||||
canvas = matrix.CreateFrameCanvas()
|
canvas = matrix.CreateFrameCanvas()
|
||||||
W, H = canvas.width, canvas.height
|
W, H = canvas.width, canvas.height
|
||||||
|
|
||||||
@@ -214,15 +209,15 @@ def main():
|
|||||||
help="highlight the entry nearest this speed")
|
help="highlight the entry nearest this speed")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
hardware = load_hardware()
|
config = load_config()
|
||||||
configured = float(hardware.get("limit_refresh_rate_hz") or 0)
|
configured = float(hardware_of(config).get("limit_refresh_rate_hz") or 0)
|
||||||
|
|
||||||
if args.demo is not None:
|
if args.demo is not None:
|
||||||
demo(hardware, args.demo, args.seconds)
|
demo(config, args.demo, args.seconds)
|
||||||
return
|
return
|
||||||
|
|
||||||
if args.measure:
|
if args.measure:
|
||||||
measured = measure_refresh(hardware)
|
measured = measure_refresh(config)
|
||||||
print("measured panel refresh: {:.1f}Hz".format(measured))
|
print("measured panel refresh: {:.1f}Hz".format(measured))
|
||||||
if configured:
|
if configured:
|
||||||
print("configured limit_refresh_rate_hz: {:.0f}".format(configured))
|
print("configured limit_refresh_rate_hz: {:.0f}".format(configured))
|
||||||
|
|||||||
+65
-44
@@ -279,50 +279,9 @@ class DisplayManager:
|
|||||||
hardware_config = self.config.get('display', {}).get('hardware', {})
|
hardware_config = self.config.get('display', {}).get('hardware', {})
|
||||||
runtime_config = self.config.get('display', {}).get('runtime', {})
|
runtime_config = self.config.get('display', {}).get('runtime', {})
|
||||||
|
|
||||||
# Basic hardware settings
|
# Every option comes from display.hardware / display.runtime, in
|
||||||
options.rows = hardware_config.get('rows', DEFAULT_ROWS)
|
# one place that scripts/scroll_speeds.py shares.
|
||||||
options.cols = hardware_config.get('cols', DEFAULT_COLS)
|
self.apply_matrix_options(options, self.config)
|
||||||
options.chain_length = hardware_config.get('chain_length', DEFAULT_CHAIN_LENGTH)
|
|
||||||
options.parallel = hardware_config.get('parallel', DEFAULT_PARALLEL)
|
|
||||||
options.hardware_mapping = hardware_config.get('hardware_mapping', 'adafruit-hat-pwm')
|
|
||||||
|
|
||||||
# Performance and stability settings
|
|
||||||
options.brightness = hardware_config.get('brightness', 90)
|
|
||||||
options.pwm_bits = hardware_config.get('pwm_bits', 10)
|
|
||||||
options.pwm_lsb_nanoseconds = hardware_config.get('pwm_lsb_nanoseconds', 150)
|
|
||||||
options.led_rgb_sequence = hardware_config.get('led_rgb_sequence', 'RGB')
|
|
||||||
options.pixel_mapper_config = self._build_pixel_mapper_config(hardware_config)
|
|
||||||
options.row_address_type = hardware_config.get('row_address_type', 0)
|
|
||||||
options.multiplexing = hardware_config.get('multiplexing', 0)
|
|
||||||
options.panel_type = hardware_config.get('panel_type', '')
|
|
||||||
options.disable_hardware_pulsing = hardware_config.get('disable_hardware_pulsing', False)
|
|
||||||
options.show_refresh_rate = hardware_config.get('show_refresh_rate', False)
|
|
||||||
options.limit_refresh_rate_hz = hardware_config.get('limit_refresh_rate_hz', 90)
|
|
||||||
options.gpio_slowdown = runtime_config.get('gpio_slowdown', 3)
|
|
||||||
|
|
||||||
# Disable internal privilege dropping - we manage this via systemd or remain root
|
|
||||||
# This prevents the library from dropping to 'daemon' user which breaks file permissions
|
|
||||||
options.drop_privileges = False
|
|
||||||
|
|
||||||
# Additional settings from config
|
|
||||||
if 'scan_mode' in hardware_config:
|
|
||||||
options.scan_mode = hardware_config.get('scan_mode')
|
|
||||||
if 'pwm_dither_bits' in hardware_config:
|
|
||||||
options.pwm_dither_bits = hardware_config.get('pwm_dither_bits')
|
|
||||||
if 'inverse_colors' in hardware_config:
|
|
||||||
options.inverse_colors = hardware_config.get('inverse_colors')
|
|
||||||
# Pi 5 only: 0=PIO/RP1 coprocessor (default, less CPU),
|
|
||||||
# 1=RIO/Registered IO (faster; gpio_slowdown effect is inverted in this mode)
|
|
||||||
if 'rp1_rio' in runtime_config:
|
|
||||||
if hasattr(options, 'rp1_rio'):
|
|
||||||
options.rp1_rio = runtime_config.get('rp1_rio')
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"rp1_rio is set in config but the installed rgbmatrix library does "
|
|
||||||
"not support it — the library was likely built without Pi 5 RP1 "
|
|
||||||
"support (mmap to 0x3f000000 instead of RP1 chip). "
|
|
||||||
"Fix: sudo RPI_RGB_FORCE_REBUILD=1 ./first_time_install.sh"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Initializing RGB Matrix with settings: rows={options.rows}, cols={options.cols}, chain_length={options.chain_length}, parallel={options.parallel}, hardware_mapping={options.hardware_mapping}")
|
logger.info(f"Initializing RGB Matrix with settings: rows={options.rows}, cols={options.cols}, chain_length={options.chain_length}, parallel={options.parallel}, hardware_mapping={options.hardware_mapping}")
|
||||||
|
|
||||||
@@ -1370,6 +1329,68 @@ class DisplayManager:
|
|||||||
|
|
||||||
return dt.strftime(f"%b %-d{suffix}")
|
return dt.strftime(f"%b %-d{suffix}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def apply_matrix_options(cls, options, config: Dict[str, Any]):
|
||||||
|
"""Fill ``options`` (an ``RGBMatrixOptions``) from the LEDMatrix config.
|
||||||
|
|
||||||
|
This is exactly what the display service drives the panel with, so a
|
||||||
|
tool that opens the matrix itself (``scripts/scroll_speeds.py``) gets
|
||||||
|
the same panel -- same runtime ``gpio_slowdown``, ``rp1_rio``,
|
||||||
|
``panel_type``, orientation and defaults -- rather than a private copy
|
||||||
|
that drifts. Does not open the matrix. Returns ``options``.
|
||||||
|
"""
|
||||||
|
display = config.get('display', {}) if isinstance(config, dict) else {}
|
||||||
|
hardware_config = display.get('hardware', {})
|
||||||
|
runtime_config = display.get('runtime', {})
|
||||||
|
|
||||||
|
# Basic hardware settings
|
||||||
|
options.rows = hardware_config.get('rows', DEFAULT_ROWS)
|
||||||
|
options.cols = hardware_config.get('cols', DEFAULT_COLS)
|
||||||
|
options.chain_length = hardware_config.get('chain_length', DEFAULT_CHAIN_LENGTH)
|
||||||
|
options.parallel = hardware_config.get('parallel', DEFAULT_PARALLEL)
|
||||||
|
options.hardware_mapping = hardware_config.get('hardware_mapping', 'adafruit-hat-pwm')
|
||||||
|
|
||||||
|
# Performance and stability settings
|
||||||
|
options.brightness = hardware_config.get('brightness', 90)
|
||||||
|
options.pwm_bits = hardware_config.get('pwm_bits', 10)
|
||||||
|
options.pwm_lsb_nanoseconds = hardware_config.get('pwm_lsb_nanoseconds', 150)
|
||||||
|
options.led_rgb_sequence = hardware_config.get('led_rgb_sequence', 'RGB')
|
||||||
|
# _build_pixel_mapper_config reads only class attributes, so the class
|
||||||
|
# stands in for an instance here.
|
||||||
|
options.pixel_mapper_config = cls._build_pixel_mapper_config(cls, hardware_config)
|
||||||
|
options.row_address_type = hardware_config.get('row_address_type', 0)
|
||||||
|
options.multiplexing = hardware_config.get('multiplexing', 0)
|
||||||
|
options.panel_type = hardware_config.get('panel_type', '')
|
||||||
|
options.disable_hardware_pulsing = hardware_config.get('disable_hardware_pulsing', False)
|
||||||
|
options.show_refresh_rate = hardware_config.get('show_refresh_rate', False)
|
||||||
|
options.limit_refresh_rate_hz = hardware_config.get('limit_refresh_rate_hz', 90)
|
||||||
|
options.gpio_slowdown = runtime_config.get('gpio_slowdown', 3)
|
||||||
|
|
||||||
|
# Disable internal privilege dropping - we manage this via systemd or remain root
|
||||||
|
# This prevents the library from dropping to 'daemon' user which breaks file permissions
|
||||||
|
options.drop_privileges = False
|
||||||
|
|
||||||
|
# Additional settings from config
|
||||||
|
if 'scan_mode' in hardware_config:
|
||||||
|
options.scan_mode = hardware_config.get('scan_mode')
|
||||||
|
if 'pwm_dither_bits' in hardware_config:
|
||||||
|
options.pwm_dither_bits = hardware_config.get('pwm_dither_bits')
|
||||||
|
if 'inverse_colors' in hardware_config:
|
||||||
|
options.inverse_colors = hardware_config.get('inverse_colors')
|
||||||
|
# Pi 5 only: 0=PIO/RP1 coprocessor (default, less CPU),
|
||||||
|
# 1=RIO/Registered IO (faster; gpio_slowdown effect is inverted in this mode)
|
||||||
|
if 'rp1_rio' in runtime_config:
|
||||||
|
if hasattr(options, 'rp1_rio'):
|
||||||
|
options.rp1_rio = runtime_config.get('rp1_rio')
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"rp1_rio is set in config but the installed rgbmatrix library does "
|
||||||
|
"not support it — the library was likely built without Pi 5 RP1 "
|
||||||
|
"support (mmap to 0x3f000000 instead of RP1 chip). "
|
||||||
|
"Fix: sudo RPI_RGB_FORCE_REBUILD=1 ./first_time_install.sh"
|
||||||
|
)
|
||||||
|
return options
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def refresh_hz(self) -> float:
|
def refresh_hz(self) -> float:
|
||||||
"""The panel's refresh rate in Hz, from the hardware config.
|
"""The panel's refresh rate in Hz, from the hardware config.
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""scripts/scroll_speeds.py must drive the panel the way the service does.
|
||||||
|
|
||||||
|
--measure and --demo open the matrix themselves. They used to build the
|
||||||
|
options from a private copy of DisplayManager's builder that had drifted: it
|
||||||
|
read gpio_slowdown from display.hardware (the service reads display.runtime),
|
||||||
|
and skipped rp1_rio, panel_type, orientation and more. On a panel that needs a
|
||||||
|
high slowdown that measured -- or garbled -- a panel the service never drives.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
os.environ.setdefault("EMULATOR", "true")
|
||||||
|
|
||||||
|
import src.display_manager as display_manager # noqa: E402
|
||||||
|
from src.display_manager import DisplayManager # noqa: E402
|
||||||
|
|
||||||
|
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "scroll_speeds.py"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def script():
|
||||||
|
spec = importlib.util.spec_from_file_location("scroll_speeds_script", SCRIPT)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class _Options:
|
||||||
|
"""Records what is set; declares rp1_rio like a Pi 5-capable binding."""
|
||||||
|
|
||||||
|
rp1_rio = None
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG = {
|
||||||
|
"display": {
|
||||||
|
"hardware": {
|
||||||
|
"rows": 64, "cols": 64, "chain_length": 2, "parallel": 1,
|
||||||
|
"hardware_mapping": "adafruit-hat-pwm",
|
||||||
|
"brightness": 70, "pwm_bits": 9, "pwm_lsb_nanoseconds": 130,
|
||||||
|
"row_address_type": 5, "panel_type": "FM6126A",
|
||||||
|
"limit_refresh_rate_hz": 120, "orientation": "180",
|
||||||
|
"pixel_mapper_config": "U-mapper",
|
||||||
|
"gpio_slowdown": 2, # a stale key in the wrong block
|
||||||
|
},
|
||||||
|
"runtime": {"gpio_slowdown": 7, "rp1_rio": 1},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _script_options(script, config, **kwargs):
|
||||||
|
with patch.object(display_manager, "RGBMatrixOptions", _Options):
|
||||||
|
return script.build_options(config, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_settings_are_used(script):
|
||||||
|
options = _script_options(script, CONFIG)
|
||||||
|
assert options.gpio_slowdown == 7
|
||||||
|
assert options.rp1_rio == 1
|
||||||
|
assert options.panel_type == "FM6126A"
|
||||||
|
assert options.row_address_type == 5
|
||||||
|
assert options.pixel_mapper_config == "U-mapper;Rotate:180"
|
||||||
|
assert options.limit_refresh_rate_hz == 120
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_options_match_the_display_service(script):
|
||||||
|
"""Same config in, same options out, attribute for attribute."""
|
||||||
|
service = DisplayManager.apply_matrix_options(_Options(), CONFIG)
|
||||||
|
assert vars(_script_options(script, CONFIG)) == vars(service)
|
||||||
|
|
||||||
|
|
||||||
|
def test_measure_runs_uncapped(script):
|
||||||
|
assert _script_options(script, CONFIG, refresh_override=0).limit_refresh_rate_hz == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_empty_config_gets_the_service_defaults(script):
|
||||||
|
options = _script_options(script, {})
|
||||||
|
assert vars(options) == vars(DisplayManager.apply_matrix_options(_Options(), {}))
|
||||||
|
assert options.gpio_slowdown == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_service_uses_the_shared_builder(test_config):
|
||||||
|
"""DisplayManager._setup_matrix fills its options through the same call."""
|
||||||
|
with patch.object(display_manager, "RGBMatrix"), \
|
||||||
|
patch.object(display_manager, "RGBMatrixOptions", _Options), \
|
||||||
|
patch.object(display_manager, "freetype"), \
|
||||||
|
patch.object(DisplayManager, "apply_matrix_options",
|
||||||
|
wraps=DisplayManager.apply_matrix_options) as shared, \
|
||||||
|
patch.dict(os.environ, {"EMULATOR": "false"}):
|
||||||
|
DisplayManager._instance = None
|
||||||
|
try:
|
||||||
|
DisplayManager(test_config)
|
||||||
|
finally:
|
||||||
|
DisplayManager._instance = None
|
||||||
|
shared.assert_called_once()
|
||||||
Reference in New Issue
Block a user