Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,20 +234,23 @@ npx @testomatio/reporter xml "pytest-results.xml" --timelimit 300 --env-file .en

### 5. upload-artifacts

Testomat.io reporter automatically uploads artifacts during run. However, either some artifacts failed to upload or you intentioanlly disabled file upload during tests to speed up reporting. In this case you can use this command to upload artifacts after the run.

It is important to have the `TESTOMATIO_RUN` environment variable set to the run ID.
Testomat.io reporter automatically uploads artifacts during run. However, either some artifacts failed to upload or you intentionally disabled file upload during tests to speed up reporting. In this case you can use this command to upload artifacts after the run.

**Usage:**

```bash
npx @testomatio/reporter upload-artifacts [options]
npx @testomatio/reporter upload-artifacts [jsonl-file] [options]
```

**Arguments:**

- `jsonl-file` (optional) - Path to JSONL debug file to upload artifacts from. When provided, artifacts are loaded from the debug file instead of local temp storage.

**Environment Variables:**

- `TESTOMATIO`: Your Testomat.io API key (required).
- `TESTOMATIO_RUN`: The previous run ID you want to upload artifacts (optional). If none set, latest run will be used.
- `TESTOMATIO_URL`: Testomat.io server URL (optional, defaults to `https://app.testomat.io`).

**Options:**

Expand All @@ -256,7 +259,7 @@ npx @testomatio/reporter upload-artifacts [options]

You still need [S3 artifacts configuration](./artifacts.md) to be set to upload artifacts to storage. In order to disable artifacts upload during tests you can use `TESTOMATIO_DISABLE_ARTIFACTS=1` while running tests.

**Examples:**
**Upload from local temp storage (default mode):**

```bash
npx @testomatio/reporter upload-artifacts
Expand Down Expand Up @@ -287,6 +290,28 @@ TESTOMATIO=tstmt_* npx @testomatio/reporter upload-artifacts

However, `upload-artifacts` command will upload all files after the run, without blocking the final result.

**Upload from JSONL debug file:**

You can upload artifacts from a JSONL debug file generated during test execution with `TESTOMATIO_DEBUG=1`. This mode reads artifact paths from the debug file and uploads them to the correct tests.

```bash
# Run tests with debug mode enabled
TESTOMATIO=tstmt_* TESTOMATIO_DEBUG=1 npx playwright test

# Upload artifacts from the debug file
TESTOMATIO=tstmt_* npx @testomatio/reporter upload-artifacts ./testomatio.debug.json
```

This approach is useful when:
- You need to re-upload artifacts after a failed upload
- You want to upload artifacts to a different run
- The original upload was skipped due to size limits

**Notes:**
- The JSONL file must contain a valid `runId` and tests with `test_id` set
- **CodeceptJS only**: trace.zip and video files are automatically recorded when using `TESTOMATIO_DEBUG=1`
- Artifacts are uploaded to the test level (Artifacts tab), not within individual steps

### 6. replay

The `replay` command allows you to re-send test data from debug files to Testomat.io. This is useful when your original test run failed to upload results properly.
Expand Down Expand Up @@ -329,7 +354,7 @@ npx @testomatio/reporter replay --env-file .env.staging

**How it works:**

The replay command uses the `ReplayService` class (located in `src/replay.js`) to:
The replay command uses the `Replay` class (located in `src/replay.js`) to:

1. Parse the debug file line by line
2. Extract environment variables, run parameters, test data, and finish parameters
Expand Down
7 changes: 7 additions & 0 deletions src/adapter/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,13 @@ async function uploadAttachments(client, attachments, messagePrefix, attachmentT
log.info(`Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
}

if (client.pipes?.length) {
const debugPipe = client.pipes.find(p => p.constructor.name === 'DebugPipe');
if (debugPipe?.isEnabled) {
debugPipe.addArtifacts(attachments);
}
}

