@testpulse.run/reporter 0.1.6 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +52 -20
  2. package/dist/index.cjs +57 -0
  3. package/dist/index.d.cts +18 -0
  4. package/dist/index.d.ts +18 -2
  5. package/dist/index.js +36 -1
  6. package/package.json +22 -44
  7. package/dist/fixtures/createTestPulseTest.d.ts +0 -14
  8. package/dist/fixtures/createTestPulseTest.js +0 -33
  9. package/dist/fixtures/options.d.ts +0 -2
  10. package/dist/fixtures/options.js +0 -36
  11. package/dist/fixtures/runtime.d.ts +0 -3
  12. package/dist/fixtures/runtime.js +0 -6
  13. package/dist/fixtures/types.d.ts +0 -8
  14. package/dist/fixtures/types.js +0 -1
  15. package/dist/fixtures.d.ts +0 -2
  16. package/dist/fixtures.js +0 -1
  17. package/dist/live/CaptureScheduler.d.ts +0 -22
  18. package/dist/live/CaptureScheduler.js +0 -87
  19. package/dist/live/EncoderProcess.d.ts +0 -5
  20. package/dist/live/EncoderProcess.js +0 -61
  21. package/dist/live/LiveCaptureService.d.ts +0 -2
  22. package/dist/live/LiveCaptureService.js +0 -103
  23. package/dist/live/LiveSessionResolver.d.ts +0 -2
  24. package/dist/live/LiveSessionResolver.js +0 -86
  25. package/dist/live/LiveSessionState.d.ts +0 -6
  26. package/dist/live/LiveSessionState.js +0 -20
  27. package/dist/live/Mp4FragmentParser.d.ts +0 -4
  28. package/dist/live/Mp4FragmentParser.js +0 -32
  29. package/dist/live/SegmentQueue.d.ts +0 -9
  30. package/dist/live/SegmentQueue.js +0 -32
  31. package/dist/live/StreamUploader.d.ts +0 -21
  32. package/dist/live/StreamUploader.js +0 -78
  33. package/dist/live/telemetry.d.ts +0 -3
  34. package/dist/live/telemetry.js +0 -15
  35. package/dist/live/types.d.ts +0 -42
  36. package/dist/live/types.js +0 -1
  37. package/dist/reporter/ArtifactUploader.d.ts +0 -13
  38. package/dist/reporter/ArtifactUploader.js +0 -48
  39. package/dist/reporter/EventQueue.d.ts +0 -7
  40. package/dist/reporter/EventQueue.js +0 -15
  41. package/dist/reporter/GitRuntimeMetadataResolver.d.ts +0 -11
  42. package/dist/reporter/GitRuntimeMetadataResolver.js +0 -91
  43. package/dist/reporter/PlaywrightReporter.d.ts +0 -22
  44. package/dist/reporter/PlaywrightReporter.js +0 -142
  45. package/dist/reporter/RunClient.d.ts +0 -19
  46. package/dist/reporter/RunClient.js +0 -42
  47. package/dist/reporter/types.d.ts +0 -25
  48. package/dist/reporter/types.js +0 -1
  49. package/dist/shared/endpoint.d.ts +0 -1
  50. package/dist/shared/endpoint.js +0 -15
  51. package/dist/shared/http.d.ts +0 -2
  52. package/dist/shared/http.js +0 -17
