spec-helpers.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import * as childProcess from 'child_process';
  2. import * as path from 'path';
  3. import * as http from 'http';
  4. import * as v8 from 'v8';
  5. import { SuiteFunction, TestFunction } from 'mocha';
  6. const addOnly = <T>(fn: Function): T => {
  7. const wrapped = (...args: any[]) => {
  8. return fn(...args);
  9. };
  10. (wrapped as any).only = wrapped;
  11. (wrapped as any).skip = wrapped;
  12. return wrapped as any;
  13. };
  14. export const ifit = (condition: boolean) => (condition ? it : addOnly<TestFunction>(it.skip));
  15. export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<SuiteFunction>(describe.skip));
  16. export const delay = (time: number = 0) => new Promise(resolve => setTimeout(resolve, time));
  17. type CleanupFunction = (() => void) | (() => Promise<void>)
  18. const cleanupFunctions: CleanupFunction[] = [];
  19. export async function runCleanupFunctions () {
  20. for (const cleanup of cleanupFunctions) {
  21. const r = cleanup();
  22. if (r instanceof Promise) { await r; }
  23. }
  24. cleanupFunctions.length = 0;
  25. }
  26. export function defer (f: CleanupFunction) {
  27. cleanupFunctions.unshift(f);
  28. }
  29. class RemoteControlApp {
  30. process: childProcess.ChildProcess;
  31. port: number;
  32. constructor (proc: childProcess.ChildProcess, port: number) {
  33. this.process = proc;
  34. this.port = port;
  35. }
  36. remoteEval = (js: string): Promise<any> => {
  37. return new Promise((resolve, reject) => {
  38. const req = http.request({
  39. host: '127.0.0.1',
  40. port: this.port,
  41. method: 'POST'
  42. }, res => {
  43. const chunks = [] as Buffer[];
  44. res.on('data', chunk => { chunks.push(chunk); });
  45. res.on('end', () => {
  46. const ret = v8.deserialize(Buffer.concat(chunks));
  47. if (Object.prototype.hasOwnProperty.call(ret, 'error')) {
  48. reject(new Error(`remote error: ${ret.error}\n\nTriggered at:`));
  49. } else {
  50. resolve(ret.result);
  51. }
  52. });
  53. });
  54. req.write(js);
  55. req.end();
  56. });
  57. }
  58. remotely = (script: Function, ...args: any[]): Promise<any> => {
  59. return this.remoteEval(`(${script})(...${JSON.stringify(args)})`);
  60. }
  61. }
  62. export async function startRemoteControlApp (extraArgs: string[] = [], options?: childProcess.SpawnOptionsWithoutStdio) {
  63. const appPath = path.join(__dirname, 'fixtures', 'apps', 'remote-control');
  64. const appProcess = childProcess.spawn(process.execPath, [appPath, ...extraArgs], options);
  65. appProcess.stderr.on('data', d => {
  66. process.stderr.write(d);
  67. });
  68. const port = await new Promise<number>(resolve => {
  69. appProcess.stdout.on('data', d => {
  70. const m = /Listening: (\d+)/.exec(d.toString());
  71. if (m && m[1] != null) {
  72. resolve(Number(m[1]));
  73. }
  74. });
  75. });
  76. defer(() => { appProcess.kill('SIGINT'); });
  77. return new RemoteControlApp(appProcess, port);
  78. }
  79. export function waitUntil (
  80. callback: () => boolean,
  81. opts: { rate?: number, timeout?: number } = {}
  82. ) {
  83. const { rate = 10, timeout = 10000 } = opts;
  84. return new Promise<void>((resolve, reject) => {
  85. let intervalId: NodeJS.Timeout | undefined; // eslint-disable-line prefer-const
  86. let timeoutId: NodeJS.Timeout | undefined;
  87. const cleanup = () => {
  88. if (intervalId) clearInterval(intervalId);
  89. if (timeoutId) clearTimeout(timeoutId);
  90. };
  91. const check = () => {
  92. let result;
  93. try {
  94. result = callback();
  95. } catch (e) {
  96. cleanup();
  97. reject(e);
  98. return;
  99. }
  100. if (result === true) {
  101. cleanup();
  102. resolve();
  103. return true;
  104. }
  105. };
  106. if (check()) {
  107. return;
  108. }
  109. intervalId = setInterval(check, rate);
  110. timeoutId = setTimeout(() => {
  111. timeoutId = undefined;
  112. cleanup();
  113. reject(new Error(`waitUntil timed out after ${timeout}ms`));
  114. }, timeout);
  115. });
  116. }
  117. export async function repeatedly<T> (
  118. fn: () => Promise<T>,
  119. opts?: { until?: (x: T) => boolean, timeLimit?: number }
  120. ) {
  121. const { until = (x: T) => !!x, timeLimit = 10000 } = opts ?? {};
  122. const begin = +new Date();
  123. while (true) {
  124. const ret = await fn();
  125. if (until(ret)) { return ret; }
  126. if (+new Date() - begin > timeLimit) { throw new Error(`repeatedly timed out (limit=${timeLimit})`); }
  127. }
  128. }