const promises = attachments.map(async attachment => {
const { rid, title, path, type } = attachment;
const file = { path, type, title };
Expand Down
97 changes: 96 additions & 1 deletion src/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import dotenv from 'dotenv';
import Replay from '../replay.js';
import { log } from '../utils/log.js';
import { formatFilterListIds } from '../utils/pipe_utils.js';
import fs from 'fs';
import path from 'path';
import { Gaxios } from 'gaxios';
import { generateShortFilename } from '../adapter/utils/step-formatter.js';

const debug = createDebugMessages('@testomatio/reporter:cli');
const version = getPackageVersion();
Expand Down Expand Up @@ -354,10 +358,101 @@ program
program
.command('upload-artifacts')
.description('Upload artifacts to Testomat.io')
.argument('[jsonl-file]', 'Path to JSONL debug file (optional)')
.option('--force', 'Re-upload artifacts even if they were uploaded before')
.action(async opts => {
.action(async (jsonlFile, opts) => {
const apiKey = config.TESTOMATIO;

// JSONL file mode: upload artifacts from debug file
if (jsonlFile) {
if (!fs.existsSync(jsonlFile)) {
log.error(`JSONL file not found: ${jsonlFile}`);
return process.exit(1);
}

const replay = new Replay({ apiKey });
const { tests, runId } = replay.parseDebugFile(jsonlFile);

if (!runId) {
log.error('runId not found in JSONL file');
return process.exit(1);
}

log.info(`Processing ${tests.length} tests from ${jsonlFile}`);

const client = new TestomatClient({
apiKey,
runId,
batchMode: BATCH_MODE.DISABLED,
});

await client.createRun();
client.uploader.checkEnabled();
client.uploader.disableLogStorage();

const apiUrl = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
const http = new Gaxios();
let uploadedCount = 0;
let failedCount = 0;

for (const test of tests) {
const testId = test.test_id;
if (!testId) continue;

const artifacts = [];

const collect = (items) => {
for (const item of items || []) {
const p = typeof item === 'object' ? item?.path : item;
if (p && fs.existsSync(p)) artifacts.push(p);
}
};
collect(test.files);

const walkSteps = (steps) => {
for (const step of steps || []) {
collect(step.artifacts);
walkSteps(step.steps);
}
};
walkSteps(test.steps);

if (artifacts.length === 0) continue;

const urls = [];
for (const artifact of artifacts) {
try {
const s3Id = test.rid || testId.replace('@', '');
const filename = generateShortFilename(artifact);
const result = await client.uploader.uploadFileByPath(artifact, [runId, s3Id, filename]);
if (result) urls.push(typeof result === 'string' ? result : result.link);
} catch (e) {
debug(`Failed to upload ${artifact}:`, e.message);
}
}

if (urls.length === 0) continue;

try {
await http.request({
method: 'POST',
url: `${apiUrl}/api/reporter/${runId}/testrun?api_key=${apiKey}`,
data: { test_id: testId, artifacts: urls },
});
uploadedCount++;
} catch (e) {
log.error(`Failed ${testId}: ${e.message}`);
failedCount++;
}
}

log.info(`🗄️ ${uploadedCount} tests with artifacts uploaded`);
if (failedCount > 0) {
log.warn(`⚠️ ${failedCount} tests failed to upload artifacts`);
}
return;
}

process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();

Expand Down
11 changes: 11 additions & 0 deletions src/pipe/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ export class DebugPipe {
log.info(`History: ${this.historyDir}`);
}

/**
* Logs artifacts data to the debug file.
* Used for trace.zip, video files and other artifacts uploaded after tests finish.
* @param {Array} artifacts - Array of artifacts with { rid, title, path, type }
*/
addArtifacts(artifacts) {
if (!this.isEnabled || !artifacts?.length) return;
const logData = { action: 'addArtifacts', artifacts, runId: this.store.runId };
this.logToFile(logData);
}

async sync() {
this.flushBufferedTests();
}
Expand Down
21 changes: 21 additions & 0 deletions src/replay.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class Replay {
const testsWithoutRid = []; // For tests without rid (backward compatibility)
const envVars = {};
let runId = null;
const artifactsToAdd = []; // Store artifacts to add after all tests are loaded

// Parse debug file line by line
for (const [lineIndex, line] of lines.entries()) {
Expand Down Expand Up @@ -119,6 +120,11 @@ export class Replay {
// Handle tests without rid (no deduplication)
testsWithoutRid.push({ ...test });
}
} else if (logEntry.action === 'addArtifacts' && logEntry.artifacts) {
if (logEntry.runId && !runId) {
runId = logEntry.runId;
}
artifactsToAdd.push(...logEntry.artifacts);
} else if (logEntry.action === 'finishRun') {
finishParams = logEntry.params || {};
if (logEntry.runId && !runId) {
Expand All @@ -138,6 +144,21 @@ export class Replay {
this.onError(`${parseErrors - 3} more parse errors occurred`);
}

for (const artifact of artifactsToAdd) {
if (artifact.rid) {
const ridToFind = artifact.rid;
const fullRid = runId ? `${runId}-${ridToFind}` : ridToFind;

const test = testsMap.get(fullRid) || testsMap.get(ridToFind);
if (test && artifact.path) {
if (!test.files) test.files = [];
if (!test.files.includes(artifact.path)) {
test.files.push(artifact.path);
}
}
}
}

// Combine tests with rid and tests without rid
const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];

Expand Down
Loading