Skip to content

Commit 1d62b09

Browse files
authored
Rewrite Debian packaging tests without Zafiro filesystem (#141)
1 parent a2acf3d commit 1d62b09

File tree

13 files changed

+349
-290
lines changed

13 files changed

+349
-290
lines changed

DotnetPackaging.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Msix.Tests"
4343
EndProject
4444
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Exe.Tests", "test\DotnetPackaging.Exe.Tests\DotnetPackaging.Exe.Tests.csproj", "{C9B6586A-332B-40B0-9F2B-BBFFD7F5CFCB}"
4545
EndProject
46+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetPackaging.Deb.Tests", "test\DotnetPackaging.Deb.Tests\DotnetPackaging.Deb.Tests.csproj", "{B9A9B63E-2D76-4BA5-BA23-8151082C9049}"
47+
EndProject
4648
Global
4749
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4850
Debug|Any CPU = Debug|Any CPU
@@ -233,6 +235,18 @@ Global
233235
{C9B6586A-332B-40B0-9F2B-BBFFD7F5CFCB}.Release|x64.Build.0 = Release|Any CPU
234236
{C9B6586A-332B-40B0-9F2B-BBFFD7F5CFCB}.Release|x86.ActiveCfg = Release|Any CPU
235237
{C9B6586A-332B-40B0-9F2B-BBFFD7F5CFCB}.Release|x86.Build.0 = Release|Any CPU
238+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
239+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Debug|Any CPU.Build.0 = Debug|Any CPU
240+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Debug|x64.ActiveCfg = Debug|Any CPU
241+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Debug|x64.Build.0 = Debug|Any CPU
242+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Debug|x86.ActiveCfg = Debug|Any CPU
243+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Debug|x86.Build.0 = Debug|Any CPU
244+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Release|Any CPU.ActiveCfg = Release|Any CPU
245+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Release|Any CPU.Build.0 = Release|Any CPU
246+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Release|x64.ActiveCfg = Release|Any CPU
247+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Release|x64.Build.0 = Release|Any CPU
248+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Release|x86.ActiveCfg = Release|Any CPU
249+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049}.Release|x86.Build.0 = Release|Any CPU
236250
EndGlobalSection
237251
GlobalSection(SolutionProperties) = preSolution
238252
HideSolutionNode = FALSE
@@ -243,6 +257,7 @@ Global
243257
{D97A5E33-6621-44DD-83D0-65F55F56E487} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
244258
{DD050A6C-D84F-47AC-8453-18A57F751587} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
245259
{C9B6586A-332B-40B0-9F2B-BBFFD7F5CFCB} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
260+
{B9A9B63E-2D76-4BA5-BA23-8151082C9049} = {7E0C2A39-1C29-4B5D-9D45-1B6B7E7A77B6}
246261
EndGlobalSection
247262
GlobalSection(ExtensibilityGlobals) = postSolution
248263
SolutionGuid = {1D869DF1-9147-472D-A3D9-D519518A6951}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
namespace DotnetPackaging.Deb.Tests;
2+
3+
internal static class ArArchiveReader
4+
{
5+
private const string Magic = "!<arch>\n";
6+
private const int HeaderLength = 60;
7+
8+
public static IReadOnlyList<ArEntry> Read(byte[] archive)
9+
{
10+
if (archive.Length < Magic.Length)
11+
{
12+
throw new InvalidDataException("AR archive is too small to contain the magic header.");
13+
}
14+
15+
var signature = Encoding.ASCII.GetString(archive, 0, Magic.Length);
16+
if (!string.Equals(signature, Magic, StringComparison.Ordinal))
17+
{
18+
throw new InvalidDataException($"Invalid AR signature '{signature}'.");
19+
}
20+
21+
var entries = new List<ArEntry>();
22+
var position = Magic.Length;
23+
24+
while (position + HeaderLength <= archive.Length)
25+
{
26+
var header = archive.AsSpan(position, HeaderLength);
27+
var name = NormalizeName(ReadString(header[..16]));
28+
var size = ParseDecimal(ReadString(header.Slice(48, 10)));
29+
30+
var dataStart = position + HeaderLength;
31+
var dataEnd = dataStart + size;
32+
if (dataEnd > archive.Length)
33+
{
34+
throw new InvalidDataException($"Entry '{name}' declares {size} bytes but archive ends earlier.");
35+
}
36+
37+
var data = archive[dataStart..dataEnd];
38+
entries.Add(new ArEntry(name, data.ToArray()));
39+
40+
// AR members are aligned to even byte boundaries.
41+
var alignedSize = size % 2 == 0 ? size : size + 1;
42+
position = dataStart + alignedSize;
43+
}
44+
45+
return entries;
46+
}
47+
48+
private static string NormalizeName(string rawName)
49+
{
50+
var trimmed = rawName.Trim();
51+
return trimmed.EndsWith("/", StringComparison.Ordinal) ? trimmed[..^1] : trimmed;
52+
}
53+
54+
private static string ReadString(ReadOnlySpan<byte> span) => Encoding.ASCII.GetString(span);
55+
56+
private static int ParseDecimal(string value)
57+
{
58+
if (!int.TryParse(value.Trim(), out var parsed))
59+
{
60+
throw new InvalidDataException($"AR header contains an invalid size value '{value}'.");
61+
}
62+
63+
return parsed;
64+
}
65+
}
66+
67+
public sealed record ArEntry(string Name, byte[] Data);

