From f148e7491bc62c93c81841ccc17c686d2a0ba5b5 Mon Sep 17 00:00:00 2001 From: Roque <32566150+jrk94@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:28:12 +0000 Subject: [PATCH] feat(json validator): validate subworkflows closes #452 --- cmf-cli/Builders/JSONValidatorCommand.cs | 125 ++ tests/Specs/JsonValidator.cs | 1526 ++++++++++++++++++++++ 2 files changed, 1651 insertions(+) diff --git a/cmf-cli/Builders/JSONValidatorCommand.cs b/cmf-cli/Builders/JSONValidatorCommand.cs index 29df4dd0..57bc2635 100644 --- a/cmf-cli/Builders/JSONValidatorCommand.cs +++ b/cmf-cli/Builders/JSONValidatorCommand.cs @@ -73,6 +73,8 @@ public Task Exec() { if (Condition()) { + List workflowNamesAndPaths = []; + List subWorkflowsToValidate = []; foreach (var file in FilesToValidate.Where(file => file.Source.FullName.Contains(".json"))) { //Open file for Read\Write @@ -87,6 +89,11 @@ public Task Exec() try { var json = JsonDocument.Parse(fileContent); + + #region Collect JSON SubWorkflows to Validate + workflowNamesAndPaths.AddRange(ExtractWorkflowNames(json)); + subWorkflowsToValidate.AddRange(ExtractSubWorkflowNamesFromAutomationWorkflow(json)); + #endregion } catch (Exception) { @@ -104,6 +111,13 @@ public Task Exec() #endregion } + + #region Validate all IoT JSON SubWorkflows exist + if (subWorkflowsToValidate.Count > 0) + { + AllSubWorkflowsExist(subWorkflowsToValidate, workflowNamesAndPaths, FilesToValidate.Select(file => file.Source.FullName).ToList()); + } + #endregion } else { @@ -111,5 +125,116 @@ public Task Exec() } return null; } + + private static bool AllSubWorkflowsExist(List isContainedList, List containsList, List filesToValidate = null) + { + // Use a HashSet for fast lookups + var set = new HashSet(containsList.Select(item => item.Name)); + + foreach (var item in isContainedList) + { + if (!set.Contains(item)) + { + throw new CliException($"The subworkflow {item} is mentioned but there is no workflow declared with that name."); + } + + var workflowLocation = containsList.FirstOrDefault(clitem => clitem.Name == item); + if (!string.IsNullOrEmpty(workflowLocation.Path) && filesToValidate != null) + { + IsPartialPathPresent(workflowLocation.Path, filesToValidate, true); + } + } + + return true; // All items are present + } + + private static bool IsPartialPathPresent(string partialPath, List filePaths, bool throwOnFalse = true) + { + foreach (var fullPath in filePaths) + { + if (fullPath.Replace("\\", "/").Contains(partialPath, StringComparison.OrdinalIgnoreCase)) + { + return true; // Return true as soon as a match is found + } + } + + if (throwOnFalse) + { + throw new CliException($"Could not find the path {partialPath} for the Workflow"); + } + + return false; // No match found + } + + private static List ExtractWorkflowNames(JsonDocument json) + { + var names = new List(); + // Navigate to the "AutomationControllerWorkflow" object + if (json.RootElement.TryGetProperty("AutomationControllerWorkflow", out JsonElement workflows) && + workflows.ValueKind == JsonValueKind.Object) + { + // Iterate through all properties in "AutomationControllerWorkflow" + foreach (var property in workflows.EnumerateObject()) + { + if (property.Value.ValueKind == JsonValueKind.Object && + property.Value.TryGetProperty("Name", out JsonElement name)) + { + property.Value.TryGetProperty("IsFile", out JsonElement isFile); + + if (isFile.ToString().ToBool()) + { + property.Value.TryGetProperty("Workflow", out JsonElement workflow); + names.Add(new WorkflowsToValidate(name.GetString(), workflow.GetString())); + } + else + { + // In this case the workflow is already in the json file, so we don't need to keep the path + names.Add(new WorkflowsToValidate(name.GetString())); + } + + } + } + } + return names; + } + + private static List ExtractSubWorkflowNamesFromAutomationWorkflow(JsonDocument json) + { + var names = new List(); + // Navigate to the "tasks" array + if (json.RootElement.TryGetProperty("tasks", out JsonElement tasks) && + tasks.ValueKind == JsonValueKind.Array) + { + foreach (var task in tasks.EnumerateArray()) + { + // Check if the task contains "settings" and "automationWorkflow" + if (task.TryGetProperty("settings", out JsonElement settings) && + settings.TryGetProperty("automationWorkflow", out JsonElement automationWorkflow) && + automationWorkflow.ValueKind == JsonValueKind.Object) + { + // Check if "IsShared" is false and retrieve "Name" + if (automationWorkflow.TryGetProperty("IsShared", out JsonElement isShared) && + isShared.ValueKind == JsonValueKind.False && + automationWorkflow.TryGetProperty("Name", out JsonElement name)) + { + names.Add(name.GetString()); + } + } + } + } + return names; + } + } + + public record WorkflowsToValidate + { + public string Path { get; set; } + public string Name { get; set; } + + public WorkflowsToValidate(string name, string path = "") + { + this.Path = path; + this.Name = name; + } } } \ No newline at end of file diff --git a/tests/Specs/JsonValidator.cs b/tests/Specs/JsonValidator.cs index 8479c81e..36094e54 100644 --- a/tests/Specs/JsonValidator.cs +++ b/tests/Specs/JsonValidator.cs @@ -523,5 +523,1531 @@ public void Data_JsonValidator_Fail_BackSlash() Assert.True(console.Error.ToString().Contains("Please normalize all slashes to be forward slashes"), $"Json Validator did not fail for IoT Data Workflow Package: {console.Error.ToString()}"); } + [Fact] + public void Data_JsonValidator_HappyPath_SubWorkflow() + { + var fileSystem = new MockFileSystem(new Dictionary + { + { "/test/cmfpackage.json", new MockFileData( + @"{ + ""packageId"": ""Cmf.Custom.Package"", + ""version"": ""1.1.0"", + ""description"": ""This package deploys Critical Manufacturing Customization"", + ""packageType"": ""Root"", + ""isInstallable"": true, + ""isUniqueInstall"": false, + ""dependencies"": [ + { + ""id"": ""Cmf.Custom.Data"", + ""version"": ""1.1.0"" + }, + { + ""id"": ""Cmf.Custom.IoT"", + ""version"": ""1.1.0"" + } + ] + }") + }, + { "/test/Data/cmfpackage.json", new CmfMockJsonData( + @"{ + ""packageId"": ""Cmf.Custom.Data"", + ""version"": ""1.1.0"", + ""description"": ""Cmf Custom Data Package"", + ""packageType"": ""Data"", + ""isInstallable"": true, + ""isUniqueInstall"": true, + ""contentToPack"": [ + { + ""source"": ""MasterData/$(version)/*"", + ""target"": ""MasterData/$(version)/"", + ""contentType"": ""MasterData"" + }, + { + ""source"": ""AutomationWorkFlows/*"", + ""target"": ""AutomationWorkFlows"", + ""contentType"": ""AutomationWorkFlows"" + } + ] + }") + }, + { "/test/Data/MasterData/1.1.0/Test.json", new MockFileData( + @"{ + ""AutomationControllerWorkflow"": { + ""1"": { + ""AutomationController"": ""TestController"", + ""Name"": ""Test"", + ""DisplayName"": ""Test"", + ""IsFile"": ""Yes"", + ""Workflow"": ""Test/test.json"", + ""Order"": ""1"" + }, + ""2"": { + ""AutomationController"": ""TestController"", + ""Name"": ""Test2"", + ""DisplayName"": ""Test2"", + ""IsFile"": ""Yes"", + ""IsFile"": ""Yes"", + ""Workflow"": ""Test2/Test2.json"", + ""Order"": ""2"" + } + } + }") + }, + { "/test/Data/AutomationWorkflows/Test/Test.json", new MockFileData( + @"{ + ""tasks"": [ + { + ""id"": ""task_356"", + ""reference"": { + ""name"": ""driverEvent"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""autoEnable"": true, + ""event"": ""OnInitialize"", + ""autoSetup"": true, + ""___cmf___description"": ""OnInitialize"" + } + }, + { + ""id"": ""task_357"", + ""reference"": { + ""name"": ""equipmentConfig"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""_inputs"": [ + { + ""name"": ""path"", + ""label"": ""Path"", + ""defaultValue"": ""c:/temp"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""path"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Path of the location to monitor for changes on the files. If no value is provided, no watcher is created, so no events will be triggered."", + ""settingKey"": ""name"" + }, + { + ""name"": ""fileMask"", + ""label"": ""FileMask"", + ""defaultValue"": ""**/*.request"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""fileMask"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Glob pattern to use for the watcher to identify the files to handle. Use a tool like https://globster.xyz/ to try a valid value to use."", + ""settingKey"": ""name"" + }, + { + ""name"": ""archivePath"", + ""label"": ""Archive Path"", + ""defaultValue"": """", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""archivePath"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Directory planned to use for archiving the files after processing. This value can later be used as a variable in the available operations to execute."", + ""settingKey"": ""name"" + }, + { + ""name"": ""watcherType"", + ""label"": ""Watcher Type"", + ""defaultValue"": ""Chokidar"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""watcherType"" + }, + ""dataType"": ""Enum"", + ""automationDataType"": 0, + ""referenceType"": 1, + ""description"": ""Type of watcher to use. Depending on the selection, different settings are necessary. Chokidar is the best overall option, but CPU and Memory heavy and slower to start when many files are present. NSFW is better other scenarios. Test both to determine the best option."", + ""valueReferenceType"": 6, + ""settings"": { + ""enumValues"": [ + ""Chokidar"", + ""NSFW"" + ] + }, + ""settingKey"": ""name"" + }, + { + ""name"": ""ignoreInitial"", + ""label"": ""Ignore Existing Files"", + ""defaultValue"": ""False"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""ignoreInitial"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Flag to define if, during startup/restart, the files that were created, changed, deleted should be processed or ignored"", + ""settingKey"": ""name"" + }, + { + ""name"": ""watcherMode"", + ""label"": ""File Watcher Mode"", + ""defaultValue"": ""Polling"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""watcherMode"" + }, + ""dataType"": ""Enum"", + ""automationDataType"": 0, + ""referenceType"": 1, + ""description"": ""(Type=Chokidar) To successfully watch files over a network (and in other non-standard situations), it is typically necessary to use Polling, however it could lead to high CPU utilization. FileSystemEvents is the most efficient method for monitoring local files."", + ""valueReferenceType"": 6, + ""settings"": { + ""enumValues"": [ + ""FileSystemEvents"", + ""Polling"" + ] + }, + ""settingKey"": ""name"" + }, + { + ""name"": ""pollingInterval"", + ""label"": ""Polling Interval (ms)"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""pollingInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Interval of file system polling (in milliseconds)"", + ""settingKey"": ""name"" + }, + { + ""name"": ""pollingBinaryInterval"", + ""label"": ""Binary Files Polling Interval (ms)"", + ""defaultValue"": ""300"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""pollingBinaryInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) Interval of file system polling for binary files (in milliseconds)"", + ""settingKey"": ""name"" + }, + { + ""name"": ""alwaysStat"", + ""label"": ""Always Stat"", + ""defaultValue"": ""True"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""alwaysStat"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Always get additional attributes (size, timestamps, etc) of the file that was identified by the watcher. Will require additional operating system resources."", + ""settingKey"": ""name"" + }, + { + ""name"": ""depth"", + ""label"": ""Subdirectory Depth"", + ""defaultValue"": ""0"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""depth"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Number of sub directories to watch. The higher the number, more memory/cpu/time will be required for watchers."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinish"", + ""label"": ""Await Write Finish"", + ""defaultValue"": ""True"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinish"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Trigger watcher events only when the file finishes writing."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinishStabilityThreshold"", + ""label"": ""Await Write Finish Stability (ms)"", + ""defaultValue"": ""2000"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinishStabilityThreshold"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Amount of time in milliseconds for a file size to remain constant before emitting its event."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinishPollInterval"", + ""label"": ""Await File Size Poll (ms)"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinishPollInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) File size polling interval."", + ""settingKey"": ""name"" + }, + { + ""name"": ""atomic"", + ""label"": ""Atomic Writes Threshold"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""atomic"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) Automatically filters out artifacts that occur within the defined value (in milliseconds) when using editors that use 'atomic writes' instead of writing directly to the source file. If defined to 0, this function is disabled."", + ""settingKey"": ""name"" + } + ], + ""connectingTimeout"": 30000, + ""setupTimeout"": 10000, + ""intervalBeforeReconnect"": 5000, + ""heartbeatInterval"": 60000, + ""___cmf___name"": ""Equipment Configuration"" + } + }, + { + ""id"": ""task_358"", + ""reference"": { + ""name"": ""driverCommand"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""command"": ""Connect"", + ""___cmf___description"": ""Connect"" + } + } + ], + ""converters"": [], + ""links"": [ + { + ""id"": ""task_356_success-task_357_activate"", + ""sourceId"": ""task_356"", + ""targetId"": ""task_357"", + ""inputName"": ""activate"", + ""outputName"": ""success"" + }, + { + ""id"": ""task_357_success-task_358_activate"", + ""sourceId"": ""task_357"", + ""targetId"": ""task_358"", + ""inputName"": ""activate"", + ""outputName"": ""success"" + } + ], + ""$id"": ""1"", + ""layout"": { + ""general"": { + ""color"": null, + ""notes"": [] + }, + ""drawers"": { + ""DIAGRAM"": { + ""tasks"": { + ""task_358"": { + ""collapsed"": false, + ""position"": { + ""x"": 1100, + ""y"": 100 + }, + ""outdated"": false + }, + ""task_356"": { + ""collapsed"": false, + ""position"": { + ""x"": 100, + ""y"": 100 + }, + ""outdated"": false + }, + ""task_357"": { + ""collapsed"": false, + ""position"": { + ""x"": 600, + ""y"": 100 + }, + ""outdated"": false + } + }, + ""links"": { + ""task_356_success-task_357_activate"": { + ""vertices"": [] + }, + ""task_357_success-task_358_activate"": { + ""vertices"": [] + } + }, + ""notes"": {}, + ""pan"": { + ""x"": 9.675338107580444, + ""y"": 3.532465519098835 + }, + ""zoom"": 0.78 + } + } + } + }") + }, + { "/test/Data/AutomationWorkflows/Test/Test2.json", new MockFileData( + @"{ + ""tasks"": [ + { + ""id"": ""task_536"", + ""reference"": { + ""name"": ""workflow"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-beta"" + } + }, + ""settings"": { + ""inputs"": [], + ""outputs"": [ + { + ""name"": ""activate"", + ""valueType"": { + ""friendlyName"": ""Activate"", + ""type"": null, + ""collectionType"": 0, + ""referenceType"": null, + ""referenceTypeName"": null, + ""referenceTypeId"": null + } + } + ], + ""retries"": 30, + ""contextsExpirationInMilliseconds"": 60000, + ""executionExpirationInMilliseconds"": 1200000, + ""executeWhenAllInputsDefined"": false, + ""automationWorkflow"": { + ""DisplayName"": ""workflow_544"", + ""IsShared"": false, + ""Name"": ""Test"" + }, + ""___cmf___name"": ""Call SubWorkflow"" + } + } + ], + ""converters"": [], + ""links"": [], + ""$id"": ""1"", + ""layout"": { + ""general"": { + ""color"": null, + ""notes"": [] + }, + ""drawers"": { + ""DIAGRAM"": { + ""tasks"": { + ""task_536"": { + ""collapsed"": false, + ""position"": { + ""x"": 559, + ""y"": 204 + }, + ""outdated"": false + }, + ""task_751"": { + ""collapsed"": false, + ""position"": { + ""x"": 1023, + ""y"": 172 + }, + ""outdated"": false + }, + ""task_762"": { + ""collapsed"": false, + ""position"": { + ""x"": 759, + ""y"": 404 + }, + ""outdated"": false + } + }, + ""links"": {}, + ""notes"": {}, + ""pan"": { + ""x"": 0, + ""y"": 3 + } + } + } + } + }") + } + }); + + BuildCommand buildCommand = new BuildCommand(fileSystem.FileSystem); + + var cmd = new Command("build"); + buildCommand.Configure(cmd); + + var console = new TestConsole(); + cmd.Invoke(new string[] { + "test/Data/" + }, console); + + Assert.True(console.Error == null || string.IsNullOrEmpty(console.Error.ToString()), $"Json Validator failed {console.Error.ToString()}"); + } + + [Fact] + public void Data_JsonValidator_FailPath_SubWorkflow_NotFoundFile() + { + var fileSystem = new MockFileSystem(new Dictionary + { + { "/test/cmfpackage.json", new MockFileData( + @"{ + ""packageId"": ""Cmf.Custom.Package"", + ""version"": ""1.1.0"", + ""description"": ""This package deploys Critical Manufacturing Customization"", + ""packageType"": ""Root"", + ""isInstallable"": true, + ""isUniqueInstall"": false, + ""dependencies"": [ + { + ""id"": ""Cmf.Custom.Data"", + ""version"": ""1.1.0"" + }, + { + ""id"": ""Cmf.Custom.IoT"", + ""version"": ""1.1.0"" + } + ] + }") + }, + { "/test/Data/cmfpackage.json", new CmfMockJsonData( + @"{ + ""packageId"": ""Cmf.Custom.Data"", + ""version"": ""1.1.0"", + ""description"": ""Cmf Custom Data Package"", + ""packageType"": ""Data"", + ""isInstallable"": true, + ""isUniqueInstall"": true, + ""contentToPack"": [ + { + ""source"": ""MasterData/$(version)/*"", + ""target"": ""MasterData/$(version)/"", + ""contentType"": ""MasterData"" + }, + { + ""source"": ""AutomationWorkFlows/*"", + ""target"": ""AutomationWorkFlows"", + ""contentType"": ""AutomationWorkFlows"" + } + ] + }") + }, + { "/test/Data/MasterData/1.1.0/Test.json", new MockFileData( + @"{ + ""AutomationControllerWorkflow"": { + ""1"": { + ""AutomationController"": ""TestController"", + ""Name"": ""Test"", + ""DisplayName"": ""Test"", + ""IsFile"": ""Yes"", + ""Workflow"": ""Error"", + ""Order"": ""1"" + }, + ""2"": { + ""AutomationController"": ""TestController"", + ""Name"": ""Test2"", + ""DisplayName"": ""Test2"", + ""IsFile"": ""Yes"", + ""IsFile"": ""Yes"", + ""Workflow"": ""Test2/Test2.json"", + ""Order"": ""2"" + } + } + }") + }, + { "/test/Data/AutomationWorkflows/Test/Test.json", new MockFileData( + @"{ + ""tasks"": [ + { + ""id"": ""task_356"", + ""reference"": { + ""name"": ""driverEvent"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""autoEnable"": true, + ""event"": ""OnInitialize"", + ""autoSetup"": true, + ""___cmf___description"": ""OnInitialize"" + } + }, + { + ""id"": ""task_357"", + ""reference"": { + ""name"": ""equipmentConfig"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""_inputs"": [ + { + ""name"": ""path"", + ""label"": ""Path"", + ""defaultValue"": ""c:/temp"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""path"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Path of the location to monitor for changes on the files. If no value is provided, no watcher is created, so no events will be triggered."", + ""settingKey"": ""name"" + }, + { + ""name"": ""fileMask"", + ""label"": ""FileMask"", + ""defaultValue"": ""**/*.request"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""fileMask"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Glob pattern to use for the watcher to identify the files to handle. Use a tool like https://globster.xyz/ to try a valid value to use."", + ""settingKey"": ""name"" + }, + { + ""name"": ""archivePath"", + ""label"": ""Archive Path"", + ""defaultValue"": """", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""archivePath"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Directory planned to use for archiving the files after processing. This value can later be used as a variable in the available operations to execute."", + ""settingKey"": ""name"" + }, + { + ""name"": ""watcherType"", + ""label"": ""Watcher Type"", + ""defaultValue"": ""Chokidar"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""watcherType"" + }, + ""dataType"": ""Enum"", + ""automationDataType"": 0, + ""referenceType"": 1, + ""description"": ""Type of watcher to use. Depending on the selection, different settings are necessary. Chokidar is the best overall option, but CPU and Memory heavy and slower to start when many files are present. NSFW is better other scenarios. Test both to determine the best option."", + ""valueReferenceType"": 6, + ""settings"": { + ""enumValues"": [ + ""Chokidar"", + ""NSFW"" + ] + }, + ""settingKey"": ""name"" + }, + { + ""name"": ""ignoreInitial"", + ""label"": ""Ignore Existing Files"", + ""defaultValue"": ""False"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""ignoreInitial"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Flag to define if, during startup/restart, the files that were created, changed, deleted should be processed or ignored"", + ""settingKey"": ""name"" + }, + { + ""name"": ""watcherMode"", + ""label"": ""File Watcher Mode"", + ""defaultValue"": ""Polling"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""watcherMode"" + }, + ""dataType"": ""Enum"", + ""automationDataType"": 0, + ""referenceType"": 1, + ""description"": ""(Type=Chokidar) To successfully watch files over a network (and in other non-standard situations), it is typically necessary to use Polling, however it could lead to high CPU utilization. FileSystemEvents is the most efficient method for monitoring local files."", + ""valueReferenceType"": 6, + ""settings"": { + ""enumValues"": [ + ""FileSystemEvents"", + ""Polling"" + ] + }, + ""settingKey"": ""name"" + }, + { + ""name"": ""pollingInterval"", + ""label"": ""Polling Interval (ms)"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""pollingInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Interval of file system polling (in milliseconds)"", + ""settingKey"": ""name"" + }, + { + ""name"": ""pollingBinaryInterval"", + ""label"": ""Binary Files Polling Interval (ms)"", + ""defaultValue"": ""300"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""pollingBinaryInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) Interval of file system polling for binary files (in milliseconds)"", + ""settingKey"": ""name"" + }, + { + ""name"": ""alwaysStat"", + ""label"": ""Always Stat"", + ""defaultValue"": ""True"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""alwaysStat"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Always get additional attributes (size, timestamps, etc) of the file that was identified by the watcher. Will require additional operating system resources."", + ""settingKey"": ""name"" + }, + { + ""name"": ""depth"", + ""label"": ""Subdirectory Depth"", + ""defaultValue"": ""0"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""depth"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Number of sub directories to watch. The higher the number, more memory/cpu/time will be required for watchers."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinish"", + ""label"": ""Await Write Finish"", + ""defaultValue"": ""True"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinish"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Trigger watcher events only when the file finishes writing."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinishStabilityThreshold"", + ""label"": ""Await Write Finish Stability (ms)"", + ""defaultValue"": ""2000"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinishStabilityThreshold"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Amount of time in milliseconds for a file size to remain constant before emitting its event."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinishPollInterval"", + ""label"": ""Await File Size Poll (ms)"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinishPollInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) File size polling interval."", + ""settingKey"": ""name"" + }, + { + ""name"": ""atomic"", + ""label"": ""Atomic Writes Threshold"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""atomic"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) Automatically filters out artifacts that occur within the defined value (in milliseconds) when using editors that use 'atomic writes' instead of writing directly to the source file. If defined to 0, this function is disabled."", + ""settingKey"": ""name"" + } + ], + ""connectingTimeout"": 30000, + ""setupTimeout"": 10000, + ""intervalBeforeReconnect"": 5000, + ""heartbeatInterval"": 60000, + ""___cmf___name"": ""Equipment Configuration"" + } + }, + { + ""id"": ""task_358"", + ""reference"": { + ""name"": ""driverCommand"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""command"": ""Connect"", + ""___cmf___description"": ""Connect"" + } + } + ], + ""converters"": [], + ""links"": [ + { + ""id"": ""task_356_success-task_357_activate"", + ""sourceId"": ""task_356"", + ""targetId"": ""task_357"", + ""inputName"": ""activate"", + ""outputName"": ""success"" + }, + { + ""id"": ""task_357_success-task_358_activate"", + ""sourceId"": ""task_357"", + ""targetId"": ""task_358"", + ""inputName"": ""activate"", + ""outputName"": ""success"" + } + ], + ""$id"": ""1"", + ""layout"": { + ""general"": { + ""color"": null, + ""notes"": [] + }, + ""drawers"": { + ""DIAGRAM"": { + ""tasks"": { + ""task_358"": { + ""collapsed"": false, + ""position"": { + ""x"": 1100, + ""y"": 100 + }, + ""outdated"": false + }, + ""task_356"": { + ""collapsed"": false, + ""position"": { + ""x"": 100, + ""y"": 100 + }, + ""outdated"": false + }, + ""task_357"": { + ""collapsed"": false, + ""position"": { + ""x"": 600, + ""y"": 100 + }, + ""outdated"": false + } + }, + ""links"": { + ""task_356_success-task_357_activate"": { + ""vertices"": [] + }, + ""task_357_success-task_358_activate"": { + ""vertices"": [] + } + }, + ""notes"": {}, + ""pan"": { + ""x"": 9.675338107580444, + ""y"": 3.532465519098835 + }, + ""zoom"": 0.78 + } + } + } + }") + }, + { "/test/Data/AutomationWorkflows/Test/Test2.json", new MockFileData( + @"{ + ""tasks"": [ + { + ""id"": ""task_536"", + ""reference"": { + ""name"": ""workflow"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-beta"" + } + }, + ""settings"": { + ""inputs"": [], + ""outputs"": [ + { + ""name"": ""activate"", + ""valueType"": { + ""friendlyName"": ""Activate"", + ""type"": null, + ""collectionType"": 0, + ""referenceType"": null, + ""referenceTypeName"": null, + ""referenceTypeId"": null + } + } + ], + ""retries"": 30, + ""contextsExpirationInMilliseconds"": 60000, + ""executionExpirationInMilliseconds"": 1200000, + ""executeWhenAllInputsDefined"": false, + ""automationWorkflow"": { + ""DisplayName"": ""workflow_544"", + ""IsShared"": false, + ""Name"": ""Test"" + }, + ""___cmf___name"": ""Call SubWorkflow"" + } + } + ], + ""converters"": [], + ""links"": [], + ""$id"": ""1"", + ""layout"": { + ""general"": { + ""color"": null, + ""notes"": [] + }, + ""drawers"": { + ""DIAGRAM"": { + ""tasks"": { + ""task_536"": { + ""collapsed"": false, + ""position"": { + ""x"": 559, + ""y"": 204 + }, + ""outdated"": false + }, + ""task_751"": { + ""collapsed"": false, + ""position"": { + ""x"": 1023, + ""y"": 172 + }, + ""outdated"": false + }, + ""task_762"": { + ""collapsed"": false, + ""position"": { + ""x"": 759, + ""y"": 404 + }, + ""outdated"": false + } + }, + ""links"": {}, + ""notes"": {}, + ""pan"": { + ""x"": 0, + ""y"": 3 + } + } + } + } + }") + } + }); + + BuildCommand buildCommand = new BuildCommand(fileSystem.FileSystem); + + var cmd = new Command("build"); + buildCommand.Configure(cmd); + + var console = new TestConsole(); + cmd.Invoke(new string[] { + "test/Data/" + }, console); + + Assert.True(!string.IsNullOrEmpty(console.Error.ToString()), $"Json Validator did not fail for IoT Data Workflow Package: {console.Error.ToString()}"); + Assert.True(console.Error.ToString().Contains("Could not find the path Error for the Workflow"), $"Json Validator did not fail for IoT Data Workflow Package: {console.Error.ToString()}"); + } + + [Fact] + public void Data_JsonValidator_FailPath_SubWorkflow_NotFoundWorkflow() + { + var fileSystem = new MockFileSystem(new Dictionary + { + { "/test/cmfpackage.json", new MockFileData( + @"{ + ""packageId"": ""Cmf.Custom.Package"", + ""version"": ""1.1.0"", + ""description"": ""This package deploys Critical Manufacturing Customization"", + ""packageType"": ""Root"", + ""isInstallable"": true, + ""isUniqueInstall"": false, + ""dependencies"": [ + { + ""id"": ""Cmf.Custom.Data"", + ""version"": ""1.1.0"" + }, + { + ""id"": ""Cmf.Custom.IoT"", + ""version"": ""1.1.0"" + } + ] + }") + }, + { "/test/Data/cmfpackage.json", new CmfMockJsonData( + @"{ + ""packageId"": ""Cmf.Custom.Data"", + ""version"": ""1.1.0"", + ""description"": ""Cmf Custom Data Package"", + ""packageType"": ""Data"", + ""isInstallable"": true, + ""isUniqueInstall"": true, + ""contentToPack"": [ + { + ""source"": ""MasterData/$(version)/*"", + ""target"": ""MasterData/$(version)/"", + ""contentType"": ""MasterData"" + }, + { + ""source"": ""AutomationWorkFlows/*"", + ""target"": ""AutomationWorkFlows"", + ""contentType"": ""AutomationWorkFlows"" + } + ] + }") + }, + { "/test/Data/MasterData/1.1.0/Test.json", new MockFileData( + @"{ + ""AutomationControllerWorkflow"": { + ""1"": { + ""AutomationController"": ""TestController"", + ""Name"": ""Test"", + ""DisplayName"": ""Test/Test.json"", + ""IsFile"": ""Yes"", + ""Workflow"": ""Error"", + ""Order"": ""1"" + }, + ""2"": { + ""AutomationController"": ""TestController"", + ""Name"": ""Test2"", + ""DisplayName"": ""Test2"", + ""IsFile"": ""Yes"", + ""IsFile"": ""Yes"", + ""Workflow"": ""Test2/Test2.json"", + ""Order"": ""2"" + } + } + }") + }, + { "/test/Data/AutomationWorkflows/Test/Test.json", new MockFileData( + @"{ + ""tasks"": [ + { + ""id"": ""task_356"", + ""reference"": { + ""name"": ""driverEvent"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""autoEnable"": true, + ""event"": ""OnInitialize"", + ""autoSetup"": true, + ""___cmf___description"": ""OnInitialize"" + } + }, + { + ""id"": ""task_357"", + ""reference"": { + ""name"": ""equipmentConfig"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""_inputs"": [ + { + ""name"": ""path"", + ""label"": ""Path"", + ""defaultValue"": ""c:/temp"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""path"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Path of the location to monitor for changes on the files. If no value is provided, no watcher is created, so no events will be triggered."", + ""settingKey"": ""name"" + }, + { + ""name"": ""fileMask"", + ""label"": ""FileMask"", + ""defaultValue"": ""**/*.request"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""fileMask"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Glob pattern to use for the watcher to identify the files to handle. Use a tool like https://globster.xyz/ to try a valid value to use."", + ""settingKey"": ""name"" + }, + { + ""name"": ""archivePath"", + ""label"": ""Archive Path"", + ""defaultValue"": """", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""archivePath"" + }, + ""dataType"": ""String"", + ""automationDataType"": 0, + ""referenceType"": 0, + ""description"": ""Directory planned to use for archiving the files after processing. This value can later be used as a variable in the available operations to execute."", + ""settingKey"": ""name"" + }, + { + ""name"": ""watcherType"", + ""label"": ""Watcher Type"", + ""defaultValue"": ""Chokidar"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""watcherType"" + }, + ""dataType"": ""Enum"", + ""automationDataType"": 0, + ""referenceType"": 1, + ""description"": ""Type of watcher to use. Depending on the selection, different settings are necessary. Chokidar is the best overall option, but CPU and Memory heavy and slower to start when many files are present. NSFW is better other scenarios. Test both to determine the best option."", + ""valueReferenceType"": 6, + ""settings"": { + ""enumValues"": [ + ""Chokidar"", + ""NSFW"" + ] + }, + ""settingKey"": ""name"" + }, + { + ""name"": ""ignoreInitial"", + ""label"": ""Ignore Existing Files"", + ""defaultValue"": ""False"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""ignoreInitial"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Flag to define if, during startup/restart, the files that were created, changed, deleted should be processed or ignored"", + ""settingKey"": ""name"" + }, + { + ""name"": ""watcherMode"", + ""label"": ""File Watcher Mode"", + ""defaultValue"": ""Polling"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""watcherMode"" + }, + ""dataType"": ""Enum"", + ""automationDataType"": 0, + ""referenceType"": 1, + ""description"": ""(Type=Chokidar) To successfully watch files over a network (and in other non-standard situations), it is typically necessary to use Polling, however it could lead to high CPU utilization. FileSystemEvents is the most efficient method for monitoring local files."", + ""valueReferenceType"": 6, + ""settings"": { + ""enumValues"": [ + ""FileSystemEvents"", + ""Polling"" + ] + }, + ""settingKey"": ""name"" + }, + { + ""name"": ""pollingInterval"", + ""label"": ""Polling Interval (ms)"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""pollingInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Interval of file system polling (in milliseconds)"", + ""settingKey"": ""name"" + }, + { + ""name"": ""pollingBinaryInterval"", + ""label"": ""Binary Files Polling Interval (ms)"", + ""defaultValue"": ""300"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""pollingBinaryInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) Interval of file system polling for binary files (in milliseconds)"", + ""settingKey"": ""name"" + }, + { + ""name"": ""alwaysStat"", + ""label"": ""Always Stat"", + ""defaultValue"": ""True"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""alwaysStat"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Always get additional attributes (size, timestamps, etc) of the file that was identified by the watcher. Will require additional operating system resources."", + ""settingKey"": ""name"" + }, + { + ""name"": ""depth"", + ""label"": ""Subdirectory Depth"", + ""defaultValue"": ""0"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""depth"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Number of sub directories to watch. The higher the number, more memory/cpu/time will be required for watchers."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinish"", + ""label"": ""Await Write Finish"", + ""defaultValue"": ""True"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinish"" + }, + ""dataType"": ""Boolean"", + ""automationDataType"": 8, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Trigger watcher events only when the file finishes writing."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinishStabilityThreshold"", + ""label"": ""Await Write Finish Stability (ms)"", + ""defaultValue"": ""2000"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinishStabilityThreshold"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar|NSFW) Amount of time in milliseconds for a file size to remain constant before emitting its event."", + ""settingKey"": ""name"" + }, + { + ""name"": ""awaitWriteFinishPollInterval"", + ""label"": ""Await File Size Poll (ms)"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""awaitWriteFinishPollInterval"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) File size polling interval."", + ""settingKey"": ""name"" + }, + { + ""name"": ""atomic"", + ""label"": ""Atomic Writes Threshold"", + ""defaultValue"": ""100"", + ""parameter"": { + ""$type"": ""Cmf.Foundation.BusinessObjects.AutomationProtocolParameter, Cmf.Foundation.BusinessObjects"", + ""Name"": ""atomic"" + }, + ""dataType"": ""Integer"", + ""automationDataType"": 5, + ""referenceType"": 0, + ""description"": ""(Type=Chokidar) Automatically filters out artifacts that occur within the defined value (in milliseconds) when using editors that use 'atomic writes' instead of writing directly to the source file. If defined to 0, this function is disabled."", + ""settingKey"": ""name"" + } + ], + ""connectingTimeout"": 30000, + ""setupTimeout"": 10000, + ""intervalBeforeReconnect"": 5000, + ""heartbeatInterval"": 60000, + ""___cmf___name"": ""Equipment Configuration"" + } + }, + { + ""id"": ""task_358"", + ""reference"": { + ""name"": ""driverCommand"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-202409173"" + } + }, + ""driver"": ""Handler"", + ""settings"": { + ""command"": ""Connect"", + ""___cmf___description"": ""Connect"" + } + } + ], + ""converters"": [], + ""links"": [ + { + ""id"": ""task_356_success-task_357_activate"", + ""sourceId"": ""task_356"", + ""targetId"": ""task_357"", + ""inputName"": ""activate"", + ""outputName"": ""success"" + }, + { + ""id"": ""task_357_success-task_358_activate"", + ""sourceId"": ""task_357"", + ""targetId"": ""task_358"", + ""inputName"": ""activate"", + ""outputName"": ""success"" + } + ], + ""$id"": ""1"", + ""layout"": { + ""general"": { + ""color"": null, + ""notes"": [] + }, + ""drawers"": { + ""DIAGRAM"": { + ""tasks"": { + ""task_358"": { + ""collapsed"": false, + ""position"": { + ""x"": 1100, + ""y"": 100 + }, + ""outdated"": false + }, + ""task_356"": { + ""collapsed"": false, + ""position"": { + ""x"": 100, + ""y"": 100 + }, + ""outdated"": false + }, + ""task_357"": { + ""collapsed"": false, + ""position"": { + ""x"": 600, + ""y"": 100 + }, + ""outdated"": false + } + }, + ""links"": { + ""task_356_success-task_357_activate"": { + ""vertices"": [] + }, + ""task_357_success-task_358_activate"": { + ""vertices"": [] + } + }, + ""notes"": {}, + ""pan"": { + ""x"": 9.675338107580444, + ""y"": 3.532465519098835 + }, + ""zoom"": 0.78 + } + } + } + }") + }, + { "/test/Data/AutomationWorkflows/Test/Test2.json", new MockFileData( + @"{ + ""tasks"": [ + { + ""id"": ""task_536"", + ""reference"": { + ""name"": ""workflow"", + ""package"": { + ""name"": ""@criticalmanufacturing/connect-iot-controller-engine-core-tasks"", + ""version"": ""11.1.0-beta"" + } + }, + ""settings"": { + ""inputs"": [], + ""outputs"": [ + { + ""name"": ""activate"", + ""valueType"": { + ""friendlyName"": ""Activate"", + ""type"": null, + ""collectionType"": 0, + ""referenceType"": null, + ""referenceTypeName"": null, + ""referenceTypeId"": null + } + } + ], + ""retries"": 30, + ""contextsExpirationInMilliseconds"": 60000, + ""executionExpirationInMilliseconds"": 1200000, + ""executeWhenAllInputsDefined"": false, + ""automationWorkflow"": { + ""DisplayName"": ""workflow_544"", + ""IsShared"": false, + ""Name"": ""Error"" + }, + ""___cmf___name"": ""Call SubWorkflow"" + } + } + ], + ""converters"": [], + ""links"": [], + ""$id"": ""1"", + ""layout"": { + ""general"": { + ""color"": null, + ""notes"": [] + }, + ""drawers"": { + ""DIAGRAM"": { + ""tasks"": { + ""task_536"": { + ""collapsed"": false, + ""position"": { + ""x"": 559, + ""y"": 204 + }, + ""outdated"": false + }, + ""task_751"": { + ""collapsed"": false, + ""position"": { + ""x"": 1023, + ""y"": 172 + }, + ""outdated"": false + }, + ""task_762"": { + ""collapsed"": false, + ""position"": { + ""x"": 759, + ""y"": 404 + }, + ""outdated"": false + } + }, + ""links"": {}, + ""notes"": {}, + ""pan"": { + ""x"": 0, + ""y"": 3 + } + } + } + } + }") + } + }); + + BuildCommand buildCommand = new BuildCommand(fileSystem.FileSystem); + + var cmd = new Command("build"); + buildCommand.Configure(cmd); + + var console = new TestConsole(); + cmd.Invoke(new string[] { + "test/Data/" + }, console); + + Assert.True(!string.IsNullOrEmpty(console.Error.ToString()), $"Json Validator did not fail for IoT Data Workflow Package: {console.Error.ToString()}"); + Assert.True(console.Error.ToString().Contains("The subworkflow Error is mentioned but there is no workflow declared with that name"), $"Json Validator did not fail for IoT Data Workflow Package: {console.Error.ToString()}"); + } + } }