Create temp sensor end-to-end test using new shared lib (#1118)

This commit is contained in:
Damon Barry 2019-04-26 13:36:00 -07:00 коммит произвёл GitHub
Родитель 02eb6e177b
Коммит d4094df3be
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 1372 добавлений и 0 удалений

Просмотреть файл

@ -171,6 +171,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Devices.Edg
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DirectMethodCloudSender", "edge-modules\DirectMethodCloudSender\DirectMethodCloudSender.csproj", "{A68ABCD4-5926-4005-9525-559DD74991C9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2300ED4C-1D5A-460F-8691-7C85E1162E0C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Devices.Edge.Test.Common", "test\Microsoft.Azure.Devices.Edge.Test.Common\Microsoft.Azure.Devices.Edge.Test.Common.csproj", "{950DACB0-B011-41AF-B0FB-245F749B01AB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Devices.Edge.Test.TempSensor", "test\Microsoft.Azure.Devices.Edge.Test.TempSensor\Microsoft.Azure.Devices.Edge.Test.TempSensor.csproj", "{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
CheckInBuild|Any CPU = CheckInBuild|Any CPU
@ -547,6 +553,22 @@ Global
{A68ABCD4-5926-4005-9525-559DD74991C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A68ABCD4-5926-4005-9525-559DD74991C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A68ABCD4-5926-4005-9525-559DD74991C9}.Release|Any CPU.Build.0 = Release|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.CheckInBuild|Any CPU.ActiveCfg = Debug|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.CheckInBuild|Any CPU.Build.0 = Debug|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.CodeCoverage|Any CPU.ActiveCfg = Debug|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.CodeCoverage|Any CPU.Build.0 = Debug|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{950DACB0-B011-41AF-B0FB-245F749B01AB}.Release|Any CPU.Build.0 = Release|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.CheckInBuild|Any CPU.ActiveCfg = Debug|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.CheckInBuild|Any CPU.Build.0 = Debug|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.CodeCoverage|Any CPU.ActiveCfg = Debug|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.CodeCoverage|Any CPU.Build.0 = Debug|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -615,6 +637,8 @@ Global
{013D53C7-3AB5-41A4-9A8D-0F2C47238773} = {578D5330-2F72-44C6-9DB5-C93B3F42C473}
{A58633ED-5302-41DF-A0F6-FDC48E5C6B04} = {578D5330-2F72-44C6-9DB5-C93B3F42C473}
{A68ABCD4-5926-4005-9525-559DD74991C9} = {578D5330-2F72-44C6-9DB5-C93B3F42C473}
{950DACB0-B011-41AF-B0FB-245F749B01AB} = {2300ED4C-1D5A-460F-8691-7C85E1162E0C}
{FCF94FF4-A608-4CC1-8D1D-215B6F88F203} = {2300ED4C-1D5A-460F-8691-7C85E1162E0C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D71830F5-3AF5-46B4-8A9E-1DCE4F2253AC}

Просмотреть файл

@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.IO;
public class DaemonConfiguration
{
const string ConfigYamlFile = @"C:\ProgramData\iotedge\config.yaml";
YamlDocument config;
public DaemonConfiguration()
{
string contents = File.ReadAllText(ConfigYamlFile);
this.config = new YamlDocument(contents);
}
public void AddHttpsProxy(Uri proxy)
{
this.config.ReplaceOrAdd("agent.env.https_proxy", proxy.ToString());
// TODO: When we allow the caller to specify an upstream protocol,
// we'll need to honor that if it's WebSocket-based, otherwise
// convert to an equivalent WebSocket-based protocol (e.g.,
// Mqtt --> MqttWs)
this.config.ReplaceOrAdd("agent.env.UpstreamProtocol", "AmqpWs");
}
public void Update()
{
var attr = File.GetAttributes(ConfigYamlFile);
File.SetAttributes(ConfigYamlFile, attr & ~FileAttributes.ReadOnly);
File.WriteAllText(ConfigYamlFile, this.config.ToString());
if (attr != 0)
{
File.SetAttributes(ConfigYamlFile, attr);
}
Console.WriteLine($"Updated daemon configuration file '{ConfigYamlFile}'");
}
}
}

Просмотреть файл

@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Edge.Util;
public class EdgeAgent : EdgeModule
{
public EdgeAgent(string deviceId, IotHub iotHub)
: base("edgeAgent", deviceId, iotHub) { }
public Task PingAsync(CancellationToken token)
{
return Profiler.Run(
"Pinging module 'edgeAgent' from the cloud",
() =>
{
return Retry.Do(
() =>
{
return this.IotHub.InvokeMethodAsync(
this.DeviceId,
"$edgeAgent",
new CloudToDeviceMethod("ping"),
token
);
},
result => result.Status == 200,
e => true,
TimeSpan.FromSeconds(5),
token
);
}
);
}
}
}

Просмотреть файл

@ -0,0 +1,216 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Edge.Util;
using Newtonsoft.Json.Linq;
public enum UpstreamProtocolType
{
Amqp,
AmqpWs,
Mqtt,
MqttWs
}
public class EdgeConfiguration
{
ConfigurationContent config;
string deviceId;
IotHub iotHub;
IReadOnlyCollection<string> Modules
{
get
{
var list = new List<string>();
this.ForEachModule((name, module) =>
{
list.Add(name);
});
return new ReadOnlyCollection<string>(list);
}
}
public EdgeConfiguration(string deviceId, string agentImage, IotHub iotHub)
{
this.config = GetBaseConfig(agentImage);
this.iotHub = iotHub;
this.deviceId = deviceId;
}
public void AddRegistryCredentials(string address, string username, string password)
{
this.UpdateAgentDesiredProperties(desired =>
{
JObject settings = desired.Get<JObject>("runtime").Get<JObject>("settings");
settings.Add("registryCredentials", JToken.FromObject(new
{
reg1 = new
{
username,
password,
address
}
}));
});
}
// Adds proxy information to each module in Edge Agent's desired properties. Call this
// method after you've added all the modules that need proxy information.
public void AddProxy(Uri proxy)
{
this.ForEachModule((name, module) =>
{
JObject env = GetOrAddObject("env", module);
env.Add("https_proxy", JToken.FromObject(new
{
value = proxy.ToString()
}));
env.Add("UpstreamProtocol", JToken.FromObject(new
{
value = UpstreamProtocolType.AmqpWs.ToString()
}));
});
}
public void AddEdgeHub(string image)
{
this.UpdateAgentDesiredProperties(desired =>
{
JObject systemModules = desired.Get<JObject>("systemModules");
systemModules.Add("edgeHub", JToken.FromObject(new
{
type = "docker",
status = "running",
restartPolicy = "always",
settings = new
{
image,
createOptions = "{\"HostConfig\":{\"PortBindings\":{\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}],\"5671/tcp\":[{\"HostPort\":\"5671\"}]}}}"
}
}));
});
// { "modulesContent": { "$edgeHub": { ... } } }
this.config.ModulesContent["$edgeHub"] = new Dictionary<string, object>
{
["properties.desired"] = new
{
schemaVersion = "1.0",
routes = new Dictionary<string, string>
{
["route1"] = "from /* INTO $upstream",
},
storeAndForwardConfiguration = new
{
timeToLiveSecs = 7200
}
}
};
}
public void AddTempSensor(string image)
{
this.UpdateAgentDesiredProperties(desired =>
{
JObject modules = GetOrAddObject("modules", desired);
modules.Add("tempSensor", JToken.FromObject(new
{
type = "docker",
status = "running",
restartPolicy = "always",
settings = new
{
image
}
}));
});
}
public Task DeployAsync(CancellationToken token)
{
string message = "Deploying edge configuration to device " +
$"'{this.deviceId}' with modules ({string.Join(", ", this.Modules)})";
return Profiler.Run(
message,
() => this.iotHub.DeployDeviceConfigurationAsync(this.deviceId, this.config, token)
);
}
void ForEachModule(Action<string, JObject> action)
{
this.UpdateAgentDesiredProperties(desired =>
{
foreach (var key in new[] { "systemModules", "modules" })
{
if (desired.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out JToken modules))
{
foreach (var module in modules.Value<JObject>())
{
action(module.Key, module.Value.Value<JObject>());
}
}
}
});
}
void UpdateAgentDesiredProperties(Action<JObject> update)
{
JObject desired = JObject.FromObject(this.config.ModulesContent["$edgeAgent"]["properties.desired"]);
update(desired);
this.config.ModulesContent["$edgeAgent"]["properties.desired"] = desired;
}
static JObject GetOrAddObject(string name, JObject parent)
{
if (parent.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out JToken token))
{
return token.Value<JObject>();
}
parent.Add(name, new JObject());
return parent.Get<JObject>(name);
}
static ConfigurationContent GetBaseConfig(string agentImage) => new ConfigurationContent
{
ModulesContent = new Dictionary<string, IDictionary<string, object>>
{
["$edgeAgent"] = new Dictionary<string, object>
{
["properties.desired"] = new
{
schemaVersion = "1.0",
runtime = new
{
type = "docker",
settings = new
{
minDockerVersion = "v1.25"
}
},
systemModules = new
{
edgeAgent = new
{
type = "docker",
settings = new
{
image = agentImage
}
}
}
}
}
}
};
}
}

