Skip to content

Commit 1d9f518

Browse files
committed
Add Flatpak packaging integration test (#107)
1 parent b7bc3d3 commit 1d9f518

File tree

3 files changed

+224
-0
lines changed

3 files changed

+224
-0
lines changed

DotnetPackaging.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Exe.Install
3939
EndProject
4040
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Exe", "src\DotnetPackaging.Exe\DotnetPackaging.Exe.csproj", "{E0875AD7-1D78-4FC3-EE2D-24AC1E066BFA}"
4141
EndProject
42+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}"
43+
EndProject
44+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Flatpak.Tests", "test\DotnetPackaging.Flatpak.Tests\DotnetPackaging.Flatpak.Tests.csproj", "{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}"
45+
EndProject
4246
Global
4347
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4448
Debug|Any CPU = Debug|Any CPU
@@ -205,11 +209,26 @@ Global
205209
{E0875AD7-1D78-4FC3-EE2D-24AC1E066BFA}.Release|x64.Build.0 = Release|Any CPU
206210
{E0875AD7-1D78-4FC3-EE2D-24AC1E066BFA}.Release|x86.ActiveCfg = Release|Any CPU
207211
{E0875AD7-1D78-4FC3-EE2D-24AC1E066BFA}.Release|x86.Build.0 = Release|Any CPU
212+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
213+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Debug|Any CPU.Build.0 = Debug|Any CPU
214+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Debug|x64.ActiveCfg = Debug|Any CPU
215+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Debug|x64.Build.0 = Debug|Any CPU
216+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Debug|x86.ActiveCfg = Debug|Any CPU
217+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Debug|x86.Build.0 = Debug|Any CPU
218+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Release|Any CPU.ActiveCfg = Release|Any CPU
219+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Release|Any CPU.Build.0 = Release|Any CPU
220+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Release|x64.ActiveCfg = Release|Any CPU
221+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Release|x64.Build.0 = Release|Any CPU
222+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Release|x86.ActiveCfg = Release|Any CPU
223+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69}.Release|x86.Build.0 = Release|Any CPU
208224
EndGlobalSection
209225
GlobalSection(SolutionProperties) = preSolution
210226
HideSolutionNode = FALSE
211227
EndGlobalSection
212228
GlobalSection(NestedProjects) = preSolution
229+
{79928283-5F6E-49F4-ACC4-FE5994DA1116} = {F33ABB7A-E6A3-4F45-BFB5-9A014B8C5F12}
230+
{DF914DC0-903B-4733-8F85-45F733F2DE5F} = {F33ABB7A-E6A3-4F45-BFB5-9A014B8C5F12}
231+
{F090C5BD-3B1C-4D32-93AD-5D90E4819A69} = {F33ABB7A-E6A3-4F45-BFB5-9A014B8C5F12}
213232
{79928283-5F6E-49F4-ACC4-FE5994DA1116} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
214233
{DF914DC0-903B-4733-8F85-45F733F2DE5F} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
215234
{6F3C5C5A-61E4-4D16-BE0D-6A0E9E9B3F9D} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
13+
<PackageReference Include="xunit" />
14+
<PackageReference Include="xunit.runner.visualstudio" />
15+
<PackageReference Include="CSharpFunctionalExtensions.FluentAssertions" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<Using Include="Xunit" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using System.Diagnostics;
2+
using FluentAssertions;
3+
using Xunit;
4+
using Xunit.Sdk;
5+
6+
namespace DotnetPackaging.Flatpak.Tests;
7+
8+
public sealed class FlatpakFromProjectTests
9+
{
10+
private const string AppId = "com.example.dotnetpackaging.tool";
11+
12+
[Fact]
13+
public async Task DotnetPackagingTool_bundle_passes_flatpak_validation()
14+
{
15+
if (!OperatingSystem.IsLinux())
16+
{
17+
throw new XunitException("Flatpak validation requires Linux.");
18+
}
19+
20+
Assert.True(CommandExists("flatpak"), "Flatpak CLI is required to verify bundles.");
21+
Assert.True(CommandExists("ostree"), "ostree CLI is required to inspect bundle contents.");
22+
23+
using var temp = new TempDirectory();
24+
var repoRoot = GetRepositoryRoot();
25+
var projectPath = Path.Combine(repoRoot, "src", "DotnetPackaging.Tool", "DotnetPackaging.Tool.csproj");
26+
var bundlePath = Path.Combine(temp.Path, "DotnetPackaging.Tool.flatpak");
27+
var repoPath = Path.Combine(temp.Path, "repo");
28+
var checkoutPath = Path.Combine(temp.Path, "checkout");
29+
30+
Directory.CreateDirectory(repoPath);
31+
32+
var environment = new Dictionary<string, string>
33+
{
34+
["DOTNET_ROLL_FORWARD"] = "Major",
35+
};
36+
37+
await Execute(
38+
"dotnet",
39+
$"run --project \"{projectPath}\" -- flatpak from-project --project \"{projectPath}\" --output \"{bundlePath}\" --system",
40+
repoRoot,
41+
environment);
42+
43+
File.Exists(bundlePath).Should().BeTrue("the CLI should produce a Flatpak bundle");
44+
45+
await Execute("ostree", $"init --mode=archive-z2 --repo=\"{repoPath}\"");
46+
await Execute("flatpak", $"build-import-bundle \"{repoPath}\" \"{bundlePath}\"");
47+
await Execute("ostree", $"checkout --repo=\"{repoPath}\" app/{AppId}/x86_64/stable \"{checkoutPath}\"");
48+
49+
var metadataPath = Path.Combine(checkoutPath, "metadata");
50+
File.Exists(metadataPath).Should().BeTrue();
51+
var metadata = await File.ReadAllTextAsync(metadataPath);
52+
metadata.Should().Contain($"name={AppId}");
53+
metadata.Should().Contain("runtime=org.freedesktop.Platform/x86_64/23.08");
54+
metadata.Should().Contain($"command={AppId}");
55+
56+
var desktopPath = Path.Combine(checkoutPath, "export", "share", "applications", $"{AppId}.desktop");
57+
File.Exists(desktopPath).Should().BeTrue();
58+
59+
var metainfoPath = Path.Combine(checkoutPath, "export", "share", "metainfo", $"{AppId}.metainfo.xml");
60+
File.Exists(metainfoPath).Should().BeTrue();
61+
62+
var wrapperPath = Path.Combine(checkoutPath, "files", "bin", AppId);
63+
File.Exists(wrapperPath).Should().BeTrue();
64+
65+
var executablePath = Path.Combine(checkoutPath, "files", "DotnetPackaging.Tool");
66+
File.Exists(executablePath).Should().BeTrue();
67+
68+
var managedAssembly = Path.Combine(checkoutPath, "files", "DotnetPackaging.dll");
69+
File.Exists(managedAssembly).Should().BeTrue();
70+
}
71+
72+
private static string GetRepositoryRoot()
73+
{
74+
var directory = AppContext.BaseDirectory;
75+
while (!string.IsNullOrEmpty(directory))
76+
{
77+
var solutionPath = Path.Combine(directory, "DotnetPackaging.sln");
78+
if (File.Exists(solutionPath))
79+
{
80+
return directory;
81+
}
82+
83+
directory = Directory.GetParent(directory)?.FullName ?? string.Empty;
84+
}
85+
86+
throw new InvalidOperationException("Could not locate the repository root.");
87+
}
88+
89+
private static async Task<CommandResult> Execute(
90+
string fileName,
91+
string arguments,
92+
string? workingDirectory = null,
93+
IDictionary<string, string>? environment = null)
94+
{
95+
var startInfo = new ProcessStartInfo(fileName, arguments)
96+
{
97+
RedirectStandardOutput = true,
98+
RedirectStandardError = true,
99+
UseShellExecute = false,
100+
};
101+
102+
if (!string.IsNullOrEmpty(workingDirectory))
103+
{
104+
startInfo.WorkingDirectory = workingDirectory;
105+
}
106+
107+
if (environment is not null)
108+
{
109+
foreach (var pair in environment)
110+
{
111+
startInfo.Environment[pair.Key] = pair.Value;
112+
}
113+
}
114+
115+
using var process = Process.Start(startInfo) ?? throw new InvalidOperationException($"Unable to start '{fileName}'.");
116+
var stdOutTask = process.StandardOutput.ReadToEndAsync();
117+
var stdErrTask = process.StandardError.ReadToEndAsync();
118+
await Task.WhenAll(stdOutTask, stdErrTask, process.WaitForExitAsync());
119+
120+
if (process.ExitCode != 0)
121+
{
122+
throw new XunitException(
123+
$"Command '{fileName} {arguments}' failed with exit code {process.ExitCode}.{Environment.NewLine}{stdOutTask.Result}{stdErrTask.Result}");
124+
}
125+
126+
return new CommandResult(stdOutTask.Result, stdErrTask.Result);
127+
}
128+
129+
private static bool CommandExists(string commandName)
130+
{
131+
var pathValue = Environment.GetEnvironmentVariable("PATH");
132+
if (string.IsNullOrWhiteSpace(pathValue))
133+
{
134+
return false;
135+
}
136+
137+
var suffixes = OperatingSystem.IsWindows()
138+
? new[] { ".exe", ".bat", ".cmd" }
139+
: new[] { string.Empty };
140+
141+
foreach (var path in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
142+
{
143+
foreach (var suffix in suffixes)
144+
{
145+
var candidate = Path.Combine(path, commandName + suffix);
146+
if (File.Exists(candidate))
147+
{
148+
return true;
149+
}
150+
}
151+
}
152+
153+
return false;
154+
}
155+
156+
private sealed record CommandResult(string StandardOutput, string StandardError);
157+
158+
private sealed class TempDirectory : IDisposable
159+
{
160+
public TempDirectory()
161+
{
162+
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "DotnetPackaging.Flatpak.Tests", Guid.NewGuid().ToString("N"));
163+
Directory.CreateDirectory(Path);
164+
}
165+
166+
public string Path { get; }
167+
168+
public void Dispose()
169+
{
170+
try
171+
{
172+
if (Directory.Exists(Path))
173+
{
174+
Directory.Delete(Path, true);
175+
}
176+
}
177+
catch
178+
{
179+
// Ignore cleanup failures
180+
}
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)