Skip to content

Commit 418e8f4

Browse files
committed
Fix wrong status issue and modify test units
Signed-off-by: Hunsup Jung <hunsup.jung@samsung.com>
1 parent b971722 commit 418e8f4

69 files changed

Lines changed: 5387 additions & 396 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Validate fingerprints.yml files changed in a PR.
4+
5+
Required fields per manufacturer section:
6+
matterManufacturer → id, deviceLabel, vendorId, productId, deviceProfileName
7+
zigbeeManufacturer → id, deviceLabel, manufacturer, model, deviceProfileName
8+
zwaveManufacturer → id, deviceLabel, manufacturerId, deviceProfileName
9+
+ at least one of: productId, productType
10+
11+
Hex fields (vendorId, productId, productType, manufacturerId) must use 0xNNNN notation.
12+
String fields must not be empty or have leading/trailing whitespace.
13+
id values that contain YAML-special characters must be quoted.
14+
15+
Indentation rules (spaces only, no tabs):
16+
Section key: col 0 e.g. "matterManufacturer:"
17+
Entry opening (- id: ...): 2-space e.g. " - id: ..."
18+
All other entry fields: 4-space e.g. " vendorId: 0x115F"
19+
20+
No trailing whitespace on any line.
21+
No duplicate id values within a file.
22+
23+
Generic sections (zigbeeGeneric, zwaveGeneric, matterGeneric, etc.) are skipped.
24+
25+
Usage:
26+
python3 tools/validate_fingerprints.py # auto-detect via git diff
27+
python3 tools/validate_fingerprints.py path/fingerprints.yml ...
28+
"""
29+
30+
import os
31+
import re
32+
import sys
33+
import subprocess
34+
from pathlib import Path
35+
36+
try:
37+
import yaml
38+
except ImportError:
39+
print("Error: pyyaml is required. pip install pyyaml", file=sys.stderr)
40+
sys.exit(2)
41+
42+
# ── Section configuration ─────────────────────────────────────────────────────
43+
44+
MANUFACTURER_SECTIONS = {'matterManufacturer', 'zigbeeManufacturer', 'zwaveManufacturer'}
45+
46+
# All fields that must be present in every entry for each section.
47+
# Z-Wave also needs productId OR productType (checked separately).
48+
REQUIRED_FIELDS = {
49+
'matterManufacturer': ['id', 'deviceLabel', 'vendorId', 'productId', 'deviceProfileName'],
50+
# manufacturer and model are checked together below: at least one must be present
51+
'zigbeeManufacturer': ['id', 'deviceLabel', 'deviceProfileName'],
52+
'zwaveManufacturer': ['id', 'deviceLabel', 'manufacturerId', 'deviceProfileName'],
53+
}
54+
55+
# These fields must be formatted as hex literals (0xNNNN).
56+
HEX_FIELDS = {'vendorId', 'productId', 'productType', 'manufacturerId'}
57+
58+
# YAML characters that force quoting when present in an unquoted scalar.
59+
_YAML_SPECIAL_RE = re.compile(r'[:{}\[\],&#*?|<>=!%@`]')
60+
61+
# Matches a valid hex literal.
62+
_HEX_RE = re.compile(r'^0x[0-9A-Fa-f]+$')
63+
64+
# Matches a field line: (indent)(key): (value)
65+
_FIELD_RE = re.compile(r'^( *)([\w]+): *(.*?) *$')
66+
67+
68+
# ── Raw-text analysis ─────────────────────────────────────────────────────────
69+
70+
def analyse_lines(lines, filepath):
71+
"""
72+
Single-pass raw-line analysis. Returns a list of errors and a dict:
73+
section_entry_lines[section] = list of (lineno, field, raw_value)
74+
for structured cross-checking later.
75+
"""
76+
errors = []
77+
section = None # current top-level key
78+
entry_indent = None # indent of the " - id:" line for current entry
79+
in_manufacturer_section = False
80+
81+
for lineno, raw in enumerate(lines, 1):
82+
line = raw.rstrip('\n')
83+
84+
# ── No tabs ───────────────────────────────────────────────────────────
85+
if '\t' in line:
86+
errors.append(f"{filepath}:{lineno}: tab character found (use spaces)")
87+
88+
# ── Trailing whitespace ───────────────────────────────────────────────
89+
if line != line.rstrip():
90+
errors.append(f"{filepath}:{lineno}: trailing whitespace")
91+
92+
stripped = line.strip()
93+
if not stripped or stripped.startswith('#'):
94+
continue
95+
96+
indent = len(line) - len(line.lstrip())
97+
98+
# ── Top-level section key detection ───────────────────────────────────
99+
if indent == 0 and line.endswith(':') and not line.startswith(' '):
100+
section = line[:-1].strip()
101+
in_manufacturer_section = section in MANUFACTURER_SECTIONS
102+
entry_indent = None
103+
continue
104+
105+
if not in_manufacturer_section:
106+
continue
107+
108+
# ── Entry opening line: " - id: ..." ───────────────────────────────
109+
if stripped.startswith('- '):
110+
if indent != 2:
111+
errors.append(
112+
f"{filepath}:{lineno}: [{section}] entry list item must be indented "
113+
f"2 spaces, found {indent}"
114+
)
115+
entry_indent = indent
116+
117+
# Check that this is the id field
118+
rest = stripped[2:] # strip "- "
119+
m = _FIELD_RE.match(' ' + rest) # re-prefix spaces for consistent match
120+
if m:
121+
field = m.group(2)
122+
raw_val = m.group(3)
123+
if field == 'id':
124+
_check_id_quoting(raw_val, lineno, section, filepath, errors)
125+
continue
126+
127+
# ── Subsequent fields of an entry ─────────────────────────────────────
128+
if entry_indent is not None:
129+
expected_indent = entry_indent + 2 # 2 + 2 = 4
130+
if indent != expected_indent:
131+
errors.append(
132+
f"{filepath}:{lineno}: [{section}] field must be indented "
133+
f"{expected_indent} spaces, found {indent}"
134+
)
135+
136+
m = _FIELD_RE.match(line)
137+
if not m:
138+
continue
139+
field = m.group(2)
140+
raw_val = m.group(3).split('#')[0].strip() # strip inline comment
141+
142+
# ── Hex field format ──────────────────────────────────────────────
143+
if field in HEX_FIELDS and raw_val:
144+
if not _HEX_RE.match(raw_val):
145+
errors.append(
146+
f"{filepath}:{lineno}: [{section}] field '{field}' "
147+
f"value {raw_val!r} must be hex notation (e.g. 0x115F)"
148+
)
149+
150+
# ── String field whitespace ───────────────────────────────────────
151+
display_val = _strip_quotes(raw_val)
152+
if field not in HEX_FIELDS and raw_val:
153+
if display_val != display_val.strip():
154+
errors.append(
155+
f"{filepath}:{lineno}: [{section}] field '{field}' "
156+
f"value has leading/trailing whitespace: {raw_val!r}"
157+
)
158+
159+
return errors
160+
161+
162+
def _strip_yaml_inline_comment(raw_val):
163+
"""
164+
Strip an inline YAML comment from a raw scalar value.
165+
166+
Handles:
167+
"quoted value" # comment → "quoted value"
168+
'quoted value' # comment → 'quoted value'
169+
bare value # comment → bare value
170+
"""
171+
if raw_val and raw_val[0] in ('"', "'"):
172+
q = raw_val[0]
173+
i = 1
174+
while i < len(raw_val):
175+
ch = raw_val[i]
176+
if q == '"' and ch == '\\':
177+
i += 2 # skip escaped character
178+
continue
179+
if q == "'" and ch == "'" and i + 1 < len(raw_val) and raw_val[i + 1] == "'":
180+
i += 2 # escaped single-quote inside single-quoted string
181+
continue
182+
if ch == q:
183+
return raw_val[:i + 1] # return up to and including closing quote
184+
i += 1
185+
return raw_val # unclosed quote — return as-is
186+
# Unquoted: strip from ' #' (space + hash = inline comment marker)
187+
idx = raw_val.find(' #')
188+
if idx != -1:
189+
return raw_val[:idx].rstrip()
190+
return raw_val
191+
192+
193+
def _check_id_quoting(raw_val, lineno, section, filepath, errors):
194+
"""Require quoting when the id value contains YAML-special characters."""
195+
raw_val = _strip_yaml_inline_comment(raw_val)
196+
is_quoted = (
197+
len(raw_val) >= 2
198+
and raw_val[0] in ('"', "'")
199+
and raw_val[-1] == raw_val[0]
200+
)
201+
inner = raw_val[1:-1] if is_quoted else raw_val
202+
if not is_quoted and _YAML_SPECIAL_RE.search(inner):
203+
errors.append(
204+
f"{filepath}:{lineno}: [{section}] id value {raw_val!r} contains "
205+
f"special characters and must be quoted"
206+
)
207+
208+
209+
def _strip_quotes(val):
210+
if len(val) >= 2 and val[0] in ('"', "'") and val[-1] == val[0]:
211+
return val[1:-1]
212+
return val
213+
214+
215+
# ── YAML structural checks ────────────────────────────────────────────────────
216+
217+
def check_structure(data, filepath):
218+
errors = []
219+
220+
for section, entries in data.items():
221+
if section not in MANUFACTURER_SECTIONS:
222+
continue
223+
224+
if not isinstance(entries, list):
225+
errors.append(f"{filepath}: [{section}] expected a list of entries, got {type(entries).__name__}")
226+
continue
227+
228+
seen_ids = {}
229+
for entry in entries:
230+
if not isinstance(entry, dict):
231+
errors.append(f"{filepath}: [{section}] entry is not a mapping: {entry!r}")
232+
continue
233+
234+
entry_id = entry.get('id', '<missing>')
235+
236+
# ── Duplicate id ──────────────────────────────────────────────────
237+
if entry_id in seen_ids:
238+
errors.append(f"{filepath}: [{section}] duplicate id {entry_id!r}")
239+
seen_ids[entry_id] = True
240+
241+
# ── Required fields ───────────────────────────────────────────────
242+
# manufacturer and model may legitimately be "" (device reports no value).
243+
ALLOW_EMPTY = {'manufacturer', 'model'}
244+
for field in REQUIRED_FIELDS[section]:
245+
if field not in entry:
246+
errors.append(
247+
f"{filepath}: [{section}] id={entry_id!r}: "
248+
f"missing required field '{field}'"
249+
)
250+
elif entry[field] is None:
251+
errors.append(
252+
f"{filepath}: [{section}] id={entry_id!r}: "
253+
f"field '{field}' has a null value"
254+
)
255+
elif field not in ALLOW_EMPTY and isinstance(entry[field], str) and entry[field].strip() == '':
256+
errors.append(
257+
f"{filepath}: [{section}] id={entry_id!r}: "
258+
f"field '{field}' is empty"
259+
)
260+
261+
# ── Zigbee: manufacturer or model ─────────────────────────────────
262+
if section == 'zigbeeManufacturer':
263+
mfr = entry.get('manufacturer')
264+
mdl = entry.get('model')
265+
mfr_blank = mfr is None or (isinstance(mfr, str) and mfr.strip() == '')
266+
mdl_blank = mdl is None or (isinstance(mdl, str) and mdl.strip() == '')
267+
if mfr_blank and mdl_blank:
268+
errors.append(
269+
f"{filepath}: [{section}] id={entry_id!r}: "
270+
"at least one of 'manufacturer' or 'model' must be provided"
271+
)
272+
273+
# ── Z-Wave: productId or productType ──────────────────────────────
274+
if section == 'zwaveManufacturer':
275+
if 'productId' not in entry and 'productType' not in entry:
276+
errors.append(
277+
f"{filepath}: [{section}] id={entry_id!r}: "
278+
"missing 'productId' or 'productType' (at least one required)"
279+
)
280+
281+
return errors
282+
283+
284+
# ── File validator ────────────────────────────────────────────────────────────
285+
286+
def validate_file(filepath: Path) -> list:
287+
try:
288+
raw = filepath.read_text(encoding='utf-8')
289+
except OSError as exc:
290+
return [f"{filepath}: cannot read file — {exc}"]
291+
292+
lines = raw.splitlines(keepends=True)
293+
294+
# Raw-text pass (indentation, whitespace, hex format, id quoting)
295+
errors = analyse_lines(lines, str(filepath))
296+
297+
# YAML structural pass (required fields, duplicates, null/empty values)
298+
try:
299+
data = yaml.safe_load(raw)
300+
except yaml.YAMLError as exc:
301+
return errors + [f"{filepath}: YAML parse error — {exc}"]
302+
303+
if not isinstance(data, dict):
304+
return errors + [f"{filepath}: unexpected top-level YAML structure"]
305+
306+
errors.extend(check_structure(data, str(filepath)))
307+
return errors
308+
309+
310+
# ── Git helper ────────────────────────────────────────────────────────────────
311+
312+
def get_changed_files() -> list:
313+
base = os.environ.get('GITHUB_BASE_REF', 'main')
314+
for ref in (f'origin/{base}', base, 'HEAD~1'):
315+
try:
316+
result = subprocess.run(
317+
['git', 'diff', '--name-only', '--diff-filter=AM', f'{ref}...HEAD'],
318+
capture_output=True, text=True, check=True
319+
)
320+
return [
321+
Path(f) for f in result.stdout.splitlines()
322+
if f.endswith('fingerprints.yml') and Path(f).exists()
323+
]
324+
except subprocess.CalledProcessError:
325+
continue
326+
return []
327+
328+
329+
# ── Main ──────────────────────────────────────────────────────────────────────
330+
331+
def main():
332+
if len(sys.argv) > 1:
333+
files = [Path(f) for f in sys.argv[1:]]
334+
else:
335+
files = get_changed_files()
336+
if not files:
337+
print("No changed fingerprints.yml files detected.")
338+
sys.exit(0)
339+
340+
all_errors = []
341+
for f in files:
342+
if not f.exists():
343+
print(f"Warning: {f} does not exist, skipping", file=sys.stderr)
344+
continue
345+
print(f"Checking {f} ...")
346+
errs = validate_file(f)
347+
all_errors.extend(errs)
348+
349+
if all_errors:
350+
print()
351+
for err in all_errors:
352+
print(err)
353+
print(f"\n✗ {len(all_errors)} error(s) found.")
354+
sys.exit(1)
355+
else:
356+
print(f"\n✓ {len(files)} file(s) passed validation.")
357+
sys.exit(0)
358+
359+
360+
if __name__ == '__main__':
361+
main()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Validate fingerprints
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize]
6+
paths:
7+
- 'drivers/**/fingerprints.yml'
8+
9+
jobs:
10+
validate-fingerprints:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0
17+
18+
- name: Install pyyaml
19+
run: pip install pyyaml --quiet
20+
21+
- name: Validate changed fingerprints.yml files
22+
env:
23+
GITHUB_BASE_REF: ${{ github.base_ref }}
24+
run: python .github/scripts/validate_fingerprints.py

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

‎Jenkinsfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def getDockerCredentialId() {
3939
def nodeLabel = params.NODE_LABEL ?: 'production'
4040
def region = get_region()
4141
if (nodeLabel == 'production' && region == 'cn') {
42-
return 'artifactory-credentials'
42+
return 'artifactory-cn'
4343
}
4444
else {
4545
return ''

0 commit comments

Comments
 (0)