diff --git a/step-automation-packages/pom.xml b/step-automation-packages/pom.xml index 06dd073971..53906c31fa 100644 --- a/step-automation-packages/pom.xml +++ b/step-automation-packages/pom.xml @@ -42,6 +42,7 @@ step-automation-packages-manager step-automation-packages-client step-automation-packages-controller + step-automation-packages-ide diff --git a/step-automation-packages/step-automation-packages-ide/pom.xml b/step-automation-packages/step-automation-packages-ide/pom.xml new file mode 100644 index 0000000000..db6d0906c9 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + ch.exense.step + step-automation-packages + 0.0.0-SNAPSHOT + + + step-automation-packages-ide + + + ch.exense.step + 21 + 21 + UTF-8 + + + + + ch.exense.step + step-plans-base-artefacts + ${project.version} + + + ch.exense.step + step-automation-packages-yaml + ${project.version} + + + + + org.mockito + mockito-core + test + + + ch.exense.step + step-plans-core + ${project.version} + test + + + ch.exense.step + step-functions-plugins-jmeter-def + ${project.version} + test + + + ch.exense.step + step-functions-plugins-node-def + ${project.version} + test + + + ch.exense.step + step-automation-packages-controller + ${project.version} + test + + + + \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java new file mode 100644 index 0000000000..e3be980294 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackageCollectionFactory.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (C) 2026, exense GmbH + * + * This file is part of STEP + * + * STEP is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * STEP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with STEP. If not, see . + ******************************************************************************/ +package step.core.collections; + +import java.io.IOException; +import java.util.Properties; + +import step.automation.packages.yaml.AutomationPackageYamlFragmentManager; +import step.core.collections.inmemory.InMemoryCollectionFactory; +import step.core.plans.Plan; + +public class AutomationPackageCollectionFactory implements CollectionFactory { + + private final InMemoryCollectionFactory baseFactory; + private final AutomationPackageYamlFragmentManager fragmentManager; + + public AutomationPackageCollectionFactory(Properties properties, AutomationPackageYamlFragmentManager fragmentManager) { + this.fragmentManager = fragmentManager; + this.baseFactory = new InMemoryCollectionFactory(properties); + } + + @Override + public Collection getCollection(String name, Class entityClass) { + + if (entityClass == Plan.class) { + return (Collection) new AutomationPackagePlanCollection(fragmentManager); + } + + return baseFactory.getCollection(name, entityClass); + } + + @Override + public Collection getVersionedCollection(String name) { + Collection baseCollection = baseFactory.getCollection(name, EntityVersion.class); + return baseCollection; + } + + @Override + public void close() throws IOException { + baseFactory.close(); + } +} diff --git a/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackagePlanCollection.java b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackagePlanCollection.java new file mode 100644 index 0000000000..3413981cbf --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/main/java/step/core/collections/AutomationPackagePlanCollection.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (C) 2026, exense GmbH + * + * This file is part of STEP + * + * STEP is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * STEP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with STEP. If not, see . + ******************************************************************************/ +package step.core.collections; + +import step.automation.packages.yaml.AutomationPackageYamlFragmentManager; +import step.core.collections.inmemory.InMemoryCollection; +import step.core.plans.Plan; + +public class AutomationPackagePlanCollection extends InMemoryCollection implements Collection { + + + private final AutomationPackageYamlFragmentManager fragmentManager; + + public AutomationPackagePlanCollection(AutomationPackageYamlFragmentManager fragmentManager) { + super(true, "plan"); + this.fragmentManager = fragmentManager; + super.save(fragmentManager.getPlans()); + } + + @Override + public Plan save(Plan p){ + return super.save(fragmentManager.savePlan(p)); + } + + @Override + public void save(Iterable iterable) { + for (Plan p : iterable) { + save(p); + } + } + + @Override + public void remove(Filter filter) { + find(filter, null, null, null, Integer.MAX_VALUE).forEach(fragmentManager::removePlan); + super.remove(filter); + } +} diff --git a/step-automation-packages/step-automation-packages-ide/src/test/java/step/core/collections/AutomationPackageCollectionTest.java b/step-automation-packages/step-automation-packages-ide/src/test/java/step/core/collections/AutomationPackageCollectionTest.java new file mode 100644 index 0000000000..b81809bd84 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/java/step/core/collections/AutomationPackageCollectionTest.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * Copyright (C) 2026, exense GmbH + * + * This file is part of STEP + * + * STEP is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * STEP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with STEP. If not, see . + ******************************************************************************/ +package step.core.collections; + +import ch.exense.commons.app.Configuration; +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import step.artefacts.Echo; +import step.automation.packages.AutomationPackageHookRegistry; +import step.automation.packages.AutomationPackageReadingException; +import step.automation.packages.JavaAutomationPackageReader; +import step.automation.packages.deserialization.AutomationPackageSerializationRegistry; +import step.automation.packages.yaml.AutomationPackageYamlFragmentManager; +import step.automation.packages.yaml.YamlAutomationPackageVersions; +import step.core.dynamicbeans.DynamicValue; +import step.core.plans.Plan; +import step.parameter.ParameterManager; +import step.parameter.automation.AutomationPackageParametersRegistration; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +public class AutomationPackageCollectionTest { + + private static final Logger log = LoggerFactory.getLogger(AutomationPackageCollectionTest.class); + + private final JavaAutomationPackageReader reader; + + private final File sourceDirectory = new File("src/test/resources/samples/step-automation-packages-sample1");; + private File destinationDirectory; + private Collection planCollection; + private final Path expectedFilesPath = sourceDirectory.toPath().resolve("expected"); + + public AutomationPackageCollectionTest() throws AutomationPackageReadingException { + AutomationPackageSerializationRegistry serializationRegistry = new AutomationPackageSerializationRegistry(); + AutomationPackageHookRegistry hookRegistry = new AutomationPackageHookRegistry(); + + // accessor is not required in this test - we only read the yaml and don't store the result anywhere + AutomationPackageParametersRegistration.registerParametersHooks(hookRegistry, serializationRegistry, Mockito.mock(ParameterManager.class)); + + this.reader = new JavaAutomationPackageReader(YamlAutomationPackageVersions.ACTUAL_JSON_SCHEMA_PATH, hookRegistry, serializationRegistry, new Configuration()); + } + + @Before + public void setUp() throws IOException, AutomationPackageReadingException { + Properties properties = new Properties(); + destinationDirectory = Files.createTempDirectory("automationPackageCollectionTest").toFile(); + FileUtils.copyDirectory(sourceDirectory, destinationDirectory); + + AutomationPackageYamlFragmentManager fragmentManager = reader.provideAutomationPackageYamlFragmentManager(destinationDirectory); + AutomationPackageCollectionFactory collectionFactory = new AutomationPackageCollectionFactory(properties, fragmentManager); + planCollection = collectionFactory.getCollection("plan", Plan.class); + } + + @After + public void tearDown() throws IOException { + FileUtils.deleteDirectory(destinationDirectory); + } + + @Test + public void testReadAllPlans() { + long count = planCollection.count(Filters.empty(), 100); + List plans = planCollection.find(Filters.empty(), null, null, null, 100).collect(Collectors.toList()); + + assertEquals(2, count); + Set names = plans.stream().map(p -> p.getAttributes().get("name")).collect(Collectors.toUnmodifiableSet()); + + assertEquals(2, names.size()); + + assertTrue(names.contains("Test Plan")); + assertTrue(names.contains("Test Plan with Composite")); + } + + @Test + public void testPlanModify() throws IOException { + Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst(); + + assertTrue(optionalPlan.isPresent()); + + Plan plan = optionalPlan.get(); + + Echo firstEcho = (Echo) plan.getRoot().getChildren().get(0); + DynamicValue text = firstEcho.getText(); + text.setDynamic(true); + text.setExpression("new Date().toString();"); + + planCollection.save(plan); + + assertFilesEqual(expectedFilesPath.resolve("plan1AfterModification.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml")); + } + + + @Test + public void testPlanRenameExisting() throws IOException { + Optional optionalPlan = planCollection.find(Filters.equals("attributes.name", "Test Plan"), null, null, null, 100).findFirst(); + + assertTrue(optionalPlan.isPresent()); + + Plan plan = optionalPlan.get(); + + plan.getAttributes().put("name", "New Plan Name"); + + planCollection.save(plan); + + assertFilesEqual(expectedFilesPath.resolve("plan1AfterRename.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml")); + } + + + @Test + public void testPlanRemoveExisting() throws IOException { + planCollection.remove(Filters.equals("attributes.name", "Test Plan")); + + assertFilesEqual(expectedFilesPath.resolve("plan1AfterRemove.yml"), destinationDirectory.toPath().resolve("plans").resolve("plan1.yml")); + } + + private void assertFilesEqual(Path expected, Path actual) throws IOException { + List expectedLines = Files.readAllLines(expected); + List actualLines = Files.readAllLines(actual); + + assertEquals(expectedLines, actualLines); + } +} diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/.apignore b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/.apignore new file mode 100644 index 0000000000..319325c32d --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/.apignore @@ -0,0 +1,2 @@ +/ignored +/ignoredFile.yml \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/automation-package.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/automation-package.yml new file mode 100644 index 0000000000..13510fa65a --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/automation-package.yml @@ -0,0 +1,9 @@ +schemaVersion: 1.0.0 +name: "My package" +fragments: + - "keywords.yml" + - "plans/*.yml" + - "schedules.yml" + - "parameters.yml" + - "parameters2.yml" + - "unknown.yml" \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModification.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModification.yml new file mode 100644 index 0000000000..642a8f1072 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterModification.yml @@ -0,0 +1,30 @@ +--- +fragments: [] +keywords: [] +plans: +- version: "1.2.0" + name: "Test Plan" + root: + testCase: + nodeName: "Test Plan" + children: + - echo: + text: + expression: "new Date().toString();" + - echo: + text: + expression: "mySimpleKey" + - callKeyword: + nodeName: "CallMyKeyword2" + inputs: + - myInput: "myValue" + keyword: "MyKeyword2" + agents: null + categories: + - "Yaml Plan" +plansPlainText: +- name: "Plain text plan" + rootType: "Sequence" + categories: + - "PlainTextPlan" + file: "plans/plan2.plan" diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRemove.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRemove.yml new file mode 100644 index 0000000000..af434784e2 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRemove.yml @@ -0,0 +1,10 @@ +--- +fragments: [] +keywords: [] +plans: [] +plansPlainText: +- name: "Plain text plan" + rootType: "Sequence" + categories: + - "PlainTextPlan" + file: "plans/plan2.plan" diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRename.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRename.yml new file mode 100644 index 0000000000..8cd5bc5d57 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/expected/plan1AfterRename.yml @@ -0,0 +1,29 @@ +--- +fragments: [] +keywords: [] +plans: +- version: "1.2.0" + name: "New Plan Name" + root: + testCase: + nodeName: "Test Plan" + children: + - echo: + text: "Just echo" + - echo: + text: + expression: "mySimpleKey" + - callKeyword: + nodeName: "CallMyKeyword2" + inputs: + - myInput: "myValue" + keyword: "MyKeyword2" + agents: null + categories: + - "Yaml Plan" +plansPlainText: +- name: "Plain text plan" + rootType: "Sequence" + categories: + - "PlainTextPlan" + file: "plans/plan2.plan" diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/ignoredFile.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/ignoredFile.yml new file mode 100644 index 0000000000..06f858207b --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/ignoredFile.yml @@ -0,0 +1 @@ +#I should be ignored \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/keywords.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/keywords.yml new file mode 100644 index 0000000000..5d33a14488 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/keywords.yml @@ -0,0 +1,30 @@ +keywords: + - JMeter: + name: "JMeter keyword from automation package" + description: "JMeter keyword 1" + executeLocally: false + useCustomTemplate: true + callTimeout: 1000 + jmeterTestplan: "jmeterProject1/jmeterProject1.xml" + - Composite: + name: "Composite keyword from AP" + plan: + root: + testCase: + children: + - echo: + text: "Just echo" + - return: + output: + - output1: "value" + - output2: + expression: "'some thing dynamic'" + - GeneralScript: + name: "GeneralScript keyword from AP" + scriptLanguage: javascript + scriptFile: "jsProject/jsSample.js" + librariesFile: "lib/fakeLib.jar" + - Node: + name: "NodeAutomation" + jsfile: "nodeProject/nodeSample.ts" + \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters.yml new file mode 100644 index 0000000000..587e90b308 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters.yml @@ -0,0 +1,14 @@ +parameters: + - key: myKey + value: myValue + description: some description + activationScript: abc + priority: 10 + protectedValue: true + scope: APPLICATION + scopeEntity: entity + - key: mySimpleKey + value: mySimpleValue + - key: myDynamicParam + value: + expression: "mySimpleKey" \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters2.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters2.yml new file mode 100644 index 0000000000..8754e85903 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/parameters2.yml @@ -0,0 +1,9 @@ +parameters: + - key: myKey2 + value: myValue2 + description: some description 2 + activationScript: abc + priority: 10 + protectedValue: true + scope: APPLICATION + scopeEntity: entity \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plan.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plan.plan new file mode 100644 index 0000000000..66b7ca6d84 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plan.plan @@ -0,0 +1,3 @@ +Sequence +Echo "Testing annotated plan" +End \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan1.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan1.yml new file mode 100644 index 0000000000..23db1ca1a7 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan1.yml @@ -0,0 +1,26 @@ +--- +fragments: [] +keywords: [] +plans: +- name: "Test Plan" + root: + testCase: + children: + - echo: + text: "Just echo" + - echo: + text: + expression: "mySimpleKey" + - callKeyword: + nodeName: "CallMyKeyword2" + inputs: + - myInput: "myValue" + keyword: "MyKeyword2" + categories: + - "Yaml Plan" +plansPlainText: +- name: "Plain text plan" + rootType: "Sequence" + categories: + - "PlainTextPlan" + file: "plans/plan2.plan" \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.plan new file mode 100644 index 0000000000..66b7ca6d84 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.plan @@ -0,0 +1,3 @@ +Sequence +Echo "Testing annotated plan" +End \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.yml new file mode 100644 index 0000000000..2a576d02da --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plans/plan2.yml @@ -0,0 +1,17 @@ +plans: + - name: Test Plan with Composite + categories: + - Yaml Plan + - Composite + root: + testCase: + children: + - echo: + text: "Calling composite" + - callKeyword: + keyword: "Composite keyword from AP" + children: + - check: + expression: "output.output1.equals('value')" + - check: + expression: "output.output2.equals('some thing dynamic')" \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/firstPlainText.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/firstPlainText.plan new file mode 100644 index 0000000000..e9dd736886 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/firstPlainText.plan @@ -0,0 +1,3 @@ +Sequence +Echo "First plain text plan" +End \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/secondPlainText.plan b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/secondPlainText.plan new file mode 100644 index 0000000000..00c3bacb0d --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/plansPlainText/secondPlainText.plan @@ -0,0 +1,3 @@ +Sequence +Echo "Second plain text plan" +End \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/schedules.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/schedules.yml new file mode 100644 index 0000000000..6d95ed7161 --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/schedules.yml @@ -0,0 +1,7 @@ +schedules: + - name: "firstSchedule" + cron: "0 15 10 ? * *" + cronExclusions: + - "0 0 9 25 * ?" + - "0 0 9 20 * ?" + planName: "Test Plan" \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/unknown.yml b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/unknown.yml new file mode 100644 index 0000000000..d378e6078d --- /dev/null +++ b/step-automation-packages/step-automation-packages-ide/src/test/resources/samples/step-automation-packages-sample1/unknown.yml @@ -0,0 +1,3 @@ +unknown: + - someFieldA: valueA + someFieldB: valueB \ No newline at end of file diff --git a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java index 37c29ec1a7..85d3f89a9f 100644 --- a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java +++ b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/AutomationPackageReader.java @@ -24,6 +24,7 @@ import step.automation.packages.model.ScriptAutomationPackageKeyword; import step.automation.packages.yaml.AutomationPackageDescriptorReader; import step.automation.packages.deserialization.AutomationPackageSerializationRegistry; +import step.automation.packages.yaml.AutomationPackageYamlFragmentManager; import step.automation.packages.yaml.model.AutomationPackageDescriptorYaml; import step.automation.packages.yaml.model.AutomationPackageFragmentYaml; import step.core.plans.Plan; @@ -98,23 +99,18 @@ public AutomationPackageContent readAutomationPackage(T automationPackageArchive * can be read as {@link step.engine.plugins.LocalFunctionPlugin.LocalFunction}, but not as {@link GeneralScriptFunction} * @param scanAnnotations true if it is required to include annotated java keywords and plans as well as located in yaml descriptor */ - public AutomationPackageContent readAutomationPackage(T automationPackageArchive, String apVersion, boolean isClasspathBased, boolean scanAnnotations) throws AutomationPackageReadingException { - try { - if (automationPackageArchive.hasAutomationPackageDescriptor()) { - try (InputStream yamlInputStream = automationPackageArchive.getDescriptorYaml()) { - AutomationPackageDescriptorYaml descriptorYaml = getOrCreateDescriptorReader().readAutomationPackageDescriptor(yamlInputStream, automationPackageArchive.getOriginalFileName()); - return buildAutomationPackage(descriptorYaml, automationPackageArchive, apVersion, isClasspathBased, scanAnnotations); - } - } else if (scanAnnotations) { - return buildAutomationPackage(null, automationPackageArchive, apVersion, isClasspathBased, scanAnnotations); - } else { - return null; - } - } catch (IOException ex) { - throw new AutomationPackageReadingException("Unable to read the automation package", ex); + public AutomationPackageContent readAutomationPackage(T archive, String apVersion, boolean isClasspathBased, boolean scanAnnotations) throws AutomationPackageReadingException { + if (archive.hasAutomationPackageDescriptor()) { + AutomationPackageDescriptorYaml descriptorYaml = getOrCreateDescriptorReader().readAutomationPackageDescriptor(archive.getDescriptorYamlUrl(), archive.getOriginalFileName()); + return buildAutomationPackage(descriptorYaml, archive, apVersion, isClasspathBased, scanAnnotations); + } else if (scanAnnotations) { + return buildAutomationPackage(null, archive, apVersion, isClasspathBased, scanAnnotations); + } else { + return null; } } + protected AutomationPackageContent buildAutomationPackage(AutomationPackageDescriptorYaml descriptor, T archive, String apVersion, boolean isClasspathBased, boolean scanAnnotations) throws AutomationPackageReadingException { AutomationPackageContent res = newContentInstance(); @@ -128,11 +124,13 @@ protected AutomationPackageContent buildAutomationPackage(AutomationPackageDescr // apply imported fragments recursively if (descriptor != null) { + readAutomationPackageYamlFragmentTree(archive, descriptor); fillAutomationPackageWithImportedFragments(res, descriptor, archive); } return res; } + private String resolveName(AutomationPackageDescriptorYaml descriptor, T archive) throws AutomationPackageReadingException { String finalName; if (descriptor != null) { @@ -179,27 +177,45 @@ protected AutomationPackageContent newContentInstance(){ abstract protected void fillAutomationPackageWithAnnotatedKeywordsAndPlans(T archive, boolean isClasspathBased, AutomationPackageContent res) throws AutomationPackageReadingException; - public void fillAutomationPackageWithImportedFragments(AutomationPackageContent targetPackage, AutomationPackageFragmentYaml fragment, T archive) throws AutomationPackageReadingException { - fillContentSections(targetPackage, fragment, archive); - if (!fragment.getFragments().isEmpty()) { - for (String importedFragmentReference : fragment.getFragments()) { + public AutomationPackageYamlFragmentManager provideAutomationPackageYamlFragmentManager(T archive) throws AutomationPackageReadingException { + AutomationPackageDescriptorReader reader = getOrCreateDescriptorReader(); + AutomationPackageDescriptorYaml descriptor = reader.readAutomationPackageDescriptor(archive.getDescriptorYamlUrl(), archive.getOriginalFileName()); + readAutomationPackageYamlFragmentTree(archive, descriptor); + return new AutomationPackageYamlFragmentManager(descriptor, getOrCreateDescriptorReader()); + } + + private void readAutomationPackageYamlFragmentTree(AutomationPackageArchive archive, AutomationPackageFragmentYaml parent) throws AutomationPackageReadingException { + + if (!parent.getFragments().isEmpty()) { + for (String importedFragmentReference : parent.getFragments()) { List resources = archive.getResourcesByPattern(importedFragmentReference); for (URL resource : resources) { - try (InputStream fragmentYamlStream = resource.openStream()) { - fragment = getOrCreateDescriptorReader().readAutomationPackageFragment(fragmentYamlStream, importedFragmentReference, archive.getOriginalFileName()); - fillAutomationPackageWithImportedFragments(targetPackage, fragment, archive); - } catch (IOException e) { - throw new AutomationPackageReadingException("Unable to read fragment in automation package: " + importedFragmentReference, e); - } + AutomationPackageFragmentYaml fragment = getOrCreateDescriptorReader().readAutomationPackageFragment(resource, archive.getOriginalFileName()); + fragment.setParent(parent); + parent.getChildren().add(fragment); + readAutomationPackageYamlFragmentTree(archive, fragment); } } } } + private void fillAutomationPackageWithImportedFragments(AutomationPackageContent targetPackage, AutomationPackageFragmentYaml fragment, T archive) throws AutomationPackageReadingException { + fillContentSections(targetPackage, fragment, archive); + + for (AutomationPackageFragmentYaml child: fragment.getChildren()) { + fillAutomationPackageWithImportedFragments(targetPackage, child, archive); + } + } + protected void fillContentSections(AutomationPackageContent targetPackage, AutomationPackageFragmentYaml fragment, T archive) throws AutomationPackageReadingException { targetPackage.getKeywords().addAll(fragment.getKeywords()); - targetPackage.getPlans().addAll(fragment.getPlans().stream().map(p -> getOrCreateDescriptorReader().getPlanReader().yamlPlanToPlan(p)).collect(Collectors.toList())); + targetPackage.getPlans().addAll(fragment.getPlans().stream().map(p -> { + Plan plan = getOrCreateDescriptorReader().getPlanReader().yamlPlanToPlan(p); + plan.getAttributes().put("fragmentUrl", fragment.getFragmentUrl().toString()); + plan.getAttributes().put("nameInYaml", p.getName()); + return plan; + }).collect(Collectors.toList())); readPlainTextPlans(targetPackage, fragment, archive); diff --git a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java index 2114924926..88c02feebe 100644 --- a/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java +++ b/step-automation-packages/step-automation-packages-manager/src/main/java/step/automation/packages/JavaAutomationPackageReader.java @@ -4,6 +4,7 @@ import org.apache.commons.lang3.StringUtils; import step.automation.packages.deserialization.AutomationPackageSerializationRegistry; import step.automation.packages.model.ScriptAutomationPackageKeyword; +import step.automation.packages.yaml.AutomationPackageYamlFragmentManager; import step.core.accessors.AbstractOrganizableObject; import step.core.dynamicbeans.DynamicValue; import step.core.plans.Plan; @@ -22,7 +23,10 @@ import step.plugins.functions.types.CompositeFunctionUtils; import step.plugins.java.GeneralScriptFunction; -import java.io.*; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; @@ -253,4 +257,17 @@ public AutomationPackageContent readAutomationPackageFromJarFile(File automation throw new AutomationPackageReadingException("IO Exception", e); } } + + /** Convenient method for test + * @param automationPackage the JAR file to be read + * @return the automation package content raed from the provided files + * @throws AutomationPackageReadingException in case of error + */ + public AutomationPackageYamlFragmentManager provideAutomationPackageYamlFragmentManager(File automationPackage) throws AutomationPackageReadingException { + try (JavaAutomationPackageArchive automationPackageArchive = new JavaAutomationPackageArchive(automationPackage, null, null)) { + return provideAutomationPackageYamlFragmentManager(automationPackageArchive); + } catch (IOException e) { + throw new AutomationPackageReadingException("IO Exception", e); + } + } } diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java index 968500bafd..7a65211340 100644 --- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java +++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageDescriptorReader.java @@ -42,6 +42,7 @@ import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -70,25 +71,35 @@ public AutomationPackageDescriptorReader(String jsonSchemaPath, AutomationPackag } } - public AutomationPackageDescriptorYaml readAutomationPackageDescriptor(InputStream yamlDescriptor, String packageFileName) throws AutomationPackageReadingException { + public AutomationPackageDescriptorYaml readAutomationPackageDescriptor(URL resource, String packageFileName) throws AutomationPackageReadingException { log.info("Reading automation package descriptor..."); - return readAutomationPackageYamlFile(yamlDescriptor, getDescriptorClass(), packageFileName); + return readAutomationPackageYamlFile(resource, getDescriptorClass(), packageFileName); + } + + public AutomationPackageDescriptorYaml readAutomationPackageDescriptor(InputStream yaml, String packageFileName) throws AutomationPackageReadingException { + log.info("Reading automation package descriptor..."); + return readAutomationPackageYamlFile(yaml, getDescriptorClass(), packageFileName); } protected Class getDescriptorClass() { return AutomationPackageDescriptorYamlImpl.class; } - public AutomationPackageFragmentYaml readAutomationPackageFragment(InputStream yamlFragment, String fragmentName, String packageFileName) throws AutomationPackageReadingException { + public AutomationPackageFragmentYaml readAutomationPackageFragment(URL resource, String packageFileName) throws AutomationPackageReadingException { + log.info("Reading automation package descriptor fragment ({})...", resource); + return readAutomationPackageYamlFile(resource, getFragmentClass(), packageFileName); + } + + public AutomationPackageFragmentYaml readAutomationPackageFragment(InputStream yaml, String fragmentName, String packageFileName) throws AutomationPackageReadingException { log.info("Reading automation package descriptor fragment ({})...", fragmentName); - return readAutomationPackageYamlFile(yamlFragment, getFragmentClass(), packageFileName); + return readAutomationPackageYamlFile(yaml, getFragmentClass(), packageFileName); } protected Class getFragmentClass() { return AutomationPackageFragmentYamlImpl.class; } - protected T readAutomationPackageYamlFile(InputStream yaml, Class targetClass, String packageFileName) throws AutomationPackageReadingException { + private T readAutomationPackageYamlFile(InputStream yaml, Class targetClass, String packageFileName) throws AutomationPackageReadingException { try { String yamlDescriptorString = new String(yaml.readAllBytes(), StandardCharsets.UTF_8); String version = null; @@ -108,6 +119,17 @@ protected T readAutomationPackageYamlF T res = yamlObjectMapper.reader().withAttribute("version", version).readValue(yamlDescriptorString, targetClass); logAfterRead(packageFileName, res); + + return res; + } catch (IOException | YamlPlanValidationException e) { + throw new AutomationPackageReadingException("Unable to read the automation package yaml. Caused by: " + e.getMessage(), e); + } + } + + private T readAutomationPackageYamlFile(URL resource, Class targetClass, String packageFileName) throws AutomationPackageReadingException { + try (InputStream yaml = resource.openStream()) { + T res = readAutomationPackageYamlFile(yaml, targetClass, packageFileName); + res.setFragmentUrl(resource); return res; } catch (IOException | YamlPlanValidationException e) { throw new AutomationPackageReadingException("Unable to read the automation package yaml. Caused by: " + e.getMessage(), e); @@ -143,7 +165,7 @@ protected String readJsonSchema(String jsonSchemaPath) { } } - protected ObjectMapper createYamlObjectMapper() { + private ObjectMapper createYamlObjectMapper() { YAMLFactory yamlFactory = new YAMLFactory(); // Disable native type id to enable conversion to generic Documents @@ -169,7 +191,11 @@ protected ObjectMapper createYamlObjectMapper() { return yamlMapper; } - public YamlPlanReader getPlanReader(){ + public ObjectMapper getYamlObjectMapper() { + return yamlObjectMapper; + } + + public YamlPlanReader getPlanReader() { return this.planReader; } diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java new file mode 100644 index 0000000000..803fbfdfee --- /dev/null +++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/AutomationPackageYamlFragmentManager.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * Copyright (C) 2026, exense GmbH + * + * This file is part of STEP + * + * STEP is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * STEP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with STEP. If not, see . + ******************************************************************************/ +package step.automation.packages.yaml; + +import com.fasterxml.jackson.core.exc.StreamWriteException; +import com.fasterxml.jackson.databind.DatabindException; +import org.yaml.snakeyaml.Yaml; +import step.automation.packages.yaml.model.AutomationPackageDescriptorYaml; +import step.automation.packages.yaml.model.AutomationPackageFragmentYaml; +import step.core.plans.Plan; +import step.plans.parser.yaml.YamlPlan; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class AutomationPackageYamlFragmentManager { + + + private final AutomationPackageDescriptorReader descriptorReader; + + private final Map planToYamlPlan = new ConcurrentHashMap<>(); + private final Map planToYamlFragment = new ConcurrentHashMap<>(); + + public AutomationPackageYamlFragmentManager(AutomationPackageDescriptorYaml descriptorYaml, AutomationPackageDescriptorReader descriptorReader) { + + this.descriptorReader = descriptorReader; + + initializeMaps(descriptorYaml); + } + + public void initializeMaps(AutomationPackageFragmentYaml fragment) { + for (YamlPlan p: fragment.getPlans()) { + Plan plan = descriptorReader.getPlanReader().yamlPlanToPlan(p); + planToYamlPlan.put(plan, p); + planToYamlFragment.put(plan, fragment); + }; + + for (AutomationPackageFragmentYaml child : fragment.getChildren()) { + initializeMaps(child); + } + } + + public Iterable getPlans() { + return planToYamlPlan.keySet(); + } + + public Plan savePlan(Plan p) { + YamlPlan newYamlPlan = descriptorReader.getPlanReader().planToYamlPlan(p); + + AutomationPackageFragmentYaml fragment = planToYamlFragment.get(p); + if (fragment == null) { + fragment = newFragmentForPlan(p); + fragment.getPlans().add(newYamlPlan); + } else { + YamlPlan yamlPlan = planToYamlPlan.get(p); + fragment.getPlans().replaceAll(plan -> plan == yamlPlan ? newYamlPlan : plan); + } + + planToYamlPlan.put(p, newYamlPlan); + writeFragment(fragment); + return p; + } + + private AutomationPackageFragmentYaml newFragmentForPlan(Plan p) { + + throw new UnsupportedOperationException("new Plan creation not yet supported in IDE"); + /* + try { + File file = new File(descriptorYaml.getFragmentUrl().toURI()); + + Path file.toPath().getParent().resolveSibling(getRelativePathForNewPlan(p)); + + } catch (URISyntaxException e) { + throw new RuntimeException(e); + }*/ + } + + public void removePlan(Plan p) { + AutomationPackageFragmentYaml fragment = planToYamlFragment.get(p); + YamlPlan yamlPlan = planToYamlPlan.get(p); + + fragment.getPlans().remove(yamlPlan); + + planToYamlPlan.remove(p); + planToYamlFragment.remove(p); + + writeFragment(fragment); + } + + private void writeFragment(AutomationPackageFragmentYaml fragment) { + try { + File file = new File(fragment.getFragmentUrl().toURI()); + descriptorReader.getYamlObjectMapper().writeValue(file, fragment); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java index 022ce163b5..82b486e780 100644 --- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java +++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AbstractAutomationPackageFragmentYaml.java @@ -25,7 +25,9 @@ import step.plans.automation.YamlPlainTextPlan; import step.plans.parser.yaml.YamlPlan; +import java.net.URL; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -38,6 +40,15 @@ public abstract class AbstractAutomationPackageFragmentYaml implements Automatio @JsonIgnore private Map> additionalFields; + @JsonIgnore + private URL url; + + @JsonIgnore + private List children = new LinkedList<>(); + + @JsonIgnore + private AutomationPackageFragmentYaml parent; + @Override public List getKeywords() { return keywords; @@ -86,4 +97,29 @@ public List getPlansPlainText() { public void setPlansPlainText(List plansPlainText) { this.plansPlainText = plansPlainText; } + + @JsonIgnore + public void setFragmentUrl(URL url) { + this.url = url; + } + + @JsonIgnore + public URL getFragmentUrl() { + return url; + } + + @JsonIgnore + public List getChildren() { + return children; + } + + @JsonIgnore + public AutomationPackageFragmentYaml getParent() { + return parent; + } + + @JsonIgnore + public void setParent(AutomationPackageFragmentYaml parent) { + this.parent = parent; + } } diff --git a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java index a141f5e090..e9ad660af9 100644 --- a/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java +++ b/step-automation-packages/step-automation-packages-yaml/src/main/java/step/automation/packages/yaml/model/AutomationPackageFragmentYaml.java @@ -22,6 +22,7 @@ import step.plans.automation.YamlPlainTextPlan; import step.plans.parser.yaml.YamlPlan; +import java.net.URL; import java.util.List; import java.util.Map; @@ -40,4 +41,14 @@ public interface AutomationPackageFragmentYaml { default List getAdditionalField(String k) { return (List) getAdditionalFields().get(k); } + + URL getFragmentUrl(); + + void setFragmentUrl(URL url); + + List getChildren(); + + AutomationPackageFragmentYaml getParent(); + + void setParent(AutomationPackageFragmentYaml parent); } diff --git a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java index da33cac6c0..1dd6326f8b 100644 --- a/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java +++ b/step-core/src/main/java/step/automation/packages/AutomationPackageArchive.java @@ -68,6 +68,8 @@ public String getAutomationPackageName() { abstract public boolean hasAutomationPackageDescriptor(); + abstract public URL getDescriptorYamlUrl(); + abstract public InputStream getDescriptorYaml(); abstract public InputStream getResourceAsStream(String resourcePath) throws IOException; diff --git a/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java b/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java index 6cfe9f89a1..4cddc605dc 100644 --- a/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java +++ b/step-core/src/main/java/step/automation/packages/JavaAutomationPackageArchive.java @@ -102,6 +102,17 @@ public boolean hasAutomationPackageDescriptor() { return false; } + @Override + public URL getDescriptorYamlUrl() { + for (String metadataFile : METADATA_FILES) { + URL yamlDescriptor = classLoaderForMainApFile.getResource(metadataFile); + if (yamlDescriptor != null) { + return yamlDescriptor; + } + } + return null; + } + @Override public InputStream getDescriptorYaml() { for (String metadataFile : METADATA_FILES) { diff --git a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java index 25a19a1b8e..bfa9ffbae9 100644 --- a/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java +++ b/step-plans/step-plans-yaml-parser/src/main/java/step/plans/parser/yaml/YamlPlanReader.java @@ -271,7 +271,7 @@ public Plan yamlPlanToPlan(YamlPlan yamlPlan) { return plan; } - protected YamlPlan planToYamlPlan(Plan plan){ + public YamlPlan planToYamlPlan(Plan plan){ YamlPlan yamlPlan = new YamlPlan(); yamlPlan.setName(plan.getAttribute(AbstractOrganizableObject.NAME)); yamlPlan.setVersion(currentVersion.toString());