test/DotnetPackaging.Deb.Tests/ArFileTests.cs

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using CSharpFunctionalExtensions;
2+
using DotnetPackaging;
3+
using DotnetPackaging.Deb.Archives.Deb;
4+
using System.IO.Abstractions;
5+
using Zafiro.DivineBytes;
6+
using Zafiro.DivineBytes.System.IO;
7+
8+
namespace DotnetPackaging.Deb.Tests;
9+
10+
internal static class DebCommandRunner
11+
{
12+
public static Task<int> BuildDebAsync(string sourceDirectory, string outputFilePath)
13+
{
14+
return BuildWithApiAsync(sourceDirectory, outputFilePath);
15+
}
16+
17+
private static async Task<int> BuildWithApiAsync(string sourceDirectory, string outputFilePath)
18+
{
19+
var fs = new FileSystem();
20+
var dirInfo = new DirectoryInfo(sourceDirectory);
21+
var container = new DirectoryContainer(new DirectoryInfoWrapper(fs, dirInfo)).AsRoot();
22+
23+
var options = new Options
24+
{
25+
Name = Maybe.From("Sample App"),
26+
Version = Maybe.From("1.0.0"),
27+
Comment = Maybe.From("Sample package for tests"),
28+
ExecutableName = Maybe.From("sample-app")
29+
};
30+
31+
var result = await DotnetPackaging.Deb.DebFile.From()
32+
.Container(container, "Sample App")
33+
.Configure(cfg => cfg.From(options))
34+
.Build();
35+
36+
if (result.IsFailure)
37+
{
38+
return -1;
39+
}
40+
41+
var data = DebMixin.ToData(result.Value);
42+
await using var stream = File.Open(outputFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
43+
await ByteSource.FromByteObservable(data.Bytes).ToStream().CopyToAsync(stream);
44+
return 0;
45+
}
46+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using DotnetPackaging.Tool.Commands;
2+
3+
namespace DotnetPackaging.Deb.Tests;
4+
5+
[CollectionDefinition("deb-package")]
6+
public class DebPackageCollection : ICollectionFixture<DebPackageFixture>
7+
{
8+
}
9+
10+
public sealed class DebPackageFixture : IAsyncLifetime
11+
{
12+
private readonly string workingDirectory = Path.Combine(Path.GetTempPath(), $"deb-tests-{Guid.NewGuid():N}");
13+
private readonly string sourceDirectory;
14+
private readonly string outputFilePath;
15+
16+
public DebPackageFixture()
17+
{
18+
sourceDirectory = Path.Combine(workingDirectory, "publish");
19+
outputFilePath = Path.Combine(workingDirectory, "sample-app.deb");
20+
}
21+
22+
public IReadOnlyList<ArEntry> ArEntries { get; private set; } = Array.Empty<ArEntry>();
23+
24+
public byte[] PackageBytes { get; private set; } = Array.Empty<byte>();
25+
26+
public string OutputPath => outputFilePath;
27+
28+
public ArEntry GetEntry(string name) => ArEntries.Single(e => string.Equals(e.Name, name, StringComparison.Ordinal));
29+
30+
public async Task InitializeAsync()
31+
{
32+
Directory.CreateDirectory(sourceDirectory);
33+
await WritePayloadAsync(sourceDirectory);
34+
Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!);
35+
36+
var exitCode = await DebCommandRunner.BuildDebAsync(sourceDirectory, outputFilePath);
37+
if (exitCode != 0)
38+
{
39+
throw new InvalidOperationException($"Building .deb failed with exit code {exitCode}.");
40+
}
41+
42+
PackageBytes = await File.ReadAllBytesAsync(outputFilePath);
43+
ArEntries = ArArchiveReader.Read(PackageBytes);
44+
}
45+
46+
public Task DisposeAsync()
47+
{
48+
if (Directory.Exists(workingDirectory))
49+
{
50+
try
51+
{
52+
Directory.Delete(workingDirectory, true);
53+
}
54+
catch
55+
{
56+
// Best-effort cleanup for temporary files.
57+
}
58+
}
59+
60+
return Task.CompletedTask;
61+
}
62+
63+
private static async Task WritePayloadAsync(string directory)
64+
{
65+
var executablePath = Path.Combine(directory, "sample-app");
66+
await File.WriteAllBytesAsync(executablePath, CreateElfStub());
67+
68+
var configDir = Path.Combine(directory, "config");
69+
Directory.CreateDirectory(configDir);
70+
await File.WriteAllTextAsync(Path.Combine(configDir, "settings.json"), "{ \"name\": \"demo\" }");
71+
}
72+
73+
private static byte[] CreateElfStub()
74+
{
75+
var bytes = new byte[64];
76+
bytes[0] = 0x7F;
77+
bytes[1] = (byte)'E';
78+
bytes[2] = (byte)'L';
79+
bytes[3] = (byte)'F';
80+
bytes[4] = 2; // 64-bit
81+
bytes[5] = 1; // Little endian
82+
BitConverter.GetBytes((ushort)2).CopyTo(bytes, 16); // ET_EXEC
83+
BitConverter.GetBytes((ushort)0x3E).CopyTo(bytes, 18); // x86_64
84+
return bytes;
85+
}
86+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
namespace DotnetPackaging.Deb.Tests;
2+
3+
[Collection("deb-package")]
4+
public class DebPackageTests
5+
{
6+
private readonly DebPackageFixture fixture;
7+
8+
public DebPackageTests(DebPackageFixture fixture)
9+
{
10+
this.fixture = fixture;
11+
}
12+
13+
[Fact]
14+
public void Ar_archive_contains_required_members()
15+
{
16+
fixture.ArEntries.Should().HaveCount(3);
17+
fixture.PackageBytes[..8].Should().Equal(Encoding.ASCII.GetBytes("!<arch>\n"));
18+
19+
fixture.ArEntries.Select(e => e.Name).Should().ContainInOrder("debian-binary", "control.tar", "data.tar");
20+
21+
var debianBinary = fixture.GetEntry("debian-binary");
22+
Encoding.ASCII.GetString(debianBinary.Data).Should().Be("2.0\n");
23+
}
24+
25+
[Fact]
26+
public void Control_tar_contains_expected_metadata()
27+
{
28+
var controlEntry = fixture.GetEntry("control.tar");
29+
var controlTar = TarArchiveReader.ReadEntries(controlEntry.Data);
30+
controlTar.Select(e => e.Name).Should().Contain("control");
31+
32+
var controlFile = controlTar.Single(e => e.Name == "control");
33+
var fields = ParseFields(Encoding.ASCII.GetString(controlFile.Data));
34+
35+
fields.Should().ContainKey("Package").WhoseValue.Should().Be("sample-app");
36+
fields.Should().ContainKey("Version").WhoseValue.Should().Be("1.0.0");
37+
fields.Should().ContainKey("Architecture").WhoseValue.Should().Be("amd64");
38+
fields.Should().ContainKey("Description").WhoseValue.Should().Be("Sample package for tests");
39+
fields.Should().ContainKey("Maintainer").WhoseValue.Should().Be("Unknown Maintainer <[email protected]>");
40+
}
41+
42+
[Fact]
43+
public void Data_tar_includes_payload_and_launchers()
44+
{
45+
var dataEntry = fixture.GetEntry("data.tar");
46+
var dataTar = TarArchiveReader.ReadEntries(dataEntry.Data);
47+
48+
dataTar.Select(e => e.Name).Should().Contain(new[]
49+
{
50+
"opt/sample-app/sample-app",
51+
"opt/sample-app/config/settings.json",
52+
"usr/share/applications/sample-app.desktop",
53+
"usr/bin/sample-app"
54+
});
55+
56+
var binary = dataTar.Single(e => e.Name == "opt/sample-app/sample-app");
57+
binary.Data.Should().StartWith(new byte[] { 0x7F, (byte)'E', (byte)'L', (byte)'F' });
58+
59+
var config = dataTar.Single(e => e.Name == "opt/sample-app/config/settings.json");
60+
Encoding.UTF8.GetString(config.Data).Should().Contain("\"demo\"");
61+
62+
var launcher = dataTar.Single(e => e.Name == "usr/bin/sample-app");
63+
var launcherText = Encoding.ASCII.GetString(launcher.Data);
64+
launcherText.Should().StartWith("#!/usr/bin/env sh\n");
65+
launcherText.Should().Contain("/opt/sample-app/sample-app");
66+
67+
var desktop = dataTar.Single(e => e.Name == "usr/share/applications/sample-app.desktop");
68+
var desktopText = Encoding.ASCII.GetString(desktop.Data);
69+
desktopText.Should().Contain("Name=Sample App");
70+
desktopText.Should().Contain("Exec=\"/opt/sample-app/sample-app\"");
71+
}
72+
73+
private static IDictionary<string, string> ParseFields(string content)
74+
{
75+
return content
76+
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
77+
.Select(line => line.Split(": ", 2, StringSplitOptions.None))
78+
.Where(parts => parts.Length == 2)
79+
.ToDictionary(parts => parts[0], parts => parts[1], StringComparer.OrdinalIgnoreCase);
80+
}
81+
}

0 commit comments

Comments
 (0)