Skip to content

Commit 5aa86ff

Browse files
authored
Add headless .zap validation, UI panel, and CLI improvements (#1704)
- Add validate-all session orchestration and GET /validate REST support - Wire main-process startValidate: merged report, optional file or stdout - CLI: register logToStdout; support multiple .zap paths (positionals, repeat -i, comma-separated for validate) - Conformance: optional skip writing SESSION_NOTICE during bulk validate so that it does not go into session notifications on every run - Validation: Matter root endpoint 0, session-wide duplicate check fix, external storage attribute skip; validate-all null endpoint pre-check - UI: Validate in ZCL toolbar, side panel (ValidationPanelPage), layout/store - Docs: add docs/validating-zap-files.md; trim README to link developer docs - Tests: extend arg and validation tests; fix duplicate-endpoint test data - Fixing float type attribute validation, float bounds from ZCL size, type-range fallback, and IEEE hex literals - Add exit code error for the CLI option when validation fails - For cli, when a .zap file is not mentioned give user the options that need to be speecified - Github: ZAP #1678
1 parent cf96884 commit 5aa86ff

19 files changed

Lines changed: 2442 additions & 93 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This software is licensed under [Apache 2.0 license](LICENSE.txt).
3737

3838
## Detailed Developer Documentation
3939

40+
- [Validating `.zap` files](docs/validating-zap-files.md)
4041
- [ZAP Template Helpers](docs/helpers.md)
4142
- [ZAP External Template Helpers](docs/external-helpers.md)
4243
- [ZAP file Extensions](docs/zap-file-extensions.md)

docs/api.md

Lines changed: 403 additions & 43 deletions
Large diffs are not rendered by default.

docs/validating-zap-files.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Validating `.zap` files
2+
3+
You can validate ZCL and data-model configuration in one or more `.zap` files without opening the UI. The same checks are available from the ZAP UI toolbar (**Validate**), which opens results in the side panel.
4+
5+
## CLI
6+
7+
### Synopsis
8+
9+
```bash
10+
zap validate -i path/to/config.zap [-z zcl.json] [-g gen-templates.json] [-o report.json]
11+
```
12+
13+
You can pass **more than one** `.zap` file in three ways:
14+
15+
- **Space-separated** after `validate` (no `-i` per file): `validate first.zap second.zap`
16+
- **Comma-separated** in one argument: `validate -i first.zap,second.zap` (also works for a single `-i` value)
17+
- **Repeat** `-i` / `--in` / `--zap` for each file: `validate -i first.zap -i second.zap`
18+
19+
One merged JSON (or YAML) report is written; it has a `zapFiles` array and one `results[]` entry per file.
20+
21+
```bash
22+
node src-script/zap-start.js validate first.zap second.zap -o report.json
23+
# or
24+
node src-script/zap-start.js validate -i first.zap,second.zap -o report.json
25+
# or
26+
node src-script/zap-start.js validate -i first.zap -i second.zap -o report.json
27+
```
28+
29+
### Options
30+
31+
| Flag | Meaning |
32+
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33+
| `-i`, `--in`, `--zap` | Input `.zap` file. For `validate`, you may pass several paths: space-separated after the command, comma-separated in one `-i` value (e.g. `-i a.zap,b.zap`), or repeat this flag. |
34+
| `-z`, `--zcl`, `--zclProperties` | ZCL metafile(s), for example `./zcl-builtin/matter/zcl.json` or `./zcl-builtin/silabs/zcl.json`. Use these to validate against a specific ZCL definition instead of only the built-in default or whatever `zcl.properties` paths are embedded in the `.zap` file. May be repeated. |
35+
| `-g`, `--gen`, `--generationTemplate` | Generation template metafile(s), for example `./test/gen-template/matter/gen-test.json`. Overrides the built-in default and template references inside the `.zap` file for validation context. May be repeated. |
36+
| `-o`, `--output`, `--validateOutput` | Write the structured validation report to a file. Use a `.yaml` or `.yml` extension for YAML output. For the `validate` command, `-o` / `--output` is treated as the report path (not the generation output directory). |
37+
38+
When **no** output path is given, the JSON report is printed to **stdout**. Progress and status messages go to **stderr** so you can redirect stdout to a file safely:
39+
40+
```bash
41+
node src-script/zap-start.js validate -i path/to/config.zap > report.json
42+
```
43+
44+
Exit code is **non-zero** when the report contains validation errors.
45+
46+
### Running from this repository
47+
48+
Use the start script so flags are passed straight through (avoiding `npm run` swallowing leading `-` flags):
49+
50+
```bash
51+
node src-script/zap-start.js validate \
52+
-i path/to/config.zap \
53+
-z ./zcl-builtin/matter/zcl.json \
54+
-g ./test/gen-template/matter/gen-test.json \
55+
-o report.json
56+
```
57+
58+
If you use `npm run zap-validate`, put **`--`** before script arguments so npm forwards `-i`, `-z`, `-g`, and `-o`:
59+
60+
```bash
61+
npm run zap-validate -- -i path/to/config.zap -z path/to/zcl.json -g path/to/templates.json -o ./report.json
62+
```
63+
64+
### Example: Matter / connectedhomeip
65+
66+
When validating an app from [connectedhomeip](https://github.com/project-chip/connectedhomeip), point `-z` and `-g` at that tree’s ZCL and template metafiles so validation matches the SDK your app targets:
67+
68+
```bash
69+
node src-script/zap-start.js validate \
70+
-i examples/your-app/your-app.zap \
71+
-z src/app/zap-templates/zcl/zcl.json \
72+
-g src/app/zap-templates/app-templates.json \
73+
-o ./report.json
74+
```
75+
76+
Paths above are relative to the connectedhomeip repository root; adjust for your checkout.
77+
78+
## Report format
79+
80+
The report is JSON (or YAML if the output path ends in `.yaml` / `.yml`). It aggregates per-file results, summaries (error counts, endpoint/cluster/attribute counts), and detailed findings for endpoints, attributes, and conformance.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"zapmatter": "node src-script/zap-start.js --logToStdout --zcl ./zcl-builtin/matter/zcl.json --gen ./test/gen-template/matter/gen-test.json",
7070
"zapmeta": "node src-script/zap-start.js --logToStdout --zcl ./test/resource/meta/zcl.json --gen ./test/resource/meta/gen-test.json --in ./test/resource/test-meta.zap",
7171
"zaphelp": "node src-script/zap-start.js --help",
72+
"zap-validate": "node src-script/zap-start.js validate",
7273
"zap-dotdot": "node src-script/zap-start.js --logToStdout --zcl ./zcl-builtin/dotdot/library.xml -g ./test/gen-template/dotdot/dotdot-templates.json",
7374
"zap-devserver": "node src-script/zap-start.js server --stateDirectory ~/.zap/zigbee-server/ --allowCors --logToStdout --gen ./test/gen-template/zigbee/gen-templates.json --reuseZapInstance",
7475
"zigbeezap-devserver": "node src-script/zap-start.js server --stateDirectory ~/.zap/zigbee-server/ --allowCors --logToStdout --gen ./test/gen-template/zigbee/gen-templates.json --reuseZapInstance",

src-electron/main-process/startup.js

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const queryPackage = require('../db/query-package.js')
4242
const util = require('../util/util.js')
4343
const importJs = require('../importexport/import.js')
4444
const exportJs = require('../importexport/export.js')
45+
const validateAll = require('../validation/validate-all.js')
4546
const watchdog = require('./watchdog')
4647
const sdkUtil = require('../util/sdk-util')
4748

@@ -629,6 +630,141 @@ async function startAnalyze(argv, options) {
629630
if (options.quitFunction != null) options.quitFunction()
630631
}
631632

633+
/**
634+
* Validate ZCL / data-model elements for one or more .zap files (headless).
635+
*
636+
* @param {*} argv
637+
* @param {*} options
638+
* @returns exit code 0 or 1 (1 if any attribute/endpoint validation errors)
639+
*/
640+
async function startValidate(argv, options) {
641+
let paths = argv.zapFiles
642+
options.logger(env.formatEmojiMessage('🤖', `Starting validation: ${paths}`))
643+
let dbFile = env.sqliteFile('validate')
644+
if (options.cleanDb && fs.existsSync(dbFile)) {
645+
options.logger(env.formatEmojiMessage('🔧', 'remove old database file'))
646+
fs.unlinkSync(dbFile)
647+
}
648+
let db = await dbApi.initDatabaseAndLoadSchema(
649+
dbFile,
650+
env.schemaFile(),
651+
env.zapVersion()
652+
)
653+
options.logger(
654+
env.formatEmojiMessage('🐝', 'database and schema initialized')
655+
)
656+
await zclLoader.loadZclMetafiles(db, argv.zclProperties, {
657+
failOnLoadingError: !argv.noLoadingFailure
658+
})
659+
if (argv.generationTemplate != null) {
660+
let ctx = await generatorEngine.loadTemplates(db, argv.generationTemplate, {
661+
failOnLoadingError: !argv.noLoadingFailure
662+
})
663+
if (ctx.error) {
664+
throw ctx.error
665+
}
666+
}
667+
668+
let exitCode = 0
669+
const mergedReport = {
670+
zapFiles: paths,
671+
results: [],
672+
summary: {
673+
errors: 0,
674+
warnings: 0,
675+
attributes: 0,
676+
endpoints: 0,
677+
clusters: 0
678+
}
679+
}
680+
681+
await util.executePromisesSequentially(paths, async (singlePath) => {
682+
let importResult = await importJs.importDataFromFile(db, singlePath, {
683+
defaultZclMetafile: argv.zclProperties,
684+
postImportScript: argv.postImportScript,
685+
packageMatch: argv.packageMatch
686+
})
687+
let sessionId = importResult.sessionId
688+
await util.ensurePackagesAndPopulateSessionOptions(db, sessionId, {
689+
zcl: argv.zclProperties,
690+
template: argv.generationTemplate
691+
})
692+
if (argv.postImportScript) {
693+
await importJs.executePostImportScript(
694+
db,
695+
sessionId,
696+
argv.postImportScript
697+
)
698+
}
699+
const report = await validateAll.validateAll(db, sessionId, {
700+
persistConformanceNotifications: false
701+
})
702+
const tagged = {
703+
...report,
704+
zapFile: singlePath,
705+
endpoints: report.endpoints.map((row) => ({
706+
...row,
707+
zapFile: singlePath
708+
})),
709+
attributes: report.attributes.map((row) => ({
710+
...row,
711+
zapFile: singlePath
712+
})),
713+
conformance: report.conformance.map((row) => ({
714+
...row,
715+
zapFile: singlePath
716+
}))
717+
}
718+
mergedReport.results.push(tagged)
719+
mergedReport.summary.errors += report.summary.errors
720+
mergedReport.summary.warnings += report.summary.warnings
721+
mergedReport.summary.attributes += report.summary.attributes
722+
mergedReport.summary.endpoints += report.summary.endpoints
723+
mergedReport.summary.clusters += report.summary.clusters
724+
if (report.summary.errors > 0) {
725+
exitCode = 1
726+
}
727+
728+
options.logger(
729+
env.formatEmojiMessage(
730+
'🔍',
731+
`${singlePath}: errors=${report.summary.errors} warnings=${report.summary.warnings}`
732+
)
733+
)
734+
})
735+
736+
options.logger(
737+
env.formatEmojiMessage(
738+
'🔧',
739+
`Validation totals: errors=${mergedReport.summary.errors} warnings=${mergedReport.summary.warnings}`
740+
)
741+
)
742+
743+
if (argv.validateOutput != null) {
744+
let outPath = argv.validateOutput
745+
let ext = path.extname(outPath).toLowerCase()
746+
let body =
747+
ext === '.yaml' || ext === '.yml'
748+
? YAML.stringify(mergedReport)
749+
: JSON.stringify(mergedReport, null, 2)
750+
let parent = path.dirname(outPath)
751+
if (!fs.existsSync(parent)) {
752+
fs.mkdirSync(parent, { recursive: true })
753+
}
754+
await fsp.writeFile(outPath, body)
755+
options.logger(
756+
env.formatEmojiMessage('👉', `Wrote validation report: ${outPath}`)
757+
)
758+
} else {
759+
process.stdout.write(JSON.stringify(mergedReport, null, 2) + '\n')
760+
}
761+
762+
await dbApi.closeDatabase(db)
763+
options.logger(env.formatEmojiMessage('🔧', 'Validation done!'))
764+
if (options.quitFunction != null) options.quitFunction()
765+
return exitCode
766+
}
767+
632768
/**
633769
* Starts zap in a server mode.
634770
*
@@ -1197,13 +1333,62 @@ async function startUpMainInstance(argv, callbacks) {
11971333
}
11981334
return startSelfCheck(argv, options)
11991335
} else if (argv._.includes('analyze')) {
1200-
if (argv.zapFiles.length < 1)
1201-
throw 'You need to specify at least one zap file.'
1336+
if (argv.zapFiles.length < 1) {
1337+
console.error(
1338+
'Error: You need to specify at least one zap file.\n\n' +
1339+
'Usage:\n' +
1340+
' npm run zap-analyze -- [options] <file.zap> [<file.zap> ...]\n\n' +
1341+
'Options:\n' +
1342+
' -i, --in, --zap Input .zap file(s). May be repeated or comma-separated.\n' +
1343+
' -z, --zcl ZCL metafile (e.g. zcl-builtin/silabs/zcl.json). May be repeated.\n' +
1344+
' -g, --gen Generation template metafile (e.g. gen-templates.json). May be repeated.\n'
1345+
)
1346+
cleanExit(argv.cleanupDelay, 1)
1347+
return
1348+
}
12021349
let options = {
12031350
quitFunction: quitFunction,
12041351
logger: console.log
12051352
}
12061353
return startAnalyze(argv, options)
1354+
} else if (argv._.includes('validate')) {
1355+
if (argv.zapFiles.length < 1) {
1356+
console.error(
1357+
'Error: You need to specify at least one zap file.\n\n' +
1358+
'Usage:\n' +
1359+
' npm run zap-validate -- [options] <file.zap> [<file.zap> ...]\n\n' +
1360+
'Options:\n' +
1361+
' -i, --in, --zap Input .zap file(s). May be repeated or comma-separated.\n' +
1362+
' -z, --zcl ZCL metafile (e.g. zcl-builtin/silabs/zcl.json). May be repeated.\n' +
1363+
' -g, --gen Generation template metafile (e.g. gen-templates.json). May be repeated.\n' +
1364+
' -o, --validateOutput Write validation report to a file (.json or .yaml). Defaults to stdout.\n'
1365+
)
1366+
cleanExit(argv.cleanupDelay, 1)
1367+
return
1368+
}
1369+
// When no report file is given, the JSON report goes to stdout, so route
1370+
// status messages to stderr to keep stdout clean and pipe-friendly.
1371+
// quitFunction is intentionally omitted here so that startValidate returns
1372+
// the numeric exit code rather than calling process.exit(0) prematurely.
1373+
// cleanExit below propagates the code to CI callers.
1374+
let options = {
1375+
quitFunction: null,
1376+
logger: argv.validateOutput == null ? console.error : console.log
1377+
}
1378+
return startValidate(argv, options)
1379+
.then((code) => {
1380+
if (code !== 0) {
1381+
env.printToStderr(
1382+
`Validation completed with errors (exit code ${code}). Check the report for details.`
1383+
)
1384+
}
1385+
cleanExit(argv.cleanupDelay, code)
1386+
})
1387+
.catch((err) => {
1388+
console.log(err)
1389+
env.printToStderr(`Zap validation error: ${err}`)
1390+
cleanExit(argv.cleanupDelay, 1)
1391+
})
12071392
} else if (argv._.includes('server')) {
12081393
return startServer(argv, quitFunction)
12091394
} else if (argv._.includes('convert')) {
@@ -1289,6 +1474,7 @@ exports.startSelfCheck = startSelfCheck
12891474
exports.clearDatabaseFile = clearDatabaseFile
12901475
exports.startConvert = startConvert
12911476
exports.startAnalyze = startAnalyze
1477+
exports.startValidate = startValidate
12921478
exports.startUpMainInstance = startUpMainInstance
12931479
exports.startUpSecondaryInstance = startUpSecondaryInstance
12941480
exports.shutdown = shutdown

src-electron/rest/user-data.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const querySession = require('../db/query-session.js')
3737
const queryPackage = require('../db/query-package.js')
3838
const asyncValidation = require('../validation/async-validation.js')
3939
const validation = require('../validation/validation.js')
40+
const validateAll = require('../validation/validate-all.js')
4041
const restApi = require('../../src-shared/rest-api.js')
4142
const zclLoader = require('../zcl/zcl-loader.js')
4243
const dbEnum = require('../../src-shared/db-enum.js')
@@ -1136,6 +1137,28 @@ function httpGetConformDataExists(db) {
11361137
}
11371138
}
11381139

1140+
/**
1141+
* HTTP GET: validate entire session (all endpoints, attribute defaults, conformance).
1142+
*
1143+
* @param {*} db
1144+
* @returns callback for the express uri registration
1145+
*/
1146+
function httpGetValidateAll(db) {
1147+
return async (request, response) => {
1148+
try {
1149+
const report = await validateAll.validateAll(db, request.zapSessionId, {
1150+
persistConformanceNotifications: false
1151+
})
1152+
response.status(StatusCodes.OK).json(report)
1153+
} catch (err) {
1154+
env.logError(err)
1155+
response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
1156+
error: err.message || String(err)
1157+
})
1158+
}
1159+
}
1160+
}
1161+
11391162
/**
11401163
* Set warning for the required element, and delete its existing warning if any.
11411164
*
@@ -1332,6 +1355,10 @@ exports.get = [
13321355
uri: restApi.uri.conformDataExists,
13331356
callback: httpGetConformDataExists
13341357
},
1358+
{
1359+
uri: restApi.uri.validate,
1360+
callback: httpGetValidateAll
1361+
},
13351362
{
13361363
uri: restApi.uri.featureMapAttribute,
13371364
callback: httpGetFeatureMapAttribute

0 commit comments

Comments
 (0)