runner refactoring pass (#7975)

* rename iterations to iterationCount

* make duration 0

* use local storage

* raise orderedJSON

* rename

* fix cli
This commit is contained in:
Jack Kavanagh
2024-09-19 16:29:11 +02:00
committed by GitHub
parent d87398f237
commit df2bc063b4
11 changed files with 322 additions and 133 deletions

View File

@@ -6,9 +6,12 @@ import * as commander from 'commander';
import consola, { BasicReporter, FancyReporter, LogLevel, logType } from 'consola';
import { cosmiconfig } from 'cosmiconfig';
import fs from 'fs';
import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from 'insomnia/src/common/constants';
import { getSendRequestCallbackMemDb } from 'insomnia/src/common/send-request';
import { UserUploadEnvironment } from 'insomnia/src/models/environment';
import { type RequestTestResult } from 'insomnia-sdk';
import { generate, runTestsCli } from 'insomnia-testing';
import orderedJSON from 'json-order';
import { parseArgsStringToArgv } from 'string-argv';
import packageJson from '../package.json';
@@ -236,10 +239,13 @@ const readFileFromPathOrUrl = async (pathOrUrl: string) => {
}
return readFile(pathOrUrl, 'utf8');
};
const getIterationDataFromFileOrUrl = async (pathOrUrl: string): Promise<Record<string, string>[]> => {
const pathToIterationData = async (pathOrUrl: string): Promise<UserUploadEnvironment[]> => {
const fileType = pathOrUrl.split('.').pop()?.toLowerCase();
const content = await readFileFromPathOrUrl(pathOrUrl);
const list = getListFromFileOrUrl(content, fileType);
return transformIterationDataToEnvironmentList(list);
};
const getListFromFileOrUrl = (content: string, fileType?: string): Record<string, string>[] => {
if (fileType === 'json') {
try {
const jsonDataContent = JSON.parse(content);
@@ -267,6 +273,21 @@ const getIterationDataFromFileOrUrl = async (pathOrUrl: string): Promise<Record<
throw new Error(`Uploaded file is unsupported ${fileType}`);
};
const transformIterationDataToEnvironmentList = (list: Record<string, string>[]): UserUploadEnvironment[] => {
return list?.map(data => {
const orderedJson = orderedJSON.parse<Record<string, any>>(
JSON.stringify(data),
JSON_ORDER_PREFIX,
JSON_ORDER_SEPARATOR,
);
return {
name: 'User Upload',
data: orderedJson.object,
dataPropertyOrder: orderedJson.map || null,
};
});
};
export const go = (args?: string[]) => {
const program = new commander.Command();
@@ -470,7 +491,7 @@ export const go = (args?: string[]) => {
try {
const iterationCount = parseInt(options.iterationCount, 10);
const iterationData = options.iterationData ? await getIterationDataFromFileOrUrl(options.iterationData) : undefined;
const iterationData = options.iterationData ? await pathToIterationData(options.iterationData) : undefined;
const sendRequest = await getSendRequestCallbackMemDb(environment._id, db, { validateSSL: !options.disableCertValidation }, iterationData, iterationCount);
let success = true;
for (let i = 0; i < iterationCount; i++) {

View File

@@ -62,7 +62,7 @@ export async function buildRenderContext(
subEnvironment,
rootGlobalEnvironment,
subGlobalEnvironment,
userUploadEnv,
userUploadEnvironment,
baseContext = {},
}: {
ancestors?: RenderContextAncestor[];
@@ -70,7 +70,7 @@ export async function buildRenderContext(
subEnvironment?: Environment;
rootGlobalEnvironment?: Environment | null;
subGlobalEnvironment?: Environment | null;
userUploadEnv?: UserUploadEnvironment;
userUploadEnvironment?: UserUploadEnvironment;
baseContext?: Record<string, any>;
},
) {
@@ -130,10 +130,10 @@ export async function buildRenderContext(
}
// user upload env in collection runner has highest priority
if (userUploadEnv) {
if (userUploadEnvironment) {
const ordered = orderedJSON.order(
userUploadEnv.data,
userUploadEnv.dataPropertyOrder,
userUploadEnvironment.data,
userUploadEnvironment.dataPropertyOrder,
JSON_ORDER_SEPARATOR,
);
envObjects.push(ordered);
@@ -336,7 +336,7 @@ interface BaseRenderContextOptions {
baseEnvironment?: Environment;
rootGlobalEnvironment?: Environment;
subGlobalEnvironment?: Environment;
userUploadEnv?: UserUploadEnvironment;
userUploadEnvironment?: UserUploadEnvironment;
purpose?: RenderPurpose;
extraInfo?: ExtraRenderInfo;
ignoreUndefinedEnvVariable?: boolean;
@@ -350,7 +350,7 @@ export async function getRenderContext(
request,
environment,
baseEnvironment,
userUploadEnv,
userUploadEnvironment,
ancestors: _ancestors,
purpose,
extraInfo,
@@ -455,8 +455,8 @@ export async function getRenderContext(
}
// Get Keys from user upload environment
if (userUploadEnv) {
getKeySource(userUploadEnv.data || {}, inKey, userUploadEnv.name || 'uploadData');
if (userUploadEnvironment) {
getKeySource(userUploadEnvironment.data || {}, inKey, userUploadEnvironment.name || 'uploadData');
}
// Add meta data helper function
@@ -490,7 +490,7 @@ export async function getRenderContext(
subGlobalEnvironment,
rootEnvironment,
subEnvironment: subEnvironment || undefined,
userUploadEnv,
userUploadEnvironment,
baseContext,
});
}
@@ -556,7 +556,7 @@ export async function getRenderedRequestAndContext(
request,
environment,
baseEnvironment,
userUploadEnv,
userUploadEnvironment,
extraInfo,
purpose,
ignoreUndefinedEnvVariable,
@@ -566,7 +566,7 @@ export async function getRenderedRequestAndContext(
const workspace = ancestors.find(isWorkspace);
const parentId = workspace ? workspace._id : 'n/a';
const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId);
const renderContext = await getRenderContext({ request, environment, ancestors, purpose, extraInfo, baseEnvironment, userUploadEnv });
const renderContext = await getRenderContext({ request, environment, ancestors, purpose, extraInfo, baseEnvironment, userUploadEnvironment });
// HACK: Switch '#}' to '# }' to prevent Nunjucks from barfing
// https://github.com/kong/insomnia/issues/895

View File

@@ -1,4 +1,3 @@
import orderedJSON from 'json-order';
import path from 'path';
import { type BaseModel, types as modelTypes } from '../models';
@@ -19,7 +18,6 @@ import {
tryToInterpolateRequest,
} from '../network/network';
import { invariant } from '../utils/invariant';
import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from './constants';
import { database } from './database';
import { generateId } from './misc';
@@ -35,7 +33,7 @@ const wrapAroundIterationOverIterationData = (list?: UserUploadEnvironment[], cu
};
return list[(currentIteration + 1) % list.length];
};
export async function getSendRequestCallbackMemDb(environmentId: string, memDB: any, settingsOverrides?: SettingsOverride, iterationData?: Record<string, any>[], iterationCount?: number) {
export async function getSendRequestCallbackMemDb(environmentId: string, memDB: any, settingsOverrides?: SettingsOverride, iterationData?: UserUploadEnvironment[], iterationCount?: number) {
// Initialize the DB in-memory and fill it with data if we're given one
await database.init(
modelTypes(),
@@ -95,23 +93,12 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
return { request, settings, clientCertificates, caCert, environment, activeEnvironmentId, workspace, timelinePath, responseId, ancestors };
};
const userUploadEnvs = iterationData?.map(data => {
const orderedJson = orderedJSON.parse<Record<string, any>>(
JSON.stringify(data),
JSON_ORDER_PREFIX,
JSON_ORDER_SEPARATOR,
);
return {
name: 'User Upload',
data: orderedJson.object,
dataPropertyOrder: orderedJson.map || null,
};
});
// Return callback helper to send requests
return async function sendRequest(requestId: string, iteration?: number) {
const requestData = await fetchInsoRequestData(requestId, environmentId);
const mutatedContext = await tryToExecutePreRequestScript(requestData, requestData.workspace._id, wrapAroundIterationOverIterationData(userUploadEnvs, iteration), iteration, iterationCount);
const getCurrentRowOfIterationData = wrapAroundIterationOverIterationData(iterationData, iteration);
const mutatedContext = await tryToExecutePreRequestScript(requestData, requestData.workspace._id, getCurrentRowOfIterationData, iteration, iterationCount);
if (mutatedContext === null) {
console.error('Time out while executing pre-request script');
return null;
@@ -125,7 +112,7 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB:
purpose: 'send',
extraInfo: undefined,
baseEnvironment: mutatedContext.baseEnvironment,
userUploadEnv: mutatedContext.userUploadEnv,
userUploadEnvironment: mutatedContext.userUploadEnvironment,
ignoreUndefinedEnvVariable,
});
// skip plugins

View File

@@ -20,6 +20,7 @@ export interface BaseEnvironment {
}
export type Environment = BaseModel & BaseEnvironment;
// This is a representation of the data taken from a csv or json file AKA iterationData
export type UserUploadEnvironment = Pick<Environment, 'data' | 'dataPropertyOrder' | 'name'>;
export const isEnvironment = (model: Pick<BaseModel, 'type'>): model is Environment => (

View File

@@ -19,7 +19,6 @@ export interface RunnerResultPerRequest {
requestName: string;
requestUrl: string;
responseCode: number;
// TODO: add request name, url, etc
}
export interface ResponseInfo {
@@ -28,15 +27,16 @@ export interface ResponseInfo {
originalRequestId: string;
}
export type RunnerResultPerRequestPerIteration = RunnerResultPerRequest[][];
export interface BaseRunnerTestResult {
source: RunnerSource;
// environmentId: string;
iterations: number;
duration: number; // millisecond
avgRespTime: number; // millisecond
iterationResults: RunnerResultPerRequest[][];
iterationResults: RunnerResultPerRequestPerIteration;
responsesInfo: ResponseInfo[];
version: '1';
version: '1'; // We might want to add or remove result features in future
}
export type RunnerTestResult = BaseModel & BaseRunnerTestResult;
@@ -48,7 +48,6 @@ export const isRunnerTestResult = (model: Pick<BaseModel, 'type'>): model is Run
export function init() {
return {
source: 'runner',
// environmentId: string;
iterations: 0,
duration: 0,
avgRespTime: 0,

View File

@@ -38,7 +38,7 @@ export interface TransformedExecuteScriptContext {
globals?: Environment;
cookieJar: CookieJar;
requestTestResults?: RequestTestResult[];
userUploadEnv?: UserUploadEnvironment;
userUploadEnvironment?: UserUploadEnvironment;
}
interface Task {

View File

@@ -155,7 +155,7 @@ export const tryToExecutePreRequestScript = async (
ancestors,
}: Awaited<ReturnType<typeof fetchRequestData>>,
workspaceId: string,
userUploadEnv?: UserUploadEnvironment,
userUploadEnvironment?: UserUploadEnvironment,
iteration?: number,
iterationCount?: number,
) => {
@@ -185,7 +185,7 @@ export const tryToExecutePreRequestScript = async (
settings,
cookieJar,
globals: activeGlobalEnvironment,
userUploadEnv,
userUploadEnvironment,
requestTestResults: new Array<RequestTestResult>(),
};
}
@@ -201,7 +201,7 @@ export const tryToExecutePreRequestScript = async (
clientCertificates,
cookieJar,
globals: activeGlobalEnvironment,
userUploadEnv,
userUploadEnvironment,
iteration,
iterationCount,
ancestors,
@@ -231,7 +231,7 @@ export const tryToExecutePreRequestScript = async (
globals: mutatedContext.globals,
cookieJar: mutatedContext.cookieJar,
requestTestResults: mutatedContext.requestTestResults,
userUploadEnv: mutatedContext.userUploadEnv,
userUploadEnvironment: mutatedContext.userUploadEnvironment,
execution: mutatedContext.execution,
};
};
@@ -294,7 +294,7 @@ export async function savePatchesMadeByScript(
}
export const tryToExecuteScript = async (context: RequestAndContextAndOptionalResponse) => {
const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response, globals, userUploadEnv, iteration, iterationCount, ancestors, eventName } = context;
const { script, request, environment, timelinePath, responseId, baseEnvironment, clientCertificates, cookieJar, response, globals, userUploadEnvironment, iteration, iterationCount, ancestors, eventName } = context;
invariant(script, 'script must be provided');
const settings = await models.settings.get();
@@ -334,9 +334,9 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
},
response,
globals: globals?.data || undefined,
iterationData: userUploadEnv ? {
name: userUploadEnv.name,
data: userUploadEnv.data || {},
iterationData: userUploadEnvironment ? {
name: userUploadEnvironment.name,
data: userUploadEnvironment.data || {},
} : undefined,
execution: {
location: requestLocation,
@@ -374,14 +374,14 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
globals.dataPropertyOrder = globalEnvPropertyOrder.map;
}
if (userUploadEnv) {
if (userUploadEnvironment) {
const userUploadEnvPropertyOrder = orderedJSON.parse(
JSON.stringify(output?.iterationData?.data || {}),
JSON_ORDER_PREFIX,
JSON_ORDER_SEPARATOR,
);
userUploadEnv.data = output?.iterationData?.data || {};
userUploadEnv.dataPropertyOrder = userUploadEnvPropertyOrder.map;
userUploadEnvironment.data = output?.iterationData?.data || {};
userUploadEnvironment.dataPropertyOrder = userUploadEnvPropertyOrder.map;
}
return {
@@ -392,7 +392,7 @@ export const tryToExecuteScript = async (context: RequestAndContextAndOptionalRe
clientCertificates: output.clientCertificates,
cookieJar: output.cookieJar,
globals,
userUploadEnv,
userUploadEnvironment,
requestTestResults: output.requestTestResults,
execution: output.execution,
};
@@ -440,7 +440,7 @@ type RequestAndContextAndResponse = RequestContextForScript & {
type RequestAndContextAndOptionalResponse = RequestContextForScript & {
script: string;
response?: sendCurlAndWriteTimelineError | sendCurlAndWriteTimelineResponse;
userUploadEnv?: UserUploadEnvironment;
userUploadEnvironment?: UserUploadEnvironment;
iteration?: number;
iterationCount?: number;
eventName?: RequestContext['requestInfo']['eventName'];
@@ -488,7 +488,7 @@ export const tryToInterpolateRequest = async ({
purpose,
extraInfo,
baseEnvironment,
userUploadEnv,
userUploadEnvironment,
ignoreUndefinedEnvVariable,
}: {
request: Request;
@@ -496,7 +496,7 @@ export const tryToInterpolateRequest = async ({
purpose?: RenderPurpose;
extraInfo?: ExtraRenderInfo;
baseEnvironment?: Environment;
userUploadEnv?: UserUploadEnvironment;
userUploadEnvironment?: UserUploadEnvironment;
ignoreUndefinedEnvVariable?: boolean;
}
) => {
@@ -505,7 +505,7 @@ export const tryToInterpolateRequest = async ({
request: request,
environment,
baseEnvironment,
userUploadEnv,
userUploadEnvironment,
purpose,
extraInfo,
ignoreUndefinedEnvVariable,

View File

@@ -6,14 +6,14 @@ import type { WorkspaceLoaderData } from '../../routes/workspace';
import { CopyButton } from '../base/copy-button';
import { Icon } from '../icon';
export const CLIPreviewModal = ({ onClose, requestIds, allSelected, iterations, delay, filePath }: { onClose: () => void; requestIds: string[]; allSelected: boolean; iterations: number; delay: number; filePath: string }) => {
export const CLIPreviewModal = ({ onClose, requestIds, allSelected, iterationCount, delay, filePath }: { onClose: () => void; requestIds: string[]; allSelected: boolean; iterationCount: number; delay: number; filePath: string }) => {
const { workspaceId } = useParams() as { workspaceId: string };
const { activeEnvironment } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const workspaceIdOrRequestIds = allSelected ? workspaceId.slice(0, 10) : '-i ' + requestIds.join(' -i ');
const iterationsArgument = iterations > 1 ? ` -n ${iterations}` : '';
const iterationCountArgument = iterationCount > 1 ? ` -n ${iterationCount}` : '';
const delayArgument = delay > 0 ? ` --delay-request ${delay}` : '';
const iterationFilePath = filePath ? ` -d "${filePath}"` : '';
const cliCommand = `inso run collection ${workspaceIdOrRequestIds} -e ${activeEnvironment._id.slice(0, 10)}${iterationsArgument}${delayArgument}${iterationFilePath}`;
const cliCommand = `inso run collection ${workspaceIdOrRequestIds} -e ${activeEnvironment._id.slice(0, 10)}${iterationCountArgument}${delayArgument}${iterationFilePath}`;
return (
<ModalOverlay

View File

@@ -0,0 +1,214 @@
// Taken from https://github.com/astoilkov/use-local-storage-state/blob/main/src/useLocalStorageState.ts
import type { Dispatch, SetStateAction } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
// in memory fallback used when `localStorage` throws an error
export const inMemoryData = new Map<string, unknown>();
export interface LocalStorageOptions<T> {
defaultValue?: T | (() => T);
storageSync?: boolean;
serializer?: {
stringify: (value: unknown) => string;
parse: (value: string) => unknown;
};
};
// - `useLocalStorageState()` return type
// - first two values are the same as `useState`
export type LocalStorageState<T> = [
T,
Dispatch<SetStateAction<T>>,
{
isPersistent: boolean;
removeItem: () => void;
},
];
export default function useLocalStorageState(
key: string,
options?: LocalStorageOptions<undefined>,
): LocalStorageState<unknown>;
export default function useLocalStorageState<T>(
key: string,
options?: Omit<LocalStorageOptions<T | undefined>, 'defaultValue'>,
): LocalStorageState<T | undefined>;
export default function useLocalStorageState<T>(
key: string,
options?: LocalStorageOptions<T>,
): LocalStorageState<T>;
export default function useLocalStorageState<T = undefined>(
key: string,
options?: LocalStorageOptions<T | undefined>,
): LocalStorageState<T | undefined> {
const serializer = options?.serializer;
const [defaultValue] = useState(options?.defaultValue);
return useLocalStorage(
key,
defaultValue,
options?.storageSync,
serializer?.parse,
serializer?.stringify,
);
}
function useLocalStorage<T>(
key: string,
defaultValue: T | undefined,
storageSync: boolean = true,
parse: (value: string) => unknown = parseJSON,
stringify: (value: unknown) => string = JSON.stringify,
): LocalStorageState<T | undefined> {
// we keep the `parsed` value in a ref because `useSyncExternalStore` requires a cached version
const storageItem = useRef<{ string: string | null; parsed: T | undefined }>({
string: null,
parsed: undefined,
});
const value = useSyncExternalStore(
// useSyncExternalStore.subscribe
useCallback(
onStoreChange => {
const onChange = (localKey: string): void => {
if (key === localKey) {
onStoreChange();
}
};
callbacks.add(onChange);
return (): void => {
callbacks.delete(onChange);
};
},
[key],
),
// useSyncExternalStore.getSnapshot
() => {
const string = goodTry(() => localStorage.getItem(key)) ?? null;
if (inMemoryData.has(key)) {
storageItem.current.parsed = inMemoryData.get(key) as T | undefined;
} else if (string !== storageItem.current.string) {
let parsed: T | undefined;
try {
parsed = string === null ? defaultValue : (parse(string) as T);
} catch {
parsed = defaultValue;
}
storageItem.current.parsed = parsed;
}
storageItem.current.string = string;
// store default value in localStorage:
// - initial issue: https://github.com/astoilkov/use-local-storage-state/issues/26
// issues that were caused by incorrect initial and secondary implementations:
// - https://github.com/astoilkov/use-local-storage-state/issues/30
// - https://github.com/astoilkov/use-local-storage-state/issues/33
if (defaultValue !== undefined && string === null) {
// reasons for `localStorage` to throw an error:
// - maximum quota is exceeded
// - under Mobile Safari (since iOS 5) when the user enters private mode
// `localStorage.setItem()` will throw
// - trying to access localStorage object when cookies are disabled in Safari throws
// "SecurityError: The operation is insecure."
// eslint-disable-next-line no-console
goodTry(() => {
const string = stringify(defaultValue);
localStorage.setItem(key, string);
storageItem.current = { string, parsed: defaultValue };
});
}
return storageItem.current.parsed;
},
// useSyncExternalStore.getServerSnapshot
() => defaultValue,
);
const setState = useCallback(
(newValue: SetStateAction<T | undefined>): void => {
const value =
newValue instanceof Function ? newValue(storageItem.current.parsed) : newValue;
// reasons for `localStorage` to throw an error:
// - maximum quota is exceeded
// - under Mobile Safari (since iOS 5) when the user enters private mode
// `localStorage.setItem()` will throw
// - trying to access `localStorage` object when cookies are disabled in Safari throws
// "SecurityError: The operation is insecure."
try {
localStorage.setItem(key, stringify(value));
inMemoryData.delete(key);
} catch {
inMemoryData.set(key, value);
}
triggerCallbacks(key);
},
[key, stringify],
);
const removeItem = useCallback(() => {
goodTry(() => localStorage.removeItem(key));
inMemoryData.delete(key);
triggerCallbacks(key);
}, [key]);
// - syncs change across tabs, windows, iframes
// - the `storage` event is called only in all tabs, windows, iframe's except the one that
// triggered the change
useEffect(() => {
if (!storageSync) {
return undefined;
}
const onStorage = (e: StorageEvent): void => {
if (e.key === key && e.storageArea === goodTry(() => localStorage)) {
triggerCallbacks(key);
}
};
window.addEventListener('storage', onStorage);
return (): void => window.removeEventListener('storage', onStorage);
}, [key, storageSync]);
return useMemo(
() => [
value,
setState,
{
isPersistent: value === defaultValue || !inMemoryData.has(key),
removeItem,
},
],
[key, setState, value, defaultValue, removeItem],
);
}
// notifies all instances using the same `key` to update
const callbacks = new Set<(key: string) => void>();
function triggerCallbacks(key: string): void {
for (const callback of [...callbacks]) {
callback(key);
}
}
// a wrapper for `JSON.parse()` that supports "undefined" value. otherwise,
// `JSON.parse(JSON.stringify(undefined))` returns the string "undefined" not the value `undefined`
function parseJSON(value: string): unknown {
return value === 'undefined' ? undefined : JSON.parse(value);
}
function goodTry<T>(tryFn: () => T): T | undefined {
try {
return tryFn();
} catch {
return undefined;
}
}

View File

@@ -26,6 +26,7 @@ import { getPathParametersFromUrl, isEventStreamRequest, isRequest, type Request
import { isRequestMeta, type RequestMeta } from '../../models/request-meta';
import type { RequestVersion } from '../../models/request-version';
import type { Response } from '../../models/response';
import type { ResponseInfo, RunnerResultPerRequestPerIteration } from '../../models/runner-test-result';
import { isWebSocketRequest, isWebSocketRequestId, type WebSocketRequest } from '../../models/websocket-request';
import type { WebSocketResponse } from '../../models/websocket-response';
import { getAuthHeader } from '../../network/authentication';
@@ -405,13 +406,14 @@ export type RunnerSource = 'runner';
export interface CollectionRunnerContext {
source: RunnerSource;
environmentId: string;
iterations: number;
iterationCount: number;
iterationData: object;
duration: number; // millisecond
testCount: number;
avgRespTime: number; // millisecond
results: RequestTestResult[];
iterationResults: RunnerResultPerRequestPerIteration;
done: boolean;
responsesInfo: ResponseInfo[];
}
export interface RunnerContextForRequest {
@@ -428,7 +430,7 @@ export interface RunnerContextForRequest {
export const sendActionImp = async ({
requestId,
workspaceId,
userUploadEnv,
userUploadEnvironment,
shouldPromptForPathAfterResponse,
ignoreUndefinedEnvVariable,
testResultCollector,
@@ -442,12 +444,12 @@ export const sendActionImp = async ({
testResultCollector?: RunnerContextForRequest;
iteration?: number;
iterationCount?: number;
userUploadEnv?: UserUploadEnvironment;
userUploadEnvironment?: UserUploadEnvironment;
}) => {
window.main.startExecution({ requestId });
const requestData = await fetchRequestData(requestId);
window.main.addExecutionStep({ requestId, stepName: 'Executing pre-request script' });
const mutatedContext = await tryToExecutePreRequestScript(requestData, workspaceId, userUploadEnv, iteration, iterationCount);
const mutatedContext = await tryToExecutePreRequestScript(requestData, workspaceId, userUploadEnvironment, iteration, iterationCount);
if ('error' in mutatedContext) {
throw {
error: mutatedContext.error,
@@ -484,7 +486,7 @@ export const sendActionImp = async ({
purpose: 'send',
extraInfo: undefined,
baseEnvironment: mutatedContext.baseEnvironment,
userUploadEnv: mutatedContext.userUploadEnv,
userUploadEnvironment: mutatedContext.userUploadEnvironment,
ignoreUndefinedEnvVariable,
});
const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult);

View File

@@ -1,4 +1,4 @@
import type { RequestContext, RequestTestResult } from 'insomnia-sdk';
import type { RequestContext } from 'insomnia-sdk';
import porderedJSON from 'json-order';
import React, { type FC, useCallback, useEffect, useMemo, useState } from 'react';
import { Button, Checkbox, DropIndicator, GridList, GridListItem, type GridListItemProps, Heading, type Key, Tab, TabList, TabPanel, Tabs, Toolbar, TooltipTrigger, useDragAndDrop } from 'react-aria-components';
@@ -15,7 +15,7 @@ import * as models from '../../models';
import type { UserUploadEnvironment } from '../../models/environment';
import { isRequest, type Request } from '../../models/request';
import { isRequestGroup } from '../../models/request-group';
import type { ResponseInfo, RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result';
import type { RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result';
import { cancelRequestById } from '../../network/cancellation';
import { invariant } from '../../utils/invariant';
import { SegmentEvent } from '../analytics';
@@ -32,8 +32,9 @@ import { RunnerTestResultPane } from '../components/panes/runner-test-result-pan
import { ResponseTimer } from '../components/response-timer';
import { getTimeAndUnit } from '../components/tags/time-tag';
import { ResponseTimelineViewer } from '../components/viewers/response-timeline-viewer';
import useLocalStorage from '../hooks/use-local-storage';
import type { OrganizationLoaderData } from './organization';
import { type RunnerSource, sendActionImp } from './request';
import { type CollectionRunnerContext, type RunnerContextForRequest, type RunnerSource, sendActionImp } from './request';
import { useRootLoaderData } from './root';
import type { Child, WorkspaceLoaderData } from './workspace';
@@ -86,21 +87,6 @@ async function aggregateAllTimelines(errorMsg: string | null, testResult: Runner
return timelines;
}
interface RunnerSettings {
iterations: number;
delay: number;
iterationData: UploadDataType[];
file: File | null;
}
// TODO: remove this when the suite management is introduced
let tempRunnerSettings: RunnerSettings = {
iterations: 1,
delay: 0,
iterationData: [],
file: null,
};
export const Runner: FC<{}> = () => {
const [searchParams, setSearchParams] = useSearchParams();
const [shouldRefresh, setShouldRefresh] = useState(false);
@@ -133,17 +119,18 @@ export const Runner: FC<{}> = () => {
setSearchParams({});
}
const [iterations, setIterations] = useState(tempRunnerSettings?.iterations || 1);
const [delay, setDelay] = useState(tempRunnerSettings?.delay || 0);
const [uploadData, setUploadData] = useState<UploadDataType[]>(tempRunnerSettings?.iterationData || []);
const [file, setFile] = useState<File | null>(tempRunnerSettings?.file || null);
const { organizationId, projectId, workspaceId } = useParams() as {
organizationId: string;
projectId: string;
workspaceId: string;
direction: 'vertical' | 'horizontal';
};
const localStorageKey = workspaceId + 'runnerSettings';
const [iterationCount, setIterationCount] = useLocalStorage<number>(localStorageKey + 'iterationCount', { defaultValue: 1 });
const [delay, setDelay] = useLocalStorage<number>(localStorageKey + 'delay', { defaultValue: 0 });
const [uploadData, setUploadData] = useLocalStorage<UploadDataType[]>(localStorageKey + 'iterationData', { defaultValue: [] });
const [file, setFile] = useLocalStorage<File | null>(localStorageKey + 'file', { defaultValue: null });
invariant(iterationCount, 'iterationCount should not be null');
const { settings } = useRootLoaderData();
const { collection } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const [showUploadModal, setShowUploadModal] = useState(false);
@@ -251,7 +238,7 @@ export const Runner: FC<{}> = () => {
}
setIsRunning(true);
window.main.trackSegmentEvent({ event: SegmentEvent.collectionRunExecute, properties: { plan: currentPlan?.type || 'scratchpad', iterations: iterations } });
window.main.trackSegmentEvent({ event: SegmentEvent.collectionRunExecute, properties: { plan: currentPlan?.type || 'scratchpad', iterations: iterationCount } });
const selected = new Set(reqList.selectedKeys);
const requests = Array.from(reqList.items)
.filter(item => selected.has(item.id));
@@ -272,7 +259,7 @@ export const Runner: FC<{}> = () => {
submit(
{
requests,
iterations,
iterationCount,
userUploadEnvs,
delay,
},
@@ -309,14 +296,9 @@ export const Runner: FC<{}> = () => {
useEffect(() => {
if (uploadData.length >= 1) {
// update iteration number from upload data length
setIterations(uploadData.length); // also update the temp settings
tempRunnerSettings = {
...tempRunnerSettings,
iterations: uploadData.length,
};
setIterationCount(uploadData.length);
}
}, [uploadData]);
}, [setIterationCount, uploadData]);
const [isRunning, setIsRunning] = useState(false);
const [timingSteps, setTimingSteps] = useState<TimingStep[]>([]);
@@ -428,22 +410,15 @@ export const Runner: FC<{}> = () => {
<div className="h-full min-w-[500px]">
<span className="mr-6 text-sm">
<input
value={iterations}
value={iterationCount}
name='Iterations'
disabled={isRunning}
onChange={e => {
try {
const iterCount = parseInt(e.target.value, 10);
if (iterCount > 0) {
setIterations(iterCount); // also update the temp settings
tempRunnerSettings = {
...tempRunnerSettings,
iterations: iterCount,
};
if (parseInt(e.target.value, 10) > 0) {
setIterationCount(parseInt(e.target.value, 10));
}
} catch (ex) {
// no op
}
} catch (ex) { }
}}
type='number'
className={iterationInputStyle}
@@ -460,10 +435,6 @@ export const Runner: FC<{}> = () => {
const delay = parseInt(e.target.value, 10);
if (delay >= 0) {
setDelay(delay); // also update the temp settings
tempRunnerSettings = {
...tempRunnerSettings,
delay,
};
}
} catch (ex) {
// no op
@@ -682,7 +653,7 @@ export const Runner: FC<{}> = () => {
onClose={() => setShowCLIModal(false)}
requestIds={Array.from(reqList.selectedKeys) as string[]}
allSelected={Array.from(reqList.selectedKeys).length === Array.from(reqList.items).length}
iterations={iterations}
iterationCount={iterationCount}
delay={delay}
filePath={file?.path || ''}
/>
@@ -692,11 +663,6 @@ export const Runner: FC<{}> = () => {
onUploadFile={(file, uploadData) => {
setFile(file);
setUploadData(uploadData); // also update the temp settings
tempRunnerSettings = {
...tempRunnerSettings,
iterationData: uploadData,
file,
};
}}
userUploadData={uploadData}
onClose={() => setShowUploadModal(false)}
@@ -860,20 +826,20 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
invariant(organizationId, 'Organization id is required');
invariant(projectId, 'Project id is required');
invariant(workspaceId, 'Workspace id is required');
const { requests, iterations, delay, userUploadEnvs } = await request.json();
const { requests, iterationCount, delay, userUploadEnvs } = await request.json();
const source: RunnerSource = 'runner';
let testCtx = {
let testCtx: CollectionRunnerContext = {
source,
environmentId: '',
iterations,
iterationCount,
iterationData: userUploadEnvs,
duration: 1, // TODO: disable this
duration: 0,
testCount: 0,
avgRespTime: 0,
iterationResults: new Array<RunnerResultPerRequest[]>(),
iterationResults: [],
done: false,
responsesInfo: new Array<ResponseInfo>(),
responsesInfo: [],
};
window.main.startExecution({ requestId: workspaceId });
@@ -890,11 +856,11 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
};
try {
for (let i = 0; i < iterations; i++) {
for (let i = 0; i < iterationCount; i++) {
// nextRequestIdOrName is used to manual set next request in iteration from pre-request script
let nextRequestIdOrName = '';
let iterationResults: RunnerResultPerRequest[] = [];
let testResultsForOneIteration: RunnerResultPerRequest[] = [];
for (let j = 0; j < requests.length; j++) {
const targetRequest = requests[j] as RequestType;
@@ -925,22 +891,22 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
invariant(activeRequestMeta, 'Request meta not found');
await new Promise(resolve => setTimeout(resolve, delay));
const resultCollector = {
const resultCollector: RunnerContextForRequest = {
requestId: targetRequest.id,
requestName: targetRequest.name,
requestUrl: targetRequest.url,
responseReason: '',
duration: 1,
duration: 0,
size: 0,
results: new Array<RequestTestResult>(),
results: [],
responseId: '',
};
const mutatedContext = await sendActionImp({
requestId: targetRequest.id,
workspaceId,
iteration: i + 1,
iterationCount: iterations,
userUploadEnv: wrapAroundIterationOverIterationData(userUploadEnvs, i),
iterationCount,
userUploadEnvironment: wrapAroundIterationOverIterationData(userUploadEnvs, i),
shouldPromptForPathAfterResponse: false,
ignoreUndefinedEnvVariable: true,
testResultCollector: resultCollector,
@@ -956,7 +922,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
results: resultCollector.results,
};
iterationResults = [...iterationResults, requestResults];
testResultsForOneIteration = [...testResultsForOneIteration, requestResults];
testCtx = {
...testCtx,
duration: testCtx.duration + resultCollector.duration,
@@ -973,7 +939,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
testCtx = {
...testCtx,
iterationResults: [...testCtx.iterationResults, iterationResults],
iterationResults: [...testCtx.iterationResults, testResultsForOneIteration],
};
}
@@ -989,8 +955,7 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) =
await models.runnerTestResult.create({
parentId: workspaceId,
source: testCtx.source,
// environmentId: string;
iterations: testCtx.iterations,
iterations: testCtx.iterationCount,
duration: testCtx.duration,
avgRespTime: testCtx.avgRespTime,
iterationResults: testCtx.iterationResults,