diff --git a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/BootService.java b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/BootService.java index 166c128..cc92e1f 100644 --- a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/BootService.java +++ b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/BootService.java @@ -9,7 +9,6 @@ package eu.nebulous.ems.boot; import com.fasterxml.jackson.databind.ObjectMapper; -import eu.nebulouscloud.exn.core.Publisher; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -18,9 +17,11 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; @Slf4j @Service @@ -35,7 +36,7 @@ public void afterPropertiesSet() throws Exception { log.info("EMS Boot Service: {}", properties.isEnabled() ? "enabled" : "disabled"); } - void processEmsBootMessage(Command command, String appId, Publisher emsBootResponsePublisher) throws IOException { + String processEmsBootMessage(Command command, String appId, BiConsumer emsBootResponsePublisher) throws IOException { // Process EMS Boot message log.info("Received a new EMS Boot message from external broker: {}", command.body()); @@ -43,12 +44,13 @@ void processEmsBootMessage(Command command, String appId, Publisher emsBootRespo Map entry = indexService.getFromIndex(appId); log.debug("Index entry for app-id: {}, entry: {}", appId, entry); if (entry==null) { - log.warn("No EMS Boot entry found for app-id: {}", appId); - return; + String msg = "No EMS Boot entry found for app-id: " + appId; + log.warn(msg); + return "ERROR "+msg; } String modelFile = entry.get(ModelsService.MODEL_FILE_KEY); String bindingsFile = entry.get(ModelsService.BINDINGS_FILE_KEY); - String solutionFile = entry.get(ModelsService.SOLUTIONS_FILE_KEY); + String solutionFile = entry.get(ModelsService.SOLUTION_FILE_KEY); String optimiserMetricsFile = entry.get(ModelsService.OPTIMISER_METRICS_FILE_KEY); log.info(""" Received EMS Boot request: @@ -60,20 +62,24 @@ void processEmsBootMessage(Command command, String appId, Publisher emsBootRespo """, appId, modelFile, bindingsFile, solutionFile, optimiserMetricsFile); if (StringUtils.isAnyBlank(appId, modelFile, bindingsFile, optimiserMetricsFile)) { - log.warn("Missing info in EMS Boot entry for app-id: {}", appId); - return; + String msg = "Missing info in EMS Boot entry for app-id: " + appId; + log.warn(msg); + return "ERROR "+msg; } - String modelStr = Files.readString(Paths.get(properties.getModelsDir(), modelFile)); + String modelStr = readFileContentsSafe(modelFile); log.debug("Model file contents:\n{}", modelStr); - String bindingsStr = Files.readString(Paths.get(properties.getModelsDir(), bindingsFile)); - Map bindingsMap = objectMapper.readValue(bindingsStr, Map.class); + + String bindingsStr = readFileContentsSafe(bindingsFile); + Map bindingsMap = bindingsStr!=null ? objectMapper.readValue(bindingsStr, Map.class) : Map.of(); log.debug("Bindings file contents:\n{}", bindingsMap); - String solutionStr = Files.readString(Paths.get(properties.getModelsDir(), solutionFile)); - Map solutionMap = objectMapper.readValue(solutionStr, Map.class); + + String solutionStr = readFileContentsSafe(solutionFile); + Map solutionMap = solutionStr!=null ? objectMapper.readValue(solutionStr, Map.class) : Map.of(); log.debug("Solution file contents:\n{}", solutionMap); - String metricsStr = Files.readString(Paths.get(properties.getModelsDir(), optimiserMetricsFile)); - List metricsList = objectMapper.readValue(metricsStr, List.class); + + String metricsStr = readFileContentsSafe(optimiserMetricsFile); + List metricsList = metricsStr!=null ? objectMapper.readValue(metricsStr, List.class) : List.of(); log.debug("Optimiser Metrics file contents:\n{}", metricsList); // Send EMS Boot response message @@ -84,7 +90,26 @@ void processEmsBootMessage(Command command, String appId, Publisher emsBootRespo "solution", solutionMap, "optimiser-metrics", metricsList ); - emsBootResponsePublisher.send(message, appId); + log.debug("EMS Boot response message: {}", message); + emsBootResponsePublisher.accept(message, appId); log.info("EMS Boot response sent"); + return "OK"; + } + + protected String readFileContentsSafe(String file) { + if (StringUtils.isBlank(file)) return null; + Path path = Paths.get(properties.getModelsDir(), file); + if (! path.toFile().exists()) { + log.warn("File not found in models dir.: {}", file); + return null; + } + + try { + String contents = Files.readString(path); + return contents; + } catch (Exception e) { + log.warn("Error while reading file: {}\n", file, e); + return null; + } } } \ No newline at end of file diff --git a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/IndexService.java b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/IndexService.java index b3ab0d9..c70f255 100644 --- a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/IndexService.java +++ b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/IndexService.java @@ -155,7 +155,7 @@ public Map getAppBindings(@NonNull String appId) throws IOExcepti } public Map getAppSolution(@NonNull String appId) throws IOException { - String fileName = getAppData(appId).get(ModelsService.SOLUTIONS_FILE_KEY); + String fileName = getAppData(appId).get(ModelsService.SOLUTION_FILE_KEY); String solutionStr = applicationContext.getBean(ModelsService.class).readFromFile(fileName); return objectMapper.readValue(solutionStr, Map.class); } @@ -268,14 +268,16 @@ public synchronized boolean deleteAppsBefore(Instant before, boolean deleteFiles public synchronized void addAppData(String appId) throws IOException { long ts = Instant.now().toEpochMilli(); - String modelStr, bindingsStr, metricsStr; + String modelStr, bindingsStr, solutionStr, metricsStr; Map data = Map.of( - "model-file", modelStr = appId + "--model--" + ts + ".yml", - "bindings-file", bindingsStr = appId + "--bindings--" + ts + ".json", - "optimiser-metrics-file", metricsStr = appId + "--metrics--" + ts + ".json" + ModelsService.MODEL_FILE_KEY, modelStr = appId + "--model--" + ts + ".yml", + ModelsService.BINDINGS_FILE_KEY, bindingsStr = appId + "--bindings--" + ts + ".json", + ModelsService.SOLUTION_FILE_KEY, solutionStr = appId + "--solution--" + ts + ".json", + ModelsService.OPTIMISER_METRICS_FILE_KEY, metricsStr = appId + "--metrics--" + ts + ".json" ); Files.writeString(Paths.get(properties.getModelsDir(), modelStr), "{}"); Files.writeString(Paths.get(properties.getModelsDir(), bindingsStr), "{}"); + Files.writeString(Paths.get(properties.getModelsDir(), solutionStr), "{}"); Files.writeString(Paths.get(properties.getModelsDir(), metricsStr), "[]"); storeToIndex(appId, data); } diff --git a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/ModelsService.java b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/ModelsService.java index 94e8eed..143e589 100644 --- a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/ModelsService.java +++ b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/ModelsService.java @@ -26,7 +26,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; @Slf4j @@ -35,7 +34,7 @@ public class ModelsService implements InitializingBean { final static String MODEL_FILE_KEY = "model-file"; final static String BINDINGS_FILE_KEY = "bindings-file"; - final static String SOLUTIONS_FILE_KEY = "solutions-file"; + final static String SOLUTION_FILE_KEY = "solution-file"; final static String OPTIMISER_METRICS_FILE_KEY = "optimiser-metrics-file"; public final static String SIMPLE_BINDING_KEY = "simple-bindings"; @@ -140,7 +139,12 @@ String extractBindings(Command command, String appId) throws IOException { // Store bindings in models store String bindingsFile = getFileName("bindings", appId, "json"); - if (bindingsMap==null) bindingsMap = Map.of(); + if (bindingsMap==null) { + bindingsMap = Map.of( + SIMPLE_BINDING_KEY, Map.of(), + COMPOSITE_BINDING_KEY, Map.of() + ); + } storeToFile(bindingsFile, objectMapper.writeValueAsString(bindingsMap)); log.info("Stored bindings in file: app-id={}, file={}", appId, bindingsFile); @@ -232,7 +236,7 @@ String extractSolution(Command command, String appId) throws IOException { log.info("Stored solution in file: app-id={}, file={}", appId, solutionFile); // Add appId-solutionFile entry in the stored Index - indexService.storeToIndex(appId, Map.of(SOLUTIONS_FILE_KEY, solutionFile)); + indexService.storeToIndex(appId, Map.of(SOLUTION_FILE_KEY, solutionFile)); return "OK"; } diff --git a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/NebulousEventsService.java b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/NebulousEventsService.java index 05ba3ad..6c49603 100644 --- a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/NebulousEventsService.java +++ b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/boot/NebulousEventsService.java @@ -166,7 +166,7 @@ private void processMessage(Command command) throws ClientException, IOException sendResponse(modelsResponsePublisher, appId, result); } else if (properties.getEmsBootTopic().equals(command.address())) { - bootService.processEmsBootMessage(command, appId, emsBootResponsePublisher); + bootService.processEmsBootMessage(command, appId, (message, app_id) -> emsBootResponsePublisher.send(message, app_id)); } else log.error("ERROR: Received message from unexpected topic: {}", command.address()); } diff --git a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/service/MvvService.java b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/service/MvvService.java index 4f6cbd9..04750f0 100644 --- a/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/service/MvvService.java +++ b/nebulous/ems-nebulous/src/main/java/eu/nebulous/ems/service/MvvService.java @@ -70,52 +70,66 @@ public boolean isEmpty() { } public void translateAndSetValues(Map varValues) { - log.info("MvvService.translateAndSetValues: New Variable Values: {}", varValues); - Map newValues = new HashMap<>(); - - // Process simple constants - final Map simpleBindings = bindings.get(ModelsService.SIMPLE_BINDING_KEY); - varValues.forEach((k,v) -> { - String constName = simpleBindings.get(k); - if (StringUtils.isNotBlank(constName)) { - if (v instanceof Number n) - newValues.put(constName, n.doubleValue()); - else - throw new IllegalArgumentException("Solution variable value is not a number: "+v); - log.trace("MvvService.translateAndSetValues: Added Simple Constant value: {} = {}", constName, n); - } else - log.warn("MvvService.translateAndSetValues: No Constant found for Solution variable: {}", k); - }); - log.debug("MvvService.translateAndSetValues: Simple Constant values: {}", newValues); - - // Process composite constants - Map pending = new HashMap<>(bindings.get(ModelsService.COMPOSITE_BINDING_KEY)); - @NotNull final Map varValues1 = varValues.entrySet().stream().collect(Collectors.toMap( - Map.Entry::getKey, e -> ((Number) e.getValue()).doubleValue() - )); - while (! pending.isEmpty()) { - Map newPending = new HashMap<>(); - pending.forEach((formula, constName) -> { - if (StringUtils.isNotBlank(formula) && StringUtils.isNotBlank(constName)) { - try { - log.trace("MvvService.translateAndSetValues: Calculating Composite Constant value: {} ::= {} -- Values: {}", constName, formula, newValues); - Double formulaValue = MathUtil.eval(formula, varValues1); - newValues.put(constName, formulaValue); - log.trace("MvvService.translateAndSetValues: Added Composite Constant value: {} = {}", constName, formulaValue); - } catch (Exception e) { - log.trace("MvvService.translateAndSetValues: Could not calculate Composite Constant value: {} ::= {} -- Values: {} -- Reason: {}", constName, formula, newValues, e.getMessage()); - newPending.put(formula, constName); + // Check if bindings are available + if (bindings == null || bindings.isEmpty()) { + log.error("MvvService.translateAndSetValues: No bindings provided: {}", bindings); + return; + } + Map simpleBindings = bindings.get(ModelsService.SIMPLE_BINDING_KEY); + Map compositeBindings = bindings.get(ModelsService.COMPOSITE_BINDING_KEY); + if (simpleBindings==null || simpleBindings.isEmpty()) { + log.error("MvvService.translateAndSetValues: No simple bindings provided: {}", bindings); + return; + } + + // Log new values + log.info("MvvService.translateAndSetValues: New Variable Values: {}", varValues); + Map newValues = new HashMap<>(); + + // Process simple constants + varValues.forEach((k, v) -> { + String constName = simpleBindings.get(k); + if (StringUtils.isNotBlank(constName)) { + if (v instanceof Number n) + newValues.put(constName, n.doubleValue()); + else + throw new IllegalArgumentException("Solution variable value is not a number: " + v); + log.trace("MvvService.translateAndSetValues: Added Simple Constant value: {} = {}", constName, n); + } else + log.warn("MvvService.translateAndSetValues: No Constant found for Solution variable: {}", k); + }); + log.debug("MvvService.translateAndSetValues: Simple Constant values: {}", newValues); + + // Process composite constants + if (compositeBindings!=null && ! compositeBindings.isEmpty()) { + Map pending = new HashMap<>(bindings.get(ModelsService.COMPOSITE_BINDING_KEY)); + @NotNull final Map varValues1 = varValues.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, e -> ((Number) e.getValue()).doubleValue() + )); + while (!pending.isEmpty()) { + Map newPending = new HashMap<>(); + pending.forEach((formula, constName) -> { + if (StringUtils.isNotBlank(formula) && StringUtils.isNotBlank(constName)) { + try { + log.trace("MvvService.translateAndSetValues: Calculating Composite Constant value: {} ::= {} -- Values: {}", constName, formula, newValues); + Double formulaValue = MathUtil.eval(formula, varValues1); + newValues.put(constName, formulaValue); + log.trace("MvvService.translateAndSetValues: Added Composite Constant value: {} = {}", constName, formulaValue); + } catch (Exception e) { + log.trace("MvvService.translateAndSetValues: Could not calculate Composite Constant value: {} ::= {} -- Values: {} -- Reason: {}", constName, formula, newValues, e.getMessage()); + newPending.put(formula, constName); + } } - } - }); - if (pending.size()==newPending.size()) - throw new IllegalArgumentException("Composite Constants cannot be calculated. Check for missing terms or circles in formulas: "+pending); - pending = newPending; + }); + if (pending.size() == newPending.size()) + throw new IllegalArgumentException("Composite Constants cannot be calculated. Check for missing terms or circles in formulas: " + pending); + pending = newPending; + } } - log.info("MvvService.translateAndSetValues: New Constant values: {}", newValues); + log.info("MvvService.translateAndSetValues: New Constant values: {}", newValues); - setControlServiceConstants(newValues); - } + setControlServiceConstants(newValues); + } private void setControlServiceConstants(@NonNull Map newValues) { this.values = newValues; diff --git a/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/AbstractBaseTest.java b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/AbstractBaseTest.java new file mode 100644 index 0000000..7f4d77a --- /dev/null +++ b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/AbstractBaseTest.java @@ -0,0 +1,183 @@ +package eu.nebulous.ems; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import eu.nebulous.ems.boot.EmsBootProperties; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.TestInstance; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractBaseTest { + + public static final String COLOR_WHITE = "\u001b[1;97m"; + public static final String COLOR_GREEN = "\u001b[32m"; + public static final String COLOR_YELLOW = "\u001b[33m"; + public static final String COLOR_RED = "\u001b[31m"; + public static final String COLOR_BLUE = "\u001b[34m"; + public static final String COLOR_MAGENTA = "\u001b[35m"; + public static final String COLOR_GREEN_INTENSE = "\u001b[1m\u001b[4m\u001b[92m"; + public static final String COLOR_RED_INTENSE = "\u001b[1m\u001b[4m\u001b[91m"; + public static final String COLOR_RESET = "\u001b[0m"; + + public static final String NO_RESULTS_COLORED = color("(no result returned)", "WARN"); + + protected String callerClass = getClass().getSimpleName(); + protected ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); + protected ObjectMapper objectMapper = new ObjectMapper(); + + protected static LinkedHashMap> globalResults = new LinkedHashMap<>(); + protected static boolean printGlobalResultsSummary = true; + protected LinkedHashMap results = new LinkedHashMap<>(); + protected boolean printResultsSummary = false; + + @FunctionalInterface + public interface CheckedBiFunction { + R apply(T t, U u) throws Exception; + } + + public AbstractBaseTest() { + globalResults.put(getClass().getSimpleName(), results); + } + + protected EmsBootProperties initializeEmsBootProperties() { + EmsBootProperties properties = new EmsBootProperties(); + properties.setModelsDir("target/nebulous-tests/"+getClass().getSimpleName()); + Paths.get(properties.getModelsDir()).toFile().mkdirs(); + properties.setModelsIndexFile(properties.getModelsDir()+"/index.json"); + return properties; + } + + protected Map parserYaml(@NonNull String testsFile) throws IOException { + try (InputStream inputStream = new FileInputStream(testsFile)) { + return yamlMapper.readValue(inputStream, Map.class); + } + } + + protected void loadAndRunTests(@NonNull String key, @NonNull String testsFile, + @NonNull CheckedBiFunction testRunner) throws IOException + { + Map data = parserYaml(testsFile); + Object testData = data.get(key); + if (testData==null) { + log.warn(color("{}: Key not found or empty: {}", "EXCEPTION"), callerClass, key); + return; + } + + int testNum = 1; + if (testData instanceof Collection testsList) { + // If test data object is a Collection iterate through it and run each test item + for (Object jsonObj : testsList) { + runTest(key, testNum, testRunner, jsonObj); + testNum++; + } + } else { + // If test data object is NOT a Collection then run ONE test passing it the test data object + runTest(key, testNum, testRunner, testData); + } + } + + private void runTest(String key, int testNum, CheckedBiFunction testRunner, Object dataObj) { + String testDescription = String.format("Test %s #%d", key, testNum); + try { + log.info(color("{}:", "INFO"), callerClass); + log.info(color("{}: ---------------------------------------------------------------", "INFO"), callerClass); + log.info(color("{}: {}: json:\n{}", "INFO"), callerClass, testDescription, dataObj); + Object resultObj = testRunner.apply(testDescription, dataObj); + String result; + String title = ""; + String expected_outcome = null; + if (resultObj instanceof String s) result = s; + else if (resultObj instanceof Map m) { + result = m.getOrDefault("result", "").toString(); + title = m.getOrDefault("title", "").toString(); + if (! title.trim().isEmpty()) title = ": "+title+":\n "; + expected_outcome = m.getOrDefault("expected_outcome", "").toString(); + } else result = resultObj.toString(); + results.put(testDescription + color(title,"WHITE"), + getPassOrFail(result, expected_outcome) + " " + color(result,result)); + log.info(color("{}: {}: Result: {}", result), callerClass, testDescription, + Objects.requireNonNullElse(result, color("(no result returned)", "WARN"))); + } catch (Exception e) { + log.warn(color("{}: {}: EXCEPTION: ", "EXCEPTION"), callerClass, testDescription, e); + results.put(testDescription, "EXCEPTION: "+e.getMessage()); + } + } + + private String getPassOrFail(String result, String expectedOutcome) { + if (StringUtils.isBlank(expectedOutcome)) return ""; + if (result==null) result = ""; + String resultFirst = result.split("[^\\p{Alnum}]", 2)[0].toUpperCase(); + return StringUtils.equalsIgnoreCase(resultFirst, expectedOutcome) || expectedOutcome.equals(result) + ? color("PASS","PASS") : color("FAIL", "FAIL"); + } + + private static String color(String message, String result) { + if (result==null) result = ""; + result = result.split("[^\\p{Alnum}]", 2)[0].toUpperCase(); + String color = null; + switch (result) { + case "OK" -> color = COLOR_GREEN; + case "ERROR" -> color = COLOR_YELLOW; + case "EXCEPTION" -> color = COLOR_RED; + case "INFO" -> color = COLOR_BLUE; + case "WARN" -> color = COLOR_MAGENTA; + case "WHITE" -> color = COLOR_WHITE; + case "PASS" -> color = COLOR_GREEN_INTENSE; + case "FAIL" -> color = COLOR_RED_INTENSE; + } + return color==null + ? message + : color + message + COLOR_RESET; + } + + public Map toMap(Object obj) throws IOException { + if (obj instanceof Map map) return map; + if (obj instanceof String s) return yamlMapper.readValue(s, Map.class); + throw new ClassCastException("toMap: Cannot cast "+obj+" to Map"); + } + + @AfterAll + public void printResultsSummary() { + if (! printResultsSummary) return; + StringBuilder sb = new StringBuilder(); + doPrintResults(callerClass, results, sb); + log.info(sb.toString()); + } + + @AfterAll + public static void printGlobalResultsSummary() { + if (! printGlobalResultsSummary) return; + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append(color("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "WHITE")).append("\n"); + sb.append(color("━━━━━ Global Test Results ━━━━━", "WHITE")).append("\n"); + sb.append(color("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "WHITE")).append("\n"); + globalResults.forEach((callerClass, results) -> { + doPrintResults(callerClass, results, sb); + }); + log.info(sb.toString()); + } + + private static void doPrintResults(String callerClass, Map results, StringBuilder sb) { + sb.append("\n"); + sb.append(String.format(color(" Test Results for: %s", "INFO"), callerClass)).append("\n"); + final AtomicInteger c = new AtomicInteger(1); + results.forEach((testDescription, result) -> { + sb.append(String.format(" %s. %s: %s", c.getAndIncrement(), testDescription, StringUtils.firstNonBlank(color(result, result), NO_RESULTS_COLORED))).append("\n"); + }); + } +} \ No newline at end of file diff --git a/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/boot/BootServiceTest.java b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/boot/BootServiceTest.java new file mode 100644 index 0000000..ae5b1c9 --- /dev/null +++ b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/boot/BootServiceTest.java @@ -0,0 +1,91 @@ +package eu.nebulous.ems.boot; + +import eu.nebulous.ems.AbstractBaseTest; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class BootServiceTest extends AbstractBaseTest { + + public static final String TESTS_YAML_FILE = "src/test/resources/BootServiceTest.yaml"; + + private EmsBootProperties properties; + private BootService bootService; + private IndexService indexService; + + @BeforeAll + public void setUp() throws IOException { + log.info("BootServiceTest: Setting up"); + properties = initializeEmsBootProperties(); + indexService = new IndexService(null, null, properties, objectMapper); + bootService = new BootService(properties, indexService, objectMapper); + log.debug("BootServiceTest: bootService: {}", bootService); + indexService.initIndexFile(); + log.debug("BootServiceTest: indexService initialized!!"); + } + + private void initializeCache(Map data) throws IOException { + String appId = Objects.requireNonNullElse(data.get("appId"), UUID.randomUUID()).toString(); + String timestamp = Objects.requireNonNullElse(data.get("timestamp"), Instant.now().toEpochMilli()).toString(); + Object model = data.get("model"); + Object bindings = data.get("bindings"); + Object solution = data.get("solution"); + Object metrics = data.get("metrics"); + + String modelFile = writeToFile(appId, "model", timestamp, model); + String bindingsFile = writeToFile(appId, "bindings", timestamp, bindings); + String solutionFile = writeToFile(appId, "sol", timestamp, solution); + String metricsFile = writeToFile(appId, "metrics", timestamp, metrics); + + indexService.deleteAll(); + Map entry = new LinkedHashMap<>(); + entry.put(ModelsService.MODEL_FILE_KEY, modelFile); + entry.put(ModelsService.BINDINGS_FILE_KEY, bindingsFile); + entry.put(ModelsService.SOLUTION_FILE_KEY, solutionFile); + entry.put(ModelsService.OPTIMISER_METRICS_FILE_KEY, metricsFile); + indexService.storeToIndex(appId, entry); + } + + private String writeToFile(String appId, String fileType, String timestamp, Object contents) throws IOException { + if (contents==null) return null; + String fileName = String.format("%s--%s--%s.json", appId.trim(), fileType.trim(), timestamp.trim()); + String contentsStr = (contents instanceof String) + ? contents.toString() + : objectMapper.writeValueAsString(contents); + Files.writeString(Paths.get(properties.getModelsDir(), fileName), contentsStr); + return fileName; + } + + @Test + void processEmsBootMessage() throws IOException { + loadAndRunTests("processEmsBootMessage", TESTS_YAML_FILE, (testDescription, yaml) -> { + Map testData = toMap(yaml); + log.debug("BootServiceTest: {}: testData: {}", testDescription, testData); + String title = testData.getOrDefault("title", "").toString(); + String expected = testData.getOrDefault("expected_outcome", "").toString(); + + initializeCache(testData); + + Command command = new Command("key", "topic", testData, null, null); + String appId = testData.getOrDefault("appId", "").toString(); + String result = bootService.processEmsBootMessage(command, appId, (message, app_id) -> {}); + return Map.of( + "result", result, + "title", title, + "expected_outcome", expected + ); + }); + } +} \ No newline at end of file diff --git a/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/boot/ModelsServiceTest.java b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/boot/ModelsServiceTest.java index 8d85152..07012e4 100644 --- a/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/boot/ModelsServiceTest.java +++ b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/boot/ModelsServiceTest.java @@ -1,28 +1,26 @@ package eu.nebulous.ems.boot; -import com.fasterxml.jackson.databind.ObjectMapper; +import eu.nebulous.ems.AbstractBaseTest; import eu.nebulous.ems.translate.TranslationService; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import java.io.IOException; import java.util.Map; @Slf4j -class ModelsServiceTest { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ModelsServiceTest extends AbstractBaseTest { + + public static final String TESTS_YAML_FILE = "src/test/resources/ModelsServiceTest.yaml"; - private ObjectMapper objectMapper; private ModelsService modelsService; - @BeforeEach + @BeforeAll public void setUp() throws IOException { log.info("ModelsServiceTest: Setting up"); - objectMapper = new ObjectMapper(); TranslationService translationService = new TranslationService(null, null); - EmsBootProperties properties = new EmsBootProperties(); - properties.setModelsDir("target"); - properties.setModelsIndexFile("target/index.json"); + EmsBootProperties properties = initializeEmsBootProperties(); IndexService indexService = new IndexService(null, null, properties, objectMapper); modelsService = new ModelsService(translationService, properties, objectMapper, indexService); log.debug("ModelsServiceTest: modelsService: {}", modelsService); @@ -32,89 +30,25 @@ public void setUp() throws IOException { @Test void extractBindings() throws IOException { - if (objectMapper==null) { - log.info("ModelsServiceTest: calling setUp!!"); - setUp(); - } - - String json = """ - { - "utilityFunctions": [ - { - "name": "test_utility", - "type": "maximize", - "expression": { - "formula": "0.5*exp((log(0.001) * (mean_cpu_consumption_all - 50)^2) /1600) + 0.5*exp((log(0.001) * (mean_requests_per_second - 7)^2) /25)", - "variables": [ - { - "name": "mean_cpu_consumption_all", - "value": "mean_cpu_consumption_all" - }, - { - "name": "mean_requests_per_second", - "value": "mean_requests_per_second" - } - ] - } - }, - { - "name": "dosage_analysis_replica_count_const", - "type": "constant", - "expression": { - "formula": "dosage_analysis_replica_count_const", - "variables": [ - { - "name": "dosage_analysis_replica_count_const", - "value": "spec_components_1_traits_0_properties_replicas" - } - ] - } - }, - { - "name": "data_collection_replica_count_const", - "type": "constant", - "expression": { - "formula": "data_collection_replica_count", - "variables": [ - { - "name": "data_collection_replica_count", - "value": "spec_components_0_traits_0_properties_replicas" - } - ] - } - }, - { - "name": "total_instances_const", - "type": "constant", - "expression": { - "formula": "data_collection_replica_count+dosage_analysis_replica_count_const+1", - "variables": [ - { - "name": "data_collection_replica_count", - "value": "spec_components_0_traits_0_properties_replicas" - }, - { - "name": "dosage_analysis_replica_count_const", - "value": "spec_components_1_traits_0_properties_replicas" - } - ] - } - } - ], - "uuid": "1487b024-dcbb-4edc-b21b-998c71757566", - "_create": true, - "_delete": true - } - """; - log.info("ModelsServiceTest: json:\n{}", json); - - Map body = objectMapper.readValue(json, Map.class); - log.info("ModelsServiceTest: body: {}", body); - - Command command = new Command("key", "topic", body, null, null); - String appId = body.getOrDefault("uuid", "").toString(); - String result = modelsService.extractBindings(command, appId); + loadAndRunTests("extractBindings", TESTS_YAML_FILE, (testDescription, json) -> { + Map body = toMap(json); + log.info("ModelsServiceTest: {}: body: {}", testDescription, body); + + Command command = new Command("key", "topic", body, null, null); + String appId = body.getOrDefault("uuid", "").toString(); + return modelsService.extractBindings(command, appId); + }); + } - log.info("ModelsServiceTest: Result: {}", result); + @Test + void extractSolution() throws IOException { + loadAndRunTests("extractSolution", TESTS_YAML_FILE, (testDescription, json) -> { + Map body = toMap(json); + log.info("ModelsServiceTest: {}: body: {}", testDescription, body); + + Command command = new Command("key", "topic", body, null, null); + String appId = body.getOrDefault("uuid", "").toString(); + return modelsService.extractSolution(command, appId); + }); } } \ No newline at end of file diff --git a/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/service/MvvServiceTest.java b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/service/MvvServiceTest.java index 5f2b1ce..6d3e991 100644 --- a/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/service/MvvServiceTest.java +++ b/nebulous/ems-nebulous/src/test/java/eu/nebulous/ems/service/MvvServiceTest.java @@ -1,40 +1,51 @@ package eu.nebulous.ems.service; -import lombok.NonNull; +import eu.nebulous.ems.AbstractBaseTest; import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.io.IOException; import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; +import java.util.Objects; @Slf4j -class MvvServiceTest { +class MvvServiceTest extends AbstractBaseTest { + + public static final String TESTS_YAML_FILE = "src/test/resources/MvvServiceTest.yaml"; + + private MvvService mvvService; + + @BeforeAll + public void setUp() throws IOException { + log.info("MvvServiceTest: Setting up"); + mvvService = new MvvService(null); + } @Test - void translateAndSetControlServiceConstants() { - @NonNull Map> bindings = Map.of( - "simple-bindings", Map.of( - "spec_components_1_traits_0_properties_replicas", "dosage_analysis_replica_count_const", - "spec_components_0_traits_0_properties_replicas", "data_collection_replica_count_const" - ), - "composite-bindings", Map.of( - "spec_components_0_traits_0_properties_replicas+spec_components_1_traits_0_properties_replicas+1", "total_instances_const" - ) - ); - Map newValues = Map.of( - "spec_components_1_traits_0_properties_replicas", 2.0, - "spec_components_0_traits_0_properties_replicas", 3.0 - ); - log.info("MvvServiceTest: bindings: {}", bindings); - log.info("MvvServiceTest: newValues: {}", newValues); - - MvvService mvvService = new MvvService(null); - mvvService.setBindings(bindings); - log.info("MvvServiceTest: values BEFORE: {}", mvvService.getValues()); - try { - mvvService.translateAndSetValues(newValues); - } catch (Exception ignored) {} - log.info("MvvServiceTest: values AFTER: {}", mvvService.getValues()); + void translateAndSetControlServiceConstants() throws IOException { + loadAndRunTests("translateAndSetControlServiceConstants", TESTS_YAML_FILE, (testDescription, yaml) -> { + Map testData = toMap(yaml); + log.debug("translateAndSetControlServiceConstants: {}: testData: {}", testDescription, testData); + String title = testData.getOrDefault("title", "").toString(); + String expected = testData.getOrDefault("expected_outcome", "").toString(); + + Map bindings = toMap(Objects.requireNonNullElse(testData.get("bindings"), Map.of())); + Map newValues = toMap(Objects.requireNonNullElse(testData.get("solution"), Map.of())); + + MvvService mvvService = new MvvService(null); + mvvService.setBindings(bindings); + log.info("translateAndSetControlServiceConstants: values BEFORE: {}", mvvService.getValues()); + try { + mvvService.translateAndSetValues(newValues); + } catch (Exception ignored) {} + log.info("translateAndSetControlServiceConstants: values AFTER: {}", mvvService.getValues()); + + return Map.of( + "result", mvvService.getValues().toString(), + "title", title, + "expected_outcome", expected + ); + }); } } \ No newline at end of file diff --git a/nebulous/ems-nebulous/src/test/resources/BootServiceTest.yaml b/nebulous/ems-nebulous/src/test/resources/BootServiceTest.yaml new file mode 100644 index 0000000..0d83a7f --- /dev/null +++ b/nebulous/ems-nebulous/src/test/resources/BootServiceTest.yaml @@ -0,0 +1,279 @@ +--- +# The EMS Boot cache contents used in BootServiceTest class +processEmsBootMessage: + - title: A properly initialized entry + expected_outcome: OK + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + "metadata": { + "name": "12345678-abcb-def0-123b-4567890abcde", + "labels": { + "app": "anAPP 2025-03-06" + } + }, + "templates": [], + "spec": { + "components": [], + "scopes": [ + { + "name": "app-wide-scope", + "components": [], + "metrics": [ + { + "name": "messages_per_minute", + "type": "raw", + "sensor": { + "type": "none", + "config": { } + } + }, + { + "name": "load_factor", + "type": "composite", + "formula": "messages_per_minute / replicas" + }, + { + "name": "replicas", + "type": "constant" + } + ], + "requirements": [ + { + "name": "combined-slo", + "type": "slo", + "constraint": "(load_factor > 1000)" + } + ] + } + ] + } + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: A minimal valid entry + expected_outcome: OK + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: Empty payload + expected_outcome: ERROR + + # --------------------------------------------------------------------------------------- + - title: No appId + expected_outcome: ERROR + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: No 'model' + expected_outcome: ERROR + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: No 'binding' + expected_outcome: ERROR + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: No 'solution' + expected_outcome: OK + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: No 'metrics' + expected_outcome: ERROR + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + + # --------------------------------------------------------------------------------------- + - title: Null model + expected_outcome: ERROR + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: null + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: Null bindings + expected_outcome: ERROR + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: null + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: Null solution + expected_outcome: OK + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: null + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: Null metrics + expected_outcome: ERROR + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: null + + # --------------------------------------------------------------------------------------- + - title: Malformed model + expected_outcome: OK + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: null + model: | + "apiVersion": "nebulous/v1", + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] + + # --------------------------------------------------------------------------------------- + - title: With timestamp + expected_outcome: OK + appId: 12345678-abcb-def0-123b-4567890abcde + timestamp: 999999999 + model: | + { + "apiVersion": "nebulous/v1", + "kind": "MetricModel", + } + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + metrics: | + [ "load_factor" ] diff --git a/nebulous/ems-nebulous/src/test/resources/ModelsServiceTest.yaml b/nebulous/ems-nebulous/src/test/resources/ModelsServiceTest.yaml new file mode 100644 index 0000000..4e364c9 --- /dev/null +++ b/nebulous/ems-nebulous/src/test/resources/ModelsServiceTest.yaml @@ -0,0 +1,273 @@ +--- +extractBindings: + - | # Example taken from DYEMAC use case + { + "utilityFunctions": [ + { + "name": "test_utility", + "type": "maximize", + "expression": { + "formula": "0.5*exp((log(0.001) * (mean_cpu_consumption_all - 50)^2) /1600) + 0.5*exp((log(0.001) * (mean_requests_per_second - 7)^2) /25)", + "variables": [ + { + "name": "mean_cpu_consumption_all", + "value": "mean_cpu_consumption_all" + }, + { + "name": "mean_requests_per_second", + "value": "mean_requests_per_second" + } + ] + } + }, + { + "name": "dosage_analysis_replica_count_const", + "type": "constant", + "expression": { + "formula": "dosage_analysis_replica_count_const", + "variables": [ + { + "name": "dosage_analysis_replica_count_const", + "value": "spec_components_1_traits_0_properties_replicas" + } + ] + } + }, + { + "name": "data_collection_replica_count_const", + "type": "constant", + "expression": { + "formula": "data_collection_replica_count", + "variables": [ + { + "name": "data_collection_replica_count", + "value": "spec_components_0_traits_0_properties_replicas" + } + ] + } + }, + { + "name": "total_instances_const", + "type": "constant", + "expression": { + "formula": "data_collection_replica_count+dosage_analysis_replica_count_const+1", + "variables": [ + { + "name": "data_collection_replica_count", + "value": "spec_components_0_traits_0_properties_replicas" + }, + { + "name": "dosage_analysis_replica_count_const", + "value": "spec_components_1_traits_0_properties_replicas" + } + ] + } + } + ], + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "_create": true, + "_delete": true + } + + - | # Test with missing 'utilityFunctions' section + { + "uuid": "12345678-abcb-def0-123b-4567890abcde" + } + + - | # Test with empty 'utilityFunctions' section + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": [ ] + } + + - | # Test with null 'utilityFunctions' section + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": null + } + + - | # Test with 'utilityFunctions' section with wrong data type + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": { "aa": "xx" } + } + + - | # Test with 'utilityFunctions' section with wrong data type + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": "dummy" + } + + - | # Test with 'utilityFunctions' section with no constants + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": [ + { + "name": "test_utility", + "type": "maximize", + "expression": { + "formula": "0.5*exp((log(0.001) * (mean_cpu_consumption_all - 50)^2) /1600) + 0.5*exp((log(0.001) * (mean_requests_per_second - 7)^2) /25)", + "variables": [ + { + "name": "mean_cpu_consumption_all", + "value": "mean_cpu_consumption_all" + }, + { + "name": "mean_requests_per_second", + "value": "mean_requests_per_second" + } + ] + } + } + ] + } + + - | # Test with 'utilityFunctions' section with simple constants + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": [ + { + "name": "dosage_analysis_replica_count_const", + "type": "constant", + "expression": { + "formula": "dosage_analysis_replica_count_const", + "variables": [ + { + "name": "dosage_analysis_replica_count_const", + "value": "spec_components_1_traits_0_properties_replicas" + } + ] + } + }, + { + "name": "data_collection_replica_count_const", + "type": "constant", + "expression": { + "formula": "data_collection_replica_count", + "variables": [ + { + "name": "data_collection_replica_count", + "value": "spec_components_0_traits_0_properties_replicas" + } + ] + } + } + ] + } + + - | # Test with 'utilityFunctions' section with simple and composite constants + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": [ + { + "name": "dosage_analysis_replica_count_const", + "type": "constant", + "expression": { + "formula": "dosage_analysis_replica_count_const", + "variables": [ + { + "name": "dosage_analysis_replica_count_const", + "value": "spec_components_1_traits_0_properties_replicas" + } + ] + } + }, + { + "name": "data_collection_replica_count_const", + "type": "constant", + "expression": { + "formula": "data_collection_replica_count", + "variables": [ + { + "name": "data_collection_replica_count", + "value": "spec_components_0_traits_0_properties_replicas" + } + ] + } + }, + { + "name": "total_instances_const", + "type": "constant", + "expression": { + "formula": "data_collection_replica_count+dosage_analysis_replica_count_const+1", + "variables": [ + { + "name": "data_collection_replica_count", + "value": "spec_components_0_traits_0_properties_replicas" + }, + { + "name": "dosage_analysis_replica_count_const", + "value": "spec_components_1_traits_0_properties_replicas" + } + ] + } + }, + { + "name": "negative_data_collection_replica_count_plus_one", + "type": "constant", + "expression": { + "formula": "-data_collection_replica_count+1", + "variables": [ + { + "name": "data_collection_replica_count", + "value": "spec_components_0_traits_0_properties_replicas" + } + ] + } + } + ] + } + + - | # Test with 'utilityFunctions' section with composite constants ONLY + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "utilityFunctions": [ + { + "name": "total_instances_const", + "type": "constant", + "expression": { + "formula": "data_collection_replica_count+dosage_analysis_replica_count_const+1", + "variables": [ + { + "name": "data_collection_replica_count", + "value": "spec_components_0_traits_0_properties_replicas" + }, + { + "name": "dosage_analysis_replica_count_const", + "value": "spec_components_1_traits_0_properties_replicas" + } + ] + } + } + ] + } + +extractSolution: + - | # Test with a proper solution + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "DeploySolution": false, + "VariableValues": { "spec_components_0_traits_0_properties_replicas": 1.0, "spec_components_1_traits_0_properties_replicas": 2.0 } + } + - | # Test with a solution with no 'DeploySolution' + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "VariableValues": { "spec_components_0_traits_0_properties_replicas": 1.0, "spec_components_1_traits_0_properties_replicas": 2.0 } + } + - | # Test with a solution with no 'VariableValues' + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "DeploySolution": false + } + - | # Test with an empty solution + { } + - | # Test with a solution with items of wrong type + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "VariableValues": { "spec_components_0_traits_0_properties_replicas": "1.0" } + } + - | # Test with a solution with null items + { + "uuid": "12345678-abcb-def0-123b-4567890abcde", + "VariableValues": { "spec_components_0_traits_0_properties_replicas": null } + } diff --git a/nebulous/ems-nebulous/src/test/resources/MvvServiceTest.yaml b/nebulous/ems-nebulous/src/test/resources/MvvServiceTest.yaml new file mode 100644 index 0000000..1d31235 --- /dev/null +++ b/nebulous/ems-nebulous/src/test/resources/MvvServiceTest.yaml @@ -0,0 +1,64 @@ +--- +# The message contents used in MvvServiceTest class +translateAndSetControlServiceConstants: + - title: A properly initialized entry + expected_outcome: '{replicas=2.0}' + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 } + + - title: Extra solution + expected_outcome: '{replicas=2.0}' + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0, spec_components_1_traits_0_properties_replicas": 1.0 } + + - title: Empty solution + expected_outcome: '{}' + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: | + { } + + - title: Null solution + expected_outcome: '{}' + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { } + } + solution: null + + - title: Empty bindings + expected_outcome: '{}' + bindings: | + { } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0, spec_components_1_traits_0_properties_replicas": 1.0 } + + - title: Null bindings + expected_outcome: '{}' + bindings: null + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0, spec_components_1_traits_0_properties_replicas": 1.0 } + + - title: With composite binding + expected_outcome: '{replicas=2.0, negative_replicas=-2.0}' + bindings: | + { + "simple-bindings": { "spec_components_0_traits_0_properties_replicas": "replicas" }, + "composite-bindings": { "-1 * spec_components_0_traits_0_properties_replicas": "negative_replicas" } + } + solution: | + { "spec_components_0_traits_0_properties_replicas": 2.0 }