mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
401 lines
20 KiB
C#
401 lines
20 KiB
C#
using System.Runtime.Versioning;
|
|
using Xunit;
|
|
|
|
namespace Wayscriber.Tools.Tests;
|
|
|
|
public sealed class ReleaseParityRegressionTests
|
|
{
|
|
private const string ArchiveRootName = "wayscriber-v1.2.3-linux-x86_64";
|
|
private const string AskpassModeVariable = EnvironmentVariables.WayscriberSshAskpass;
|
|
private const string PassphraseVariable = EnvironmentVariables.AurSshPassphrase;
|
|
private const string SyntheticPassphrase = "synthetic-passphrase";
|
|
private const string RepositoryDeployKey = "repository-deploy-key";
|
|
private const string TestDotnetSdk = "11.0.100-rc.1.26425.128";
|
|
private const string DotnetLogVariable = "WAYSCRIBER_DOTNET_LOG";
|
|
|
|
[Fact]
|
|
public async Task AurAskpassEnvironmentIsScopedToSshAdd( )
|
|
{
|
|
using var fixture = new TemporaryDirectory( "wayscriber-aur-askpass-test" );
|
|
var githubEnvironment = Path.Combine( fixture.Path, "github-environment" );
|
|
var environment = new Dictionary<string, string?>( StringComparer.Ordinal )
|
|
{
|
|
[EnvironmentVariables.Home] = fixture.Path,
|
|
[EnvironmentVariables.AurSshPrivateKey] = "private-key",
|
|
[EnvironmentVariables.AurSshKnownHosts] = "aur.archlinux.org ssh-ed25519 host-key\n",
|
|
[PassphraseVariable] = SyntheticPassphrase,
|
|
[EnvironmentVariables.GitHubEnvironment] = githubEnvironment,
|
|
};
|
|
var runner = new AskpassAurRunner( );
|
|
var context = CreateContext( runner, name => environment.GetValueOrDefault( name ) );
|
|
var command = ReleaseAurCommands.Commands.Single( item => item.Area == "aur" && item.Name == "prepare-ssh" );
|
|
|
|
Assert.Equal( ExitCodes.Success, await command.Handler( context, [] ) );
|
|
|
|
var sshAdd = Assert.Single( runner.Requests, request => request.FileName == "setsid" );
|
|
Assert.Equal( "1", sshAdd.Environment![AskpassModeVariable] );
|
|
Assert.Equal( SyntheticPassphrase, sshAdd.Environment[PassphraseVariable] );
|
|
|
|
var git = Assert.Single( runner.Requests, request => request.FileName == "git" );
|
|
Assert.False( git.Environment!.ContainsKey( AskpassModeVariable ) );
|
|
Assert.False( git.Environment.ContainsKey( PassphraseVariable ) );
|
|
Assert.Equal( AskpassAurRunner.AgentSocket, git.Environment[EnvironmentVariables.SshAuthSocket] );
|
|
Assert.Equal( AskpassAurRunner.AgentProcessId, git.Environment[EnvironmentVariables.SshAgentProcessId] );
|
|
|
|
var persisted = File.ReadAllText( githubEnvironment );
|
|
Assert.Contains( $"SSH_AUTH_SOCK={AskpassAurRunner.AgentSocket}\n", persisted, StringComparison.Ordinal );
|
|
Assert.Contains( $"SSH_AGENT_PID={AskpassAurRunner.AgentProcessId}\n", persisted, StringComparison.Ordinal );
|
|
Assert.Contains( "GIT_SSH_COMMAND=", persisted, StringComparison.Ordinal );
|
|
Assert.DoesNotContain( AskpassModeVariable, persisted, StringComparison.Ordinal );
|
|
Assert.DoesNotContain( PassphraseVariable, persisted, StringComparison.Ordinal );
|
|
Assert.DoesNotContain( SyntheticPassphrase, persisted, StringComparison.Ordinal );
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ArchInstallerCheckRejectsUnexpectedTopLevelFiles( )
|
|
{
|
|
using var fixture = CreateArchiveFixture( );
|
|
File.WriteAllText( Path.Combine( fixture.Path, "unexpected.txt" ), "unexpected\n" );
|
|
var archive = await CreateArchive( fixture.Path );
|
|
|
|
var error = await RunArchInstallerCheckExpectingFailure( fixture.Path, archive );
|
|
|
|
Assert.Contains( "unexpected", error.Message, StringComparison.OrdinalIgnoreCase );
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ArchInstallerCheckRejectsASymbolicLinkArchiveRoot( )
|
|
{
|
|
using var fixture = CreateArchiveFixture( );
|
|
using var externalPayload = CreateArchiveFixture( );
|
|
var archiveRoot = Path.Combine( fixture.Path, ArchiveRootName );
|
|
Directory.Delete( archiveRoot, recursive: true );
|
|
Directory.CreateSymbolicLink( archiveRoot, Path.Combine( externalPayload.Path, ArchiveRootName ) );
|
|
var archive = await CreateArchive( fixture.Path );
|
|
|
|
var error = await RunArchInstallerCheckExpectingFailure( fixture.Path, archive );
|
|
|
|
Assert.Contains( "symbolic link or special file", error.Message, StringComparison.Ordinal );
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData( "hard-link" )]
|
|
[InlineData( "fifo" )]
|
|
public async Task ArchInstallerCheckRejectsUnsupportedFileTypes( string kind )
|
|
{
|
|
using var fixture = CreateArchiveFixture( );
|
|
var usr = Path.Combine( fixture.Path, ArchiveRootName, "usr" );
|
|
if ( kind == "hard-link" )
|
|
{
|
|
var source = Path.Combine( usr, "bin/wayscriber" );
|
|
var link = Path.Combine( usr, "bin/wayscriber-copy" );
|
|
await RunProcess( fixture.Path, "ln", [source, link] );
|
|
}
|
|
else
|
|
{
|
|
var specialDirectory = Path.Combine( usr, "share/wayscriber" );
|
|
Directory.CreateDirectory( specialDirectory );
|
|
await RunProcess( fixture.Path, "mkfifo", [Path.Combine( specialDirectory, "release-pipe" )] );
|
|
}
|
|
var archive = await CreateArchive( fixture.Path );
|
|
|
|
var error = await RunArchInstallerCheckExpectingFailure( fixture.Path, archive );
|
|
|
|
var expected = kind == "hard-link" ? "hard-linked file" : "symbolic link or special file";
|
|
Assert.Contains( expected, error.Message, StringComparison.Ordinal );
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData( "ExecStart=/usr/bin/wayscriber --active\n" )]
|
|
[InlineData( "ExecStartPost=/usr/bin/env wayscriber --active\n" )]
|
|
[InlineData( "ExecStartPost=/usr/bin/env \\\n wayscriber --active\n" )]
|
|
[InlineData( "ExecStartPost=/usr/bin/env \\\n# ignored by systemd\n; ignored by systemd\n wayscriber --active\n" )]
|
|
public async Task ArchInstallerCheckRejectsAdditionalWayscriberServiceCommands( string command )
|
|
{
|
|
using var fixture = CreateArchiveFixture( );
|
|
var service = Path.Combine( fixture.Path, ArchiveRootName, "usr/lib/systemd/user/wayscriber.service" );
|
|
File.AppendAllText( service, command );
|
|
var archive = await CreateArchive( fixture.Path );
|
|
|
|
var error = await RunArchInstallerCheckExpectingFailure( fixture.Path, archive );
|
|
|
|
Assert.Contains( "service is incompatible", error.Message, StringComparison.Ordinal );
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData( RepositoryDeployKey )]
|
|
[InlineData( RepositoryDeployKey + "\n" )]
|
|
public async Task RepositoryDeployKeyAlwaysEndsWithOnePreservedNewline( string configuredKey )
|
|
{
|
|
var environment = new Dictionary<string, string?>( StringComparer.Ordinal )
|
|
{
|
|
[EnvironmentVariables.DeployHost] = "packages.example.invalid",
|
|
[EnvironmentVariables.DeployPath] = "/srv/wayscriber",
|
|
[EnvironmentVariables.DeployUser] = "deploy",
|
|
[EnvironmentVariables.PackageRepositorySshKey] = configuredKey,
|
|
[EnvironmentVariables.PackageRepositorySshKnownHosts] = "packages.example.invalid ssh-ed25519 host-key\n",
|
|
};
|
|
var runner = new RepositoryDeployRunner( );
|
|
var context = CreateContext( runner, name => environment.GetValueOrDefault( name ) );
|
|
var command = ReleaseAurCommands.Commands.Single( item =>
|
|
item.Area == CommandAreas.Release && item.Name == CommandNames.DeployPackageRepositories );
|
|
|
|
Assert.Equal( ExitCodes.Success, await command.Handler( context, [] ) );
|
|
|
|
Assert.Equal( RepositoryDeployKey + "\n", runner.DeployKey );
|
|
}
|
|
|
|
[Fact]
|
|
public void ManagedDesktopAssetBlocksReplaceAndRejectStaleEntries( )
|
|
{
|
|
var recipe = AssetsCommand.CreateRecipe( FindRepository( ) )["source"]!.AsObject( );
|
|
var marker = recipe["marker"]!.GetValue<string>( );
|
|
var end = recipe["end_marker"]!.GetValue<string>( );
|
|
var anchor = recipe["anchor"]!.GetValue<string>( );
|
|
var expectedLines = recipe["lines"]!.AsArray( ).Select( item => item!.GetValue<string>( ) ).ToArray( );
|
|
const string staleLine = " install -Dm644 packaging/icons/wayscriber-obsolete.png \"$pkgdir/usr/share/icons/hicolor/obsolete/wayscriber.png\"";
|
|
var input = anchor + "\n\n" + marker + "\n" + string.Join( '\n', expectedLines ) + "\n" + staleLine + "\n" + end;
|
|
|
|
Assert.False( ReleaseAurCommands.HasExactDesktopAssetBlock( input, recipe ) );
|
|
|
|
var result = ReleaseAurCommands.ApplyDesktopAssets( input, recipe );
|
|
|
|
Assert.True( ReleaseAurCommands.HasExactDesktopAssetBlock( result, recipe ) );
|
|
Assert.DoesNotContain( staleLine, result, StringComparison.Ordinal );
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData( PackageChannels.Source )]
|
|
[InlineData( PackageChannels.Binary )]
|
|
[InlineData( PackageChannels.Configurator )]
|
|
public void CompleteMarkerlessDesktopAssetsAreMigratedAndStaleEntriesAreRemoved( string channel )
|
|
{
|
|
var recipe = AssetsCommand.CreateRecipe( FindRepository( ) )[channel]!.AsObject( );
|
|
var anchor = recipe["anchor"]!.GetValue<string>( );
|
|
var expectedLines = recipe["lines"]!.AsArray( ).Select( item => item!.GetValue<string>( ) ).ToArray( );
|
|
var staleName = channel == PackageChannels.Configurator ? "retired-legacy-icon" : "retired-icon";
|
|
var staleLine = $" install -Dm644 packaging/icons/{staleName}.png \"$pkgdir/usr/share/icons/hicolor/obsolete/{staleName}.png\"";
|
|
var input = anchor + "\n\n" + string.Join( '\n', expectedLines ) + "\n" + staleLine;
|
|
|
|
Assert.False( ReleaseAurCommands.HasExactDesktopAssetBlock( input, recipe ) );
|
|
|
|
var result = ReleaseAurCommands.ApplyDesktopAssets( input, recipe );
|
|
|
|
Assert.True( ReleaseAurCommands.HasExactDesktopAssetBlock( result, recipe ) );
|
|
Assert.DoesNotContain( staleLine, result, StringComparison.Ordinal );
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData( PackageChannels.Source )]
|
|
[InlineData( PackageChannels.Binary )]
|
|
[InlineData( PackageChannels.Configurator )]
|
|
public void MarkerlessPreviousManifestSubsetsAreMigrated( string channel )
|
|
{
|
|
var recipe = AssetsCommand.CreateRecipe( FindRepository( ) )[channel]!.AsObject( );
|
|
var anchor = recipe["anchor"]!.GetValue<string>( );
|
|
var previousManifestLines = recipe["lines"]!.AsArray( ).Select( item => item!.GetValue<string>( ) ).SkipLast( 1 );
|
|
var input = anchor + "\n\n" + string.Join( '\n', previousManifestLines );
|
|
|
|
var result = ReleaseAurCommands.ApplyDesktopAssets( input, recipe );
|
|
|
|
Assert.True( ReleaseAurCommands.HasExactDesktopAssetBlock( result, recipe ) );
|
|
}
|
|
|
|
[Fact]
|
|
public void InstallDirectoryNormalizationPreservesFilesystemRoot( )
|
|
{
|
|
Assert.Equal( Path.GetPathRoot( Environment.CurrentDirectory ), NativeDesktopCommands.NormalizeBinDirectory( "/" ) );
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StandaloneGateUsesSdkResolutionAndRunsEveryCsharpBuild( )
|
|
{
|
|
if ( !OperatingSystem.IsLinux( ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
using var fixture = CreateStandaloneGateFixture( dotnetSdkAvailable: true );
|
|
var dotnetLog = Path.Combine( fixture.Path, "dotnet.log" );
|
|
|
|
var result = await RunStandaloneGate( fixture.Path, dotnetLog, new HashSet<int> { ExitCodes.Success } );
|
|
|
|
Assert.Equal( ExitCodes.Success, result.ExitCode );
|
|
var invocations = File.ReadAllText( dotnetLog );
|
|
Assert.Contains( "build tools/wayscriber.cs --disable-build-servers --verbosity quiet", invocations, StringComparison.Ordinal );
|
|
Assert.Contains( "build tools/install.cs --disable-build-servers --verbosity quiet", invocations, StringComparison.Ordinal );
|
|
Assert.Contains( "build tools/wayscriber.tests.cs --disable-build-servers --verbosity quiet", invocations, StringComparison.Ordinal );
|
|
Assert.Contains( "format style tools/wayscriber.cs --no-restore --verify-no-changes", invocations, StringComparison.Ordinal );
|
|
Assert.Contains( "format whitespace tools/install.cs --no-restore --verify-no-changes", invocations, StringComparison.Ordinal );
|
|
Assert.Contains( "format whitespace tools/wayscriber.tests.cs --no-restore --verify-no-changes", invocations, StringComparison.Ordinal );
|
|
Assert.Contains( "run tools/wayscriber.tests.cs --no-build --verbosity quiet", invocations, StringComparison.Ordinal );
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StandaloneGateSkipsOnlyCsharpChecksWhenPinnedSdkIsUnavailable( )
|
|
{
|
|
if ( !OperatingSystem.IsLinux( ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
using var fixture = CreateStandaloneGateFixture( dotnetSdkAvailable: false );
|
|
var dotnetLog = Path.Combine( fixture.Path, "dotnet.log" );
|
|
|
|
var result = await RunStandaloneGate( fixture.Path, dotnetLog, new HashSet<int> { ExitCodes.Success } );
|
|
|
|
Assert.Equal( ExitCodes.Success, result.ExitCode );
|
|
Assert.Contains( "SDK selected by global.json is unavailable", result.StandardOutput, StringComparison.Ordinal );
|
|
Assert.False( File.Exists( dotnetLog ) );
|
|
}
|
|
|
|
private static ToolContext CreateContext( IProcessRunner runner, Func<string, string?>? environment = null ) =>
|
|
new( FindRepository( ), TextWriter.Null, TextWriter.Null, runner, CancellationToken.None, environment );
|
|
|
|
private static TemporaryDirectory CreateArchiveFixture( )
|
|
{
|
|
var fixture = new TemporaryDirectory( "wayscriber-archive-validation-test" );
|
|
var usr = Path.Combine( fixture.Path, ArchiveRootName, "usr" );
|
|
Directory.CreateDirectory( Path.Combine( usr, "bin" ) );
|
|
Directory.CreateDirectory( Path.Combine( usr, "lib/systemd/user" ) );
|
|
File.WriteAllText( Path.Combine( usr, "bin/wayscriber" ), "binary\n" );
|
|
File.WriteAllText( Path.Combine( usr, "lib/systemd/user/wayscriber.service" ), "ExecStart=/usr/bin/wayscriber --daemon\n" );
|
|
File.WriteAllText( Path.Combine( fixture.Path, "installer.sh" ), """
|
|
# ARCH_INSTALL_MANIFEST_BEGIN
|
|
release_manifest() {
|
|
printf '%s\n' \
|
|
'0644 bin/wayscriber' \
|
|
'0644 lib/systemd/user/wayscriber.service'
|
|
}
|
|
# ARCH_INSTALL_MANIFEST_END
|
|
""" );
|
|
return fixture;
|
|
}
|
|
|
|
private static async Task<string> CreateArchive( string directory )
|
|
{
|
|
var archive = Path.Combine( directory, "wayscriber.tar.gz" );
|
|
var entries = Directory.EnumerateFileSystemEntries( directory )
|
|
.Select( Path.GetFileName )
|
|
.Where( name => name is not "installer.sh" and not "wayscriber.tar.gz" )
|
|
.Select( name => name! )
|
|
.ToArray( );
|
|
await RunProcess( directory, "tar", ["-czf", archive, "--", .. entries] );
|
|
return archive;
|
|
}
|
|
|
|
private static async Task<ToolException> RunArchInstallerCheckExpectingFailure( string directory, string archive )
|
|
{
|
|
var context = CreateContext( new ProcessRunner( TextWriter.Null, TextWriter.Null ) );
|
|
var command = PackagingCommands.Commands.Single( item => item.Area == "package" && item.Name == "check-arch-installer" );
|
|
return await Assert.ThrowsAsync<ToolException>( ( ) => command.Handler(
|
|
context,
|
|
["--installer", Path.Combine( directory, "installer.sh" ), "--archive", archive] ) );
|
|
}
|
|
|
|
private static async Task RunProcess( string directory, string fileName, IReadOnlyList<string> arguments )
|
|
{
|
|
var runner = new ProcessRunner( TextWriter.Null, TextWriter.Null );
|
|
await runner.RunAsync( new ProcessRequest( fileName, arguments, directory, CaptureOutput: true, Trace: false ), CancellationToken.None );
|
|
}
|
|
|
|
[SupportedOSPlatform( "linux" )]
|
|
private static TemporaryDirectory CreateStandaloneGateFixture( bool dotnetSdkAvailable )
|
|
{
|
|
var fixture = new TemporaryDirectory( "wayscriber-standalone-gate-test" );
|
|
var tools = Path.Combine( fixture.Path, "tools" );
|
|
var fakeBin = Path.Combine( fixture.Path, "fake-bin" );
|
|
Directory.CreateDirectory( tools );
|
|
Directory.CreateDirectory( fakeBin );
|
|
File.Copy( Path.Combine( FindRepository( ), "tools/lint-and-test.sh" ), Path.Combine( tools, "lint-and-test.sh" ) );
|
|
File.Copy( Path.Combine( FindRepository( ), "global.json" ), Path.Combine( fixture.Path, "global.json" ) );
|
|
File.CreateSymbolicLink( Path.Combine( fakeBin, "bash" ), "/usr/bin/true" );
|
|
WriteExecutable( Path.Combine( fakeBin, "cargo" ), "#!/usr/bin/bash\nprintf 'test result: ok. 1 passed; 0 failed;\\n'\n" );
|
|
WriteExecutable( Path.Combine( fakeBin, "dotnet" ), $$"""
|
|
#!/usr/bin/bash
|
|
if [[ "$1" == "--version" ]]; then
|
|
{{(dotnetSdkAvailable ? $"printf '%s\\n' '{TestDotnetSdk}'" : "exit 1")}}
|
|
else
|
|
printf '%s\n' "$*" >> "${WAYSCRIBER_DOTNET_LOG:?}"
|
|
fi
|
|
""" );
|
|
foreach ( var check in new[] { "check-nixpkgs-recipe.py", "check-rust-source-coverage.py", "check-process-sites.py",
|
|
"check-config-writers.py", "check-shared-dependencies.py" } )
|
|
{
|
|
WriteExecutable( Path.Combine( tools, check ), "#!/usr/bin/true\n" );
|
|
}
|
|
|
|
return fixture;
|
|
}
|
|
|
|
[SupportedOSPlatform( "linux" )]
|
|
private static void WriteExecutable( string path, string content )
|
|
{
|
|
File.WriteAllText( path, content );
|
|
File.SetUnixFileMode( path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute );
|
|
}
|
|
|
|
private static Task<ProcessResult> RunStandaloneGate( string repository, string dotnetLog, IReadOnlySet<int> allowedExitCodes )
|
|
{
|
|
var environment = new Dictionary<string, string?>
|
|
{
|
|
[EnvironmentVariables.Path] = Path.Combine( repository, "fake-bin" ) + Path.PathSeparator +
|
|
Environment.GetEnvironmentVariable( EnvironmentVariables.Path ),
|
|
[DotnetLogVariable] = dotnetLog,
|
|
};
|
|
var request = new ProcessRequest( "/usr/bin/bash", [Path.Combine( repository, "tools/lint-and-test.sh" )], repository,
|
|
environment, CaptureOutput: true, Trace: false, AllowedExitCodes: allowedExitCodes );
|
|
return new ProcessRunner( TextWriter.Null, TextWriter.Null ).RunAsync( request, CancellationToken.None );
|
|
}
|
|
|
|
private static string FindRepository( )
|
|
{
|
|
var directory = AppContext.GetData( "EntryPointFileDirectoryPath" ) as string ?? Environment.CurrentDirectory;
|
|
return Path.GetFullPath( Path.Combine( directory, ".." ) );
|
|
}
|
|
|
|
private sealed class AskpassAurRunner : IProcessRunner
|
|
{
|
|
public const string AgentProcessId = "24680";
|
|
public const string AgentSocket = "/tmp/wayscriber-agent.sock";
|
|
|
|
public List<ProcessRequest> Requests { get; } = [];
|
|
|
|
public Task<ProcessResult> RunAsync( ProcessRequest request, CancellationToken cancellationToken )
|
|
{
|
|
Requests.Add( request );
|
|
if ( request.FileName == "ssh-agent" )
|
|
{
|
|
return Task.FromResult( new ProcessResult( ExitCodes.Success,
|
|
$"SSH_AUTH_SOCK={AgentSocket}; export SSH_AUTH_SOCK;\nSSH_AGENT_PID={AgentProcessId}; export SSH_AGENT_PID;\n", string.Empty ) );
|
|
}
|
|
if ( request.FileName == "setsid" || request.FileName == "git" )
|
|
{
|
|
return Task.FromResult( new ProcessResult( ExitCodes.Success, string.Empty, string.Empty ) );
|
|
}
|
|
|
|
throw new Xunit.Sdk.XunitException( $"Unexpected process: {request.FileName} {string.Join( ' ', request.Arguments )}" );
|
|
}
|
|
}
|
|
|
|
private sealed class RepositoryDeployRunner : IProcessRunner
|
|
{
|
|
public string? DeployKey { get; private set; }
|
|
|
|
public Task<ProcessResult> RunAsync( ProcessRequest request, CancellationToken cancellationToken )
|
|
{
|
|
if ( request.FileName == "ssh" )
|
|
{
|
|
var keyArgument = Array.IndexOf( request.Arguments.ToArray( ), "-i" );
|
|
DeployKey = File.ReadAllText( request.Arguments[keyArgument + 1] );
|
|
}
|
|
else if ( request.FileName != "rsync" )
|
|
{
|
|
throw new Xunit.Sdk.XunitException( $"Unexpected process: {request.FileName} {string.Join( ' ', request.Arguments )}" );
|
|
}
|
|
|
|
return Task.FromResult( new ProcessResult( ExitCodes.Success, string.Empty, string.Empty ) );
|
|
}
|
|
}
|
|
}
|