blob: 21b15f94dcffadb0c10ea603e17a8c6163ce38c0 [file]
/*
*
* Copyright (c) 2021 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const basePath = '../../../../';
const testPath = 'src/app/tests/suites/';
const certificationPath = 'src/app/tests/suites/certification/';
const zapPath = basePath + 'third_party/zap/repo/';
const YAML = require(zapPath + 'node_modules/yaml');
const fs = require('fs');
const path = require('path');
// Import helpers from zap core
const templateUtil = require(zapPath + 'dist/src-electron/generator/template-util.js')
const { getClusters, getCommands, getAttributes, getEvents, isTestOnlyCluster }
= require('./simulated-clusters/SimulatedClusters.js');
const { asBlocks, ensureClusters } = require('./ClustersHelper.js');
const { Variables } = require('./variables/Variables.js');
const kIdentityName = 'identity';
const kClusterName = 'cluster';
const kEndpointName = 'endpoint';
const kGroupId = 'groupId';
const kCommandName = 'command';
const kWaitCommandName = 'wait';
const kIndexName = 'index';
const kValuesName = 'values';
const kConstraintsName = 'constraints';
const kArgumentsName = 'arguments';
const kResponseName = 'response';
const kDisabledName = 'disabled';
const kResponseErrorName = 'error';
const kResponseWrongErrorName = 'errorWrongValue';
const kPICSName = 'PICS';
const kSaveAsName = 'saveAs';
class NullObject {
toString()
{
return "YOU SHOULD HAVE CHECKED (isLiteralNull definedValue)"
}
};
function throwError(test, errorStr)
{
console.error('Error in: ' + test.filename + '.yaml for test with label: "' + test.label + '"\n');
console.error(errorStr);
throw new Error();
}
function setDefault(test, name, defaultValue)
{
if (!(name in test)) {
if (defaultValue == null) {
const errorStr = 'Test does not have any "' + name + '" defined.';
throwError(test, errorStr);
}
test[name] = defaultValue;
}
}
function setDefaultType(test)
{
if (kWaitCommandName in test) {
setDefaultTypeForWaitCommand(test);
} else {
setDefaultTypeForCommand(test);
}
}
function setDefaultTypeForWaitCommand(test)
{
const type = test[kWaitCommandName];
switch (type) {
case 'readEvent':
test.isEvent = true;
test.isReadEvent = true;
break;
case 'subscribeEvent':
test.isEvent = true;
test.isSubscribe = true;
test.isSubscribeEvent = true;
break;
case 'readAttribute':
test.isAttribute = true;
test.isReadAttribute = true;
break;
case 'writeAttribute':
test.isAttribute = true;
test.isWriteAttribute = true;
break;
case 'subscribeAttribute':
test.isAttribute = true;
test.isSubscribe = true;
test.isSubscribeAttribute = true;
break;
default:
test.isCommand = true;
test.command = test.wait
break;
}
test.isWait = true;
}
function setDefaultTypeForCommand(test)
{
const type = test[kCommandName];
switch (type) {
case 'readEvent':
test.commandName = 'Read';
test.isEvent = true;
test.isReadEvent = true;
break;
case 'subscribeEvent':
test.commandName = 'Subscribe';
test.isEvent = true;
test.isSubscribe = true;
test.isSubscribeEvent = true;
break;
case 'readAttribute':
test.commandName = 'Read';
test.isAttribute = true;
test.isReadAttribute = true;
break;
case 'writeAttribute':
test.commandName = 'Write';
test.isAttribute = true;
test.isWriteAttribute = true;
if ((kGroupId in test)) {
test.isGroupCommand = true;
test.groupId = parseInt(test[kGroupId], 10);
}
break;
case 'subscribeAttribute':
test.commandName = 'Subscribe';
test.isAttribute = true;
test.isSubscribe = true;
test.isSubscribeAttribute = true;
break;
case 'waitForReport':
test.commandName = 'Report';
test.isAttribute = true;
test.isWaitForReport = true;
break;
default:
test.commandName = test.command;
test.isCommand = true;
if ((kGroupId in test)) {
test.isGroupCommand = true;
test.groupId = parseInt(test[kGroupId], 10);
}
break;
}
// Sanity Check for GroupId usage
// Only two types of actions can be send to Group : Write attribute, and Commands
// Spec : Action 8.2.4
if ((kGroupId in test) && !test.isGroupCommand) {
printErrorAndExit(this, 'Wrong Yaml configuration. Action : ' + test.commandName + " can't be sent to group " + test[kGroupId]);
}
test.isWait = false;
}
function setDefaultPICS(test)
{
const defaultPICS = '';
setDefault(test, kPICSName, defaultPICS);
if (test[kPICSName] == '') {
return;
}
const items = test[kPICSName].split(/[&|() !]+/g).filter(item => item.length);
items.forEach(key => {
if (!PICS.has(key)) {
const errorStr = 'PICS database does not contains any defined value for: ' + key;
throwError(test, errorStr);
}
})
}
function setDefaultArguments(test)
{
const defaultArguments = {};
setDefault(test, kArgumentsName, defaultArguments);
const defaultArgumentsValues = [];
setDefault(test[kArgumentsName], kValuesName, defaultArgumentsValues);
if (!test.isWriteAttribute) {
return;
}
if (!('value' in test[kArgumentsName])) {
const errorStr = 'Test does not have a "value" defined.';
throwError(test, errorStr);
}
test[kArgumentsName].values.push({ name : test.attribute, value : test[kArgumentsName].value });
delete test[kArgumentsName].value;
}
function ensureValidError(response, errorName)
{
if (isNaN(response[errorName])) {
response[errorName] = "EMBER_ZCL_STATUS_" + response[errorName];
}
}
function setDefaultResponse(test)
{
const defaultResponse = {};
setDefault(test, kResponseName, defaultResponse);
const hasResponseError = (kResponseErrorName in test[kResponseName]) || (kResponseWrongErrorName in test[kResponseName]);
const defaultResponseError = 0;
setDefault(test[kResponseName], kResponseErrorName, defaultResponseError);
setDefault(test[kResponseName], kResponseWrongErrorName, defaultResponseError);
const defaultResponseValues = [];
setDefault(test[kResponseName], kValuesName, defaultResponseValues);
const defaultResponseConstraints = {};
setDefault(test[kResponseName], kConstraintsName, defaultResponseConstraints);
const defaultResponseSaveAs = '';
setDefault(test[kResponseName], kSaveAsName, defaultResponseSaveAs);
const hasResponseValue = 'value' in test[kResponseName];
const hasResponseConstraints = 'constraints' in test[kResponseName] && Object.keys(test[kResponseName].constraints).length;
const hasResponseValueOrConstraints = hasResponseValue || hasResponseConstraints;
if (test.isCommand && hasResponseValueOrConstraints) {
const errorStr = 'Test has a "value" or a "constraints" defined.\n' +
'\n' +
'Command should explicitly use the response argument name. Example: \n' +
'- label: "Send Test Specific Command"\n' +
' command: "testSpecific"\n' +
' response: \n' +
' values: \n' +
' - name: "returnValue"\n' +
' - value: 7\n';
throwError(test, errorStr);
}
ensureValidError(test[kResponseName], kResponseErrorName);
ensureValidError(test[kResponseName], kResponseWrongErrorName);
// Step that waits for a particular event does not requires constraints nor expected values.
if (test.isWait) {
return;
}
if (!test.isAttribute && !test.isEvent) {
return;
}
if (test.isWriteAttribute || test.isSubscribe) {
if (hasResponseValueOrConstraints) {
const errorStr = 'Test has a "value" or a "constraints" defined.';
throwError(test, errorStr);
}
return;
}
if (!hasResponseValueOrConstraints && !hasResponseError) {
console.log(test);
console.log(test[kResponseName]);
const errorStr = 'Test does not have a "value" or a "constraints" defined and is not expecting an error.';
throwError(test, errorStr);
}
if (hasResponseValueOrConstraints) {
const name = test.isAttribute ? test.attribute : test.event;
const response = test[kResponseName];
const responseValue = hasResponseValue ? { value : response.value } : null;
const constraintsValue = hasResponseConstraints ? { constraints : response.constraints } : null;
response.values.push({ name, saveAs : response.saveAs, ...responseValue, ...constraintsValue });
}
delete test[kResponseName].value;
}
function setDefaults(test, defaultConfig)
{
const defaultIdentityName = kIdentityName in defaultConfig ? defaultConfig[kIdentityName] : "alpha";
const defaultClusterName = defaultConfig[kClusterName] || null;
const defaultEndpointId = kEndpointName in defaultConfig ? defaultConfig[kEndpointName] : null;
const defaultDisabled = false;
setDefaultType(test);
setDefault(test, kIdentityName, defaultIdentityName);
setDefault(test, kClusterName, defaultClusterName);
setDefault(test, kEndpointName, defaultEndpointId);
setDefault(test, kDisabledName, defaultDisabled);
setDefaultPICS(test);
setDefaultArguments(test);
setDefaultResponse(test);
}
function parse(filename)
{
let filepath;
const isCertificationTest = filename.startsWith('Test_TC_');
if (isCertificationTest) {
filepath = path.resolve(__dirname, basePath + certificationPath + filename + '.yaml');
} else {
filepath = path.resolve(__dirname, basePath + testPath + filename + '.yaml');
}
const data = fs.readFileSync(filepath, { encoding : 'utf8', flag : 'r' });
const yaml = YAML.parse(data);
// "subscribeAttribute" command expects a report to be acked before
// it got a success response.
// In order to validate that the report has been received with the proper value
// a "subscribeAttribute" command can have a response configured into the test step
// definition. In this case, a new async "waitForReport" test step will be synthesized
// and added to the list of tests.
yaml.tests.forEach((test, index) => {
if (test.command == "subscribeAttribute" && test.response) {
// Create a new report test where the expected response is the response argument
// for the "subscribeAttributeTest"
const reportTest = {
label : "Report: " + test.label,
command : "waitForReport",
attribute : test.attribute,
response : test.response,
async : true,
allocateSubscribeDataCallback : true,
};
delete test.response;
// insert the new report test into the tests list
yaml.tests.splice(index, 0, reportTest);
// Associate the "subscribeAttribute" test with the synthesized report test
test.hasWaitForReport = true;
test.waitForReport = reportTest;
test.allocateSubscribeDataCallback = !test.hasWaitForReport;
}
});
const defaultConfig = yaml.config || [];
yaml.tests.forEach(test => {
test.filename = filename;
test.testName = yaml.name;
setDefaults(test, defaultConfig);
});
// Filter disabled tests
yaml.tests = yaml.tests.filter(test => !test.disabled);
yaml.tests.forEach((test, index) => {
setDefault(test, kIndexName, index);
});
yaml.filename = filename;
yaml.timeout = yaml.config.timeout;
yaml.totalTests = yaml.tests.length;
return yaml;
}
function printErrorAndExit(context, msg)
{
console.log(context.testName, ': ', context.label);
console.log(msg);
process.exit(1);
}
function assertCommandOrAttributeOrEvent(context)
{
const clusterName = context.cluster;
return getClusters(context).then(clusters => {
if (!clusters.find(cluster => cluster.name == clusterName)) {
const names = clusters.map(item => item.name);
printErrorAndExit(context, 'Missing cluster "' + clusterName + '" in: \n\t* ' + names.join('\n\t* '));
}
let filterName;
let items;
if (context.isCommand) {
filterName = context.command;
items = getCommands(context, clusterName);
} else if (context.isAttribute) {
filterName = context.attribute;
items = getAttributes(context, clusterName);
} else if (context.isEvent) {
filterName = context.event;
items = getEvents(context, clusterName);
} else {
printErrorAndExit(context, 'Unsupported command type: ', context);
}
return items.then(items => {
const filter = item => item.name.toLowerCase() == filterName.toLowerCase();
const item = items.find(filter);
const itemType = (context.isCommand ? 'Command' : context.isAttribute ? 'Attribute' : 'Event');
// If the command/attribute/event is not found, it could be because of a typo in the test
// description, or an attribute/event name not matching the spec, or a wrongly configured zap
// file.
if (!item) {
const names = items.map(item => item.name);
printErrorAndExit(context, 'Missing ' + itemType + ' "' + filterName + '" in: \n\t* ' + names.join('\n\t* '));
}
// If the command/attribute/event has been found but the response can not be found, it could be
// because of a wrongly configured cluster definition.
if (!item.response) {
printErrorAndExit(context, 'Missing ' + itemType + ' "' + filterName + '" response');
}
return item;
});
});
}
const PICS = (() => {
let filepath = path.resolve(__dirname, basePath + certificationPath + 'PICS.yaml');
const data = fs.readFileSync(filepath, { encoding : 'utf8', flag : 'r' });
const yaml = YAML.parse(data);
const getAll = () => yaml.PICS;
const get = (id) => has(id) ? yaml.PICS.filter(pics => pics.id == id)[0] : null;
const has = (id) => !!(yaml.PICS.filter(pics => pics.id == id)).length;
const PICS = {
getAll : getAll,
get : get,
has : has,
};
return PICS;
})();
//
// Templates
//
function chip_tests_pics(options)
{
return templateUtil.collectBlocks(PICS.getAll(), options, this);
}
async function chip_tests(list, options)
{
// Set a global on our items so assertCommandOrAttributeOrEvent can work.
let global = this.global;
const items = Array.isArray(list) ? list : list.split(',');
const names = items.map(name => name.trim());
let tests = names.map(item => parse(item));
const context = this;
tests = await Promise.all(tests.map(async function(test) {
test.tests = await Promise.all(test.tests.map(async function(item) {
item.global = global;
if (item.isCommand) {
let command = await assertCommandOrAttributeOrEvent(item);
item.commandObject = command;
} else if (item.isAttribute) {
let attr = await assertCommandOrAttributeOrEvent(item);
item.attributeObject = attr;
} else if (item.isEvent) {
let evt = await assertCommandOrAttributeOrEvent(item);
item.eventObject = evt;
}
return item;
}));
const variables = await Variables(context, test);
test.variables = {
config : variables.config,
tests : variables.tests,
};
return test;
}));
return templateUtil.collectBlocks(tests, options, this);
}
function chip_tests_items(options)
{
return templateUtil.collectBlocks(this.tests, options, this);
}
function getVariable(context, key, name)
{
while (!('variables' in context) && context.parent) {
context = context.parent;
}
if (typeof context === 'undefined' || !('variables' in context)) {
return null;
}
return context.variables[key].find(variable => variable.name == name);
}
function getVariableOrThrow(context, key, name)
{
const variable = getVariable(context, key, name);
if (variable == null) {
throw new Error(`Variable ${name} can not be found`);
}
return variable;
}
function chip_tests_variables(options)
{
return templateUtil.collectBlocks(this.variables.tests, options, this);
}
function chip_tests_variables_has(name, options)
{
const variable = getVariable(this, 'tests', name);
return !!variable;
}
function chip_tests_variables_get_type(name, options)
{
const variable = getVariableOrThrow(this, 'tests', name);
return variable.type;
}
function chip_tests_config(options)
{
return templateUtil.collectBlocks(this.variables.config, options, this);
}
function chip_tests_config_has(name, options)
{
const variable = getVariable(this, 'config', name);
return !!variable;
}
function chip_tests_config_get_default_value(name, options)
{
const variable = getVariableOrThrow(this, 'config', name);
return variable.defaultValue;
}
function chip_tests_config_get_type(name, options)
{
const variable = getVariableOrThrow(this, 'config', name);
return variable.type;
}
// test_cluster_command_value and test_cluster_value-equals are recursive partials using #each. At some point the |global|
// context is lost and it fails. Make sure to attach the global context as a property of the | value |
// that is evaluated.
function attachGlobal(global, value)
{
if (Array.isArray(value)) {
value = value.map(v => attachGlobal(global, v));
} else if (value instanceof Object) {
for (key in value) {
if (key == "global") {
continue;
}
value[key] = attachGlobal(global, value[key]);
}
} else if (value === null) {
value = new NullObject();
} else {
switch (typeof value) {
case 'number':
value = new Number(value);
break;
case 'string':
value = new String(value);
break;
case 'boolean':
value = new Boolean(value);
break;
default:
throw new Error('Unsupported value: ' + JSON.stringify(value));
}
}
value.global = global;
return value;
}
function chip_tests_item_parameters(options)
{
const commandValues = this.arguments.values;
const promise = assertCommandOrAttributeOrEvent(this).then(item => {
if ((this.isAttribute || this.isEvent) && !this.isWriteAttribute) {
if (this.isSubscribe) {
const minInterval = { name : 'minInterval', type : 'int16u', chipType : 'uint16_t', definedValue : this.minInterval };
const maxInterval = { name : 'maxInterval', type : 'int16u', chipType : 'uint16_t', definedValue : this.maxInterval };
return [ minInterval, maxInterval ];
}
return [];
}
const commandArgs = item.arguments;
const commands = commandArgs.map(commandArg => {
commandArg = JSON.parse(JSON.stringify(commandArg));
const expected = commandValues.find(value => value.name.toLowerCase() == commandArg.name.toLowerCase());
if (!expected) {
if (commandArg.isOptional) {
return undefined;
}
printErrorAndExit(this,
'Missing "' + commandArg.name + '" in arguments list: \n\t* '
+ commandValues.map(command => command.name).join('\n\t* '));
}
commandArg.definedValue = attachGlobal(this.global, expected.value);
return commandArg;
});
return commands.filter(item => item !== undefined);
});
return asBlocks.call(this, promise, options);
}
function chip_tests_item_response_parameters(options)
{
const responseValues = this.response.values.slice();
const promise = assertCommandOrAttributeOrEvent(this).then(item => {
if (this.isWriteAttribute) {
return [];
}
const responseArgs = item.response.arguments;
const responses = responseArgs.map(responseArg => {
responseArg = JSON.parse(JSON.stringify(responseArg));
const expectedIndex = responseValues.findIndex(value => value.name.toLowerCase() == responseArg.name.toLowerCase());
if (expectedIndex != -1) {
const expected = responseValues.splice(expectedIndex, 1)[0];
if ('value' in expected) {
responseArg.hasExpectedValue = true;
responseArg.expectedValue = attachGlobal(this.global, expected.value);
}
if ('constraints' in expected) {
responseArg.hasExpectedConstraints = true;
responseArg.expectedConstraints = expected.constraints;
}
if ('saveAs' in expected) {
responseArg.saveAs = expected.saveAs;
}
}
return responseArg;
});
const unusedResponseValues = responseValues.filter(response => 'value' in response);
unusedResponseValues.forEach(unusedResponseValue => {
printErrorAndExit(this,
'Missing "' + unusedResponseValue.name + '" in response arguments list:\n\t* '
+ responseArgs.map(response => response.name).join('\n\t* '));
});
return responses;
});
return asBlocks.call(this, promise, options);
}
function isLiteralNull(value, options)
{
// Literal null might look different depending on whether it went through
// attachGlobal or not.
return (value === null) || (value instanceof NullObject);
}
function octetStringEscapedForCLiteral(value)
{
// Escape control characters, things outside the ASCII range, and single
// quotes (because that's our string terminator).
return value.replace(/\p{Control}|\P{ASCII}|"/gu, ch => {
let code = ch.charCodeAt(0);
code = code.toString(16);
if (code.length == 1) {
code = "0" + code;
}
return "\\x" + code;
});
}
// Structs may not always provide values for optional members.
function if_include_struct_item_value(structValue, name, options)
{
let hasValue = (name in structValue);
if (hasValue) {
return options.fn(this);
}
if (!this.isOptional) {
throw new Error(`Value not provided for ${name} where one is expected`);
}
return options.inverse(this);
}
//
// Module exports
//
exports.chip_tests = chip_tests;
exports.chip_tests_items = chip_tests_items;
exports.chip_tests_item_parameters = chip_tests_item_parameters;
exports.chip_tests_item_response_parameters = chip_tests_item_response_parameters;
exports.chip_tests_pics = chip_tests_pics;
exports.chip_tests_config = chip_tests_config;
exports.chip_tests_config_has = chip_tests_config_has;
exports.chip_tests_config_get_default_value = chip_tests_config_get_default_value;
exports.chip_tests_config_get_type = chip_tests_config_get_type;
exports.chip_tests_variables = chip_tests_variables;
exports.chip_tests_variables_has = chip_tests_variables_has;
exports.chip_tests_variables_get_type = chip_tests_variables_get_type;
exports.isTestOnlyCluster = isTestOnlyCluster;
exports.isLiteralNull = isLiteralNull;
exports.octetStringEscapedForCLiteral = octetStringEscapedForCLiteral;
exports.if_include_struct_item_value = if_include_struct_item_value;