Просмотреть файл

@ -0,0 +1,119 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Edge.Util;
public enum EdgeDaemonStatus
{
Running = ServiceControllerStatus.Running,
Stopped = ServiceControllerStatus.Stopped
}
public class EdgeDaemon
{
private string scriptDir;
public EdgeDaemon(string scriptDir)
{
this.scriptDir = scriptDir;
}
public Task InstallAsync(
string deviceConnectionString,
Option<string> packagesPath,
Option<Uri> proxy,
CancellationToken token
)
{
string installCommand = "Install-IoTEdge -Manual -ContainerOs Windows " +
$"-DeviceConnectionString '{deviceConnectionString}'";
packagesPath.ForEach(p => installCommand += $" -OfflineInstallationPath '{p}'");
proxy.ForEach(p =>
installCommand += $" -InvokeWebRequestParameters @{{ '-Proxy' = '{p}' }}"
);
var commands = new[]
{
"$ProgressPreference='SilentlyContinue'",
$". {this.scriptDir}\\IotEdgeSecurityDaemon.ps1",
installCommand
};
string message = "Installing edge daemon";
packagesPath.ForEach(p => message += $" from packages in '{p}'");
return Profiler.Run(
message,
() => Process.RunAsync("powershell", string.Join(";", commands), token)
);
}
public Task UninstallAsync(CancellationToken token)
{
var commands = new[]
{
"$ProgressPreference='SilentlyContinue'",
$". {this.scriptDir}\\IotEdgeSecurityDaemon.ps1",
"Uninstall-IoTEdge -Force"
};
return Profiler.Run(
"Uninstalling edge daemon",
() => Process.RunAsync("powershell", string.Join(";", commands), token)
);
}
public Task StartAsync(CancellationToken token)
{
var sc = new ServiceController("iotedge");
return Profiler.Run(
"Starting edge daemon",
async () =>
{
if (sc.Status != ServiceControllerStatus.Running)
{
sc.Start();
await this._WaitForStatusAsync(sc, ServiceControllerStatus.Running, token);
}
}
);
}
public Task StopAsync(CancellationToken token)
{
var sc = new ServiceController("iotedge");
return Profiler.Run(
"Stopping edge daemon",
async () => {
if (sc.Status != ServiceControllerStatus.Stopped)
{
sc.Stop();
await this._WaitForStatusAsync(sc, ServiceControllerStatus.Stopped, token);
}
}
);
}
public Task WaitForStatusAsync(EdgeDaemonStatus desired, CancellationToken token)
{
var sc = new ServiceController("iotedge");
return Profiler.Run(
$"Waiting for edge daemon to enter the '{desired.ToString().ToLower()}' state",
() => this._WaitForStatusAsync(sc, (ServiceControllerStatus)desired, token)
);
}
async Task _WaitForStatusAsync(ServiceController sc, ServiceControllerStatus desired, CancellationToken token)
{
while (sc.Status != desired)
{
await Task.Delay(250, token).ConfigureAwait(false);
sc.Refresh();
}
}
}
}

