diff --git a/.gencode_hash.txt b/.gencode_hash.txt index 8c2504e26..dfa06e50f 100644 --- a/.gencode_hash.txt +++ b/.gencode_hash.txt @@ -5,7 +5,7 @@ cde866298a8a54168d3582b50ec93160209d07338d3d6c58d215855bc35cb158 gencode/docs/c 08583688b20f892c0b453f41787ac01a46ac601663736bcd6ed6f57be0758e79 gencode/docs/configuration_endpoint.html 6cf94d6cb600c75cde32a64bd78acb3ed3b54adfad08dbf6bb159b467e8925c9 gencode/docs/configuration_execution.html 6f8a3766b840e96881e3573d594e28864917efb288e068d6c82de55deb576694 gencode/docs/configuration_pod.html -fe6dabc7b6634449d457df758bcd441e7ecb87d8461c7e1ce26364f6b7f66f7f gencode/docs/configuration_pubber.html +dc77a21a05f9f98a23403a1db0c3b468ef50d028e4c4934734d08e15f36b1d57 gencode/docs/configuration_pubber.html 69bee6bfcd2f786d95cfd563ecb497296361c8945a1422385f78d25008f38ef1 gencode/docs/event.html 0a41807b999f80bd283fcee03e6df5845e13b122d9be7c7d4723b19a334b89d5 gencode/docs/event_discovery.html 79331072f185ca6e875b2e658e7e0dc471cd40be6b973ce9ee3f371aca474e9b gencode/docs/event_mapping.html @@ -100,7 +100,7 @@ fc3a9415c04d8a06954dbdbfdff5d68ab113cce3948532c19df555778ffb04fa gencode/java/u ca2e7566106818ca7e5190c8041eb86f0c9b3251b0bda8c3ea7ce11a0c891a0a gencode/java/udmi/schema/Position.java 3df66bb1a37a9e0b2b6cf392f8c64d404a73c83e5e13c02bb4844f09b9a04b70 gencode/java/udmi/schema/Properties.java d5e11dc354995681b894bcb62e4e21da7f0be68aaa3643a3dc5aa13610741f29 gencode/java/udmi/schema/PubberConfiguration.java -2fa3172b3ee42593da30c45dd47eb586f962e5b667c98a08410b1d8b525f22b9 gencode/java/udmi/schema/PubberOptions.java +3bff2c98c8d41816b44c7623d542428a6378d3c7a4f9440ae35b395fb1f1e386 gencode/java/udmi/schema/PubberOptions.java 703ed0cecb60f284b52e76dc4d612eb798ba3e8781cc40698a46611b36e6b0c9 gencode/java/udmi/schema/SchemaValidationState.java dc7c549b3358f9b2ceccc8633af3d0fca7eed7c05c94c3396815363b889b4c16 gencode/java/udmi/schema/SequenceValidationState.java 9da49b22341a65580d085fd9d00eaadcaefcf7b813988ef844c617aa4b8a9a4f gencode/java/udmi/schema/SetupUdmiConfig.java @@ -183,7 +183,7 @@ b48ae013d203eb31cc388a084c0cc6f93d4d5534336d4da2ecbccd84a085ea2b gencode/python 5c50847e136a033ea511209238bb570499b43fbee6189dae06603132dcb9f01f gencode/python/udmi/schema/model_testing_target.py 7c9a755ae06c44fc2b8117d7918b6401ce08a7218b86033fb3320eda7286e581 gencode/python/udmi/schema/monitoring.py 3a3873c7db8d5fa13b43629031d36417a4ec4838d612a7b8c2fce7c22fdee009 gencode/python/udmi/schema/monitoring_metric.py -6dd7eb5e89d98715d7aa13d8e7362b2e1d821543a8b879a65e8fbf0abf4cc3de gencode/python/udmi/schema/options_pubber.py +743395d4b764d4fd2614aef85cab680498e5f2560767bc24f424d6594f14d75c gencode/python/udmi/schema/options_pubber.py 6c5f3dd1c5ca9d821e3c48298af118fc7eafd97af9265dfd34b2ed8642efca77 gencode/python/udmi/schema/persistent_device.py a58f8c98e837a5b56126ca0f410e02f1e9cfcd80a8cb429e0ef522defab1f690 gencode/python/udmi/schema/properties.py e604cf0280fe772de5f4e5ecf10dc6c564b6177eeff9cd9fb8b385af8fe10a95 gencode/python/udmi/schema/state.py diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c1c529d7c..ffd386b5f 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -35,7 +35,7 @@ jobs: - name: bin/run_tests all_tests run: bin/run_tests all_tests - name: all test output - if: ${{ always() }} + if: ${{ !cancelled() }} run: more out/test_* spelling: @@ -154,33 +154,33 @@ jobs: - name: registrar clean run: bin/test_regclean $TARGET_PROJECT - name: sequence tests clean - if: ${{ always() }} + if: ${{ !cancelled() }} run: bin/test_sequencer clean nocheck $TARGET_PROJECT - name: sequence tests alpha - if: ${{ always() }} + if: ${{ !cancelled() }} run: bin/test_sequencer alpha nocheck $TARGET_PROJECT - name: sequence test post-process - if: ${{ always() }} + if: ${{ !cancelled() }} run: egrep ' test .* after .*s ' out/sequencer.log | tee out/timing_sequencer.out - name: itemized sequencer tests - if: ${{ always() }} + if: ${{ !cancelled() }} run: bin/test_itemized $TARGET_PROJECT - name: output files - if: ${{ always() }} + if: ${{ !cancelled() }} run: more out/*.out* | cat - name: pubber logs - if: ${{ always() }} + if: ${{ !cancelled() }} run: more out/pubber.log* pubber/out/*.json | cat - name: itemized test post-process - if: ${{ always() }} + if: ${{ !cancelled() }} run: egrep ' test .* after .*s ' out/sequencer.log-* | tee out/timing_itemized.out - name: support bundle - if: ${{ always() }} + if: ${{ !cancelled() }} run: | bin/support ${{ github.repository_owner }}_${{ github.job }}_ tar -tzvf *udmi-support*.tgz - uses: actions/upload-artifact@v3 - if: ${{ always() }} + if: ${{ !cancelled() }} with: if-no-files-found: error name: udmi-support_${{ github.run_id }} @@ -206,23 +206,25 @@ jobs: - name: registrar clean run: bin/test_regclean solo $TARGET_PROJECT - name: telemetry validator + if: ${{ !cancelled() }} run: bin/test_validator $TARGET_PROJECT - name: validator result - if: ${{ always() }} + if: ${{ !cancelled() }} run: cat /tmp/validator.out || true - name: message captures - if: ${{ always() }} + if: ${{ !cancelled() }} run: | cd sites/udmi_site_model/out find . -type f | sort | xargs ls -l find . -type f | sort | xargs more | cat - name: nostate sequences + if: ${{ !cancelled() }} run: bin/test_sequencer nostate full $TARGET_PROJECT - name: support bundle - if: ${{ always() }} + if: ${{ !cancelled() }} run: UDMI_REGISTRY_SUFFIX=_b bin/support ${{ github.repository_owner }}_${{ github.job }}_ - uses: actions/upload-artifact@v3 - if: ${{ always() }} + if: ${{ !cancelled() }} with: if-no-files-found: error name: udmi-support_${{ github.run_id }} @@ -252,13 +254,13 @@ jobs: - name: bin/test_redirect run: bin/test_redirect $TARGET_PROJECT - name: pubber.log - if: ${{ always() }} + if: ${{ !cancelled() }} run: more out/pubber.log* | cat - name: support bundle - if: ${{ always() }} + if: ${{ !cancelled() }} run: UDMI_REGISTRY_SUFFIX=_r bin/support ${{ github.repository_owner }}_${{ github.job }}_ - uses: actions/upload-artifact@v3 - if: ${{ always() }} + if: ${{ !cancelled() }} with: if-no-files-found: error name: udmi-support_${{ github.run_id }} @@ -289,8 +291,8 @@ jobs: ls -d sites/udmi_site_model*/out*/devices/AHU-1/tests/* find out* -name 'schema*.out*' -size +0 | xargs more | cat - name: Sequencer validation - if: ${{ always() }} + if: ${{ !cancelled() }} run: bin/test_sequcheck - name: Itemized validation - if: ${{ always() }} + if: ${{ !cancelled() }} run: bin/test_itemcheck diff --git a/bin/support b/bin/support index 12a85fc00..6008dfac6 100755 --- a/bin/support +++ b/bin/support @@ -37,6 +37,10 @@ if [[ -n $UDMI_REGISTRY_SUFFIX && -n $site_model ]]; then site_model=$site_model${UDMI_REGISTRY_SUFFIX} fi +echo Lingering pubber processes: +ps ax | fgrep pubber | fgrep java || true +echo + echo tar --exclude-vcs -czf /tmp/$ARCHIVE $site_model $OUT_DIR tar --exclude-vcs -czf /tmp/$ARCHIVE $site_model $OUT_DIR mv /tmp/$ARCHIVE . diff --git a/bin/test_itemized b/bin/test_itemized index 1a7a6fb55..d9196777d 100755 --- a/bin/test_itemized +++ b/bin/test_itemized @@ -104,7 +104,11 @@ while read -u 7 action test_name pubber_opts; do echo bin/sequencer $seq_opts $SITE_PATH $PROJECT_ID $DEVICE_ID $serial_no $test_name bin/sequencer $seq_opts $SITE_PATH $PROJECT_ID $DEVICE_ID $serial_no $test_name + echo Cleaning up pubber processes... + (ps ax | fgrep pubber | fgrep java) || true kill $(ps ax | fgrep pubber | fgrep java | awk '{print $1}') || true + sleep 2 + ! (ps ax | fgrep pubber | fgrep java) || fail Unexpected lingering pubber processes egrep "(RESULT|CPBLTY) [a-z]+ [a-z.]+ $test_name[^ ]* " $SEQUENCER_OUT | while read result; do echo $test_marker ${result/* NOTICE /} >> $RESULTS_OUT diff --git a/bin/test_sequcheck b/bin/test_sequcheck index 801ef4361..21f5aad42 100755 --- a/bin/test_sequcheck +++ b/bin/test_sequcheck @@ -26,8 +26,11 @@ diff -u out/schema.out $UDMI_ROOT/etc/schema${suffix}.out || failures+="schema " cp $UDMI_ROOT/docs/specs/sequences/generated.md out/generated.tmp # Save for test/comparison bin/gencode_seq || failures+="gencode " -echo Comparing diff out/generated.tmp docs/specs/sequences/generated.md -diff -u out/generated.tmp $UDMI_ROOT/docs/specs/sequences/generated.md || failures+="gencode_seq " +mv $UDMI_ROOT/docs/specs/sequences/generated.md out/generated.out +mv out/generated.tmp $UDMI_ROOT/docs/specs/sequences/generated.md + +echo Comparing diff out/generated.out docs/specs/sequences/generated.md +diff -u out/generated.out $UDMI_ROOT/docs/specs/sequences/generated.md || failures+="gencode_seq " SAMPLE_TEST_ROW="| system | system_min_loglevel | alpha | pass | Sequence complete |" target=sites/udmi_site_model/out/devices/AHU-1/results.md diff --git a/common/src/main/java/com/google/udmi/util/DiffEntry.java b/common/src/main/java/com/google/udmi/util/DiffEntry.java new file mode 100644 index 000000000..77c67bc57 --- /dev/null +++ b/common/src/main/java/com/google/udmi/util/DiffEntry.java @@ -0,0 +1,23 @@ +package com.google.udmi.util; + +import static java.lang.String.format; + +public record DiffEntry(DiffAction action, String key, String value) { + public enum DiffAction { + ADD("Add"), + SET("Set"), + REMOVE("Remove"); + + final String value; + + DiffAction(String value) { + this.value = value; + } + } + + @Override + public String toString() { + return action == DiffAction.REMOVE ? format("%s `%s`", action().value, key) + : format("%s `%s` = %s", action().value, key, value); + } +} diff --git a/common/src/main/java/com/google/udmi/util/GeneralUtils.java b/common/src/main/java/com/google/udmi/util/GeneralUtils.java index f95d87c79..f2c7fc139 100644 --- a/common/src/main/java/com/google/udmi/util/GeneralUtils.java +++ b/common/src/main/java/com/google/udmi/util/GeneralUtils.java @@ -75,8 +75,8 @@ public static String[] arrayOf(String... args) { return args; } - public static String changedLines(List nullableChanges) { - List changes = ofNullable(nullableChanges).orElse(ImmutableList.of()); + public static String changedLines(List nullableChanges) { + List changes = ofNullable(nullableChanges).orElse(ImmutableList.of()); String terminator = changes.size() == 0 ? "." : ":"; String header = format("Changed %d fields%s%s", changes.size(), terminator, SEPARATOR); return (header + INDENTED_LINES.join(changes)).trim(); @@ -288,6 +288,10 @@ public static void ifTrueThen(Object conditional, Runnable action, Runnable } } + public static T ifNotTrueGet(Object conditional, Supplier supplier) { + return isTrue(conditional) ? null : supplier.get(); + } + public static boolean isTrue(Object value) { return Boolean.TRUE.equals(value); } diff --git a/docs/specs/sequences/generated.md b/docs/specs/sequences/generated.md index fcd610598..24e1f3913 100644 --- a/docs/specs/sequences/generated.md +++ b/docs/specs/sequences/generated.md @@ -196,4 +196,6 @@ Check that a device publishes correct software information in state messages Check that last_update state is correctly set in response to a config update. -1. Wait for state last_config matches config timestamp +1. Wait for state last_config matches first config timestamp +1. Force config update to trigger another config update +1. Wait for state last_config matches new config timestamp diff --git a/etc/schema.out b/etc/schema.out index 94a69a9b1..81a2742d0 100644 --- a/etc/schema.out +++ b/etc/schema.out @@ -1,9 +1,9 @@ -RESULT pass schemas event_pointset_alpha ALPHA 5/5 All schema validations passed -RESULT pass schemas event_pointset_beta BETA 5/5 All schema validations passed -RESULT pass schemas event_pointset_stable STABLE 5/5 All schema validations passed -RESULT pass schemas event_system_alpha ALPHA 5/5 All schema validations passed -RESULT pass schemas event_system_beta BETA 5/5 All schema validations passed -RESULT pass schemas event_system_stable STABLE 5/5 All schema validations passed -RESULT pass schemas state_update_alpha ALPHA 5/5 All schema validations passed -RESULT pass schemas state_update_beta BETA 5/5 All schema validations passed -RESULT pass schemas state_update_stable STABLE 5/5 All schema validations passed +RESULT pass schemas device_state_alpha ALPHA 5/5 Schema validation passed +RESULT pass schemas device_state_beta BETA 5/5 Schema validation passed +RESULT pass schemas device_state_stable STABLE 5/5 Schema validation passed +RESULT pass schemas event_pointset_beta BETA 5/5 Schema validation passed +RESULT pass schemas event_system_alpha ALPHA 5/5 Schema validation passed +RESULT pass schemas event_system_stable STABLE 5/5 Schema validation passed +RESULT pass schemas state_update_alpha ALPHA 5/5 Schema validation passed +RESULT pass schemas state_update_beta BETA 5/5 Schema validation passed +RESULT pass schemas state_update_stable STABLE 5/5 Schema validation passed diff --git a/etc/schema_itemized.out b/etc/schema_itemized.out index de48626c6..4bcf7cae0 100644 --- a/etc/schema_itemized.out +++ b/etc/schema_itemized.out @@ -4,29 +4,37 @@ 04 pointset_remove_point 05 too_much_state 06 feature_enumeration -07 valid_serial_no RESULT pass schemas event_pointset_alpha ALPHA 5/5 All schema validations passed -07 valid_serial_no RESULT pass schemas event_system_alpha ALPHA 5/5 All schema validations passed -07 valid_serial_no RESULT pass schemas state_update_alpha ALPHA 5/5 All schema validations passed +07 valid_serial_no RESULT pass schemas device_state_alpha ALPHA 5/5 Schema validation passed +07 valid_serial_no RESULT pass schemas event_system_alpha ALPHA 5/5 Schema validation passed +07 valid_serial_no RESULT pass schemas state_update_alpha ALPHA 5/5 Schema validation passed 08 writeback_success 09 writeback_success 10 pointset_sample_rate 11 system_mode_restart 12 config_logging 13 broken_config -14 broken_config +14 broken_config RESULT pass schemas device_state_alpha ALPHA 5/5 Schema validation passed +14 broken_config RESULT pass schemas event_system_alpha ALPHA 5/5 Schema validation passed +14 broken_config RESULT pass schemas state_update_alpha ALPHA 5/5 Schema validation passed 15 broken_config -16 broken_config +16 broken_config RESULT pass schemas device_state_alpha ALPHA 5/5 Schema validation passed +16 broken_config RESULT pass schemas event_system_alpha ALPHA 5/5 Schema validation passed +16 broken_config RESULT pass schemas state_update_alpha ALPHA 5/5 Schema validation passed 17 broken_config -18 system_last_update RESULT fail schemas event_pointset_stable STABLE 5/5 Schema violations found 18 system_last_update RESULT fail schemas state_update_stable STABLE 5/5 Schema violations found -18 system_last_update RESULT pass schemas event_system_stable STABLE 5/5 All schema validations passed -19 state_make_model -23 valid_serial_no -24 system_last_update -25 system_min_loglevel +18 system_last_update RESULT pass schemas device_state_stable STABLE 5/5 Schema validation passed +18 system_last_update RESULT pass schemas event_system_stable STABLE 5/5 Schema validation passed +19 system_last_update RESULT fail schemas device_state_stable STABLE 5/5 Schema violations found +19 system_last_update RESULT pass schemas event_system_stable STABLE 5/5 Schema validation passed +19 system_last_update RESULT pass schemas state_update_stable STABLE 5/5 Schema validation passed +20 state_make_model +24 valid_serial_no +25 system_last_update 26 system_min_loglevel -30 gateway_proxy_events +27 system_min_loglevel RESULT pass schemas device_state_alpha ALPHA 5/5 Schema validation passed +27 system_min_loglevel RESULT pass schemas event_system_alpha ALPHA 5/5 Schema validation passed 31 gateway_proxy_events -35 gateway_proxy_events +32 gateway_proxy_events 36 gateway_proxy_events -40 device_config_acked +37 gateway_proxy_events +41 device_config_acked diff --git a/etc/schema_nostate.out b/etc/schema_nostate.out index 33c0fe7b9..de6cd983e 100644 --- a/etc/schema_nostate.out +++ b/etc/schema_nostate.out @@ -1,2 +1,3 @@ -RESULT pass schemas event_pointset_beta BETA 5/5 All schema validations passed -RESULT pass schemas event_system_beta BETA 5/5 All schema validations passed +RESULT pass schemas device_state_beta BETA 5/5 Schema validation passed +RESULT pass schemas event_pointset_beta BETA 5/5 Schema validation passed +RESULT pass schemas event_system_beta BETA 5/5 Schema validation passed diff --git a/etc/sequencer_nostate.out b/etc/sequencer_nostate.out index 9d64ffbc6..5d1a6e6fc 100644 --- a/etc/sequencer_nostate.out +++ b/etc/sequencer_nostate.out @@ -10,4 +10,4 @@ RESULT skip pointset pointset_request_extraneous BETA 0/0 State testing disabled RESULT skip pointset pointset_sample_rate BETA 0/0 State testing disabled RESULT skip system state_make_model BETA 0/0 State testing disabled RESULT skip system state_software BETA 0/0 State testing disabled -RESULT fail system system_last_update STABLE 0/5 Timeout waiting for state last_config matches config timestamp +RESULT fail system system_last_update STABLE 0/5 Timeout waiting for state last_config matches first config timestamp diff --git a/etc/test_itemized.in b/etc/test_itemized.in index 6ce600544..20cbfff5d 100644 --- a/etc/test_itemized.in +++ b/etc/test_itemized.in @@ -16,6 +16,7 @@ TEST broken_config noState TEST broken_config noLog TEST broken_config badCategory TEST system_last_update extraField=fnooz +TEST system_last_update dupeState TEST state_make_model badState # Test with various device metadata.json options diff --git a/etc/test_itemized.out b/etc/test_itemized.out index 686d3ece5..6c89aa354 100644 --- a/etc/test_itemized.out +++ b/etc/test_itemized.out @@ -21,13 +21,14 @@ 17 CPBLTY fail system broken_config.logging ALPHA 0/1 Timeout after 30s waiting for log category `system.config.receive` level `DEBUG` to be logged 17 RESULT fail system broken_config ALPHA 0/5 expected: but was: 18 RESULT pass system system_last_update STABLE 5/5 Sequence complete -19 RESULT fail system state_make_model BETA 0/5 Timeout waiting for no applicable system status -23 RESULT skip system valid_serial_no ALPHA 0/0 State testing disabled -24 RESULT fail system system_last_update STABLE 0/5 Timeout waiting for state last_config matches config timestamp -25 RESULT fail system system_min_loglevel ALPHA 0/5 Received state update with no-state device -26 RESULT pass system system_min_loglevel ALPHA 5/5 Sequence complete -30 RESULT pass gateway gateway_proxy_events ALPHA 5/5 Sequence complete -31 RESULT fail gateway gateway_proxy_events ALPHA 0/5 Timeout waiting for Missing data from AHU-22 -35 RESULT pass gateway gateway_proxy_events ALPHA 5/5 Sequence complete -36 RESULT fail gateway gateway_proxy_events ALPHA 0/5 Received state update with no-state device -40 RESULT skip system device_config_acked BETA 0/0 No config check for proxy device +19 RESULT pass system system_last_update STABLE 5/5 Sequence complete +20 RESULT fail system state_make_model BETA 0/5 Timeout waiting for no applicable system status +24 RESULT skip system valid_serial_no ALPHA 0/0 State testing disabled +25 RESULT fail system system_last_update STABLE 0/5 Timeout waiting for state last_config matches first config timestamp +26 RESULT fail system system_min_loglevel ALPHA 0/5 Received state update with no-state device +27 RESULT pass system system_min_loglevel ALPHA 5/5 Sequence complete +31 RESULT pass gateway gateway_proxy_events ALPHA 5/5 Sequence complete +32 RESULT fail gateway gateway_proxy_events ALPHA 0/5 Timeout waiting for Missing data from AHU-22 +36 RESULT pass gateway gateway_proxy_events ALPHA 5/5 Sequence complete +37 RESULT fail gateway gateway_proxy_events ALPHA 0/5 Received state update with no-state device +41 RESULT skip system device_config_acked BETA 0/0 No config check for proxy device diff --git a/gencode/docs/configuration_pubber.html b/gencode/docs/configuration_pubber.html index 9a60580fe..7236c91d7 100644 --- a/gencode/docs/configuration_pubber.html +++ b/gencode/docs/configuration_pubber.html @@ -1417,6 +1417,46 @@

+ + + + +
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + +
@@ -2177,6 +2217,46 @@

+

+ + + +
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + +
diff --git a/gencode/java/udmi/schema/PubberOptions.java b/gencode/java/udmi/schema/PubberOptions.java index 92c457a35..2c8edea97 100644 --- a/gencode/java/udmi/schema/PubberOptions.java +++ b/gencode/java/udmi/schema/PubberOptions.java @@ -21,6 +21,7 @@ "noConfigAck", "noPersist", "noLastStart", + "noLastConfig", "badCategory", "badVersion", "noProxy", @@ -40,6 +41,7 @@ "spamState", "tweakState", "badState", + "dupeState", "noLog", "featureEnableSwap", "disableWriteback", @@ -60,6 +62,8 @@ public class PubberOptions { public Boolean noPersist; @JsonProperty("noLastStart") public Boolean noLastStart; + @JsonProperty("noLastConfig") + public Boolean noLastConfig; @JsonProperty("badCategory") public Boolean badCategory; @JsonProperty("badVersion") @@ -98,6 +102,8 @@ public class PubberOptions { public Boolean tweakState; @JsonProperty("badState") public Boolean badState; + @JsonProperty("dupeState") + public Boolean dupeState; @JsonProperty("noLog") public Boolean noLog; @JsonProperty("featureEnableSwap") @@ -126,6 +132,7 @@ public int hashCode() { int result = 1; result = ((result* 31)+((this.skewClock == null)? 0 :this.skewClock.hashCode())); result = ((result* 31)+((this.noPersist == null)? 0 :this.noPersist.hashCode())); + result = ((result* 31)+((this.noLastConfig == null)? 0 :this.noLastConfig.hashCode())); result = ((result* 31)+((this.noLog == null)? 0 :this.noLog.hashCode())); result = ((result* 31)+((this.noHardware == null)? 0 :this.noHardware.hashCode())); result = ((result* 31)+((this.messageTrace == null)? 0 :this.messageTrace.hashCode())); @@ -148,6 +155,7 @@ public int hashCode() { result = ((result* 31)+((this.extraField == null)? 0 :this.extraField.hashCode())); result = ((result* 31)+((this.emptyMissing == null)? 0 :this.emptyMissing.hashCode())); result = ((result* 31)+((this.fixedSampleRate == null)? 0 :this.fixedSampleRate.hashCode())); + result = ((result* 31)+((this.dupeState == null)? 0 :this.dupeState.hashCode())); result = ((result* 31)+((this.featureEnableSwap == null)? 0 :this.featureEnableSwap.hashCode())); result = ((result* 31)+((this.extraDevice == null)? 0 :this.extraDevice.hashCode())); result = ((result* 31)+((this.noConfigAck == null)? 0 :this.noConfigAck.hashCode())); @@ -166,7 +174,7 @@ public boolean equals(Object other) { return false; } PubberOptions rhs = ((PubberOptions) other); - return (((((((((((((((((((((((((((((((this.skewClock == rhs.skewClock)||((this.skewClock!= null)&&this.skewClock.equals(rhs.skewClock)))&&((this.noPersist == rhs.noPersist)||((this.noPersist!= null)&&this.noPersist.equals(rhs.noPersist))))&&((this.noLog == rhs.noLog)||((this.noLog!= null)&&this.noLog.equals(rhs.noLog))))&&((this.noHardware == rhs.noHardware)||((this.noHardware!= null)&&this.noHardware.equals(rhs.noHardware))))&&((this.messageTrace == rhs.messageTrace)||((this.messageTrace!= null)&&this.messageTrace.equals(rhs.messageTrace))))&&((this.softwareFirmwareValue == rhs.softwareFirmwareValue)||((this.softwareFirmwareValue!= null)&&this.softwareFirmwareValue.equals(rhs.softwareFirmwareValue))))&&((this.noWriteback == rhs.noWriteback)||((this.noWriteback!= null)&&this.noWriteback.equals(rhs.noWriteback))))&&((this.noLastStart == rhs.noLastStart)||((this.noLastStart!= null)&&this.noLastStart.equals(rhs.noLastStart))))&&((this.tweakState == rhs.tweakState)||((this.tweakState!= null)&&this.tweakState.equals(rhs.tweakState))))&&((this.spamState == rhs.spamState)||((this.spamState!= null)&&this.spamState.equals(rhs.spamState))))&&((this.noState == rhs.noState)||((this.noState!= null)&&this.noState.equals(rhs.noState))))&&((this.badState == rhs.badState)||((this.badState!= null)&&this.badState.equals(rhs.badState))))&&((this.noProxy == rhs.noProxy)||((this.noProxy!= null)&&this.noProxy.equals(rhs.noProxy))))&&((this.missingPoint == rhs.missingPoint)||((this.missingPoint!= null)&&this.missingPoint.equals(rhs.missingPoint))))&&((this.badCategory == rhs.badCategory)||((this.badCategory!= null)&&this.badCategory.equals(rhs.badCategory))))&&((this.extraPoint == rhs.extraPoint)||((this.extraPoint!= null)&&this.extraPoint.equals(rhs.extraPoint))))&&((this.smokeCheck == rhs.smokeCheck)||((this.smokeCheck!= null)&&this.smokeCheck.equals(rhs.smokeCheck))))&&((this.redirectRegistry == rhs.redirectRegistry)||((this.redirectRegistry!= null)&&this.redirectRegistry.equals(rhs.redirectRegistry))))&&((this.noPointState == rhs.noPointState)||((this.noPointState!= null)&&this.noPointState.equals(rhs.noPointState))))&&((this.disableWriteback == rhs.disableWriteback)||((this.disableWriteback!= null)&&this.disableWriteback.equals(rhs.disableWriteback))))&&((this.barfConfig == rhs.barfConfig)||((this.barfConfig!= null)&&this.barfConfig.equals(rhs.barfConfig))))&&((this.extraField == rhs.extraField)||((this.extraField!= null)&&this.extraField.equals(rhs.extraField))))&&((this.emptyMissing == rhs.emptyMissing)||((this.emptyMissing!= null)&&this.emptyMissing.equals(rhs.emptyMissing))))&&((this.fixedSampleRate == rhs.fixedSampleRate)||((this.fixedSampleRate!= null)&&this.fixedSampleRate.equals(rhs.fixedSampleRate))))&&((this.featureEnableSwap == rhs.featureEnableSwap)||((this.featureEnableSwap!= null)&&this.featureEnableSwap.equals(rhs.featureEnableSwap))))&&((this.extraDevice == rhs.extraDevice)||((this.extraDevice!= null)&&this.extraDevice.equals(rhs.extraDevice))))&&((this.noConfigAck == rhs.noConfigAck)||((this.noConfigAck!= null)&&this.noConfigAck.equals(rhs.noConfigAck))))&&((this.badVersion == rhs.badVersion)||((this.badVersion!= null)&&this.badVersion.equals(rhs.badVersion))))&&((this.fixedLogLevel == rhs.fixedLogLevel)||((this.fixedLogLevel!= null)&&this.fixedLogLevel.equals(rhs.fixedLogLevel))))&&((this.configStateDelay == rhs.configStateDelay)||((this.configStateDelay!= null)&&this.configStateDelay.equals(rhs.configStateDelay)))); + return (((((((((((((((((((((((((((((((((this.skewClock == rhs.skewClock)||((this.skewClock!= null)&&this.skewClock.equals(rhs.skewClock)))&&((this.noPersist == rhs.noPersist)||((this.noPersist!= null)&&this.noPersist.equals(rhs.noPersist))))&&((this.noLastConfig == rhs.noLastConfig)||((this.noLastConfig!= null)&&this.noLastConfig.equals(rhs.noLastConfig))))&&((this.noLog == rhs.noLog)||((this.noLog!= null)&&this.noLog.equals(rhs.noLog))))&&((this.noHardware == rhs.noHardware)||((this.noHardware!= null)&&this.noHardware.equals(rhs.noHardware))))&&((this.messageTrace == rhs.messageTrace)||((this.messageTrace!= null)&&this.messageTrace.equals(rhs.messageTrace))))&&((this.softwareFirmwareValue == rhs.softwareFirmwareValue)||((this.softwareFirmwareValue!= null)&&this.softwareFirmwareValue.equals(rhs.softwareFirmwareValue))))&&((this.noWriteback == rhs.noWriteback)||((this.noWriteback!= null)&&this.noWriteback.equals(rhs.noWriteback))))&&((this.noLastStart == rhs.noLastStart)||((this.noLastStart!= null)&&this.noLastStart.equals(rhs.noLastStart))))&&((this.tweakState == rhs.tweakState)||((this.tweakState!= null)&&this.tweakState.equals(rhs.tweakState))))&&((this.spamState == rhs.spamState)||((this.spamState!= null)&&this.spamState.equals(rhs.spamState))))&&((this.noState == rhs.noState)||((this.noState!= null)&&this.noState.equals(rhs.noState))))&&((this.badState == rhs.badState)||((this.badState!= null)&&this.badState.equals(rhs.badState))))&&((this.noProxy == rhs.noProxy)||((this.noProxy!= null)&&this.noProxy.equals(rhs.noProxy))))&&((this.missingPoint == rhs.missingPoint)||((this.missingPoint!= null)&&this.missingPoint.equals(rhs.missingPoint))))&&((this.badCategory == rhs.badCategory)||((this.badCategory!= null)&&this.badCategory.equals(rhs.badCategory))))&&((this.extraPoint == rhs.extraPoint)||((this.extraPoint!= null)&&this.extraPoint.equals(rhs.extraPoint))))&&((this.smokeCheck == rhs.smokeCheck)||((this.smokeCheck!= null)&&this.smokeCheck.equals(rhs.smokeCheck))))&&((this.redirectRegistry == rhs.redirectRegistry)||((this.redirectRegistry!= null)&&this.redirectRegistry.equals(rhs.redirectRegistry))))&&((this.noPointState == rhs.noPointState)||((this.noPointState!= null)&&this.noPointState.equals(rhs.noPointState))))&&((this.disableWriteback == rhs.disableWriteback)||((this.disableWriteback!= null)&&this.disableWriteback.equals(rhs.disableWriteback))))&&((this.barfConfig == rhs.barfConfig)||((this.barfConfig!= null)&&this.barfConfig.equals(rhs.barfConfig))))&&((this.extraField == rhs.extraField)||((this.extraField!= null)&&this.extraField.equals(rhs.extraField))))&&((this.emptyMissing == rhs.emptyMissing)||((this.emptyMissing!= null)&&this.emptyMissing.equals(rhs.emptyMissing))))&&((this.fixedSampleRate == rhs.fixedSampleRate)||((this.fixedSampleRate!= null)&&this.fixedSampleRate.equals(rhs.fixedSampleRate))))&&((this.dupeState == rhs.dupeState)||((this.dupeState!= null)&&this.dupeState.equals(rhs.dupeState))))&&((this.featureEnableSwap == rhs.featureEnableSwap)||((this.featureEnableSwap!= null)&&this.featureEnableSwap.equals(rhs.featureEnableSwap))))&&((this.extraDevice == rhs.extraDevice)||((this.extraDevice!= null)&&this.extraDevice.equals(rhs.extraDevice))))&&((this.noConfigAck == rhs.noConfigAck)||((this.noConfigAck!= null)&&this.noConfigAck.equals(rhs.noConfigAck))))&&((this.badVersion == rhs.badVersion)||((this.badVersion!= null)&&this.badVersion.equals(rhs.badVersion))))&&((this.fixedLogLevel == rhs.fixedLogLevel)||((this.fixedLogLevel!= null)&&this.fixedLogLevel.equals(rhs.fixedLogLevel))))&&((this.configStateDelay == rhs.configStateDelay)||((this.configStateDelay!= null)&&this.configStateDelay.equals(rhs.configStateDelay)))); } } diff --git a/gencode/python/udmi/schema/options_pubber.py b/gencode/python/udmi/schema/options_pubber.py index 4e0956db7..89f74f9de 100644 --- a/gencode/python/udmi/schema/options_pubber.py +++ b/gencode/python/udmi/schema/options_pubber.py @@ -10,6 +10,7 @@ def __init__(self): self.noConfigAck = None self.noPersist = None self.noLastStart = None + self.noLastConfig = None self.badCategory = None self.badVersion = None self.noProxy = None @@ -29,6 +30,7 @@ def __init__(self): self.spamState = None self.tweakState = None self.badState = None + self.dupeState = None self.noLog = None self.featureEnableSwap = None self.disableWriteback = None @@ -46,6 +48,7 @@ def from_dict(source): result.noConfigAck = source.get('noConfigAck') result.noPersist = source.get('noPersist') result.noLastStart = source.get('noLastStart') + result.noLastConfig = source.get('noLastConfig') result.badCategory = source.get('badCategory') result.badVersion = source.get('badVersion') result.noProxy = source.get('noProxy') @@ -65,6 +68,7 @@ def from_dict(source): result.spamState = source.get('spamState') result.tweakState = source.get('tweakState') result.badState = source.get('badState') + result.dupeState = source.get('dupeState') result.noLog = source.get('noLog') result.featureEnableSwap = source.get('featureEnableSwap') result.disableWriteback = source.get('disableWriteback') @@ -101,6 +105,8 @@ def to_dict(self): result['noPersist'] = self.noPersist # 5 if self.noLastStart: result['noLastStart'] = self.noLastStart # 5 + if self.noLastConfig: + result['noLastConfig'] = self.noLastConfig # 5 if self.badCategory: result['badCategory'] = self.badCategory # 5 if self.badVersion: @@ -139,6 +145,8 @@ def to_dict(self): result['tweakState'] = self.tweakState # 5 if self.badState: result['badState'] = self.badState # 5 + if self.dupeState: + result['dupeState'] = self.dupeState # 5 if self.noLog: result['noLog'] = self.noLog # 5 if self.featureEnableSwap: diff --git a/pubber/src/main/java/daq/pubber/Pubber.java b/pubber/src/main/java/daq/pubber/Pubber.java index d05091121..010a3e054 100644 --- a/pubber/src/main/java/daq/pubber/Pubber.java +++ b/pubber/src/main/java/daq/pubber/Pubber.java @@ -35,6 +35,7 @@ import static udmi.schema.BlobsetConfig.SystemBlobsets.IOT_ENDPOINT_CONFIG; import static udmi.schema.EndpointConfiguration.Protocol.MQTT; +import com.google.api.services.cloudiot.v1.model.DeviceState; import com.google.common.collect.ImmutableMap; import com.google.daq.mqtt.util.CatchingScheduledThreadPoolExecutor; import com.google.udmi.util.GeneralUtils; @@ -129,7 +130,7 @@ public class Pubber extends ManagerBase implements ManagerHost { .put(SystemEvent.class, getEventsSuffix("system")) .put(PointsetEvent.class, getEventsSuffix("pointset")) .put(ExtraPointsetEvent.class, getEventsSuffix("pointset")) - .put(InjectedMessage.class, getEventsSuffix("invalid")) + .put(InjectedMessage.class, getEventsSuffix("racoon")) .put(InjectedState.class, MqttDevice.STATE_TOPIC) .put(DiscoveryEvent.class, getEventsSuffix("discovery")) .build(); @@ -481,6 +482,7 @@ public void update(Object update) { publishSynchronousState(); } else if (checkTarget instanceof SystemState) { deviceState.system = (SystemState) checkValue; + ifTrueThen(options.dupeState, () -> sendDupeState()); } else if (checkTarget instanceof PointsetState) { deviceState.pointset = (PointsetState) checkValue; } else if (checkTarget instanceof LocalnetState) { @@ -496,6 +498,14 @@ public void update(Object update) { markStateDirty(); } + private void sendDupeState() { + State dupeState = new State(); + dupeState.system = deviceState.system; + dupeState.timestamp = deviceState.timestamp; + dupeState.version = deviceState.version; + publishStateMessage(dupeState); + } + @Override public void publish(Object message) { publishDeviceMessage(message); @@ -1167,6 +1177,15 @@ private void publishStateMessage() { } private void publishStateMessage(Object stateToSend) { + try { + stateLock.lock(); + publishStateMessageRaw(stateToSend); + } finally { + stateLock.unlock(); + } + } + + private void publishStateMessageRaw(Object stateToSend) { if (configLatch == null || configLatch.getCount() > 0) { warn("Dropping state update until config received..."); return; diff --git a/pubber/src/main/java/daq/pubber/SystemManager.java b/pubber/src/main/java/daq/pubber/SystemManager.java index 82762ab16..df55ed308 100644 --- a/pubber/src/main/java/daq/pubber/SystemManager.java +++ b/pubber/src/main/java/daq/pubber/SystemManager.java @@ -5,6 +5,7 @@ import static com.google.udmi.util.GeneralUtils.getTimestamp; import static com.google.udmi.util.GeneralUtils.ifNotNullGet; import static com.google.udmi.util.GeneralUtils.ifNotNullThen; +import static com.google.udmi.util.GeneralUtils.ifNotTrueGet; import static com.google.udmi.util.GeneralUtils.ifNotTrueThen; import static com.google.udmi.util.GeneralUtils.isTrue; import static com.google.udmi.util.JsonUtil.isoConvert; @@ -224,7 +225,7 @@ public void setPersistentData(DevicePersistent persistentData) { void updateConfig(SystemConfig system, Date timestamp) { systemConfig = system; - systemState.last_config = timestamp; + systemState.last_config = ifNotTrueGet(options.noLastConfig, () -> timestamp); updateInterval(ifNotNullGet(system, config -> config.metrics_rate_sec)); updateState(); } diff --git a/schema/options_pubber.json b/schema/options_pubber.json index f9bce8731..d037dbaab 100644 --- a/schema/options_pubber.json +++ b/schema/options_pubber.json @@ -19,6 +19,9 @@ "noLastStart": { "type": "boolean" }, + "noLastConfig": { + "type": "boolean" + }, "badCategory": { "type": "boolean" }, @@ -76,6 +79,9 @@ "badState": { "type": "boolean" }, + "dupeState": { + "type": "boolean" + }, "noLog": { "type": "boolean" }, diff --git a/udmis/build.gradle b/udmis/build.gradle index 0eeb27454..67e989ca9 100644 --- a/udmis/build.gradle +++ b/udmis/build.gradle @@ -92,7 +92,7 @@ dependencies { exclude group: 'com.google.guava', module: 'guava-jdk5' } - implementation ('io.github.clearblade:clearblade-cloud-iot:1.0.2') { + implementation ('io.github.clearblade:clearblade-cloud-iot:1.0.3') { // Exclude transitive dependency causing problems with running unit tests in the IDE. exclude group: 'org.junit.jupiter', module: 'junit-jupiter-api' } diff --git a/validator/sequences/broken_config/sequence.md b/validator/sequences/broken_config/sequence.md index 449261e31..52babad66 100644 --- a/validator/sequences/broken_config/sequence.md +++ b/validator/sequences/broken_config/sequence.md @@ -14,6 +14,7 @@ Check that the device correctly handles a broken (non-json) config message. 1. Wait for log category `system.config.parse` level `ERROR` to be logged 1. Check that log category `system.config.apply` level `NOTICE` not logged 1. Force reset config +1. Wait for state last_config sync 1. Wait for log category `system.config.apply` level `NOTICE` to be logged 1. Wait for restored state synchronized 1. Update config before last_config updated: diff --git a/validator/sequences/system_last_update/sequence.md b/validator/sequences/system_last_update/sequence.md index 2909b72ad..528500e8f 100644 --- a/validator/sequences/system_last_update/sequence.md +++ b/validator/sequences/system_last_update/sequence.md @@ -3,4 +3,6 @@ Check that last_update state is correctly set in response to a config update. -1. Wait for state last_config matches config timestamp +1. Wait for state last_config matches first config timestamp +1. Force config update to trigger another config update +1. Wait for state last_config matches new config timestamp diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/PointsetBase.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/PointsetBase.java index ab6314739..5a78b292d 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/PointsetBase.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/PointsetBase.java @@ -53,5 +53,4 @@ protected TargetTestingModel getTarget(String target) { } return testingMetadata; } - } diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/SequenceBase.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/SequenceBase.java index 5a410c32f..d10345359 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/SequenceBase.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/SequenceBase.java @@ -3,17 +3,19 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Predicates.not; import static com.google.common.base.Strings.emptyToNull; +import static com.google.daq.mqtt.sequencer.SequenceBase.Capabilities.LAST_CONFIG; import static com.google.daq.mqtt.sequencer.semantic.SemanticValue.actualize; import static com.google.daq.mqtt.util.IotReflectorClient.REFLECTOR_PREFIX; -import static com.google.daq.mqtt.validator.Validator.CONFIG_PREFIX; -import static com.google.daq.mqtt.validator.Validator.STATE_PREFIX; import static com.google.udmi.util.CleanDateFormat.cleanDate; import static com.google.udmi.util.CleanDateFormat.dateEquals; import static com.google.udmi.util.Common.DEVICE_ID_KEY; import static com.google.udmi.util.Common.EXCEPTION_KEY; import static com.google.udmi.util.Common.GATEWAY_ID_KEY; import static com.google.udmi.util.Common.TIMESTAMP_KEY; +import static com.google.udmi.util.GeneralUtils.CSV_JOINER; +import static com.google.udmi.util.GeneralUtils.catchToElse; import static com.google.udmi.util.GeneralUtils.changedLines; import static com.google.udmi.util.GeneralUtils.friendlyStackTrace; import static com.google.udmi.util.GeneralUtils.getTimestamp; @@ -60,6 +62,7 @@ import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.daq.mqtt.sequencer.semantic.SemanticDate; import com.google.daq.mqtt.sequencer.semantic.SemanticValue; import com.google.daq.mqtt.util.MessagePublisher; @@ -71,6 +74,7 @@ import com.google.daq.mqtt.validator.Validator.MessageBundle; import com.google.udmi.util.CleanDateFormat; import com.google.udmi.util.Common; +import com.google.udmi.util.DiffEntry; import com.google.udmi.util.GeneralUtils; import com.google.udmi.util.JsonUtil; import com.google.udmi.util.SiteModel; @@ -159,6 +163,9 @@ public class SequenceBase { public static final String SCHEMA_BUCKET = "schemas"; public static final int SCHEMA_SCORE = 5; public static final int CAPABILITY_SCORE = 1; + public static final String STATUS_LEVEL_VIOLATION = "STATUS_LEVEL"; + public static final String DEVICE_STATE_SCHEMA = "device_state"; + private static final String ALL_CHANGES = ""; private static final int FUNCTIONS_VERSION_BETA = 11; private static final int FUNCTIONS_VERSION_ALPHA = FUNCTIONS_VERSION_BETA; private static final long CONFIG_BARRIER_MS = 1000; @@ -198,7 +205,7 @@ public class SequenceBase { private static final int LOG_TIMEOUT_SEC = 10; private static final long ONE_SECOND_MS = 1000; private static final int EXIT_CODE_PRESERVE = -9; - private static final String SYSTEM_TESTING_MARKER = " `system.testing"; + private static final String SYSTEM_TESTING_MARKER = "system.testing"; private static final BiMap RESULT_LEVEL_MAP = ImmutableBiMap.of( SequenceResult.START, Level.INFO, SKIP, Level.WARNING, @@ -222,8 +229,11 @@ public class SequenceBase { STATE_QUERY_CUTOFF_SEC)); private static final String FAKE_DEVICE_ID = "TAP-1"; private static final String NO_EXTRA_DETAIL = ""; + private static final Duration DEFAULT_WAIT_TIME = Duration.ofSeconds(10); private static final Duration LOG_WAIT_TIME = Duration.ofSeconds(30); - private static final Duration DEFAULT_WAIT_TIMEOUT = Duration.ofHours(30); + private static final Duration DEFAULT_LOOP_TIMEOUT = Duration.ofHours(30); + private static final Set SYSTEM_STATE_CHANGES = ImmutableSet.of( + "timestamp", "system.last_config", "system.status"); protected static Metadata deviceMetadata; protected static String projectId; protected static String cloudRegion; @@ -258,7 +268,10 @@ public class SequenceBase { private final Queue logEntryQueue = new LinkedBlockingDeque<>(); private final Stack waitingCondition = new Stack<>(); private final SortedMap> validationResults = new TreeMap<>(); + private final Map deviceStateViolations = new ConcurrentHashMap<>(); private final Map capabilityExceptions = new ConcurrentHashMap<>(); + private final Set allowedDeviceStateChanges = new HashSet<>(); + @Rule public Timeout globalTimeout = new Timeout(NORM_TIMEOUT_MS, TimeUnit.MILLISECONDS); @Rule @@ -266,6 +279,7 @@ public class SequenceBase { protected State deviceState; protected boolean configAcked; protected String lastSerialNo; + private int maxAllowedStatusLevel; private String extraField; private Instant lastConfigUpdate; private boolean enforceSerial; @@ -287,6 +301,7 @@ public class SequenceBase { private int startStateCount; private Boolean expectedSystemStatus; private Description testDescription; + private SubFolder testSchema; private static void setupSequencer() { exeConfig = SequenceRunner.ensureExecutionConfig(); @@ -458,16 +473,6 @@ private static String makeMessageBase(Envelope attributes) { return format("%s_%s%s", subType, subFolder, deviceSuffix); } - @NotNull - private static Predicate>> isInterestingValidation() { - return entry -> isInterestingValidation(entry.getKey()); - } - - private static boolean isInterestingValidation(String schemaName) { - return !schemaName.startsWith(CONFIG_PREFIX) - && (!schemaName.startsWith(STATE_PREFIX) || schemaName.equals(STATE_UPDATE_MESSAGE_TYPE)); - } - private static void emitSequenceResult(SequenceResult result, String bucket, String name, String stage, int score, int total, String message) { emitSequencerOut(format(RESULT_FORMAT, result, bucket, name, stage, score, total, message)); @@ -521,7 +526,7 @@ private static Entry summarizeSchemaResults( logEntry.message = "No messages validated"; logEntry.level = WARNING.value(); } else if (failures.isEmpty()) { - logEntry.message = "All schema validations passed"; + logEntry.message = "Schema validation passed"; logEntry.level = NOTICE.value(); } else { logEntry.message = "Schema violations found"; @@ -566,12 +571,23 @@ private static Map getCapabilities(Description desc) { List list = ofNullable(all).map(array -> Arrays.asList(all.value())) .orElseGet(ArrayList::new); ifNotNullThen(desc.getAnnotation(Capability.class), list::add); - return list.stream().collect(Collectors.toMap(Capability::value, cap -> cap)); + return list.stream() + .collect(Collectors.toMap(Capability::value, cap -> cap)); } catch (Exception e) { throw new RuntimeException("While extracting capabilities for " + desc.getMethodName(), e); } } + @NotNull + private Predicate>> isInterestingValidation() { + return entry -> isInterestingValidation(entry.getKey()); + } + + private boolean isInterestingValidation(String schemaName) { + String targetSchema = format("event_%s", ifNotNullGet(testSchema, SubFolder::value)); + return schemaName.equals(targetSchema) || schemaName.equals(STATE_UPDATE_MESSAGE_TYPE); + } + private Map.Entry emitCapabilityResult(Capabilities capability, Exception state, Capability cap, Bucket bucket, String methodName) { boolean pass = state instanceof CapabilitySuccess; @@ -696,6 +712,7 @@ public void setUp() { assumeTrue("Feature bucket not enabled", isBucketEnabled(testBucket)); assertTrue("exceptions map should be empty", capabilityExceptions.isEmpty()); + assertTrue("allowed changes map should be empty", allowedDeviceStateChanges.isEmpty()); if (!deviceSupportsState()) { boolean featureDisabled = ifNotNullGet(testDescription.getAnnotation(Feature.class), @@ -715,6 +732,7 @@ public void setUp() { enforceSerial = false; recordMessages = true; recordSequence = false; + maxAllowedStatusLevel = NOTICE.value(); resetConfig(resetRequired); @@ -732,12 +750,19 @@ public void setUp() { clearReceivedEvents(); validationResults.clear(); + waitForStateConfigSync(); + recordSequence = true; waitingConditionPush("executing test"); debug(format("stage begin %s at %s", currentWaitingCondition(), timeSinceStart())); } + private void waitForStateConfigSync() { + forCapability(LAST_CONFIG, + () -> waitFor("state last_config sync", this::lastConfigUpdatedString)); + } + private boolean deviceSupportsState() { return !isTrue(catchToNull(() -> deviceMetadata.testing.nostate)); } @@ -758,6 +783,8 @@ protected void resetConfig() { } protected void resetConfig(boolean fullReset) { + allowDeviceStateChange(ALL_CHANGES); + recordSequence("Force reset config"); withRecordSequence(false, () -> { debug("Starting reset_config full reset " + fullReset); @@ -775,6 +802,9 @@ protected void resetConfig(boolean fullReset) { debug("Done with reset_config"); resetRequired = false; }); + + waitForStateConfigSync(); + disallowDeviceStateChange(ALL_CHANGES); } private void waitForConfigSync() { @@ -793,8 +823,8 @@ private void recordResult(SequenceResult result, Description description, String Feature feature = description.getAnnotation(Feature.class); Map capabilities = getCapabilities(description); Bucket bucket = getBucket(feature); - String stage = (feature == null ? Feature.DEFAULT_STAGE : feature.stage()).name(); - int base = (feature == null ? Feature.DEFAULT_SCORE : feature.score()); + final String stage = (feature == null ? Feature.DEFAULT_STAGE : feature.stage()).name(); + final int base = (feature == null ? Feature.DEFAULT_SCORE : feature.score()); boolean isSkip = result == SKIP; boolean isPass = result == PASS; @@ -802,6 +832,11 @@ private void recordResult(SequenceResult result, Description description, String AtomicInteger total = new AtomicInteger(isSkip ? 0 : base); AtomicInteger score = new AtomicInteger(isPass ? base : 0); + if (!capabilities.containsKey(LAST_CONFIG)) { + debug("Removing implicit system capability LAST_CONFIG"); + capabilityExceptions.remove(LAST_CONFIG); + } + ifTrueThen(isPass, () -> assertEquals("executed test capabilities", capabilities.keySet(), capabilityExceptions.keySet())); @@ -835,6 +870,12 @@ private void recordSchemaValidations(Description description) { collectSchemaResult(description, schemaName, FAIL, result.detail)); } }); + + SequenceResult result = deviceStateViolations.isEmpty() ? PASS : FAIL; + String message = result == PASS + ? "Only expected device state changes observed" + : "Unexpected device state changes: " + CSV_JOINER.join(deviceStateViolations.keySet()); + collectSchemaResult(description, DEVICE_STATE_SCHEMA, result, message); } private String uniqueKey(Entry entry) { @@ -1139,8 +1180,8 @@ private void captureConfigChange(String reason) { String header = format("Update config%s: ", suffix); debug(header + isoConvert(deviceConfig.timestamp)); recordRawMessage(deviceConfig, LOCAL_CONFIG_UPDATE); - List allDiffs = SENT_CONFIG_DIFFERNATOR.computeChanges(deviceConfig); - List filteredDiffs = filterTesting(allDiffs); + List allDiffs = SENT_CONFIG_DIFFERNATOR.computeChanges(deviceConfig); + List filteredDiffs = filterTesting(allDiffs); if (!filteredDiffs.isEmpty()) { recordSequence(header); filteredDiffs.forEach(this::recordBullet); @@ -1240,6 +1281,10 @@ protected void checkNotThat(String description, Supplier condition) { recordSequence("Check that " + notDescription); } + private void waitFor(String description, Supplier evaluator) { + waitFor(description, DEFAULT_WAIT_TIME, evaluator); + } + private void waitFor(String description, Duration maxWait, Supplier evaluator) { AtomicReference detail = new AtomicReference<>(); whileDoing(description, () -> { @@ -1272,8 +1317,7 @@ private String checkLogged(String category, Level exactLevel) { protected void checkNotLogged(String category, Level minLevel) { withRecordSequence(false, () -> { ifTrueThen(deviceSupportsState(), () -> - untilTrue("last_config synchronized", - () -> dateEquals(deviceConfig.timestamp, deviceState.system.last_config))); + untilTrue("last_config synchronized", this::lastConfigUpdated)); processLogMessages(); }); final Instant endTime = lastConfigUpdate.plusSeconds(LOG_TIMEOUT_SEC); @@ -1345,18 +1389,18 @@ private void whileDoing(String description, Runnable action, Consumer private void waitingConditionPop(Instant startTime) { Duration between = Duration.between(startTime, Instant.now()); - debug(format("stage finished %s at %s after %ss", currentWaitingCondition(), + debug(format("Stage finished %s at %s after %ss", currentWaitingCondition(), timeSinceStart(), between.toSeconds())); waitingCondition.pop(); ifTrueThen(!waitingCondition.isEmpty(), - () -> trace(format("stage resume %s at %s", currentWaitingCondition(), timeSinceStart()))); + () -> trace(format("Stage resume %s at %s", currentWaitingCondition(), timeSinceStart()))); } private void waitingConditionPush(String condition) { ifTrueThen(!waitingCondition.isEmpty(), () -> trace(format("stage suspend %s at %s", currentWaitingCondition(), timeSinceStart()))); waitingCondition.push("waiting for " + condition); - info(format("stage start %s at %s", currentWaitingCondition(), timeSinceStart())); + info(format("Stage start %s at %s", currentWaitingCondition(), timeSinceStart())); } private String currentWaitingCondition() { @@ -1376,7 +1420,7 @@ private void untilLoop(String description, Supplier evaluator, Supplier } private void messageEvaluateLoop(Supplier evaluator) { - messageEvaluateLoop(DEFAULT_WAIT_TIMEOUT, evaluator); + messageEvaluateLoop(DEFAULT_LOOP_TIMEOUT, evaluator); } private void messageEvaluateLoop(Duration maxWait, Supplier evaluator) { @@ -1400,6 +1444,10 @@ private void recordSequence(String step) { } } + private void recordBullet(DiffEntry step) { + recordBullet(step.toString()); + } + private void recordBullet(String step) { if (recordSequence) { info("Device config " + step); @@ -1609,7 +1657,7 @@ private synchronized void handleUpdateMessage(String subTypeRaw, error("Shouldn't be seeing this!"); return; } - List changes = updateDeviceConfig(config); + List changes = updateDeviceConfig(config); debug(format("Updated config %s %s", isoConvert(config.timestamp), txnId)); if (updateCount == CAPABILITY_SCORE) { info(format("Initial config #%03d", updateCount), stringify(deviceConfig)); @@ -1635,12 +1683,13 @@ private synchronized void handleUpdateMessage(String subTypeRaw, } checkState(deviceSupportsState(), "Received state update with no-state device"); boolean deltaState = RECV_STATE_DIFFERNATOR.isInitialized(); - List stateChanges = RECV_STATE_DIFFERNATOR.computeChanges(converted); + List stateChanges = RECV_STATE_DIFFERNATOR.computeChanges(converted); Instant start = ofNullable(convertedState.timestamp).orElseGet(Date::new).toInstant(); long delta = Duration.between(start, Instant.now()).getSeconds(); debug(format("Updated state after %ds %s %s", delta, timestamp, txnId)); if (deltaState) { info(format("Updated state #%03d", updateCount), changedLines(stateChanges)); + validateIntermediateState(convertedState, stateChanges); } else { info(format("Initial state #%03d", updateCount), stringify(converted)); } @@ -1657,7 +1706,49 @@ private synchronized void handleUpdateMessage(String subTypeRaw, } } - private List updateDeviceConfig(Config config) { + protected void expectedStatusLevel(Level level) { + maxAllowedStatusLevel = level.value(); + } + + private void validateIntermediateState(State convertedState, List stateChanges) { + if (!recordSequence || !shouldValidateSchema(SubFolder.VALIDATION)) { + return; + } + + int statusLevel = catchToElse(() -> convertedState.system.status.level, Level.TRACE.value()); + if (statusLevel > maxAllowedStatusLevel) { + String message = format("System status level %d exceeded allowed threshold %d", statusLevel, + maxAllowedStatusLevel); + deviceStateViolations.put(STATUS_LEVEL_VIOLATION, message); + warning(message); + } + Map badChanges = stateChanges.stream().filter(not(this::changeAllowed)) + .collect(Collectors.toMap(DiffEntry::key, DiffEntry::toString)); + badChanges.values().stream().map(x -> "Unexpected state change: " + x).forEach(this::warning); + deviceStateViolations.putAll(badChanges); + } + + private boolean changeAllowed(DiffEntry change) { + String key = change.key(); + return SYSTEM_STATE_CHANGES.stream().anyMatch(key::startsWith) + || allowedDeviceStateChanges.stream().anyMatch(key::startsWith); + } + + protected void allowDeviceStateChange(String changePrefix) { + info("Allowing device state change " + changePrefix); + if (!allowedDeviceStateChanges.add(changePrefix)) { + throw new AbortMessageLoop("State change prefix already allowed: " + changePrefix); + } + } + + protected void disallowDeviceStateChange(String changePrefix) { + info("Disallowing device state change " + changePrefix); + if (!allowedDeviceStateChanges.remove(changePrefix)) { + throw new AbortMessageLoop("Unexpected state change removal: " + changePrefix); + } + } + + private List updateDeviceConfig(Config config) { if (deviceConfig == null) { return null; } @@ -1732,8 +1823,8 @@ private String configTransactionsListString() { /** * Filter out any testing-oriented messages, since they should not impact behavior. */ - private List filterTesting(List allDiffs) { - return allDiffs.stream().filter(message -> !message.contains(SYSTEM_TESTING_MARKER)) + private List filterTesting(List allDiffs) { + return allDiffs.stream().filter(message -> !message.key().startsWith(SYSTEM_TESTING_MARKER)) .collect(Collectors.toList()); } @@ -1875,7 +1966,11 @@ private void updateMirrorConfig(String receivedConfig) { } } - protected boolean stateMatchesConfigTimestamp() { + protected String lastConfigUpdatedString() { + return lastConfigUpdated() ? null : ""; + } + + protected boolean lastConfigUpdated() { Date expectedConfig = deviceConfig.timestamp; Date lastConfig = deviceState.system.last_config; return dateEquals(expectedConfig, lastConfig); @@ -2033,13 +2128,17 @@ protected void forCapability(Capabilities capability, Runnable action) { } } + private boolean shouldValidateSchema(SubFolder folder) { + return testSchema != null && (testSchema == folder || folder == SubFolder.VALIDATION); + } + /** * Master list of test capabilities. */ public enum Capabilities { DEVICE_STATE("device_state"), LOGGING("logging"), - ACKNOWLEDGE("acknowledge"); + LAST_CONFIG("last_config"); private final String value; @@ -2092,6 +2191,8 @@ protected void starting(@NotNull Description description) { testStage = getTestStage(description); testBucket = getBucket(description); testResult = SequenceResult.START; + testSchema = ifNotNullGet(description.getAnnotation(ValidateSchema.class), + ValidateSchema::value); testDir = new File(new File(deviceOutputDir, TESTS_OUT_DIR), testName); FileUtils.deleteDirectory(testDir); @@ -2123,10 +2224,8 @@ protected void finished(Description description) { throw new IllegalStateException("Unexpected test method name"); } - if (testResult == PASS) { - ValidateSchema annotation = description.getAnnotation(ValidateSchema.class); - ifNotNullThen(annotation, a -> recordSchemaValidations(description)); - } + ifTrueThen(testResult == PASS && shouldValidateSchema(SubFolder.VALIDATION), + () -> recordSchemaValidations(description)); notice("ending test " + testName + " after " + timeSinceStart() + " " + START_END_MARKER); testName = null; diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/ValidateSchema.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/ValidateSchema.java index f3c68ad79..b6d0315d9 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/ValidateSchema.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/ValidateSchema.java @@ -4,6 +4,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import udmi.schema.Envelope.SubFolder; /** * Indicate that test post-processing should apply schema validation. Don't include by @@ -13,4 +14,9 @@ @Target({ElementType.METHOD}) public @interface ValidateSchema { + /** + * Limit the subFolder that should be validated. + */ + SubFolder value(); + } diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/BlobsetSequences.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/BlobsetSequences.java index d95c0e4d5..2a5c72526 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/BlobsetSequences.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/BlobsetSequences.java @@ -18,6 +18,7 @@ import com.google.daq.mqtt.sequencer.semantic.SemanticValue; import java.util.Date; import java.util.HashMap; +import org.junit.Before; import org.junit.Test; import udmi.schema.BlobBlobsetConfig; import udmi.schema.BlobBlobsetConfig.BlobPhase; @@ -27,6 +28,7 @@ import udmi.schema.EndpointConfiguration; import udmi.schema.EndpointConfiguration.Protocol; import udmi.schema.Entry; +import udmi.schema.Envelope.SubFolder; import udmi.schema.Level; import udmi.schema.Operation.SystemMode; @@ -44,6 +46,11 @@ public class BlobsetSequences extends SequenceBase { "projects/%s/locations/%s/registries/%s/devices/%s"; private static final String BOGUS_ENDPOINT_HOSTNAME = "twiddily.fiddily.fog"; + @Before + public void setupExpectedParameters() { + allowDeviceStateChange("blobset"); + } + private String generateEndpointConfigClientId(String registryId) { return String.format( ENDPOINT_CONFIG_CLIENT_ID, @@ -160,7 +167,7 @@ public void endpoint_connection_success_reconnect() { @Feature(stage = ALPHA, bucket = ENDPOINT) @Summary("Failed connection because of bad hash.") - @ValidateSchema + @ValidateSchema(SubFolder.BLOBSET) @Test public void endpoint_connection_bad_hash() { setDeviceConfigEndpointBlob(getAlternateEndpointHostname(), registryId, true); @@ -200,7 +207,7 @@ public void endpoint_failure_and_restart() { private void check_endpoint_connection_success(boolean doRestart) { // Phase one: initiate connection to alternate registry. - untilTrue("initial last_config matches config timestamp", this::stateMatchesConfigTimestamp); + untilTrue("initial last_config matches config timestamp", this::lastConfigUpdated); setDeviceConfigEndpointBlob(getAlternateEndpointHostname(), altRegistry, false); untilSuccessfulRedirect(BlobPhase.APPLY); @@ -208,7 +215,7 @@ private void check_endpoint_connection_success(boolean doRestart) { // Phase two: verify connection to alternate registry. untilSuccessfulRedirect(BlobPhase.FINAL); untilTrue("alternate last_config matches config timestamp", - this::stateMatchesConfigTimestamp); + this::lastConfigUpdated); untilClearedRedirect(); if (doRestart) { @@ -225,7 +232,7 @@ private void check_endpoint_connection_success(boolean doRestart) { // Phase four: verify restoration of initial registry connection. whileDoing("restoring main connection", () -> { untilSuccessfulRedirect(BlobPhase.FINAL); - untilTrue("restored last_config matches config timestamp", this::stateMatchesConfigTimestamp); + untilTrue("restored last_config matches config timestamp", this::lastConfigUpdated); untilClearedRedirect(); }); } @@ -238,6 +245,8 @@ public void system_mode_restart() { } private void check_system_restart() { + allowDeviceStateChange("system.operation."); + // Prepare for the restart. final Date dateZero = new Date(0); untilTrue("last_start is not zero", diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/ConfigSequences.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/ConfigSequences.java index 01de95408..e38c963b7 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/ConfigSequences.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/ConfigSequences.java @@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; import udmi.schema.Entry; +import udmi.schema.Envelope.SubFolder; import udmi.schema.Level; /** @@ -51,24 +52,26 @@ public class ConfigSequences extends SequenceBase { @Test(timeout = ONE_MINUTE_MS) @Feature(stage = STABLE, bucket = SYSTEM, nostate = true) @Summary("Check that last_update state is correctly set in response to a config update.") - @ValidateSchema + @ValidateSchema(SubFolder.SYSTEM) public void system_last_update() { - untilTrue("state last_config matches config timestamp", this::stateMatchesConfigTimestamp); - ensureStateUpdate(); + untilTrue("state last_config matches first config timestamp", this::lastConfigUpdated); + forceConfigUpdate("trigger another config update"); + untilTrue("state last_config matches new config timestamp", this::lastConfigUpdated); } @Test @Feature(stage = ALPHA, bucket = SYSTEM) - @ValidateSchema + @ValidateSchema(SubFolder.SYSTEM) public void valid_serial_no() { ifNullSkipTest(serialNo, "No test serial number provided"); - untilTrue("received serial number matches", () -> serialNo.equals(lastSerialNo)); ensureStateUpdate(); + untilTrue("received serial number matches", () -> serialNo.equals(lastSerialNo)); } @Test(timeout = TWO_MINUTES_MS) @Feature(stage = ALPHA, bucket = SYSTEM, nostate = true) @Summary("Check that the min log-level config is honored by the device.") + @ValidateSchema(SubFolder.SYSTEM) public void system_min_loglevel() { Integer savedLevel = deviceConfig.system.min_loglevel; checkState(SYSTEM_CONFIG_APPLY_LEVEL.value() >= savedLevel, "invalid saved level"); @@ -119,7 +122,10 @@ public void device_config_acked() { @Feature(stage = ALPHA, bucket = SYSTEM, score = 4) @Capability(value = LOGGING, stage = ALPHA) @Summary("Check that the device correctly handles a broken (non-json) config message.") + @ValidateSchema(SubFolder.SYSTEM) public void broken_config() { + expectedStatusLevel(Level.ERROR); + deviceConfig.system.min_loglevel = Level.DEBUG.value(); updateConfig("starting broken_config"); Date stableConfig = deviceConfig.timestamp; diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/DiscoverySequences.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/DiscoverySequences.java index a8ec6b6f2..9f5e23a4f 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/DiscoverySequences.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/DiscoverySequences.java @@ -37,6 +37,7 @@ import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import org.junit.Before; import org.junit.Test; import udmi.schema.Bucket; import udmi.schema.DiscoveryConfig; @@ -56,6 +57,11 @@ public class DiscoverySequences extends SequenceBase { private HashMap previousGenerations; private Set families; + @Before + public void setupExpectedParameters() { + allowDeviceStateChange("discovery"); + } + private static boolean isActive(Entry entry) { return Optional.ofNullable(entry.getValue().stage).orElse(STABLE).compareTo(BETA) >= 0; } diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/GatewaySequences.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/GatewaySequences.java index d1d8b477c..d69139ec0 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/GatewaySequences.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/GatewaySequences.java @@ -13,6 +13,7 @@ import com.google.daq.mqtt.sequencer.Summary; import java.util.HashSet; import java.util.Set; +import org.junit.Before; import org.junit.Test; import udmi.schema.Bucket; import udmi.schema.FeatureEnumeration.FeatureStage; @@ -22,6 +23,11 @@ */ public class GatewaySequences extends SequenceBase { + @Before + public void setupExpectedParameters() { + allowDeviceStateChange("gateway"); + } + @Override public void setUp() { ifTrueSkipTest(catchToTrue(() -> deviceMetadata.gateway.proxy_ids.isEmpty()), "Not a gateway"); diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/PointsetSequences.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/PointsetSequences.java index 51484a084..750ebc8cb 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/PointsetSequences.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/PointsetSequences.java @@ -24,7 +24,9 @@ import java.util.List; import java.util.Map.Entry; import java.util.stream.Collectors; +import org.junit.Before; import org.junit.Test; +import udmi.schema.Envelope.SubFolder; import udmi.schema.Level; import udmi.schema.PointPointsetConfig; import udmi.schema.PointPointsetEvent; @@ -40,6 +42,11 @@ public class PointsetSequences extends PointsetBase { private static final int DEFAULT_SAMPLE_RATE_SEC = 10; private static final String POINTS_MAP_PATH = "pointset.points"; + @Before + public void setupExpectedParameters() { + allowDeviceStateChange("pointset."); + } + private boolean isErrorState(PointPointsetState pointState) { return ofNullable(catchToNull(() -> pointState.status.level)).orElse(Level.INFO.value()) >= Level.ERROR.value(); @@ -130,8 +137,7 @@ public void pointset_publish() { ifNullSkipTest(deviceConfig.pointset, "no pointset found in config"); untilTrue("receive a pointset event", - () -> (countReceivedEvents(PointsetEvent.class) > 1 - )); + () -> (countReceivedEvents(PointsetEvent.class) > 1)); } /** @@ -203,7 +209,7 @@ private String samplingMessagesCheckMessage(SamplingRange samplingRange) { @Test(timeout = THREE_MINUTES_MS) @Summary("Check handling of sample rate and sample limit sec") @Feature(stage = BETA, bucket = POINTSET, nostate = true) - @ValidateSchema + @ValidateSchema(SubFolder.POINTSET) public void pointset_publish_interval() { ifNullSkipTest(deviceConfig.pointset, "no pointset found in config"); diff --git a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/WritebackSequences.java b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/WritebackSequences.java index bac156da6..548f1a8fb 100644 --- a/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/WritebackSequences.java +++ b/validator/src/main/java/com/google/daq/mqtt/sequencer/sequences/WritebackSequences.java @@ -9,6 +9,7 @@ import com.google.daq.mqtt.sequencer.Summary; import java.util.List; import java.util.Objects; +import org.junit.Before; import org.junit.Test; import udmi.schema.PointPointsetState.Value_state; import udmi.schema.PointsetEvent; @@ -24,6 +25,11 @@ public class WritebackSequences extends PointsetBase { public static final String APPLIED_STATE = "applied"; public static final String DEFAULT_STATE = null; + @Before + public void setupExpectedParameters() { + allowDeviceStateChange("pointset.points."); + } + /** * Checks `value_state` for the point in the state matches the provided string. * diff --git a/validator/src/main/java/com/google/daq/mqtt/util/ObjectDiffEngine.java b/validator/src/main/java/com/google/daq/mqtt/util/ObjectDiffEngine.java index c44774cc7..bb56cc525 100644 --- a/validator/src/main/java/com/google/daq/mqtt/util/ObjectDiffEngine.java +++ b/validator/src/main/java/com/google/daq/mqtt/util/ObjectDiffEngine.java @@ -1,11 +1,14 @@ package com.google.daq.mqtt.util; +import static com.google.udmi.util.DiffEntry.DiffAction.ADD; +import static com.google.udmi.util.DiffEntry.DiffAction.REMOVE; +import static com.google.udmi.util.DiffEntry.DiffAction.SET; import static com.google.udmi.util.JsonUtil.isoConvert; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.daq.mqtt.sequencer.semantic.SemanticValue; -import com.google.udmi.util.JsonUtil; +import com.google.udmi.util.DiffEntry; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; @@ -35,11 +38,11 @@ public ObjectDiffEngine() { * @param updatedObject new object * @return list of differences against the previous object */ - public List computeChanges(Object updatedObject) { + public List computeChanges(Object updatedObject) { // TODO: Hack alert! State should be handled semantically, but for now show raw changes. ignoreSemantics = updatedObject instanceof State; Map updated = extractDefinitions(updatedObject); - List updates = new ArrayList<>(); + List updates = new ArrayList<>(); accumulateDifference("", previous, updated, updates); previous = updated; return updates; @@ -115,7 +118,7 @@ private boolean isBaseType(Object value) { @SuppressWarnings("unchecked") void accumulateDifference(String prefix, Map left, Map right, - List updates) { + List updates) { right.forEach((key, value) -> { String describedKey = describedKey(prefix, key); String raw = describeValue(prefix, key, semanticValue(value)); @@ -127,20 +130,20 @@ void accumulateDifference(String prefix, Map left, Map) leftValue, (Map) value, updates); } } else { - updates.add(String.format("Add `%s` = %s", describedKey, describedValue)); + updates.add(new DiffEntry(ADD, describedKey, describedValue)); } }); if (left != null) { left.forEach((key, value) -> { if (!right.containsKey(key)) { String describedKey = describedKey(prefix, key); - updates.add(String.format("Remove `%s`", describedKey)); + updates.add(new DiffEntry(REMOVE, describedKey, null)); } }); } @@ -211,10 +214,10 @@ public boolean equals(Object oneObject, Object twoObject) { * @param endObject ending object * @return list of differences going from start to end objects */ - public List diff(Object startObject, Object endObject) { + public List diff(Object startObject, Object endObject) { Map left = extractValues(startObject); Map right = extractValues(endObject); - List updates = new ArrayList<>(); + List updates = new ArrayList<>(); accumulateDifference("", left, right, updates); return updates; }