Skip to content
Merged
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
4 changes: 4 additions & 0 deletions lib/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const TO_FINISH_WITH_OPTIONS: ToFinishWithOptionsWithDefaults = {
*/
export const DEFAULT_TEST_RUN_DURATION_MS = 60 * 60 * 1000; // 1 hour

// Prevent orphaned runs - timeout the Actor 1 minute before the test does so everything is logged correctly.
// - Otherwise the run link URL can get lost.
export const DEFAULT_TEST_ACTOR_TIMEOUT_SECS = DEFAULT_TEST_RUN_DURATION_MS / 1000 - 60;

/**
* Delay before checking the dataset and statistics after a run finishes to resolve eventual consistency.
* - This value should ensure that the dataset and statistics are fully updated before any assertions are made.
Expand Down
38 changes: 20 additions & 18 deletions lib/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ApifyClient } from 'apify-client';
import type { SuiteFactory, TestContext, TestFunction } from 'vitest';
import { describe as vitestDescribe, ExpectStatic, test as vitestTest } from 'vitest';

import { DATASET_SYNC_DELAY_MS, DEFAULT_TEST_RUN_DURATION_MS } from './consts.js';
import { DATASET_SYNC_DELAY_MS, DEFAULT_TEST_ACTOR_TIMEOUT_SECS, DEFAULT_TEST_RUN_DURATION_MS } from './consts.js';
import { extendExpect } from './extend-expect.js';
import { RunTestResult } from './run-test-result.js';
import type { ActorBuild, ActorTestOptions, RunOptions } from './types.js';
Expand Down Expand Up @@ -39,6 +39,7 @@ const DEFAULT_TEST_OPTIONS: ActorTestOptions = {
concurrent: true,
// test should finish within 1 hour
timeout: DEFAULT_TEST_RUN_DURATION_MS,
retry: 1,
};

/**
Expand All @@ -51,12 +52,6 @@ export const describe = (name: string, fn?: SuiteFactory<object>, options: Actor
vitestDescribe.runIf(!!TESTER_APIFY_TOKEN || !!RUN_ALL_PLATFORM_TESTS)(name, options, fn);
};

const DEFAULT_TEST_ACTOR_OPTIONS: ActorTestOptions = {
retry: 1,
// prevent orphaned runs
timeout: DEFAULT_TEST_RUN_DURATION_MS,
};

/**
* @param actorId - The actor's raw platform ID or its full name (`owner/name`, e.g. `"apify/web-scraper"`).
*/
Expand All @@ -66,10 +61,8 @@ export const testActor = <T>(
fn: TestFunction<{ run: ReturnType<typeof createStartRunFn<T>> }>,
testOptions?: ActorTestOptions,
) => {
const options = {
...DEFAULT_TEST_ACTOR_OPTIONS,
...testOptions,
};
const options = { ...DEFAULT_TEST_OPTIONS, ...testOptions };

const name = `${actorId}: ${testName}`;
// `RUN_ALL_PLATFORM_TESTS` is needed for the scheduled tests, which have no `ACTOR_BUILDS` to match the
// tests against - without it, every test would be filtered out as an actor we didn't build.
Expand Down Expand Up @@ -100,10 +93,8 @@ export const testStandbyActor = <I = any, O = any>(
fn: TestFunction<{ callStandby: ReturnType<typeof createStartStandbyFn<I, O>> }>,
testOptions?: ActorTestOptions,
) => {
const options = {
...DEFAULT_TEST_ACTOR_OPTIONS,
...testOptions,
};
const options = { ...DEFAULT_TEST_OPTIONS, ...testOptions };

const name = `${actorId}: ${testName}`;
// `RUN_ALL_PLATFORM_TESTS` is needed for the scheduled tests, which have no `ACTOR_BUILDS` to match the
// tests against - without it, every test would be filtered out as an actor we didn't build.
Expand Down Expand Up @@ -279,13 +270,20 @@ const createStartRunFn = <T>(actorId: string, testContext: TestContext) => {
return new RunTestResult(apifyClient, run);
}

const actor = apifyClient.actor(actorId);

const actorInput = {
...(prefilledInput && (await getActorPrefilledInput(apifyClient, actorId, buildId))),
...input,
};
const run = await actor.call(actorInput, { build, log: null, ...options });

const actor = apifyClient.actor(actorId);
const actorInfo = await actor.get();
const timeout = Math.min(
actorInfo?.defaultRunOptions?.timeoutSecs ?? DEFAULT_TEST_ACTOR_TIMEOUT_SECS,
DEFAULT_TEST_ACTOR_TIMEOUT_SECS,
);

const actorOptions = { timeout, build, log: null, ...options };
const run = await actor.call(actorInput, actorOptions);

const runLink = generateRunLink(run);
await annotate(`${task.name} - ${runLink}`, 'run_link');
Expand All @@ -306,3 +304,7 @@ const createStartRunFn = <T>(actorId: string, testContext: TestContext) => {
const generateRunLink = (run: ActorRun | ActorRunListItem): string => {
return `https://console.apify.com/view/runs/${run.id}`;
};

/** Used for unit testing */
// eslint-disable-next-line no-underscore-dangle
export const _private = { createStartRunFn } as const;
14 changes: 11 additions & 3 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ export type ActorBuild = {

export type RunOptions<T> = {
input: Omit<T, 'actorName'>;
options?: ActorCallOptions;
options?: Omit<ActorCallOptions, 'timeout'> & {
/**
* Timeout for the actor run in seconds. Zero value means there is no timeout.
* - If `undefined`, the run uses timeout of the default Actor run configuration.
*
* @default 3540 // 59 minutes (finish the run before the test)
*/
timeout?: number;
};
prefilledInput?: boolean;
/**
* If you specify `runId`, all the other options will be ignored and this run's data will
Expand Down Expand Up @@ -138,8 +146,8 @@ export type ActorTestOptions = Omit<TestOptions, 'retry' | 'timeout'> & {
// we are just extending the docs here to replace the default value, otherwise it's the exact same
retry?: TestOptions['retry'];
/**
* Timeout for the actor run in milliseconds. Zero value means there is no timeout.
* - If `undefined`, the run uses timeout of the default Actor run configuration.
* Timeout for the test in milliseconds. Zero value means there is no timeout.
* - If `undefined`, the run uses timeout of the default test configuration.
*
* @default 60 * 60 * 1000 // 1 hour
*/
Expand Down
53 changes: 53 additions & 0 deletions test/unit/lib.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Actor, ActorClient, ActorRun } from 'apify-client';
import { ApifyClient } from 'apify-client';
import type { TestContext } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { DEFAULT_TEST_ACTOR_TIMEOUT_SECS } from '../../lib/consts.js';
import { _private } from '../../lib/lib.js';
import * as UtilsModule from '../../lib/utils.js';

const { createStartRunFn } = _private;

describe('createStartRunFn()', () => {
const actorCallMock = vi.fn(async () => Promise.resolve({ id: 'fake-run-id' } as ActorRun));
const testContext = { task: {}, annotate: vi.fn() as TestContext['annotate'] } as TestContext;

vi.spyOn(UtilsModule, 'sleep').mockResolvedValue(undefined);

beforeEach(vi.clearAllMocks);

it('should apply default timeout to actor runs', async () => {
// Arrange

vi.spyOn(ApifyClient.prototype, 'actor').mockReturnValue({
call: actorCallMock as ActorClient['call'],
get: vi.fn(async () => ({ defaultRunOptions: {} }) as Actor) as ActorClient['get'],
} as ActorClient);

// Act
await createStartRunFn('123', testContext)({ input: {} });

// Assert
expect(actorCallMock).toHaveBeenCalledTimes(1);
expect(actorCallMock).toHaveBeenCalledWith(
{},
{ build: undefined, log: null, timeout: DEFAULT_TEST_ACTOR_TIMEOUT_SECS },
);
});

it('should respect the default actor run timeout if less', async () => {
// Arrange
vi.spyOn(ApifyClient.prototype, 'actor').mockReturnValue({
call: actorCallMock as ActorClient['call'],
get: vi.fn(async () => ({ defaultRunOptions: { timeoutSecs: 60 } }) as Actor) as ActorClient['get'],
} as ActorClient);

// Act
await createStartRunFn('123', testContext)({ input: {} });

// Assert
expect(actorCallMock).toHaveBeenCalledTimes(1);
expect(actorCallMock).toHaveBeenCalledWith({}, { build: undefined, log: null, timeout: 60 });
});
});
Loading