Просмотреть файл

@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
public class EdgeDevice
{
readonly Device device;
readonly IotHub iotHub;
readonly bool owned;
public string ConnectionString =>
$"HostName={this.iotHub.Hostname};" +
$"DeviceId={this.device.Id};" +
$"SharedAccessKey={this.device.Authentication.SymmetricKey.PrimaryKey}";
public string Id => this.device.Id;
EdgeDevice(Device device, bool owned, IotHub iotHub)
{
this.device = device;
this.iotHub = iotHub;
this.owned = owned;
}
public static Task<EdgeDevice> CreateIdentityAsync(
string deviceId,
IotHub iotHub,
CancellationToken token
)
{
return Profiler.Run(
$"Creating edge device '{deviceId}' on hub '{iotHub.Hostname}'",
async () => {
Device device = await iotHub.CreateEdgeDeviceIdentityAsync(deviceId, token);
return new EdgeDevice(device, true, iotHub);
}
);
}
public static async Task<EdgeDevice> GetOrCreateIdentityAsync(
string deviceId,
IotHub iotHub,
CancellationToken token
)
{
Device device = await iotHub.GetDeviceIdentityAsync(deviceId, token);
if (device != null)
{
if (!device.Capabilities.IotEdge)
{
throw new InvalidOperationException(
$"Device '{device.Id}' exists, but is not an edge device"
);
}
Console.WriteLine($"Device '{device.Id}' already exists on hub '{iotHub.Hostname}'");
return new EdgeDevice(device, false, iotHub);
}
else
{
return await CreateIdentityAsync(deviceId, iotHub, token);
}
}
public Task DeleteIdentityAsync(CancellationToken token)
{
return Profiler.Run(
$"Deleting device '{this.Id}'",
() => this.iotHub.DeleteDeviceIdentityAsync(this.device, token)
);
}
public async Task MaybeDeleteIdentityAsync(CancellationToken token)
{
if (this.owned)
{
await this.DeleteIdentityAsync(token);
}
else
{
Console.WriteLine($"Pre-existing device '{this.Id}' was not deleted");
}
}
}
}

