Skip to content

Commit

Permalink
[Unified Recorder] Call proxy-tool through dev-tool (#18322)
Browse files Browse the repository at this point in the history
* test-proxy starter code for starting

* adding in the requirements

* test-proxy starter code for starting

* clean up

* gets RootLocation

* os.platform() === "win32" check

* cleanup

* testProxyUtils.ts

* checkpoint

* node side looks like it's working ✔️

* readme formatting

* same console.log in win and lin

* test:node-with-proxy

* "sdk-type": "utility",

* lock file

* "sdk-type": "utility",

* lock file

* dev-tool test:browser

* dotenv.config() call not needed since dev-tool does it by default

* run subcommand

* Partly switching to "fs-extra"

* fsExtra -> fs

* test:node-{js|ts}-input

* default options

* --single-run

* remove dev-tool shortcut

* runOnlyTestCommand

* dedeuplicate with shouldRunProxyTool and runTestsWithProxyTool methods

* minor changes

* add console.logs

* --mocha=\"--whatever\" and refactoring

* simplify test scripts

* more refactoring

* unintended duplication

* const sdkType = contents["sdk-type"];

* dead code

* removing the if check

* use an array instead

* lock file

* "sdk-type": "utility",

* "sdk-type": "utility",

* lock file

* npm run integration-test:node

* js -> ts

* lock file

* moving commands/run/testUtils.ts -> src/util/testUtils.ts

* lock file

* lock file

* bug fix

* PROXY_MANUAL_START

* getTestMode

* readme

* lock file from main

* lock file

* dump logs

* fix windows path

* PROXY_MANUAL_START in karma.conf

* duplication

* clean karma conf

* clean package.json

* waits for the proxy tool - draft

* wait-for-proxy-endpoint finish

* Update sdk/test-utils/recorder-new/test/testProxyTests.spec.ts

* beautify the tests

* fix the test mode log

* minor updates to tests

* test-info

* no need to pass test mode
  • Loading branch information
HarshaNalluru authored Nov 20, 2021
1 parent df9ad9d commit 2de7b65
Show file tree
Hide file tree
Showing 25 changed files with 431 additions and 68 deletions.
13 changes: 11 additions & 2 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions common/tools/dev-tool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"private": true,
"prettier": "../eslint-plugin-azure-sdk/prettier.json",
"dependencies": {
"concurrently": "^6.3.0",
"chalk": "~4.1.1",
"dotenv": "^8.2.0",
"fs-extra": "^8.1.0",
Expand All @@ -56,6 +57,7 @@
"@rollup/plugin-json": "^4.0.0",
"@rollup/plugin-multi-entry": "^3.0.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"@types/concurrently": "^6.3.0",
"@types/chai": "^4.1.6",
"@types/chai-as-promised": "^7.1.0",
"@types/fs-extra": "^8.0.0",
Expand Down
4 changes: 3 additions & 1 deletion common/tools/dev-tool/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const log = createPrinter("dev-tool");
export const baseCommands = {
about: () => import("./about"),
package: () => import("./package"),
samples: () => import("./samples")
samples: () => import("./samples"),
"test-proxy": () => import("./test-proxy"),
run: () => import("./run")
} as const;

/**
Expand Down
12 changes: 12 additions & 0 deletions common/tools/dev-tool/src/commands/run/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license

import { subCommand, makeCommandInfo } from "../../framework/command";

export const commandInfo = makeCommandInfo("run", "run scripts such as test:node");

export default subCommand(commandInfo, {
"test:node-ts-input": () => import("./testNodeTSInput"),
"test:node-js-input": () => import("./testNodeJSInput"),
"test:browser": () => import("./testBrowser")
});
24 changes: 24 additions & 0 deletions common/tools/dev-tool/src/commands/run/testBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license

import { leafCommand, makeCommandInfo } from "../../framework/command";
import { runTestsWithProxyTool } from "../../util/testUtils";

export const commandInfo = makeCommandInfo(
"test:browser",
"runs the browser tests using karma with the default and the provided options; starts the proxy-tool in record and playback modes",
{
karma: {
kind: "string",
description: "Karma options (such as --single-run)",
default: "--single-run"
}
}
);

export default leafCommand(commandInfo, async (options) => {
return runTestsWithProxyTool({
command: `karma start ${options.karma}`,
name: "browser-tests"
});
});
25 changes: 25 additions & 0 deletions common/tools/dev-tool/src/commands/run/testNodeJSInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license

import { leafCommand, makeCommandInfo } from "../../framework/command";
import { runTestsWithProxyTool } from "../../util/testUtils";

export const commandInfo = makeCommandInfo(
"test:node-js-input",
"runs the node tests using mocha with the default and the provided options; starts the proxy-tool in record and playback modes",
{
mocha: {
kind: "string",
description:
"Mocha options along with the bundled test file(JS) with rollup as expected by mocha",
default: '--timeout 5000000 "dist-esm/test/{,!(browser)/**/}/*.spec.js"'
}
}
);

export default leafCommand(commandInfo, async (options) => {
return runTestsWithProxyTool({
command: `nyc mocha -r esm --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --full-trace ${options.mocha}`,
name: "node-tests"
});
});
25 changes: 25 additions & 0 deletions common/tools/dev-tool/src/commands/run/testNodeTSInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license

import { leafCommand, makeCommandInfo } from "../../framework/command";
import { runTestsWithProxyTool } from "../../util/testUtils";

export const commandInfo = makeCommandInfo(
"test:node-ts-input",
"runs the node tests using mocha with the default and the provided options; starts the proxy-tool in record and playback modes",
{
mocha: {
kind: "string",
description:
"Mocha options along with the test files(glob pattern) in TS as expected by mocha",
default: '--timeout 1200000 --exclude "test/**/browser/*.spec.ts" "test/**/*.spec.ts"'
}
}
);

