diff --git a/saltgui/index.html b/saltgui/index.html
index ecb89b125..b34f47328 100644
--- a/saltgui/index.html
+++ b/saltgui/index.html
@@ -72,9 +72,9 @@
📖︎
❓︎
Manual Run
-
-
-
+
+
+
diff --git a/saltgui/static/scripts/Api.js b/saltgui/static/scripts/Api.js
index 813052425..2d3a98c54 100644
--- a/saltgui/static/scripts/Api.js
+++ b/saltgui/static/scripts/Api.js
@@ -1,6 +1,5 @@
/* global config EventSource window */
-import {Character} from "./Character.js";
import {CommandBox} from "./CommandBox.js";
import {Router} from "./Router.js";
import {TargetType} from "./TargetType.js";
@@ -359,7 +358,15 @@ export class API {
};
if (pMethod === "POST") {
- options.body = JSON.stringify(pParams);
+ // BigInt is not JSON-serializable; convert to marker string so JSON.stringify
+ // can process it, then regex removes quotes to produce unquoted JSON numbers.
+ // Use \x00 (null byte) as delimiter since it cannot be typed, preventing collisions.
+ options.body = JSON.stringify(pParams, (_key, value) => { // eslint-disable-line no-unused-vars
+ if (typeof value === 'bigint') {
+ return `\x00BIGINT${value.toString()}\x00`;
+ }
+ return value;
+ }).replace(/"\\u0000BIGINT(-?\d+)\\u0000"/g, '$1');
}
/* eslint-disable compat/compat */
@@ -481,7 +488,7 @@ export class API {
// the character counts include "-----BEGIN PUBLIC KEY-----" at the beginning and
// include "-----END PUBLIC KEY-----" at the end.
if (data.pub && data.pub.length > 75) {
- data.pub = data.pub.substring(0, 35) + Character.HORIZONTAL_ELLIPSIS + data.pub.substring(data.pub.length - 33);
+ data.pub = Utils.truncateString(data.pub, 35, 33);
}
// salt/beacon///
diff --git a/saltgui/static/scripts/Character.js b/saltgui/static/scripts/Character.js
index d5d9005fe..d2b34e4f6 100644
--- a/saltgui/static/scripts/Character.js
+++ b/saltgui/static/scripts/Character.js
@@ -31,6 +31,7 @@ export class Character {
Character.BALLOT_BOX_WITH_CHECK = "\u2611";
Character.GEAR = "\u2699";
Character.WARNING_SIGN = "\u26A0" + Character._VARIATION_SELECTOR_16;
+ Character.NO_ENTRY_SIGN = "\u26D4" + Character._VARIATION_SELECTOR_16;
Character.HEAVY_CHECK_MARK = "\u2714";
Character.HEAVY_MULTIPLICATION_X = "\u2716" + Character._VARIATION_SELECTOR_15;
Character.BLACK_QUESTION_MARK_ORNAMENT = "\u2753" + Character._VARIATION_SELECTOR_15;
diff --git a/saltgui/static/scripts/CommandBox.js b/saltgui/static/scripts/CommandBox.js
index 0911572a4..915829760 100644
--- a/saltgui/static/scripts/CommandBox.js
+++ b/saltgui/static/scripts/CommandBox.js
@@ -26,6 +26,18 @@ export class CommandBox {
RunType.createMenu();
TargetType.createMenu();
+ // Re-validate target when target type changes
+ const targetField = document.getElementById("target");
+ TargetType.onTargetTypeChange = () => {
+ CommandBox._validateAndDisplayTargetInput(targetField.value);
+ };
+
+ // Re-validate command when run type (Normal/Async) changes
+ const commandField = document.getElementById("command");
+ RunType.onRunTypeChange = () => {
+ CommandBox._validateAndDisplayCommandInput(commandField.value);
+ };
+
const manualRun = document.getElementById("popup-run-command");
Utils.addTableHelp(manualRun, "Click for help", "bottom-center");
const helpButton = manualRun.querySelector("#help");
@@ -244,14 +256,20 @@ export class CommandBox {
if (targetField.value === "##connected") {
// just replace it with the actual value
targetField.value = Utils.getStorageItem("session", "connected", "");
+ // run validation after programmatic field change
+ CommandBox._validateAndDisplayTargetInput(targetField.value);
+ return;
}
const targetType = targetField.value;
TargetType.autoSelectTargetType(targetType);
+ CommandBox._validateAndDisplayTargetInput(targetField.value);
});
document.getElementById("command").
addEventListener("input", () => {
this.cmdmenu.verifyAll();
+ const commandField = document.getElementById("command");
+ CommandBox._validateAndDisplayCommandInput(commandField.value);
});
}
@@ -280,6 +298,10 @@ export class CommandBox {
const commandField = document.getElementById("command");
commandField.value = pCommand;
}
+
+ // run validations when template is applied
+ CommandBox._validateAndDisplayTargetInput(pTarget || "");
+ CommandBox._validateAndDisplayCommandInput(pCommand || "");
}
static _applyTemplateByTemplate (pTemplate) {
@@ -354,18 +376,35 @@ export class CommandBox {
const targetField = document.getElementById("target");
const targetValue = targetField.value;
const commandField = document.getElementById("command");
- const commandValue = commandField.value;
+ let commandValue = commandField.value;
const targetType = TargetType.menuTargetType._value;
+ const runType = RunType.getRunType();
- const patWhitespaceAll = /\s/g;
- const commandValueNoTabs = commandValue.replace(patWhitespaceAll, " ");
- if (commandValueNoTabs !== commandValue) {
- commandField.value = commandValueNoTabs;
- CommandBox._showError("The command contains unsupported whitespace characters.\nThese have now been replaced by regular space characters.\nUse 'Run command' again to run the updated command.");
+ // Collect all validation errors and warnings from all fields
+ const commandValidation = CommandBox._validateCommandField(commandValue);
+ const targetValidation = CommandBox._validateTargetField(targetValue, targetType);
+ const formValidation = CommandBox._validateForm(commandValue, targetValue, runType);
+
+ const allErrors = [
+ ...commandValidation.errors,
+ ...targetValidation.errors,
+ ...formValidation.errors
+ ];
+ const allWarnings = [
+ ...commandValidation.warnings,
+ ...targetValidation.warnings
+ ];
+
+ if (allErrors.length > 0) {
+ CommandBox._displayFormValidationIndicator(formValidation);
+ CommandBox._displayValidationOutput({ errors: allErrors, warnings: allWarnings }, output);
return;
}
+ const patWhitespaceAll = /\s/g;
+ commandValue = commandValue.replace(patWhitespaceAll, " ");
+
const func = this.getRunParams(targetType, targetValue, commandValue);
if (func === null) {
return;
@@ -621,6 +660,8 @@ export class CommandBox {
const targetField = document.getElementById("target");
targetField.value = lst;
TargetType.autoSelectTargetType(lst);
+ // run validation after programmatic field change
+ CommandBox._validateAndDisplayTargetInput(lst);
}
}
@@ -661,98 +702,427 @@ export class CommandBox {
CommandBox.onRunReturn("ERROR:\n\n" + pMessage, "");
}
- getRunParams (pTargetType, pTarget, pToRun, pisRunTypeNormalOnly = false, pCanUseFullReturn = true) {
+ static _validateAndDisplayCommandInput (pCommand) {
+ const validationResult = CommandBox._validateCommandField(pCommand);
+ CommandBox._displayCmdValidationIndicator(validationResult);
+ const targetField = document.getElementById("target");
+ if (targetField) {
+ const runType = RunType.getRunType();
+ const formValidation = CommandBox._validateForm(pCommand, targetField.value, runType);
+ CommandBox._displayFormValidationIndicator(formValidation);
+ }
+ CommandBox._updateContinuousValidationOutput(validationResult, null);
+ }
- // The leading # was used to indicate a nodegroup
- if (pTargetType === "nodegroup" && pTarget.startsWith("#")) {
- // remove the leading "#" as that is not part of the syntax for nodegroups
- pTarget = pTarget.substring(1);
+ static _validateAndDisplayTargetInput (pTarget) {
+ const targetType = TargetType.menuTargetType._value;
+ const validationResult = CommandBox._validateTargetField(pTarget, targetType);
+ CommandBox._displayTargetValidationIndicator(validationResult);
+ const commandField = document.getElementById("command");
+ if (commandField) {
+ const runType = RunType.getRunType();
+ const formValidation = CommandBox._validateForm(commandField.value, pTarget, runType);
+ CommandBox._displayFormValidationIndicator(formValidation);
}
+ CommandBox._updateContinuousValidationOutput(null, validationResult);
+ }
- if (pToRun === "") {
- CommandBox._showError("'Command' field cannot be empty");
- return null;
+ static _updateContinuousValidationOutput (pCommandValidation, pTargetValidation) {
+ const outputField = document.getElementById("popup-output");
+ const outputText = outputField.innerText;
+
+ // only update if output panel is showing validation (starts with validation icons or waiting)
+ const isShowingValidation =
+ outputText.startsWith(Character.NO_ENTRY_SIGN) ||
+ outputText.startsWith(Character.WARNING_SIGN) ||
+ outputText.startsWith(Character.HEAVY_CHECK_MARK) ||
+ outputText.startsWith("Waiting for command");
+ if (!isShowingValidation) {
+ return;
}
- // collection for unnamed parameters
- const argsArray = [];
+ const commandField = document.getElementById("command");
+ const targetField = document.getElementById("target");
- // collection for named parameters
+ // use provided validation or recalculate if not provided
+ const commandValidation = pCommandValidation || CommandBox._validateCommandField(commandField.value);
+ const targetType = CommandBox._getTargetType();
+ const targetValidation = pTargetValidation || CommandBox._validateTargetField(targetField.value, targetType);
+ const runType = RunType.getRunType();
+ const formValidation = CommandBox._validateForm(commandField.value, targetField.value, runType);
+ CommandBox._displayFormValidationIndicator(formValidation);
+
+ const allErrors = [
+ ...commandValidation.errors,
+ ...targetValidation.errors,
+ ...formValidation.errors
+ ];
+ const allWarnings = [
+ ...commandValidation.warnings,
+ ...targetValidation.warnings
+ ];
+
+ CommandBox._displayValidationOutput({ errors: allErrors, warnings: allWarnings }, outputField);
+ }
+
+ static _getTargetType () {
+ return TargetType._getTargetType();
+ }
+
+ static _validateCommandField (pCommand) {
+ const errors = [];
+ const warnings = [];
+
+ if (pCommand.trim() === "") {
+ return { errors, warnings };
+ }
+
+ const validators = [
+ CommandBox._validateCommandFieldParseError,
+ CommandBox._validateCommandFieldFunctionNameExists,
+ CommandBox._validateCommandFieldFunctionNameIsString,
+ CommandBox._validateCommandFieldRunnerPrefix,
+ CommandBox._validateCommandFieldWheelParameters
+ ];
+
+ const tokenArray = [];
+ const argsArray = [];
const argsObject = {};
- const ret = ParseCommandLine.parseCommandLine(pToRun, argsArray, argsObject);
- if (ret !== null) {
- // that is an error message being returned
- CommandBox._showError(ret);
- return null;
+ const parseResult = ParseCommandLine.parseCommandLine(pCommand, tokenArray, argsArray, argsObject);
+
+ for (const validator of validators) {
+ const result = validator(parseResult, tokenArray, argsArray);
+ if (result) {
+ errors.push(result);
+ break;
+ }
}
- if (argsArray.length === 0) {
- CommandBox._showError("First (unnamed) parameter is the function name, it is mandatory");
- return null;
+ CommandBox._collectWarningsFromParsing(pCommand, warnings);
+ CommandBox._collectUnsupportedWhitespaceWarning(pCommand, warnings);
+
+ return { errors, warnings };
+ }
+
+ static _validateCommandFieldParseError (pParseResult, pTokenArrayUnused, pArgsArrayUnused) { // eslint-disable-line no-unused-vars
+ if (pParseResult !== null) {
+ return pParseResult;
}
+ return null;
+ }
- const functionToRun = argsArray.shift();
+ static _validateCommandFieldFunctionNameExists (pParseResult, pTokenArrayUnused, pArgsArray) { // eslint-disable-line no-unused-vars
+ if (pParseResult !== null) {
+ return null;
+ }
+ if (pArgsArray.length === 0) {
+ return "First (unnamed) parameter must be the function name, it is mandatory";
+ }
+ return null;
+ }
- const validationError = CommandBox._validateFunctionParams(functionToRun, pTarget, pTargetType);
- if (validationError) {
- CommandBox._showError(validationError);
+ static _validateCommandFieldFunctionNameIsString (pParseResult, pTokenArrayUnused, pArgsArray) { // eslint-disable-line no-unused-vars
+ if (pParseResult !== null || pArgsArray.length === 0) {
return null;
}
+ const firstArg = pArgsArray[0];
+ if (typeof firstArg !== "string") {
+ const truncatedToken = Utils.truncateString(String(firstArg), 50);
+ return "First (unnamed) parameter must be the function name, it must be a string\nin: (" + typeof firstArg + ") " + truncatedToken;
+ }
+ return null;
+ }
- if (functionToRun.startsWith("wheel.") && argsArray.length > 0) {
- CommandBox._showError("Wheel commands can only take named parameters");
+ static _validateCommandFieldRunnerPrefix (pParseResult, pTokenArrayUnused, pArgsArray) { // eslint-disable-line no-unused-vars
+ if (pParseResult !== null || pArgsArray.length === 0) {
return null;
}
+ const firstArg = pArgsArray[0];
+ if (typeof firstArg !== "string") {
+ return null;
+ }
+ if (firstArg === "runner" || firstArg.startsWith("runner.")) {
+ return "Runner commands must be prefixed with 'runners.'\nin: " + firstArg;
+ }
+ return null;
+ }
- const fullReturn = pCanUseFullReturn && Utils.getStorageItemBoolean("session", "full_return");
+ static _validateCommandFieldWheelParameters (pParseResult, pTokenArray, pArgsArray) {
+ if (pParseResult !== null || pArgsArray.length === 0) {
+ return null;
+ }
+ const firstArg = pArgsArray[0];
+ if (typeof firstArg !== "string") {
+ return null;
+ }
+ if (firstArg.startsWith("wheel.") && pArgsArray.length > 1) {
+ return "Wheel commands can only take named parameters\nin: " + pTokenArray[1];
+ }
+ return null;
+ }
- let params = CommandBox._buildCommandParams(functionToRun, pTarget, pTargetType, argsArray, argsObject, fullReturn);
+ static _collectWarningsFromParsing (pCommand, pWarnings) {
+ const tokens = CommandBox._extractTokensFromCommand(pCommand);
- const runType = RunType.getRunType();
- if (!pisRunTypeNormalOnly && runType === "async") {
- if (params.client !== "local") {
- CommandBox._showError("Async is not supported for '" + functionToRun + "'");
- return null;
+ for (const token of tokens) {
+ const result = ParseCommandLine._convertStringToValue(token);
+ if (result.warning) {
+ pWarnings.push(result.warning + "\nin: " + token);
+ }
+ if (result.warnings) {
+ for (const warning of result.warnings) {
+ pWarnings.push(warning + "\nin: " + token);
+ }
}
- params.client = "local_async";
- // return will look like:
- // { "jid": "20180718173942195461", "minions": [ ... ] }
}
+ }
- return this.api.apiRequest("POST", "/", params);
+ static _collectUnsupportedWhitespaceWarning (pCommand, pWarnings) {
+ const patWhitespaceAll = /\s/g;
+ const commandValueNoSpaces = pCommand.replace(patWhitespaceAll, " ");
+ if (commandValueNoSpaces !== pCommand) {
+ pWarnings.push("The command contains unsupported whitespace characters\nThese will be replaced by regular space characters when the command is run");
+ }
}
- static _validateFunctionParams (pFunctionToRun, pTarget, pTargetType) {
- if (typeof pFunctionToRun !== "string") {
- return "First (unnamed) parameter is the function name, it must be a string, not a " + typeof pFunctionToRun;
+ static _extractTokensFromCommand (pCommand) {
+ const tokens = [];
+ let currentToken = "";
+
+ for (const char of pCommand) {
+ if (char === " ") {
+ if (currentToken) {
+ tokens.push(currentToken);
+ // for named parameters, also add the value part for validation
+ const equalSignIndex = currentToken.indexOf("=");
+ if (equalSignIndex > 0) {
+ tokens.push(currentToken.substring(equalSignIndex + 1));
+ }
+ currentToken = "";
+ }
+ } else {
+ currentToken += char;
+ }
+ }
+
+ if (currentToken) {
+ tokens.push(currentToken);
+ // for named parameters, also add the value part for validation
+ const equalSignIndex = currentToken.indexOf("=");
+ if (equalSignIndex > 0) {
+ tokens.push(currentToken.substring(equalSignIndex + 1));
+ }
}
- // prevent a common spelling error
- if (pFunctionToRun === "runner" || pFunctionToRun.startsWith("runner.")) {
- return "'Runner' commands must be prefixed with 'runners.'";
+ return tokens;
+ }
+
+ static _validateForm (pCommand, pTarget, pRunType) {
+ const errors = [];
+ const validators = [
+ CommandBox._validateFormTargetNotEmpty,
+ CommandBox._validateFormAsyncCompatibility
+ ];
+
+ const tokenArray = [];
+ const argsArray = [];
+ const argsObject = {};
+
+ ParseCommandLine.parseCommandLine(pCommand, tokenArray, argsArray, argsObject);
+
+ if (argsArray.length > 0 && typeof argsArray[0] === "string") {
+ const functionName = argsArray[0];
+ for (const validator of validators) {
+ const result = validator(functionName, pTarget, pRunType);
+ if (result) {
+ errors.push(result);
+ }
+ }
}
+ return { errors, warnings: [] };
+ }
+
+ static _validateFormTargetNotEmpty (pFunctionName, pTarget, pRunTypeUnused) { // eslint-disable-line no-unused-vars
+ const isRunners = pFunctionName === "runners" || pFunctionName.startsWith("runners.");
// RUNNERS commands do not have a target (MASTER is the target)
// WHEEL commands also do not have a target
// but we use the TARGET value to form the usually required MATCH parameter
// therefore for WHEEL commands it is still required
- if (pTarget === "" && pFunctionToRun !== "runners" && !pFunctionToRun.startsWith("runners.")) {
- return "'Target' field cannot be empty";
+ if (pTarget === "" && !isRunners) {
+ return "Target cannot be empty for this command\nin: " + pFunctionName;
}
+ return null;
+ }
- // SALT API returns a 500-InternalServerError when it hits an unknown group
- // Let's improve on that
- if (pTargetType === "nodegroup") {
- const nodeGroups = Utils.getStorageItemObject("session", "nodegroups");
- if (!(pTarget in nodeGroups)) {
- return "Unknown nodegroup '" + pTarget + "'";
+ static _validateFormAsyncCompatibility (pFunctionName, pTargetUnused, pRunType) { // eslint-disable-line no-unused-vars
+ const isRunners = pFunctionName === "runners" || pFunctionName.startsWith("runners.");
+ const isWheel = pFunctionName === "wheel" || pFunctionName.startsWith("wheel.");
+ // Async validation - runners and wheel do not support async
+ if (pRunType === "async" && (isRunners || isWheel)) {
+ return "Async is not supported for '" + pFunctionName + "'";
+ }
+ return null;
+ }
+
+ static _validateTargetField (pTarget, pTargetType) {
+ const errors = [];
+ const validators = [
+ CommandBox._validateTargetFieldNodegroupExists
+ ];
+
+ for (const validator of validators) {
+ const result = validator(pTarget, pTargetType);
+ if (result) {
+ errors.push(result);
}
}
+ return { errors, warnings: [] };
+ }
+
+ static _validateTargetFieldNodegroupExists (pTarget, pTargetType) {
+ if (pTarget.trim() !== "" && pTargetType === "nodegroup") {
+ let nodeGroupName = pTarget;
+ if (nodeGroupName.startsWith("#")) {
+ nodeGroupName = nodeGroupName.substring(1);
+ }
+ const nodeGroups = Utils.getStorageItemObject("session", "nodegroups");
+ if (!(nodeGroupName in nodeGroups)) {
+ return "Unknown nodegroup\nin: " + pTarget;
+ }
+ }
return null;
}
+ static _displayCmdValidationIndicator (pValidationResult) {
+ const indicatorElement = document.getElementById("cmd-validation-indicator");
+
+ if (pValidationResult.errors.length === 0 && pValidationResult.warnings.length === 0) {
+ indicatorElement.textContent = "";
+ Utils.addToolTip(indicatorElement, "");
+ return;
+ }
+
+ const hasErrors = pValidationResult.errors.length > 0;
+ const messages = [
+ ...pValidationResult.errors,
+ ...pValidationResult.warnings
+ ];
+
+ const tooltipText = messages.join("\n");
+ indicatorElement.textContent = hasErrors ? Character.NO_ENTRY_SIGN : Character.WARNING_SIGN;
+ indicatorElement.style.color = hasErrors ? "red" : "orange";
+ Utils.addToolTip(indicatorElement, tooltipText, "bottom-center");
+ }
+
+ static _displayTargetValidationIndicator (pValidationResult) {
+ const indicatorElement = document.getElementById("target-validation-indicator");
+
+ if (pValidationResult.errors.length === 0 && pValidationResult.warnings.length === 0) {
+ indicatorElement.textContent = "";
+ Utils.addToolTip(indicatorElement, "");
+ return;
+ }
+
+ const hasErrors = pValidationResult.errors.length > 0;
+ const messages = [
+ ...pValidationResult.errors,
+ ...pValidationResult.warnings
+ ];
+
+ indicatorElement.textContent = hasErrors ? Character.NO_ENTRY_SIGN : Character.WARNING_SIGN;
+ indicatorElement.style.color = hasErrors ? "red" : "orange";
+
+ const tooltipText = messages.join("\n");
+ Utils.addToolTip(indicatorElement, tooltipText, "bottom-center");
+ }
+
+ static _displayFormValidationIndicator (pValidationResult) {
+ const indicatorElement = document.getElementById("form-validation-indicator");
+ if (!indicatorElement) {
+ // element may not exist in test context
+ return;
+ }
+
+ if (pValidationResult.errors.length === 0 && pValidationResult.warnings.length === 0) {
+ indicatorElement.textContent = "";
+ Utils.addToolTip(indicatorElement, "");
+ return;
+ }
+
+ const hasErrors = pValidationResult.errors.length > 0;
+ const messages = [
+ ...pValidationResult.errors,
+ ...pValidationResult.warnings
+ ];
+
+ indicatorElement.textContent = hasErrors ? Character.NO_ENTRY_SIGN : Character.WARNING_SIGN;
+ indicatorElement.style.color = hasErrors ? "red" : "orange";
+
+ const tooltipText = messages.join("\n");
+ Utils.addToolTip(indicatorElement, tooltipText, "bottom-center");
+ }
+
+ static _displayValidationOutput (pValidationResult, pOutputElement) {
+ const errors = pValidationResult.errors || [];
+ const warnings = pValidationResult.warnings || [];
+ let output = "";
+
+ for (const err of errors) {
+ output += Character.NO_ENTRY_SIGN + " " + ParseCommandLine.formatErrorMessage(err) + "\n";
+ }
+
+ for (const wrn of warnings) {
+ output += Character.WARNING_SIGN + " " + ParseCommandLine.formatErrorMessage(wrn) + "\n";
+ }
+
+ output += "\nWaiting for command" + Character.HORIZONTAL_ELLIPSIS;
+
+ pOutputElement.innerText = output.trimStart();
+ }
+
+ getRunParams (pTargetType, pTarget, pToRun, pisRunTypeNormalOnly = false, pCanUseFullReturn = true) {
+
+ // The leading # was used to indicate a nodegroup
+ if (pTargetType === "nodegroup" && pTarget.startsWith("#")) {
+ // remove the leading "#" as that is not part of the syntax for nodegroups
+ pTarget = pTarget.substring(1);
+ }
+
+ // collection for unnamed parameters
+ const tokenArray = [];
+ const argsArray = [];
+
+ // collection for named parameters
+ const argsObject = {};
+
+ const ret = ParseCommandLine.parseCommandLine(pToRun, tokenArray, argsArray, argsObject);
+ if (ret !== null) {
+ // that is an error message being returned
+ CommandBox._showError(ret);
+ return null;
+ }
+
+ const functionToRun = argsArray.shift();
+
+ const fullReturn = pCanUseFullReturn && Utils.getStorageItemBoolean("session", "full_return");
+
+ let params = CommandBox._buildCommandParams(functionToRun, pTarget, pTargetType, argsArray, argsObject, fullReturn);
+
+ const runType = RunType.getRunType();
+ if (!pisRunTypeNormalOnly && runType === "async") {
+ // Async mode - primary validation is in _validateForm()
+ // which ensures async is only used with local (non-runners/wheel) commands
+ params.client += "_async";
+ // return will look like:
+ // { "jid": "20180718173942195461", "minions": [ ... ] }
+ }
+
+ return this.api.apiRequest("POST", "/", params);
+ }
+
static _buildCommandParams (pFunctionToRun, pTarget, pTargetType, pArgsArray, pArgsObject, pFullReturn) {
let params = {};
diff --git a/saltgui/static/scripts/Documentation.js b/saltgui/static/scripts/Documentation.js
index d9a6ea32f..5b5b87662 100644
--- a/saltgui/static/scripts/Documentation.js
+++ b/saltgui/static/scripts/Documentation.js
@@ -46,9 +46,10 @@ export class Documentation {
const commandField = document.getElementById("command");
const commandLine = commandField.value;
+ const tokenArray = [];
const argsArray = [];
const argsObject = {};
- ParseCommandLine.parseCommandLine(commandLine, argsArray, argsObject);
+ ParseCommandLine.parseCommandLine(commandLine, tokenArray, argsArray, argsObject);
if (!argsArray.length) {
// No command entered yet (or only name-value pairs)
@@ -194,9 +195,10 @@ export class Documentation {
static _getKeywordFragments (pCommandLine) {
+ const tokenArray = [];
const argsArray = [];
const argsObject = {};
- ParseCommandLine.parseCommandLine(pCommandLine, argsArray, argsObject);
+ ParseCommandLine.parseCommandLine(pCommandLine, tokenArray, argsArray, argsObject);
// empty commandline
if (!argsArray.length) {
@@ -253,9 +255,10 @@ export class Documentation {
html += Documentation._buildModulesSection(cmd);
html += Documentation._buildSpecialModulesSection(cmd);
+ const tokenArray = [];
const argsArray = [];
const argsObject = {};
- ParseCommandLine.parseCommandLine(commandLine, argsArray, argsObject);
+ ParseCommandLine.parseCommandLine(commandLine, tokenArray, argsArray, argsObject);
html += Documentation._buildBeaconDetailsSection(cmd, argsArray);
const output = document.querySelector(".run-command pre");
diff --git a/saltgui/static/scripts/ParseCommandLine.js b/saltgui/static/scripts/ParseCommandLine.js
index 2cce801ce..5e190d08f 100644
--- a/saltgui/static/scripts/ParseCommandLine.js
+++ b/saltgui/static/scripts/ParseCommandLine.js
@@ -16,6 +16,7 @@
// the dictionary will be filled with one element named "x"
import {Character} from "./Character.js";
+import {Utils} from "./Utils.js";
export class ParseCommandLine {
@@ -24,13 +25,14 @@ export class ParseCommandLine {
}
static getCommandFromCommandLine (pCommandLine) {
+ const tokenArray = [];
const argsArray = [];
const argsObject = {};
- ParseCommandLine.parseCommandLine(pCommandLine, argsArray, argsObject);
+ ParseCommandLine.parseCommandLine(pCommandLine, tokenArray, argsArray, argsObject);
return argsArray[0];
}
- static parseCommandLine (pToRun, pArgsArray, pArgsObject) {
+ static parseCommandLine (pToRun, pTokenArray, pArgsArray, pArgsObject) {
// just in case the user typed some extra whitespace
// at the start of the line
pToRun = pToRun.trim();
@@ -57,10 +59,10 @@ export class ParseCommandLine {
return parseResult.error;
}
- const { value, remaining } = parseResult;
+ const { value, remaining, token } = parseResult;
pToRun = remaining;
- const addResult = ParseCommandLine._addArgumentToCollections(name, value, pArgsArray, pArgsObject);
+ const addResult = ParseCommandLine._addArgumentToCollections(name, token, value, pTokenArray, pArgsArray, pArgsObject);
if (addResult.error) {
return addResult.error;
}
@@ -87,7 +89,7 @@ export class ParseCommandLine {
name = toRun.substring(0, firstEqualSign);
toRun = toRun.substring(firstEqualSign + 1);
if (toRun === "" || toRun[0] === " ") {
- return { error: "Must have value for named parameter '" + name + "'" };
+ return { error: "Must have value for named parameter\nin: " + name + "=" };
}
}
@@ -98,7 +100,7 @@ export class ParseCommandLine {
const patPlaceHolder = /^<[a-z]+>/;
if (patPlaceHolder.test(pToRun)) {
const placeHolder = pToRun.replace(/>.*/, ">");
- return { error: "Must fill in all placeholders, e.g. " + placeHolder };
+ return { error: "Must fill in all placeholders\ne.g. " + placeHolder };
}
return { error: null };
}
@@ -129,7 +131,8 @@ export class ParseCommandLine {
let endCharPos = pToRun.indexOf(endChar, charPos);
if (endCharPos < 0) {
const extraInfo = ParseCommandLine._getJsonErrorInfo(objType);
- return { error: "No valid " + objType + " found" + extraInfo };
+ const problematicToken = Utils.truncateString(pToRun, 50);
+ return { error: "No valid " + objType + " found" + extraInfo + "\nin: " + problematicToken };
}
// parse what we have found so far
@@ -143,7 +146,7 @@ export class ParseCommandLine {
}
if (parseAttempt.isFatal) {
- // valid JSON but followed by text - return error immediately
+ // valid JSON but followed by other text - return error immediately
return parseAttempt;
}
@@ -179,12 +182,15 @@ export class ParseCommandLine {
// the first part of the string is valid JSON
let endCharPos = pEndCharPos + pEndChar.length;
if (endCharPos < pToRun.length && pToRun[endCharPos] !== " ") {
- return { error: "Valid " + pObjType + ", but followed by text:" + pToRun.substring(endCharPos) + Character.HORIZONTAL_ELLIPSIS, isFatal: true };
+ const validPart = Utils.truncateString(pToRun.substring(0, endCharPos), 50);
+ const extraText = Utils.truncateString(pToRun.substring(endCharPos), 50);
+ return { error: "Valid " + pObjType + ", but followed by extra text\n" + pObjType + ": " + validPart + "\nextra: " + extraText, isFatal: true };
}
// valid JSON and not followed by strange characters
const newToRun = pToRun.substring(endCharPos);
- return { error: null, remaining: newToRun, value };
+ const token = pToRun.substring(0, endCharPos);
+ return { error: null, remaining: newToRun, token, value };
}
static _parseStringValue (pToRun) {
@@ -202,7 +208,14 @@ export class ParseCommandLine {
if (conversionResult.error) {
return { error: conversionResult.error };
}
- return { error: null, remaining: toRun, value: conversionResult.value };
+ const result = { error: null, remaining: toRun, token: str, value: conversionResult.value };
+ if (conversionResult.warning) {
+ result.warning = conversionResult.warning;
+ }
+ if (conversionResult.warnings) {
+ result.warnings = conversionResult.warnings;
+ }
+ return result;
}
static _convertStringToValue (pStr) {
@@ -211,6 +224,15 @@ export class ParseCommandLine {
const patNull = /^(?:None|null|Null|NULL)$/;
const patBooleanFalse = /^(?:false|False|FALSE)$/;
const patBooleanTrue = /^(?:true|True|TRUE)$/;
+ const patHexadecimal = /^[-+]?0[xX][0-9a-fA-F]+$/;
+ const patBinary = /^[-+]?0[bB][01]+$/;
+ const patOctal = /^[-+]?0[0-7]+$/;
+ // looks like octal but may not be valid (contains 8 or 9):
+ const patOctalLike = /^[-+]?0\d+$/;
+ // same as in yaml/resolver.py from PyYaml:
+ const patSexagesimal = /^[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+$/;
+ // looks like sexagesimal but may not be valid:
+ const patSexagesimalLike = /^[-+]?\d+(?::\d+)+$/;
const patInteger = /^(?:(?:0)|(?:[-+]?[1-9]\d*))$/;
const patFloat = /^[-+]?(?:\d+[.]?\d*|[.]\d+)(?:[eE][-+]?\d+)?$/; // NOSONAR S8786
@@ -223,30 +245,200 @@ export class ParseCommandLine {
} else if (ParseCommandLine.getPatJid().test(pStr)) {
// jobIds look like numbers but must be strings
return { value: pStr };
+ } else if (patHexadecimal.test(pStr)) {
+ const result = ParseCommandLine._parseHexadecimal(pStr);
+ return result;
+ } else if (patBinary.test(pStr)) {
+ const result = ParseCommandLine._parseBinary(pStr);
+ return result;
+ } else if (patOctal.test(pStr)) {
+ const result = ParseCommandLine._parseOctal(pStr);
+ return result;
+ } else if (patSexagesimal.test(pStr)) {
+ const result = ParseCommandLine._parseSexagesimal(pStr);
+ return result;
} else if (patInteger.test(pStr)) {
- return { value: Number.parseInt(pStr, 10) };
+ const result = ParseCommandLine._parseDecimalInteger(pStr);
+ return result;
+ } else if (patOctalLike.test(pStr)) {
+ // Looks like octal but doesn't match valid pattern (contains 8 or 9)
+ return { value: pStr, warning: "Octal format not recognized by salt,\nassuming string value" };
} else if (patFloat.test(pStr)) {
const value = Number.parseFloat(pStr);
if (!Number.isFinite(value)) {
return { error: "Numeric argument has overflowed or is infinity" };
}
return { value };
+ } else if (patSexagesimalLike.test(pStr)) {
+ // Looks like sexagesimal but doesn't match valid pattern
+ return { value: pStr, warning: "Sexagesimal format not recognized by salt,\nassuming string value" };
} else {
return { value: pStr };
}
}
- static _addArgumentToCollections (pName, pValue, pArgsArray, pArgsObject) {
+ static _check64BitRange (pValue) {
+ let bigValue;
+
+ try {
+ bigValue = BigInt(pValue);
+ } catch (err) {
+ return { isValid: false, warning: err.toString() };
+ }
+
+ const min64 = -0x8000000000000000n;
+ const max64 = 0x7FFFFFFFFFFFFFFFn;
+ if (bigValue < min64 || bigValue > max64) {
+ return { isValid: false, warning: "Argument exceeds integer range, it will be sent as string" };
+ }
+
+ const minSafeInt = BigInt(Number.MIN_SAFE_INTEGER);
+ const maxSafeInt = BigInt(Number.MAX_SAFE_INTEGER);
+ if (bigValue < minSafeInt || bigValue > maxSafeInt) {
+ return { isValid: true, warning: "Argument exceeds safe-integer range, precision will not be lost in the request,\nbut similar values will loose precision in the response" }
+ }
+
+ return { isValid: true };
+ }
+
+ static _parseHexadecimal (pStr) {
+ const hasNegativeSign = pStr[0] === "-";
+ let cleanStr = pStr;
+ if (pStr[0] === "-" || pStr[0] === "+") {
+ cleanStr = pStr.substring(1);
+ }
+ let value;
+ try {
+ value = BigInt(cleanStr) * (hasNegativeSign ? -1n : 1n);
+ } catch (err) { // eslint-disable-line no-unused-vars
+ return { value: pStr, warning: "Invalid hexadecimal format" };
+ }
+ const rangeCheck = ParseCommandLine._check64BitRange(value);
+ if (!rangeCheck.isValid) {
+ return { value: pStr, warning: rangeCheck.warning };
+ }
+ const warnings = [];
+ if (rangeCheck.warning) {
+ warnings.push(rangeCheck.warning);
+ }
+ if (cleanStr[1] === "X") {
+ warnings.push("Hexadecimal numbers with 'X' are supported here, but usually not in 'salt'");
+ }
+ return { value, warnings };
+ }
+
+ static _parseBinary (pStr) {
+ const hasNegativeSign = pStr[0] === "-";
+ let cleanStr = pStr;
+ if (pStr[0] === "-" || pStr[0] === "+") {
+ cleanStr = pStr.substring(1);
+ }
+ let value;
+ try {
+ value = BigInt(cleanStr) * (hasNegativeSign ? -1n : 1n);
+ } catch (err) { // eslint-disable-line no-unused-vars
+ return { value: pStr, warning: "Invalid binary format" };
+ }
+ const rangeCheck = ParseCommandLine._check64BitRange(value);
+ if (!rangeCheck.isValid) {
+ return { value: pStr, warning: rangeCheck.warning };
+ }
+ const warnings = [];
+ if (rangeCheck.warning) {
+ warnings.push(rangeCheck.warning);
+ }
+ if (cleanStr[1] === "B") {
+ warnings.push("Binary numbers with 'B' are supported here, but usually not in 'salt'");
+ }
+ return { value, warnings };
+ }
+
+ static _parseOctal (pStr) {
+ const hasNegativeSign = pStr[0] === "-";
+ let cleanStr = pStr;
+ if (pStr[0] === "-" || pStr[0] === "+") {
+ cleanStr = pStr.substring(1);
+ }
+ let value;
+ try {
+ value = BigInt("0o" + cleanStr.substring(1)) * (hasNegativeSign ? -1n : 1n);
+ } catch (err) { // eslint-disable-line no-unused-vars
+ return { value: pStr, warning: "Invalid octal format" };
+ }
+ const rangeCheck = ParseCommandLine._check64BitRange(value);
+ if (!rangeCheck.isValid) {
+ return { value: pStr, warning: rangeCheck.warning };
+ }
+ const warnings = ["Octal numbers are supported here, but usually not in 'salt'"];
+ if (rangeCheck.warning) {
+ warnings.push(rangeCheck.warning);
+ }
+ return { value, warnings };
+ }
+
+ static _parseSexagesimal (pStr) {
+ const sign = pStr[0] === "-" ? -1n : 1n;
+ const cleanStr = pStr.replace(/^[-+]/, "");
+ const parts = cleanStr.split(":");
+
+ // Calculate the value: treat as mixed-radix base-60
+ let value = 0n;
+ for (let i = 0; i < parts.length; i++) {
+ let baseMultiplier = 1n;
+ const exponent = parts.length - 1 - i;
+ for (let step = 0; step < exponent; step++) {
+ baseMultiplier *= 60n;
+ }
+ value += BigInt(parts[i]) * baseMultiplier;
+ }
+
+ value = value * sign;
+ const rangeCheck = ParseCommandLine._check64BitRange(value);
+ if (!rangeCheck.isValid) {
+ return { value: pStr, warning: rangeCheck.warning };
+ }
+ if (rangeCheck.warning) {
+ return { value: value, warning: rangeCheck.warning };
+ }
+ if (cleanStr[0] === "0") {
+ return { value, warning: "Sexagesimal numbers with leading zero are supported here, but usually not in 'salt'" };
+ }
+ return { value };
+ }
+
+ static _parseDecimalInteger (pStr) {
+ let value;
+ try {
+ value = BigInt(pStr);
+ } catch (err) { // eslint-disable-line no-unused-vars
+ return { value: pStr, warning: "Invalid integer format" };
+ }
+ const rangeCheck = ParseCommandLine._check64BitRange(value);
+ if (!rangeCheck.isValid) {
+ return { value: pStr, warning: rangeCheck.warning };
+ }
+ if (rangeCheck.warning) {
+ return { value: value, warning: rangeCheck.warning };
+ }
+ return { value };
+ }
+
+ static _addArgumentToCollections (pName, pToken, pValue, pTokenArray, pArgsArray, pArgsObject) {
if (pName === null) {
// anonymous parameter
+ pTokenArray.push(pToken);
pArgsArray.push(pValue);
} else if (pName in pArgsObject) {
// named parameter which already exists
- return { error: "Duplicate named variable '" + pName + "'" };
+ return { error: "Duplicate named variable\nin: " + pName };
} else {
// named parameter
pArgsObject[pName] = pValue;
}
return { error: null };
}
+
+ static formatErrorMessage (pMessage) {
+ return pMessage.replaceAll("\n", "\n" + Character.NO_BREAK_SPACE.repeat(5));
+ }
}
diff --git a/saltgui/static/scripts/RunType.js b/saltgui/static/scripts/RunType.js
index d9fe5f23d..a9290ef70 100644
--- a/saltgui/static/scripts/RunType.js
+++ b/saltgui/static/scripts/RunType.js
@@ -18,6 +18,7 @@ export class RunType {
static _updateRunTypeText () {
const runType = RunType.getRunType();
+ RunType._notifyRunTypeChange();
switch (runType) {
case "normal":
@@ -65,4 +66,10 @@ export class RunType {
}
return runType;
}
+
+ static _notifyRunTypeChange () {
+ if (RunType.onRunTypeChange) {
+ RunType.onRunTypeChange();
+ }
+ }
}
diff --git a/saltgui/static/scripts/TargetType.js b/saltgui/static/scripts/TargetType.js
index 7e2353772..9d425d84c 100644
--- a/saltgui/static/scripts/TargetType.js
+++ b/saltgui/static/scripts/TargetType.js
@@ -72,6 +72,10 @@ export class TargetType {
TargetType.menuTargetType._value = targetType;
TargetType._setMenuMarker();
+
+ if (TargetType.onTargetTypeChange) {
+ TargetType.onTargetTypeChange();
+ }
}
static _setMenuMarker () {
diff --git a/saltgui/static/scripts/Utils.js b/saltgui/static/scripts/Utils.js
index 37dae46a4..1c3b9b6c8 100644
--- a/saltgui/static/scripts/Utils.js
+++ b/saltgui/static/scripts/Utils.js
@@ -785,4 +785,16 @@ export class Utils {
static mySortFunction (aa, bb) {
return aa.localeCompare(bb, "en", {"numeric": true});
}
+
+ static truncateString (pStr, pMaxPrefixLength = 50, pMaxSuffixLength = 0) {
+ const maxTotalLength = pMaxPrefixLength + pMaxSuffixLength;
+ if (pStr.length > maxTotalLength) {
+ if (pMaxSuffixLength > 0) {
+ return pStr.substring(0, pMaxPrefixLength) + Character.HORIZONTAL_ELLIPSIS + pStr.substring(pStr.length - pMaxSuffixLength);
+ } else {
+ return pStr.substring(0, pMaxPrefixLength) + Character.HORIZONTAL_ELLIPSIS;
+ }
+ }
+ return pStr;
+ }
}
diff --git a/saltgui/static/scripts/output/Output.js b/saltgui/static/scripts/output/Output.js
index 8b23a1eba..43fe74a9a 100644
--- a/saltgui/static/scripts/output/Output.js
+++ b/saltgui/static/scripts/output/Output.js
@@ -43,6 +43,26 @@ export class Output {
return stateOutput.includes(pRequestedStateOutput);
}
+ static _containsNonSafeIntegers (pValue) {
+ const minSafeInt = Number.MIN_SAFE_INTEGER;
+ const maxSafeInt = Number.MAX_SAFE_INTEGER;
+
+ if (typeof pValue === "number" && Number.isInteger(pValue)) {
+ if (pValue < minSafeInt || pValue > maxSafeInt) {
+ return true;
+ }
+ }
+
+ if (typeof pValue === "object" && pValue !== null) {
+ if (Array.isArray(pValue)) {
+ return pValue.some(item => Output._containsNonSafeIntegers(item));
+ }
+ return Object.values(pValue).some(item => Output._containsNonSafeIntegers(item));
+ }
+
+ return false;
+ }
+
// Re-organize the output to let it appear as if the output comes
// from a single node called "RUNNER" or "MASTER".
// This way all responses are organized by minion
@@ -1032,6 +1052,7 @@ export class Output {
}
if (!Object.keys(pContext.pMinionData).length) {
+ // this is the same error-text as the 'salt' commandline uses
pContext.pOutputContainer.innerText = "No minions matched the target. No command was sent, no jid was assigned.\nERROR: No return received";
}
}
@@ -1070,7 +1091,7 @@ export class Output {
const pJobId = pOptions.jobId;
const pInitialStatus = pOptions.initialStatus;
const pHighlightMinionId = pOptions.highlightMinionId;
- const pExtraInfo = pOptions.extraInfo;
+ let pExtraInfo = pOptions.extraInfo;
// remove old content
pOutputContainer.innerText = "";
@@ -1087,6 +1108,14 @@ export class Output {
const cntMinions = pMinionData.length;
const downloadObject = {};
+ if (Output._containsNonSafeIntegers(pResponse)) {
+ if (!pExtraInfo) {
+ pExtraInfo = [];
+ }
+ pExtraInfo = [...pExtraInfo];
+ pExtraInfo.unshift(Character.CIRCLED_INFORMATION_SOURCE + " The response contains integer(s) outside the safe-integer range that may have lost precision");
+ }
+
Output._addResponseSummary(topSummaryDiv, pCommand, pResponse, pMinionData, pInitialStatus);
Output._addExtraInfo(pOutputContainer, pExtraInfo);
diff --git a/saltgui/static/scripts/panels/Panel.js b/saltgui/static/scripts/panels/Panel.js
index 40d97bc57..9c5312209 100644
--- a/saltgui/static/scripts/panels/Panel.js
+++ b/saltgui/static/scripts/panels/Panel.js
@@ -1138,6 +1138,9 @@ export class Panel {
command.value = pCommandString;
// the menu may become (in)visible due to content of command field
this.router.commandbox.cmdmenu.verifyAll();
+ // run validations when panel is opened
+ CommandBox._validateAndDisplayTargetInput(pTargetString);
+ CommandBox._validateAndDisplayCommandInput(pCommandString);
}
clearPanel () {
diff --git a/saltgui/static/stylesheets/controls.css b/saltgui/static/stylesheets/controls.css
index 4712eb442..9e70be543 100644
--- a/saltgui/static/stylesheets/controls.css
+++ b/saltgui/static/stylesheets/controls.css
@@ -46,4 +46,11 @@ input:disabled {
opacity: 0.2;
}
+.cmd-validation-indicator {
+ display: inline-block;
+ font-size: 18px;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+
/* end */
diff --git a/tests/unit/CommandBox.test.js b/tests/unit/CommandBox.test.js
index 06b80ef87..bb8f95cd6 100644
--- a/tests/unit/CommandBox.test.js
+++ b/tests/unit/CommandBox.test.js
@@ -1,8 +1,11 @@
/* global describe it */
+import {Character} from "../../saltgui/static/scripts/Character.js";
import {CommandBox} from "../../saltgui/static/scripts/CommandBox.js";
import {assert} from "chai";
+Character.init();
+
describe("Unittests for CommandBox.js", () => {
describe("_getReadOnlyPanels", () => {
@@ -131,4 +134,200 @@ describe("Unittests for CommandBox.js", () => {
});
});
+ describe("_validateCommandField", () => {
+ it("test empty command returns no error", () => {
+ const result = CommandBox._validateCommandField("");
+ assert.isArray(result.errors);
+ assert.isArray(result.warnings);
+ assert.equal(result.errors.length, 0);
+ assert.equal(result.warnings.length, 0);
+ });
+
+ it("test unterminated double quote returns error", () => {
+ const result = CommandBox._validateCommandField("\"hello");
+ assert.isArray(result.errors);
+ assert.include(result.errors[0], "No valid double-quoted-string found");
+ });
+
+ it("test terminated double quotes returns no error", () => {
+ const result = CommandBox._validateCommandField("\"hello\"");
+ assert.equal(result.errors.length, 0);
+ });
+
+ it("test sexagesimal out of range treated as string with warning", () => {
+ const result = CommandBox._validateCommandField("cmd 1:99");
+ assert.equal(result.errors.length, 0);
+ assert.isArray(result.warnings);
+ assert.isTrue(result.warnings.some(err => err.includes("Sexagesimal format not recognized")));
+ });
+
+ it("test valid sexagesimal returns no error", () => {
+ const result = CommandBox._validateCommandField("cmd 1:30");
+ assert.equal(result.errors.length, 0);
+ });
+
+ it("test 64-bit overflow returns warning with original argument", () => {
+ const result = CommandBox._validateCommandField("cmd 99999999999999999999");
+ assert.isArray(result.warnings);
+ assert.isTrue(result.warnings.some(wrn => wrn.includes("exceeds integer range") && wrn.includes("in: 99999999999999999999") && !wrn.startsWith("number")));
+ });
+
+ it("test valid number returns no warning", () => {
+ const result = CommandBox._validateCommandField("cmd 123");
+ assert.equal(result.warnings.length, 0);
+ });
+
+ it("test empty command returns no error", () => {
+ const result = CommandBox._validateCommandField("");
+ assert.isArray(result.errors);
+ assert.isEmpty(result.errors);
+ });
+
+ it("test first unnamed argument as hexadecimal number returns error", () => {
+ const result = CommandBox._validateCommandField("0xFF arg");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("must be a string") && err.includes("(bigint)")));
+ });
+
+ it("test first unnamed argument as binary number returns error", () => {
+ const result = CommandBox._validateCommandField("0b1010");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("must be a string") && err.includes("(bigint)")));
+ });
+
+ it("test first unnamed argument as octal number returns error", () => {
+ const result = CommandBox._validateCommandField("010");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("must be a string") && err.includes("(bigint)")));
+ });
+
+ it("test first unnamed argument as decimal number returns error", () => {
+ const result = CommandBox._validateCommandField("123 arg");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("must be a string") && err.includes("(bigint)")));
+ });
+
+ it("test first unnamed argument as quoted string returns no error", () => {
+ const result = CommandBox._validateCommandField("\"0xFF\"");
+ assert.equal(result.errors.length, 0);
+ });
+
+ it("test runner instead of runners returns error", () => {
+ const result = CommandBox._validateCommandField("runner.test");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("Runner commands must be prefixed with 'runners.'")));
+ });
+
+ it("test wheel command with unnamed parameter returns error", () => {
+ const result = CommandBox._validateCommandField("wheel.key.accept minion1");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("Wheel commands can only take named parameters")));
+ });
+
+ it("test wheel command with only named parameters returns no error", () => {
+ const result = CommandBox._validateCommandField("wheel.key.accept match=minion1");
+ assert.equal(result.errors.length, 0);
+ });
+
+ it("test named parameter without value returns error", () => {
+ const result = CommandBox._validateCommandField("cmd x=");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("Must have value for named parameter")));
+ });
+
+ it("test duplicate named parameter returns error", () => {
+ const result = CommandBox._validateCommandField("cmd x=1 x=2");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("Duplicate named variable")));
+ });
+
+ it("test unfilled placeholder returns error", () => {
+ const result = CommandBox._validateCommandField("cmd ");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("Must fill in all placeholders")));
+ });
+
+ it("test invalid dictionary returns error", () => {
+ const result = CommandBox._validateCommandField("cmd {\"a}\":1");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("No valid dictionary found")));
+ });
+
+ it("test invalid array returns error", () => {
+ const result = CommandBox._validateCommandField("cmd [1,2");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("No valid array found")));
+ });
+
+ it("test dictionary followed by text returns error", () => {
+ const result = CommandBox._validateCommandField("cmd {\"a\":1}extra");
+ assert.isArray(result.errors);
+ assert.isTrue(result.errors.some(err => err.includes("Valid dictionary, but followed by extra text")));
+ assert.isTrue(result.errors.some(err => err.includes("dictionary:")));
+ assert.isTrue(result.errors.some(err => err.includes("extra:")));
+ });
+
+ it("test valid named parameter returns no error", () => {
+ const result = CommandBox._validateCommandField("cmd x=5");
+ assert.equal(result.errors.length, 0);
+ });
+
+ it("test valid dictionary returns no error", () => {
+ const result = CommandBox._validateCommandField("cmd {\"a\":1}");
+ assert.equal(result.errors.length, 0);
+ });
+
+ it("test valid array returns no error", () => {
+ const result = CommandBox._validateCommandField("cmd [1,2,3]");
+ assert.equal(result.errors.length, 0);
+ });
+ });
+
+ describe("_displayValidationOutput", () => {
+ it("test displays errors with error icon", () => {
+ const mockOutput = {innerText: ""};
+ const validationResult = {
+ errors: ["Error 1", "Error 2"],
+ warnings: []
+ };
+ CommandBox._displayValidationOutput(validationResult, mockOutput);
+ assert.include(mockOutput.innerText, Character.NO_ENTRY_SIGN + " Error 1");
+ assert.include(mockOutput.innerText, Character.NO_ENTRY_SIGN + " Error 2");
+ });
+
+ it("test displays warnings with warning icon", () => {
+ const mockOutput = {innerText: ""};
+ const validationResult = {
+ errors: [],
+ warnings: ["Warning 1", "Warning 2"]
+ };
+ CommandBox._displayValidationOutput(validationResult, mockOutput);
+ assert.include(mockOutput.innerText, Character.WARNING_SIGN + " Warning 1");
+ assert.include(mockOutput.innerText, Character.WARNING_SIGN + " Warning 2");
+ });
+
+ it("test displays both errors and warnings with icons", () => {
+ const mockOutput = {innerText: ""};
+ const validationResult = {
+ errors: ["Error 1"],
+ warnings: ["Warning 1"]
+ };
+ CommandBox._displayValidationOutput(validationResult, mockOutput);
+ assert.include(mockOutput.innerText, Character.NO_ENTRY_SIGN + " Error 1");
+ assert.include(mockOutput.innerText, Character.WARNING_SIGN + " Warning 1");
+ });
+
+ it("test displays errors before warnings", () => {
+ const mockOutput = {innerText: ""};
+ const validationResult = {
+ errors: ["Error 1"],
+ warnings: ["Warning 1"]
+ };
+ CommandBox._displayValidationOutput(validationResult, mockOutput);
+ const errorIndex = mockOutput.innerText.indexOf(Character.NO_ENTRY_SIGN);
+ const warningIndex = mockOutput.innerText.indexOf(Character.WARNING_SIGN);
+ assert.isTrue(errorIndex < warningIndex);
+ });
+ });
+
});
diff --git a/tests/unit/ParseCommandLine.test.js b/tests/unit/ParseCommandLine.test.js
index 52ff9eea2..17b21d046 100644
--- a/tests/unit/ParseCommandLine.test.js
+++ b/tests/unit/ParseCommandLine.test.js
@@ -11,22 +11,23 @@ describe("Unittests for ParseCommandLine.js", () => {
describe("getCommandFromCommandLine", () => {
it("test extract command from simple command line", () => {
const result = ParseCommandLine.getCommandFromCommandLine("test");
- assert.equal(result, "test");
+ assert.isTrue(result.startsWith("test"));
});
it("test extract command from command line with parameters", () => {
const result = ParseCommandLine.getCommandFromCommandLine("test arg1 arg2");
- assert.equal(result, "test");
+ assert.isTrue(result.startsWith("test"));
});
it("test extract command from command line with named parameters", () => {
const result = ParseCommandLine.getCommandFromCommandLine("test x=5 y=10");
- assert.equal(result, "test");
+ assert.isTrue(result.startsWith("test"));
});
});
describe("parseCommandLine", () => {
it("test comprehensive parsing scenarios", () => {
+ let tokens = [];
let args = [];
let params = {};
let result;
@@ -34,9 +35,11 @@ describe("Unittests for ParseCommandLine.js", () => {
// GENERAL ERROR HANDLING
// null means: it was all ok
+ tokens = [];
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("test", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("test", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "test");
@@ -45,20 +48,23 @@ describe("Unittests for ParseCommandLine.js", () => {
// broken json will return a readable error message
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("{\"test\"", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("{\"test\"", tokens, args, params);
assert.isTrue(result.startsWith("No valid dictionary found,"));
// remaining placeholder
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("aap mies", args, params);
- assert.equal(result, "Must fill in all placeholders, e.g. ");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("aap mies", tokens, args, params);
+ assert.isTrue(result.startsWith("Must fill in all placeholders"));
// GENERAL WHITESPACE HANDLING
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine(" name=true", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine(" name=true", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 0);
assert.equal(Object.keys(params).length, 1);
@@ -66,7 +72,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("name=true ", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("name=true ", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 0);
assert.equal(Object.keys(params).length, 1);
@@ -77,28 +84,35 @@ describe("Unittests for ParseCommandLine.js", () => {
// name-value-pair without value is not ok
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("test=", args, params);
- assert.equal(result, "Must have value for named parameter 'test'");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("test=", tokens, args, params);
+ assert.isTrue(result.startsWith("Must have value for named parameter\n"));
+ assert.isTrue(result.includes("in: test"));
// name-value-pair without value is not ok
// make sure it does not confuse it with furher parameters
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("test= arg2 arg3", args, params);
- assert.equal(result, "Must have value for named parameter 'test'");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("test= arg2 arg3", tokens, args, params);
+ assert.isTrue(result.startsWith("Must have value for named parameter\n"));
+ assert.isTrue(result.includes("in: test"));
// duplicate named parameter
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("x=1 y=2 x=3", args, params);
- assert.equal(result, "Duplicate named variable 'x'");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("x=1 y=2 x=3", tokens, args, params);
+ assert.isTrue(result.startsWith("Duplicate named variable\n"));
+ assert.isTrue(result.includes("in: x"));
// DICTIONARY
// a regular dictionary
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("{\"a\":1}", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("{\"a\":1}", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.deepEqual(args[0], {"a": 1});
@@ -107,28 +121,34 @@ describe("Unittests for ParseCommandLine.js", () => {
// a broken dictionary
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("{\"a}\":1", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("{\"a}\":1", tokens, args, params);
assert.isTrue(result.startsWith("No valid dictionary found,"));
// a regular dictionary with } in its name
// test that the parser is not confused
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("{\"a}\":1}", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("{\"a}\":1}", tokens, args, params);
assert.equal(result, null);
// a regular dictionary with } after its value
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("{\"a}\":1}}", args, params);
- assert.equal(result, "Valid dictionary, but followed by text:}" + Character.HORIZONTAL_ELLIPSIS);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("{\"a}\":1}}", tokens, args, params);
+ assert.isTrue(result.startsWith("Valid dictionary, but followed by extra text\n"));
+ assert.isTrue(result.includes("dictionary: {\"a}\":1}"));
+ assert.isTrue(result.includes("extra: }"));
// ARRAYS
// a simple array
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("[1,2]", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("[1,2]", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.deepEqual(args[0], [1, 2]);
@@ -137,6 +157,7 @@ describe("Unittests for ParseCommandLine.js", () => {
// a simple array that is not closed
args = [];
params = {};
+ tokens = [];
result = ParseCommandLine.parseCommandLine("[1,2", args, params);
assert.isTrue(result.startsWith("No valid array found,"));
@@ -145,7 +166,8 @@ describe("Unittests for ParseCommandLine.js", () => {
// a simple string
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("string", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("string", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "string");
@@ -154,7 +176,8 @@ describe("Unittests for ParseCommandLine.js", () => {
// a number that looks like a jobid
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("20180820003411338317", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("20180820003411338317", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "20180820003411338317");
@@ -165,7 +188,8 @@ describe("Unittests for ParseCommandLine.js", () => {
// a simple string
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("\"string\"", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("\"string\"", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "string");
@@ -174,15 +198,18 @@ describe("Unittests for ParseCommandLine.js", () => {
// an unclosed string
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("\"string", args, params);
- assert.equal(result, "No valid double-quoted-string found");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("\"string", tokens, args, params);
+ assert.isTrue(result.startsWith("No valid double-quoted-string found"));
+ assert.isTrue(result.includes("in: \"string"));
// TRIPLE-DOUBLE-QUOTED-STRINGS
// a simple string
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("\"\"\"string\"\"\"", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("\"\"\"string\"\"\"", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "string");
@@ -191,7 +218,8 @@ describe("Unittests for ParseCommandLine.js", () => {
// a simple string with embedded nasties
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("\"\"\"abc\"\"def\\ghi\"\"\"", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("\"\"\"abc\"\"def\\ghi\"\"\"", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "abc\"\"def\\ghi");
@@ -200,14 +228,18 @@ describe("Unittests for ParseCommandLine.js", () => {
// an unclosed string (no end quotes)
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("\"\"\"string", args, params);
- assert.equal(result, "No valid triple-quoted-string found");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("\"\"\"string", tokens, args, params);
+ assert.isTrue(result.startsWith("No valid triple-quoted-string found"));
+ assert.isTrue(result.includes("in: \"\"\"string"));
// an unclosed string (too few endquotes)
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("\"\"\"string\"\"", args, params);
- assert.equal(result, "No valid triple-quoted-string found");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("\"\"\"string\"\"", tokens, args, params);
+ assert.isTrue(result.startsWith("No valid triple-quoted-string found"));
+ assert.isTrue(result.includes("in: \"\"\"string\"\""));
// SINGLE-QUOTED-STRINGS (never supported!)
@@ -215,7 +247,8 @@ describe("Unittests for ParseCommandLine.js", () => {
// it evalueates as a string (the whole thing)
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("'string'", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("'string'", tokens, args, params);
assert.equal(result, null);
assert.equal(args.length, 1);
assert.equal(args[0], "'string'");
@@ -226,7 +259,8 @@ describe("Unittests for ParseCommandLine.js", () => {
// even when that looks rediculous
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("'string", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("'string", tokens, args, params);
assert.equal(result, null);
assert.equal(args.length, 1);
assert.equal(args[0], "'string");
@@ -236,44 +270,71 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("0", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
- assert.equal(args[0], 0);
+ assert.equal(args[0], 0n);
assert.equal(Object.keys(params).length, 0);
// an integer that almost looks like a jobid, but one digit less
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("2018082000341133831", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("2018082000341133831", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
- assert.equal(args[0], 2018082000341133831);
+ assert.equal(args[0], 2018082000341133831n);
assert.equal(Object.keys(params).length, 0);
// an integer that almost looks like a jobid, but one digit more
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("201808200034113383170", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("201808200034113383170", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
- assert.equal(args[0], 201808200034113383170);
+ assert.equal(args[0], 201808200034113383170n);
assert.equal(Object.keys(params).length, 0);
// an integer that almost looks like a jobid, just not a true date-time
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("20182820003411338317", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("20182820003411338317", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], 20182820003411338317);
assert.equal(Object.keys(params).length, 0);
+ // max int64 value
+ args = [];
+ params = {};
+ tokens = [];
+ // 9223372036854775807 = 0x7FFFFFFFFFFFFFFF
+ result = ParseCommandLine.parseCommandLine("9223372036854775807", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 9223372036854775807n);
+ assert.equal(Object.keys(params).length, 0);
+
+ // beyond max int64 value
+ args = [];
+ params = {};
+ tokens = [];
+ // 9223372036854775808 = 0x8000000000000000
+ result = ParseCommandLine.parseCommandLine("9223372036854775808", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], "9223372036854775808");
+ assert.equal(Object.keys(params).length, 0);
+
// FLOAT
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("0.", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0.", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], 0);
@@ -281,7 +342,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine(".0", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine(".0", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], 0);
@@ -289,7 +351,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("0.0", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0.0", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], 0.0);
@@ -297,7 +360,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("0.0.0", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0.0.0", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "0.0.0");
@@ -305,7 +369,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine(".", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine(".", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], ".");
@@ -313,7 +378,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("1e99", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1e99", tokens, args, params);
assert.equal(result, null);
assert.equal(args.length, 1);
assert.equal(args[0], 1e99);
@@ -321,7 +387,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("-1e99", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("-1e99", tokens, args, params);
assert.equal(result, null);
assert.equal(args.length, 1);
assert.equal(args[0], -1e99);
@@ -329,7 +396,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("+1e99", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("+1e99", tokens, args, params);
assert.equal(result, null);
assert.equal(args.length, 1);
assert.equal(args[0], 1e99);
@@ -337,7 +405,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("1e-99", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1e-99", tokens, args, params);
assert.equal(result, null);
assert.equal(args.length, 1);
assert.equal(args[0], 1e-99);
@@ -345,7 +414,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("1e+99", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1e+99", tokens, args, params);
assert.equal(result, null);
assert.equal(args.length, 1);
assert.equal(args[0], 1e99);
@@ -353,14 +423,224 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("1e999", args, params);
- assert.equal(result, "Numeric argument has overflowed or is infinity");
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1e999", tokens, args, params);
+ assert.isTrue(result.startsWith("Numeric argument has overflowed or is infinity"));
+
+ // HEXADECIMAL
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0xFF", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 255n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0x10", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 16n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("-0x10", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], -16n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0x0", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 0n);
+ assert.equal(Object.keys(params).length, 0);
+
+ // very large hexadecimal (256 times 'f', exceeds 64-bit range)
+ args = [];
+ params = {};
+ tokens = [];
+ const veryLargeHex = "0x" + "f".repeat(256);
+ result = ParseCommandLine.parseCommandLine(veryLargeHex, tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], veryLargeHex);
+ assert.equal(Object.keys(params).length, 0);
+
+ // BINARY
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0b1010", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 10n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0b101", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 5n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("-0b101", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], -5n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0b0", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 0n);
+ assert.equal(Object.keys(params).length, 0);
+
+ // OCTAL
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("077", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 63n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("010", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 8n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("-010", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], -8n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("00", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 0n);
+ assert.equal(Object.keys(params).length, 0);
+
+ // SEXAGESIMAL
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1:30", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 90n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1:30:45", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 5445n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1:02", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 62n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("99:00", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], 5940n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("-1:30", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], -90n);
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1:99", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], "1:99");
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("1:30:99", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], "1:30:99");
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("0:30", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], "0:30");
+ assert.equal(Object.keys(params).length, 0);
+
+ args = [];
+ params = {};
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("01:30", tokens, args, params);
+ assert.isNull(result);
+ assert.equal(args.length, 1);
+ assert.equal(args[0], "01:30");
+ assert.equal(Object.keys(params).length, 0);
// NULL
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("null", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("null", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], null);
@@ -368,7 +648,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("Null", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("Null", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], null);
@@ -376,7 +657,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("NULL", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("NULL", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], null);
@@ -384,7 +666,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("NUll", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("NUll", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "NUll");
@@ -394,7 +677,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("none", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("none", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "none");
@@ -402,7 +686,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("None", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("None", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], null);
@@ -410,7 +695,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("NONE", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("NONE", tokens, args, params);
// GENERAL WHITESPACE HANDLING
@@ -421,7 +707,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("NOne", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("NOne", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "NOne");
@@ -431,7 +718,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("true", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("true", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], true);
@@ -439,7 +727,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("True", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("True", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], true);
@@ -447,7 +736,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("TRUE", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("TRUE", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], true);
@@ -455,7 +745,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("TRue", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("TRue", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "TRue");
@@ -463,7 +754,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("false", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("false", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], false);
@@ -471,7 +763,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("False", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("False", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], false);
@@ -479,7 +772,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("FALSE", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("FALSE", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], false);
@@ -487,7 +781,8 @@ describe("Unittests for ParseCommandLine.js", () => {
args = [];
params = {};
- result = ParseCommandLine.parseCommandLine("FAlse", args, params);
+ tokens = [];
+ result = ParseCommandLine.parseCommandLine("FAlse", tokens, args, params);
assert.isNull(result);
assert.equal(args.length, 1);
assert.equal(args[0], "FAlse");