Просмотреть файл

@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Edge.Util;
public enum EdgeModuleStatus
{
Running,
Stopped
}
public class EdgeModule
{
protected string DeviceId;
protected IotHub IotHub;
public string Id { get; }
public EdgeModule(string id, string deviceId, IotHub iotHub)
{
this.DeviceId = deviceId;
this.IotHub = iotHub;
this.Id = id;
}
public Task WaitForStatusAsync(EdgeModuleStatus desired, CancellationToken token)
{
return WaitForStatusAsync(new []{this}, desired, token);
}
public static Task WaitForStatusAsync(EdgeModule[] modules, EdgeModuleStatus desired, CancellationToken token)
{
string FormatModulesList() => modules.Length == 1
? $"module '{modules.First().Id}'"
: $"modules ({string.Join(", ", modules.Select(module => module.Id))})";
async Task WaitForStatusAsync() {
try
{
await Retry.Do(
async () =>
{
string[] result = await Process.RunAsync("iotedge", "list", token);
return result
.Where(ln => {
var columns = ln.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
foreach (var module in modules)
{
// each line is "name status"
if (columns[0] == module.Id &&
columns[1].Equals(desired.ToString(), StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}).ToArray();
},
a => a.Length == modules.Length,
e =>
{
// Retry if iotedged's management endpoint is still starting up,
// and therefore isn't responding to `iotedge list` yet
bool DaemonNotReady(string details) =>
details.Contains("Could not list modules", StringComparison.OrdinalIgnoreCase) ||
details.Contains("Socket file could not be found", StringComparison.OrdinalIgnoreCase);
return DaemonNotReady(e.ToString());
},
TimeSpan.FromSeconds(5),
token
);
}
catch (OperationCanceledException)
{
throw new Exception($"Error: timed out waiting for {FormatModulesList()} to start");
}
catch (Exception e)
{
throw new Exception($"Error searching for {FormatModulesList()}: {e}");
}
}
return Profiler.Run(
$"Waiting for {FormatModulesList()} to enter the '{desired.ToString().ToLower()}' state",
WaitForStatusAsync
);
}
public Task WaitForEventsReceivedAsync(CancellationToken token)
{
return Profiler.Run(
$"Receiving events from device '{this.DeviceId}' on Event Hub '{this.IotHub.EntityPath}'",
() => this.IotHub.ReceiveEventsAsync(
this.DeviceId,
data =>
{
data.SystemProperties.TryGetValue("iothub-connection-device-id", out object devId);
data.SystemProperties.TryGetValue("iothub-connection-module-id", out object modId);
return devId != null && devId.ToString().Equals(this.DeviceId)
&& modId != null && modId.ToString().Equals(this.Id);
},
token
)
);
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using Microsoft.Azure.Devices.Edge.Util;
public class EnvironmentVariable
{
public static string Expect(string name) => Preconditions.CheckNonWhiteSpace(
Environment.GetEnvironmentVariable(name),
name
);
}
}

Просмотреть файл

@ -0,0 +1,157 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices;
using Microsoft.Azure.Devices.Common;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Azure.Devices.Shared;
using Microsoft.Azure.EventHubs;
using Newtonsoft.Json;
using DeviceTransportType = Microsoft.Azure.Devices.TransportType;
using EventHubTransportType = Microsoft.Azure.EventHubs.TransportType;
public class IotHub
{
readonly string eventHubEndpoint;
readonly string iotHubConnectionString;
readonly Lazy<RegistryManager> registryManager;
readonly Lazy<ServiceClient> serviceClient;
readonly Lazy<EventHubClient> eventHubClient;
public string Hostname =>
IotHubConnectionStringBuilder.Create(this.iotHubConnectionString).HostName;
public string EntityPath =>
(new EventHubsConnectionStringBuilder(this.eventHubEndpoint)).EntityPath;
RegistryManager RegistryManager => this.registryManager.Value;
ServiceClient ServiceClient => this.serviceClient.Value;
EventHubClient EventHubClient => this.eventHubClient.Value;
public IotHub(string iotHubConnectionString, string eventHubEndpoint, Option<Uri> proxyUri)
{
this.eventHubEndpoint = eventHubEndpoint;
this.iotHubConnectionString = iotHubConnectionString;
Option<IWebProxy> proxy = proxyUri.Map(p => new WebProxy(p) as IWebProxy);
this.registryManager = new Lazy<RegistryManager>(() =>
{
var settings = new HttpTransportSettings();
proxy.ForEach(p => settings.Proxy = p);
return RegistryManager.CreateFromConnectionString(
this.iotHubConnectionString,
settings
);
});
this.serviceClient = new Lazy<ServiceClient>(() =>
{
var settings = new ServiceClientTransportSettings();
proxy.ForEach(p => settings.HttpProxy = p);
return ServiceClient.CreateFromConnectionString(
this.iotHubConnectionString,
DeviceTransportType.Amqp_WebSocket_Only,
settings
);
});
this.eventHubClient = new Lazy<EventHubClient>(() =>
{
var builder = new EventHubsConnectionStringBuilder(this.eventHubEndpoint)
{
TransportType = EventHubTransportType.AmqpWebSockets
};
var client = EventHubClient.CreateFromConnectionString(builder.ToString());
proxy.ForEach(p => client.WebProxy = p);
return client;
});
}
public Task<Device> GetDeviceIdentityAsync(string deviceId, CancellationToken token) =>
this.RegistryManager.GetDeviceAsync(deviceId, token);
public Task<Device> CreateEdgeDeviceIdentityAsync(string deviceId, CancellationToken token)
{
var device = new Device(deviceId)
{
Authentication = new AuthenticationMechanism() { Type = AuthenticationType.Sas },
Capabilities = new DeviceCapabilities() { IotEdge = true }
};
return this.RegistryManager.AddDeviceAsync(device, token);
}
public Task DeleteDeviceIdentityAsync(Device device, CancellationToken token) =>
this.RegistryManager.RemoveDeviceAsync(device);
public Task DeployDeviceConfigurationAsync(
string deviceId,
ConfigurationContent config,
CancellationToken token
) => this.RegistryManager.ApplyConfigurationContentOnDeviceAsync(deviceId, config, token);
public Task<Twin> GetTwinAsync(
string deviceId,
string moduleId,
CancellationToken token
) => this.RegistryManager.GetTwinAsync(deviceId, moduleId, token);
public async Task UpdateTwinAsync(
string deviceId,
string moduleId,
object twinPatch,
CancellationToken token
)
{
Twin twin = await this.GetTwinAsync(deviceId, moduleId, token);
string patch = JsonConvert.SerializeObject(twinPatch);
await this.RegistryManager.UpdateTwinAsync(
deviceId, moduleId, patch, twin.ETag, token);
}
public Task<CloudToDeviceMethodResult> InvokeMethodAsync(
string deviceId,
string moduleId,
CloudToDeviceMethod method,
CancellationToken token
) => this.ServiceClient.InvokeDeviceMethodAsync(deviceId, moduleId, method, token);
public async Task ReceiveEventsAsync(
string deviceId,
Func<EventData, bool> onEventReceived,
CancellationToken token
)
{
EventHubClient client = this.EventHubClient;
int count = (await client.GetRuntimeInformationAsync()).PartitionCount;
string partition = EventHubPartitionKeyResolver.ResolveToPartition(deviceId, count);
PartitionReceiver receiver = client.CreateReceiver("$Default", partition, EventPosition.FromEnd());
var result = new TaskCompletionSource<bool>();
using (token.Register(() => result.TrySetCanceled()))
{
receiver.SetReceiveHandler(
new PartitionReceiveHandler(
data =>
{
bool done = onEventReceived(data);
if (done)
{
result.TrySetResult(true);
}
return done;
}
)
);
await result.Task;
}
await receiver.CloseAsync();
}
}
}

Просмотреть файл

@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Condition="'$(DotNet_Runtime)' != 'netcoreapp3.0'">
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(OS)|$(DotNet_Runtime)' == 'Unix|netcoreapp3.0'">
<TargetFramework>netcoreapp3.0</TargetFramework>
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<!--
Normally, the 'Debug' configuration would work for code coverage, but Microsoft.CodeCoverage currently requires '<DebugType>full</DebugType>' for .NET Core.
See https://github.com/Microsoft/vstest-docs/blob/06f9dc0aeb47be7204dc4e1a98c110ead3e978c7/docs/analyze.md#setup-a-project.
That setting seems to break the "Open Test" context menu in VS IDE, so we'll use a dedicated configuration for code coverage.
-->
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU' ">
<IntermediateOutputPath>obj\CodeCoverage</IntermediateOutputPath>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\CodeCoverage</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices" Version="1.17.1" />
<PackageReference Include="Microsoft.Azure.EventHubs" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="RunProcessAsTask" Version="1.2.3" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />
<PackageReference Include="YamlDotNet" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\edge-util\src\Microsoft.Azure.Devices.Edge.Util\Microsoft.Azure.Devices.Edge.Util.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\stylecop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<Import Project="..\..\stylecop.props" />
</Project>

Просмотреть файл

@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Edge.Util;
using Microsoft.Azure.Devices.Shared;
using Newtonsoft.Json.Linq;
public class ModuleTwin
{
string deviceId;
IotHub iotHub;
string moduleId;
public ModuleTwin(string moduleId, string deviceId, IotHub iotHub)
{
this.deviceId = deviceId;
this.iotHub = iotHub;
this.moduleId = moduleId;
}
public Task UpdateDesiredPropertiesAsync(object patch, CancellationToken token)
{
return Profiler.Run(
$"Updating twin for module '{this.moduleId}'",
() => this.iotHub.UpdateTwinAsync(this.deviceId, this.moduleId, patch, token)
);
}
public Task WaitForReportedPropertyUpdatesAsync(object expectedPatch, CancellationToken token)
{
return Profiler.Run(
$"Waiting for expected twin updates for module '{this.moduleId}'",
() => {
return Retry.Do(
async () => {
Twin twin = await this.iotHub.GetTwinAsync(this.deviceId, this.moduleId, token);
return twin.Properties.Reported;
},
reported => {
JObject expected = JObject.FromObject(expectedPatch)
.Value<JObject>("properties")
.Value<JObject>("reported");
return expected.Value<JObject>().All<KeyValuePair<string, JToken>>(
prop => reported.Contains(prop.Key) && reported[prop.Key] == prop.Value
);
},
null,
TimeSpan.FromSeconds(5),
token
);
}
);
}
}
}

Просмотреть файл

@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.EventHubs;
class PartitionReceiveHandler : IPartitionReceiveHandler
{
readonly Func<EventData, bool> onEventReceived;
public PartitionReceiveHandler(Func<EventData, bool> onEventReceived)
{
this.onEventReceived = onEventReceived;
}
public int MaxBatchSize { get; set; }
public Task ProcessEventsAsync(IEnumerable<EventData> events)
{
if (events != null)
{
foreach (EventData data in events)
{
if (this.onEventReceived(data))
{
break;
}
}
}
return Task.CompletedTask;
}
public Task ProcessErrorAsync(Exception error) => throw error;
}
}

Просмотреть файл

@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using RunProcessAsTask;
public class Process
{
public static async Task<string[]> RunAsync(string name, string args, int timeoutSeconds = 15)
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)))
{
return await RunAsync(name, args, cts.Token);
}
}
public static async Task<string[]> RunAsync(string name, string args, CancellationToken token)
{
var info = new ProcessStartInfo
{
FileName = name,
Arguments = args,
};
using (ProcessResults result = await ProcessEx.RunAsync(info, token))
{
if (result.ExitCode != 0)
{
throw new Win32Exception(result.ExitCode, $"'{name}' failed with: {string.Join("\n", result.StandardError)}");
}
return result.StandardOutput;
}
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
public class Profiler
{
public static Task Run(string startMessage, Func<Task> func, string endMessage = "") => Run(
startMessage,
async () => {
await func();
return true;
},
endMessage
);
public static async Task<T> Run<T>(string startMessage, Func<Task<T>> func, string endMessage = "")
{
Console.Write(startMessage + (string.IsNullOrEmpty(endMessage) ? "..." : "\n"));
var stopwatch = new Stopwatch();
stopwatch.Start();
T t = await func();
stopwatch.Stop();
TimeSpan ts = stopwatch.Elapsed;
Console.WriteLine(
(string.IsNullOrEmpty(endMessage) ? "done" : endMessage) + $" [{ts}]");
return t;
}
}
}

