Enhance Home Assistant dashboard designer documentation and validation scripts #1566
- Added workflow notes for direct updates and post-edit validation steps in README.md. - Updated SKILL.md to clarify the workflow for editing dashboards and added validation order. - Revised dashboard_rules.md to emphasize direct-update workflow and validation requirements. - Enhanced validate_lovelace_view.py to enforce new validation rules for card styles and section constraints. - Improved error handling and validation logic for dashboard sections in validate_lovelace_view.py.feature/powerwall-live-activity-1598
parent
b3e039e509
commit
1d8aed02df
@ -0,0 +1,127 @@
|
|||||||
|
import contextlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_PATH = (
|
||||||
|
Path(__file__).resolve().parents[1] / "scripts" / "validate_lovelace_view.py"
|
||||||
|
)
|
||||||
|
MODULE_NAME = "validate_lovelace_view"
|
||||||
|
SPEC = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
|
||||||
|
if SPEC is None or SPEC.loader is None:
|
||||||
|
raise RuntimeError(f"Unable to load module spec from {MODULE_PATH}")
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[MODULE_NAME] = MODULE
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateLovelaceViewTests(unittest.TestCase):
|
||||||
|
def _run_validator(self, yaml_text: str, *extra_args: str) -> tuple[int, str, str]:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
view_path = Path(tmpdir) / "view.yaml"
|
||||||
|
view_path.write_text(yaml_text, encoding="utf-8")
|
||||||
|
|
||||||
|
stdout = io.StringIO()
|
||||||
|
stderr = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
||||||
|
rc = MODULE.main([str(view_path), *extra_args])
|
||||||
|
return rc, stdout.getvalue(), stderr.getvalue()
|
||||||
|
|
||||||
|
def test_valid_classic_view_passes(self) -> None:
|
||||||
|
yaml_text = """
|
||||||
|
title: Main
|
||||||
|
path: main
|
||||||
|
cards:
|
||||||
|
- type: custom:button-card
|
||||||
|
template:
|
||||||
|
- status_chip
|
||||||
|
"""
|
||||||
|
rc, out, err = self._run_validator(yaml_text)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertEqual(out.strip(), "")
|
||||||
|
self.assertEqual(err.strip(), "")
|
||||||
|
|
||||||
|
def test_horizontal_stack_fails(self) -> None:
|
||||||
|
yaml_text = """
|
||||||
|
title: Main
|
||||||
|
path: main
|
||||||
|
cards:
|
||||||
|
- type: horizontal-stack
|
||||||
|
cards:
|
||||||
|
- type: custom:button-card
|
||||||
|
template: status_chip
|
||||||
|
"""
|
||||||
|
rc, out, _ = self._run_validator(yaml_text)
|
||||||
|
self.assertEqual(rc, 1)
|
||||||
|
self.assertIn("Disallowed card type: horizontal-stack", out)
|
||||||
|
|
||||||
|
def test_button_card_missing_template_fails(self) -> None:
|
||||||
|
yaml_text = """
|
||||||
|
title: Main
|
||||||
|
path: main
|
||||||
|
cards:
|
||||||
|
- type: custom:button-card
|
||||||
|
entity: light.kitchen
|
||||||
|
"""
|
||||||
|
rc, out, _ = self._run_validator(yaml_text)
|
||||||
|
self.assertEqual(rc, 1)
|
||||||
|
self.assertIn("custom:button-card must set template", out)
|
||||||
|
|
||||||
|
def test_sections_wrapper_rule_fails_when_full_width_missing(self) -> None:
|
||||||
|
yaml_text = """
|
||||||
|
title: Sections
|
||||||
|
path: sections
|
||||||
|
type: sections
|
||||||
|
sections:
|
||||||
|
- type: grid
|
||||||
|
column_span: 4
|
||||||
|
cards:
|
||||||
|
- type: custom:vertical-stack-in-card
|
||||||
|
cards:
|
||||||
|
- type: custom:button-card
|
||||||
|
template: status_chip
|
||||||
|
"""
|
||||||
|
rc, out, _ = self._run_validator(yaml_text)
|
||||||
|
self.assertEqual(rc, 1)
|
||||||
|
self.assertIn('Wrapper card must set grid_options.columns: "full".', out)
|
||||||
|
|
||||||
|
def test_sections_wrapper_rule_passes_when_full_width_wrapper_present(self) -> None:
|
||||||
|
yaml_text = """
|
||||||
|
title: Sections
|
||||||
|
path: sections
|
||||||
|
type: sections
|
||||||
|
sections:
|
||||||
|
- type: grid
|
||||||
|
column_span: 4
|
||||||
|
cards:
|
||||||
|
- type: custom:vertical-stack-in-card
|
||||||
|
grid_options:
|
||||||
|
columns: "full"
|
||||||
|
cards:
|
||||||
|
- type: custom:button-card
|
||||||
|
template: status_chip
|
||||||
|
"""
|
||||||
|
rc, out, err = self._run_validator(yaml_text)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertEqual(out.strip(), "")
|
||||||
|
self.assertEqual(err.strip(), "")
|
||||||
|
|
||||||
|
def test_unknown_include_tag_is_parse_safe(self) -> None:
|
||||||
|
yaml_text = """
|
||||||
|
title: Included Sections
|
||||||
|
path: included_sections
|
||||||
|
type: sections
|
||||||
|
sections: !include ../sections/core.yaml
|
||||||
|
"""
|
||||||
|
rc, out, err = self._run_validator(yaml_text)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertIn("validator cannot inspect included sections content", out)
|
||||||
|
self.assertEqual(err.strip(), "")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
<h1 align="center">
|
||||||
|
<a name="logo" href="https://www.vCloudInfo.com/tag/iot"><img src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/x_profile.png" alt="Bear Stone Smart Home" width="200"></a>
|
||||||
|
<br>
|
||||||
|
Bear Stone Smart Home Documentation
|
||||||
|
</h1>
|
||||||
|
<h4 align="center">Be sure to :star: my configuration repo so you can keep up to date on any daily progress!</h4>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://x.com/ccostan)
|
||||||
|
[](https://www.youtube.com/vCloudInfo?sub_confirmation=1)
|
||||||
|
[](https://github.com/CCOSTAN) <br>
|
||||||
|
[](https://github.com/CCOSTAN/Home-AssistantConfig/blob/master/config/.HA_VERSION)
|
||||||
|
[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
|
||||||
|
[](https://github.com/CCOSTAN/Home-AssistantConfig/commits/master)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# HA YAML DRY Verifier (Codex Skill)
|
||||||
|
|
||||||
|
This directory contains the `homeassistant-yaml-dry-verifier` skill and the CLI used to detect repeated YAML structures in Home Assistant automations/scripts/packages.
|
||||||
|
|
||||||
|
### Quick navigation
|
||||||
|
- You are here: `codex_skills/homeassistant-yaml-dry-verifier/`
|
||||||
|
- [Repo overview](../../README.md) | [Codex skills](../README.md) | [Packages](../../config/packages/README.md) | [Scripts](../../config/script/README.md)
|
||||||
|
|
||||||
|
## What This Skill Does
|
||||||
|
|
||||||
|
- Detects repeated `trigger`, `condition`, `action`, and `sequence` blocks.
|
||||||
|
- Detects repeated entries inside those blocks.
|
||||||
|
- Detects duplicate entries within a single block (`INTRA`).
|
||||||
|
- Detects package-defined scripts called from multiple files (`CENTRAL_SCRIPT`).
|
||||||
|
- Collapses noisy ENTRY reports when they are already fully explained by an identical `FULL_BLOCK` finding.
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
Run on one file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages/bearclaw.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Run on broader scope:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages config/script
|
||||||
|
```
|
||||||
|
|
||||||
|
Strict mode (non-zero exit if findings exist):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python codex_skills/homeassistant-yaml-dry-verifier/scripts/verify_ha_yaml_dry.py config/packages config/script --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Model
|
||||||
|
|
||||||
|
The CLI prints:
|
||||||
|
- Scan summary counts
|
||||||
|
- `FULL_BLOCK` findings
|
||||||
|
- `ENTRY` findings
|
||||||
|
- `INTRA` findings
|
||||||
|
- `CENTRAL_SCRIPT` findings
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
- `0`: success (or findings in non-strict mode)
|
||||||
|
- `1`: findings present in strict mode
|
||||||
|
- `2`: parse/path errors
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This verifier intentionally keeps text output and a small CLI surface.
|
||||||
|
- It does not implement suppression files, severity scoring, JSON output, or diff-only mode.
|
||||||
|
- Use it as a fast pre-refactor signal and pair with Home Assistant config validation before restart/reload.
|
||||||
|
|
||||||
|
**All of my configuration files are tested against the most stable version of home-assistant.**
|
||||||
|
|
||||||
|
<a name="bottom" href="https://github.com/CCOSTAN/Home-AssistantConfig#logo"><img align="right" border="0" src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/config/www/custom_ui/floorplan/images/branding/up_arrow.png" width="25" ></a>
|
||||||
|
|
||||||
|
**Still have questions on my Config?** <br>
|
||||||
|
**Message me on X :** [](https://www.x.com/ccostan)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a target="_blank" href="https://www.buymeacoffee.com/vCloudInfo"><img src="https://www.buymeacoffee.com/assets/img/BMC-btn-logo.svg" alt="Buy me a coffee"><span style="margin-left:5px">You can buy me a coffee</span></a><a target="_blank" href="https://www.buymeacoffee.com/vCloudInfo"><img src="https://www.buymeacoffee.com/assets/img/BMC-btn-logo.svg" alt="Buy me a coffee"></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://eepurl.com/dmXFYz"><img align="center" border="0" src="https://raw.githubusercontent.com/CCOSTAN/Home-AssistantConfig/master/config/www/custom_ui/floorplan/images/branding/email_link.png" height="50" ></a><br>
|
||||||
|
<a href="https://www.vcloudinfo.com/p/affiliate-disclosure.html">
|
||||||
|
Affiliate Disclosure
|
||||||
|
</a></p>
|
||||||
@ -0,0 +1,302 @@
|
|||||||
|
import contextlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MODULE_PATH = (
|
||||||
|
Path(__file__).resolve().parents[1] / "scripts" / "verify_ha_yaml_dry.py"
|
||||||
|
)
|
||||||
|
MODULE_NAME = "verify_ha_yaml_dry"
|
||||||
|
SPEC = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
|
||||||
|
if SPEC is None or SPEC.loader is None:
|
||||||
|
raise RuntimeError(f"Unable to load module spec from {MODULE_PATH}")
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[MODULE_NAME] = MODULE
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyHaYamlDryTests(unittest.TestCase):
|
||||||
|
def _run_verifier(
|
||||||
|
self,
|
||||||
|
files: dict[str, str],
|
||||||
|
*extra_args: str,
|
||||||
|
scan_subpath: str = ".",
|
||||||
|
) -> tuple[int, str, str]:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
root = Path(tmpdir)
|
||||||
|
for rel_path, content in files.items():
|
||||||
|
file_path = root / rel_path
|
||||||
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
file_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
scan_path = root / scan_subpath
|
||||||
|
stdout = io.StringIO()
|
||||||
|
stderr = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
||||||
|
rc = MODULE.main([str(scan_path), *extra_args])
|
||||||
|
return rc, stdout.getvalue(), stderr.getvalue()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _full_block_group_order(output: str) -> list[str]:
|
||||||
|
marker = "\nFULL_BLOCK findings:\n"
|
||||||
|
if marker not in output:
|
||||||
|
return []
|
||||||
|
section = output.split(marker, 1)[1]
|
||||||
|
for stop in ("\nENTRY findings:\n", "\nINTRA findings:\n", "\nCENTRAL_SCRIPT findings:\n"):
|
||||||
|
if stop in section:
|
||||||
|
section = section.split(stop, 1)[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
groups: list[str] = []
|
||||||
|
for line in section.splitlines():
|
||||||
|
text = line.strip()
|
||||||
|
match = re.match(r"^\d+\.\s+([a-z]+\.[a-z_]+)\s+repeated\s+\d+\s+times$", text)
|
||||||
|
if match:
|
||||||
|
groups.append(match.group(1))
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def test_full_block_detection_with_fixture(self) -> None:
|
||||||
|
files = {
|
||||||
|
"automations.yaml": """
|
||||||
|
- alias: A1
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.one
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
- alias: A2
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.two
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
rc, out, err = self._run_verifier(files)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertIn("Duplicate full-block groups: 1", out)
|
||||||
|
self.assertIn("FULL_BLOCK findings:", out)
|
||||||
|
self.assertIn("automation.action repeated 2 times", out)
|
||||||
|
|
||||||
|
def test_entry_detection_with_fixture(self) -> None:
|
||||||
|
files = {
|
||||||
|
"automations.yaml": """
|
||||||
|
- alias: A1
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.one
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: script.shared_handler
|
||||||
|
- alias: A2
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.two
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: script.shared_handler
|
||||||
|
- delay: "00:00:01"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
rc, out, err = self._run_verifier(files)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertIn("Duplicate entry groups: 1", out)
|
||||||
|
self.assertIn("ENTRY findings:", out)
|
||||||
|
self.assertIn("automation.action entry repeated 2 times", out)
|
||||||
|
|
||||||
|
def test_intra_detection_with_fixture(self) -> None:
|
||||||
|
files = {
|
||||||
|
"automations.yaml": """
|
||||||
|
- alias: Repeat Inside Block
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.one
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: light.den
|
||||||
|
- service: light.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: light.den
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
rc, out, err = self._run_verifier(files)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertIn("INTRA findings:", out)
|
||||||
|
self.assertIn("INTRA automation.action: Repeat Inside Block has 2 duplicated entries", out)
|
||||||
|
|
||||||
|
def test_central_script_detection_with_package_definition_and_multi_caller(self) -> None:
|
||||||
|
files = {
|
||||||
|
"config/packages/shared_scripts.yaml": """
|
||||||
|
script:
|
||||||
|
my_shared_script:
|
||||||
|
alias: Shared Script
|
||||||
|
sequence:
|
||||||
|
- service: logbook.log
|
||||||
|
data:
|
||||||
|
name: Shared
|
||||||
|
message: Hello
|
||||||
|
""",
|
||||||
|
"config/automations/caller_one.yaml": """
|
||||||
|
automation:
|
||||||
|
- alias: Caller One
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.one
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: script.my_shared_script
|
||||||
|
""",
|
||||||
|
"config/automations/caller_two.yaml": """
|
||||||
|
automation:
|
||||||
|
- alias: Caller Two
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.two
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: script.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: script.my_shared_script
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
rc, out, err = self._run_verifier(files)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertIn("Central-script findings: 1", out)
|
||||||
|
self.assertIn("script.my_shared_script is package-defined and called from 2 files", out)
|
||||||
|
self.assertIn("suggestion: Move definition to config/script/my_shared_script.yaml", out)
|
||||||
|
|
||||||
|
def test_subsumed_entry_groups_are_collapsed(self) -> None:
|
||||||
|
files = {
|
||||||
|
"automations.yaml": """
|
||||||
|
- alias: A1
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.one
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
- alias: A2
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.two
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
rc, out, err = self._run_verifier(files)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
self.assertIn("Duplicate full-block groups: 1", out)
|
||||||
|
self.assertIn("Duplicate entry groups: 0", out)
|
||||||
|
|
||||||
|
def test_full_block_findings_order_is_deterministic(self) -> None:
|
||||||
|
files = {
|
||||||
|
"automations.yaml": """
|
||||||
|
- alias: A1
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.same
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
- alias: A2
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.same
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
""",
|
||||||
|
"scripts.yaml": """
|
||||||
|
script:
|
||||||
|
script_one:
|
||||||
|
sequence:
|
||||||
|
- service: light.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
script_two:
|
||||||
|
sequence:
|
||||||
|
- service: light.turn_off
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
rc, out, err = self._run_verifier(files)
|
||||||
|
self.assertEqual(rc, 0, out + err)
|
||||||
|
order = self._full_block_group_order(out)
|
||||||
|
self.assertGreaterEqual(len(order), 3, out)
|
||||||
|
self.assertEqual(order[:3], ["automation.action", "automation.trigger", "script.sequence"])
|
||||||
|
|
||||||
|
def test_exit_codes_for_strict_and_non_strict_modes(self) -> None:
|
||||||
|
files = {
|
||||||
|
"automations.yaml": """
|
||||||
|
- alias: A1
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.one
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
- alias: A2
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.two
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
rc_non_strict, _, _ = self._run_verifier(files)
|
||||||
|
rc_strict, _, _ = self._run_verifier(files, "--strict")
|
||||||
|
self.assertEqual(rc_non_strict, 0)
|
||||||
|
self.assertEqual(rc_strict, 1)
|
||||||
|
|
||||||
|
def test_parse_error_path_returns_exit_code_two(self) -> None:
|
||||||
|
files = {
|
||||||
|
"good.yaml": """
|
||||||
|
automation:
|
||||||
|
- alias: Good
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: binary_sensor.one
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: light.kitchen
|
||||||
|
""",
|
||||||
|
"bad.yaml": "automation: [\n",
|
||||||
|
}
|
||||||
|
rc, out, err = self._run_verifier(files)
|
||||||
|
self.assertEqual(rc, 2, out + err)
|
||||||
|
self.assertIn("Parse errors:", out)
|
||||||
|
self.assertIn("bad.yaml", out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in new issue