public void SmartDictionary_Should_HaveNewItem_When_NewDesiredPropertyAdded() { // Arrange const string NEW_KEY = "new key"; const string NEW_VALUE = "new value"; ISmartDictionary reportedProps = this.GetTestProperties(); this.target.RegisterChangeUpdateAsync(this.sdkClient.Object, DEVICE_ID, reportedProps).CompleteOrTimeout(); TwinCollection desiredProps = new TwinCollection(); desiredProps[NEW_KEY] = NEW_VALUE; // Act // Use reflection to invoke private callback MethodInfo methodInfo = this.target.GetType().GetMethod("OnChangeCallback", BindingFlags.Instance | BindingFlags.NonPublic); methodInfo.Invoke(this.target, new object[] { desiredProps, null }); var result = reportedProps.Get(NEW_KEY); // Assert Assert.Equal(result, NEW_VALUE); }
public void Invoke( Script script, Dictionary <string, object> context, ISmartDictionary state, ISmartDictionary properties) { switch (script.Type.ToLowerInvariant()) { default: this.log.Error("Unknown script type", () => new { script.Type }); throw new NotSupportedException($"Unknown script type `{script.Type}`."); case JAVASCRIPT_SCRIPT: this.log.Debug("Invoking JS", () => new { script.Path, context, state }); this.jsInterpreter.Invoke(script, context, state, properties); this.log.Debug("JS invocation complete"); break; case INTERNAL_SCRIPT: this.log.Debug("Invoking internal script", () => new { script.Path, context, state }); this.intInterpreter.Invoke(script.Path, script.Params, context, state, properties); this.log.Debug("Internal script complete"); break; } }
/// <summary> /// Load a JS file and execute the main() function, passing in /// context information and the output from the previous execution. /// Modifies the internal device state with the latest values. /// </summary> public void Invoke( string filename, Dictionary <string, object> context, ISmartDictionary state, ISmartDictionary properties) { this.Invoke(filename, null, context, state, properties); }
public void ItCanBeInitializedWithNullValue() { // Act this.target = new SmartDictionary(null); // Assert Assert.True(this.target.Changed); }
public void Should_Throw_On_Get_When_Key_Does_Not_Exist() { // Arrange this.target = this.GetTestProperties(); const string KEY = "KeyThatDoesNotExist"; // Act and Assert Assert.Throws <KeyNotFoundException>(() => this.target.Get(KEY)); }
/// <summary> /// Load a JS file and execute the main() function, passing in /// context information and the output from the previous execution. /// Modifies the internal device state with the latest values. /// </summary> public void Invoke( string filename, object scriptParams, Dictionary <string, object> context, ISmartDictionary state, ISmartDictionary properties) { this.deviceState = state; this.deviceProperties = properties; var engine = new Engine(); // Inject the logger in the JS context, to allow the JS function // logging into the service logs engine.SetValue("log", new Action <object>(this.JsLog)); // register callback for state updates engine.SetValue("updateState", new Action <JsValue>(this.UpdateState)); // register callback for property updates engine.SetValue("updateProperty", new Action <string, object>(this.UpdateProperty)); // register sleep function for javascript use engine.SetValue("sleep", new Action <int>(this.Sleep)); try { Program program; if (programs.ContainsKey(filename)) { program = programs[filename]; } else { var sourceCode = this.LoadScript(filename); this.log.Info("Compiling script source code", () => new { filename }); program = parser.Parse(sourceCode); programs.Add(filename, program); } this.log.Debug("Executing JS function", () => new { filename }); engine.Execute(program).Invoke( "main", context, this.deviceState.GetAll(), this.deviceProperties.GetAll(), scriptParams); this.log.Debug("JS function success", () => new { filename, this.deviceState }); } catch (Exception e) { this.log.Error("JS function failure", () => new { e.Message, e.GetType().FullName }); } }
public async Task RegisterMethodsForDeviceAsync( IDictionary <string, Script> methods, ISmartDictionary deviceState, ISmartDictionary deviceProperties) { this.log.Debug("Attempting to register device methods", () => new { this.deviceId }); await this.deviceMethods.RegisterMethodsAsync(this.deviceId, methods, deviceState, deviceProperties); }
// For each sensors specified, generate a random number in the range requested private void RunRandomNumberScript(object scriptParams, ISmartDictionary state) { var sensors = this.JsonParamAsDictionary(scriptParams); foreach (var sensor in sensors) { (double min, double max) = this.GetMinMaxParameters(sensor.Value); var value = this.random.NextDouble() * (max - min) + min; state.Set(sensor.Key, value); } }
public void Should_Be_Empty_On_Get_All_When_No_Properties_Added() { // Arrange this.target = this.GetEmptyProperties(); // Act var props = this.target.GetAll(); // Assert Assert.Empty(props); }
public void Has_Should_Return_False_When_Property_Does_Not_Exist() { // Arrange this.target = this.GetEmptyProperties(); const string KEY = "KeyThatDoesNotExist"; // Act var result = this.target.Has(KEY); // Assert Assert.False(result); }
public async Task RegisterMethodsAsync( IDeviceClientWrapper client, string deviceId, IDictionary <string, Script> methods, ISmartDictionary deviceState, ISmartDictionary deviceProperties, IScriptInterpreter scriptInterpreter) { if (methods == null) { return; } if (this.isRegistered) { this.log.Error("Application error, each device must have a separate instance"); throw new Exception("Application error, each device must have a separate instance of " + this.GetType().FullName); } if (!this.methodsEnabled) { this.isRegistered = true; this.log.Debug("Skipping methods registration, methods are disabled in the global configuration."); return; } this.deviceId = deviceId; this.cloudToDeviceMethods = methods; this.deviceState = deviceState; this.deviceProperties = deviceProperties; this.log.Debug("Setting up methods for device.", () => new { this.deviceId, methodsCount = this.cloudToDeviceMethods.Count }); // walk this list and add a method handler for each method specified foreach (var item in this.cloudToDeviceMethods) { this.log.Debug("Setting up method for device.", () => new { item.Key, this.deviceId }); await client.SetMethodHandlerAsync(item.Key, this.ExecuteMethodAsync, scriptInterpreter); this.log.Debug("Method for device setup successfully", () => new { this.deviceId, methodName = item.Key }); } this.isRegistered = true; }
public void Has_Should_Return_True_When_Property_Exists() { // Arrange this.target = this.GetEmptyProperties(); const string KEY = "testHasKey"; const string VALUE = "testHasValue"; this.target.Set(KEY, VALUE); // Act var result = this.target.Has(KEY); // Assert Assert.True(result); }
public void Changed_Should_Return_True_When_New_Property_Added() { // Arrange this.target = this.GetEmptyProperties(); Assert.False(this.target.Changed); const string KEY = "testKey"; const string VALUE = "testValue"; // Act this.target.Set(KEY, VALUE); // Assert Assert.True(this.target.Changed); }
public void Should_Add_Value_When_Set() { // Arrange this.target = this.GetEmptyProperties(); const string KEY = "testSetKey"; const string VALUE = "testSetValue"; // Act this.target.Set(KEY, VALUE); var result = this.target.Get(KEY); // Assert Assert.NotNull(result); Assert.Equal(result, VALUE); }
public void Changed_Should_Be_False_When_Reset() { // Arrange this.target = this.GetTestProperties(); const string KEY = "testKey"; const string VALUE = "testValue"; this.target.Set(KEY, VALUE); // Act this.target.ResetChanged(); // Assert Assert.False(this.target.Changed); }
public void ItCanDetectUnchangedValues() { // Arrange this.target = this.GetTestProperties(); var key = Guid.NewGuid().ToString(); var value = Guid.NewGuid().ToString(); this.target.Set(key, value, false); this.target.ResetChanged(); // Act - Assert this.target.Set(key, value, true); Assert.False(this.target.Changed); // Assert this.target.Set(key, value, false); Assert.True(this.target.Changed); }
/// <summary> /// Updates the reported properties in the device twin on the IoT Hub /// </summary> public async Task UpdatePropertiesAsync(ISmartDictionary properties) { try { var reportedProperties = this.SmartDictionaryToTwinCollection(properties); await this.client.UpdateReportedPropertiesAsync(reportedProperties); this.log.Debug("Update reported properties for device", () => new { this.deviceId, reportedProperties }); } catch (KeyNotFoundException e) { // This exception sometimes occurs when calling UpdateReportedPropertiesAsync. // Still unknown why, apparently an issue with the internal AMQP library // used by IoT SDK. We need to collect extra information to report the issue. this.log.Error("Unexpected error, failed to update reported properties", () => new { Protocol = this.protocol.ToString(), Exception = e.GetType().FullName, e.Message, e.StackTrace, e.Data, e.Source, e.TargetSite, e.InnerException // This appears to always be null in this scenario }); } catch (Exception e) { this.log.Error("Failed to update reported properties", () => new { Protocol = this.protocol.ToString(), ExceptionMessage = e.Message, Exception = e.GetType().FullName, e.InnerException }); } }
public void Should_Return_Value_When_Calling_Get_And_Property_Exists() { // Arrange this.target = this.GetTestProperties(); var expectedCount = this.GetTestChillerModel().Properties.Count; // Act var props = this.target.GetAll(); // Assert Assert.NotEmpty(props); Assert.Equal(props.Count, expectedCount); Assert.Equal("TestChiller", props["Type"]); Assert.Equal("1.0", props["Firmware"]); Assert.Equal("TestCH101", props["Model"]); Assert.Equal("TestBuilding 2", props["Location"]); Assert.Equal(47.640792, props["Latitude"]); Assert.Equal(-122.126258, props["Longitude"]); }
public void Should_Return_All_Test_Properties_On_Get_All_When_Initialized_With_Device_Model() { // Arrange this.target = this.GetTestProperties(); var expectedCount = this.GetTestChillerModel().Properties.Count; // Act var props = this.target.GetAll(); // Assert Assert.NotEmpty(props); Assert.Equal(props.Count, expectedCount); Assert.Equal("TestChiller", props["Type"]); Assert.Equal("1.0", props["Firmware"]); Assert.Equal("TestCH101", props["Model"]); Assert.Equal("TestBuilding 2", props["Location"]); Assert.Equal(47.640792, props["Latitude"]); Assert.Equal(-122.126258, props["Longitude"]); }
public async Task RegisterChangeUpdateAsync(string deviceId, ISmartDictionary deviceProperties) { if (this.isRegistered) { this.log.Error("Application error, each device must have a separate instance"); throw new Exception("Application error, each device must have a separate instance of " + this.GetType().FullName); } this.deviceId = deviceId; this.deviceProperties = deviceProperties ?? new SmartDictionary(); this.log.Debug("Setting up callback for desired properties updates.", () => new { this.deviceId }); // Set callback that IoT Hub calls whenever the client receives a desired properties state update. await this.client.SetDesiredPropertyUpdateCallbackAsync(this.OnChangeCallback, null); this.log.Debug("Callback for desired properties updates setup successfully", () => new { this.deviceId }); this.isRegistered = true; }
// For each sensors specified, increase the current state, up to a maximum, then restart from a minimum private void RunIncreasingScript(object scriptParams, ISmartDictionary state) { var sensors = this.JsonParamAsDictionary(scriptParams); foreach (var sensor in sensors) { // Extract scripts parameters from the device model script configuration (double min, double max, double step) = this.GetMinMaxStepParameters(sensor.Value); // Add the sensor to the state if missing if (!state.Has(sensor.Key)) { state.Set(sensor.Key, min); } double current = Convert.ToDouble(state.Get(sensor.Key)); double next = AreEqual(current, max) ? min : Math.Min(current + step, max); state.Set(sensor.Key, next); } }
public async Task RegisterMethodsAsync( string deviceId, IDictionary <string, Script> methods, ISmartDictionary deviceState, ISmartDictionary deviceProperties) { if (this.isRegistered) { this.log.Error("Application error, each device must have a separate instance", () => { }); throw new Exception("Application error, each device must have a separate instance of " + this.GetType().FullName); } this.deviceId = deviceId; this.cloudToDeviceMethods = methods; this.deviceState = deviceState; this.deviceProperties = deviceProperties; this.log.Debug("Setting up methods for device.", () => new { this.deviceId, methodsCount = this.cloudToDeviceMethods.Count }); // walk this list and add a method handler for each method specified foreach (var item in this.cloudToDeviceMethods) { this.log.Debug("Setting up method for device.", () => new { item.Key, this.deviceId }); await this.client.SetMethodHandlerAsync(item.Key, this.ExecuteMethodAsync, null); this.log.Debug("Method for device setup successfully", () => new { this.deviceId, methodName = item.Key }); } this.isRegistered = true; }
public void SmartDictionary_Should_Not_Update_When_DesiredPropertiesValueIsTheSame() { // Arrange ISmartDictionary reportedProps = GetTestProperties(); reportedProps.ResetChanged(); Assert.False(reportedProps.Changed); this.target.RegisterChangeUpdateAsync(DEVICE_ID, reportedProps); TwinCollection desiredProps = new TwinCollection { [KEY1] = VALUE1 // This should be the same value in props }; // Act // Use reflection to invoke private callback MethodInfo methodInfo = this.target.GetType().GetMethod("OnChangeCallback", BindingFlags.Instance | BindingFlags.NonPublic); methodInfo.Invoke(this.target, new object[] { desiredProps, null }); // Assert Assert.False(reportedProps.Changed); }
public void SmartDictionary_Should_UpdateValue_When_DesiredPropertiesChange() { // Arrange const string NEW_VALUE = "new value"; ISmartDictionary reportedProps = GetTestProperties(); this.target.RegisterChangeUpdateAsync(DEVICE_ID, reportedProps); TwinCollection desiredProps = new TwinCollection(); desiredProps[KEY1] = NEW_VALUE; // Act // Use reflection to invoke private callback MethodInfo methodInfo = this.target.GetType().GetMethod("OnChangeCallback", BindingFlags.Instance | BindingFlags.NonPublic); methodInfo.Invoke(this.target, new object[] { desiredProps, null }); var result = reportedProps.Get(KEY1); // Assert Assert.Equal(result, NEW_VALUE); }
public async Task RegisterChangeUpdateAsync(IDeviceClientWrapper client, string deviceId, ISmartDictionary deviceProperties) { if (this.isRegistered) { this.log.Error("Application error, each device must have a separate instance"); throw new Exception("Application error, each device must have a separate instance of " + this.GetType().FullName); } if (!this.deviceTwinEnabled) { this.isRegistered = true; this.log.Debug("Skipping twin notification registration, twin operations are disabled in the global configuration."); return; } this.deviceId = deviceId; this.deviceProperties = deviceProperties ?? new SmartDictionary(); this.log.Debug("Setting up callback for desired properties updates.", () => new { this.deviceId }); // Set callback that IoT Hub calls whenever the client receives a desired properties state update. await client.SetDesiredPropertyUpdateCallbackAsync(this.OnChangeCallback, null); this.log.Debug("Callback for desired properties updates setup successfully", () => new { this.deviceId }); this.isRegistered = true; }
public SmartDictionaryTest(ITestOutputHelper log) { // Initialize device properties this.target = this.GetTestProperties(); }
/// <summary> /// Load a JS file and execute the main() function, passing in /// context information and the output from the previous execution. /// Modifies the internal device state with the latest values. /// </summary> public void Invoke( Script script, Dictionary <string, object> context, ISmartDictionary state, ISmartDictionary properties) { this.deviceState = state; this.deviceProperties = properties; var engine = new Engine(); // Inject the logger in the JS context, to allow the JS function // logging into the service logs engine.SetValue("log", new Action <object>(this.JsLog)); // register callback for state updates engine.SetValue("updateState", new Action <JsValue>(this.UpdateState)); // register callback for property updates engine.SetValue("updateProperty", new Action <string, object>(this.UpdateProperty)); // register sleep function for javascript use engine.SetValue("sleep", new Action <int>(this.Sleep)); try { Program program; bool isInStorage = string.Equals(script.Path.Trim(), DeviceModelScript.DeviceModelScriptPath.Storage.ToString(), StringComparison.OrdinalIgnoreCase); string filename = isInStorage ? script.Id : script.Path; if (programs.ContainsKey(filename)) { program = programs[filename]; } else { // TODO: refactor the code to avoid blocking // https://github.com/Azure/device-simulation-dotnet/issues/240 var task = this.LoadScriptAsync(filename, isInStorage); task.Wait(TimeSpan.FromSeconds(30)); var sourceCode = task.Result; this.log.Debug("Compiling script source code", () => new { filename }); program = parser.Parse(sourceCode); programs.Add(filename, program); } this.log.Debug("Executing JS function", () => new { filename }); engine.Execute(program).Invoke( "main", context, this.deviceState.GetAll(), this.deviceProperties.GetAll(), script.Params); this.log.Debug("JS function success", () => new { filename, this.deviceState }); } catch (Exception e) { this.log.Error("JS function failure", e); } }
/// <summary> /// Updates the reported properties in the device twin on the IoT Hub /// </summary> public async Task UpdatePropertiesAsync(ISmartDictionary properties) { var start = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); long GetTimeSpentMsecs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - start; try { var reportedProperties = this.SmartDictionaryToTwinCollection(properties); await this.client.UpdateReportedPropertiesAsync(reportedProperties); var timeSpentMsecs = GetTimeSpentMsecs(); this.log.Debug("Update reported properties for device", () => new { this.deviceId, timeSpentMsecs, reportedProperties }); } catch (NullReferenceException) { // In case of multi-threaded access to the client, nothing to do if (this.client == null) { return; } throw; } catch (KeyNotFoundException e) { // This exception sometimes occurs when calling UpdateReportedPropertiesAsync. // Still unknown why, apparently an issue with the internal AMQP library // used by IoT SDK. We need to collect extra information to report the issue. // The exception is logged differently than usual to capture info that might help fixing the SDK. var timeSpentMsecs = GetTimeSpentMsecs(); this.log.Error("Unexpected error, failed to update reported properties", () => new { timeSpentMsecs, this.deviceId, Protocol = this.protocol.ToString(), Exception = e.GetType().FullName, e.Message, e.StackTrace, e.Data, e.Source, e.TargetSite, e.InnerException // This appears to always be null in this scenario }); throw new BrokenDeviceClientException("Unexpected error, failed to update reported properties", e); } catch (TimeoutException e) { // Note: this exception can occur in case of throttling, and // the caller should not recreate the client var timeSpentMsecs = GetTimeSpentMsecs(); this.log.Error("Reported properties update timed out", () => new { timeSpentMsecs, this.deviceId, Protocol = this.protocol.ToString(), e }); throw new PropertySendException("Reported properties update timed out", e); } catch (IotHubCommunicationException e) { // Note: this exception can occur in case of throttling, and // the caller should not recreate the client var timeSpentMsecs = GetTimeSpentMsecs(); this.log.Error("Failed to update reported properties", () => new { timeSpentMsecs, this.deviceId, Protocol = this.protocol.ToString(), e }); throw new PropertySendException("Failed to update reported properties", e); } catch (Exception e) { var timeSpentMsecs = GetTimeSpentMsecs(); this.log.Error("Failed to update reported properties", () => new { timeSpentMsecs, this.deviceId, Protocol = this.protocol.ToString(), e }); throw new BrokenDeviceClientException("Failed to update reported properties", e); } }