Просмотреть файл

@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.Common
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using YamlDotNet.Serialization;
class YamlDocument
{
readonly Dictionary<object, object> root;
public YamlDocument(string input)
{
var reader = new StringReader(input);
var deserializer = new Deserializer();
this.root = (Dictionary<object, object>)deserializer.Deserialize(reader);
}
public void ReplaceOrAdd(string dottedKey, string value)
{
Dictionary<object, object> node = this.root;
string[] segments = dottedKey.Split('.');
foreach (string key in segments.SkipLast(1))
{
if (!node.ContainsKey(key))
{
node.Add(key, new Dictionary<object, object>());
}
node = (Dictionary<object, object>)node[key];
}
string leaf = segments.Last();
if (!node.ContainsKey(leaf))
{
node.Add(leaf, value);
}
node[leaf] = value;
}
public override string ToString()
{
var serializer = new Serializer();
return serializer.Serialize(this.root);
}
}
}

Просмотреть файл

@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Condition="'$(DotNet_Runtime)' != 'netcoreapp3.0'">
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(OS)|$(DotNet_Runtime)' == 'Unix|netcoreapp3.0'">
<TargetFramework>netcoreapp3.0</TargetFramework>
<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release;CodeCoverage;CheckInBuild</Configurations>
<LangVersion>7.1</LangVersion>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>
<!--
Normally, the 'Debug' configuration would work for code coverage, but Microsoft.CodeCoverage currently requires '<DebugType>full</DebugType>' for .NET Core.
See https://github.com/Microsoft/vstest-docs/blob/06f9dc0aeb47be7204dc4e1a98c110ead3e978c7/docs/analyze.md#setup-a-project.
That setting seems to break the "Open Test" context menu in VS IDE, so we'll use a dedicated configuration for code coverage.
-->
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeCoverage|AnyCPU' ">
<IntermediateOutputPath>obj\CodeCoverage</IntermediateOutputPath>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\CodeCoverage</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Azure.Devices.Edge.Test.Common\Microsoft.Azure.Devices.Edge.Test.Common.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\stylecop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<Import Project="..\..\stylecop.props" />
</Project>

