-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #911 from gemini-testing/HERMIONE-1504.dev_server_tp
feat: add ability to use dev server
- Loading branch information
Showing
10 changed files
with
865 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import _ from "lodash"; | ||
import { spawn } from "child_process"; | ||
import debug from "debug"; | ||
import { Config } from "../config"; | ||
import { findCwd, pipeLogsWithPrefix, waitDevServerReady } from "./utils"; | ||
import logger = require("../utils/logger"); | ||
import type { Testplane } from "../testplane"; | ||
|
||
export type DevServerOpts = { testplane: Testplane; devServerConfig: Config["devServer"]; configPath: string }; | ||
|
||
export type InitDevServer = (opts: DevServerOpts) => Promise<void>; | ||
|
||
export const initDevServer: InitDevServer = async ({ testplane, devServerConfig, configPath }) => { | ||
if (!devServerConfig || !devServerConfig.command) { | ||
return; | ||
} | ||
|
||
logger.log("Starting dev server with command", `"${devServerConfig.command}"`); | ||
|
||
const debugLog = debug("testplane:dev-server"); | ||
|
||
if (!_.isEmpty(devServerConfig.args)) { | ||
debugLog("Dev server args:", JSON.stringify(devServerConfig.args)); | ||
} | ||
|
||
if (!_.isEmpty(devServerConfig.env)) { | ||
debugLog("Dev server env:", JSON.stringify(devServerConfig.env, null, 4)); | ||
} | ||
|
||
const devServer = spawn(devServerConfig.command, devServerConfig.args, { | ||
env: { ...process.env, ...devServerConfig.env }, | ||
cwd: devServerConfig.cwd || findCwd(configPath), | ||
shell: true, | ||
windowsHide: true, | ||
}); | ||
|
||
if (devServerConfig.logs) { | ||
pipeLogsWithPrefix(devServer, "[dev server] "); | ||
} | ||
|
||
devServer.once("exit", (code, signal) => { | ||
if (signal !== "SIGINT") { | ||
const errorMessage = [ | ||
"An error occured while launching dev server", | ||
`Dev server failed with code '${code}' (signal: ${signal})`, | ||
].join("\n"); | ||
testplane.halt(new Error(errorMessage), 5000); | ||
} | ||
}); | ||
|
||
process.once("exit", () => { | ||
devServer.kill("SIGINT"); | ||
}); | ||
|
||
await waitDevServerReady(devServer, devServerConfig.readinessProbe); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { pipeline, Transform, TransformCallback } from "stream"; | ||
import path from "path"; | ||
import fs from "fs"; | ||
import chalk from "chalk"; | ||
import type { ChildProcessWithoutNullStreams } from "child_process"; | ||
import logger from "../utils/logger"; | ||
import type { Config } from "../config"; | ||
|
||
export const findCwd = (configPath: string): string => { | ||
let prev = configPath; | ||
|
||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const dir = path.dirname(prev); | ||
|
||
if (dir === prev) { | ||
return path.dirname(configPath); | ||
} | ||
|
||
const foundPackageJson = fs.existsSync(path.join(dir, "package.json")); | ||
|
||
if (foundPackageJson) { | ||
return dir; | ||
} | ||
|
||
prev = dir; | ||
} | ||
}; | ||
|
||
class WithPrefixTransformer extends Transform { | ||
prefix: string; | ||
includePrefix: boolean; | ||
|
||
constructor(prefix: string) { | ||
super(); | ||
|
||
this.prefix = chalk.green(prefix); | ||
this.includePrefix = true; | ||
} | ||
|
||
_transform(chunk: string, _: string, callback: TransformCallback): void { | ||
const chunkString = chunk.toString(); | ||
const chunkRows = chunkString.split("\n"); | ||
|
||
const includeSuffix = chunkString.endsWith("\n") && chunkRows.pop() === ""; | ||
|
||
const resultPrefix = this.includePrefix ? this.prefix : ""; | ||
const resultSuffix = includeSuffix ? "\n" : ""; | ||
const resultData = resultPrefix + chunkRows.join("\n" + this.prefix) + resultSuffix; | ||
|
||
this.push(resultData); | ||
this.includePrefix = includeSuffix; | ||
|
||
callback(); | ||
} | ||
} | ||
|
||
export const pipeLogsWithPrefix = (childProcess: ChildProcessWithoutNullStreams, prefix: string): void => { | ||
const logOnErrorCb = (error: Error | null): void => { | ||
if (error) { | ||
logger.error("Got an error trying to pipeline dev server logs:", error.message); | ||
} | ||
}; | ||
|
||
pipeline(childProcess.stdout, new WithPrefixTransformer(prefix), process.stdout, logOnErrorCb); | ||
pipeline(childProcess.stderr, new WithPrefixTransformer(prefix), process.stderr, logOnErrorCb); | ||
}; | ||
|
||
const defaultIsReadyFn = (response: Awaited<ReturnType<typeof globalThis.fetch>>): boolean => { | ||
return response.status >= 200 && response.status < 300; | ||
}; | ||
|
||
export const waitDevServerReady = async ( | ||
devServer: ChildProcessWithoutNullStreams, | ||
readinessProbe: Config["devServer"]["readinessProbe"], | ||
): Promise<void> => { | ||
if (typeof readinessProbe !== "function" && !readinessProbe.url) { | ||
return; | ||
} | ||
|
||
logger.log("Waiting for dev server to be ready"); | ||
|
||
if (typeof readinessProbe === "function") { | ||
return Promise.resolve() | ||
.then(() => readinessProbe(devServer)) | ||
.then(res => { | ||
logger.log("Dev server is ready"); | ||
|
||
return res; | ||
}); | ||
} | ||
|
||
const isReadyFn = readinessProbe.isReady || defaultIsReadyFn; | ||
|
||
let isSuccess = false; | ||
let isError = false; | ||
|
||
const timeoutPromise = new Promise<never>((_, reject) => { | ||
setTimeout(() => { | ||
if (!isError && !isSuccess) { | ||
isError = true; | ||
reject(new Error(`Dev server is still not ready after ${readinessProbe.timeouts.waitServerTimeout}ms`)); | ||
} | ||
}, readinessProbe.timeouts.waitServerTimeout).unref(); | ||
}); | ||
|
||
const readyPromise = new Promise<void>(resolve => { | ||
const tryToFetch = async (): Promise<void> => { | ||
const signal = AbortSignal.timeout(readinessProbe.timeouts.probeRequestTimeout); | ||
|
||
try { | ||
const response = await fetch(readinessProbe.url!, { signal }); | ||
const isReady = await isReadyFn(response); | ||
|
||
if (!isReady) { | ||
throw new Error("Dev server is not ready yet"); | ||
} | ||
|
||
if (!isError && !isSuccess) { | ||
isSuccess = true; | ||
logger.log("Dev server is ready"); | ||
resolve(); | ||
} | ||
} catch (error) { | ||
const err = error as { cause?: { code?: string } }; | ||
|
||
if (!isError && !isSuccess) { | ||
setTimeout(tryToFetch, readinessProbe.timeouts.probeRequestInterval).unref(); | ||
|
||
const errorMessage = err && err.cause && (err.cause.code || err.cause); | ||
|
||
if (errorMessage && errorMessage !== "ECONNREFUSED") { | ||
logger.warn("Dev server ready probe failed:", errorMessage); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
tryToFetch(); | ||
}); | ||
|
||
return Promise.race([timeoutPromise, readyPromise]); | ||
}; | ||
|
||
export default { findCwd, pipeLogsWithPrefix, waitDevServerReady }; |
Oops, something went wrong.