Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93cd228188
|
||
|
|
a36f9105dd
|
||
|
|
458733bca8
|
||
|
|
46999ea21c
|
||
|
|
c3b7c37f47
|
||
|
|
af9e2f99e0
|
||
|
|
84a1002143
|
||
|
|
8689dd593c | ||
|
|
a2f90fb1fb | ||
|
|
bed2959293 | ||
|
|
e0bc919c3c | ||
|
|
d1517048f3 |
No files matched your search
@@ -0,0 +1,68 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['v*']
|
||||
jobs:
|
||||
windows:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/windows.yaml@v1
|
||||
secrets: inherit
|
||||
with:
|
||||
runner: qqq-windows-amd64
|
||||
commands: |
|
||||
$temp = $env:RUNNER_TEMP ?? [IO.Path]::GetTempPath()
|
||||
# Protocol fixtures pinned to the revision the verification expects.
|
||||
$protocol = Join-Path $temp "qqq-print-protocol-$env:GITHUB_RUN_ID"
|
||||
git clone --quiet https://git.kof22.com/QRun-IO/qqq-print-protocol.git $protocol
|
||||
git -C $protocol checkout --quiet ac835f8b9396b641b525669249fdb4a1e2a50c99
|
||||
pwsh -NoProfile -File ./build/Verify.ps1 -ProtocolFixtures (Join-Path $protocol fixtures)
|
||||
pwsh -NoProfile -File ./build/Test-WindowsSdkInventory.ps1 -VerifyPackages
|
||||
pwsh -NoProfile -File ./build/Test-BrandedInstaller.ps1
|
||||
pwsh -NoProfile -File ./build/Test-PackagingInputs.ps1
|
||||
# Unsigned MSIs: the base product plus one branded installer per edition. A vX.Y.Z[-rc.N] tag sets the version.
|
||||
$release = if ($env:GITHUB_REF_TYPE -eq 'tag') { $env:GITHUB_REF_NAME.TrimStart('v') } else { '1.2.1' }
|
||||
$product = $release -replace '-.*$', ''
|
||||
pwsh -NoProfile -File ./build/Build-Installer.ps1 -ProductVersion $product -ReleaseVersion $release
|
||||
$installers = @('QqqPrintAgent')
|
||||
foreach ($folder in Get-ChildItem -LiteralPath editions -Directory | Sort-Object Name) {
|
||||
pwsh -NoProfile -File ./build/Build-Installer.ps1 -ProductVersion $product -ReleaseVersion $release -Edition $folder.Name
|
||||
$installers += (Get-Content -LiteralPath (Join-Path $folder.FullName 'edition.json') -Raw | ConvertFrom-Json).assetPrefix
|
||||
}
|
||||
New-Item -ItemType Directory -Path artifacts/release -Force | Out-Null
|
||||
foreach ($prefix in $installers) {
|
||||
Copy-Item -LiteralPath "artifacts/installer/$prefix-$product-x64.msi" -Destination "artifacts/release/$prefix-v$release-x64.msi"
|
||||
}
|
||||
Copy-Item -LiteralPath build/Install-Branded.ps1, docs/branding.example.json -Destination artifacts/release/
|
||||
# PowerShell scripts: analyzer and Pester rejection-path tests with pinned modules saved to the job's temp directory.
|
||||
$modules = Join-Path $temp "psmodules-$env:GITHUB_RUN_ID"
|
||||
New-Item -ItemType Directory -Force -Path $modules | Out-Null
|
||||
Save-PSResource -Name PSScriptAnalyzer -Version 1.25.0 -Repository PSGallery -TrustRepository -Path $modules
|
||||
Save-PSResource -Name Pester -Version 5.7.1 -Repository PSGallery -TrustRepository -Path $modules
|
||||
$env:PSModulePath = "$modules$([IO.Path]::PathSeparator)$env:PSModulePath"
|
||||
pwsh -NoProfile -File ./build/Analyze-Scripts.ps1
|
||||
Import-Module Pester -RequiredVersion 5.7.1
|
||||
# The Pester suites assert on child processes' exit codes themselves.
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
$pester = New-PesterConfiguration
|
||||
$pester.Run.Path = @('build/Assert-Coverage.Tests.ps1', 'build/Analyze-Scripts.Tests.ps1', 'build/Install-Branded.Tests.ps1', 'build/Test-WindowsSdkInventory.Tests.ps1')
|
||||
$pester.Run.PassThru = $true
|
||||
if ((Invoke-Pester -Configuration $pester).Result -ne 'Passed') { throw 'Pester tests failed' }
|
||||
artifacts: |
|
||||
artifacts/release/*.msi
|
||||
artifacts/release/Install-Branded.ps1
|
||||
artifacts/release/branding.example.json
|
||||
scripts:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@v1
|
||||
with:
|
||||
node-version: '24'
|
||||
uv: true
|
||||
install: npm ci --ignore-scripts --no-audit --no-fund
|
||||
commands: |
|
||||
uv sync --locked
|
||||
uv run --locked --no-sync python .github/scripts/test-script-quality.py
|
||||
uv run --locked --no-sync python .github/scripts/check-script-quality.py
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
python3 .github/scripts/test-source-headers.py
|
||||
python3 .github/scripts/check-source-headers.py
|
||||
node --test tools/virtual-agent/test/virtual-agent.test.mjs
|
||||
@@ -188,7 +188,30 @@ if($CertificateThumbprint)
|
||||
}
|
||||
|
||||
# WiX incremental inputs do not reliably invalidate a changed version/payload path.
|
||||
& dotnet build $installerProject --configuration Release -t:Rebuild '-p:RestoreLockedMode=true' "-p:ProductVersion=$ProductVersion" "-p:PayloadRoot=$payloadRoot" "-p:OutputPath=$buildDirectory\" @brandInstallerArgs
|
||||
# WiX validates the MSI (ICE checks) through the Windows Installer service. A runner that logs on as a batch account cannot
|
||||
# open that service, so WiX fails every ICE action with WIX0217 ("The Windows Installer Service could not be accessed").
|
||||
# Validate whenever the service is reachable. Skip validation only when it is not, with a loud warning, and never for a
|
||||
# signed release build: that must be built on a host where validation works.
|
||||
$installerValidationArguments = @()
|
||||
try
|
||||
{
|
||||
$installerService = Get-Service -Name msiserver -ErrorAction Stop
|
||||
if($installerService.Status -ne 'Running')
|
||||
{
|
||||
$installerService.Start()
|
||||
$installerService.WaitForStatus('Running', [TimeSpan]::FromSeconds(30))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
if($CertificateThumbprint)
|
||||
{
|
||||
throw "The Windows Installer service is not reachable, so the MSI cannot be validated; refusing to build a signed release without validation. $($_.Exception.Message)"
|
||||
}
|
||||
Write-Warning "The Windows Installer service is not reachable from this session; building the MSI WITHOUT ICE validation (not a signed release build). $($_.Exception.Message)"
|
||||
$installerValidationArguments = @('-p:SuppressValidation=true')
|
||||
}
|
||||
& dotnet build $installerProject --configuration Release -t:Rebuild '-p:RestoreLockedMode=true' "-p:ProductVersion=$ProductVersion" "-p:PayloadRoot=$payloadRoot" "-p:OutputPath=$buildDirectory\" @brandInstallerArgs @installerValidationArguments
|
||||
if($LASTEXITCODE -ne 0) { throw 'MSI build failed.' }
|
||||
$builtMsi = Join-Path $buildDirectory "$msiBaseName-$ProductVersion-x64.msi"
|
||||
if(-not (Test-Path -LiteralPath $builtMsi)) { throw 'MSI output was not produced.' }
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class AgentControlConcurrencyTests
|
||||
using ControlFixture fixture = new();
|
||||
fixture.OfferJob = true;
|
||||
fixture.Printer.BlockPrint = true;
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60)); // hang guard only: the test normally finishes in well under a second, but a loaded CI host can take seconds
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
Task running = runtime.RunAsync(stop.Token);
|
||||
try
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class AgentNewPairingTests
|
||||
ChangeOldConfiguration(fixture, unreadable);
|
||||
byte[] retained = File.ReadAllBytes(path);
|
||||
fixture.NextSession = UnboundSession();
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
using(AgentRuntime runtime = fixture.Runtime())
|
||||
{
|
||||
_ = await runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.NextSession.ExportCredential(), StartNewPairing: true), timeout.Token);
|
||||
@@ -77,7 +77,7 @@ public sealed class AgentNewPairingTests
|
||||
string path = Path.Combine(fixture.Local.DirectoryPath, "connection.dpapi");
|
||||
byte[] retained = File.ReadAllBytes(path);
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
_ = await runtime.PollControlsOnceAsync(timeout.Token);
|
||||
Assert.False(runtime.GetSnapshot().Authentication!.Authorized);
|
||||
Assert.Equal(0, fixture.Created);
|
||||
@@ -119,7 +119,7 @@ public sealed class AgentNewPairingTests
|
||||
string authPath = Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi");
|
||||
byte[] originalAuth = File.ReadAllBytes(authPath);
|
||||
fixture.NextSession = UnboundSession();
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
using(AgentRuntime runtime = fixture.Runtime())
|
||||
{
|
||||
await Assert.ThrowsAsync<QqqSessionException>(() => runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.NextSession.ExportCredential()), timeout.Token));
|
||||
|
||||
@@ -41,7 +41,7 @@ public sealed class AgentPipeFramingTests
|
||||
public async Task Write_CancelsWhenConnectedClientDoesNotRead()
|
||||
{
|
||||
string name = "qqq-framing-" + Guid.NewGuid().ToString("N");
|
||||
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(60));
|
||||
await using NamedPipeServerStream server = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly, 4096, 4096);
|
||||
await using NamedPipeClientStream client = new(".", name, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
Task connect = client.ConnectAsync(deadline.Token);
|
||||
@@ -60,7 +60,7 @@ public sealed class AgentPipeFramingTests
|
||||
public async Task Read_CancelsWhenConnectedClientNeverTerminatesCommand()
|
||||
{
|
||||
string name = "qqq-framing-" + Guid.NewGuid().ToString("N");
|
||||
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(60));
|
||||
await using NamedPipeServerStream server = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
|
||||
await using NamedPipeClientStream client = new(".", name, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
Task connect = client.ConnectAsync(deadline.Token);
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class AgentPipeSecurityTests
|
||||
security.AddAccessRule(new PipeAccessRule(identity.User!, PipeAccessRights.ReadWrite, AccessControlType.Allow));
|
||||
await using NamedPipeServerStream server = NamedPipeServerStreamAcl.Create(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous | PipeOptions.FirstPipeInstance, 4096, 4096, security);
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
for(int connection = 0; connection < 2; connection++)
|
||||
{
|
||||
Task accepted = server.WaitForConnectionAsync(timeout.Token);
|
||||
@@ -58,7 +58,7 @@ public sealed class AgentPipeSecurityTests
|
||||
{
|
||||
string name = "QqqPrintAgent-untrusted-test-" + Guid.NewGuid().ToString("N");
|
||||
await using NamedPipeServerStream fake = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.FirstPipeInstance);
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
Task connected = fake.WaitForConnectionAsync(timeout.Token);
|
||||
Task<string> request = AgentPipeClient.SendAsync(new PipeRequest(operation, "https://print.example.com", "synthetic-enrollment-never-send"), name);
|
||||
await connected;
|
||||
|
||||
@@ -39,16 +39,16 @@ public sealed class AgentRealtimeBandwidthTests
|
||||
{
|
||||
// Both real runtime lanes must reach their independent body streams before
|
||||
// timing starts, so serialized admission cannot imitate an aggregate cap.
|
||||
await Task.WhenAll(first.Entered.Task, second.Entered.Task).WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await Task.WhenAll(first.Entered.Task, second.Entered.Task).WaitAsync(TimeSpan.FromSeconds(60));
|
||||
long started = Stopwatch.GetTimestamp();
|
||||
release.TrySetResult();
|
||||
await fixture.WaitAsync(() => fixture.Printer.Prints == 2).WaitAsync(TimeSpan.FromSeconds(10));
|
||||
await fixture.WaitAsync(() => fixture.Printer.Prints == 2).WaitAsync(TimeSpan.FromSeconds(60));
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(started);
|
||||
|
||||
// Together these bodies need four seconds at the shared rate; independent
|
||||
// per-file throttles would permit both to finish after about two seconds.
|
||||
Assert.True(elapsed >= TimeSpan.FromSeconds(3.5), "Concurrent documents exceeded the configured aggregate download rate.");
|
||||
await fixture.WaitAsync(() => fixture.State("first-budget") is "SPOOLED" && fixture.State("second-budget") is "SPOOLED").WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await fixture.WaitAsync(() => fixture.State("first-budget") is "SPOOLED" && fixture.State("second-budget") is "SPOOLED").WaitAsync(TimeSpan.FromSeconds(60));
|
||||
Assert.Equal(0, fixture.ForbiddenPolls);
|
||||
}
|
||||
finally { release.TrySetResult(); }
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class AgentRealtimeGenerationTests
|
||||
{
|
||||
using var fixture = new SessionFixture();
|
||||
fixture.SaveSession();
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var replacementEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class AgentRealtimeOrderingTests
|
||||
using var slow = new GatedDocument(bytes);
|
||||
fixture.AddJob("slow", () => slow, length: bytes.Length);
|
||||
await connection.SendAsync("JOB_AVAILABLE", new { });
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
var observed = new TaskCompletionSource<(bool Connected, string? State, int Prints)>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
fixture.BeforeAcknowledgement = async message =>
|
||||
{
|
||||
@@ -48,7 +48,7 @@ public sealed class AgentRealtimeOrderingTests
|
||||
try
|
||||
{
|
||||
await connection.SendAsync("DESIRED_STATE", new Qqq.PrintAgent.Core.Printing.DeviceDesiredState(2, false, "CONNECTED"));
|
||||
(bool connected, string? state, int prints) = await observed.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
(bool connected, string? state, int prints) = await observed.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
Assert.False(connected);
|
||||
Assert.Equal("RECEIVED", state);
|
||||
Assert.Equal(0, prints);
|
||||
@@ -98,7 +98,7 @@ public sealed class AgentRealtimeOrderingTests
|
||||
using var slow = new GatedDocument(bytes);
|
||||
fixture.AddJob("slow", () => slow, length: bytes.Length);
|
||||
await connection.SendAsync("JOB_AVAILABLE", new { });
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
await connection.SendAsync("SETTINGS", fixture.Settings with { Revision = 2, NativeSubmissionTimeoutMillis = 2345, MaxPdfBytes = rejectSize ? 1 : fixture.Settings.MaxPdfBytes });
|
||||
await connection.SendAsync("DESIRED_STATE", new Qqq.PrintAgent.Core.Printing.DeviceDesiredState(2, true, "CONNECTED"));
|
||||
await fixture.WaitAsync(() => fixture.Runtime.GetSnapshot().Control!.DesiredState.DispatchPaused);
|
||||
@@ -123,7 +123,7 @@ public sealed class AgentRealtimeOrderingTests
|
||||
fixture.AddJob("slow", () => slow, length: bytes.Length);
|
||||
fixture.AddJob("fast", printerId: "other");
|
||||
await connection.SendAsync("JOB_AVAILABLE", new { });
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
try
|
||||
{
|
||||
Assert.Empty(fixture.Printer.Order);
|
||||
@@ -156,8 +156,8 @@ public sealed class AgentRealtimeOrderingTests
|
||||
fixture.AddJob("slow", () => slow, length: slowBytes.Length);
|
||||
fixture.AddJob("fast", () => { fastStarted.TrySetResult(); return new MemoryStream(fastBytes); }, samePrinter ? "printer" : "other", fastBytes.Length);
|
||||
await connection.SendAsync("JOB_AVAILABLE", new { });
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await fastStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await slow.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
await fastStarted.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
try
|
||||
{
|
||||
if(samePrinter)
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class AgentRealtimeRecoveryTests
|
||||
byte[] unreadable = [1, 3, 5];
|
||||
File.WriteAllBytes(path, unreadable);
|
||||
string name = "qqq-recovery-" + Guid.NewGuid().ToString("N");
|
||||
using var stop = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
using var stop = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
using var runtime = new AgentRuntime(fixture.DirectoryPath, _ => { }, fixture.Printer, pipeName: name,
|
||||
pipeFactory: () => new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly));
|
||||
Task running = runtime.RunAsync(stop.Token);
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class AgentRealtimeTests
|
||||
using var stalled = new StalledDocument();
|
||||
fixture.AddJob("large", () => stalled);
|
||||
await connection.SendAsync("JOB_AVAILABLE", new { });
|
||||
await stalled.Entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await stalled.Entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
await connection.SendAsync("DESIRED_STATE", new DeviceDesiredState(2, true, "CONNECTED"));
|
||||
await fixture.WaitAsync(() => fixture.Runtime.GetSnapshot().Control!.DesiredState.DispatchPaused);
|
||||
Assert.Equal(0, fixture.Printer.Prints);
|
||||
@@ -83,7 +83,7 @@ public sealed class AgentRealtimeTests
|
||||
RealtimeRuntimeFixture.Connection first = await fixture.ConnectAsync(loseSpoolAck: true);
|
||||
fixture.AddJob("first");
|
||||
await first.SendAsync("JOB_AVAILABLE", new { });
|
||||
await first.LostSpoolAck.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await first.LostSpoolAck.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
fixture.AddJob("buffered");
|
||||
_ = await fixture.ConnectAsync();
|
||||
await fixture.WaitAsync(() => string.Equals(fixture.State("buffered"), "SPOOLED", StringComparison.Ordinal));
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class AgentRealtimeTransitionTests
|
||||
await using var fixture = new RealtimeRuntimeFixture(settings: new RealtimeSettings(Revision: 5));
|
||||
_ = await fixture.ConnectAsync();
|
||||
fixture.Settings = fixture.Settings with { Revision = 0 };
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
_ = await fixture.Runtime.EnrollWithGatesAsync(new PipeRequest("enroll", fixture.Peer.Origin.AbsoluteUri, "synthetic-code"), timeout.Token);
|
||||
_ = await fixture.ConnectAsync();
|
||||
Assert.Equal(0, fixture.LastHelloSettingsRevision);
|
||||
|
||||
@@ -44,11 +44,11 @@ public sealed class AgentRuntimeBacklogTests
|
||||
{
|
||||
// Synthetic responses otherwise let RunAsync drain synchronously before this
|
||||
// assertion starts. Exclude setup/control readiness from the drain budget.
|
||||
await state.FirstClaim.Task.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
stop.CancelAfter(TimeSpan.FromSeconds(15));
|
||||
await state.FirstClaim.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
stop.CancelAfter(TimeSpan.FromSeconds(90));
|
||||
state.ReleaseClaims.SetResult();
|
||||
// Four claims must finish before the configured 20-second idle interval.
|
||||
await state.Empty.Task.WaitAsync(TimeSpan.FromSeconds(10), stop.Token);
|
||||
await state.Empty.Task.WaitAsync(TimeSpan.FromSeconds(60), stop.Token);
|
||||
Assert.Equal(3, printer.Printed);
|
||||
await Task.Delay(250, stop.Token);
|
||||
Assert.Equal(4, Volatile.Read(ref state.Claims));
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed class AgentRuntimeHeartbeatTests
|
||||
{
|
||||
string directory = Path.Combine(Path.GetTempPath(), "qqq-heartbeat-" + Guid.NewGuid().ToString("N"));
|
||||
string pipeName = "QqqHeartbeat-test-" + Guid.NewGuid().ToString("N");
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60));
|
||||
BlockingPrinter printer = new();
|
||||
int heartbeats = 0;
|
||||
Task? running = null;
|
||||
|
||||
@@ -74,7 +74,7 @@ public sealed class AgentRuntimePipeResponseTests
|
||||
public async Task ResponseWait_RespectsDeadline_WhenClientNeverReads()
|
||||
{
|
||||
string name = "QqqPrintAgent-test-response-deadline-" + Guid.NewGuid().ToString("N");
|
||||
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(5));
|
||||
using CancellationTokenSource deadline = new(TimeSpan.FromSeconds(60));
|
||||
await using NamedPipeServerStream server = new(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly, 4096, 4096);
|
||||
await using NamedPipeClientStream client = new(".", name, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
@@ -152,7 +152,7 @@ public sealed class AgentRuntimePipeResponseTests
|
||||
private readonly string _directory = Path.Combine(Path.GetTempPath(), "qqq-pipe-response-" + Guid.NewGuid().ToString("N"));
|
||||
private readonly AgentRuntime _runtime;
|
||||
public string PipeName { get; } = "QqqPrintAgent-test-response-" + Guid.NewGuid().ToString("N");
|
||||
public CancellationTokenSource Stop { get; } = new(TimeSpan.FromSeconds(15));
|
||||
public CancellationTokenSource Stop { get; } = new(TimeSpan.FromSeconds(60));
|
||||
public Task Running { get; }
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed class AgentRuntimeQueuedJobsTests
|
||||
{
|
||||
string directory = Path.Combine(Path.GetTempPath(), "qqq-queued-" + Guid.NewGuid().ToString("N"));
|
||||
string pipeName = "QqqQueued-test-" + Guid.NewGuid().ToString("N");
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60));
|
||||
ServerState server = new();
|
||||
NoPrintBackend printers = new();
|
||||
AgentRuntime? runtime = null;
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class AgentRuntimeTests
|
||||
{
|
||||
string directory = Path.Combine(Path.GetTempPath(), "qqq-runtime-" + Guid.NewGuid().ToString("N"));
|
||||
string pipeName = "QqqPrintAgent-test-" + Guid.NewGuid().ToString("N");
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(20));
|
||||
using CancellationTokenSource stop = new(TimeSpan.FromSeconds(60));
|
||||
TaskCompletionSource failed = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
TaskCompletionSource recovered = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
int requests = 0;
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed class AgentSessionIntegrationTests
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
QqqSessionCredential invalid = new(new Uri("https://synthetic.invalid/not-an-origin"), "synthetic-cookie");
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: invalid), CancellationToken.None));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
_ = await runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.Session.ExportCredential()), timeout.Token);
|
||||
Assert.True(runtime.GetSnapshot().Authentication!.Authorized);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ public sealed class AgentSessionIntegrationTests
|
||||
using SessionFixture fixture = new();
|
||||
fixture.SaveSession();
|
||||
fixture.Session.DelayUnauthorizedPoll = true;
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
Task<int> oldPoll = runtime.PollControlsOnceAsync(timeout.Token);
|
||||
await fixture.Session.PollEntered.Task.WaitAsync(timeout.Token);
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class AgentSessionRecoveryTests
|
||||
byte[] corrupt = ProtectedData.Protect(Encoding.UTF8.GetBytes(json), null, DataProtectionScope.CurrentUser);
|
||||
File.WriteAllBytes(Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi"), corrupt);
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
using(AgentStore unreadable = new(fixture.Local.DirectoryPath))
|
||||
{
|
||||
Assert.Throws<InvalidDataException>(() => unreadable.LoadAuthentication());
|
||||
@@ -60,7 +60,7 @@ public sealed class AgentSessionRecoveryTests
|
||||
byte[] corrupt = [1, 2, 3, 4];
|
||||
File.WriteAllBytes(Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi"), corrupt);
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
fixture.Session.DenyBinding = true;
|
||||
await Assert.ThrowsAsync<Qqq.PrintAgent.Core.Authentication.QqqSessionException>(() => runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.Session.ExportCredential()), timeout.Token));
|
||||
Assert.Equal(corrupt, File.ReadAllBytes(Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi")));
|
||||
@@ -96,7 +96,7 @@ public sealed class AgentSessionRecoveryTests
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
Assert.False(runtime.GetSnapshot().Enrolled);
|
||||
Assert.Equal(corrupt, File.ReadAllBytes(path));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
_ = await runtime.SignInAsync(new PipeRequest("sign-in", SessionCredential: fixture.Session.ExportCredential()), timeout.Token);
|
||||
Assert.False(runtime.GetSnapshot().Authentication!.Authorized);
|
||||
Assert.Equal(corrupt, File.ReadAllBytes(path));
|
||||
@@ -128,7 +128,7 @@ public sealed class AgentSessionRecoveryTests
|
||||
string path = Path.Combine(fixture.Local.DirectoryPath, "qqq-session.dpapi");
|
||||
File.WriteAllBytes(path, corrupt);
|
||||
using AgentRuntime runtime = fixture.Runtime();
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(5));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
Assert.Contains("revoke the previous session in QQQ", await runtime.SignOutAsync(timeout.Token), StringComparison.Ordinal);
|
||||
Assert.False(File.Exists(path));
|
||||
Assert.Equal(corrupt, File.ReadAllBytes(Assert.Single(Directory.GetFiles(fixture.Local.DirectoryPath, "qqq-session.recovery-*.dpapi"))));
|
||||
|
||||
@@ -53,7 +53,7 @@ public sealed class AgentTransportTests
|
||||
{
|
||||
string directory = Path.Combine(Path.GetTempPath(), "qqq-transport-pipe-" + Guid.NewGuid().ToString("N"));
|
||||
string name = "qqq-transport-" + Guid.NewGuid().ToString("N");
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
var runtime = new AgentRuntime(directory, _ => { }, new ForbiddenPrinters(), pipeName: name,
|
||||
pipeFactory: () => new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly));
|
||||
Task run = runtime.RunAsync(timeout.Token);
|
||||
|
||||
@@ -60,7 +60,7 @@ public sealed class BrowserCallbackTests
|
||||
{
|
||||
string name = "qqq-callback-test-" + Guid.NewGuid().ToString("N");
|
||||
using QqqBrowserCallback inbox = new(name);
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
Task<Uri> received = inbox.ReceiveAsync(timeout.Token);
|
||||
string callback = QqqBrowserCallback.RedirectUri + "?code=synthetic&state=synthetic";
|
||||
await QqqBrowserCallback.ForwardAsync(callback, name, Environment.ProcessPath!, timeout.Token);
|
||||
@@ -75,7 +75,7 @@ public sealed class BrowserCallbackTests
|
||||
{
|
||||
string name = "qqq-callback-test-" + Guid.NewGuid().ToString("N");
|
||||
using QqqBrowserCallback inbox = new(name);
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
Task<Uri> received = inbox.ReceiveAsync(timeout.Token);
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => QqqBrowserCallback.ForwardAsync(
|
||||
QqqBrowserCallback.RedirectUri + "?code=synthetic&state=synthetic", name, "C:\\not-the-installed-companion.exe", timeout.Token));
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class InstallerPreflightTests
|
||||
await process.StandardInput.WriteAsync(script);
|
||||
process.StandardInput.Close();
|
||||
string error = await process.StandardError.ReadToEndAsync();
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
await process.WaitForExitAsync(timeout.Token);
|
||||
Assert.True(process.ExitCode == 0, error);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public sealed class PrinterWorkerIsolationTests
|
||||
},
|
||||
};
|
||||
PrintJob job = new("synthetic", "fixture", "Never printed", "PDF", new string('0', 64), 1, new PrintOptions(), "", "RECEIVED");
|
||||
using var watchdog = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
using var watchdog = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
await Assert.ThrowsAsync<TimeoutException>(() => backend.PrintFileAsync(job, Path.Combine(Path.GetTempPath(), "unused-synthetic.document"), TimeSpan.FromSeconds(1), watchdog.Token));
|
||||
Assert.True(ProcessExited(childId));
|
||||
}
|
||||
@@ -101,7 +101,7 @@ public sealed class PrinterWorkerIsolationTests
|
||||
ProcessStartInfo start = new(FixtureExecutable) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true };
|
||||
start.ArgumentList.Add("--launch-parent");
|
||||
using Process parent = Process.Start(start) ?? throw new InvalidOperationException("Parent fixture launch failed.");
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(15));
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(60));
|
||||
try
|
||||
{
|
||||
string? line = await parent.StandardOutput.ReadLineAsync(timeout.Token);
|
||||
|
||||
@@ -63,7 +63,7 @@ public sealed class QqqBrowserCallbackInboxTests
|
||||
{
|
||||
string name = "QqqPrintAgent.Auth.Test." + Guid.NewGuid().ToString("N");
|
||||
using QqqBrowserCallback inbox = new(name);
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
Task<Uri> receiving = inbox.ReceiveAsync(timeout.Token);
|
||||
var steps = new List<string>();
|
||||
await QqqBrowserCallback.ForwardAsync(QqqBrowserCallback.RedirectUri + "?code=test-code&state=test-state", name, Environment.ProcessPath!, timeout.Token, steps.Add);
|
||||
|
||||
@@ -116,7 +116,7 @@ internal sealed class RealtimeRuntimeFixture : IAsyncDisposable
|
||||
{
|
||||
WebSocket socket;
|
||||
try
|
||||
{ socket = await Peer.AcceptAsync(_stopping.Token).WaitAsync(TimeSpan.FromSeconds(5)); }
|
||||
{ socket = await Peer.AcceptAsync(_stopping.Token).WaitAsync(TimeSpan.FromSeconds(60)); }
|
||||
catch(TimeoutException exception) { throw new InvalidOperationException(Diagnostic("WebSocket handshake"), exception); }
|
||||
var connection = new Connection(socket, this, loseSpoolAck, _stopping.Token);
|
||||
_connections.Add(connection);
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class ApplicationTrafficTransportTests
|
||||
Assert.Equal(handler.Downloaded, before.DownloadedBytes);
|
||||
Assert.True(before.UploadedBytes > 0);
|
||||
using var api = PrintApiClient.ForSession(session);
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
string staging = Path.Combine(Path.GetTempPath(), "qqq-traffic-" + Guid.NewGuid().ToString("N"));
|
||||
var job = new PrintJob("job", "printer", "Synthetic", "PDF", Convert.ToHexString(SHA256.HashData(document)), document.Length, new PrintOptions(), "", "CLAIMED");
|
||||
Task<DownloadedDocument> download = api.DownloadFileAsync(job, staging, new RealtimeSettings(), timeout.Token);
|
||||
@@ -88,7 +88,7 @@ public sealed class ApplicationTrafficTransportTests
|
||||
public async Task WebSocketCountsSentAndFragmentedUtf8PayloadsWithoutCountingQueuedFramesOrHandshake()
|
||||
{
|
||||
using var peer = new LoopbackWebSocketPeer();
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
using var client = new ClientWebSocket();
|
||||
client.Options.AddSubProtocol("qqq-printing.v1");
|
||||
Task<WebSocket> accepting = peer.AcceptAsync(timeout.Token);
|
||||
|
||||
@@ -661,9 +661,9 @@ public sealed class QqqSessionAuthenticationTests
|
||||
await client.SignInWithPasswordAsync("user", "password");
|
||||
await client.BindDeviceAsync(new QqqDeviceCredential("device-1", "proof"));
|
||||
Task<HttpResponseMessage> printing = client.SendPrintingAsync(HttpMethod.Get, "device/jobs/job-1/content");
|
||||
await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
using HttpResponseMessage control = await client.SendPrintingAsync(HttpMethod.Post, "device/control/poll").WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await client.SignOutAsync().WaitAsync(TimeSpan.FromSeconds(5));
|
||||
await entered.Task.WaitAsync(TimeSpan.FromSeconds(60));
|
||||
using HttpResponseMessage control = await client.SendPrintingAsync(HttpMethod.Post, "device/control/poll").WaitAsync(TimeSpan.FromSeconds(60));
|
||||
await client.SignOutAsync().WaitAsync(TimeSpan.FromSeconds(60));
|
||||
pending.SetResult(Response("{}"));
|
||||
await Assert.ThrowsAsync<QqqSessionException>(async () => { using HttpResponseMessage response = await printing; });
|
||||
Assert.False(client.IsAuthenticated);
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed class RealtimeTransportTests
|
||||
public async Task NativeCookieAndDeviceProofUpgradeKeepsReceivingDuringStalledDocument()
|
||||
{
|
||||
using var peer = new LoopbackWebSocketPeer();
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));
|
||||
using var body = new BlockingDocument();
|
||||
using var handler = new NativeHandler(body);
|
||||
using var session = new QqqSessionClient(peer.Origin, handler, allowLoopbackHttp: true);
|
||||
|
||||
Reference in new issue
Block a user