Просмотреть файл

@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.TempSensor
{
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Azure.Devices.Edge.Test.Common;
using Microsoft.Azure.Devices.Edge.Util;
[Command(
Name = "temp-sensor",
Description = "tempSensor end-to-end test",
ExtendedHelpText = @"
The following variables must be set in your environment:
E2E_IOT_HUB_CONNECTION_STRING
E2E_EVENT_HUB_ENDPOINT
If you specify `--registry` and `--user`, the following variable must also be set:
E2E_CONTAINER_REGISTRY_PASSWORD
"
)]
[HelpOption]
[RegistryAndUserOptionsMustBeSpecifiedTogether()]
class Program
{
const string DefaultAgentImage = "mcr.microsoft.com/azureiotedge-agent:1.0";
const string DefaultHubImage = "mcr.microsoft.com/azureiotedge-hub:1.0";
const string DefaultSensorImage = "mcr.microsoft.com/azureiotedge-simulated-temperature-sensor:1.0";
[Required]
[Argument(0, Name = "device-id", Description = "Device ID")]
public string DeviceId { get; }
[Required]
[DirectoryExists]
[Argument(1, Name = "installer-path", Description = "Path to IotEdgeSecurityDaemon.ps1")]
public string InstallerPath { get; }
[DirectoryExists]
[Option("--package-path", Description = "Path to installation packages")]
public string PackagesPath { get; }
[Option("--proxy", Description = "HTTPS proxy for communication with IoT Hub")]
public Uri Proxy { get; }
[Option("--agent", Description = "Edge Agent image name, default is '" + DefaultAgentImage + "'")]
public string AgentImage { get; } = DefaultAgentImage;
[Option("--hub", Description = "Edge Hub image name, default is '" + DefaultHubImage + "'")]
public string HubImage { get; } = DefaultHubImage;
[Option("--temp-sensor", Description = "Simulated temp sensor image name, default is '" + DefaultSensorImage + "'")]
public string SensorImage { get; } = DefaultSensorImage;
[Option("--registry", Description = "Hostname[:port] of the container registry")]
public string RegistryAddress { get; }
[Option("--user", Description = "Username for container registry login")]
public string RegistryUser { get; }
Task<int> OnExecuteAsync()
{
var test = new Test();
return test.RunAsync(new Test.Args
{
DeviceId = this.DeviceId,
ConnectionString = EnvironmentVariable.Expect("E2E_IOT_HUB_CONNECTION_STRING"),
Endpoint = EnvironmentVariable.Expect("E2E_EVENT_HUB_ENDPOINT"),
InstallerPath = this.InstallerPath,
PackagesPath = Option.Maybe(this.PackagesPath),
Proxy = Option.Maybe(this.Proxy),
AgentImage = this.AgentImage,
HubImage = this.HubImage,
SensorImage = this.SensorImage,
Registry = this.RegistryAddress != null
? Option.Some((
this.RegistryAddress,
this.RegistryUser,
EnvironmentVariable.Expect("E2E_CONTAINER_REGISTRY_PASSWORD")))
: Option.None<(string, string, string)>()
});
}
static Task<int> Main(string[] args) => CommandLineApplication.ExecuteAsync<Program>(args);
}
[AttributeUsage(AttributeTargets.Class)]
public class RegistryAndUserOptionsMustBeSpecifiedTogether : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (value is Program obj)
{
if (obj.RegistryAddress == null ^ obj.RegistryUser == null)
{
return new ValidationResult("--registry and --user must be specified together");
}
}
return ValidationResult.Success;
}
}
}

