Skip to content
Closed
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
1 change: 1 addition & 0 deletions sdk/test-utils/recorder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export {
export { env } from "./utils/env";
export { delay } from "./utils/delay";
export { CustomMatcherOptions } from "./matcher";
export { TestInfo, MochaTest, VitestTestContext } from "./testInfo";
62 changes: 52 additions & 10 deletions sdk/test-utils/recorder/src/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import {
RecorderStartOptions,
RecordingStateManager,
} from "./utils/utils";
import { Test } from "mocha";
import { assetsJsonPath, sessionFilePath } from "./utils/sessionFilePath";
import { assetsJsonPath, sessionFilePath, TestContext } from "./utils/sessionFilePath";
import { SanitizerOptions } from "./utils/utils";
import { paths } from "./utils/paths";
import { addSanitizers, transformsInfo } from "./sanitizer";
Expand All @@ -35,6 +34,45 @@ import { isBrowser, isNode } from "@azure/core-util";
import { env } from "./utils/env";
import { decodeBase64 } from "./utils/encoding";
import { AdditionalPolicyConfig } from "@azure/core-client";
import { isMochaTest, isVitestTestContext, TestInfo, VitestSuite } from "./testInfo";

/**
* Caculates session file path and JSON assets path from test context
*
* @internal
*/
export function calculatePaths(testContext: TestInfo): TestContext {
if (isMochaTest(testContext)) {
if (!testContext.parent) {
throw new RecorderError(
`The parent of test '${testContext.title}' is undefined, so a file path for its recording could not be generated. Please place the test inside a describe block.`,
);
}
return {
suiteTitle: testContext.parent.fullTitle(),
testTitle: testContext.title,
};
} else if (isVitestTestContext(testContext)) {
if (!testContext.task.name || !testContext.task.suite.name) {
throw new RecorderError(
`Unable to determine the recording file path. Unexpected empty Vitest context`,
);
}
const suites: string[] = [];
let p: VitestSuite | undefined = testContext.task.suite;
while (p?.name) {
suites.push(p.name);
p = p.suite;
}

return {
suiteTitle: suites.reverse().join("_"),
testTitle: testContext.task.name,
};
} else {
throw new RecorderError(`Unrecognized test info: ${testContext}`);
}
}

/**
* This client manages the recorder life cycle and interacts with the proxy-tool to do the recording,
Expand All @@ -54,19 +92,23 @@ export class Recorder {
private variables: Record<string, string>;
private matcherSet = false;

constructor(private testContext?: Test | undefined) {
constructor(private testContext?: TestInfo) {
if (!this.testContext) {
throw new Error(
"Unable to determine the recording file path, testContext provided is not defined.",
);
}

logger.info(`[Recorder#constructor] Creating a recorder instance in ${getTestMode()} mode`);
if (isRecordMode() || isPlaybackMode()) {
if (this.testContext) {
this.sessionFile = sessionFilePath(this.testContext);
this.assetsJson = assetsJsonPath();
const context = calculatePaths(this.testContext);

this.sessionFile = sessionFilePath(context);
this.assetsJson = assetsJsonPath();

if (this.testContext) {
logger.info(`[Recorder#constructor] Using a session file located at ${this.sessionFile}`);
this.httpClient = createDefaultHttpClient();
} else {
throw new Error(
"Unable to determine the recording file path, testContext provided is not defined.",
);
}
}
this.variables = {};
Expand Down
91 changes: 91 additions & 0 deletions sdk/test-utils/recorder/src/testInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* Represents a Test.
*/
export type TestInfo = MochaTest | VitestTestContext;

/**
* Represents a Mocha Test.
*/
export interface MochaTest {
/**
* The title of the test.
*/
title: string;
/**
* The parent of the Mocha Test Suite.
*/
parent?: MochaTestSuite;
}

/**
* Represents a Mocha Test Suite.
*/
export interface MochaTestSuite {
fullTitle(): string;
}

/**
* Represents a Vitest Test Context
*/
export interface VitestTestContext {
/**
* The Vitest Context Task.
*/
task: VitestTask;
}

export interface VitestTaskBase {
name: string;
suite?: VitestSuite;
}

/**
* Represents a Vitest Test Context Task
*/
export interface VitestTask extends VitestTaskBase {
/**
* The Vitest Context Task Name.
*/
name: string;
/**
* The Vitest Context Task Suite.
*/
suite: VitestSuite;
}

/**
* Represents a Vitest Test Suite.
*/
export interface VitestSuite extends VitestTaskBase {
/**
* The Vitest Context Task Suite Name.
*/
name: string;
}

/**
* Determines whether the given test is a Mocha Test.
* @param test - The test to check.
* @returns true if the given test is a Mocha Test.
*/
export function isMochaTest(test: unknown): test is MochaTest {
return typeof test === "object" && test != null && "title" in test;
}

/**
* Determines whether the given test is a Vitest Test.
* @param test - The test to check.
* @returns true if the given test is a Vitest Test.
*/
export function isVitestTestContext(test: unknown): test is VitestTestContext {
return (
typeof test == "function" &&
"task" in test &&
typeof test.task === "object" &&
test.task != null &&
"name" in test.task
);
}
20 changes: 9 additions & 11 deletions sdk/test-utils/recorder/src/utils/sessionFilePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
import { isNode } from "@azure/core-util";
import { generateTestRecordingFilePath } from "./filePathGenerator";
import { relativeRecordingsPath } from "./relativePathCalculator";
import { RecorderError } from "./utils";

export function sessionFilePath(testContext: Mocha.Test): string {
export interface TestContext {
suiteTitle: string; // describe(suiteTitle, () => {})
testTitle: string; // it(testTitle, () => {})
}

export function sessionFilePath(testContext: TestContext): string {
// sdk/service/project/recordings/{node|browsers}/<describe-block-title>/recording_<test-title>.json
return `${relativeRecordingsPath()}/${recordingFilePath(testContext)}`;
}
Expand All @@ -16,17 +20,11 @@ export function sessionFilePath(testContext: Mocha.Test): string {
*
* `{node|browsers}/<describe-block-title>/recording_<test-title>.json`
*/
export function recordingFilePath(testContext: Mocha.Test): string {
if (!testContext.parent) {
throw new RecorderError(
`Test ${testContext.title} is not inside a describe block, so a file path for its recording could not be generated. Please place the test inside a describe block.`,
);
}

export function recordingFilePath(testContext: TestContext): string {
return generateTestRecordingFilePath(
isNode ? "node" : "browsers",
testContext.parent.fullTitle(),
testContext.title,
testContext.suiteTitle,
testContext.testTitle,
);
}

Expand Down
62 changes: 62 additions & 0 deletions sdk/test-utils/recorder/test/recorder.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { expect } from "chai";

import { calculatePaths } from "../src/recorder";

describe("Recorder file paths", () => {
it("calculates paths for a Mocha test", () => {
const mochaTest = {
title: "mocha test title",
parent: {
fullTitle: () => "mocha suite title",
},
};
const context = calculatePaths(mochaTest);

expect(context).to.eql({
suiteTitle: "mocha suite title",
testTitle: "mocha test title",
});
});

it("calculates paths for a vitest test", () => {
const vitestTest = () => {
/* no-op */
};
(vitestTest as any).task = {
name: "vitest test title",
suite: {
name: "vitest suite title",
},
};

const context = calculatePaths(vitestTest as any);
expect(context).to.eql({
suiteTitle: "vitest suite title",
testTitle: "vitest test title",
});
});

it("calculates paths for a vitest test with nested suites", () => {
const vitestTest = () => {
/* no-op */
};
(vitestTest as any).task = {
name: "vitest test title",
suite: {
name: "vitest suite title",
suite: {
name: "toplevel suite",
},
},
};

const context = calculatePaths(vitestTest as any);
expect(context).to.eql({
suiteTitle: "toplevel suite_vitest suite title",
testTitle: "vitest test title",
});
});
});