From 911c559fd477e0364b6dbbe381fd7b887a893a34 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 22:30:21 +0000 Subject: [PATCH] ci: run the whole test tree and make the plugin-safety job assert something real MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unit-tests CI job ran an explicit 24-file allowlist that had rotted: 63 of 90 test files (display, vegas, store manager, web API, web_interface) never ran on a PR. The job now runs all of test/ (minus test/plugins, which the plugin-safety job owns) so new test files are enrolled by default and any exclusion needs a visible, commented --ignore. The plugin-safety job was a green no-op: plugins/ is empty in CI, so every test skipped with 'Manifest not found'. It now renders a bundled deterministic fixture plugin (test/fixtures/plugins/ci-fixture-plugin, golden images included for all 8 default sizes) via LEDMATRIX_PLUGINS_DIR, and sets LEDMATRIX_REQUIRE_PLUGINS=1 so discovering zero plugins fails loudly instead of skipping green. The per-plugin suites document that they target dev machines with real plugins installed. Coverage is now measured and enforced in exactly one place — the CI unit-tests step (--cov=src --cov=web_interface --cov-fail-under=45, from a measured 47% baseline). pytest.ini previously declared --cov-fail-under=30 but CI always passed --no-cov, so the gate had never run anywhere; local pytest is now coverage-free and fast. Enabling the 63 unenrolled files surfaced three cases of test rot, fixed here: test_display_controller_vegas_tick.py could not collect without the hardware rgbmatrix module (now uses the emulator convention), the state-reconciliation unrecoverable-cache tests broke when production added the is_plugin_uninstalled tombstone check (bare Mock returned truthy), and test_get_system_status assumed the optional psutil dependency (now installed via requirements-test.txt and guarded by importorskip). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh --- .github/workflows/test.yml | 53 ++++++------------ pytest.ini | 11 ++-- requirements-test.txt | 2 + .../ci-fixture-plugin/config_schema.json | 31 ++++++++++ .../plugins/ci-fixture-plugin/manager.py | 38 +++++++++++++ .../plugins/ci-fixture-plugin/manifest.json | 13 +++++ .../ci-fixture-plugin/requirements.txt | 1 + .../test/golden/128x32/ci-fixture.png | Bin 0 -> 359 bytes .../test/golden/128x64/ci-fixture.png | Bin 0 -> 586 bytes .../test/golden/128x96/ci-fixture.png | Bin 0 -> 849 bytes .../test/golden/256x128/ci-fixture.png | Bin 0 -> 1180 bytes .../test/golden/256x32/ci-fixture.png | Bin 0 -> 395 bytes .../test/golden/64x32/ci-fixture.png | Bin 0 -> 319 bytes .../test/golden/64x64/ci-fixture.png | Bin 0 -> 466 bytes .../test/golden/96x48/ci-fixture.png | Bin 0 -> 454 bytes test/plugins/conftest.py | 12 +++- test/plugins/test_basketball_scoreboard.py | 5 ++ test/plugins/test_calendar.py | 5 ++ test/plugins/test_clock_simple.py | 5 ++ test/plugins/test_odds_ticker.py | 5 ++ test/plugins/test_soccer_scoreboard.py | 5 ++ test/plugins/test_text_display.py | 5 ++ test/test_display_controller_vegas_tick.py | 9 +++ test/test_web_api.py | 3 + .../test_state_reconciliation.py | 4 ++ 25 files changed, 163 insertions(+), 44 deletions(-) create mode 100644 test/fixtures/plugins/ci-fixture-plugin/config_schema.json create mode 100644 test/fixtures/plugins/ci-fixture-plugin/manager.py create mode 100644 test/fixtures/plugins/ci-fixture-plugin/manifest.json create mode 100644 test/fixtures/plugins/ci-fixture-plugin/requirements.txt create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/128x32/ci-fixture.png create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/128x64/ci-fixture.png create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/128x96/ci-fixture.png create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/256x128/ci-fixture.png create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/256x32/ci-fixture.png create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/64x32/ci-fixture.png create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/64x64/ci-fixture.png create mode 100644 test/fixtures/plugins/ci-fixture-plugin/test/golden/96x48/ci-fixture.png diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aadebc4d..062e85b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,12 @@ jobs: plugin-safety: name: Plugin safety harness + unit tests runs-on: ubuntu-latest + env: + # The bundled fixture plugin gives the harness at least one real plugin + # to render, and REQUIRE_PLUGINS turns "discovered zero plugins" into a + # hard failure instead of a silent all-skip green run. + LEDMATRIX_PLUGINS_DIR: test/fixtures/plugins + LEDMATRIX_REQUIRE_PLUGINS: "1" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -29,12 +35,9 @@ jobs: pip install -r requirements.txt -r requirements-test.txt pip install RGBMatrixEmulator - - name: Run harness + visual rendering tests + - name: Run plugin safety harness run: | - pytest --no-cov \ - test/plugins/test_harness.py \ - test/plugins/test_visual_rendering.py \ - test/plugins/test_plugin_matrix.py + pytest --no-cov test/plugins/ unit-tests: name: Core unit tests @@ -55,35 +58,15 @@ jobs: pip install -r requirements.txt -r requirements-test.txt pip install RGBMatrixEmulator - # Safety net for the shared sports/scroll/style infrastructure. These - # suites existed but were not enrolled in CI, so a refactor of - # src/base_classes or src/common could regress them silently. Enrolled - # explicitly (not `pytest test/`) so known hardware-only suites don't - # break CI; grow this list as more suites are made headless. + # Run the ENTIRE test tree (except test/plugins, which the + # plugin-safety job owns). New test files are enrolled automatically; + # excluding anything requires a visible, commented --ignore here. + # Coverage is measured and enforced only in this step — pytest.ini + # deliberately carries no coverage flags so local runs stay fast. - name: Run core unit suites run: | - pytest --no-cov \ - test/test_skin_system.py \ - test/test_font_manager.py \ - test/test_data_sources.py \ - test/test_api_extractors.py \ - test/test_scroll_helper.py \ - test/test_scroll_helper_continuous.py \ - test/test_adaptive_layout.py \ - test/test_loader_compat_warning.py \ - test/test_sports_base_characterization.py \ - test/test_element_style.py \ - test/test_sports_core_promotions.py \ - test/test_sports_modes_promotions.py \ - test/test_sports_capabilities.py \ - test/test_sports_scroll.py \ - test/test_version_consistency.py \ - test/test_plugin_compatibility_gate.py \ - test/test_install_preserves_existing.py \ - test/test_core_owned_config_keys.py \ - test/test_async_plugin_updates.py \ - test/test_plugin_update_reservation.py \ - test/test_template_targets.py \ - test/test_widget_scripts.py \ - test/test_doc_links.py \ - test/web_interface/test_cache.py + pytest -m "not hardware" test/ \ + --ignore=test/plugins \ + --cov=src --cov=web_interface \ + --cov-report=term \ + --cov-fail-under=45 diff --git a/pytest.ini b/pytest.ini index a13cc6cb..aaffa360 100644 --- a/pytest.ini +++ b/pytest.ini @@ -10,16 +10,13 @@ python_functions = test_* testpaths = test # Output options -# Note: Coverage options require pytest-cov to be installed -# Run: pip install pytest-cov -addopts = +# Coverage is deliberately NOT configured here: a bare local `pytest` should +# be fast and dependency-light. Coverage is measured and enforced in exactly +# one place — the unit-tests job in .github/workflows/test.yml. +addopts = -v --strict-markers --tb=short - --cov=src - --cov-report=term-missing - --cov-report=html - --cov-fail-under=30 # Markers markers = diff --git a/requirements-test.txt b/requirements-test.txt index 2faccf94..60e26951 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,4 +4,6 @@ pytest>=9.0.3,<10.0.0 pytest-cov>=4.1.0,<5.0.0 pytest-mock>=3.11.0,<4.0.0 freezegun>=1.2,<2 # deterministic time for golden-image tests +psutil>=6.0.0,<8.0.0 # optional at runtime; installed for tests so the + # /system/status endpoint's real path is exercised mypy>=1.5.0,<2.0.0 # static type checking (also pinned in .pre-commit-config.yaml) diff --git a/test/fixtures/plugins/ci-fixture-plugin/config_schema.json b/test/fixtures/plugins/ci-fixture-plugin/config_schema.json new file mode 100644 index 00000000..9410d90a --- /dev/null +++ b/test/fixtures/plugins/ci-fixture-plugin/config_schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CI Fixture Plugin", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "display_duration": { + "type": "number", + "default": 5 + }, + "border_color": { + "type": "array", + "items": {"type": "integer", "minimum": 0, "maximum": 255}, + "minItems": 3, + "maxItems": 3, + "default": [0, 255, 0], + "description": "RGB color of the border rectangle." + }, + "diagonal_color": { + "type": "array", + "items": {"type": "integer", "minimum": 0, "maximum": 255}, + "minItems": 3, + "maxItems": 3, + "default": [255, 0, 0], + "description": "RGB color of the diagonals." + } + } +} diff --git a/test/fixtures/plugins/ci-fixture-plugin/manager.py b/test/fixtures/plugins/ci-fixture-plugin/manager.py new file mode 100644 index 00000000..6e3d5d6e --- /dev/null +++ b/test/fixtures/plugins/ci-fixture-plugin/manager.py @@ -0,0 +1,38 @@ +""" +CI fixture plugin. + +Exists so the plugin safety harness (test/plugins/test_plugin_matrix.py and +the plugin-safety CI job) always has at least one real plugin to load and +render — without it, an empty plugins/ directory turns the whole job into a +green no-op. The render is deliberately trivial and fully deterministic: +a border rectangle plus both diagonals, sized from the display manager's +declared dimensions. No fonts, no network, no time dependence, so golden +images are stable across platforms. +""" + +from PIL import ImageDraw + +from src.plugin_system.base_plugin import BasePlugin + + +class CIFixturePlugin(BasePlugin): + def update(self) -> None: + """Nothing to fetch — the render is self-contained.""" + + def display(self, force_clear: bool = False) -> None: + width = self.display_manager.matrix.width + height = self.display_manager.matrix.height + border = tuple(self.config.get("border_color", [0, 255, 0])) + diagonal = tuple(self.config.get("diagonal_color", [255, 0, 0])) + + image = self.display_manager.image + draw = ImageDraw.Draw(image) + # Blank only the declared panel area, then draw edge-to-edge content: + # the border proves the plugin reads dynamic dimensions (any overflow + # or underfill at any size is a harness bug or a dimensions bug), the + # diagonals make golden comparisons sensitive to size/offset drift. + draw.rectangle([0, 0, width - 1, height - 1], fill=(0, 0, 0)) + draw.rectangle([0, 0, width - 1, height - 1], outline=border) + draw.line([0, 0, width - 1, height - 1], fill=diagonal) + draw.line([0, height - 1, width - 1, 0], fill=diagonal) + self.display_manager.update_display() diff --git a/test/fixtures/plugins/ci-fixture-plugin/manifest.json b/test/fixtures/plugins/ci-fixture-plugin/manifest.json new file mode 100644 index 00000000..40d188cb --- /dev/null +++ b/test/fixtures/plugins/ci-fixture-plugin/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "ci-fixture-plugin", + "name": "CI Fixture Plugin", + "version": "1.0.0", + "description": "Bundled test fixture so the plugin safety harness always has at least one real plugin to render in CI. Draws a deterministic border + diagonals pattern at any panel size. Not installable from the store and never shipped to devices.", + "author": "LEDMatrix", + "entry_point": "manager.py", + "class_name": "CIFixturePlugin", + "display_modes": ["ci-fixture"], + "update_interval": 3600, + "min_ledmatrix_version": "2.0.0", + "compatible_versions": [">=2.0.0"] +} diff --git a/test/fixtures/plugins/ci-fixture-plugin/requirements.txt b/test/fixtures/plugins/ci-fixture-plugin/requirements.txt new file mode 100644 index 00000000..81444625 --- /dev/null +++ b/test/fixtures/plugins/ci-fixture-plugin/requirements.txt @@ -0,0 +1 @@ +# No dependencies — the fixture must load in any environment. diff --git a/test/fixtures/plugins/ci-fixture-plugin/test/golden/128x32/ci-fixture.png b/test/fixtures/plugins/ci-fixture-plugin/test/golden/128x32/ci-fixture.png new file mode 100644 index 0000000000000000000000000000000000000000..32608a8c3a0c8efb18231730a363afa8e5d12f44 GIT binary patch literal 359 zcmV-t0hs=YP)oBOg6$FQQI>6VfY{HR*_n00>tM%hC~Z z0g%=hvq(ow2S6@?kdbOI=T z1F-f*WLyAij~=Oh5lP4fFev>3m6eJ=z+BQx42!`NU=8UdL}T;^SX+7t_84se-YLBV zON@E|mwt!t82JD${RvGmjsPxQf@+LoQ@U?YA4%Gt{srq!L5{~wKKcLv002ovPDHLk FV1oWkkY)e? literal 0 HcmV?d00001 diff --git a/test/fixtures/plugins/ci-fixture-plugin/test/golden/128x64/ci-fixture.png b/test/fixtures/plugins/ci-fixture-plugin/test/golden/128x64/ci-fixture.png new file mode 100644 index 0000000000000000000000000000000000000000..de738059fbc4e7fc37f36ae09c5e794ec19bb173 GIT binary patch literal 586 zcmV-Q0=4~#P)$vXXTP?G9@w5DBC_G8a_qUiZeL%Dz^%Wv zpYp&r+s^~P-&Ypx7k=o6i@~nes*l}0#jv>JCEoyudO|VG)U#d_K-80qVYL3mJ^+Y% zp8ytyaYz+iE8%K*?-8SJiTF#x&}gY{LI0HEv)J|{N>Kp7c4PiqcS*Aw`08kG@uIW)40Mug0I6X=RfHZ@?Cu3jH2e^AFe*@}IP*|;@ z8UD3*t~3BW<7Q+?05CdeE(U6VnGv%vkOHh4j-P=Jz+xu!TE3`WDW_kZQ=2W)eJKrX&~PJE0NSBIOu~UeElBOcD2O6j4p^@+3Ze+<2uwQ^xT!c`tP8b8NCi zQbCkK9RX{H0zDxI9W{Dr3tmB#rZS!M1g{`UrH-6vhvH^h4j$_D)`}|yQEp3i*oP|x zQBFE?sGWaYEEdQ#AY|y>cG|!*m$gVSqfHB3tpIoq98Rz;OPip zBv!sS*S?U&FHg5rkhuylbQHHJnckulH|ZT%-K3|jAb>4@kDZRF>z4Ag3UA+9@HgBY`EJVDPVqG$o;+DbwHK zZ;e+$(`cuJjEhLl!qzO8f|g2uho|Ln1x>G=5_~!uSjq(l|H@DZBMOT2|LBjR zPeIFSr^J|!8ka8y|AFRkS``%OztR>_y@EnJm9*=qV##9gJE#h+MnRE&H}BCsD=4%x zNS%%f%M^ppNHNTkf+Br(?jyY_D6}I)Sx5QyX&^;DzsPq70!cD?_MJjI>QEhRd4B{J b4J`+&P57YIlVMgKN%kMoq6Hzv3hK-&SCM0 z*^i`}n-iAmIz#Y?aZ}|e=b=ro1TSq?-K<5fcH34VUmEwmuLVOd98Mu-=RNxp!Q@nQ z4+t9G{aONBQ~`pOPyAgZfgO${eQC~OgwVLAsR=r3;B6zCSyDn_`lK6IEfB7V@%tgi z$4XLxY?|=-`WsM0w3Yo=7LW234C&VwM?fJgxiwS_r1(U77ry~;NJDRHf*PlEAUwae zZ8a384lO)`hS;il=TR4iVz+h-Unz&+*lUy0)q>Jd?)`rleIXby+a+y+J4?NLfBn-& zQXyS;`Jk5s?lhO@2ByOZ!>;Gf%j_8n3qCg*qJt6dYhI0EmXuDL*7(@Q9SY8KG5(Rz z@lhcjh=);0-ZPPF;Vo}<@S1A<;EtKU11^t2VIpy|I*LQ-4DF{qcL|}Os@d6)3Y{F! z*i9fVfIaLx96f4_m@7B41s^h3p}B%xc)&=p8+vV}n7y|1 z;0e%Ks+P7i4VQ!KvL5ZyioERr+om#@E|>*O`iN-Cw8qX(yTeUH&zrb(Z!4i*^y0KJ z8MyqI51JGfh&5g+1on@0jPc!Su&NniLtM!tIW8Z?k@L}>r7*d%f>7b^>wG~B}*xZD-cH`xrNjVKu!a@7K`R+GL^R# zGz;QeoP5bl0dr^5-6%YdOOum@m4!H(q&-}%XtPJ;PdCd%NyXsczZ-%?>TuBAVSSQN z^0lB}AYk*kn&|5EQ@4cZtj9~WPUDWGmIYCw2`R`@I9ZbgVW3Ol$|F-(Gqt=|6uS`> zZsCgjy<8>Zb`^y}C*ZJNr%44h>ldQN37$o)sc2!~zD5QHd~?EnBPuOp-*^mOtdb4^ zuhgG^=aFj&YNq&U-&yHJa3!tMGI?Xfa!F_T^yho=7nm-aj8pVdhd_WY)#^eXC#d;f zRLjLIjocPbJb$U=uVqS0cJH`Npd9`?iJdn#h{ytDb)h}lePGenMr>9Yd_jLnaXx$_ zA*i=Jo=~7Pcz~=DR6HWmyD;@E^I0%?z)IVF=3bhcNY4k4?>!nJfvj)i^1N;p=hR~1G9xqEY_s?8rX+FIw4>#P{#_A_C)6-v*yUG-xxK-ag)HMbtX zx=r0rf5NhqB1bM`>~7B|vn{kA?e1jadm&zWKA7!;*tvH-h8-~Vr-Hu>iLfT`$(Z6X o#)hIJ_>XUi_>T-Ihp>0+~GAv!hiJ4Srw?iE_zJskVVwu5#Nfa*6zXq}6{>J_l^- zs^m+)t*@W%4%lw6GV1_n2nNntRB62d&^!%B<0ruWvKbA4rg6C3G+?I&Rt2DeZYmG^ z=k!1TGz7yhzvswM05k-{j(BVCAOITX??qBApyL2&n7=!*8WJ)94LQ51*#B-&t^jCa z1jEE+%z8T2XlQ+9{_L?cYw)gfqWqVZ)})E z4bI;o5r7thmth|B%|!r2qLfaIrx}~&3P8?MAQ%k+13;JDi7`W52Y?IMBB{m<2LXt; z=zhiPrVItZ?;^r)Fb)L3{-R(O`@vWhfK-DNp0YE4j|RX_lo7!G`MWm&6gtjY)J@d^ ppwQvVU@+>}q4Rol#8$l?{RhXDL5|#HwhjOQ002ovPDHLkV1gg>qv`+v literal 0 HcmV?d00001 diff --git a/test/fixtures/plugins/ci-fixture-plugin/test/golden/64x32/ci-fixture.png b/test/fixtures/plugins/ci-fixture-plugin/test/golden/64x32/ci-fixture.png new file mode 100644 index 0000000000000000000000000000000000000000..9312505bd58000e5f88d25570bfe74e899787448 GIT binary patch literal 319 zcmV-F0l@x=P)9$IxBC?ri%(j0_=A=xL017!yQl)6}|_R^N^VI(R-qKJf+sYBYzTogxB&n#M84Ix7I6 z0lo}NH#Gn>%+9cP7zBU@kA?6$hb90hgglV%<>vsXC1FI|0NenzdW^VxmouOiGk8lx zy}~+As*p3V@Fmm>8uu^m29Sh{U^oD@fqWSzfF*<%V-Vo={n;66K*SHx!cYLBA85uH zBEZZ?N-g6C@YF-SoY4hjc&#E~oB%RJ=elP60kTEs>M0004*Nklk02o z8HBf2NOc4jwj0>*j;A)>6M$-nA|e`il-2i#L_hjy}@)$(5n*pOaa zh!3zWy{sG)U~_swQ4YY7^xV=@fP?8-0mcBw(^rjR3;=?puj+>B0fb6lMMok50;aDb zf;|Dkrmt$_H2}fWS7Ez>R7Y&#?G@gek_fM-MnM$V10uelUJf`xAE`SJLXbh&>DT;sy zUbJ7PdpoZHB3Pg=rk34S0}(71pQ(L^H-QKij>#mot0o|VO~hf6-H8AY7lBD*bul2$ zi%D-qVIaVHCoD`G&yPFXZ*TUrW`DhBn)iU{>IyeAv zB}@@cKHY%098&}u&KZb9CcGy?FYgURFT{V)bBJZjK}7iK%eeuS3?G5X0jPFhUnWza zmLa{EjDVyDvu9EV(ip~)NfAhU02?M1Aj?BQn@JeR>$66WNen2$Nu36h08j)POqQt$ zC>jkW#dH%WG7Z+wR1MS%4c5X`0n{T6c8{qDsCOEymgxd$BpR%oDGM|z4OYVR2WSQw w?40QbXf_%w$Fu>>OoJVn((`kuT>NvWH(5ku*sm$;o&W#<07*qoM6N<$g2u|azyJUM literal 0 HcmV?d00001 diff --git a/test/plugins/conftest.py b/test/plugins/conftest.py index 27bb6718..cff6dd42 100644 --- a/test/plugins/conftest.py +++ b/test/plugins/conftest.py @@ -23,9 +23,17 @@ os.environ['EMULATOR'] = 'true' def plugins_dir() -> Path: """Get the plugins directory path. - Checks plugins/ first, then falls back to plugin-repos/ - for monorepo development environments. + Honors LEDMATRIX_PLUGINS_DIR (first entry) when set — the same override + test_plugin_matrix.py uses, so CI can point every plugin suite at the + bundled fixture plugins. Otherwise checks plugins/ first, then falls + back to plugin-repos/ for monorepo development environments. """ + env = os.environ.get('LEDMATRIX_PLUGINS_DIR') + if env: + first = env.split(os.pathsep)[0] + if first: + return Path(first) + plugins_path = project_root / 'plugins' plugin_repos_path = project_root / 'plugin-repos' diff --git a/test/plugins/test_basketball_scoreboard.py b/test/plugins/test_basketball_scoreboard.py index 0fbd8b79..b93fb89f 100644 --- a/test/plugins/test_basketball_scoreboard.py +++ b/test/plugins/test_basketball_scoreboard.py @@ -1,5 +1,10 @@ """ Integration tests for basketball-scoreboard plugin. + +Requires the real plugin to be installed (plugins/ or plugin-repos/, +or the dir named by LEDMATRIX_PLUGINS_DIR) — on machines without it, +every test here skips by design. CI covers plugin safety with the +bundled fixture plugin via test_plugin_matrix.py instead. """ import pytest diff --git a/test/plugins/test_calendar.py b/test/plugins/test_calendar.py index 18528fdb..4d874940 100644 --- a/test/plugins/test_calendar.py +++ b/test/plugins/test_calendar.py @@ -1,5 +1,10 @@ """ Integration tests for calendar plugin. + +Requires the real plugin to be installed (plugins/ or plugin-repos/, +or the dir named by LEDMATRIX_PLUGINS_DIR) — on machines without it, +every test here skips by design. CI covers plugin safety with the +bundled fixture plugin via test_plugin_matrix.py instead. """ import pytest diff --git a/test/plugins/test_clock_simple.py b/test/plugins/test_clock_simple.py index 507feec9..9b25a59e 100644 --- a/test/plugins/test_clock_simple.py +++ b/test/plugins/test_clock_simple.py @@ -1,5 +1,10 @@ """ Integration tests for clock-simple plugin. + +Requires the real plugin to be installed (plugins/ or plugin-repos/, +or the dir named by LEDMATRIX_PLUGINS_DIR) — on machines without it, +every test here skips by design. CI covers plugin safety with the +bundled fixture plugin via test_plugin_matrix.py instead. """ import pytest diff --git a/test/plugins/test_odds_ticker.py b/test/plugins/test_odds_ticker.py index 90209548..231ed7de 100644 --- a/test/plugins/test_odds_ticker.py +++ b/test/plugins/test_odds_ticker.py @@ -1,5 +1,10 @@ """ Integration tests for odds-ticker plugin. + +Requires the real plugin to be installed (plugins/ or plugin-repos/, +or the dir named by LEDMATRIX_PLUGINS_DIR) — on machines without it, +every test here skips by design. CI covers plugin safety with the +bundled fixture plugin via test_plugin_matrix.py instead. """ import pytest diff --git a/test/plugins/test_soccer_scoreboard.py b/test/plugins/test_soccer_scoreboard.py index 36212bfd..d5526fb8 100644 --- a/test/plugins/test_soccer_scoreboard.py +++ b/test/plugins/test_soccer_scoreboard.py @@ -1,5 +1,10 @@ """ Integration tests for soccer-scoreboard plugin. + +Requires the real plugin to be installed (plugins/ or plugin-repos/, +or the dir named by LEDMATRIX_PLUGINS_DIR) — on machines without it, +every test here skips by design. CI covers plugin safety with the +bundled fixture plugin via test_plugin_matrix.py instead. """ import pytest diff --git a/test/plugins/test_text_display.py b/test/plugins/test_text_display.py index a43815ea..34adfa49 100644 --- a/test/plugins/test_text_display.py +++ b/test/plugins/test_text_display.py @@ -1,5 +1,10 @@ """ Integration tests for text-display plugin. + +Requires the real plugin to be installed (plugins/ or plugin-repos/, +or the dir named by LEDMATRIX_PLUGINS_DIR) — on machines without it, +every test here skips by design. CI covers plugin safety with the +bundled fixture plugin via test_plugin_matrix.py instead. """ import pytest diff --git a/test/test_display_controller_vegas_tick.py b/test/test_display_controller_vegas_tick.py index 643359f3..538e9272 100644 --- a/test/test_display_controller_vegas_tick.py +++ b/test/test_display_controller_vegas_tick.py @@ -11,9 +11,18 @@ orphaning VegasModeCoordinator.mark_plugin_updated() -- it has had zero callers since. """ +import os from typing import Dict, List, Optional from unittest.mock import MagicMock +# display_controller imports display_manager, which imports the hardware +# rgbmatrix module unless EMULATOR=true was set before import. Use the +# emulator (same convention as test_display_dirty_tracking.py and +# test/plugins/conftest.py) so this file collects on machines without the +# hardware library — and so display_manager gets the emulator binding no +# matter which test module imports it first. +os.environ.setdefault("EMULATOR", "true") + from src.display_controller import DisplayController diff --git a/test/test_web_api.py b/test/test_web_api.py index 418bb68c..8cadf922 100644 --- a/test/test_web_api.py +++ b/test/test_web_api.py @@ -393,6 +393,9 @@ class TestSystemAPI: @patch('web_interface.blueprints.api_v3.subprocess') def test_get_system_status(self, mock_subprocess, client): """Test getting system status.""" + # The endpoint returns 503 without psutil, which is an optional + # runtime dependency (requirements-test.txt installs it for CI). + pytest.importorskip("psutil") mock_result = MagicMock() mock_result.stdout = 'active\n' mock_result.returncode = 0 diff --git a/test/web_interface/test_state_reconciliation.py b/test/web_interface/test_state_reconciliation.py index 19f23833..7d01b886 100644 --- a/test/web_interface/test_state_reconciliation.py +++ b/test/web_interface/test_state_reconciliation.py @@ -367,6 +367,10 @@ class TestStateReconciliationUnrecoverable(unittest.TestCase): self.store_manager.fetch_registry.return_value = {"plugins": []} self.store_manager.install_plugin.return_value = False self.store_manager.was_recently_uninstalled.return_value = False + # A bare Mock() returns a truthy Mock for is_plugin_uninstalled(), + # which reads as "persistently uninstalled" and skips auto-repair + # entirely — these tests need the repair path to run. + self.store_manager.is_plugin_uninstalled.return_value = False self.reconciler = StateReconciliation( state_manager=self.state_manager,