diff --git a/validator/build.gradle b/validator/build.gradle index 0d02cd3b0..d485296ca 100644 --- a/validator/build.gradle +++ b/validator/build.gradle @@ -32,7 +32,7 @@ sourceSets { checkstyle { ignoreFailures = false - maxWarnings = 40 + maxWarnings = 50 } checkstyleMain.source = 'src/main/java' diff --git a/validator/src/main/java/com/google/daq/mqtt/registrar/LocalDevice.java b/validator/src/main/java/com/google/daq/mqtt/registrar/LocalDevice.java index 4e09a34b0..1404e0110 100644 --- a/validator/src/main/java/com/google/daq/mqtt/registrar/LocalDevice.java +++ b/validator/src/main/java/com/google/daq/mqtt/registrar/LocalDevice.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.util.ISO8601DateFormat; import com.github.fge.jsonschema.core.exceptions.ProcessingException; +import com.github.fge.jsonschema.core.report.LogLevel; +import com.github.fge.jsonschema.core.report.ProcessingMessage; import com.github.fge.jsonschema.core.report.ProcessingReport; import com.github.fge.jsonschema.main.JsonSchema; import com.google.api.services.cloudiot.v1.model.DeviceCredential; @@ -189,6 +191,7 @@ class LocalDevice { private final String generation; private final List deviceCredentials = new ArrayList<>(); private final TreeMap siteMetadata; + private final boolean validateMetadata; private String deviceNumId; @@ -197,12 +200,13 @@ class LocalDevice { LocalDevice( File siteDir, File devicesDir, String deviceId, Map schemas, - String generation, Metadata siteMetadata) { + String generation, Metadata siteMetadata, boolean validateMetadata) { try { this.deviceId = deviceId; this.schemas = schemas; this.generation = generation; this.siteDir = siteDir; + this.validateMetadata = validateMetadata; if (siteMetadata != null) { this.siteMetadata = OBJECT_MAPPER.convertValue(siteMetadata, TreeMap.class); } else { @@ -218,14 +222,29 @@ class LocalDevice { } } + LocalDevice( + File siteDir, File devicesDir, String deviceId, Map schemas, + String generation, Metadata siteMetadata) { + this(siteDir, devicesDir, deviceId, schemas, generation, siteMetadata, false); + } + LocalDevice( File siteDir, File devicesDir, String deviceId, Map schemas, String generation) { this(siteDir, devicesDir, deviceId, schemas, generation, null); } - static boolean deviceExists(File devicesDir, String deviceName) { - return new File(new File(devicesDir, deviceName), METADATA_JSON).isFile(); + public static void parseMetadataValidateProcessingReport(ProcessingReport report) + throws ValidationException { + if (report.isSuccess()) { + return; + } + + for (ProcessingMessage msg : report) { + if (msg.getLogLevel().compareTo(LogLevel.ERROR) >= 0) { + throw ValidationException.fromProcessingReport(report); + } + } } private void prepareOutDir() { @@ -235,6 +254,10 @@ private void prepareOutDir() { File exceptionLog = new File(outDir, EXCEPTION_LOG_FILE); exceptionLog.delete(); } + + static boolean deviceExists(File devicesDir, String deviceName) { + return new File(new File(devicesDir, deviceName), METADATA_JSON).isFile(); + } public void validateExpected() { Path relativized = siteDir.toPath().relativize(deviceDir.toPath()); @@ -281,7 +304,7 @@ private void deepMergeDefaults(Map destination, Map cloudDevices; private Metadata siteMetadata; + private Map> lastErrorSummary; + private boolean validateMetadata = false; + private List deviceList; /** * Main entry point for registrar. @@ -107,33 +112,11 @@ public class Registrar { public static void main(String[] args) { ArrayList argList = new ArrayList<>(List.of(args)); Registrar registrar = new Registrar(); - try { - boolean processAllDevices = processArgs(argList, registrar); - - if (registrar.schemaBase == null) { - registrar.setToolRoot(null); - } - - registrar.loadSiteMetadata(); - - if (processAllDevices) { - registrar.processDevices(); - } else { - registrar.processDevices(argList); - } - - registrar.writeErrors(); - registrar.shutdown(); - } catch (ExceptionMap em) { - ExceptionMap.format(em, ERROR_FORMAT_INDENT).write(System.err); - throw new RuntimeException("mapped exceptions", em); - } catch (Exception ex) { - ex.printStackTrace(); - throw new RuntimeException("main exception", ex); - } + processArgs(argList, registrar); + registrar.execute(); } - private static boolean processArgs(List argList, Registrar registrar) { + public static void processArgs(List argList, Registrar registrar) { while (argList.size() > 0) { String option = argList.remove(0); switch (option) { @@ -155,17 +138,39 @@ private static boolean processArgs(List argList, Registrar registrar) { case "-l": registrar.setIdleLimit(argList.remove(0)); break; + case "-t": + registrar.setValidateMetadata(true); + break; case "--": - return false; + break; default: if (option.startsWith("-")) { throw new RuntimeException("Unknown cmdline option " + option); } + // Add the current non-option back into the list and use it as device names list. argList.add(0, option); - return false; + registrar.setDeviceList(argList); + return; } } - return true; + } + + public void execute() { + try { + if (schemaBase == null) { + setToolRoot(null); + } + loadSiteMetadata(); + processDevices(); + writeErrors(); + shutdown(); + } catch (ExceptionMap em) { + ExceptionMap.format(em, ERROR_FORMAT_INDENT).write(System.err); + throw new RuntimeException("mapped exceptions", em); + } catch (Exception ex) { + ex.printStackTrace(); + throw new RuntimeException("main exception", ex); + } } private void setIdleLimit(String option) { @@ -177,6 +182,14 @@ private void setUpdateFlag(boolean update) { updateCloudIoT = update; } + private void setValidateMetadata(boolean validateMetadata) { + this.validateMetadata = validateMetadata; + } + + private void setDeviceList(List deviceList) { + this.deviceList = deviceList; + } + private void setFeedTopic(String feedTopic) { System.err.println("Sending device feed to topic " + feedTopic); feedPusher = new PubSubPusher(projectId, feedTopic); @@ -186,6 +199,10 @@ private void setFeedTopic(String feedTopic) { } } + protected Map> getLastErrorSummary() { + return lastErrorSummary; + } + private void writeErrors() throws Exception { Map> errorSummary = new TreeMap<>(); DeviceExceptionManager dem = new DeviceExceptionManager(siteDir); @@ -223,9 +240,10 @@ private void writeErrors() throws Exception { String version = Optional.ofNullable(System.getenv(UDMI_VERSION_KEY)).orElse("unknown"); errorSummary.put(VERSION_KEY, Map.of(VERSION_MAIN_KEY, version)); OBJECT_MAPPER.writeValue(summaryFile, errorSummary); + lastErrorSummary = errorSummary; } - private void setSitePath(String sitePath) { + protected void setSitePath(String sitePath) { Preconditions.checkNotNull(SCHEMA_NAME, "schemaName not set yet"); siteDir = new File(sitePath); summaryFile = new File(siteDir, REGISTRATION_SUMMARY_JSON); @@ -259,7 +277,7 @@ private String getGenerationString() { } private void processDevices() { - processDevices(null); + processDevices(this.deviceList); } private void processDevices(List devices) { @@ -638,7 +656,7 @@ private Map loadDevices(File siteDir, File devicesDir, localDevices.computeIfAbsent( deviceName, keyName -> new LocalDevice(siteDir, devicesDir, deviceName, schemas, generation, - siteMetadata)); + siteMetadata, validateMetadata)); try { localDevice.loadCredentials(); } catch (Exception e) { @@ -659,7 +677,7 @@ private Map loadDevices(File siteDir, File devicesDir, return localDevices; } - private void setProjectId(String projectId) { + protected void setProjectId(String projectId) { if (NO_SITE.equals(projectId) || projectId == null) { return; } @@ -667,7 +685,7 @@ private void setProjectId(String projectId) { initializeCloudProject(); } - private void setToolRoot(String toolRoot) { + protected void setToolRoot(String toolRoot) { schemaBase = new File(toolRoot, SCHEMA_BASE_PATH); File[] schemaFiles = schemaBase.listFiles(file -> file.getName().endsWith(SCHEMA_SUFFIX)); for (File schemaFile : Objects.requireNonNull(schemaFiles)) { @@ -705,6 +723,9 @@ private void loadSiteMetadata() { File siteMetadataFile = new File(siteDir, SITE_METADATA_JSON); try (InputStream targetStream = new FileInputStream(siteMetadataFile)) { + // At this time, do not validate the Metadata schema because, by its nature of being + // a partial overlay on each device Metadata, this Metadata will likely be incomplete + // and fail validation. schemas.get(METADATA_JSON).validate(OBJECT_MAPPER.readTree(targetStream)); } catch (FileNotFoundException e) { return; @@ -720,6 +741,10 @@ private void loadSiteMetadata() { } } + protected Map getSchemas() { + return schemas; + } + class RelativeDownloader implements URIDownloader { @Override diff --git a/validator/src/main/java/com/google/daq/mqtt/registrar/RegistrarTest.java b/validator/src/main/java/com/google/daq/mqtt/registrar/RegistrarTest.java new file mode 100644 index 000000000..9229ebb29 --- /dev/null +++ b/validator/src/main/java/com/google/daq/mqtt/registrar/RegistrarTest.java @@ -0,0 +1,93 @@ +package com.google.daq.mqtt.registrar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.fge.jsonschema.core.exceptions.ProcessingException; +import com.github.fge.jsonschema.core.report.LogLevel; +import com.github.fge.jsonschema.core.report.ProcessingMessage; +import com.github.fge.jsonschema.core.report.ProcessingReport; +import com.github.fge.jsonschema.main.JsonSchema; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import org.junit.Test; +import udmi.schema.Metadata; + +public class RegistrarTest { + + private static final String SCHEMA_BASE_PATH = "schema"; + private static final String METADATA_JSON = "metadata.json"; + private static final String PROJECT_ID = "unit-testing"; + private static final String SITE_PATH = "../sites/udmi_site_model"; + private static final String TOOL_ROOT = "../"; + + private static final String SYSTEM_LOCATION_SITE = "ZZ-TRI-FECTA"; + private static final String DEVICE_NAME = "AHU-1"; + private ObjectMapper mapper = new ObjectMapper(); + + public class RegistrarUnderTest extends Registrar { + protected JsonSchema getJsonSchema(String schemaName) { + return getSchemas().get(schemaName); + } + } + + private void assertErrorSummaryValidateSuccess(Map> summary) { + if ((summary == null) || (summary.get("Validating") == null) + || (summary.get("Validating").size() == 0)) { + return; + } + fail(summary.get("Validating").toString()); + } + + private void assertErrorSummaryValidateFailure(Map> summary) { + if ((summary == null) || (summary.get("Validating") == null)) { + fail("Error summary for Validating key is null"); + } + if (summary.get("Validating").size() == 0) { + fail("Error summary for Validating key is size 0"); + } + } + + private RegistrarUnderTest getRegistrarUnderTest() { + RegistrarUnderTest registrar = new RegistrarUnderTest(); + registrar.setSitePath(SITE_PATH); + registrar.setProjectId(PROJECT_ID); + registrar.setToolRoot(TOOL_ROOT); + return registrar; + } + + @Test public void metadataValidateSuccessTest() { + final RegistrarUnderTest registrar = getRegistrarUnderTest(); + + ArrayList argList = new ArrayList(); + argList.add("-s"); + argList.add(SITE_PATH); + Registrar.processArgs(argList, registrar); + registrar.execute(); + assertErrorSummaryValidateSuccess(registrar.getLastErrorSummary()); + } + + @Test public void metadataValidateFailureTest() { + final RegistrarUnderTest registrar = getRegistrarUnderTest(); + + ArrayList argList = new ArrayList(); + argList.add("-t"); + argList.add("-s"); + argList.add(SITE_PATH); + Registrar.processArgs(argList, registrar); + registrar.execute(); + assertErrorSummaryValidateFailure(registrar.getLastErrorSummary()); + } + +} diff --git a/validator/src/test/java/com/google/daq/mqtt/validator/ValidatorTest.java b/validator/src/test/java/com/google/daq/mqtt/validator/ValidatorTest.java index 48f299341..9e8c69ffd 100644 --- a/validator/src/test/java/com/google/daq/mqtt/validator/ValidatorTest.java +++ b/validator/src/test/java/com/google/daq/mqtt/validator/ValidatorTest.java @@ -52,6 +52,15 @@ public class ValidatorTest { validator.setSiteDir(SITE_DIR); } + @Test + public void emptySystemBlock() { + MessageBundle bundle = getMessageBundle("event", "pointset", new PointsetEvent()); + bundle.message.remove("system"); + validator.validateMessage(bundle); + MetadataReport report = getMetadataReport(); + assertEquals("One error device", 1, report.errorDevices.size()); + } + @Test public void emptyPointsetEvent() { MessageBundle bundle = getMessageBundle("event", "pointset", new PointsetEvent());