Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/ci_integration_tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Integration tests with Home Assistant
on:
pull_request:
branches:
- main
- dev
push:
branches:
- main
- dev
schedule:
- cron: "0 2 * * *" # run at 2 AM UTC

jobs:
init:
name: Initialize tests with Home Assistant
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.versions.outputs.versions }}
steps:
- name: Set versions
id: versions
run: |
versions='"stable", "beta"'
if ${{ github.event_name == 'schedule' }}; then
versions+=', "dev"'
fi
echo "[${versions}]"
echo "::set-output name=versions::[${versions}]"

test_with_home_assistant:
name: Integration tests with Home Assistant
needs: init
strategy:
fail-fast: false
matrix:
channel: ${{ fromJson(needs.init.outputs.versions) }}
with_integration: [yes, no]
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout the repository
uses: actions/checkout@main

- name: 🥅 Install .Net 6
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x" # SDK Version

- name: 🛠️ Build code
run: dotnet build

- name: 🔽 Download HA integration
if: matrix.with_integration == 'yes'
run: |
git clone --depth 1 https://github.com/net-daemon/integration.git /tmp/integration
mv /tmp/integration/custom_components ${{github.workspace}}/tests/Integration/HA/config/
mv /tmp/integration/.github/test_configuration/.storage ${{github.workspace}}/tests/Integration/HA/config/

- name: 👷 Setup Home Assistant
id: homeassistant
uses: ludeeus/setup-homeassistant@main
with:
tag: ${{ matrix.channel }}
config-dir: ./tests/Integration/HA/config

- name: 🧪 Run integration tests
env:
HomeAssistant__Token: ${{ steps.homeassistant.outputs.token }}
HomeAssistant__Host: "127.0.0.1"
NetDaemon__ApplicationConfigurationFolder: ${{github.workspace}}/tests/Integration/NetDaemon.Tests.Integration/apps
NetDaemon__Admin: false
run: |
dotnet test ${{github.workspace}}/tests/Integration/NetDaemon.Tests.Integration

- name: Get Home Assistant log in failure
if: ${{ failure() }}
run: docker logs ${{ steps.homeassistant.outputs.container_name }}

- name: Discord failure notification
if: ${{ github.event_name == 'schedule' && failure() }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_ACTION_FAILURE }}
uses: Ilshidur/action-discord@0.3.2
with:
args: "[Scheduled action failed!](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}})"
15 changes: 11 additions & 4 deletions tests/Integration/HA/config/configuration.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

# Configure a default setup of Home Assistant (frontend, api, etc)
default_config:
http:
# ssl_certificate: /config/ssl/homeassistant.local.pem
# ssl_key: /config/ssl/homeassistant.local-key.pem

# Text to speech
tts:
Expand All @@ -20,8 +22,13 @@ input_select:
options:
- Visitors
- Visitors with kids
- Home Alone
- Home Alone

input_text:
test_result:
name: Test results
initial: No tests is run yet

template:
- trigger:
- platform: webhook
Expand All @@ -36,5 +43,5 @@ automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml

logger:
default: debug
#logger:
# default: debug
78 changes: 78 additions & 0 deletions tests/Integration/NetDaemon.Tests.Integration/BasicTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using FluentAssertions;
using NetDaemon.AppModel;
using NetDaemon.HassModel.Common;
using NetDaemon.HassModel.Entities;
using NetDaemon.Tests.Integration.Helpers;
using Xunit;

namespace NetDaemon.Tests.Integration;

[NetDaemonApp]
public class BasicApp
{
public BasicApp(
IHaContext haContext
)
{
haContext.StateChanges()
.Where(n =>
n.Entity.EntityId == "input_select.who_cooks"
)
.Subscribe(
s => haContext.CallService("input_text", "set_value",
ServiceTarget.FromEntities("input_text.test_result"), new {value = s.New?.State})
);
}
}