Просмотреть файл

@ -0,0 +1,122 @@
// Copyright (c) Microsoft. All rights reserved.
namespace Microsoft.Azure.Devices.Edge.Test.TempSensor
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Devices.Edge.Test.Common;
using Microsoft.Azure.Devices.Edge.Util;
public class Test
{
public class Args
{
public string DeviceId;
public string ConnectionString;
public string Endpoint;
public string InstallerPath;
public Option<string> PackagesPath;
public Option<Uri> Proxy;
public string AgentImage;
public string HubImage;
public string SensorImage;
public Option<(string address, string username, string password)> Registry;
}
public Task<int> RunAsync(Args args) => Profiler.Run(
"Running tempSensor test",
async () =>
{
using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)))
{
CancellationToken token = cts.Token;
// ** setup
var iotHub = new IotHub(args.ConnectionString, args.Endpoint, args.Proxy);
var device = await EdgeDevice.GetOrCreateIdentityAsync(
args.DeviceId,
iotHub,
token);
var daemon = new EdgeDaemon(args.InstallerPath);
await daemon.UninstallAsync(token);
await daemon.InstallAsync(
device.ConnectionString,
args.PackagesPath,
args.Proxy,
token);
await args.Proxy.Match(
async p =>
{
await daemon.StopAsync(token);
var yaml = new DaemonConfiguration();
yaml.AddHttpsProxy(p);
yaml.Update();
await daemon.StartAsync(token);
},
() => daemon.WaitForStatusAsync(EdgeDaemonStatus.Running, token)
);
var agent = new EdgeAgent(device.Id, iotHub);
await agent.WaitForStatusAsync(EdgeModuleStatus.Running, token);
await agent.PingAsync(token);
// ** test
var config = new EdgeConfiguration(device.Id, args.AgentImage, iotHub);
args.Registry.ForEach(
r => config.AddRegistryCredentials(r.address, r.username, r.password)
);
config.AddEdgeHub(args.HubImage);
args.Proxy.ForEach(p => config.AddProxy(p));
config.AddTempSensor(args.SensorImage);
await config.DeployAsync(token);
var hub = new EdgeModule("edgeHub", device.Id, iotHub);
var sensor = new EdgeModule("tempSensor", device.Id, iotHub);
await EdgeModule.WaitForStatusAsync(
new[] { hub, sensor },
EdgeModuleStatus.Running,
token);
await sensor.WaitForEventsReceivedAsync(token);
var sensorTwin = new ModuleTwin(sensor.Id, device.Id, iotHub);
await sensorTwin.UpdateDesiredPropertiesAsync(
new
{
properties = new
{
desired = new
{
SendData = true,
SendInterval = 10
}
}
},
token);
await sensorTwin.WaitForReportedPropertyUpdatesAsync(
new
{
properties = new
{
reported = new
{
SendData = true,
SendInterval = 10
}
}
},
token);
// ** teardown
await daemon.StopAsync(token);
await device.MaybeDeleteIdentityAsync(token);
}
return 0;
},
"Completed tempSensor test"
);
}
}