export default leafCommand(commandInfo, async (options) => {
return runTestsWithProxyTool({
command: `mocha -r esm -r ts-node/register --reporter ../../../common/tools/mocha-multi-reporter.js --full-trace ${options.mocha}`,
name: "node-tests"
});
});
14 changes: 14 additions & 0 deletions common/tools/dev-tool/src/commands/test-proxy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license

import { subCommand, makeCommandInfo } from "../../framework/command";

export const commandInfo = makeCommandInfo(
"test-proxy",
"runs the proxy-tool with the `docker run ...` command"
);

export default subCommand(commandInfo, {
start: () => import("./start"),
"wait-for-proxy-endpoint": () => import("./waitForProxyEndpoint")
});
18 changes: 18 additions & 0 deletions common/tools/dev-tool/src/commands/test-proxy/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { leafCommand, makeCommandInfo } from "../../framework/command";
import { config } from "dotenv";
import { startProxyTool } from "../../util/testProxyUtils";
config();

export const commandInfo = makeCommandInfo(
"test-proxy",
"runs the proxy-tool with the `docker run ...` command",
{}
);

export default leafCommand(commandInfo, async (_options) => {
await startProxyTool();
return true;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { leafCommand, makeCommandInfo } from "../../framework/command";
import { config } from "dotenv";
import { isProxyToolActive } from "../../util/testProxyUtils";
import { checkWithTimeout } from "../../util/checkWithTimeout";
config();

export const commandInfo = makeCommandInfo(
"test-proxy",
"waits for the proxy tool to be active or fails in 2 minutes",
{}
);

export default leafCommand(commandInfo, async (_options) => {
const result = await checkWithTimeout(isProxyToolActive, 1000, 120000);
return result;
});
32 changes: 32 additions & 0 deletions common/tools/dev-tool/src/util/checkWithTimeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license

import { createPrinter } from "./printer";
const log = createPrinter("check-with-timeout");

/**
* - Maximum wait duration for the expected event to happen = `10000 ms`(default value is 10 seconds)(= maxWaitTimeInMilliseconds)
* - Keep checking whether the predicate is true after every `1000 ms`(default value is 1 second) (= delayBetweenRetriesInMilliseconds)
*/
export async function checkWithTimeout(
predicate: () => boolean | Promise<boolean>,
delayBetweenRetriesInMilliseconds: number = 1000,
maxWaitTimeInMilliseconds: number = 10000
): Promise<boolean> {
const maxTime = Date.now() + maxWaitTimeInMilliseconds;
while (Date.now() < maxTime) {
if (await predicate()) {
log.info(`checkWithTimeout condition returned true`);
return true;
}
await delay(delayBetweenRetriesInMilliseconds);
}
return false;
}

async function delay(timeInMs: number) {
return new Promise((resolve) => {
log.info(`waiting for ${timeInMs}ms`);
setTimeout(resolve, timeInMs);
});
}
87 changes: 87 additions & 0 deletions common/tools/dev-tool/src/util/testProxyUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { spawn } from "child_process";
import path from "path";
import { IncomingMessage, request, RequestOptions } from "http";
import fs from "fs-extra";
import { createPrinter } from "./printer";

const log = createPrinter("test-proxy");
export async function startProxyTool() {
log.info(
`Attempting to start test proxy at http://localhost:5000 & https://localhost:5001.\n`
);

const subprocess = spawn(await getDockerRunCommand(), [], {
shell: true
});

const outFileName = "test-proxy-output.log";
const out = fs.createWriteStream(`./${outFileName}`, { flags: 'a' });
subprocess.stdout.pipe(out);
subprocess.stderr.pipe(out);

log.info(`Check the output file "${outFileName}" for test-proxy logs.`);
}

async function getRootLocation(start?: string): Promise<string> {
start ??= process.cwd();
if (await fs.pathExists(path.join(start, "rush.json"))) {
return start;
} else {
const nextPath = path.resolve(start, "..");
if (nextPath === start) {
throw new Error("Reached filesystem root, but no rush.json was found.");
} else {
return getRootLocation(nextPath);
}
}
}

async function getDockerRunCommand() {
const repoRoot = await getRootLocation(); // /workspaces/azure-sdk-for-js/
const testProxyRecordingsLocation = "/etc/testproxy";
const allowLocalhostAccess = "--add-host host.docker.internal:host-gateway";
const imageToLoad = `azsdkengsys.azurecr.io/engsys/testproxy-lin:${await getImageTag()}`;
return `docker run -v ${repoRoot}:${testProxyRecordingsLocation} -p 5001:5001 -p 5000:5000 ${allowLocalhostAccess} ${imageToLoad}`;
}

export async function isProxyToolActive() {
try {
await makeRequest("http://localhost:5000/info/available", {});
log.info(`Proxy tool seems to be active at http://localhost:5000\n`);
return true;
} catch (error) {
return false;
}
}

async function makeRequest(uri: string, requestOptions: RequestOptions): Promise<IncomingMessage> {
return new Promise<IncomingMessage>((resolve, reject) => {
const req = request(uri, requestOptions, resolve);
req.once("error", reject);
req.end();
});
}

async function getImageTag() {
// Grab the tag from the `/eng/common/testproxy/docker-start-proxy.ps1` file [..is used to run the proxy-tool in the CI]
//
// $SELECTED_IMAGE_TAG = "1147815";
// (Bot regularly updates the tag in the file above.)
try {
const contentInPWSHScript = await fs.readFile(
`${path.join(await getRootLocation(), "eng/common/testproxy/docker-start-proxy.ps1")}`,
"utf-8"
);
const tag = contentInPWSHScript.match(/\$SELECTED_IMAGE_TAG \= \"(.*)\"/)![1];
log.info(`Image tag obtained from the powershell script => ${tag}\n`);
return tag;
} catch (_) {
log.warn(
`Unable to get the image tag from the powershell script, trying "latest" tag instead\n`
);
return "latest";
}
}
48 changes: 48 additions & 0 deletions common/tools/dev-tool/src/util/testUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { isProxyToolActive } from "./testProxyUtils";
import concurrently from "concurrently";
import { createPrinter } from "./printer";

const log = createPrinter("preparing-proxy-tool");

async function shouldRunProxyTool(): Promise<boolean> {
const mode = process.env.TEST_MODE;
createPrinter("test-info").info(`===TEST_MODE="${mode}"===`);
if (mode === "live") {
return false; // No need to start the proxy tool in the live mode
} else {
const isActive = await isProxyToolActive();
if (isActive) {
// No need to run a new one if it is already active
// Especially, CI uses this path
log.info(
`Proxy tool seems to be active, not attempting to start the test proxy at http://localhost:5000 & https://localhost:5001.\n`
);
}
return !isActive;
}
}

export async function runTestsWithProxyTool(testCommandObj: concurrently.CommandObj) {
if (
await shouldRunProxyTool() // Boolean to figure out if we need to run just the mocha command or the test-proxy too
) {
const testProxyCMD = "dev-tool test-proxy start";
const waitForProxyEndpointCMD = "dev-tool test-proxy wait-for-proxy-endpoint";
await concurrently(
[
{ command: testProxyCMD },
{
command: `${waitForProxyEndpointCMD} && ${testCommandObj.command}`, // Waits for the proxy endpoint to be active and then starts running the tests
name: testCommandObj.name
}
],
{
killOthers: ["failure", "success"],
successCondition: "first"
}
);
} else {
await concurrently([testCommandObj]);
}
return true;
}
Loading

0 comments on commit 2de7b65

Please sign in to comment.