@@ -1,103 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { CaptureScheduler } from "./CaptureScheduler.js";
3
- import { startEncoderProcess, waitForEncoder } from "./EncoderProcess.js";
4
- import { resolveLiveCaptureSession } from "./LiveSessionResolver.js";
5
- import { wireFragmentedMp4Stream } from "./Mp4FragmentParser.js";
6
- import { SegmentQueue } from "./SegmentQueue.js";
7
- import { StreamUploader } from "./StreamUploader.js";
8
- import { createTimingLogger, logTiming } from "./telemetry.js";
9
- export async function startLiveCapture(page, options = {}) {
10
- const session = await resolveLiveCaptureSession(options);
11
- const intervalMs = options.intervalMs ?? session.intervalMs;
12
- const profile = options.profile ?? session.profile ?? "focus";
13
- const descriptor = await buildDescriptor(page, profile, intervalMs);
14
- const streamId = randomUUID();
15
- const captureStartedAt = Date.now();
16
- const logTimingOnce = createTimingLogger(captureStartedAt);
17
- const encoder = startEncoderProcess(descriptor, options.ffmpegPath);
18
- const uploader = new StreamUploader(session, streamId, descriptor, captureStartedAt);
19
- const uploadQueue = new SegmentQueue((error) => {
20
- console.warn(`[TestPulse] Live streaming disabled for run ${session.runId}: ${error.message}`);
21
- });
22
- const scheduler = new CaptureScheduler(page, encoder, descriptor, intervalMs, logTimingOnce);
23
- let stopped = false;
24
- let previewUploaded = false;
25
- let sequence = 0;
26
- let gopId = 0;
27
- logTiming("captureStart", captureStartedAt);
28
- wireFragmentedMp4Stream(encoder.stdout, {
29
- onInitSegment(payload) {
30
- void enqueueSegment("init", payload);
31
- },
32
- onMediaSegment(payload) {
33
- if ((sequence + 1) % Math.max(1, Math.round(descriptor.gopDurationMs / descriptor.segmentDurationMs)) === 0) {
34
- gopId += 1;
35
- }
36
- void enqueueSegment("media", payload);
37
- }
38
- });
39
- await uploader.start();
40
- const initialFrame = await scheduler.captureInitialFrame();
41
- if (initialFrame && !previewUploaded && !stopped) {
42
- try {
43
- await uploader.uploadPreview(initialFrame);
44
- previewUploaded = true;
45
- logTimingOnce("previewPosted", { bytes: initialFrame.length });
46
- }
47
- catch (error) {
48
- console.warn(`[TestPulse] Live preview upload skipped for run ${session.runId}: ${error instanceof Error ? error.message : String(error)}`);
49
- }
50
- }
51
- scheduler.writeInitialFrame(initialFrame);
52
- scheduler.start();
53
- return {
54
- async stop() {
55
- if (stopped) {
56
- return;
57
- }
58
- stopped = true;
59
- scheduler.stop();
60
- encoder.stdin.end();
61
- await waitForEncoder(encoder);
62
- await uploadQueue.flush();
63
- await uploader.stop();
64
- }
65
- };
66
- function enqueueSegment(segmentKind, payload) {
67
- const segmentSequence = ++sequence;
68
- return uploadQueue.enqueue(async () => {
69
- await uploader.uploadSegment({
70
- sequence: segmentSequence,
71
- gopId,
72
- segmentKind,
73
- payload,
74
- publishedAtUtc: new Date().toISOString()
75
- });
76
- });
77
- }
78
- }
79
- async function buildDescriptor(page, profile, intervalMs) {
80
- const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
81
- const focusWidth = Math.min(1280, viewport.width);
82
- const focusHeight = even(Math.min(720, viewport.height));
83
- const gridWidth = 640;
84
- const gridHeight = 360;
85
- const sampledFramesPerSecond = profile === "grid"
86
- ? Math.max(1, Math.round(1000 / Math.max(250, intervalMs ?? 250)))
87
- : Math.max(1, Math.round(1000 / Math.max(83, intervalMs ?? 83)));
88
- const framesPerSecond = Math.max(8, sampledFramesPerSecond);
89
- return {
90
- streamId: randomUUID(),
91
- profile,
92
- mimeCodec: "video/mp4; codecs=\"avc1.42E01E\"",
93
- width: even(profile === "grid" ? gridWidth : focusWidth),
94
- height: profile === "grid" ? gridHeight : focusHeight,
95
- framesPerSecond,
96
- targetBitrateKbps: profile === "grid" ? 300 : 1200,
97
- segmentDurationMs: 250,
98
- gopDurationMs: 500
99
- };
100
- }
101
- function even(value) {
102
- return value % 2 === 0 ? value : value - 1;
103
- }
@@ -1,2 +0,0 @@
1
- import type { LiveCaptureOptions, LiveCaptureState } from "./types.js";
2
- export declare function resolveLiveCaptureSession(options: LiveCaptureOptions, now?: () => number, wait?: (ms: number) => Promise<unknown>): Promise<LiveCaptureState>;
@@ -1,86 +0,0 @@
1
- import { access, readdir, readFile } from "node:fs/promises";
2
- import { getLiveCaptureStateDirectory, getLiveCaptureStatePath } from "./LiveSessionState.js";
3
- export async function resolveLiveCaptureSession(options, now = () => Date.now(), wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
4
- if (options.endpoint && options.projectId && options.apiKey && options.runId && options.streamPublisherToken) {
5
- return {
6
- endpoint: options.endpoint,
7
- projectId: options.projectId,
8
- apiKey: options.apiKey,
9
- runId: options.runId,
10
- streamPublisherToken: options.streamPublisherToken,
11
- createdAt: new Date(now()).toISOString()
12
- };
13
- }
14
- if (!await shouldWaitForState(options)) {
15
- const path = options.sessionKey
16
- ? getLiveCaptureStatePath(options.sessionKey, options.stateDirectory)
17
- : getLiveCaptureStateDirectory(options.stateDirectory);
18
- throw new Error(`TestPulse live streaming session state was not found at ${path}.`);
19
- }
20
- const deadline = now() + 30_000;
21
- while (now() < deadline) {
22
- const session = options.sessionKey
23
- ? await tryReadStateFile(getLiveCaptureStatePath(options.sessionKey, options.stateDirectory), now)
24
- : await tryReadLatestStateFile(options.stateDirectory, now);
25
- if (session) {
26
- return session;
27
- }
28
- await wait(250);
29
- }
30
- const path = options.sessionKey
31
- ? getLiveCaptureStatePath(options.sessionKey, options.stateDirectory)
32
- : getLiveCaptureStateDirectory(options.stateDirectory);
33
- throw new Error(`TestPulse live streaming session state was not found at ${path}.`);
34
- }
35
- async function shouldWaitForState(options) {
36
- if (options.sessionKey) {
37
- return true;
38
- }
39
- try {
40
- const directory = getLiveCaptureStateDirectory(options.stateDirectory);
41
- await access(directory);
42
- const entries = await readdir(directory);
43
- return entries.some((entry) => entry.endsWith(".json"));
44
- }
45
- catch {
46
- return false;
47
- }
48
- }
49
- async function tryReadLatestStateFile(stateDirectory, now) {
50
- try {
51
- const directory = getLiveCaptureStateDirectory(stateDirectory);
52
- const entries = (await readdir(directory))
53
- .filter((entry) => entry.endsWith(".json"))
54
- .sort();
55
- let freshest = null;
56
- let freshestCreatedAt = -Infinity;
57
- for (const entry of entries) {
58
- const candidate = await tryReadStateFile(`${directory}/${entry}`, now);
59
- if (!candidate) {
60
- continue;
61
- }
62
- const createdAt = Date.parse(candidate.createdAt);
63
- if (createdAt > freshestCreatedAt) {
64
- freshest = candidate;
65
- freshestCreatedAt = createdAt;
66
- }
67
- }
68
- return freshest;
69
- }
70
- catch {
71
- return null;
72
- }
73
- }
74
- async function tryReadStateFile(path, now) {
75
- try {
76
- const payload = JSON.parse(await readFile(path, "utf8"));
77
- const createdAt = Date.parse(payload.createdAt);
78
- if (payload.endpoint && payload.projectId && payload.apiKey && payload.runId && payload.streamPublisherToken && !Number.isNaN(createdAt) && createdAt > now() - 60_000) {
79
- return payload;
80
- }
81
- }
82
- catch {
83
- // Reporter may not have created the state file yet.
84
- }
85
- return null;
86
- }
@@ -1,6 +0,0 @@
1
- import type { LiveCaptureState } from "./types.js";
2
- export declare function getLiveCaptureSessionKey(sessionKey?: string): string;
3
- export declare function getLiveCaptureStateDirectory(stateDirectory?: string): string;
4
- export declare function getLiveCaptureStatePath(sessionKey?: string, stateDirectory?: string): string;
5
- export declare function writeLiveCaptureState(state: LiveCaptureState, sessionKey?: string, stateDirectory?: string): Promise<void>;
6
- export declare function deleteLiveCaptureState(sessionKey?: string, stateDirectory?: string): Promise<void>;
@@ -1,20 +0,0 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- export function getLiveCaptureSessionKey(sessionKey) {
5
- return sessionKey?.trim() || "default";
6
- }
7
- export function getLiveCaptureStateDirectory(stateDirectory) {
8
- return stateDirectory?.trim() || join(tmpdir(), "testpulse-live-capture");
9
- }
10
- export function getLiveCaptureStatePath(sessionKey, stateDirectory) {
11
- return join(getLiveCaptureStateDirectory(stateDirectory), `${getLiveCaptureSessionKey(sessionKey)}.json`);
12
- }
13
- export async function writeLiveCaptureState(state, sessionKey, stateDirectory) {
14
- const path = getLiveCaptureStatePath(sessionKey, stateDirectory);
15
- await mkdir(getLiveCaptureStateDirectory(stateDirectory), { recursive: true });
16
- await writeFile(path, JSON.stringify(state), "utf8");
17
- }
18
- export async function deleteLiveCaptureState(sessionKey, stateDirectory) {
19
- await rm(getLiveCaptureStatePath(sessionKey, stateDirectory), { force: true });
20
- }
@@ -1,4 +0,0 @@
1
- export declare function wireFragmentedMp4Stream(stdout: NodeJS.ReadableStream, handlers: {
2
- onInitSegment(payload: Buffer): void;
3
- onMediaSegment(payload: Buffer): void;
4
- }): void;
@@ -1,32 +0,0 @@
1
- export function wireFragmentedMp4Stream(stdout, handlers) {
2
- let buffer = Buffer.alloc(0);
3
- const initBoxes = [];
4
- let initEmitted = false;
5
- let pendingFragment = [];
6
- stdout.on("data", (chunk) => {
7
- buffer = Buffer.concat([buffer, chunk]);
8
- while (buffer.length >= 8) {
9
- const size = buffer.readUInt32BE(0);
10
- if (size <= 0 || buffer.length < size) {
11
- return;
12
- }
13
- const box = buffer.subarray(0, size);
14
- buffer = buffer.subarray(size);
15
- const type = box.subarray(4, 8).toString("ascii");
16
- if (!initEmitted) {
17
- initBoxes.push(box);
18
- if (type === "moov") {
19
- initEmitted = true;
20
- handlers.onInitSegment(Buffer.concat(initBoxes));
21
- initBoxes.length = 0;
22
- }
23
- continue;
24
- }
25
- pendingFragment.push(box);
26
- if (type === "mdat") {
27
- handlers.onMediaSegment(Buffer.concat(pendingFragment));
28
- pendingFragment = [];
29
- }
30
- }
31
- });
32
- }
@@ -1,9 +0,0 @@
1
- export declare class SegmentQueue {
2
- private readonly onError;
3
- private chain;
4
- private disabled;
5
- private firstError;
6
- constructor(onError: (error: Error) => void);
7
- enqueue(task: () => Promise<void>): Promise<void>;
8
- flush(): Promise<void>;
9
- }
@@ -1,32 +0,0 @@
1
- export class SegmentQueue {
2
- onError;
3
- chain = Promise.resolve();
4
- disabled = false;
5
- firstError = null;
6
- constructor(onError) {
7
- this.onError = onError;
8
- }
9
- enqueue(task) {
10
- if (this.disabled) {
11
- return this.chain;
12
- }
13
- this.chain = this.chain.then(task).catch((error) => {
14
- const resolved = error instanceof Error ? error : new Error(String(error));
15
- if (!this.firstError) {
16
- this.firstError = resolved;
17
- this.onError(resolved);
18
- }
19
- this.disabled = true;
20
- });
21
- return this.chain;
22
- }
23
- async flush() {
24
- await this.chain.catch((error) => {
25
- const resolved = error instanceof Error ? error : new Error(String(error));
26
- if (!this.firstError) {
27
- this.firstError = resolved;
28
- this.onError(resolved);
29
- }
30
- });
31
- }
32
- }
@@ -1,21 +0,0 @@
1
- import type { LiveCaptureState, SegmentKind, StreamDescriptor } from "./types.js";
2
- export declare class StreamUploader {
3
- private readonly session;
4
- private readonly streamId;
5
- private readonly descriptor;
6
- private readonly captureStartedAt;
7
- private firstInitUploaded;
8
- private firstMediaUploaded;
9
- constructor(session: LiveCaptureState, streamId: string, descriptor: StreamDescriptor, captureStartedAt: number);
10
- start(): Promise<void>;
11
- uploadPreview(frame: Buffer): Promise<void>;
12
- uploadSegment(segment: {
13
- sequence: number;
14
- gopId: number;
15
- segmentKind: SegmentKind;
16
- payload: Buffer;
17
- publishedAtUtc: string;
18
- }): Promise<void>;
19
- stop(): Promise<void>;
20
- private buildHeaders;
21
- }
@@ -1,78 +0,0 @@
1
- import { postJson, postJsonOrThrow } from "../shared/http.js";
2
- import { logTiming } from "./telemetry.js";
3
- export class StreamUploader {
4
- session;
5
- streamId;
6
- descriptor;
7
- captureStartedAt;
8
- firstInitUploaded = false;
9
- firstMediaUploaded = false;
10
- constructor(session, streamId, descriptor, captureStartedAt) {
11
- this.session = session;
12
- this.streamId = streamId;
13
- this.descriptor = descriptor;
14
- this.captureStartedAt = captureStartedAt;
15
- }
16
- async start() {
17
- await postJsonOrThrow(`${this.session.endpoint}/ingest/v1/projects/${encodeURIComponent(this.session.projectId)}/runs/${this.session.runId}/stream/start`, {
18
- token: this.session.streamPublisherToken,
19
- streamId: this.streamId,
20
- profile: this.descriptor.profile,
21
- mimeCodec: this.descriptor.mimeCodec,
22
- width: this.descriptor.width,
23
- height: this.descriptor.height,
24
- framesPerSecond: this.descriptor.framesPerSecond,
25
- targetBitrateKbps: this.descriptor.targetBitrateKbps,
26
- segmentDurationMs: this.descriptor.segmentDurationMs,
27
- gopDurationMs: this.descriptor.gopDurationMs
28
- }, "TestPulse live request failed with", this.buildHeaders());
29
- }
30
- async uploadPreview(frame) {
31
- await postJsonOrThrow(`${this.session.endpoint}/ingest/v1/projects/${encodeURIComponent(this.session.projectId)}/runs/${this.session.runId}/stream/preview`, {
32
- token: this.session.streamPublisherToken,
33
- streamId: this.streamId,
34
- capturedAtUtc: new Date().toISOString(),
35
- contentType: "image/jpeg",
36
- payloadBase64: frame.toString("base64"),
37
- width: this.descriptor.width,
38
- height: this.descriptor.height
39
- }, "TestPulse live request failed with", this.buildHeaders());
40
- }
41
- async uploadSegment(segment) {
42
- await postJsonOrThrow(`${this.session.endpoint}/ingest/v1/projects/${encodeURIComponent(this.session.projectId)}/runs/${this.session.runId}/stream/segments`, {
43
- token: this.session.streamPublisherToken,
44
- streamId: this.streamId,
45
- sequence: segment.sequence,
46
- segmentKind: segment.segmentKind,
47
- gopId: segment.gopId,
48
- ptsStart: segment.sequence * this.descriptor.segmentDurationMs,
49
- ptsEnd: (segment.sequence + 1) * this.descriptor.segmentDurationMs,
50
- durationMs: this.descriptor.segmentDurationMs,
51
- publishedAtUtc: segment.publishedAtUtc,
52
- discontinuity: false,
53
- streamResetRequired: false,
54
- mimeCodec: this.descriptor.mimeCodec,
55
- payloadBase64: segment.payload.toString("base64")
56
- }, "TestPulse live request failed with", this.buildHeaders());
57
- if (segment.segmentKind === "init" && !this.firstInitUploaded) {
58
- this.firstInitUploaded = true;
59
- logTiming("firstInitSegmentUploaded", this.captureStartedAt, { sequence: segment.sequence });
60
- }
61
- if (segment.segmentKind === "media" && !this.firstMediaUploaded) {
62
- this.firstMediaUploaded = true;
63
- logTiming("firstMediaSegmentUploaded", this.captureStartedAt, { sequence: segment.sequence });
64
- }
65
- }
66
- async stop() {
67
- await postJson(`${this.session.endpoint}/ingest/v1/projects/${encodeURIComponent(this.session.projectId)}/runs/${this.session.runId}/stream/stop`, {
68
- token: this.session.streamPublisherToken,
69
- streamId: this.streamId,
70
- reason: "capture-stopped"
71
- }, this.buildHeaders());
72
- }
73
- buildHeaders() {
74
- return {
75
- "X-Project-Api-Key": this.session.apiKey
76
- };
77
- }
78
- }
@@ -1,3 +0,0 @@
1
- import type { TimingKey } from "./types.js";
2
- export declare function logTiming(key: TimingKey, captureStartedAt?: number, extra?: Record<string, number | string>): void;
3
- export declare function createTimingLogger(captureStartedAt: number): (key: TimingKey, extra?: Record<string, number | string>) => void;
@@ -1,15 +0,0 @@
1
- export function logTiming(key, captureStartedAt = Date.now(), extra) {
2
- const elapsedMs = Math.max(0, Date.now() - captureStartedAt);
3
- const suffix = extra ? ` ${JSON.stringify(extra)}` : "";
4
- console.info(`[TestPulse] ${key} ${elapsedMs}ms${suffix}`);
5
- }
6
- export function createTimingLogger(captureStartedAt) {
7
- const loggedTimings = new Set();
8
- return (key, extra) => {
9
- if (loggedTimings.has(key)) {
10
- return;
11
- }
12
- loggedTimings.add(key);
13
- logTiming(key, captureStartedAt, extra);
14
- };
15
- }
@@ -1,42 +0,0 @@
1
- import type { Page } from "@playwright/test";
2
- export type LiveCaptureState = {
3
- endpoint: string;
4
- projectId: string;
5
- apiKey: string;
6
- runId: string;
7
- streamPublisherToken: string;
8
- createdAt: string;
9
- intervalMs?: number;
10
- profile?: StreamProfile;
11
- };
12
- export type StreamProfile = "focus" | "grid";
13
- export type StreamDescriptor = {
14
- streamId: string;
15
- profile: StreamProfile;
16
- mimeCodec: string;
17
- width: number;
18
- height: number;
19
- framesPerSecond: number;
20
- targetBitrateKbps: number;
21
- segmentDurationMs: number;
22
- gopDurationMs: number;
23
- };
24
- export type SegmentKind = "init" | "media";
25
- export type TimingKey = "captureStart" | "firstScreenshotTaken" | "previewPosted" | "firstFrameWrittenToEncoder" | "firstInitSegmentUploaded" | "firstMediaSegmentUploaded";
26
- export interface LiveCaptureOptions {
27
- endpoint?: string;
28
- projectId?: string;
29
- apiKey?: string;
30
- runId?: string;
31
- streamPublisherToken?: string;
32
- sessionKey?: string;
33
- stateDirectory?: string;
34
- intervalMs?: number;
35
- profile?: StreamProfile;
36
- ffmpegPath?: string;
37
- }
38
- export interface LiveCaptureHandle {
39
- stop(): Promise<void>;
40
- }
41
- export interface LivePage extends Pick<Page, "screenshot" | "viewportSize"> {
42
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,13 +0,0 @@
1
- import type { TestCase, TestResult } from "@playwright/test/reporter";
2
- import type { EventQueue } from "./EventQueue.js";
3
- import type { RunClient } from "./RunClient.js";
4
- export declare class ArtifactUploader {
5
- private readonly eventQueue;
6
- private readonly ensureRunStarted;
7
- private readonly runClient;
8
- private readonly uploadedArtifacts;
9
- constructor(eventQueue: EventQueue, ensureRunStarted: () => Promise<string>, runClient: RunClient);
10
- enqueueArtifacts(test: TestCase, result: TestResult, eventSequence: number): void;
11
- }
12
- export declare function getArtifactType(attachment: TestResult["attachments"][number]): "image" | "video" | null;
13
- export declare function getContentType(artifactType: "image" | "video"): string;
@@ -1,48 +0,0 @@
1
- export class ArtifactUploader {
2
- eventQueue;
3
- ensureRunStarted;
4
- runClient;
5
- uploadedArtifacts = new Set();
6
- constructor(eventQueue, ensureRunStarted, runClient) {
7
- this.eventQueue = eventQueue;
8
- this.ensureRunStarted = ensureRunStarted;
9
- this.runClient = runClient;
10
- }
11
- enqueueArtifacts(test, result, eventSequence) {
12
- for (const attachment of result.attachments ?? []) {
13
- if (!attachment.path) {
14
- continue;
15
- }
16
- const artifactType = getArtifactType(attachment);
17
- if (!artifactType || this.uploadedArtifacts.has(attachment.path)) {
18
- continue;
19
- }
20
- this.uploadedArtifacts.add(attachment.path);
21
- this.eventQueue.enqueue(async () => {
22
- const runId = await this.ensureRunStarted();
23
- await this.runClient.uploadArtifact(runId, {
24
- artifactType,
25
- filePath: attachment.path,
26
- contentType: attachment.contentType ?? getContentType(artifactType),
27
- testId: test.id,
28
- eventSequence: artifactType === "image" ? eventSequence : undefined
29
- });
30
- });
31
- }
32
- }
33
- }
34
- export function getArtifactType(attachment) {
35
- const contentType = attachment.contentType?.toLowerCase() ?? "";
36
- const name = attachment.name?.toLowerCase() ?? "";
37
- const path = attachment.path?.toLowerCase() ?? "";
38
- if (contentType.startsWith("image/") || name.includes("screenshot") || /\.(png|jpe?g|webp)$/i.test(path)) {
39
- return "image";
40
- }
41
- if (contentType.startsWith("video/") || name.includes("video") || /\.(webm|mp4)$/i.test(path)) {
42
- return "video";
43
- }
44
- return null;
45
- }
46
- export function getContentType(artifactType) {
47
- return artifactType === "image" ? "image/png" : "video/webm";
48
- }
@@ -1,7 +0,0 @@
1
- export declare class EventQueue {
2
- private sequence;
3
- private chain;
4
- nextSequence(task: (sequence: number) => Promise<void>): number;
5
- enqueue(task: () => Promise<void>): void;
6
- flush(): Promise<void>;
7
- }
@@ -1,15 +0,0 @@
1
- export class EventQueue {
2
- sequence = 0;
3
- chain = Promise.resolve();
4
- nextSequence(task) {
5
- const sequence = ++this.sequence;
6
- this.chain = this.chain.then(() => task(sequence));
7
- return sequence;
8
- }
9
- enqueue(task) {
10
- this.chain = this.chain.then(task);
11
- }
12
- async flush() {
13
- await this.chain;
14
- }
15
- }
@@ -1,11 +0,0 @@
1
- type CommandExecutor = (command: string, args: string[], cwd: string) => Promise<string>;
2
- export interface GitRuntimeMetadataOptions {
3
- branch?: string;
4
- commitSha?: string;
5
- }
6
- export interface GitRuntimeMetadata {
7
- branch?: string;
8
- commitSha?: string;
9
- }
10
- export declare function resolveGitRuntimeMetadata(options: GitRuntimeMetadataOptions, cwd?: string, environment?: NodeJS.ProcessEnv, executor?: CommandExecutor): Promise<GitRuntimeMetadata>;
11
- export {};
@@ -1,91 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
- const execFileAsync = promisify(execFile);
4
- const branchEnvironmentVariables = [
5
- "GITHUB_HEAD_REF",
6
- "GITHUB_REF_NAME",
7
- "CI_COMMIT_REF_NAME",
8
- "BUILD_SOURCEBRANCHNAME",
9
- "BRANCH_NAME"
10
- ];
11
- const commitEnvironmentVariables = [
12
- "GITHUB_SHA",
13
- "CI_COMMIT_SHA",
14
- "BUILD_SOURCEVERSION",
15
- "COMMIT_SHA"
16
- ];
17
- export async function resolveGitRuntimeMetadata(options, cwd = process.cwd(), environment = process.env, executor = executeGitCommand) {
18
- const branch = options.branch?.trim() || getFirstEnvironmentValue(branchEnvironmentVariables, environment);
19
- const configuredCommitSha = options.commitSha?.trim();
20
- if (branch && configuredCommitSha) {
21
- return {
22
- branch,
23
- commitSha: configuredCommitSha
24
- };
25
- }
26
- const gitBranch = branch || await tryGetGitBranch(cwd, executor);
27
- const commitSha = configuredCommitSha || await tryGetGitCommit(cwd, environment, executor);
28
- if (!commitSha) {
29
- return {
30
- branch: gitBranch || undefined,
31
- commitSha: undefined
32
- };
33
- }
34
- const hasPendingChanges = await tryHasPendingChanges(cwd, executor);
35
- const displayCommitSha = shortenCommitSha(commitSha);
36
- return {
37
- branch: gitBranch || undefined,
38
- commitSha: hasPendingChanges ? `${displayCommitSha} + pending changes` : displayCommitSha
39
- };
40
- }
41
- async function tryGetGitBranch(cwd, executor) {
42
- try {
43
- const branch = await executor("git", ["rev-parse", "--abbrev-ref", "HEAD"], cwd);
44
- if (!branch || branch === "HEAD") {
45
- return undefined;
46
- }
47
- return branch;
48
- }
49
- catch {
50
- return undefined;
51
- }
52
- }
53
- async function tryGetGitCommit(cwd, environment, executor) {
54
- const environmentCommit = getFirstEnvironmentValue(commitEnvironmentVariables, environment);
55
- if (environmentCommit) {
56
- return environmentCommit;
57
- }
58
- try {
59
- return await executor("git", ["rev-parse", "HEAD"], cwd);
60
- }
61
- catch {
62
- return undefined;
63
- }
64
- }
65
- async function tryHasPendingChanges(cwd, executor) {
66
- try {
67
- const status = await executor("git", ["status", "--porcelain"], cwd);
68
- return status.length > 0;
69
- }
70
- catch {
71
- return false;
72
- }
73
- }
74
- function getFirstEnvironmentValue(keys, environment) {
75
- for (const key of keys) {
76
- const value = environment[key]?.trim();
77
- if (value) {
78
- return value;
79
- }
80
- }
81
- return undefined;
82
- }
83
- async function executeGitCommand(command, args, cwd) {
84
- const result = await execFileAsync(command, args, { cwd });
85
- return result.stdout.trim();
86
- }
87
- function shortenCommitSha(commitSha) {
88
- return /^[0-9a-f]{8,}$/i.test(commitSha)
89
- ? commitSha.slice(0, 7)
90
- : commitSha;
91
- }