public class BasicTests : IClassFixture<MakeSureNetDaemonIsRunningFixture>
{
private readonly IHaContext _haContext;

public BasicTests(
MakeSureNetDaemonIsRunningFixture _,
IHaContext haContext
)
{
_haContext = haContext;
}

[Fact]
public async Task BasicTestApp_ShouldChangeStateOfInputTextToTheStateOfInputSelect_WhenChange()
{
var optionToSet = GetDifferentOptionThanCurrentlySelected();

var waitTask = _haContext.StateChanges()
.Where(n => n.Entity.EntityId == "input_text.test_result")
.Timeout(TimeSpan.FromMilliseconds(5000))
.FirstAsync()
.ToTask();

_haContext.CallService(
"input_select",
"select_option",
ServiceTarget.FromEntities("input_select.who_cooks"),
new {option = optionToSet});

var result = await waitTask.ConfigureAwait(false);

result.New!.State.Should().Be(optionToSet);
}

private string GetDifferentOptionThanCurrentlySelected()
{
var currentState = _haContext.GetState("input_select.who_cooks")?.State
?? throw new InvalidOperationException();

var useOption = currentState switch
{
"Paulus" => "Anne Therese",
_ => "Paulus"
};
return useOption;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Threading;
using System.Threading.Tasks;
using NetDaemon.Client.Common;
using Xunit;

namespace NetDaemon.Tests.Integration.Helpers;

/// <summary>
/// This fixture is ran once per test session and will
/// make sure Home Assistant is running and we have a
/// valid connection before tests start
/// </summary>
public class MakeSureNetDaemonIsRunningFixture : IAsyncLifetime
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly IHomeAssistantRunner _homeAssistantRunner;

public MakeSureNetDaemonIsRunningFixture(
IHomeAssistantRunner homeAssistantRunner
)
{
_homeAssistantRunner = homeAssistantRunner;
}

public async Task InitializeAsync()
{
var connectionRetries = 0;
while (!_cancellationTokenSource.Token.IsCancellationRequested)
{
if (_homeAssistantRunner is not null)
break;
if (connectionRetries++ > 20) // 40 seconds and we give up
return;
await Task.Delay(2000, _cancellationTokenSource.Token).ConfigureAwait(false);
}

// Introduce a small delay so everything is connected and logged
await Task.Delay(2000, _cancellationTokenSource.Token).ConfigureAwait(false);
}

public Task DisposeAsync()
{
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.4.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"/>
<PackageReference Include="xunit" Version="2.4.1"/>
<PackageReference Include="Xunit.DependencyInjection" Version="8.3.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.Extensions.Hosting.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<HintPath>..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.0\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\AppModel\NetDaemon.AppModel\NetDaemon.AppModel.csproj"/>
<ProjectReference Include="..\..\..\src\Runtime\NetDaemon.Runtime\NetDaemon.Runtime.csproj"/>
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="apps/**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
28 changes: 28 additions & 0 deletions tests/Integration/NetDaemon.Tests.Integration/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Reflection;
using Microsoft.Extensions.Hosting;
using NetDaemon.AppModel;
using NetDaemon.Runtime;

namespace NetDaemon.Tests.Integration;

/// <summary>
/// Startup class
/// </summary>
/// <remarks>
/// XUnit.DependencyInjection logic finds the startup here and we can make our custom
/// host builder that allows dependency injection in test classes
/// </remarks>
public class Startup
{
public IHostBuilder CreateHostBuilder(AssemblyName assemblyName)
{
return Host.CreateDefaultBuilder()
.UseNetDaemonAppSettings()
.UseNetDaemonRuntime()
.ConfigureServices((_, services) =>
services
.AddAppsFromAssembly(Assembly.GetExecutingAssembly())
.AddNetDaemonStateManager()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Warning"
},
"ConsoleThemeType": "System"
},
"HomeAssistant": {
"Host": "ENTER YOUR IP TO Development Home Assistant here",
"Port": 8124,
"Ssl": false,
"Token": "ENTER YOUR TOKEN",
"InsecureBypassCertificateErrors": false
},
"NetDaemon": {
"Admin": false,
"ApplicationConfigurationFolder": "./apps"
}
}
Empty file.
20 changes: 20 additions & 0 deletions tests/Integration/NetDaemon.Tests.Integration/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Warning"
},
"ConsoleThemeType": "Ansi"
},
"HomeAssistant": {
"Host": "localhost",
"Port": 8123,
"Ssl": false,
"Token": "",
"InsecureBypassCertificateErrors": false
},
"NetDaemon": {
"Admin": true,
"ApplicationConfigurationFolder": "./apps"
}
}