// Copyright 2026 TesterArmy.
// Modified by Dario Kondratiuk.
// SPDX-License-Identifier: Apache-2.0
using E2E.Engine;
using E2E.Internal;
namespace E2E;
///
/// The video of one attempt, from the runner's side (upstream execute.ts launchSession, closeSession,
/// stopVideo, and registerVideos): starts the video the attempt's mode asks for once the engine is up, and
/// after the verdict collects it, keeps it or deletes it by the mode, and checks what the engine handed back. A video a
/// default mode asked for fails quietly; one somebody set fails the launch or the cleanup.
///
internal sealed class ExecuteVideo
{
private readonly VideoRecording _recording;
private readonly IEngineSession _session;
private readonly string _artifactsDir;
private ExecuteVideo(VideoRecording recording, IEngineSession session, string artifactsDir)
{
_recording = recording;
_session = session;
_artifactsDir = artifactsDir;
}
///
/// The video this attempt records, or null for none. A mode a target or a test set is required: an engine that cannot
/// record it fails here, before the engine starts, with UNSUPPORTED_ARTIFACT. A run-wide or default mode is
/// skipped where the engine cannot record. Upstream validateEngine, for the one attempt a session is. The port has
/// no retries to look ahead to, so an attempt that does not record under a retry mode is never refused.
///
public static VideoRecording? Plan(E2ESessionOptions options)
{
var video = RecordingModes.AttemptVideo(options.Video, options.Attempt - 1);
if (video is null)
{
return null;
}
if ((options.Engine.Capabilities & EngineCapabilities.Video) != 0)
{
return video;
}
if (options.Video.Source is RecordingSource.Target or RecordingSource.Test)
{
var target = string.IsNullOrWhiteSpace(options.TargetName) ? options.Engine.Platform : options.TargetName;
var setBy = options.Video.Source == RecordingSource.Test ? $"test \"{options.TestTitle}\" sets" : "the target sets";
throw new ConfigurationException(
"UNSUPPORTED_ARTIFACT",
$"target \"{target}\" (engine {options.Engine.Platform}) cannot record video, and {setBy} video: '{RecordingModes.Name(options.Video.Mode)}'");
}
return null;
}
///
/// Starts the video within the launch timeout. A required video's failure fails the start; a default one's is dropped.
/// A cancel of is never dropped.
///
public static async Task StartAsync(VideoRecording? recording, IEngineSession session, string artifactsDir, TimeSpan launchTimeout, CancellationToken cancellationToken)
{
if (recording is null)
{
return null;
}
using var launch = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
if (launchTimeout > TimeSpan.Zero && launchTimeout != Timeout.InfiniteTimeSpan)
{
launch.CancelAfter(launchTimeout);
}
try
{
await session.StartVideoAsync(launch.Token).ConfigureAwait(false);
}
#pragma warning disable CA1031 // A default video is best effort: whatever stops it, the attempt goes on without one.
catch (Exception) when (recording.Policy == VideoPolicy.BestEffort && !cancellationToken.IsCancellationRequested)
#pragma warning restore CA1031
{
return null;
}
catch (Exception ex) when ((ex is OperationCanceledException || ex is EngineException { Code: EngineErrorCodes.Cancelled }) && launch.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
throw new EngineException("ENVIRONMENT_UNAVAILABLE", "The video did not start within the launch timeout (" + launchTimeout.TotalMilliseconds.ToString(System.Globalization.CultureInfo.InvariantCulture) + " ms).", ex);
}
return new ExecuteVideo(recording, session, artifactsDir);
}
/// How a final attempt ended, from the error the framework reports for it.
public static AttemptStatus StatusOf(Exception? error) => error switch
{
null => AttemptStatus.Passed,
SkipException => AttemptStatus.Skipped,
OperationCanceledException => AttemptStatus.Interrupted,
E2EException { Code: "TEST_TIMEOUT" } => AttemptStatus.TimedOut,
_ => AttemptStatus.Failed,
};
/// The segments the attempt keeps, set by even when it then throws.
public IReadOnlyList Kept { get; private set; } = [];
///
/// Finalizes the video within the cleanup timeout and sets to the segments the attempt keeps. A video
/// the mode does not keep for this verdict is deleted. A required video that cannot be finalized throws, since it is a
/// cleanup failure, and so does a link that is not an http(s) URL, once every other recording is kept; a default video
/// that cannot be finalized is dropped.
///
public async Task StopAsync(AttemptStatus status, TimeSpan cleanupTimeout, IArtifactSink? sink = null)
{
using var cleanup = new CancellationTokenSource();
if (cleanupTimeout > TimeSpan.Zero && cleanupTimeout != Timeout.InfiniteTimeSpan)
{
cleanup.CancelAfter(cleanupTimeout);
}
try
{
var segments = await _session.StopVideoAsync(cleanup.Token).ConfigureAwait(false);
// Recorded so a failure could be looked at; a pass has nothing to show.
if (!RecordingModes.Keeps(_recording.Keep, status))
{
foreach (var file in segments.OfType())
{
// An engine's path is its own word: only a file inside the attempt's directory is ours to delete.
var full = Path.GetFullPath(Path.Combine(_artifactsDir, file.Path));
if (full.StartsWith(Path.GetFullPath(_artifactsDir) + Path.DirectorySeparatorChar, StringComparison.Ordinal))
{
File.Delete(full);
}
}
return;
}
Register(segments, sink);
}
#pragma warning disable CA1031 // A default video is best effort: whatever stops it, the attempt reports none.
catch (Exception) when (_recording.Policy == VideoPolicy.BestEffort)
#pragma warning restore CA1031
{
}
}
///
/// Keeps an attempt's recordings, in order: a file by its path, a link by its URL. An engine's own link is held to what a
/// provider's is, an http(s) URL; one that is not fails the cleanup once every other recording is kept, so it costs no
/// other video. Upstream registerVideos.
///
// An engine's path is its own word: only a file inside the attempt's directory is ours to register.
private bool InsideAttemptDir(string path) =>
Path.GetFullPath(Path.Combine(_artifactsDir, path)).StartsWith(Path.GetFullPath(_artifactsDir) + Path.DirectorySeparatorChar, StringComparison.Ordinal);
private void Register(IReadOnlyList segments, IArtifactSink? sink)
{
var rejected = segments.OfType().Where(link => ProviderRecordings.HostedVideoUrl(link.Url) is null).ToList();
Kept = [.. segments.Where(segment => segment is not VideoLink link || !rejected.Contains(link))];
foreach (var segment in Kept)
{
var startedAt = DateTimeOffset.TryParse(segment.StartedAt, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out var parsed) ? parsed : (DateTimeOffset?)null;
switch (segment)
{
case VideoFile file when sink is not null && InsideAttemptDir(file.Path):
sink.Register(E2E.Reports.ArtifactKind.Video, file.Path.Replace(Path.DirectorySeparatorChar, '/'), new ArtifactRegistration { StartedAt = startedAt });
break;
case VideoLink link:
sink?.Link(link.Url, new ArtifactLink(link.MediaType, startedAt ?? DateTimeOffset.UtcNow));
break;
}
}
if (rejected.Count > 0)
{
throw new EngineException(EngineErrorCodes.EngineFailure, "the engine returned a video link that is not an http(s) URL: " + string.Join(", ", rejected.Select(link => $"\"{link.Url}\"")), retryable: false);
}
}
}