From e3cf6756247dacb5ee0027d1f5149cf27b81aa6f Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 5 Apr 2021 13:23:49 -0700 Subject: [PATCH] test: remove a copy of folio, use upstream (#6080) --- .gitignore | 2 +- package-lock.json | 6 +- package.json | 19 +- test/fixtures.ts | 197 -- test/http.fixtures.ts | 102 - test/playwright.fixtures.ts | 209 -- test/utils.ts | 2 +- tests/config/android.config.ts | 2 +- tests/config/androidEnv.ts | 2 +- tests/config/androidTest.ts | 4 +- tests/config/browserEnv.ts | 2 +- tests/config/browserTest.ts | 4 +- tests/config/cliEnv.ts | 2 +- tests/config/cliTest.ts | 4 +- tests/config/default.config.ts | 2 +- tests/config/electronEnv.ts | 2 +- tests/config/electronTest.ts | 4 +- tests/config/pageTest.ts | 4 +- tests/config/playwrightTest.ts | 4 +- tests/config/serverEnv.ts | 2 +- tests/folio/.gitignore | 1 - tests/folio/cli.js | 19 - tests/folio/src/cli.ts | 278 --- tests/folio/src/dispatcher.ts | 392 ---- tests/folio/src/expect.ts | 58 - tests/folio/src/expectType.ts | 150 -- tests/folio/src/globals.ts | 25 - tests/folio/src/golden.ts | 171 -- tests/folio/src/index.ts | 27 - tests/folio/src/ipc.ts | 66 - tests/folio/src/loader.ts | 94 - tests/folio/src/reporters/base.ts | 246 -- tests/folio/src/reporters/dot.ts | 57 - tests/folio/src/reporters/empty.ts | 30 - tests/folio/src/reporters/json.ts | 136 -- tests/folio/src/reporters/junit.ts | 185 -- tests/folio/src/reporters/line.ts | 81 - tests/folio/src/reporters/list.ts | 79 - tests/folio/src/reporters/multiplexer.ts | 65 - tests/folio/src/runner.ts | 132 -- tests/folio/src/spec.ts | 222 -- tests/folio/src/test.ts | 237 -- tests/folio/src/transform.ts | 80 - tests/folio/src/types.ts | 196 -- tests/folio/src/util.ts | 139 -- tests/folio/src/worker.ts | 119 - tests/folio/src/workerRunner.ts | 462 ---- tests/folio/third_party/diff_match_patch.js | 2222 ------------------- tests/folio/tsconfig.json | 22 - utils/doclint/test/class-testapi.md | 20 - utils/doclint/test/missingDocs.spec.js | 42 - utils/doclint/test/test-api-class.ts | 35 - utils/doclint/test/test-api.ts | 18 - 53 files changed, 33 insertions(+), 6648 deletions(-) delete mode 100644 test/fixtures.ts delete mode 100644 test/http.fixtures.ts delete mode 100644 test/playwright.fixtures.ts delete mode 100644 tests/folio/.gitignore delete mode 100755 tests/folio/cli.js delete mode 100644 tests/folio/src/cli.ts delete mode 100644 tests/folio/src/dispatcher.ts delete mode 100644 tests/folio/src/expect.ts delete mode 100644 tests/folio/src/expectType.ts delete mode 100644 tests/folio/src/globals.ts delete mode 100644 tests/folio/src/golden.ts delete mode 100644 tests/folio/src/index.ts delete mode 100644 tests/folio/src/ipc.ts delete mode 100644 tests/folio/src/loader.ts delete mode 100644 tests/folio/src/reporters/base.ts delete mode 100644 tests/folio/src/reporters/dot.ts delete mode 100644 tests/folio/src/reporters/empty.ts delete mode 100644 tests/folio/src/reporters/json.ts delete mode 100644 tests/folio/src/reporters/junit.ts delete mode 100644 tests/folio/src/reporters/line.ts delete mode 100644 tests/folio/src/reporters/list.ts delete mode 100644 tests/folio/src/reporters/multiplexer.ts delete mode 100644 tests/folio/src/runner.ts delete mode 100644 tests/folio/src/spec.ts delete mode 100644 tests/folio/src/test.ts delete mode 100644 tests/folio/src/transform.ts delete mode 100644 tests/folio/src/types.ts delete mode 100644 tests/folio/src/util.ts delete mode 100644 tests/folio/src/worker.ts delete mode 100644 tests/folio/src/workerRunner.ts delete mode 100644 tests/folio/third_party/diff_match_patch.js delete mode 100644 tests/folio/tsconfig.json delete mode 100644 utils/doclint/test/class-testapi.md delete mode 100644 utils/doclint/test/missingDocs.spec.js delete mode 100644 utils/doclint/test/test-api-class.ts delete mode 100644 utils/doclint/test/test-api.ts diff --git a/.gitignore b/.gitignore index e9c2f05ce1bb5..5be73dc582f79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /node_modules/ /test-results/ /test/coverage-report -/test/test-user-data-dir* +/tests/coverage-report .local-browsers/ /.dev_profile* .DS_Store diff --git a/package-lock.json b/package-lock.json index 65aa6dd6c94f3..1e6c00b30b5b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8131,9 +8131,9 @@ } }, "folio": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/folio/-/folio-0.3.17.tgz", - "integrity": "sha512-aYbhifQ/A0E6ZwEUdBGU900+aW2R243pxkWF0GhYceQxFTbkoCLIEHegTOpo4VtNsNHyM3sI/Xz3DkmITiwRcg==", + "version": "0.3.20-alpha", + "resolved": "https://registry.npmjs.org/folio/-/folio-0.3.20-alpha.tgz", + "integrity": "sha512-uJWYgfLa1l91NSbBV2pxKR7g91/Ti7cLuvfVI6M9uI5H7JaDhEEgPsLM9RG7rwoJ7AtIOFRJSbdMIwyRwvzWxw==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", diff --git a/package.json b/package.json index 5d5300f4600ee..7094f52f82af1 100644 --- a/package.json +++ b/package.json @@ -9,27 +9,26 @@ "node": ">=10.17.0" }, "scripts": { - "ctest": "npm run build-folio && node tests/folio/cli.js --config=tests/config/default.config.ts chromium", - "ftest": "npm run build-folio && node tests/folio/cli.js --config=tests/config/default.config.ts firefox", - "wtest": "npm run build-folio && node tests/folio/cli.js --config=tests/config/default.config.ts webkit", - "atest": "npm run build-folio && node tests/folio/cli.js --config=tests/config/android.config.ts", - "test": "npm run build-folio && node tests/folio/cli.js --config=tests/config/default.config.ts", + "ctest": "folio --config=tests/config/default.config.ts chromium", + "ftest": "folio --config=tests/config/default.config.ts firefox", + "wtest": "folio --config=tests/config/default.config.ts webkit", + "atest": "folio --config=tests/config/android.config.ts", + "test": "folio --config=tests/config/default.config.ts", "eslint": "[ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts . || eslint --ext js,ts .", "tsc": "tsc -p .", "tsc-installer": "tsc -p ./src/install/tsconfig.json", "doc": "node utils/doclint/cli.js", - "lint": "npm run eslint && npm run tsc && npm run doc && npm run check-deps && node utils/generate_channels.js && node utils/generate_types/ --check-clean && npm run test-types && folio utils/doclint/test/", + "lint": "npm run eslint && npm run tsc && npm run doc && npm run check-deps && node utils/generate_channels.js && node utils/generate_types/ --check-clean && npm run test-types", "clean": "rimraf lib", "prepare": "node install-from-github.js", "build": "node utils/build/build.js", "watch": "node utils/build/build.js --watch", - "test-types": "node utils/generate_types/ && npx -p typescript@3.7.5 tsc -p utils/generate_types/test/tsconfig.json && tsc -p ./test/", + "test-types": "node utils/generate_types/ && npx -p typescript@3.7.5 tsc -p utils/generate_types/test/tsconfig.json && tsc -p ./tests/", "roll-browser": "node utils/roll_browser.js", "check-deps": "node utils/check_deps.js", "build-android-driver": "./utils/build_android_driver.sh", "storybook": "start-storybook -p 6006 -s public", - "build-storybook": "build-storybook -s public", - "build-folio": "tsc -p ./tests/folio" + "build-storybook": "build-storybook -s public" }, "author": { "name": "Microsoft Corporation" @@ -80,7 +79,7 @@ "eslint-plugin-notice": "^0.9.10", "eslint-plugin-react-hooks": "^4.2.0", "file-loader": "^6.1.0", - "folio": "=0.3.17", + "folio": "=0.3.20-alpha", "formidable": "^1.2.2", "html-webpack-plugin": "^4.4.1", "ncp": "^2.0.0", diff --git a/test/fixtures.ts b/test/fixtures.ts deleted file mode 100644 index 1c14ed629b47c..0000000000000 --- a/test/fixtures.ts +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import assert from 'assert'; -import childProcess from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import util from 'util'; -import os from 'os'; -import type { AndroidDevice, Browser, BrowserContext, BrowserType, Page } from '../index'; -import { installCoverageHooks } from './coverage'; -import { folio as httpFolio } from './http.fixtures'; -import { folio as playwrightFolio } from './playwright.fixtures'; -import { PlaywrightClient } from '../lib/remote/playwrightClient'; -import { start } from '../lib/outofprocess'; -import { removeFolders } from '../lib/utils/utils'; -export { expect, config } from 'folio'; - -const mkdtempAsync = util.promisify(fs.mkdtemp); - -const getExecutablePath = browserName => { - if (browserName === 'chromium' && process.env.CRPATH) - return process.env.CRPATH; - if (browserName === 'firefox' && process.env.FFPATH) - return process.env.FFPATH; - if (browserName === 'webkit' && process.env.WKPATH) - return process.env.WKPATH; -}; - -type ModeParameters = { - mode: 'default' | 'driver' | 'service'; -}; -type WorkerFixtures = { - toImpl: (rpcObject: any) => any; - androidDevice: AndroidDevice; - androidDeviceBrowser: BrowserContext; -}; -type TestFixtures = { - createUserDataDir: () => Promise; - launchPersistent: (options?: Parameters['launchPersistentContext']>[1]) => Promise<{ context: BrowserContext, page: Page }>; -}; - -const fixtures = playwrightFolio.union(httpFolio).extend(); - -fixtures.mode.initParameter('Testing mode', process.env.PWMODE as any || 'default'); - -fixtures.createUserDataDir.init(async ({ }, run) => { - const dirs: string[] = []; - async function createUserDataDir() { - // We do not put user data dir in testOutputPath, - // because we do not want to upload them as test result artifacts. - // - // Additionally, it is impossible to upload user data dir after test run: - // - Firefox removes lock file later, presumably from another watchdog process? - // - WebKit has circular symlinks that makes CI go crazy. - const dir = await mkdtempAsync(path.join(os.tmpdir(), 'playwright-test-')); - dirs.push(dir); - return dir; - } - await run(createUserDataDir); - await removeFolders(dirs); -}); - -fixtures.launchPersistent.init(async ({ createUserDataDir, browserOptions, browserType }, run) => { - let context; - async function launchPersistent(options) { - if (context) - throw new Error('can only launch one persitent context'); - const userDataDir = await createUserDataDir(); - context = await browserType.launchPersistentContext(userDataDir, { ...browserOptions, ...options }); - const page = context.pages()[0]; - return { context, page }; - } - await run(launchPersistent); - if (context) - await context.close(); -}); - -fixtures.browserOptions.override(async ({ browserName, headful, slowMo, browserChannel }, run) => { - const executablePath = getExecutablePath(browserName); - if (executablePath) - console.error(`Using executable at ${executablePath}`); - await run({ - channel: browserChannel as any, - executablePath, - handleSIGINT: false, - slowMo, - headless: !headful, - }); -}); - -fixtures.playwright.override(async ({ browserName, testWorkerIndex, platform, mode }, run) => { - assert(platform); // Depend on platform to generate all tests. - const { coverage, uninstall } = installCoverageHooks(browserName); - require('../lib/utils/utils').setUnderTest(); - if (mode === 'driver') { - const playwrightObject = await start(); - await run(playwrightObject); - await playwrightObject.stop(); - await teardownCoverage(); - } else if (mode === 'service') { - const port = 9407 + testWorkerIndex * 2; - const spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'service.js'), [String(port)], { - stdio: 'pipe' - }); - spawnedProcess.stderr.pipe(process.stderr); - await new Promise(f => { - spawnedProcess.stdout.on('data', data => { - if (data.toString().includes('Listening on')) - f(); - }); - }); - spawnedProcess.unref(); - const onExit = (exitCode, signal) => { - throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`); - }; - spawnedProcess.on('exit', onExit); - const client = await PlaywrightClient.connect(`ws://localhost:${port}/ws`); - await run(client.playwright()); - await client.close(); - spawnedProcess.removeListener('exit', onExit); - const processExited = new Promise(f => spawnedProcess.on('exit', f)); - spawnedProcess.kill(); - await processExited; - await teardownCoverage(); - } else { - const playwright = require('../index'); - await run(playwright); - await teardownCoverage(); - } - - async function teardownCoverage() { - uninstall(); - const coveragePath = path.join(__dirname, 'coverage-report', testWorkerIndex + '.json'); - const coverageJSON = [...coverage.keys()].filter(key => coverage.get(key)); - await fs.promises.mkdir(path.dirname(coveragePath), { recursive: true }); - await fs.promises.writeFile(coveragePath, JSON.stringify(coverageJSON, undefined, 2), 'utf8'); - } -}); - -fixtures.toImpl.init(async ({ playwright }, run) => { - await run((playwright as any)._toImpl); -}, { scope: 'worker' }); - -fixtures.testParametersPathSegment.override(async ({ browserName }, run) => { - await run(browserName); -}); - -fixtures.androidDevice.init(async ({ playwright }, runTest) => { - const [device] = await playwright._android.devices(); - await device.shell('am force-stop org.chromium.webview_shell'); - await device.shell('am force-stop com.android.chrome'); - device.setDefaultTimeout(120000); - await runTest(device); - await device.close(); -}, { scope: 'worker' }); - -fixtures.androidDeviceBrowser.init(async ({ androidDevice }, runTest) => { - await runTest(await androidDevice.launchBrowser()); -}, { scope: 'worker' }); - -if (process.env.PW_ANDROID_TESTS) { - fixtures.page.override(async ({ androidDeviceBrowser }, run) => { - for (const page of androidDeviceBrowser.pages()) - await page.close(); - run(await androidDeviceBrowser.newPage()); - }); -} - -export const folio = fixtures.build(); - -folio.generateParametrizedTests( - 'platform', - process.env.PWTESTREPORT ? ['win32', 'darwin', 'linux'] : [process.platform as ('win32' | 'linux' | 'darwin')]); - -export const it = folio.it; -export const fit = folio.fit; -export const test = folio.test; -export const xit = folio.xit; -export const describe = folio.describe; -export const beforeEach = folio.beforeEach; -export const afterEach = folio.afterEach; -export const beforeAll = folio.beforeAll; -export const afterAll = folio.afterAll; diff --git a/test/http.fixtures.ts b/test/http.fixtures.ts deleted file mode 100644 index 27e20d1a928e2..0000000000000 --- a/test/http.fixtures.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { folio as base } from 'folio'; -import path from 'path'; -import socks from 'socksv5'; -import { Server as WebSocketServer } from 'ws'; -import { TestServer } from '../utils/testserver'; - -type HttpWorkerFixtures = { - asset: (path: string) => string; - httpService: { server: TestServer, httpsServer: TestServer }; - socksPort: number, -}; - -type HttpTestFixtures = { - server: TestServer; - httpsServer: TestServer; - webSocketServer: WebSocketServer; -}; - -const fixtures = base.extend(); -fixtures.httpService.init(async ({ testWorkerIndex }, test) => { - const assetsPath = path.join(__dirname, 'assets'); - const cachedPath = path.join(__dirname, 'assets', 'cached'); - - const port = 8907 + testWorkerIndex * 3; - const server = await TestServer.create(assetsPath, port); - server.enableHTTPCache(cachedPath); - - const httpsPort = port + 1; - const httpsServer = await TestServer.createHTTPS(assetsPath, httpsPort); - httpsServer.enableHTTPCache(cachedPath); - - await test({ server, httpsServer }); - - await Promise.all([ - server.stop(), - httpsServer.stop(), - ]); -}, { scope: 'worker' }); - -fixtures.asset.init(async ({ }, test) => { - await test(p => path.join(__dirname, `assets`, p)); -}, { scope: 'worker' }); - -fixtures.server.init(async ({ httpService }, test) => { - httpService.server.reset(); - await test(httpService.server); -}); - -fixtures.httpsServer.init(async ({ httpService }, test) => { - httpService.httpsServer.reset(); - await test(httpService.httpsServer); -}); - -fixtures.webSocketServer.init(async ({ testWorkerIndex }, run) => { - const webSocketServer = new WebSocketServer({ - port: 8907 + testWorkerIndex * 3 + 2, - }); - await run(webSocketServer); - await new Promise(x => webSocketServer.close(x)); -}); - -fixtures.socksPort.init(async ({ testWorkerIndex }, run) => { - const server = socks.createServer((info, accept, deny) => { - let socket; - if ((socket = accept(true))) { - // Catch and ignore ECONNRESET errors. - socket.on('error', () => {}); - const body = 'Served by the SOCKS proxy'; - socket.end([ - 'HTTP/1.1 200 OK', - 'Connection: close', - 'Content-Type: text/html', - 'Content-Length: ' + Buffer.byteLength(body), - '', - body - ].join('\r\n')); - } - }); - const socksPort = 9107 + testWorkerIndex * 2; - server.listen(socksPort, 'localhost'); - server.useAuth(socks.auth.None()); - await run(socksPort); - server.close(); -}, { scope: 'worker' }); - -export const folio = fixtures.build(); diff --git a/test/playwright.fixtures.ts b/test/playwright.fixtures.ts deleted file mode 100644 index cc214d87d2eb4..0000000000000 --- a/test/playwright.fixtures.ts +++ /dev/null @@ -1,209 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * ============================================================================= - * DO NOT EDIT THIS FILE - * EDIT THE ONE IN @playwright/test and copy it over here. - * ============================================================================= - */ - -import { config, folio as baseFolio } from 'folio'; -import type { Browser, BrowserContext, BrowserContextOptions, BrowserType, LaunchOptions, Page } from '../index'; -export { expect, config } from 'folio'; - -// Test timeout for e2e tests is 30 seconds. -config.timeout = 30000; - -// Parameters ------------------------------------------------------------------ -// ... these can be used to run tests in different modes. - -type PlaywrightParameters = { - // Browser type name. - browserName: 'chromium' | 'firefox' | 'webkit'; - // Browser release channel, if applicable. - browserChannel: string | undefined; - // Whether to run tests headless or headful. - headful: boolean; - // Operating system. - platform: 'win32' | 'linux' | 'darwin'; - // Generate screenshot on failure. - screenshotOnFailure: boolean; - // Slows down Playwright operations by the specified amount of milliseconds. - slowMo: number; - // Whether to record videos for all tests. - video: boolean; -}; - - -// Worker fixture declarations ------------------------------------------------- -// ... these live as long as the worker process. - -type PlaywrightWorkerFixtures = { - // Playwright library. - playwright: typeof import('../index'); - // Browser type (Chromium / WebKit / Firefox) - browserType: BrowserType; - // Default browserType.launch() options. - browserOptions: LaunchOptions; - // Browser instance, shared for the worker. - browser: Browser; - // True iff browserName is Chromium - isChromium: boolean; - // True iff browserName is Firefox - isFirefox: boolean; - // True iff browserName is WebKit - isWebKit: boolean; - // True iff running on Windows. - isWindows: boolean; - // True iff running on Mac. - isMac: boolean; - // True iff running on Linux. - isLinux: boolean; -}; - - -// Test fixture definitions, those are created for each test ------------------ - -type PlaywrightTestFixtures = { - // Default browser.newContext() options. - contextOptions: BrowserContextOptions; - // Factory for creating a context with given additional options. - contextFactory: (options?: BrowserContextOptions) => Promise; - // Context instance for test. - context: BrowserContext; - // Page instance for test. - page: Page; -}; - -const fixtures = baseFolio.extend(); -fixtures.browserName.initParameter('Browser type name', (process.env.BROWSER || 'chromium') as 'chromium' | 'firefox' | 'webkit'); -fixtures.headful.initParameter('Whether to run tests headless or headful', process.env.HEADFUL ? true : false); -fixtures.platform.initParameter('Operating system', process.platform as ('win32' | 'linux' | 'darwin')); -fixtures.screenshotOnFailure.initParameter('Generate screenshot on failure', false); -fixtures.slowMo.initParameter('Slows down Playwright operations by the specified amount of milliseconds', 0); -fixtures.video.initParameter('Record videos while running tests', false); -fixtures.browserChannel.initParameter('Browser release channel', process.env.PW_CHROMIUM_CHANNEL); - -fixtures.browserOptions.init(async ({ headful, slowMo, browserChannel }, run) => { - await run({ - handleSIGINT: false, - slowMo, - headless: !headful, - channel: browserChannel as any, - }); -}, { scope: 'worker' }); - -fixtures.playwright.init(async ({ }, run) => { - const playwright = require('playwright'); - await run(playwright); -}, { scope: 'worker' }); - -fixtures.browserType.init(async ({ playwright, browserName }, run) => { - const browserType = (playwright as any)[browserName]; - await run(browserType); -}, { scope: 'worker' }); - -fixtures.browser.init(async ({ browserType, browserOptions }, run) => { - const browser = await browserType.launch(browserOptions); - await run(browser); - await browser.close(); -}, { scope: 'worker' }); - -fixtures.isChromium.init(async ({ browserName }, run) => { - await run(browserName === 'chromium'); -}, { scope: 'worker' }); - -fixtures.isFirefox.init(async ({ browserName }, run) => { - await run(browserName === 'firefox'); -}, { scope: 'worker' }); - -fixtures.isWebKit.init(async ({ browserName }, run) => { - await run(browserName === 'webkit'); -}, { scope: 'worker' }); - -fixtures.isWindows.init(async ({ platform }, run) => { - await run(platform === 'win32'); -}, { scope: 'worker' }); - -fixtures.isMac.init(async ({ platform }, run) => { - await run(platform === 'darwin'); -}, { scope: 'worker' }); - -fixtures.isLinux.init(async ({ platform }, run) => { - await run(platform === 'linux'); -}, { scope: 'worker' }); - -fixtures.contextOptions.init(async ({ video, testInfo }, run) => { - await run({ - recordVideo: video ? { dir: testInfo.outputPath('') } : undefined, - _traceDir: process.env.PWTRACE ? testInfo.outputPath('') : undefined, - } as any); -}); - -fixtures.contextFactory.init(async ({ browser, contextOptions, testInfo, screenshotOnFailure }, run) => { - const contexts: BrowserContext[] = []; - async function contextFactory(options: BrowserContextOptions = {}) { - const context = await browser.newContext({ ...contextOptions, ...options }); - contexts.push(context); - return context; - } - await run(contextFactory); - - if (screenshotOnFailure && (testInfo.status !== testInfo.expectedStatus)) { - let ordinal = 0; - for (const context of contexts) { - for (const page of context.pages()) - await page.screenshot({ timeout: 5000, path: testInfo.outputPath(`test-failed-${++ordinal}.png`) }); - } - } - for (const context of contexts) - await context.close(); -}); - -fixtures.context.init(async ({ contextFactory }, run) => { - const context = await contextFactory(); - await run(context); - // Context factory is taking care of closing the context, - // so that it could capture a screenshot on failure. -}); - -fixtures.page.init(async ({ context }, run) => { - // Always create page off context so that they matched. - await run(await context.newPage()); - // Context fixture is taking care of closing the page. -}); - -fixtures.testParametersPathSegment.override(async ({ browserName, platform }, run) => { - await run(browserName + '-' + platform); -}); - -export const folio = fixtures.build(); -export const it = folio.it; -export const fit = folio.fit; -export const xit = folio.xit; -export const test = folio.test; -export const describe = folio.describe; -export const beforeEach = folio.beforeEach; -export const afterEach = folio.afterEach; -export const beforeAll = folio.beforeAll; -export const afterAll = folio.afterAll; - -// If browser is not specified, we are running tests against all three browsers. - -folio.generateParametrizedTests( - 'browserName', - process.env.BROWSER ? [process.env.BROWSER] as any : ['chromium', 'webkit', 'firefox']); diff --git a/test/utils.ts b/test/utils.ts index a2e45f08c8aaa..45fc1003dcc5b 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { expect } from './fixtures'; +import { expect } from 'folio'; import type { Frame, Page } from '../index'; export async function attachFrame(page: Page, frameId: string, url: string): Promise { diff --git a/tests/config/android.config.ts b/tests/config/android.config.ts index 451f5484f4637..b25b4418080c3 100644 --- a/tests/config/android.config.ts +++ b/tests/config/android.config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setConfig, Config } from '../folio/out'; +import { setConfig, Config } from 'folio'; import * as path from 'path'; import { test as pageTest } from './pageTest'; import { test as androidTest } from './androidTest'; diff --git a/tests/config/androidEnv.ts b/tests/config/androidEnv.ts index 061605bf87510..726918eec787a 100644 --- a/tests/config/androidEnv.ts +++ b/tests/config/androidEnv.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Env, WorkerInfo, TestInfo } from '../folio/out'; +import type { Env, WorkerInfo, TestInfo } from 'folio'; import type { AndroidDevice, BrowserContext } from '../../index'; import * as os from 'os'; import { AndroidTestArgs } from './androidTest'; diff --git a/tests/config/androidTest.ts b/tests/config/androidTest.ts index 07de5bb1eeb49..86abb6d61e005 100644 --- a/tests/config/androidTest.ts +++ b/tests/config/androidTest.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { newTestType } from '../folio/out'; +import { newTestType } from 'folio'; import type { AndroidDevice } from '../../index'; import type { CommonTestArgs } from './pageTest'; import type { ServerTestArgs } from './serverTest'; -export { expect } from '../folio/out'; +export { expect } from 'folio'; export type AndroidTestArgs = CommonTestArgs & { androidDevice: AndroidDevice; diff --git a/tests/config/browserEnv.ts b/tests/config/browserEnv.ts index 907fb49782f3d..8813cfbc06b56 100644 --- a/tests/config/browserEnv.ts +++ b/tests/config/browserEnv.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Env, WorkerInfo, TestInfo } from '../folio/out'; +import type { Env, WorkerInfo, TestInfo } from 'folio'; import type { Browser, BrowserContext, BrowserContextOptions, BrowserType, LaunchOptions } from '../../index'; import { installCoverageHooks } from '../../test/coverage'; import { start } from '../../lib/outofprocess'; diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 29945fa462cdf..932a4d4a4d5fe 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { newTestType } from '../folio/out'; +import { newTestType } from 'folio'; import type { Browser, BrowserContextOptions, BrowserContext, Page } from '../../index'; import type { PlaywrightTestArgs } from './playwrightTest'; import type { ServerTestArgs } from './serverTest'; -export { expect } from '../folio/out'; +export { expect } from 'folio'; export type BrowserTestArgs = PlaywrightTestArgs & { browser: Browser; diff --git a/tests/config/cliEnv.ts b/tests/config/cliEnv.ts index d742bb71c8966..85ec2c25d4e0f 100644 --- a/tests/config/cliEnv.ts +++ b/tests/config/cliEnv.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Env, TestInfo, WorkerInfo } from '../folio/out'; +import type { Env, TestInfo, WorkerInfo } from 'folio'; import { PageEnv } from './browserEnv'; import { CLIMock, CLITestArgs, Recorder } from './cliTest'; import * as http from 'http'; diff --git a/tests/config/cliTest.ts b/tests/config/cliTest.ts index f20c4c6d0a869..9ea2f01d9d6de 100644 --- a/tests/config/cliTest.ts +++ b/tests/config/cliTest.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { newTestType } from '../folio/out'; +import { newTestType } from 'folio'; import type { Page, BrowserContext } from '../../index'; import type { ServerTestArgs } from './serverTest'; import type { BrowserTestArgs } from './browserTest'; @@ -22,7 +22,7 @@ import * as http from 'http'; import * as path from 'path'; import type { Source } from '../../src/server/supplements/recorder/recorderTypes'; import { ChildProcess, spawn } from 'child_process'; -export { expect } from '../folio/out'; +export { expect } from 'folio'; interface CLIHTTPServer { setHandler: (handler: http.RequestListener) => void diff --git a/tests/config/default.config.ts b/tests/config/default.config.ts index 7e88194311338..3e6d12a3d5451 100644 --- a/tests/config/default.config.ts +++ b/tests/config/default.config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setConfig, Config } from '../folio/out'; +import { setConfig, Config } from 'folio'; import * as path from 'path'; import { test as playwrightTest, slowTest as playwrightSlowTest } from './playwrightTest'; import { test as browserTest, contextTest, proxyTest, slowTest as browserSlowTest } from './browserTest'; diff --git a/tests/config/electronEnv.ts b/tests/config/electronEnv.ts index d991cd73e7200..dd34a04758240 100644 --- a/tests/config/electronEnv.ts +++ b/tests/config/electronEnv.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Env, TestInfo } from '../folio/out'; +import type { Env, TestInfo } from 'folio'; import { PlaywrightEnv } from './browserEnv'; import * as path from 'path'; import { ElectronTestArgs } from './electronTest'; diff --git a/tests/config/electronTest.ts b/tests/config/electronTest.ts index 71db54f0d1c1c..1942ffc28141f 100644 --- a/tests/config/electronTest.ts +++ b/tests/config/electronTest.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { newTestType } from '../folio/out'; +import { newTestType } from 'folio'; import { ElectronApplication, Page } from '../../index'; import type { CommonTestArgs } from './pageTest'; import type { ServerTestArgs } from './serverTest'; -export { expect } from '../folio/out'; +export { expect } from 'folio'; export type ElectronTestArgs = CommonTestArgs & { electronApp: ElectronApplication; diff --git a/tests/config/pageTest.ts b/tests/config/pageTest.ts index 45e18d33e279b..520cd76512af6 100644 --- a/tests/config/pageTest.ts +++ b/tests/config/pageTest.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { newTestType } from '../folio/out'; +import { newTestType } from 'folio'; import type { Page } from '../../index'; import type { ServerTestArgs } from './serverTest'; -export { expect } from '../folio/out'; +export { expect } from 'folio'; export type CommonTestArgs = { mode: 'default' | 'driver' | 'service'; diff --git a/tests/config/playwrightTest.ts b/tests/config/playwrightTest.ts index fb8669aa41262..d05f319cbcfad 100644 --- a/tests/config/playwrightTest.ts +++ b/tests/config/playwrightTest.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { newTestType } from '../folio/out'; +import { newTestType } from 'folio'; import type { Browser, BrowserType, LaunchOptions, BrowserContext, Page } from '../../index'; import { CommonTestArgs } from './pageTest'; import type { ServerTestArgs } from './serverTest'; import { RemoteServer, RemoteServerOptions } from './remoteServer'; -export { expect } from '../folio/out'; +export { expect } from 'folio'; export type PlaywrightTestArgs = CommonTestArgs & { browserType: BrowserType; diff --git a/tests/config/serverEnv.ts b/tests/config/serverEnv.ts index 99657ef15039d..15c8f453c9e00 100644 --- a/tests/config/serverEnv.ts +++ b/tests/config/serverEnv.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { WorkerInfo, TestInfo, Env } from '../folio/out'; +import type { WorkerInfo, TestInfo, Env } from 'folio'; import { TestServer } from '../../utils/testserver'; import * as path from 'path'; import socks from 'socksv5'; diff --git a/tests/folio/.gitignore b/tests/folio/.gitignore deleted file mode 100644 index 89f9ac04aac6c..0000000000000 --- a/tests/folio/.gitignore +++ /dev/null @@ -1 +0,0 @@ -out/ diff --git a/tests/folio/cli.js b/tests/folio/cli.js deleted file mode 100755 index a46c799d045d5..0000000000000 --- a/tests/folio/cli.js +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node - -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -require('./out/cli'); \ No newline at end of file diff --git a/tests/folio/src/cli.ts b/tests/folio/src/cli.ts deleted file mode 100644 index 988c73ce2a68a..0000000000000 --- a/tests/folio/src/cli.ts +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { default as ignore } from 'fstream-ignore'; -import * as commander from 'commander'; -import * as fs from 'fs'; -import * as path from 'path'; -import EmptyReporter from './reporters/empty'; -import DotReporter from './reporters/dot'; -import JSONReporter from './reporters/json'; -import JUnitReporter from './reporters/junit'; -import LineReporter from './reporters/line'; -import ListReporter from './reporters/list'; -import { Multiplexer } from './reporters/multiplexer'; -import { Runner } from './runner'; -import { Config, FullConfig, Reporter } from './types'; -import { Loader } from './loader'; -import { createMatcher } from './util'; - -export const reporters: { [name: string]: new () => Reporter } = { - 'dot': DotReporter, - 'json': JSONReporter, - 'junit': JUnitReporter, - 'line': LineReporter, - 'list': ListReporter, - 'null': EmptyReporter, -}; - -const availableReporters = Object.keys(reporters).map(r => `"${r}"`).join(); - -const defaultConfig: FullConfig = { - forbidOnly: false, - globalTimeout: 0, - grep: /.*/, - maxFailures: 0, - outputDir: path.resolve(process.cwd(), 'test-results'), - quiet: false, - repeatEach: 1, - retries: 0, - shard: null, - snapshotDir: '__snapshots__', - testDir: path.resolve(process.cwd()), - testIgnore: 'node_modules/**', - testMatch: '**/?(*.)+(spec|test).[jt]s', - timeout: 10000, - updateSnapshots: false, - workers: Math.ceil(require('os').cpus().length / 2), -}; - -const loadProgram = new commander.Command(); -loadProgram.helpOption(false); -addRunnerOptions(loadProgram); -loadProgram.action(async command => { - try { - await runTests(command); - } catch (e) { - console.log(e); - process.exit(1); - } -}); -loadProgram.parse(process.argv); - -async function runTests(command: any) { - if (command.help === undefined) { - console.log(loadProgram.helpInformation()); - process.exit(0); - } - - const reporterList: string[] = command.reporter.split(','); - const reporterObjects: Reporter[] = reporterList.map(c => { - if (reporters[c]) - return new reporters[c](); - try { - const p = path.resolve(process.cwd(), c); - return new (require(p).default)(); - } catch (e) { - console.error('Invalid reporter ' + c, e); - process.exit(1); - } - }); - - const loader = new Loader(); - loader.addConfig(defaultConfig); - - function loadConfig(configName: string) { - const configFile = path.resolve(process.cwd(), configName); - if (fs.existsSync(configFile)) { - loader.loadConfigFile(configFile); - return true; - } - return false; - } - - if (command.config) { - if (!loadConfig(command.config)) - throw new Error(`${command.config} does not exist`); - } else if (!loadConfig('folio.config.ts') && !loadConfig('folio.config.js')) { - throw new Error(`Configuration file not found. Either pass --config, or create folio.config.(js|ts) file`); - } - - loader.addConfig(configFromCommand(command)); - loader.addConfig({ testMatch: normalizeFilePatterns(loader.config().testMatch) }); - loader.addConfig({ testIgnore: normalizeFilePatterns(loader.config().testIgnore) }); - - const testDir = loader.config().testDir; - if (!fs.existsSync(testDir)) - throw new Error(`${testDir} does not exist`); - if (!fs.statSync(testDir).isDirectory()) - throw new Error(`${testDir} is not a directory`); - - const allAliases = new Set(loader.runLists().map(s => s.alias)); - const runListFilter: string[] = []; - const testFileFilter: string[] = []; - for (const arg of command.args) { - if (allAliases.has(arg)) - runListFilter.push(arg); - else - testFileFilter.push(arg); - } - - const allFiles = await collectFiles(testDir); - const testFiles = filterFiles(testDir, allFiles, testFileFilter, createMatcher(loader.config().testMatch), createMatcher(loader.config().testIgnore)); - for (const file of testFiles) - loader.loadTestFile(file); - - const reporter = new Multiplexer(reporterObjects); - const runner = new Runner(loader, reporter, runListFilter.length ? runListFilter : undefined); - - if (command.list) { - runner.list(); - return; - } - - const result = await runner.run(); - if (result === 'sigint') - process.exit(130); - - if (result === 'forbid-only') { - console.error('====================================='); - console.error(' --forbid-only found a focused test.'); - console.error('====================================='); - process.exit(1); - } - if (result === 'no-tests') { - console.error('================='); - console.error(' no tests found.'); - console.error('================='); - process.exit(1); - } - process.exit(result === 'failed' ? 1 : 0); -} - -async function collectFiles(testDir: string): Promise { - const entries: any[] = []; - let callback = () => {}; - const promise = new Promise(f => callback = f); - ignore({ path: testDir, ignoreFiles: ['.gitignore'] }) - .on('child', (entry: any) => entries.push(entry)) - .on('end', callback); - await promise; - return entries.filter(e => e.type === 'File').sort((a, b) => { - if (a.depth !== b.depth && (a.dirname.startsWith(b.dirname) || b.dirname.startsWith(a.dirname))) - return a.depth - b.depth; - return a.path > b.path ? 1 : (a.path < b.path ? -1 : 0); - }).map(e => e.path); -} - -function filterFiles(base: string, files: string[], filters: string[], filesMatch: (value: string) => boolean, filesIgnore: (value: string) => boolean): string[] { - return files.filter(file => { - file = path.relative(base, file); - if (filesIgnore(file)) - return false; - if (!filesMatch(file)) - return false; - if (filters.length && !filters.find(filter => file.includes(filter))) - return false; - return true; - }); -} - -function addRunnerOptions(program: commander.Command) { - program = program - .version('Version alpha') - .option('-c, --config ', `Configuration file (default: "folio.config.ts" or "folio.config.js")`) - .option('--forbid-only', `Fail if exclusive test(s) encountered (default: ${defaultConfig.forbidOnly})`) - .option('-g, --grep ', `Only run tests matching this string or regexp (default: "${defaultConfig.grep}")`) - .option('--global-timeout ', `Specify maximum time this test suite can run in milliseconds (default: 0 for unlimited)`) - .option('-h, --help', `Display help`) - .option('-j, --workers ', `Number of concurrent workers, use 1 to run in single worker (default: number of CPU cores / 2)`) - .option('--list', `Only collect all the test and report them`) - .option('--max-failures ', `Stop after the first N failures (default: ${defaultConfig.maxFailures})`) - .option('--output ', `Folder for output artifacts (default: "test-results")`) - .option('--quiet', `Suppress stdio`) - .option('--repeat-each ', `Specify how many times to run the tests (default: ${defaultConfig.repeatEach})`) - .option('--reporter ', `Specify reporter to use, comma-separated, can be ${availableReporters}`, process.env.CI ? 'dot' : 'line') - .option('--retries ', `Specify retry count (default: ${defaultConfig.retries})`) - .option('--shard ', `Shard tests and execute only selected shard, specify in the form "current/all", 1-based, for example "3/5"`) - .option('--snapshot-dir ', `Snapshot directory, relative to tests directory (default: "${defaultConfig.snapshotDir}"`) - .option('--test-dir ', `Directory containing test files (default: current directory)`) - .option('--test-ignore ', `Pattern used to ignore test files (default: "${defaultConfig.testIgnore}")`) - .option('--test-match ', `Pattern used to find test files (default: "${defaultConfig.testMatch}")`) - .option('--timeout ', `Specify test timeout threshold in milliseconds (default: ${defaultConfig.timeout})`) - .option('-u, --update-snapshots', `Whether to update snapshots with actual results (default: ${defaultConfig.updateSnapshots})`) - .option('-x', `Stop after the first failure`); -} - -function configFromCommand(command: any): Config { - const config: Config = {}; - if (command.forbidOnly) - config.forbidOnly = true; - if (command.globalTimeout) - config.globalTimeout = parseInt(command.globalTimeout, 10); - if (command.grep) - config.grep = maybeRegExp(command.grep); - if (command.maxFailures || command.x) - config.maxFailures = command.x ? 1 : parseInt(command.maxFailures, 10); - if (command.output) - config.outputDir = path.resolve(process.cwd(), command.output); - if (command.quiet) - config.quiet = command.quiet; - if (command.repeatEach) - config.repeatEach = parseInt(command.repeatEach, 10); - if (command.retries) - config.retries = parseInt(command.retries, 10); - if (command.shard) { - const pair = command.shard.split('/').map((t: string) => parseInt(t, 10)); - config.shard = { current: pair[0] - 1, total: pair[1] }; - } - if (command.snapshotDir) - config.snapshotDir = command.snapshotDir; - if (command.testDir) - config.testDir = path.resolve(process.cwd(), command.testDir); - if (command.testMatch) - config.testMatch = maybeRegExp(command.testMatch); - if (command.testIgnore) - config.testIgnore = maybeRegExp(command.testIgnore); - if (command.timeout) - config.timeout = parseInt(command.timeout, 10); - if (command.updateSnapshots) - config.updateSnapshots = !!command.updateSnapshots; - if (command.workers) - config.workers = parseInt(command.workers, 10); - return config; -} - -function normalizeFilePattern(pattern: string): string { - if (!pattern.includes('/') && !pattern.includes('\\')) - pattern = '**/' + pattern; - return pattern; -} - -function normalizeFilePatterns(patterns: string | RegExp | (string | RegExp)[]) { - if (typeof patterns === 'string') - patterns = normalizeFilePattern(patterns); - else if (Array.isArray(patterns)) - patterns = patterns.map(item => typeof item === 'string' ? normalizeFilePattern(item) : item); - return patterns; -} - -function maybeRegExp(pattern: string): string | RegExp { - const match = pattern.match(/^\/(.*)\/([gi]*)$/); - if (match) - return new RegExp(match[1], match[2]); - return pattern; -} diff --git a/tests/folio/src/dispatcher.ts b/tests/folio/src/dispatcher.ts deleted file mode 100644 index 5bd64eb04fcb1..0000000000000 --- a/tests/folio/src/dispatcher.ts +++ /dev/null @@ -1,392 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import child_process from 'child_process'; -import path from 'path'; -import { EventEmitter } from 'events'; -import { RunPayload, TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, TestStatus, WorkerInitParams } from './ipc'; -import { TestResult, Reporter } from './types'; -import { Suite, Test } from './test'; -import { Loader } from './loader'; - -type DispatcherEntry = { - runPayload: RunPayload; - hash: string; - repeatEachIndex: number; - runListIndex: number; -}; - -export class Dispatcher { - private _workers = new Set(); - private _freeWorkers: Worker[] = []; - private _workerClaimers: (() => void)[] = []; - - private _testById = new Map(); - private _queue: DispatcherEntry[] = []; - private _stopCallback: () => void; - readonly _loader: Loader; - private _suite: Suite; - private _reporter: Reporter; - private _hasWorkerErrors = false; - private _isStopped = false; - private _failureCount = 0; - private _didRunGlobalSetup = false; - _globalSetupResult: any = undefined; - - constructor(loader: Loader, suite: Suite, reporter: Reporter) { - this._loader = loader; - this._reporter = reporter; - - this._suite = suite; - for (const suite of this._suite.suites) { - for (const spec of suite._allSpecs()) { - for (const test of spec.tests) - this._testById.set(test._id, { test, result: test._appendTestResult() }); - } - } - - this._queue = this._filesSortedByWorkerHash(); - - // Shard tests. - let total = this._suite.totalTestCount(); - let shardDetails = ''; - - const shard = this._loader.config().shard; - if (shard) { - const shardSize = Math.ceil(total / shard.total); - const from = shardSize * shard.current; - const to = shardSize * (shard.current + 1); - shardDetails = `, shard ${shard.current + 1} of ${shard.total}`; - let current = 0; - total = 0; - const filteredQueue: DispatcherEntry[] = []; - for (const entry of this._queue) { - if (current >= from && current < to) { - filteredQueue.push(entry); - total += entry.runPayload.entries.length; - } - current += entry.runPayload.entries.length; - } - this._queue = filteredQueue; - } - - if (process.stdout.isTTY) { - const workers = new Set(); - suite.findSpec(test => { - for (const variant of test.tests) - workers.add(test.file + variant._workerHash); - }); - console.log(); - const jobs = Math.min(this._loader.config().workers, workers.size); - console.log(`Running ${total} test${total > 1 ? 's' : ''} using ${jobs} worker${jobs > 1 ? 's' : ''}${shardDetails}`); - } - } - - _filesSortedByWorkerHash(): DispatcherEntry[] { - const entriesByWorkerHashAndFile = new Map>(); - for (const suite of this._suite.suites) { - for (const spec of suite._allSpecs()) { - for (const test of spec.tests) { - let entriesByFile = entriesByWorkerHashAndFile.get(test._workerHash); - if (!entriesByFile) { - entriesByFile = new Map(); - entriesByWorkerHashAndFile.set(test._workerHash, entriesByFile); - } - let entry = entriesByFile.get(spec.file); - if (!entry) { - entry = { - runPayload: { - entries: [], - file: spec.file, - }, - repeatEachIndex: test._repeatEachIndex, - runListIndex: test._runListIndex, - hash: test._workerHash, - }; - entriesByFile.set(spec.file, entry); - } - entry.runPayload.entries.push({ - retry: this._testById.get(test._id).result.retry, - testId: test._id, - }); - } - } - } - - const result: DispatcherEntry[] = []; - for (const entriesByFile of entriesByWorkerHashAndFile.values()) { - for (const entry of entriesByFile.values()) - result.push(entry); - } - result.sort((a, b) => a.hash < b.hash ? -1 : (a.hash === b.hash ? 0 : 1)); - return result; - } - - async run() { - if (this._loader.globalSetup) { - this._didRunGlobalSetup = true; - this._globalSetupResult = await this._loader.globalSetup(); - } - // Loop in case job schedules more jobs - while (this._queue.length && !this._isStopped) - await this._dispatchQueue(); - } - - async _dispatchQueue() { - const jobs = []; - while (this._queue.length) { - if (this._isStopped) - break; - const entry = this._queue.shift(); - const requiredHash = entry.hash; - let worker = await this._obtainWorker(entry); - while (!this._isStopped && worker.hash && worker.hash !== requiredHash) { - worker.stop(); - worker = await this._obtainWorker(entry); - } - if (this._isStopped) - break; - jobs.push(this._runJob(worker, entry)); - } - await Promise.all(jobs); - } - - async _runJob(worker: Worker, entry: DispatcherEntry) { - worker.run(entry.runPayload); - let doneCallback; - const result = new Promise(f => doneCallback = f); - worker.once('done', (params: DonePayload) => { - // We won't file remaining if: - // - there are no remaining - // - we are here not because something failed - // - no unrecoverable worker error - if (!params.remaining.length && !params.failedTestId && !params.fatalError) { - this._freeWorkers.push(worker); - this._notifyWorkerClaimer(); - doneCallback(); - return; - } - - // When worker encounters error, we will stop it and create a new one. - worker.stop(); - - let remaining = params.remaining; - const failedTestIds = new Set(); - - // In case of fatal error, report all remaining tests as failing with this error. - if (params.fatalError) { - for (const { testId } of remaining) { - const { test, result } = this._testById.get(testId); - this._reporter.onTestBegin(test); - result.error = params.fatalError; - this._reportTestEnd(test, result, 'failed'); - failedTestIds.add(testId); - } - // Since we pretent that all remaining tests failed, there is nothing else to run, - // except for possible retries. - remaining = []; - } - if (params.failedTestId) - failedTestIds.add(params.failedTestId); - - // Only retry expected failures, not passes and only if the test failed. - for (const testId of failedTestIds) { - const pair = this._testById.get(testId); - if (pair.test.expectedStatus === 'passed' && pair.test.results.length < this._loader.config().retries + 1) { - pair.result = pair.test._appendTestResult(); - remaining.unshift({ - retry: pair.result.retry, - testId: pair.test._id, - }); - } - } - - if (remaining.length) - this._queue.unshift({ ...entry, runPayload: { ...entry.runPayload, entries: remaining } }); - - // This job is over, we just scheduled another one. - doneCallback(); - }); - return result; - } - - async _obtainWorker(entry: DispatcherEntry) { - const claimWorker = (): Promise | null => { - // Use available worker. - if (this._freeWorkers.length) - return Promise.resolve(this._freeWorkers.pop()); - // Create a new worker. - if (this._workers.size < this._loader.config().workers) - return this._createWorker(entry); - return null; - }; - - // Note: it is important to claim the worker synchronously, - // so that we won't miss a _notifyWorkerClaimer call while awaiting. - let worker = claimWorker(); - if (!worker) { - // Wait for available or stopped worker. - await new Promise(f => this._workerClaimers.push(f)); - worker = claimWorker(); - } - return worker; - } - - async _notifyWorkerClaimer() { - if (this._isStopped || !this._workerClaimers.length) - return; - const callback = this._workerClaimers.shift(); - callback(); - } - - _createWorker(entry: DispatcherEntry) { - const worker = new Worker(this); - worker.on('testBegin', (params: TestBeginPayload) => { - const { test, result: testRun } = this._testById.get(params.testId); - testRun.workerIndex = params.workerIndex; - this._reporter.onTestBegin(test); - }); - worker.on('testEnd', (params: TestEndPayload) => { - const { test, result } = this._testById.get(params.testId); - result.data = params.data; - result.duration = params.duration; - result.error = params.error; - test.expectedStatus = params.expectedStatus; - test.annotations = params.annotations; - test.timeout = params.timeout; - if (params.expectedStatus === 'skipped') - test.skipped = true; - this._reportTestEnd(test, result, params.status); - }); - worker.on('stdOut', (params: TestOutputPayload) => { - const chunk = chunkFromParams(params); - const pair = this._testById.get(params.testId); - if (pair) - pair.result.stdout.push(chunk); - this._reporter.onStdOut(chunk, pair ? pair.test : undefined); - }); - worker.on('stdErr', (params: TestOutputPayload) => { - const chunk = chunkFromParams(params); - const pair = this._testById.get(params.testId); - if (pair) - pair.result.stderr.push(chunk); - this._reporter.onStdErr(chunk, pair ? pair.test : undefined); - }); - worker.on('teardownError', ({error}) => { - this._hasWorkerErrors = true; - this._reporter.onError(error); - }); - worker.on('exit', () => { - this._workers.delete(worker); - this._notifyWorkerClaimer(); - if (this._stopCallback && !this._workers.size) - this._stopCallback(); - }); - this._workers.add(worker); - return worker.init(entry).then(() => worker); - } - - async stop() { - this._isStopped = true; - if (this._workers.size) { - const result = new Promise(f => this._stopCallback = f); - for (const worker of this._workers) - worker.stop(); - await result; - } - if (this._didRunGlobalSetup && this._loader.globalTeardown) - await this._loader.globalTeardown(this._globalSetupResult); - } - - private _reportTestEnd(test: Test, result: TestResult, status: TestStatus) { - if (this._isStopped) - return; - result.status = status; - if (result.status !== 'skipped' && result.status !== test.expectedStatus) - ++this._failureCount; - const maxFailures = this._loader.config().maxFailures; - if (!maxFailures || this._failureCount <= maxFailures) - this._reporter.onTestEnd(test, result); - if (maxFailures && this._failureCount === maxFailures) - this._isStopped = true; - } - - hasWorkerErrors(): boolean { - return this._hasWorkerErrors; - } -} - -let lastWorkerIndex = 0; - -class Worker extends EventEmitter { - process: child_process.ChildProcess; - runner: Dispatcher; - hash: string; - index: number; - stdout: any[]; - stderr: any[]; - - constructor(runner: Dispatcher) { - super(); - this.runner = runner; - this.index = lastWorkerIndex++; - - this.process = child_process.fork(path.join(__dirname, 'worker.js'), { - detached: false, - env: { - FORCE_COLOR: process.stdout.isTTY ? '1' : '0', - DEBUG_COLORS: process.stdout.isTTY ? '1' : '0', - FOLIO_WORKER_INDEX: String(this.index), - ...process.env - }, - // Can't pipe since piping slows down termination for some reason. - stdio: ['ignore', 'ignore', process.env.PW_RUNNER_DEBUG ? 'inherit' : 'ignore', 'ipc'] - }); - this.process.on('exit', () => this.emit('exit')); - this.process.on('error', e => {}); // do not yell at a send to dead process. - this.process.on('message', (message: any) => { - const { method, params } = message; - this.emit(method, params); - }); - } - - async init(entry: DispatcherEntry) { - this.hash = entry.hash; - const params: WorkerInitParams = { - workerIndex: this.index, - repeatEachIndex: entry.repeatEachIndex, - runListIndex: entry.runListIndex, - globalSetupResult: this.runner._globalSetupResult, - loader: this.runner._loader.serialize(), - }; - this.process.send({ method: 'init', params }); - await new Promise(f => this.process.once('message', f)); // Ready ack - } - - run(runPayload: RunPayload) { - this.process.send({ method: 'run', params: runPayload }); - } - - stop() { - this.process.send({ method: 'stop' }); - } -} - -function chunkFromParams(params: TestOutputPayload): string | Buffer { - if (typeof params.text === 'string') - return params.text; - return Buffer.from(params.buffer, 'base64'); -} diff --git a/tests/folio/src/expect.ts b/tests/folio/src/expect.ts deleted file mode 100644 index 09cffa98d92ab..0000000000000 --- a/tests/folio/src/expect.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Expect } from './expectType'; -import expectLibrary from 'expect'; -import { currentTestInfo } from './globals'; -import { compare } from './golden'; - -export const expect: Expect = expectLibrary; - -const snapshotOrdinalSymbol = Symbol('snapshotOrdinalSymbol'); - -function toMatchSnapshot(received: Buffer | string, nameOrOptions?: string | { name?: string, threshold?: number }, optOptions: { threshold?: number } = {}) { - let options: { name?: string, threshold?: number }; - const testInfo = currentTestInfo(); - if (typeof nameOrOptions === 'string') - options = { name: nameOrOptions, ...optOptions }; - else - options = { ...nameOrOptions }; - - let name = options.name; - if (!name) { - const ordinal = (testInfo as any)[snapshotOrdinalSymbol] || 0; - (testInfo as any)[snapshotOrdinalSymbol] = ordinal + 1; - let extension: string; - if (typeof received === 'string') - extension = '.txt'; - else if (received[0] === 0x89 && received[1] === 0x50 && received[2] === 0x4E && received[3] === 0x47) - extension = '.png'; - else if (received[0] === 0xFF && received[1] === 0xD8 && received[2] === 0xFF) - extension = '.jpeg'; - else - extension = '.dat'; - name = 'snapshot' + (ordinal ? '_' + ordinal : '') + extension; - } - const { pass, message } = compare(received, name, testInfo.snapshotPath, testInfo.outputPath, testInfo.config.updateSnapshots, options); - return { pass, message: () => message }; -} - -expectLibrary.extend({ toMatchSnapshot }); - -// TEMPORARY HACK: two folios currently fight for their own toMatchSnapshot. -export function extendAgain() { - expectLibrary.extend({ toMatchSnapshot }); -} diff --git a/tests/folio/src/expectType.ts b/tests/folio/src/expectType.ts deleted file mode 100644 index bf69140485954..0000000000000 --- a/tests/folio/src/expectType.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export declare type AsymmetricMatcher = Record; -export declare type Expect = { - (actual: T): Matchers; - [id: string]: AsymmetricMatcher; - not: { - [id: string]: AsymmetricMatcher; - }; -}; - -export interface Matchers { - /** - * If you know how to test something, `.not` lets you test its opposite. - */ - not: Matchers; - /** - * Use resolves to unwrap the value of a fulfilled promise so any other - * matcher can be chained. If the promise is rejected the assertion fails. - */ - resolves: Matchers>; - /** - * Unwraps the reason of a rejected promise so any other matcher can be chained. - * If the promise is fulfilled the assertion fails. - */ - rejects: Matchers>; - /** - * Checks that a value is what you expect. It uses `===` to check strict equality. - * Don't use `toBe` with floating-point numbers. - */ - toBe(expected: unknown): R; - /** - * Using exact equality with floating point numbers is a bad idea. - * Rounding means that intuitive things fail. - * The default for numDigits is 2. - */ - toBeCloseTo(expected: number, numDigits?: number): R; - /** - * Ensure that a variable is not undefined. - */ - toBeDefined(): R; - /** - * When you don't care what a value is, you just want to - * ensure a value is false in a boolean context. - */ - toBeFalsy(): R; - /** - * For comparing floating point numbers. - */ - toBeGreaterThan(expected: number | bigint): R; - /** - * For comparing floating point numbers. - */ - toBeGreaterThanOrEqual(expected: number | bigint): R; - /** - * Ensure that an object is an instance of a class. - * This matcher uses `instanceof` underneath. - */ - toBeInstanceOf(expected: Function): R; - /** - * For comparing floating point numbers. - */ - toBeLessThan(expected: number | bigint): R; - /** - * For comparing floating point numbers. - */ - toBeLessThanOrEqual(expected: number | bigint): R; - /** - * This is the same as `.toBe(null)` but the error messages are a bit nicer. - * So use `.toBeNull()` when you want to check that something is null. - */ - toBeNull(): R; - /** - * Use when you don't care what a value is, you just want to ensure a value - * is true in a boolean context. In JavaScript, there are six falsy values: - * `false`, `0`, `''`, `null`, `undefined`, and `NaN`. Everything else is truthy. - */ - toBeTruthy(): R; - /** - * Used to check that a variable is undefined. - */ - toBeUndefined(): R; - /** - * Used to check that a variable is NaN. - */ - toBeNaN(): R; - /** - * Used when you want to check that an item is in a list. - * For testing the items in the list, this uses `===`, a strict equality check. - */ - toContain(expected: unknown): R; - /** - * Used when you want to check that an item is in a list. - * For testing the items in the list, this matcher recursively checks the - * equality of all fields, rather than checking for object identity. - */ - toContainEqual(expected: unknown): R; - /** - * Used when you want to check that two objects have the same value. - * This matcher recursively checks the equality of all fields, rather than checking for object identity. - */ - toEqual(expected: unknown): R; - /** - * Use to check if property at provided reference keyPath exists for an object. - * For checking deeply nested properties in an object you may use dot notation or an array containing - * the keyPath for deep references. - * - * Optionally, you can provide a value to check if it's equal to the value present at keyPath - * on the target object. This matcher uses 'deep equality' (like `toEqual()`) and recursively checks - * the equality of all fields. - * - * @example - * - * expect(houseForSale).toHaveProperty('kitchen.area', 20); - */ - toHaveProperty(keyPath: string | Array, value?: unknown): R; - /** - * Check that a string matches a regular expression. - */ - toMatch(expected: string | RegExp): R; - /** - * Used to check that a JavaScript object matches a subset of the properties of an object - */ - toMatchObject(expected: Record | Array): R; - /** - * Use to test that objects have the same types as well as structure. - */ - toStrictEqual(expected: unknown): R; - /** - * Match snapshot - */ - toMatchSnapshot(options?: { - name?: string, - threshold?: number - }): R; - /** - * Match snapshot - */ - toMatchSnapshot(name: string, options?: { - threshold?: number - }): R; -} -export {}; diff --git a/tests/folio/src/globals.ts b/tests/folio/src/globals.ts deleted file mode 100644 index 43733abd0c73d..0000000000000 --- a/tests/folio/src/globals.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { TestInfo } from './types'; - -let currentTestInfoValue: TestInfo | null = null; -export function setCurrentTestInfo(testInfo: TestInfo | null) { - currentTestInfoValue = testInfo; -} -export function currentTestInfo(): TestInfo | null { - return currentTestInfoValue; -} diff --git a/tests/folio/src/golden.ts b/tests/folio/src/golden.ts deleted file mode 100644 index a20f9786a32c9..0000000000000 --- a/tests/folio/src/golden.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import colors from 'colors/safe'; -import fs from 'fs'; -import path from 'path'; -import jpeg from 'jpeg-js'; -import pixelmatch from 'pixelmatch'; -import { PNG } from 'pngjs'; -import { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL } from '../third_party/diff_match_patch'; - -const extensionToMimeType: { [key: string]: string } = { - 'dat': 'application/octet-string', - 'jpeg': 'image/jpeg', - 'jpg': 'image/jpeg', - 'png': 'image/png', - 'txt': 'text/plain', -}; - -const GoldenComparators: { [key: string]: any } = { - 'application/octet-string': compareBuffers, - 'image/png': compareImages, - 'image/jpeg': compareImages, - 'text/plain': compareText, -}; - -function compareBuffers(actualBuffer: Buffer | string, expectedBuffer: Buffer, mimeType: string): { diff?: object; errorMessage?: string; } | null { - if (!actualBuffer || !(actualBuffer instanceof Buffer)) - return { errorMessage: 'Actual result should be Buffer.' }; - if (Buffer.compare(actualBuffer, expectedBuffer)) - return { errorMessage: 'Buffers differ' }; - return null; -} - -function compareImages(actualBuffer: Buffer | string, expectedBuffer: Buffer, mimeType: string, options = {}): { diff?: object; errorMessage?: string; } | null { - if (!actualBuffer || !(actualBuffer instanceof Buffer)) - return { errorMessage: 'Actual result should be Buffer.' }; - - const actual = mimeType === 'image/png' ? PNG.sync.read(actualBuffer) : jpeg.decode(actualBuffer); - const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer); - if (expected.width !== actual.width || expected.height !== actual.height) { - return { - errorMessage: `Sizes differ; expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. ` - }; - } - const diff = new PNG({width: expected.width, height: expected.height}); - const count = pixelmatch(expected.data, actual.data, diff.data, expected.width, expected.height, { threshold: 0.2, ...options }); - return count > 0 ? { diff: PNG.sync.write(diff) } : null; -} - -function compareText(actual: Buffer | string, expectedBuffer: Buffer): { diff?: object; errorMessage?: string; diffExtension?: string; } | null { - if (typeof actual !== 'string') - return { errorMessage: 'Actual result should be string' }; - const expected = expectedBuffer.toString('utf-8'); - if (expected === actual) - return null; - const dmp = new diff_match_patch(); - const d = dmp.diff_main(expected, actual); - dmp.diff_cleanupSemantic(d); - return { - errorMessage: diff_prettyTerminal(d) - }; -} - -export function compare(actual: Buffer | string, name: string, snapshotPath: (name: string) => string, outputPath: (name: string) => string, updateSnapshots: boolean, options?: { threshold?: number }): { pass: boolean; message?: string; } { - const snapshotFile = snapshotPath(name); - if (!fs.existsSync(snapshotFile)) { - fs.mkdirSync(path.dirname(snapshotFile), { recursive: true }); - fs.writeFileSync(snapshotFile, actual); - return { - pass: false, - message: snapshotFile + ' is missing in golden results, writing actual.' - }; - } - const expected = fs.readFileSync(snapshotFile); - const extension = path.extname(snapshotFile).substring(1); - const mimeType = extensionToMimeType[extension] || 'application/octet-string'; - const comparator = GoldenComparators[mimeType]; - if (!comparator) { - return { - pass: false, - message: 'Failed to find comparator with type ' + mimeType + ': ' + snapshotFile, - }; - } - - const result = comparator(actual, expected, mimeType, options); - if (!result) - return { pass: true }; - - if (updateSnapshots) { - fs.mkdirSync(path.dirname(snapshotFile), { recursive: true }); - fs.writeFileSync(snapshotFile, actual); - console.log('Updating snapshot at ' + snapshotFile); - return { - pass: true, - message: snapshotFile + ' running with --p-update-snapshots, writing actual.' - }; - } - const outputFile = outputPath(name); - const expectedPath = addSuffix(outputFile, '-expected'); - const actualPath = addSuffix(outputFile, '-actual'); - const diffPath = addSuffix(outputFile, '-diff'); - fs.writeFileSync(expectedPath, expected); - fs.writeFileSync(actualPath, actual); - if (result.diff) - fs.writeFileSync(diffPath, result.diff); - - const output = [ - colors.red(`Snapshot comparison failed:`), - ]; - if (result.errorMessage) { - output.push(''); - output.push(indent(result.errorMessage, ' ')); - } - output.push(''); - output.push(`Expected: ${colors.yellow(expectedPath)}`); - output.push(`Received: ${colors.yellow(actualPath)}`); - if (result.diff) - output.push(` Diff: ${colors.yellow(diffPath)}`); - - return { - pass: false, - message: output.join('\n'), - }; -} - -function indent(lines: string, tab: string) { - return lines.replace(/^(?=.+$)/gm, tab); -} - -function addSuffix(filePath: string, suffix: string, customExtension?: string): string { - const dirname = path.dirname(filePath); - const ext = path.extname(filePath); - const name = path.basename(filePath, ext); - return path.join(dirname, name + suffix + (customExtension || ext)); -} - -function diff_prettyTerminal(diffs) { - const html = []; - for (let x = 0; x < diffs.length; x++) { - const op = diffs[x][0]; // Operation (insert, delete, equal) - const data = diffs[x][1]; // Text of change. - const text = data; - switch (op) { - case DIFF_INSERT: - html[x] = colors.green(text); - break; - case DIFF_DELETE: - html[x] = colors.strikethrough(colors.red(text)); - break; - case DIFF_EQUAL: - html[x] = text; - break; - } - } - return html.join(''); -} diff --git a/tests/folio/src/index.ts b/tests/folio/src/index.ts deleted file mode 100644 index cbe92353054a6..0000000000000 --- a/tests/folio/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright 2019 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { TestType } from './types'; -import { newTestTypeImpl } from './spec'; - -export * from './types'; -export { expect } from './expect'; -export { setConfig, globalSetup, globalTeardown } from './spec'; - -export function newTestType(): TestType { - return newTestTypeImpl(); -} diff --git a/tests/folio/src/ipc.ts b/tests/folio/src/ipc.ts deleted file mode 100644 index 6d95a98bd32d2..0000000000000 --- a/tests/folio/src/ipc.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Config, TestStatus, TestError } from './types'; -export type { TestStatus } from './types'; - -export type WorkerInitParams = { - workerIndex: number; - repeatEachIndex: number; - runListIndex: number; - globalSetupResult: any; - loader: { - configs: (string | Config)[]; - }; -}; - -export type TestBeginPayload = { - testId: string; - workerIndex: number, -}; - -export type TestEndPayload = { - testId: string; - duration: number; - status: TestStatus; - error?: TestError; - data: any; - expectedStatus: TestStatus; - annotations: any[]; - timeout: number; -}; - -export type TestEntry = { - testId: string; - retry: number; -}; - -export type RunPayload = { - file: string; - entries: TestEntry[]; -}; - -export type DonePayload = { - failedTestId?: string; - fatalError?: any; - remaining: TestEntry[]; -}; - -export type TestOutputPayload = { - testId?: string; - text?: string; - buffer?: string; -}; diff --git a/tests/folio/src/loader.ts b/tests/folio/src/loader.ts deleted file mode 100644 index b53a687202e38..0000000000000 --- a/tests/folio/src/loader.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { installTransform } from './transform'; -import { Config, FullConfig } from './types'; -import { prependErrorMessage } from './util'; -import { configFile, setCurrentFile, RunListDescription } from './spec'; - -type SerializedLoaderData = { - configs: (string | Config)[]; -}; - -export class Loader { - globalSetup?: () => any; - globalTeardown?: (globalSetupResult: any) => any; - - private _mergedConfig: FullConfig; - private _layeredConfigs: { config: Config, source?: string }[] = []; - - constructor() { - this._mergedConfig = {} as any; - } - - deserialize(data: SerializedLoaderData) { - for (const config of data.configs) { - if (typeof config === 'string') - this.loadConfigFile(config); - else - this.addConfig(config); - } - } - - loadConfigFile(file: string) { - const revertBabelRequire = installTransform(); - try { - require(file); - this.addConfig(configFile.config || {}); - this._layeredConfigs[this._layeredConfigs.length - 1].source = file; - this.globalSetup = configFile.globalSetup; - this.globalTeardown = configFile.globalTeardown; - } catch (e) { - // Drop the stack. - throw new Error(e.message); - } finally { - revertBabelRequire(); - } - } - - addConfig(config: Config) { - this._layeredConfigs.push({ config }); - this._mergedConfig = { ...this._mergedConfig, ...config }; - } - - loadTestFile(file: string) { - const revertBabelRequire = installTransform(); - setCurrentFile(file); - try { - require(file); - } catch (e) { - prependErrorMessage(e, `Error while reading ${file}:\n`); - throw e; - } finally { - setCurrentFile(); - revertBabelRequire(); - } - } - - config(): FullConfig { - return this._mergedConfig; - } - - runLists(): RunListDescription[] { - return configFile.runLists; - } - - serialize(): SerializedLoaderData { - return { - configs: this._layeredConfigs.map(c => c.source || c.config), - }; - } -} diff --git a/tests/folio/src/reporters/base.ts b/tests/folio/src/reporters/base.ts deleted file mode 100644 index 677774aa15c8a..0000000000000 --- a/tests/folio/src/reporters/base.ts +++ /dev/null @@ -1,246 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { codeFrameColumns } from '@babel/code-frame'; -import colors from 'colors/safe'; -import fs from 'fs'; -import milliseconds from 'ms'; -import path from 'path'; -import StackUtils from 'stack-utils'; -import { TestStatus, Test, Suite, TestResult, TestError, Reporter } from '../types'; -import { FullConfig } from '../types'; - -const stackUtils = new StackUtils(); - -export class BaseReporter implements Reporter { - duration = 0; - config: FullConfig; - suite: Suite; - timeout: number; - fileDurations = new Map(); - monotonicStartTime: number; - - constructor() { - } - - onBegin(config: FullConfig, suite: Suite) { - this.monotonicStartTime = monotonicTime(); - this.config = config; - this.suite = suite; - } - - onTestBegin(test: Test) { - } - - onStdOut(chunk: string | Buffer) { - if (!this.config.quiet) - process.stdout.write(chunk); - } - - onStdErr(chunk: string | Buffer) { - if (!this.config.quiet) - process.stderr.write(chunk); - } - - onTestEnd(test: Test, result: TestResult) { - const spec = test.spec; - let duration = this.fileDurations.get(spec.file) || 0; - duration += result.duration; - this.fileDurations.set(spec.file, duration); - } - - onError(error: TestError) { - console.log(formatError(error)); - } - - onTimeout(timeout: number) { - this.timeout = timeout; - } - - onEnd() { - this.duration = monotonicTime() - this.monotonicStartTime; - } - - private _printSlowTests() { - const fileDurations = [...this.fileDurations.entries()]; - fileDurations.sort((a, b) => b[1] - a[1]); - let insertedGap = false; - for (let i = 0; i < 10 && i < fileDurations.length; ++i) { - const baseName = path.basename(fileDurations[i][0]); - const duration = fileDurations[i][1]; - if (duration < 15000) - break; - if (!insertedGap) { - insertedGap = true; - console.log(); - } - console.log(colors.yellow(' Slow test: ') + baseName + colors.yellow(` (${milliseconds(duration)})`)); - } - console.log(); - } - - epilogue(full: boolean) { - let skipped = 0; - let expected = 0; - const unexpected: Test[] = []; - const flaky: Test[] = []; - - this.suite.findTest(test => { - switch (test.status()) { - case 'skipped': ++skipped; break; - case 'expected': ++expected; break; - case 'unexpected': unexpected.push(test); break; - case 'flaky': flaky.push(test); break; - } - }); - - if (expected) - console.log(colors.green(` ${expected} passed`) + colors.dim(` (${milliseconds(this.duration)})`)); - if (skipped) - console.log(colors.yellow(` ${skipped} skipped`)); - if (unexpected.length) { - console.log(colors.red(` ${unexpected.length} failed`)); - this._printTestHeaders(unexpected); - } - if (flaky.length) { - console.log(colors.red(` ${flaky.length} flaky`)); - this._printTestHeaders(flaky); - } - if (this.timeout) - console.log(colors.red(` Timed out waiting ${this.timeout / 1000}s for the entire test run`)); - - if (full && unexpected.length) { - console.log(''); - this._printFailures(unexpected); - } - this._printSlowTests(); - } - - private _printTestHeaders(tests: Test[]) { - tests.forEach(test => { - console.log(formatTestHeader(this.config, test, ' ')); - }); - } - - private _printFailures(failures: Test[]) { - failures.forEach((test, index) => { - console.log(formatFailure(this.config, test, index + 1)); - }); - } - - hasResultWithStatus(test: Test, status: TestStatus): boolean { - return !!test.results.find(r => r.status === status); - } - - willRetry(test: Test, result: TestResult): boolean { - return result.status !== 'passed' && result.status !== test.expectedStatus && test.results.length <= this.config.retries; - } -} - -export function formatFailure(config: FullConfig, test: Test, index?: number): string { - const tokens: string[] = []; - tokens.push(formatTestHeader(config, test, ' ', index)); - for (const result of test.results) { - if (result.status === 'passed') - continue; - tokens.push(formatResult(test, result)); - } - tokens.push(''); - return tokens.join('\n'); -} - -function formatTestHeader(config: FullConfig, test: Test, indent: string, index?: number): string { - const tokens: string[] = []; - const spec = test.spec; - let relativePath = path.relative(config.testDir, spec.file) || path.basename(spec.file); - relativePath += ':' + spec.line + ':' + spec.column; - const passedUnexpectedlySuffix = test.results[0].status === 'passed' ? ' -- passed unexpectedly' : ''; - const runListName = test.alias ? `[${test.alias}] ` : ''; - const header = `${indent}${index ? index + ') ' : ''}${relativePath} › ${runListName}${spec.fullTitle()}${passedUnexpectedlySuffix}`; - tokens.push(colors.red(pad(header, '='))); - return tokens.join('\n'); -} - -function formatResult(test: Test, result: TestResult): string { - const tokens: string[] = []; - if (result.retry) - tokens.push(colors.gray(pad(`\n Retry #${result.retry}`, '-'))); - if (result.status === 'timedOut') { - tokens.push(''); - tokens.push(indent(colors.red(`Timeout of ${test.timeout}ms exceeded.`), ' ')); - } else { - tokens.push(indent(formatError(result.error, test.spec.file), ' ')); - } - return tokens.join('\n'); -} - -function formatError(error: TestError, file?: string) { - const stack = error.stack; - const tokens = []; - if (stack) { - tokens.push(''); - const messageLocation = error.stack.indexOf(error.message); - const preamble = error.stack.substring(0, messageLocation + error.message.length); - tokens.push(preamble); - const position = file ? positionInFile(stack, file) : null; - if (position) { - const source = fs.readFileSync(file, 'utf8'); - tokens.push(''); - tokens.push(codeFrameColumns(source, { - start: position, - }, - { highlightCode: true} - )); - } - tokens.push(''); - tokens.push(colors.dim(stack.substring(preamble.length + 1))); - } else { - tokens.push(''); - tokens.push(error.value); - } - return tokens.join('\n'); -} - -function pad(line: string, char: string): string { - return line + ' ' + colors.gray(char.repeat(Math.max(0, 100 - line.length - 1))); -} - -function indent(lines: string, tab: string) { - return lines.replace(/^(?=.+$)/gm, tab); -} - -function positionInFile(stack: string, file: string): { column: number; line: number; } { - // Stack will have /private/var/folders instead of /var/folders on Mac. - file = fs.realpathSync(file); - for (const line of stack.split('\n')) { - const parsed = stackUtils.parseLine(line); - if (!parsed) - continue; - if (path.resolve(process.cwd(), parsed.file) === file) - return {column: parsed.column, line: parsed.line}; - } - return null; -} - -function monotonicTime(): number { - const [seconds, nanoseconds] = process.hrtime(); - return seconds * 1000 + (nanoseconds / 1000000 | 0); -} - -const asciiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g'); -export function stripAscii(str: string): string { - return str.replace(asciiRegex, ''); -} diff --git a/tests/folio/src/reporters/dot.ts b/tests/folio/src/reporters/dot.ts deleted file mode 100644 index fa85b57873a92..0000000000000 --- a/tests/folio/src/reporters/dot.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import colors from 'colors/safe'; -import { BaseReporter } from './base'; -import { Test, TestResult } from '../types'; - -class DotReporter extends BaseReporter { - private _counter = 0; - - onTestEnd(test: Test, result: TestResult) { - super.onTestEnd(test, result); - if (++this._counter === 81) { - process.stdout.write('\n'); - return; - } - if (result.status === 'skipped') { - process.stdout.write(colors.yellow('°')); - return; - } - if (this.willRetry(test, result)) { - process.stdout.write(colors.gray('×')); - return; - } - switch (test.status()) { - case 'expected': process.stdout.write(colors.green('·')); break; - case 'unexpected': process.stdout.write(colors.red(test.results[test.results.length - 1].status === 'timedOut' ? 'T' : 'F')); break; - case 'flaky': process.stdout.write(colors.yellow('±')); break; - } - } - - onTimeout(timeout: number) { - super.onTimeout(timeout); - this.onEnd(); - } - - onEnd() { - super.onEnd(); - process.stdout.write('\n'); - this.epilogue(true); - } -} - -export default DotReporter; diff --git a/tests/folio/src/reporters/empty.ts b/tests/folio/src/reporters/empty.ts deleted file mode 100644 index 93d1560a3b528..0000000000000 --- a/tests/folio/src/reporters/empty.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FullConfig, TestResult, Test, Suite, TestError, Reporter } from '../types'; - -class EmptyReporter implements Reporter { - onBegin(config: FullConfig, suite: Suite) {} - onTestBegin(test: Test) {} - onStdOut(chunk: string | Buffer, test?: Test) {} - onStdErr(chunk: string | Buffer, test?: Test) {} - onTestEnd(test: Test, result: TestResult) {} - onTimeout(timeout: number) {} - onError(error: TestError) {} - onEnd() {} -} - -export default EmptyReporter; diff --git a/tests/folio/src/reporters/json.ts b/tests/folio/src/reporters/json.ts deleted file mode 100644 index c4d69e0636582..0000000000000 --- a/tests/folio/src/reporters/json.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; -import EmptyReporter from './empty'; -import { FullConfig, Test, Suite, Spec, TestResult, TestError } from '../types'; - -export interface SerializedSuite { - title: string; - file: string; - column: number; - line: number; - specs: ReturnType[]; - suites?: SerializedSuite[]; -} - -export type ReportFormat = { - config: FullConfig; - errors?: TestError[]; - suites?: SerializedSuite[]; -}; - -function toPosixPath(aPath: string): string { - return aPath.split(path.sep).join(path.posix.sep); -} - -class JSONReporter extends EmptyReporter { - config: FullConfig; - suite: Suite; - private _errors: TestError[] = []; - - onBegin(config: FullConfig, suite: Suite) { - this.config = config; - this.suite = suite; - } - - onTimeout() { - this.onEnd(); - } - - onError(error: TestError): void { - this._errors.push(error); - } - - onEnd() { - outputReport({ - config: { - ...this.config, - outputDir: toPosixPath(this.config.outputDir), - testDir: toPosixPath(this.config.testDir), - }, - suites: this.suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s), - errors: this._errors - }); - } - - private _serializeSuite(suite: Suite): null | SerializedSuite { - if (!suite.findSpec(test => true)) - return null; - const suites = suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s); - return { - title: suite.title, - file: toPosixPath(path.relative(this.config.testDir, suite.file)), - line: suite.line, - column: suite.column, - specs: suite.specs.map(test => this._serializeTestSpec(test)), - suites: suites.length ? suites : undefined, - }; - } - - private _serializeTestSpec(spec: Spec) { - return { - title: spec.title, - ok: spec.ok(), - tests: spec.tests.map(r => this._serializeTest(r)), - file: toPosixPath(path.relative(this.config.testDir, spec.file)), - line: spec.line, - column: spec.column, - }; - } - - private _serializeTest(test: Test) { - return { - timeout: test.timeout, - annotations: test.annotations, - expectedStatus: test.expectedStatus, - results: test.results.map(r => this._serializeTestResult(r)) - }; - } - - private _serializeTestResult(result: TestResult) { - return { - workerIndex: result.workerIndex, - status: result.status, - duration: result.duration, - error: result.error, - stdout: result.stdout.map(s => stdioEntry(s)), - stderr: result.stderr.map(s => stdioEntry(s)), - data: result.data, - retry: result.retry, - }; - } -} - -function outputReport(report: ReportFormat) { - const reportString = JSON.stringify(report, undefined, 2); - const outputName = process.env[`FOLIO_JSON_OUTPUT_NAME`]; - if (outputName) { - fs.mkdirSync(path.dirname(outputName), { recursive: true }); - fs.writeFileSync(outputName, reportString); - } else { - console.log(reportString); - } -} - -function stdioEntry(s: string | Buffer): any { - if (typeof s === 'string') - return { text: s }; - return { buffer: s.toString('base64') }; -} - -export default JSONReporter; diff --git a/tests/folio/src/reporters/junit.ts b/tests/folio/src/reporters/junit.ts deleted file mode 100644 index daac185c97a94..0000000000000 --- a/tests/folio/src/reporters/junit.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; -import { FullConfig } from '../types'; -import EmptyReporter from './empty'; -import { Suite, Test } from '../test'; -import { monotonicTime } from '../util'; -import { formatFailure, stripAscii } from './base'; - -class JUnitReporter extends EmptyReporter { - private config: FullConfig; - private suite: Suite; - private timestamp: number; - private startTime: number; - private totalTests = 0; - private totalFailures = 0; - private totalSkipped = 0; - - onBegin(config: FullConfig, suite: Suite) { - this.config = config; - this.suite = suite; - this.timestamp = Date.now(); - this.startTime = monotonicTime(); - } - - onEnd() { - const duration = monotonicTime() - this.startTime; - const children: XMLEntry[] = []; - for (const suite of this.suite.suites) - children.push(this._buildTestSuite(suite)); - const tokens: string[] = []; - - const self = this; - const root: XMLEntry = { - name: 'testsuites', - attributes: { - id: process.env[`FOLIO_JUNIT_SUITE_ID`] || '', - name: process.env[`FOLIO_JUNIT_SUITE_NAME`] || '', - tests: self.totalTests, - failures: self.totalFailures, - skipped: self.totalSkipped, - errors: 0, - time: duration / 1000 - }, - children - }; - - serializeXML(root, tokens); - const reportString = tokens.join('\n'); - const outputName = process.env[`FOLIO_JUNIT_OUTPUT_NAME`]; - if (outputName) { - fs.mkdirSync(path.dirname(outputName), { recursive: true }); - fs.writeFileSync(outputName, reportString); - } else { - console.log(reportString); - } - } - - private _buildTestSuite(suite: Suite): XMLEntry { - let tests = 0; - let skipped = 0; - let failures = 0; - let duration = 0; - const children: XMLEntry[] = []; - - suite.findTest(test => { - ++tests; - if (test.skipped) - ++skipped; - if (!test.ok()) - ++failures; - for (const result of test.results) - duration += result.duration; - this._addTestCase(test, children); - }); - this.totalTests += tests; - this.totalSkipped += skipped; - this.totalFailures += failures; - - const entry: XMLEntry = { - name: 'testsuite', - attributes: { - name: path.relative(this.config.testDir, suite.file), - timestamp: this.timestamp, - hostname: '', - tests, - failures, - skipped, - time: duration / 1000, - errors: 0, - }, - children - }; - - return entry; - } - - private _addTestCase(test: Test, entries: XMLEntry[]) { - const entry = { - name: 'testcase', - attributes: { - name: test.spec.fullTitle(), - classname: path.relative(this.config.testDir, test.spec.file) + ' ' + test.spec.parent.fullTitle(), - time: (test.results.reduce((acc, value) => acc + value.duration, 0)) / 1000 - }, - children: [] - }; - entries.push(entry); - - if (test.skipped) { - entry.children.push({ name: 'skipped'}); - return; - } - - if (!test.ok()) { - entry.children.push({ - name: 'failure', - attributes: { - message: `${path.basename(test.spec.file)}:${test.spec.line}:${test.spec.column} ${test.spec.title}`, - type: 'FAILURE', - }, - text: stripAscii(formatFailure(this.config, test)) - }); - } - for (const result of test.results) { - for (const stdout of result.stdout) { - entries.push({ - name: 'system-out', - text: stdout.toString() - }); - } - - for (const stderr of result.stderr) { - entries.push({ - name: 'system-err', - text: stderr.toString() - }); - } - } - } -} - -type XMLEntry = { - name: string; - attributes?: { [name: string]: string | number | boolean }; - children?: XMLEntry[]; - text?: string; -}; - -function serializeXML(entry: XMLEntry, tokens: string[]) { - const attrs: string[] = []; - for (const name of Object.keys(entry.attributes || {})) - attrs.push(`${name}="${escape(String(entry.attributes[name]))}"`); - tokens.push(`<${entry.name}${attrs.length ? ' ' : ''}${attrs.join(' ')}>`); - for (const child of entry.children || []) - serializeXML(child, tokens); - if (entry.text) - tokens.push(escape(entry.text)); - tokens.push(``); -} - -function escape(text: string): string { - text = text.replace(/"/g, '"'); - text = text.replace(/&/g, '&'); - text = text.replace(//g, '>'); - return text; -} - -export default JUnitReporter; diff --git a/tests/folio/src/reporters/line.ts b/tests/folio/src/reporters/line.ts deleted file mode 100644 index 3e9f400b18902..0000000000000 --- a/tests/folio/src/reporters/line.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import colors from 'colors/safe'; -import * as path from 'path'; -import { BaseReporter, formatFailure } from './base'; -import { FullConfig, Test, Suite, TestResult } from '../types'; - -class LineReporter extends BaseReporter { - private _total: number; - private _current = 0; - private _failures = 0; - private _lastTest: Test; - - onBegin(config: FullConfig, suite: Suite) { - super.onBegin(config, suite); - this._total = suite.totalTestCount(); - console.log(); - } - - onStdOut(chunk: string | Buffer, test?: Test) { - this._dumpToStdio(test, chunk, process.stdout); - } - - onStdErr(chunk: string | Buffer, test?: Test) { - this._dumpToStdio(test, chunk, process.stderr); - } - - private _fullTitle(test: Test) { - const baseName = path.basename(test.spec.file); - const runListName = test.alias ? `[${test.alias}] ` : ''; - return `${baseName} - ${runListName}${test.spec.fullTitle()}`; - } - - private _dumpToStdio(test: Test | undefined, chunk: string | Buffer, stream: NodeJS.WriteStream) { - if (this.config.quiet) - return; - stream.write(`\u001B[1A\u001B[2K`); - if (test && this._lastTest !== test) { - // Write new header for the output. - stream.write(colors.gray(this._fullTitle(test) + `\n`)); - this._lastTest = test; - } - - stream.write(chunk); - console.log(); - } - - onTestEnd(test: Test, result: TestResult) { - super.onTestEnd(test, result); - const width = process.stdout.columns - 1; - const title = `[${++this._current}/${this._total}] ${this._fullTitle(test)}`.substring(0, width); - process.stdout.write(`\u001B[1A\u001B[2K${title}\n`); - if (!this.willRetry(test, result) && !test.ok()) { - process.stdout.write(`\u001B[1A\u001B[2K`); - console.log(formatFailure(this.config, test, ++this._failures)); - console.log(); - } - } - - onEnd() { - process.stdout.write(`\u001B[1A\u001B[2K`); - super.onEnd(); - this.epilogue(false); - } -} - -export default LineReporter; diff --git a/tests/folio/src/reporters/list.ts b/tests/folio/src/reporters/list.ts deleted file mode 100644 index 15e7340c3f303..0000000000000 --- a/tests/folio/src/reporters/list.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import colors from 'colors/safe'; -import milliseconds from 'ms'; -import { BaseReporter } from './base'; -import { FullConfig, Suite, Test, TestResult } from '../types'; - -class ListReporter extends BaseReporter { - private _failure = 0; - private _lastRow = 0; - private _testRows = new Map(); - - onBegin(config: FullConfig, suite: Suite) { - super.onBegin(config, suite); - console.log(); - } - - onTestBegin(test: Test) { - super.onTestBegin(test); - if (process.stdout.isTTY) - process.stdout.write(' ' + colors.gray(test.spec.fullTitle() + ': ') + '\n'); - this._testRows.set(test, this._lastRow++); - } - - onTestEnd(test: Test, result: TestResult) { - super.onTestEnd(test, result); - const spec = test.spec; - - const duration = colors.dim(` (${milliseconds(result.duration)})`); - let text = ''; - if (result.status === 'skipped') { - text = colors.green(' - ') + colors.cyan(spec.fullTitle()); - } else { - const statusMark = result.status === 'passed' ? ' ✓ ' : ' x '; - if (result.status === test.expectedStatus) - text = '\u001b[2K\u001b[0G' + colors.green(statusMark) + colors.gray(spec.fullTitle()) + duration; - else - text = '\u001b[2K\u001b[0G' + colors.red(`${statusMark}${++this._failure}) ` + spec.fullTitle()) + duration; - } - - const testRow = this._testRows.get(test); - // Go up if needed - if (process.stdout.isTTY && testRow !== this._lastRow) - process.stdout.write(`\u001B[${this._lastRow - testRow}A`); - // Erase line - if (process.stdout.isTTY) - process.stdout.write('\u001B[2K'); - process.stdout.write(text); - // Go down if needed. - if (testRow !== this._lastRow) { - if (process.stdout.isTTY) - process.stdout.write(`\u001B[${this._lastRow - testRow}E`); - else - process.stdout.write('\n'); - } - } - - onEnd() { - super.onEnd(); - process.stdout.write('\n'); - this.epilogue(true); - } -} - -export default ListReporter; diff --git a/tests/folio/src/reporters/multiplexer.ts b/tests/folio/src/reporters/multiplexer.ts deleted file mode 100644 index a7b0d50ac094e..0000000000000 --- a/tests/folio/src/reporters/multiplexer.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { FullConfig, Suite, Test, TestError, TestResult, Reporter } from '../types'; - -export class Multiplexer implements Reporter { - private _reporters: Reporter[]; - - constructor(reporters: Reporter[]) { - this._reporters = reporters; - } - - onBegin(config: FullConfig, suite: Suite) { - for (const reporter of this._reporters) - reporter.onBegin(config, suite); - } - - onTestBegin(test: Test) { - for (const reporter of this._reporters) - reporter.onTestBegin(test); - } - - onStdOut(chunk: string | Buffer, test?: Test) { - for (const reporter of this._reporters) - reporter.onStdOut(chunk, test); - } - - onStdErr(chunk: string | Buffer, test?: Test) { - for (const reporter of this._reporters) - reporter.onStdErr(chunk, test); - } - - onTestEnd(test: Test, result: TestResult) { - for (const reporter of this._reporters) - reporter.onTestEnd(test, result); - } - - onTimeout(timeout: number) { - for (const reporter of this._reporters) - reporter.onTimeout(timeout); - } - - onEnd() { - for (const reporter of this._reporters) - reporter.onEnd(); - } - - onError(error: TestError) { - for (const reporter of this._reporters) - reporter.onError(error); - } -} diff --git a/tests/folio/src/runner.ts b/tests/folio/src/runner.ts deleted file mode 100644 index 9a5d041b28fa4..0000000000000 --- a/tests/folio/src/runner.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright 2019 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import rimraf from 'rimraf'; -import { promisify } from 'util'; -import { Dispatcher } from './dispatcher'; -import { Reporter } from './types'; -import { createMatcher, monotonicTime, raceAgainstDeadline } from './util'; -import { Suite } from './test'; -import { Loader } from './loader'; - -const removeFolderAsync = promisify(rimraf); - -type RunResult = 'passed' | 'failed' | 'sigint' | 'forbid-only' | 'no-tests'; - -export class Runner { - private _reporter: Reporter; - private _loader: Loader; - private _rootSuite: Suite; - - constructor(loader: Loader, reporter: Reporter, runListFilter?: string[]) { - this._reporter = reporter; - this._loader = loader; - - // This makes sure we don't generate 1000000 tests if only one spec is focused. - const filtered = new Set(); - for (const { fileSuites } of loader.runLists()) { - for (const fileSuite of fileSuites.values()) { - if (fileSuite._hasOnly()) - filtered.add(fileSuite); - } - } - - this._rootSuite = new Suite(''); - const grepMatcher = createMatcher(loader.config().grep); - - const nonEmptySuites = new Set(); - for (let runListIndex = 0; runListIndex < loader.runLists().length; runListIndex++) { - const runList = loader.runLists()[runListIndex]; - if (runListFilter && !runListFilter.includes(runList.alias)) - continue; - for (const fileSuite of runList.fileSuites.values()) { - if (filtered.size && !filtered.has(fileSuite)) - continue; - const specs = fileSuite._allSpecs().filter(spec => grepMatcher(spec.fullTitle())); - if (!specs.length) - continue; - fileSuite._renumber(); - for (const spec of specs) { - for (let i = 0; i < loader.config().repeatEach; ++i) - spec._appendTest(runListIndex, runList.alias, i); - } - nonEmptySuites.add(fileSuite); - } - } - for (const fileSuite of nonEmptySuites) - this._rootSuite._addSuite(fileSuite); - - filterOnly(this._rootSuite); - } - - list() { - this._reporter.onBegin(this._loader.config(), this._rootSuite); - this._reporter.onEnd(); - } - - async run(): Promise { - await removeFolderAsync(this._loader.config().outputDir).catch(e => {}); - - if (this._loader.config().forbidOnly) { - const hasOnly = this._rootSuite.findSpec(t => t._only) || this._rootSuite.findSuite(s => s._only); - if (hasOnly) - return 'forbid-only'; - } - - const total = this._rootSuite.totalTestCount(); - if (!total) - return 'no-tests'; - const globalDeadline = this._loader.config().globalTimeout ? this._loader.config().globalTimeout + monotonicTime() : 0; - const { result, timedOut } = await raceAgainstDeadline(this._runTests(this._rootSuite), globalDeadline); - if (timedOut) { - this._reporter.onTimeout(this._loader.config().globalTimeout); - process.exit(1); - } - return result; - } - - private async _runTests(suite: Suite): Promise { - const dispatcher = new Dispatcher(this._loader, suite, this._reporter); - let sigint = false; - let sigintCallback: () => void; - const sigIntPromise = new Promise(f => sigintCallback = f); - const sigintHandler = () => { - process.off('SIGINT', sigintHandler); - sigint = true; - sigintCallback(); - }; - process.on('SIGINT', sigintHandler); - this._reporter.onBegin(this._loader.config(), suite); - await Promise.race([dispatcher.run(), sigIntPromise]); - await dispatcher.stop(); - this._reporter.onEnd(); - if (sigint) - return 'sigint'; - return dispatcher.hasWorkerErrors() || suite.findSpec(spec => !spec.ok()) ? 'failed' : 'passed'; - } -} - -function filterOnly(suite: Suite) { - const onlySuites = suite.suites.filter(child => filterOnly(child) || child._only); - const onlyTests = suite.specs.filter(spec => spec._only); - if (onlySuites.length || onlyTests.length) { - suite.suites = onlySuites; - suite.specs = onlyTests; - return true; - } - return false; -} diff --git a/tests/folio/src/spec.ts b/tests/folio/src/spec.ts deleted file mode 100644 index 43032a72acab9..0000000000000 --- a/tests/folio/src/spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from './expect'; -import { currentTestInfo } from './globals'; -import { Spec, Suite } from './test'; -import { callLocation, errorWithCallLocation, interpretCondition } from './util'; -import { Config, Env, RunWithConfig, TestInfo, TestType, WorkerInfo } from './types'; - -Error.stackTraceLimit = 15; - -let currentFile: string | undefined; -export function setCurrentFile(file?: string) { - currentFile = file; -} - -export type RunListDescription = { - alias: string; - fileSuites: Map; - env: Env; - config: RunWithConfig; - testType: TestType; -}; - -export const configFile: { - config?: Config, - globalSetup?: () => any, - globalTeardown?: (globalSetupResult: any) => any, - runLists: RunListDescription[] -} = { runLists: [] }; - -function mergeEnvs(envs: any[]): any { - if (envs.length === 1) - return envs[0]; - const forward = [...envs]; - const backward = [...forward].reverse(); - return { - beforeAll: async (workerInfo: WorkerInfo) => { - for (const env of forward) { - if (env.beforeAll) - await env.beforeAll(workerInfo); - } - }, - afterAll: async (workerInfo: WorkerInfo) => { - let error: Error | undefined; - for (const env of backward) { - if (env.afterAll) { - try { - await env.afterAll(workerInfo); - } catch (e) { - error = error || e; - } - } - } - if (error) - throw error; - }, - beforeEach: async (testInfo: TestInfo) => { - let result = undefined; - for (const env of forward) { - if (env.beforeEach) { - const r = await env.beforeEach(testInfo); - result = result === undefined ? r : { ...result, ...r }; - } - } - return result; - }, - afterEach: async (testInfo: TestInfo) => { - let error: Error | undefined; - for (const env of backward) { - if (env.afterEach) { - try { - await env.afterEach(testInfo); - } catch (e) { - error = error || e; - } - } - } - if (error) - throw error; - }, - }; -} - -export function newTestTypeImpl(): any { - const fileSuites = new Map(); - let suites: Suite[] = []; - - function ensureSuiteForCurrentLocation() { - const location = callLocation(currentFile); - let fileSuite = fileSuites.get(location.file); - if (!fileSuite) { - fileSuite = new Suite(''); - fileSuite.file = location.file; - fileSuites.set(location.file, fileSuite); - } - if (suites[suites.length - 1] !== fileSuite) - suites = [fileSuite]; - return location; - } - - function spec(type: 'default' | 'only', title: string, options: Function | any, fn?: Function) { - if (!currentFile) - throw errorWithCallLocation(`Test can only be defined in a test file.`); - const location = ensureSuiteForCurrentLocation(); - - if (typeof fn !== 'function') { - fn = options; - options = {}; - } - const spec = new Spec(title, fn, suites[0]); - spec.file = location.file; - spec.line = location.line; - spec.column = location.column; - spec.testOptions = options; - - if (type === 'only') - spec._only = true; - } - - function describe(type: 'default' | 'only', title: string, fn: Function) { - if (!currentFile) - throw errorWithCallLocation(`Suite can only be defined in a test file.`); - const location = ensureSuiteForCurrentLocation(); - - const child = new Suite(title, suites[0]); - child.file = location.file; - child.line = location.line; - child.column = location.column; - - if (type === 'only') - child._only = true; - - suites.unshift(child); - fn(); - suites.shift(); - } - - function hook(name: string, fn: Function) { - if (!currentFile) - throw errorWithCallLocation(`Hook can only be defined in a test file.`); - ensureSuiteForCurrentLocation(); - suites[0]._addHook(name, fn); - } - - const modifier = (type: 'skip' | 'fail' | 'fixme', arg?: boolean | string, description?: string) => { - if (currentFile) { - const processed = interpretCondition(arg, description); - if (processed.condition) - suites[0]._annotations.push({ type, description: processed.description }); - return; - } - - const testInfo = currentTestInfo(); - if (!testInfo) - throw new Error(`test.${type} can only be called inside the test`); - (testInfo[type] as any)(arg, description); - }; - - const test: any = spec.bind(null, 'default'); - test.expect = expect; - test.only = spec.bind(null, 'only'); - test.describe = describe.bind(null, 'default'); - test.describe.only = describe.bind(null, 'only'); - test.beforeEach = hook.bind(null, 'beforeEach'); - test.afterEach = hook.bind(null, 'afterEach'); - test.beforeAll = hook.bind(null, 'beforeAll'); - test.afterAll = hook.bind(null, 'afterAll'); - test.skip = modifier.bind(null, 'skip'); - test.fixme = modifier.bind(null, 'fixme'); - test.fail = modifier.bind(null, 'fail'); - test.runWith = (...envs: any[]) => { - let alias = ''; - if (typeof envs[0] === 'string') { - alias = envs[0]; - envs = envs.slice(1); - } - let options = envs[envs.length - 1]; - if (!envs.length || options.beforeAll || options.beforeEach || options.afterAll || options.afterEach) - options = {}; - else - envs = envs.slice(0, envs.length - 1); - configFile.runLists.push({ - fileSuites, - env: mergeEnvs(envs), - alias, - config: { timeout: options.timeout }, - testType: test, - }); - }; - return test; -} - -export function setConfig(config: Config) { - // TODO: add config validation. - configFile.config = config; -} - -export function globalSetup(globalSetupFunction: () => any) { - if (typeof globalSetupFunction !== 'function') - throw errorWithCallLocation(`globalSetup takes a single function argument.`); - configFile.globalSetup = globalSetupFunction; -} - -export function globalTeardown(globalTeardownFunction: (globalSetupResult: any) => any) { - if (typeof globalTeardownFunction !== 'function') - throw errorWithCallLocation(`globalTeardown takes a single function argument.`); - configFile.globalTeardown = globalTeardownFunction; -} diff --git a/tests/folio/src/test.ts b/tests/folio/src/test.ts deleted file mode 100644 index f208ec251b7e7..0000000000000 --- a/tests/folio/src/test.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as types from './types'; - -class Base { - title: string; - file: string; - line: number; - column: number; - parent?: Suite; - - _only = false; - _ordinal: number; - - constructor(title: string, parent?: Suite) { - this.title = title; - this.parent = parent; - } - - titlePath(): string[] { - if (!this.parent) - return []; - if (!this.title) - return this.parent.titlePath(); - return [...this.parent.titlePath(), this.title]; - } - - fullTitle(): string { - return this.titlePath().join(' '); - } -} - -export class Spec extends Base implements types.Spec { - fn: Function; - tests: Test[] = []; - testOptions: any = {}; - - constructor(title: string, fn: Function, suite: Suite) { - super(title, suite); - this.fn = fn; - suite._addSpec(this); - } - - ok(): boolean { - return !this.tests.find(r => !r.ok()); - } - - _appendTest(runListIndex: number, runListAlias: string, repeatEachIndex: number) { - const test = new Test(this); - test.alias = runListAlias; - test._runListIndex = runListIndex; - test._workerHash = `${runListIndex}#repeat-${repeatEachIndex}`; - test._id = `${this._ordinal}@${this.file}::[${test._workerHash}]`; - test._repeatEachIndex = repeatEachIndex; - this.tests.push(test); - return test; - } -} - -export class Suite extends Base implements types.Suite { - suites: Suite[] = []; - specs: Spec[] = []; - _entries: (Suite | Spec)[] = []; - _hooks: { type: string, fn: Function } [] = []; - _annotations: { type: 'skip' | 'fixme' | 'fail', description?: string }[] = []; - - constructor(title: string, parent?: Suite) { - super(title, parent); - if (parent) - parent._addSuite(this); - } - - _clear() { - this.suites = []; - this.specs = []; - this._entries = []; - this._hooks = []; - this._annotations = []; - } - - _addSpec(spec: Spec) { - spec.parent = this; - this.specs.push(spec); - this._entries.push(spec); - } - - _addSuite(suite: Suite) { - suite.parent = this; - this.suites.push(suite); - this._entries.push(suite); - } - - findTest(fn: (test: Test) => boolean | void): boolean { - for (const suite of this.suites) { - if (suite.findTest(fn)) - return true; - } - for (const spec of this.specs) { - for (const test of spec.tests) { - if (fn(test)) - return true; - } - } - return false; - } - - findSpec(fn: (spec: Spec) => boolean | void): boolean { - for (const suite of this.suites) { - if (suite.findSpec(fn)) - return true; - } - for (const spec of this.specs) { - if (fn(spec)) - return true; - } - return false; - } - - findSuite(fn: (suite: Suite) => boolean | void): boolean { - if (fn(this)) - return true; - for (const suite of this.suites) { - if (suite.findSuite(fn)) - return true; - } - return false; - } - - totalTestCount(): number { - let total = 0; - for (const suite of this.suites) - total += suite.totalTestCount(); - for (const spec of this.specs) - total += spec.tests.length; - return total; - } - - _allSpecs(): Spec[] { - const result: Spec[] = []; - this.findSpec(test => { result.push(test); }); - return result; - } - - _renumber() { - // All tests are identified with their ordinals. - let ordinal = 0; - this.findSpec((test: Spec) => { - test._ordinal = ordinal++; - }); - } - - _hasOnly(): boolean { - if (this._only) - return true; - if (this.suites.find(suite => suite._hasOnly())) - return true; - if (this.specs.find(spec => spec._only)) - return true; - } - - _addHook(type: string, fn: any) { - this._hooks.push({ type, fn }); - } -} - -export class Test implements types.Test { - spec: Spec; - results: types.TestResult[] = []; - - skipped = false; - expectedStatus: types.TestStatus = 'passed'; - timeout = 0; - annotations: any[] = []; - alias = ''; - - _id: string; - _workerHash: string; - _repeatEachIndex: number; - _runListIndex: number; - - constructor(spec: Spec) { - this.spec = spec; - } - - status(): 'skipped' | 'expected' | 'unexpected' | 'flaky' { - if (this.skipped) - return 'skipped'; - // List mode bail out. - if (!this.results.length) - return 'skipped'; - if (this.results.length === 1 && this.expectedStatus === this.results[0].status) - return 'expected'; - let hasPassedResults = false; - for (const result of this.results) { - // Missing status is Ok when running in shards mode. - if (!result.status) - return 'skipped'; - if (result.status === this.expectedStatus) - hasPassedResults = true; - } - if (hasPassedResults) - return 'flaky'; - return 'unexpected'; - } - - ok(): boolean { - const status = this.status(); - return status === 'expected' || status === 'flaky' || status === 'skipped'; - } - - _appendTestResult(): types.TestResult { - const result: types.TestResult = { - retry: this.results.length, - workerIndex: 0, - duration: 0, - stdout: [], - stderr: [], - data: {} - }; - this.results.push(result); - return result; - } -} diff --git a/tests/folio/src/transform.ts b/tests/folio/src/transform.ts deleted file mode 100644 index acdac1e5719e0..0000000000000 --- a/tests/folio/src/transform.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as crypto from 'crypto'; -import * as os from 'os'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as pirates from 'pirates'; -import * as babel from '@babel/core'; -import * as sourceMapSupport from 'source-map-support'; - -const version = 3; -const cacheDir = path.join(os.tmpdir(), 'playwright-transform-cache'); -const sourceMaps: Map = new Map(); - -sourceMapSupport.install({ - environment: 'node', - handleUncaughtExceptions: false, - retrieveSourceMap(source) { - if (!sourceMaps.has(source)) - return null; - const sourceMapPath = sourceMaps.get(source); - if (!fs.existsSync(sourceMapPath)) - return null; - return { - map: JSON.parse(fs.readFileSync(sourceMapPath, 'utf-8')), - url: source - }; - } -}); - -function calculateCachePath(content: string, filePath: string): string { - const hash = crypto.createHash('sha1').update(content).update(filePath).update(String(version)).digest('hex'); - const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash; - return path.join(cacheDir, hash[0] + hash[1], fileName); -} - -export function installTransform(): () => void { - return pirates.addHook((code, filename) => { - const cachePath = calculateCachePath(code, filename); - const codePath = cachePath + '.js'; - const sourceMapPath = cachePath + '.map'; - sourceMaps.set(filename, sourceMapPath); - if (fs.existsSync(codePath)) - return fs.readFileSync(codePath, 'utf8'); - - const result = babel.transformFileSync(filename, { - babelrc: false, - configFile: false, - presets: [ - ['@babel/preset-env', { targets: {node: '10.17.0'} }], - ['@babel/preset-typescript', { onlyRemoveTypeImports: true }], - ], - plugins: [['@babel/plugin-proposal-class-properties', {loose: true}]], - sourceMaps: 'both', - }); - if (result.code) { - fs.mkdirSync(path.dirname(cachePath), {recursive: true}); - if (result.map) - fs.writeFileSync(sourceMapPath, JSON.stringify(result.map), 'utf8'); - fs.writeFileSync(codePath, result.code, 'utf8'); - } - return result.code; - }, { - exts: ['.ts'] - }); -} diff --git a/tests/folio/src/types.ts b/tests/folio/src/types.ts deleted file mode 100644 index 2851e104618fc..0000000000000 --- a/tests/folio/src/types.ts +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Expect } from './expectType'; - -export interface RunWithConfig { - timeout?: number; - // TODO: move retries, outputDir, repeatEach, snapshotDir, testPathSegment here from Config. -} - -export interface Config extends RunWithConfig { - forbidOnly?: boolean; - globalTimeout?: number; - grep?: string | RegExp | (string | RegExp)[]; - maxFailures?: number; - outputDir?: string; - quiet?: boolean; - repeatEach?: number; - retries?: number; - shard?: { total: number, current: number } | null; - snapshotDir?: string; - testDir?: string; - testIgnore?: string | RegExp | (string | RegExp)[]; - testMatch?: string | RegExp | (string | RegExp)[]; - updateSnapshots?: boolean; - workers?: number; -} -export type FullConfig = Required; - -interface TestModifier { - skip(): void; - skip(condition: boolean): void; - skip(description: string): void; - skip(condition: boolean, description: string): void; - - fixme(): void; - fixme(condition: boolean): void; - fixme(description: string): void; - fixme(condition: boolean, description: string): void; - - fail(): void; - fail(condition: boolean): void; - fail(description: string): void; - fail(condition: boolean, description: string): void; -} - -export type TestStatus = 'passed' | 'failed' | 'timedOut' | 'skipped'; - -export interface WorkerInfo { - config: FullConfig; - workerIndex: number; - globalSetupResult: any; -} - -export interface TestInfo extends WorkerInfo, TestModifier { - // Declaration - title: string; - file: string; - line: number; - column: number; - fn: Function; - - // Modifiers - expectedStatus: TestStatus; - timeout: number; - annotations: any[]; - testOptions: any; // TODO: make testOptions typed. - repeatEachIndex: number; - retry: number; - - // Results - duration: number; - status?: TestStatus; - error?: any; - stdout: (string | Buffer)[]; - stderr: (string | Buffer)[]; - data: any; - - // Paths - snapshotPathSegment: string; - snapshotPath: (...pathSegments: string[]) => string; - outputPath: (...pathSegments: string[]) => string; -} - -interface SuiteFunction { - (name: string, inner: () => void): void; -} - -interface TestFunction { - (name: string, inner: (args: TestArgs, testInfo: TestInfo) => Promise | void): void; - (name: string, options: TestOptions, fn: (args: TestArgs, testInfo: TestInfo) => any): void; -} - -export interface TestType extends TestFunction, TestModifier { - only: TestFunction; - describe: SuiteFunction & { - only: SuiteFunction; - }; - - beforeEach: (inner: (args: TestArgs, testInfo: TestInfo) => Promise | void) => void; - afterEach: (inner: (args: TestArgs, testInfo: TestInfo) => Promise | void) => void; - beforeAll: (inner: (workerInfo: WorkerInfo) => Promise | void) => void; - afterAll: (inner: (workerInfo: WorkerInfo) => Promise | void) => void; - - expect: Expect; - - runWith(config?: RunWithConfig): void; - runWith(alias: string, config?: RunWithConfig): void; - runWith(env: Env, config?: RunWithConfig): void; - runWith(alias: string, env: Env, config?: RunWithConfig): void; - runWith(env1: Env, env2: Env, config?: RunWithConfig): RunWithOrNever; - runWith(alias: string, env1: Env, env2: Env, config?: RunWithConfig): RunWithOrNever; - runWith(env1: Env, env2: Env, env3: Env, config?: RunWithConfig): RunWithOrNever; - runWith(alias: string, env1: Env, env2: Env, env3: Env, config?: RunWithConfig): RunWithOrNever; -} - -export interface Env { - beforeAll?(workerInfo: WorkerInfo): Promise; - beforeEach?(testInfo: TestInfo): Promise; - afterEach?(testInfo: TestInfo): Promise; - afterAll?(workerInfo: WorkerInfo): Promise; -} - -type RunWithOrNever = CombinedTestArgs extends ExpectedTestArgs ? void : never; - -// ---------- Reporters API ----------- - -export interface Suite { - title: string; - file: string; - line: number; - column: number; - suites: Suite[]; - specs: Spec[]; - findTest(fn: (test: Test) => boolean | void): boolean; - findSpec(fn: (spec: Spec) => boolean | void): boolean; - totalTestCount(): number; -} -export interface Spec { - title: string; - file: string; - line: number; - column: number; - tests: Test[]; - fullTitle(): string; - ok(): boolean; -} -export interface Test { - spec: Spec; - results: TestResult[]; - skipped: boolean; - expectedStatus: TestStatus; - timeout: number; - annotations: any[]; - alias: string; - status(): 'skipped' | 'expected' | 'unexpected' | 'flaky'; - ok(): boolean; -} -export interface TestResult { - retry: number; - workerIndex: number, - duration: number; - status?: TestStatus; - error?: TestError; - stdout: (string | Buffer)[]; - stderr: (string | Buffer)[]; - data: any; -} -export interface TestError { - message?: string; - stack?: string; - value?: string; -} -export interface Reporter { - onBegin(config: FullConfig, suite: Suite): void; - onTestBegin(test: Test): void; - onStdOut(chunk: string | Buffer, test?: Test): void; - onStdErr(chunk: string | Buffer, test?: Test): void; - onTestEnd(test: Test, result: TestResult): void; - onTimeout(timeout: number): void; - onError(error: TestError): void; - onEnd(): void; -} diff --git a/tests/folio/src/util.ts b/tests/folio/src/util.ts deleted file mode 100644 index 7e172e611c1a1..0000000000000 --- a/tests/folio/src/util.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import path from 'path'; -import util from 'util'; -import StackUtils from 'stack-utils'; -import { TestError } from './types'; -import { default as minimatch } from 'minimatch'; - -const FOLIO_DIRS = [__dirname, path.join(__dirname, '..', 'src')]; -const cwd = process.cwd(); -const stackUtils = new StackUtils({ cwd }); - -export async function raceAgainstDeadline(promise: Promise, deadline: number): Promise<{ result?: T, timedOut?: boolean }> { - if (!deadline) - return { result: await promise }; - - const timeout = deadline - monotonicTime(); - if (timeout <= 0) - return { timedOut: true }; - - let timer: NodeJS.Timer; - let done = false; - let fulfill: (t: { result?: T, timedOut?: boolean }) => void; - let reject: (e: Error) => void; - const result = new Promise((f, r) => { - fulfill = f; - reject = r; - }); - setTimeout(() => { - done = true; - fulfill({ timedOut: true }); - }, timeout); - promise.then(result => { - clearTimeout(timer); - if (!done) { - done = true; - fulfill({ result }); - } - }).catch(e => { - clearTimeout(timer); - if (!done) - reject(e); - }); - return result; -} - -export function serializeError(error: Error | any): TestError { - if (error instanceof Error) { - return { - message: error.message, - stack: error.stack - }; - } - return { - value: util.inspect(error) - }; -} - -function callFrames(): string[] { - const obj = { stack: '' }; - Error.captureStackTrace(obj); - const frames = obj.stack.split('\n').slice(1); - while (frames.length && FOLIO_DIRS.some(dir => frames[0].includes(dir))) - frames.shift(); - return frames; -} - -export function callLocation(fallbackFile: string): {file: string, line: number, column: number} { - const frames = callFrames(); - if (!frames.length) - return {file: fallbackFile, line: 1, column: 1}; - const location = stackUtils.parseLine(frames[0]); - return { - file: path.resolve(cwd, location.file), - line: location.line, - column: location.column, - }; -} - -export function errorWithCallLocation(message: string): Error { - const frames = callFrames(); - const error = new Error(message); - error.stack = 'Error: ' + message + '\n' + frames.join('\n'); - return error; -} - -export function monotonicTime(): number { - const [seconds, nanoseconds] = process.hrtime(); - return seconds * 1000 + (nanoseconds / 1000000 | 0); -} - -export function prependErrorMessage(e: Error, message: string) { - let stack = e.stack || ''; - if (stack.includes(e.message)) - stack = stack.substring(stack.indexOf(e.message) + e.message.length); - let m = e.message; - if (m.startsWith('Error:')) - m = m.substring('Error:'.length); - e.message = message + m; - e.stack = e.message + stack; -} - -export function interpretCondition(arg?: boolean | string, description?: string): { condition: boolean, description?: string } { - if (arg === undefined && description === undefined) - return { condition: true }; - if (typeof arg === 'string') - return { condition: true, description: arg }; - return { condition: !!arg, description }; -} - -export function createMatcher(patterns: string | RegExp | (string | RegExp)[]): (value: string) => boolean { - const list = Array.isArray(patterns) ? patterns : [patterns]; - return (value: string) => { - for (const pattern of list) { - if (pattern instanceof RegExp || Object.prototype.toString.call(pattern) === '[object RegExp]') { - if ((pattern as RegExp).test(value)) - return true; - } else { - if (minimatch(value, pattern)) - return true; - } - } - return false; - }; -} diff --git a/tests/folio/src/worker.ts b/tests/folio/src/worker.ts deleted file mode 100644 index 9a8a1efbe696e..0000000000000 --- a/tests/folio/src/worker.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Console } from 'console'; -import * as util from 'util'; -import { RunPayload, TestOutputPayload, WorkerInitParams } from './ipc'; -import { serializeError } from './util'; -import { WorkerRunner } from './workerRunner'; - -let closed = false; - -sendMessageToParent('ready'); - -global.console = new Console({ - stdout: process.stdout, - stderr: process.stderr, - colorMode: process.env.FORCE_COLOR === '1', -}); - -process.stdout.write = chunk => { - const outPayload: TestOutputPayload = { - testId: workerRunner ? workerRunner._testId : undefined, - ...chunkToParams(chunk) - }; - sendMessageToParent('stdOut', outPayload); - return true; -}; - -if (!process.env.PW_RUNNER_DEBUG) { - process.stderr.write = chunk => { - const outPayload: TestOutputPayload = { - testId: workerRunner ? workerRunner._testId : undefined, - ...chunkToParams(chunk) - }; - sendMessageToParent('stdErr', outPayload); - return true; - }; -} - -process.on('disconnect', gracefullyCloseAndExit); -process.on('SIGINT',() => {}); -process.on('SIGTERM',() => {}); - -let workerRunner: WorkerRunner; - -process.on('unhandledRejection', (reason, promise) => { - if (workerRunner) - workerRunner.unhandledError(reason); -}); - -process.on('uncaughtException', error => { - if (workerRunner) - workerRunner.unhandledError(error); -}); - -process.on('message', async message => { - if (message.method === 'init') { - const initParams = message.params as WorkerInitParams; - workerRunner = new WorkerRunner(initParams); - for (const event of ['testBegin', 'testEnd', 'done']) - workerRunner.on(event, sendMessageToParent.bind(null, event)); - return; - } - if (message.method === 'stop') { - await gracefullyCloseAndExit(); - return; - } - if (message.method === 'run') { - const runPayload = message.params as RunPayload; - await workerRunner!.run(runPayload); - } -}); - -async function gracefullyCloseAndExit() { - if (closed) - return; - closed = true; - // Force exit after 30 seconds. - setTimeout(() => process.exit(0), 30000); - // Meanwhile, try to gracefully shutdown. - try { - if (workerRunner) { - workerRunner.stop(); - await workerRunner.cleanup(); - } - } catch (e) { - process.send({ method: 'teardownError', params: { error: serializeError(e) } }); - } - process.exit(0); -} - -function sendMessageToParent(method, params = {}) { - try { - process.send({ method, params }); - } catch (e) { - // Can throw when closing. - } -} - -function chunkToParams(chunk: Buffer | string): { text?: string, buffer?: string } { - if (chunk instanceof Buffer) - return { buffer: chunk.toString('base64') }; - if (typeof chunk !== 'string') - return { text: util.inspect(chunk) }; - return { text: chunk }; -} diff --git a/tests/folio/src/workerRunner.ts b/tests/folio/src/workerRunner.ts deleted file mode 100644 index 0825ada8a50b0..0000000000000 --- a/tests/folio/src/workerRunner.ts +++ /dev/null @@ -1,462 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs'; -import path from 'path'; -import { EventEmitter } from 'events'; -import { interpretCondition, monotonicTime, raceAgainstDeadline, serializeError } from './util'; -import { TestBeginPayload, TestEndPayload, RunPayload, TestEntry, DonePayload, WorkerInitParams } from './ipc'; -import { setCurrentTestInfo } from './globals'; -import { Loader } from './loader'; -import { Spec, Suite, Test } from './test'; -import { TestInfo, WorkerInfo } from './types'; -import { RunListDescription } from './spec'; -import { extendAgain } from './expect'; - -export class WorkerRunner extends EventEmitter { - private _params: WorkerInitParams; - private _loader: Loader; - private _runList: RunListDescription; - private _outputPathSegment: string; - private _workerInfo: WorkerInfo; - private _envInitialized = false; - - private _failedTestId: string | undefined; - private _fatalError: any | undefined; - private _entries: Map; - private _remaining: Map; - private _isStopped: any; - _testId: string | null; - private _testInfo: TestInfo | null = null; - private _file: string; - private _timeout: number; - - constructor(params: WorkerInitParams) { - super(); - this._params = params; - } - - stop() { - this._isStopped = true; - this._testId = null; - this._setCurrentTestInfo(null); - } - - async cleanup() { - if (!this._envInitialized) - return; - this._envInitialized = false; - if (this._runList.env.afterAll) { - // TODO: separate timeout for afterAll? - const result = await raceAgainstDeadline(this._runList.env.afterAll(this._workerInfo), this._deadline()); - if (result.timedOut) - throw new Error(`Timeout of ${this._timeout}ms exceeded while shutting down environment`); - } - } - - unhandledError(error: Error | any) { - if (this._isStopped) - return; - if (this._testInfo) { - this._testInfo.status = 'failed'; - this._testInfo.error = serializeError(error); - this._failedTestId = this._testId; - this.emit('testEnd', buildTestEndPayload(this._testId, this._testInfo)); - } else { - // No current test - fatal error. - this._fatalError = serializeError(error); - } - this._reportDoneAndStop(); - } - - private _deadline() { - return this._timeout ? monotonicTime() + this._timeout : 0; - } - - private async _loadIfNeeded() { - if (this._loader) - return; - - this._loader = new Loader(); - this._loader.deserialize(this._params.loader); - this._runList = this._loader.runLists()[this._params.runListIndex]; - const sameAliasAndTestType = this._loader.runLists().filter(runList => runList.alias === this._runList.alias && runList.testType === this._runList.testType); - if (sameAliasAndTestType.length > 1) - this._outputPathSegment = this._runList.alias + (sameAliasAndTestType.indexOf(this._runList) + 1); - else - this._outputPathSegment = this._runList.alias; - this._timeout = this._runList.config.timeout === undefined ? this._loader.config().timeout : this._runList.config.timeout; - this._workerInfo = { - workerIndex: this._params.workerIndex, - config: this._loader.config(), - globalSetupResult: this._params.globalSetupResult, - }; - - if (this._isStopped) - return; - - if (this._runList.env.beforeAll) { - // TODO: separate timeout for beforeAll? - const result = await raceAgainstDeadline(this._runList.env.beforeAll(this._workerInfo), this._deadline()); - if (result.timedOut) { - this._fatalError = serializeError(new Error(`Timeout of ${this._timeout}ms exceeded while initializing environment`)); - this._reportDoneAndStop(); - } - } - this._envInitialized = true; - } - - async run(runPayload: RunPayload) { - this._file = runPayload.file; - this._entries = new Map(runPayload.entries.map(e => [ e.testId, e ])); - this._remaining = new Map(runPayload.entries.map(e => [ e.testId, e ])); - - await this._loadIfNeeded(); - if (this._isStopped) - return; - - this._loader.loadTestFile(this._file); - extendAgain(); - const fileSuite = this._runList.fileSuites.get(this._file); - if (fileSuite) { - fileSuite._renumber(); - fileSuite.findSpec(spec => { - spec._appendTest(this._params.runListIndex, this._runList.alias, this._params.repeatEachIndex); - }); - await this._runSuite(fileSuite); - } - if (this._isStopped) - return; - this._reportDone(); - } - - private async _runSuite(suite: Suite) { - if (this._isStopped) - return; - const skipHooks = !this._hasTestsToRun(suite); - for (const hook of suite._hooks) { - if (hook.type !== 'beforeAll' || skipHooks) - continue; - if (this._isStopped) - return; - // TODO: separate timeout for beforeAll? - const result = await raceAgainstDeadline(hook.fn(this._workerInfo), this._deadline()); - if (result.timedOut) { - this._fatalError = serializeError(new Error(`Timeout of ${this._timeout}ms exceeded while running beforeAll hook`)); - this._reportDoneAndStop(); - } - } - for (const entry of suite._entries) { - if (entry instanceof Suite) - await this._runSuite(entry); - else - await this._runSpec(entry); - } - for (const hook of suite._hooks) { - if (hook.type !== 'afterAll' || skipHooks) - continue; - if (this._isStopped) - return; - // TODO: separate timeout for afterAll? - const result = await raceAgainstDeadline(hook.fn(this._workerInfo), this._deadline()); - if (result.timedOut) { - this._fatalError = serializeError(new Error(`Timeout of ${this._timeout}ms exceeded while running afterAll hook`)); - this._reportDoneAndStop(); - } - } - } - - private async _runSpec(spec: Spec) { - if (this._isStopped) - return; - const test = spec.tests[0]; - if (!this._entries.has(test._id)) - return; - const { retry } = this._entries.get(test._id); - // TODO: support some of test.slow(), test.setTimeout(), describe.slow() and describe.setTimeout() - const deadline = this._deadline(); - this._remaining.delete(test._id); - - const testId = test._id; - this._testId = testId; - - const config = this._workerInfo.config; - - const relativePath = path.relative(config.testDir, spec.file.replace(/\.(spec|test)\.(js|ts)/, '')); - const sanitizedTitle = spec.title.replace(/[^\w\d]+/g, '-'); - const relativeTestPath = path.join(relativePath, sanitizedTitle); - - const testInfo: TestInfo = { - ...this._workerInfo, - title: spec.title, - file: spec.file, - line: spec.line, - column: spec.column, - fn: spec.fn, - repeatEachIndex: this._params.repeatEachIndex, - retry, - expectedStatus: 'passed', - annotations: [], - duration: 0, - status: 'passed', - stdout: [], - stderr: [], - timeout: this._timeout, - data: {}, - snapshotPathSegment: '', - outputPath: (...pathSegments: string[]): string => { - let suffix = this._outputPathSegment; - if (testInfo.retry) - suffix += (suffix ? '-' : '') + 'retry' + testInfo.retry; - if (testInfo.repeatEachIndex) - suffix += (suffix ? '-' : '') + 'repeat' + testInfo.repeatEachIndex; - const basePath = path.join(config.outputDir, relativeTestPath, suffix); - fs.mkdirSync(basePath, { recursive: true }); - return path.join(basePath, ...pathSegments); - }, - snapshotPath: (...pathSegments: string[]): string => { - const basePath = path.join(config.testDir, config.snapshotDir, relativeTestPath, testInfo.snapshotPathSegment); - return path.join(basePath, ...pathSegments); - }, - testOptions: spec.testOptions, - skip: (arg?: boolean | string, description?: string) => modifier(testInfo, 'skip', arg, description), - fixme: (arg?: boolean | string, description?: string) => modifier(testInfo, 'fixme', arg, description), - fail: (arg?: boolean | string, description?: string) => modifier(testInfo, 'fail', arg, description), - }; - this._setCurrentTestInfo(testInfo); - - // Preprocess suite annotations. - for (let parent = spec.parent; parent; parent = parent.parent) - testInfo.annotations.push(...parent._annotations); - if (testInfo.annotations.some(a => a.type === 'skip' || a.type === 'fixme')) - testInfo.expectedStatus = 'skipped'; - else if (testInfo.annotations.some(a => a.type === 'fail')) - testInfo.expectedStatus = 'failed'; - - this.emit('testBegin', buildTestBeginPayload(testId, testInfo)); - - if (testInfo.expectedStatus === 'skipped') { - testInfo.status = 'skipped'; - this.emit('testEnd', buildTestEndPayload(testId, testInfo)); - return; - } - - const startTime = monotonicTime(); - - const testArgsResult = await raceAgainstDeadline(this._runEnvBeforeEach(testInfo), deadline); - if (testArgsResult.timedOut && testInfo.status === 'passed') - testInfo.status = 'timedOut'; - if (this._isStopped) - return; - - const testArgs = testArgsResult.result; - // Do not run test/teardown if we failed to initialize. - if (testArgs !== undefined) { - const result = await raceAgainstDeadline(this._runTestWithBeforeHooks(test, testInfo, testArgs), deadline); - // Do not overwrite test failure upon hook timeout. - if (result.timedOut && testInfo.status === 'passed') - testInfo.status = 'timedOut'; - if (this._isStopped) - return; - - if (!result.timedOut) { - const hooksResult = await raceAgainstDeadline(this._runAfterHooks(test, testInfo, testArgs), deadline); - // Do not overwrite test failure upon hook timeout. - if (hooksResult.timedOut && testInfo.status === 'passed') - testInfo.status = 'timedOut'; - } else { - // A timed-out test gets a full additional timeout to run after hooks. - const newDeadline = this._deadline(); - await raceAgainstDeadline(this._runAfterHooks(test, testInfo, testArgs), newDeadline); - } - } - if (this._isStopped) - return; - - testInfo.duration = monotonicTime() - startTime; - this.emit('testEnd', buildTestEndPayload(testId, testInfo)); - if (testInfo.status !== 'passed') { - this._failedTestId = this._testId; - this._reportDoneAndStop(); - } - this._setCurrentTestInfo(null); - this._testId = null; - } - - private _setCurrentTestInfo(testInfo: TestInfo | null) { - this._testInfo = testInfo; - setCurrentTestInfo(testInfo); - } - - // Returns TestArgs or undefined when env.beforeEach has failed. - private async _runEnvBeforeEach(testInfo: TestInfo): Promise { - try { - let testArgs: any = {}; - if (this._runList.env.beforeEach) - testArgs = await this._runList.env.beforeEach(testInfo); - if (testArgs === undefined) - testArgs = {}; - return testArgs; - } catch (error) { - testInfo.status = 'failed'; - testInfo.error = serializeError(error); - // Failed to initialize environment - no need to run any hooks now. - return undefined; - } - } - - private async _runTestWithBeforeHooks(test: Test, testInfo: TestInfo, testArgs: any) { - try { - await this._runHooks(test.spec.parent, 'beforeEach', testArgs, testInfo); - } catch (error) { - if (error instanceof SkipError) { - testInfo.status = 'skipped'; - } else { - testInfo.status = 'failed'; - testInfo.error = serializeError(error); - } - // Continue running afterEach hooks even after the failure. - } - - // Do not run the test when beforeEach hook fails. - if (this._isStopped || testInfo.status === 'failed' || testInfo.status === 'skipped') - return; - - try { - await test.spec.fn(testArgs, testInfo); - testInfo.status = 'passed'; - } catch (error) { - if (error instanceof SkipError) { - testInfo.status = 'skipped'; - } else { - testInfo.status = 'failed'; - testInfo.error = serializeError(error); - } - } - } - - private async _runAfterHooks(test: Test, testInfo: TestInfo, testArgs: any) { - try { - await this._runHooks(test.spec.parent, 'afterEach', testArgs, testInfo); - } catch (error) { - // Do not overwrite test failure error. - if (!(error instanceof SkipError) && testInfo.status === 'passed') { - testInfo.status = 'failed'; - testInfo.error = serializeError(error); - // Continue running even after the failure. - } - } - try { - if (this._runList.env.afterEach) - await this._runList.env.afterEach(testInfo); - } catch (error) { - // Do not overwrite test failure error. - if (testInfo.status === 'passed') { - testInfo.status = 'failed'; - testInfo.error = serializeError(error); - } - } - } - - private async _runHooks(suite: Suite, type: 'beforeEach' | 'afterEach', testArgs: any, testInfo: TestInfo) { - if (this._isStopped) - return; - const all = []; - for (let s = suite; s; s = s.parent) { - const funcs = s._hooks.filter(e => e.type === type).map(e => e.fn); - all.push(...funcs.reverse()); - } - if (type === 'beforeEach') - all.reverse(); - let error: Error | undefined; - for (const hook of all) { - try { - await hook(testArgs, testInfo); - } catch (e) { - // Always run all the hooks, and capture the first error. - error = error || e; - } - } - if (error) - throw error; - } - - private _reportDone() { - const donePayload: DonePayload = { - failedTestId: this._failedTestId, - fatalError: this._fatalError, - remaining: [...this._remaining.values()], - }; - this.emit('done', donePayload); - } - - private _reportDoneAndStop() { - if (this._isStopped) - return; - this._reportDone(); - this.stop(); - } - - private _hasTestsToRun(suite: Suite): boolean { - return suite.findSpec(spec => { - const entry = this._entries.get(spec.tests[0]._id); - if (!entry) - return; - for (let parent = spec.parent; parent; parent = parent.parent) { - if (parent._annotations.some(a => a.type === 'skip' || a.type === 'fixme')) - return; - } - return true; - }); - } -} - -function buildTestBeginPayload(testId: string, testInfo: TestInfo): TestBeginPayload { - return { - testId, - workerIndex: testInfo.workerIndex - }; -} - -function buildTestEndPayload(testId: string, testInfo: TestInfo): TestEndPayload { - return { - testId, - duration: testInfo.duration, - status: testInfo.status!, - error: testInfo.error, - data: testInfo.data, - expectedStatus: testInfo.expectedStatus, - annotations: testInfo.annotations, - timeout: testInfo.timeout, - }; -} - -function modifier(testInfo: TestInfo, type: 'skip' | 'fail' | 'fixme', arg?: boolean | string, description?: string) { - const processed = interpretCondition(arg, description); - if (!processed.condition) - return; - testInfo.annotations.push({ type, description: processed.description }); - if (type === 'skip' || type === 'fixme') { - testInfo.expectedStatus = 'skipped'; - throw new SkipError(processed.description); - } else if (type === 'fail') { - if (testInfo.expectedStatus !== 'skipped') - testInfo.expectedStatus = 'failed'; - } -} - -class SkipError extends Error { -} diff --git a/tests/folio/third_party/diff_match_patch.js b/tests/folio/third_party/diff_match_patch.js deleted file mode 100644 index ba0df0f6abd13..0000000000000 --- a/tests/folio/third_party/diff_match_patch.js +++ /dev/null @@ -1,2222 +0,0 @@ -/** - * Diff Match and Patch - * Copyright 2018 The diff-match-patch Authors. - * https://github.com/google/diff-match-patch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Computes the difference between two texts to create a patch. - * Applies the patch onto another text, allowing for errors. - * @author fraser@google.com (Neil Fraser) - */ - -/** - * Class containing the diff, match and patch methods. - * @constructor - */ -var diff_match_patch = function() { - - // Defaults. - // Redefine these in your program to override the defaults. - - // Number of seconds to map a diff before giving up (0 for infinity). - this.Diff_Timeout = 1.0; - // Cost of an empty edit operation in terms of edit characters. - this.Diff_EditCost = 4; - // At what point is no match declared (0.0 = perfection, 1.0 = very loose). - this.Match_Threshold = 0.5; - // How far to search for a match (0 = exact location, 1000+ = broad match). - // A match this many characters away from the expected location will add - // 1.0 to the score (0.0 is a perfect match). - this.Match_Distance = 1000; - // When deleting a large block of text (over ~64 characters), how close do - // the contents have to be to match the expected contents. (0.0 = perfection, - // 1.0 = very loose). Note that Match_Threshold controls how closely the - // end points of a delete need to match. - this.Patch_DeleteThreshold = 0.5; - // Chunk size for context length. - this.Patch_Margin = 4; - - // The number of bits in an int. - this.Match_MaxBits = 32; -}; - - -// DIFF FUNCTIONS - - -/** - * The data structure representing a diff is an array of tuples: - * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] - * which means: delete 'Hello', add 'Goodbye' and keep ' world.' - */ -var DIFF_DELETE = -1; -var DIFF_INSERT = 1; -var DIFF_EQUAL = 0; - -/** - * Class representing one diff tuple. - * Attempts to look like a two-element array (which is what this used to be). - * @param {number} op Operation, one of: DIFF_DELETE, DIFF_INSERT, DIFF_EQUAL. - * @param {string} text Text to be deleted, inserted, or retained. - * @constructor - */ -diff_match_patch.Diff = function(op, text) { - this[0] = op; - this[1] = text; -}; - -diff_match_patch.Diff.prototype.length = 2; - -/** - * Emulate the output of a two-element array. - * @return {string} Diff operation as a string. - */ -diff_match_patch.Diff.prototype.toString = function() { - return this[0] + ',' + this[1]; -}; - - -/** - * Find the differences between two texts. Simplifies the problem by stripping - * any common prefix or suffix off the texts before diffing. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean=} opt_checklines Optional speedup flag. If present and false, - * then don't run a line-level diff first to identify the changed areas. - * Defaults to true, which does a faster, slightly less optimal diff. - * @param {number=} opt_deadline Optional time when the diff should be complete - * by. Used internally for recursive calls. Users should set DiffTimeout - * instead. - * @return {!Array.} Array of diff tuples. - */ -diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines, - opt_deadline) { - // Set a deadline by which time the diff must be complete. - if (typeof opt_deadline == 'undefined') { - if (this.Diff_Timeout <= 0) { - opt_deadline = Number.MAX_VALUE; - } else { - opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000; - } - } - var deadline = opt_deadline; - - // Check for null inputs. - if (text1 == null || text2 == null) { - throw new Error('Null input. (diff_main)'); - } - - // Check for equality (speedup). - if (text1 == text2) { - if (text1) { - return [new diff_match_patch.Diff(DIFF_EQUAL, text1)]; - } - return []; - } - - if (typeof opt_checklines == 'undefined') { - opt_checklines = true; - } - var checklines = opt_checklines; - - // Trim off common prefix (speedup). - var commonlength = this.diff_commonPrefix(text1, text2); - var commonprefix = text1.substring(0, commonlength); - text1 = text1.substring(commonlength); - text2 = text2.substring(commonlength); - - // Trim off common suffix (speedup). - commonlength = this.diff_commonSuffix(text1, text2); - var commonsuffix = text1.substring(text1.length - commonlength); - text1 = text1.substring(0, text1.length - commonlength); - text2 = text2.substring(0, text2.length - commonlength); - - // Compute the diff on the middle block. - var diffs = this.diff_compute_(text1, text2, checklines, deadline); - - // Restore the prefix and suffix. - if (commonprefix) { - diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, commonprefix)); - } - if (commonsuffix) { - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, commonsuffix)); - } - this.diff_cleanupMerge(diffs); - return diffs; -}; - - -/** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines, - deadline) { - var diffs; - - if (!text1) { - // Just add some text (speedup). - return [new diff_match_patch.Diff(DIFF_INSERT, text2)]; - } - - if (!text2) { - // Just delete some text (speedup). - return [new diff_match_patch.Diff(DIFF_DELETE, text1)]; - } - - var longtext = text1.length > text2.length ? text1 : text2; - var shorttext = text1.length > text2.length ? text2 : text1; - var i = longtext.indexOf(shorttext); - if (i != -1) { - // Shorter text is inside the longer text (speedup). - diffs = [new diff_match_patch.Diff(DIFF_INSERT, longtext.substring(0, i)), - new diff_match_patch.Diff(DIFF_EQUAL, shorttext), - new diff_match_patch.Diff(DIFF_INSERT, - longtext.substring(i + shorttext.length))]; - // Swap insertions for deletions if diff is reversed. - if (text1.length > text2.length) { - diffs[0][0] = diffs[2][0] = DIFF_DELETE; - } - return diffs; - } - - if (shorttext.length == 1) { - // Single character string. - // After the previous speedup, the character can't be an equality. - return [new diff_match_patch.Diff(DIFF_DELETE, text1), - new diff_match_patch.Diff(DIFF_INSERT, text2)]; - } - - // Check to see if the problem can be split in two. - var hm = this.diff_halfMatch_(text1, text2); - if (hm) { - // A half-match was found, sort out the return data. - var text1_a = hm[0]; - var text1_b = hm[1]; - var text2_a = hm[2]; - var text2_b = hm[3]; - var mid_common = hm[4]; - // Send both pairs off for separate processing. - var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline); - var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline); - // Merge the results. - return diffs_a.concat([new diff_match_patch.Diff(DIFF_EQUAL, mid_common)], - diffs_b); - } - - if (checklines && text1.length > 100 && text2.length > 100) { - return this.diff_lineMode_(text1, text2, deadline); - } - - return this.diff_bisect_(text1, text2, deadline); -}; - - -/** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) { - // Scan the text on a line-by-line basis first. - var a = this.diff_linesToChars_(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - var linearray = a.lineArray; - - var diffs = this.diff_main(text1, text2, false, deadline); - - // Convert the diff back to original text. - this.diff_charsToLines_(diffs, linearray); - // Eliminate freak matches (e.g. blank lines) - this.diff_cleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); - var pointer = 0; - var count_delete = 0; - var count_insert = 0; - var text_delete = ''; - var text_insert = ''; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - count_insert++; - text_insert += diffs[pointer][1]; - break; - case DIFF_DELETE: - count_delete++; - text_delete += diffs[pointer][1]; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (count_delete >= 1 && count_insert >= 1) { - // Delete the offending records and add the merged ones. - diffs.splice(pointer - count_delete - count_insert, - count_delete + count_insert); - pointer = pointer - count_delete - count_insert; - var subDiff = - this.diff_main(text_delete, text_insert, false, deadline); - for (var j = subDiff.length - 1; j >= 0; j--) { - diffs.splice(pointer, 0, subDiff[j]); - } - pointer = pointer + subDiff.length; - } - count_insert = 0; - count_delete = 0; - text_delete = ''; - text_insert = ''; - break; - } - pointer++; - } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; -}; - - -/** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) { - // Cache the text lengths to prevent multiple calls. - var text1_length = text1.length; - var text2_length = text2.length; - var max_d = Math.ceil((text1_length + text2_length) / 2); - var v_offset = max_d; - var v_length = 2 * max_d; - var v1 = new Array(v_length); - var v2 = new Array(v_length); - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for (var x = 0; x < v_length; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[v_offset + 1] = 0; - v2[v_offset + 1] = 0; - var delta = text1_length - text2_length; - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - var front = (delta % 2 != 0); - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - var k1start = 0; - var k1end = 0; - var k2start = 0; - var k2end = 0; - for (var d = 0; d < max_d; d++) { - // Bail out if deadline is reached. - if ((new Date()).getTime() > deadline) { - break; - } - - // Walk the front path one step. - for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - var k1_offset = v_offset + k1; - var x1; - if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) { - x1 = v1[k1_offset + 1]; - } else { - x1 = v1[k1_offset - 1] + 1; - } - var y1 = x1 - k1; - while (x1 < text1_length && y1 < text2_length && - text1.charAt(x1) == text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1_offset] = x1; - if (x1 > text1_length) { - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2_length) { - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - var k2_offset = v_offset + delta - k1; - if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { - // Mirror x2 onto top-left coordinate system. - var x2 = text1_length - v2[k2_offset]; - if (x1 >= x2) { - // Overlap detected. - return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - var k2_offset = v_offset + k2; - var x2; - if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) { - x2 = v2[k2_offset + 1]; - } else { - x2 = v2[k2_offset - 1] + 1; - } - var y2 = x2 - k2; - while (x2 < text1_length && y2 < text2_length && - text1.charAt(text1_length - x2 - 1) == - text2.charAt(text2_length - y2 - 1)) { - x2++; - y2++; - } - v2[k2_offset] = x2; - if (x2 > text1_length) { - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2_length) { - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - var k1_offset = v_offset + delta - k2; - if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { - var x1 = v1[k1_offset]; - var y1 = v_offset + x1 - k1_offset; - // Mirror x2 onto top-left coordinate system. - x2 = text1_length - x2; - if (x1 >= x2) { - // Overlap detected. - return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); - } - } - } - } - } - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [new diff_match_patch.Diff(DIFF_DELETE, text1), - new diff_match_patch.Diff(DIFF_INSERT, text2)]; -}; - - -/** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ -diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y, - deadline) { - var text1a = text1.substring(0, x); - var text2a = text2.substring(0, y); - var text1b = text1.substring(x); - var text2b = text2.substring(y); - - // Compute both diffs serially. - var diffs = this.diff_main(text1a, text2a, false, deadline); - var diffsb = this.diff_main(text1b, text2b, false, deadline); - - return diffs.concat(diffsb); -}; - - -/** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ -diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) { - var lineArray = []; // e.g. lineArray[4] == 'Hello\n' - var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 - - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[0] = ''; - - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diff_linesToCharsMunge_(text) { - var chars = ''; - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - var lineStart = 0; - var lineEnd = -1; - // Keeping our own length variable is faster than looking it up. - var lineArrayLength = lineArray.length; - while (lineEnd < text.length - 1) { - lineEnd = text.indexOf('\n', lineStart); - if (lineEnd == -1) { - lineEnd = text.length - 1; - } - var line = text.substring(lineStart, lineEnd + 1); - - if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : - (lineHash[line] !== undefined)) { - chars += String.fromCharCode(lineHash[line]); - } else { - if (lineArrayLength == maxLines) { - // Bail out at 65535 because - // String.fromCharCode(65536) == String.fromCharCode(0) - line = text.substring(lineStart); - lineEnd = text.length; - } - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; - } - lineStart = lineEnd + 1; - } - return chars; - } - // Allocate 2/3rds of the space for text1, the rest for text2. - var maxLines = 40000; - var chars1 = diff_linesToCharsMunge_(text1); - maxLines = 65535; - var chars2 = diff_linesToCharsMunge_(text2); - return {chars1: chars1, chars2: chars2, lineArray: lineArray}; -}; - - -/** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.} diffs Array of diff tuples. - * @param {!Array.} lineArray Array of unique strings. - * @private - */ -diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) { - for (var i = 0; i < diffs.length; i++) { - var chars = diffs[i][1]; - var text = []; - for (var j = 0; j < chars.length; j++) { - text[j] = lineArray[chars.charCodeAt(j)]; - } - diffs[i][1] = text.join(''); - } -}; - - -/** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ -diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) { - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { - return 0; - } - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - var pointermin = 0; - var pointermax = Math.min(text1.length, text2.length); - var pointermid = pointermax; - var pointerstart = 0; - while (pointermin < pointermid) { - if (text1.substring(pointerstart, pointermid) == - text2.substring(pointerstart, pointermid)) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; -}; - - -/** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ -diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) { - // Quick check for common null cases. - if (!text1 || !text2 || - text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { - return 0; - } - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - var pointermin = 0; - var pointermax = Math.min(text1.length, text2.length); - var pointermid = pointermax; - var pointerend = 0; - while (pointermin < pointermid) { - if (text1.substring(text1.length - pointermid, text1.length - pointerend) == - text2.substring(text2.length - pointermid, text2.length - pointerend)) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; -}; - - -/** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ -diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) { - // Cache the text lengths to prevent multiple calls. - var text1_length = text1.length; - var text2_length = text2.length; - // Eliminate the null case. - if (text1_length == 0 || text2_length == 0) { - return 0; - } - // Truncate the longer string. - if (text1_length > text2_length) { - text1 = text1.substring(text1_length - text2_length); - } else if (text1_length < text2_length) { - text2 = text2.substring(0, text1_length); - } - var text_length = Math.min(text1_length, text2_length); - // Quick check for the worst case. - if (text1 == text2) { - return text_length; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: https://neil.fraser.name/news/2010/11/04/ - var best = 0; - var length = 1; - while (true) { - var pattern = text1.substring(text_length - length); - var found = text2.indexOf(pattern); - if (found == -1) { - return best; - } - length += found; - if (found == 0 || text1.substring(text_length - length) == - text2.substring(0, length)) { - best = length; - length++; - } - } -}; - - -/** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ -diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) { - if (this.Diff_Timeout <= 0) { - // Don't risk returning a non-optimal diff if we have unlimited time. - return null; - } - var longtext = text1.length > text2.length ? text1 : text2; - var shorttext = text1.length > text2.length ? text2 : text1; - if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { - return null; // Pointless. - } - var dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diff_halfMatchI_(longtext, shorttext, i) { - // Start with a 1/4 length substring at position i as a seed. - var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); - var j = -1; - var best_common = ''; - var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; - while ((j = shorttext.indexOf(seed, j + 1)) != -1) { - var prefixLength = dmp.diff_commonPrefix(longtext.substring(i), - shorttext.substring(j)); - var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i), - shorttext.substring(0, j)); - if (best_common.length < suffixLength + prefixLength) { - best_common = shorttext.substring(j - suffixLength, j) + - shorttext.substring(j, j + prefixLength); - best_longtext_a = longtext.substring(0, i - suffixLength); - best_longtext_b = longtext.substring(i + prefixLength); - best_shorttext_a = shorttext.substring(0, j - suffixLength); - best_shorttext_b = shorttext.substring(j + prefixLength); - } - } - if (best_common.length * 2 >= longtext.length) { - return [best_longtext_a, best_longtext_b, - best_shorttext_a, best_shorttext_b, best_common]; - } else { - return null; - } - } - - // First check if the second quarter is the seed for a half-match. - var hm1 = diff_halfMatchI_(longtext, shorttext, - Math.ceil(longtext.length / 4)); - // Check again based on the third quarter. - var hm2 = diff_halfMatchI_(longtext, shorttext, - Math.ceil(longtext.length / 2)); - var hm; - if (!hm1 && !hm2) { - return null; - } else if (!hm2) { - hm = hm1; - } else if (!hm1) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[4].length > hm2[4].length ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - var text1_a, text1_b, text2_a, text2_b; - if (text1.length > text2.length) { - text1_a = hm[0]; - text1_b = hm[1]; - text2_a = hm[2]; - text2_b = hm[3]; - } else { - text2_a = hm[0]; - text2_b = hm[1]; - text1_a = hm[2]; - text1_b = hm[3]; - } - var mid_common = hm[4]; - return [text1_a, text1_b, text2_a, text2_b, mid_common]; -}; - - -/** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) { - var changes = false; - var equalities = []; // Stack of indices where equalities are found. - var equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - var lastEquality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - var pointer = 0; // Index of current position. - // Number of characters that changed prior to the equality. - var length_insertions1 = 0; - var length_deletions1 = 0; - // Number of characters that changed after the equality. - var length_insertions2 = 0; - var length_deletions2 = 0; - while (pointer < diffs.length) { - if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. - equalities[equalitiesLength++] = pointer; - length_insertions1 = length_insertions2; - length_deletions1 = length_deletions2; - length_insertions2 = 0; - length_deletions2 = 0; - lastEquality = diffs[pointer][1]; - } else { // An insertion or deletion. - if (diffs[pointer][0] == DIFF_INSERT) { - length_insertions2 += diffs[pointer][1].length; - } else { - length_deletions2 += diffs[pointer][1].length; - } - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastEquality && (lastEquality.length <= - Math.max(length_insertions1, length_deletions1)) && - (lastEquality.length <= Math.max(length_insertions2, - length_deletions2))) { - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, - new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - // Throw away the equality we just deleted. - equalitiesLength--; - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - length_insertions1 = 0; // Reset the counters. - length_deletions1 = 0; - length_insertions2 = 0; - length_deletions2 = 0; - lastEquality = null; - changes = true; - } - } - pointer++; - } - - // Normalize the diff. - if (changes) { - this.diff_cleanupMerge(diffs); - } - this.diff_cleanupSemanticLossless(diffs); - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while (pointer < diffs.length) { - if (diffs[pointer - 1][0] == DIFF_DELETE && - diffs[pointer][0] == DIFF_INSERT) { - var deletion = diffs[pointer - 1][1]; - var insertion = diffs[pointer][1]; - var overlap_length1 = this.diff_commonOverlap_(deletion, insertion); - var overlap_length2 = this.diff_commonOverlap_(insertion, deletion); - if (overlap_length1 >= overlap_length2) { - if (overlap_length1 >= deletion.length / 2 || - overlap_length1 >= insertion.length / 2) { - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, - insertion.substring(0, overlap_length1))); - diffs[pointer - 1][1] = - deletion.substring(0, deletion.length - overlap_length1); - diffs[pointer + 1][1] = insertion.substring(overlap_length1); - pointer++; - } - } else { - if (overlap_length2 >= deletion.length / 2 || - overlap_length2 >= insertion.length / 2) { - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice(pointer, 0, new diff_match_patch.Diff(DIFF_EQUAL, - deletion.substring(0, overlap_length2))); - diffs[pointer - 1][0] = DIFF_INSERT; - diffs[pointer - 1][1] = - insertion.substring(0, insertion.length - overlap_length2); - diffs[pointer + 1][0] = DIFF_DELETE; - diffs[pointer + 1][1] = - deletion.substring(overlap_length2); - pointer++; - } - } - pointer++; - } - pointer++; - } -}; - - -/** - * Look for single edits surrounded on both sides by equalities - * which can be shifted sideways to align the edit to a word boundary. - * e.g: The cat came. -> The cat came. - * @param {!Array.} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) { - /** - * Given two strings, compute a score representing whether the internal - * boundary falls on logical boundaries. - * Scores range from 6 (best) to 0 (worst). - * Closure, but does not reference any external variables. - * @param {string} one First string. - * @param {string} two Second string. - * @return {number} The score. - * @private - */ - function diff_cleanupSemanticScore_(one, two) { - if (!one || !two) { - // Edges are the best. - return 6; - } - - // Each port of this function behaves slightly differently due to - // subtle differences in each language's definition of things like - // 'whitespace'. Since this function's purpose is largely cosmetic, - // the choice has been made to use each language's native features - // rather than force total conformity. - var char1 = one.charAt(one.length - 1); - var char2 = two.charAt(0); - var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_); - var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_); - var whitespace1 = nonAlphaNumeric1 && - char1.match(diff_match_patch.whitespaceRegex_); - var whitespace2 = nonAlphaNumeric2 && - char2.match(diff_match_patch.whitespaceRegex_); - var lineBreak1 = whitespace1 && - char1.match(diff_match_patch.linebreakRegex_); - var lineBreak2 = whitespace2 && - char2.match(diff_match_patch.linebreakRegex_); - var blankLine1 = lineBreak1 && - one.match(diff_match_patch.blanklineEndRegex_); - var blankLine2 = lineBreak2 && - two.match(diff_match_patch.blanklineStartRegex_); - - if (blankLine1 || blankLine2) { - // Five points for blank lines. - return 5; - } else if (lineBreak1 || lineBreak2) { - // Four points for line breaks. - return 4; - } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) { - // Three points for end of sentences. - return 3; - } else if (whitespace1 || whitespace2) { - // Two points for whitespace. - return 2; - } else if (nonAlphaNumeric1 || nonAlphaNumeric2) { - // One point for non-alphanumeric. - return 1; - } - return 0; - } - - var pointer = 1; - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] == DIFF_EQUAL && - diffs[pointer + 1][0] == DIFF_EQUAL) { - // This is a single edit surrounded by equalities. - var equality1 = diffs[pointer - 1][1]; - var edit = diffs[pointer][1]; - var equality2 = diffs[pointer + 1][1]; - - // First, shift the edit as far left as possible. - var commonOffset = this.diff_commonSuffix(equality1, edit); - if (commonOffset) { - var commonString = edit.substring(edit.length - commonOffset); - equality1 = equality1.substring(0, equality1.length - commonOffset); - edit = commonString + edit.substring(0, edit.length - commonOffset); - equality2 = commonString + equality2; - } - - // Second, step character by character right, looking for the best fit. - var bestEquality1 = equality1; - var bestEdit = edit; - var bestEquality2 = equality2; - var bestScore = diff_cleanupSemanticScore_(equality1, edit) + - diff_cleanupSemanticScore_(edit, equality2); - while (edit.charAt(0) === equality2.charAt(0)) { - equality1 += edit.charAt(0); - edit = edit.substring(1) + equality2.charAt(0); - equality2 = equality2.substring(1); - var score = diff_cleanupSemanticScore_(equality1, edit) + - diff_cleanupSemanticScore_(edit, equality2); - // The >= encourages trailing rather than leading whitespace on edits. - if (score >= bestScore) { - bestScore = score; - bestEquality1 = equality1; - bestEdit = edit; - bestEquality2 = equality2; - } - } - - if (diffs[pointer - 1][1] != bestEquality1) { - // We have an improvement, save it back to the diff. - if (bestEquality1) { - diffs[pointer - 1][1] = bestEquality1; - } else { - diffs.splice(pointer - 1, 1); - pointer--; - } - diffs[pointer][1] = bestEdit; - if (bestEquality2) { - diffs[pointer + 1][1] = bestEquality2; - } else { - diffs.splice(pointer + 1, 1); - pointer--; - } - } - } - pointer++; - } -}; - -// Define some regex patterns for matching boundaries. -diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/; -diff_match_patch.whitespaceRegex_ = /\s/; -diff_match_patch.linebreakRegex_ = /[\r\n]/; -diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/; -diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/; - -/** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) { - var changes = false; - var equalities = []; // Stack of indices where equalities are found. - var equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - var lastEquality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - var pointer = 0; // Index of current position. - // Is there an insertion operation before the last equality. - var pre_ins = false; - // Is there a deletion operation before the last equality. - var pre_del = false; - // Is there an insertion operation after the last equality. - var post_ins = false; - // Is there a deletion operation after the last equality. - var post_del = false; - while (pointer < diffs.length) { - if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. - if (diffs[pointer][1].length < this.Diff_EditCost && - (post_ins || post_del)) { - // Candidate found. - equalities[equalitiesLength++] = pointer; - pre_ins = post_ins; - pre_del = post_del; - lastEquality = diffs[pointer][1]; - } else { - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastEquality = null; - } - post_ins = post_del = false; - } else { // An insertion or deletion. - if (diffs[pointer][0] == DIFF_DELETE) { - post_del = true; - } else { - post_ins = true; - } - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if (lastEquality && ((pre_ins && pre_del && post_ins && post_del) || - ((lastEquality.length < this.Diff_EditCost / 2) && - (pre_ins + pre_del + post_ins + post_del) == 3))) { - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, - new diff_match_patch.Diff(DIFF_DELETE, lastEquality)); - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastEquality = null; - if (pre_ins && pre_del) { - // No changes made which could affect previous entry, keep going. - post_ins = post_del = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? - equalities[equalitiesLength - 1] : -1; - post_ins = post_del = false; - } - changes = true; - } - } - pointer++; - } - - if (changes) { - this.diff_cleanupMerge(diffs); - } -}; - - -/** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.} diffs Array of diff tuples. - */ -diff_match_patch.prototype.diff_cleanupMerge = function(diffs) { - // Add a dummy entry at the end. - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, '')); - var pointer = 0; - var count_delete = 0; - var count_insert = 0; - var text_delete = ''; - var text_insert = ''; - var commonlength; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - count_insert++; - text_insert += diffs[pointer][1]; - pointer++; - break; - case DIFF_DELETE: - count_delete++; - text_delete += diffs[pointer][1]; - pointer++; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (count_delete + count_insert > 1) { - if (count_delete !== 0 && count_insert !== 0) { - // Factor out any common prefixies. - commonlength = this.diff_commonPrefix(text_insert, text_delete); - if (commonlength !== 0) { - if ((pointer - count_delete - count_insert) > 0 && - diffs[pointer - count_delete - count_insert - 1][0] == - DIFF_EQUAL) { - diffs[pointer - count_delete - count_insert - 1][1] += - text_insert.substring(0, commonlength); - } else { - diffs.splice(0, 0, new diff_match_patch.Diff(DIFF_EQUAL, - text_insert.substring(0, commonlength))); - pointer++; - } - text_insert = text_insert.substring(commonlength); - text_delete = text_delete.substring(commonlength); - } - // Factor out any common suffixies. - commonlength = this.diff_commonSuffix(text_insert, text_delete); - if (commonlength !== 0) { - diffs[pointer][1] = text_insert.substring(text_insert.length - - commonlength) + diffs[pointer][1]; - text_insert = text_insert.substring(0, text_insert.length - - commonlength); - text_delete = text_delete.substring(0, text_delete.length - - commonlength); - } - } - // Delete the offending records and add the merged ones. - pointer -= count_delete + count_insert; - diffs.splice(pointer, count_delete + count_insert); - if (text_delete.length) { - diffs.splice(pointer, 0, - new diff_match_patch.Diff(DIFF_DELETE, text_delete)); - pointer++; - } - if (text_insert.length) { - diffs.splice(pointer, 0, - new diff_match_patch.Diff(DIFF_INSERT, text_insert)); - pointer++; - } - pointer++; - } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { - // Merge this equality with the previous one. - diffs[pointer - 1][1] += diffs[pointer][1]; - diffs.splice(pointer, 1); - } else { - pointer++; - } - count_insert = 0; - count_delete = 0; - text_delete = ''; - text_insert = ''; - break; - } - } - if (diffs[diffs.length - 1][1] === '') { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: ABAC -> ABAC - var changes = false; - pointer = 1; - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] == DIFF_EQUAL && - diffs[pointer + 1][0] == DIFF_EQUAL) { - // This is a single edit surrounded by equalities. - if (diffs[pointer][1].substring(diffs[pointer][1].length - - diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { - // Shift the edit over the previous equality. - diffs[pointer][1] = diffs[pointer - 1][1] + - diffs[pointer][1].substring(0, diffs[pointer][1].length - - diffs[pointer - 1][1].length); - diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; - diffs.splice(pointer - 1, 1); - changes = true; - } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == - diffs[pointer + 1][1]) { - // Shift the edit over the next equality. - diffs[pointer - 1][1] += diffs[pointer + 1][1]; - diffs[pointer][1] = - diffs[pointer][1].substring(diffs[pointer + 1][1].length) + - diffs[pointer + 1][1]; - diffs.splice(pointer + 1, 1); - changes = true; - } - } - pointer++; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - this.diff_cleanupMerge(diffs); - } -}; - - -/** - * loc is a location in text1, compute and return the equivalent location in - * text2. - * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 - * @param {!Array.} diffs Array of diff tuples. - * @param {number} loc Location within text1. - * @return {number} Location within text2. - */ -diff_match_patch.prototype.diff_xIndex = function(diffs, loc) { - var chars1 = 0; - var chars2 = 0; - var last_chars1 = 0; - var last_chars2 = 0; - var x; - for (x = 0; x < diffs.length; x++) { - if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. - chars1 += diffs[x][1].length; - } - if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. - chars2 += diffs[x][1].length; - } - if (chars1 > loc) { // Overshot the location. - break; - } - last_chars1 = chars1; - last_chars2 = chars2; - } - // Was the location was deleted? - if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { - return last_chars2; - } - // Add the remaining character length. - return last_chars2 + (loc - last_chars1); -}; - - -/** - * Convert a diff array into a pretty HTML report. - * @param {!Array.} diffs Array of diff tuples. - * @return {string} HTML representation. - */ -diff_match_patch.prototype.diff_prettyHtml = function(diffs) { - var html = []; - var pattern_amp = /&/g; - var pattern_lt = //g; - var pattern_para = /\n/g; - for (var x = 0; x < diffs.length; x++) { - var op = diffs[x][0]; // Operation (insert, delete, equal) - var data = diffs[x][1]; // Text of change. - var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') - .replace(pattern_gt, '>').replace(pattern_para, '¶
'); - switch (op) { - case DIFF_INSERT: - html[x] = '' + text + ''; - break; - case DIFF_DELETE: - html[x] = '' + text + ''; - break; - case DIFF_EQUAL: - html[x] = '' + text + ''; - break; - } - } - return html.join(''); -}; - - -/** - * Compute and return the source text (all equalities and deletions). - * @param {!Array.} diffs Array of diff tuples. - * @return {string} Source text. - */ -diff_match_patch.prototype.diff_text1 = function(diffs) { - var text = []; - for (var x = 0; x < diffs.length; x++) { - if (diffs[x][0] !== DIFF_INSERT) { - text[x] = diffs[x][1]; - } - } - return text.join(''); -}; - - -/** - * Compute and return the destination text (all equalities and insertions). - * @param {!Array.} diffs Array of diff tuples. - * @return {string} Destination text. - */ -diff_match_patch.prototype.diff_text2 = function(diffs) { - var text = []; - for (var x = 0; x < diffs.length; x++) { - if (diffs[x][0] !== DIFF_DELETE) { - text[x] = diffs[x][1]; - } - } - return text.join(''); -}; - - -/** - * Compute the Levenshtein distance; the number of inserted, deleted or - * substituted characters. - * @param {!Array.} diffs Array of diff tuples. - * @return {number} Number of changes. - */ -diff_match_patch.prototype.diff_levenshtein = function(diffs) { - var levenshtein = 0; - var insertions = 0; - var deletions = 0; - for (var x = 0; x < diffs.length; x++) { - var op = diffs[x][0]; - var data = diffs[x][1]; - switch (op) { - case DIFF_INSERT: - insertions += data.length; - break; - case DIFF_DELETE: - deletions += data.length; - break; - case DIFF_EQUAL: - // A deletion and an insertion is one substitution. - levenshtein += Math.max(insertions, deletions); - insertions = 0; - deletions = 0; - break; - } - } - levenshtein += Math.max(insertions, deletions); - return levenshtein; -}; - - -/** - * Crush the diff into an encoded string which describes the operations - * required to transform text1 into text2. - * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. - * Operations are tab-separated. Inserted text is escaped using %xx notation. - * @param {!Array.} diffs Array of diff tuples. - * @return {string} Delta text. - */ -diff_match_patch.prototype.diff_toDelta = function(diffs) { - var text = []; - for (var x = 0; x < diffs.length; x++) { - switch (diffs[x][0]) { - case DIFF_INSERT: - text[x] = '+' + encodeURI(diffs[x][1]); - break; - case DIFF_DELETE: - text[x] = '-' + diffs[x][1].length; - break; - case DIFF_EQUAL: - text[x] = '=' + diffs[x][1].length; - break; - } - } - return text.join('\t').replace(/%20/g, ' '); -}; - - -/** - * Given the original text1, and an encoded string which describes the - * operations required to transform text1 into text2, compute the full diff. - * @param {string} text1 Source string for the diff. - * @param {string} delta Delta text. - * @return {!Array.} Array of diff tuples. - * @throws {!Error} If invalid input. - */ -diff_match_patch.prototype.diff_fromDelta = function(text1, delta) { - var diffs = []; - var diffsLength = 0; // Keeping our own length var is faster in JS. - var pointer = 0; // Cursor in text1 - var tokens = delta.split(/\t/g); - for (var x = 0; x < tokens.length; x++) { - // Each token begins with a one character parameter which specifies the - // operation of this token (delete, insert, equality). - var param = tokens[x].substring(1); - switch (tokens[x].charAt(0)) { - case '+': - try { - diffs[diffsLength++] = - new diff_match_patch.Diff(DIFF_INSERT, decodeURI(param)); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in diff_fromDelta: ' + param); - } - break; - case '-': - // Fall through. - case '=': - var n = parseInt(param, 10); - if (isNaN(n) || n < 0) { - throw new Error('Invalid number in diff_fromDelta: ' + param); - } - var text = text1.substring(pointer, pointer += n); - if (tokens[x].charAt(0) == '=') { - diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_EQUAL, text); - } else { - diffs[diffsLength++] = new diff_match_patch.Diff(DIFF_DELETE, text); - } - break; - default: - // Blank tokens are ok (from a trailing \t). - // Anything else is an error. - if (tokens[x]) { - throw new Error('Invalid diff operation in diff_fromDelta: ' + - tokens[x]); - } - } - } - if (pointer != text1.length) { - throw new Error('Delta length (' + pointer + - ') does not equal source text length (' + text1.length + ').'); - } - return diffs; -}; - - -// MATCH FUNCTIONS - - -/** - * Locate the best instance of 'pattern' in 'text' near 'loc'. - * @param {string} text The text to search. - * @param {string} pattern The pattern to search for. - * @param {number} loc The location to search around. - * @return {number} Best match index or -1. - */ -diff_match_patch.prototype.match_main = function(text, pattern, loc) { - // Check for null inputs. - if (text == null || pattern == null || loc == null) { - throw new Error('Null input. (match_main)'); - } - - loc = Math.max(0, Math.min(loc, text.length)); - if (text == pattern) { - // Shortcut (potentially not guaranteed by the algorithm) - return 0; - } else if (!text.length) { - // Nothing to match. - return -1; - } else if (text.substring(loc, loc + pattern.length) == pattern) { - // Perfect match at the perfect spot! (Includes case of null pattern) - return loc; - } else { - // Do a fuzzy compare. - return this.match_bitap_(text, pattern, loc); - } -}; - - -/** - * Locate the best instance of 'pattern' in 'text' near 'loc' using the - * Bitap algorithm. - * @param {string} text The text to search. - * @param {string} pattern The pattern to search for. - * @param {number} loc The location to search around. - * @return {number} Best match index or -1. - * @private - */ -diff_match_patch.prototype.match_bitap_ = function(text, pattern, loc) { - if (pattern.length > this.Match_MaxBits) { - throw new Error('Pattern too long for this browser.'); - } - - // Initialise the alphabet. - var s = this.match_alphabet_(pattern); - - var dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Compute and return the score for a match with e errors and x location. - * Accesses loc and pattern through being a closure. - * @param {number} e Number of errors in match. - * @param {number} x Location of match. - * @return {number} Overall score for match (0.0 = good, 1.0 = bad). - * @private - */ - function match_bitapScore_(e, x) { - var accuracy = e / pattern.length; - var proximity = Math.abs(loc - x); - if (!dmp.Match_Distance) { - // Dodge divide by zero error. - return proximity ? 1.0 : accuracy; - } - return accuracy + (proximity / dmp.Match_Distance); - } - - // Highest score beyond which we give up. - var score_threshold = this.Match_Threshold; - // Is there a nearby exact match? (speedup) - var best_loc = text.indexOf(pattern, loc); - if (best_loc != -1) { - score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); - // What about in the other direction? (speedup) - best_loc = text.lastIndexOf(pattern, loc + pattern.length); - if (best_loc != -1) { - score_threshold = - Math.min(match_bitapScore_(0, best_loc), score_threshold); - } - } - - // Initialise the bit arrays. - var matchmask = 1 << (pattern.length - 1); - best_loc = -1; - - var bin_min, bin_mid; - var bin_max = pattern.length + text.length; - var last_rd; - for (var d = 0; d < pattern.length; d++) { - // Scan for the best match; each iteration allows for one more error. - // Run a binary search to determine how far from 'loc' we can stray at this - // error level. - bin_min = 0; - bin_mid = bin_max; - while (bin_min < bin_mid) { - if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) { - bin_min = bin_mid; - } else { - bin_max = bin_mid; - } - bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); - } - // Use the result from this iteration as the maximum for the next. - bin_max = bin_mid; - var start = Math.max(1, loc - bin_mid + 1); - var finish = Math.min(loc + bin_mid, text.length) + pattern.length; - - var rd = Array(finish + 2); - rd[finish + 1] = (1 << d) - 1; - for (var j = finish; j >= start; j--) { - // The alphabet (s) is a sparse hash, so the following line generates - // warnings. - var charMatch = s[text.charAt(j - 1)]; - if (d === 0) { // First pass: exact match. - rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; - } else { // Subsequent passes: fuzzy match. - rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) | - (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | - last_rd[j + 1]; - } - if (rd[j] & matchmask) { - var score = match_bitapScore_(d, j - 1); - // This match will almost certainly be better than any existing match. - // But check anyway. - if (score <= score_threshold) { - // Told you so. - score_threshold = score; - best_loc = j - 1; - if (best_loc > loc) { - // When passing loc, don't exceed our current distance from loc. - start = Math.max(1, 2 * loc - best_loc); - } else { - // Already passed loc, downhill from here on in. - break; - } - } - } - } - // No hope for a (better) match at greater error levels. - if (match_bitapScore_(d + 1, loc) > score_threshold) { - break; - } - last_rd = rd; - } - return best_loc; -}; - - -/** - * Initialise the alphabet for the Bitap algorithm. - * @param {string} pattern The text to encode. - * @return {!Object} Hash of character locations. - * @private - */ -diff_match_patch.prototype.match_alphabet_ = function(pattern) { - var s = {}; - for (var i = 0; i < pattern.length; i++) { - s[pattern.charAt(i)] = 0; - } - for (var i = 0; i < pattern.length; i++) { - s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); - } - return s; -}; - - -// PATCH FUNCTIONS - - -/** - * Increase the context until it is unique, - * but don't let the pattern expand beyond Match_MaxBits. - * @param {!diff_match_patch.patch_obj} patch The patch to grow. - * @param {string} text Source text. - * @private - */ -diff_match_patch.prototype.patch_addContext_ = function(patch, text) { - if (text.length == 0) { - return; - } - if (patch.start2 === null) { - throw Error('patch not initialized'); - } - var pattern = text.substring(patch.start2, patch.start2 + patch.length1); - var padding = 0; - - // Look for the first and last matches of pattern in text. If two different - // matches are found, increase the pattern length. - while (text.indexOf(pattern) != text.lastIndexOf(pattern) && - pattern.length < this.Match_MaxBits - this.Patch_Margin - - this.Patch_Margin) { - padding += this.Patch_Margin; - pattern = text.substring(patch.start2 - padding, - patch.start2 + patch.length1 + padding); - } - // Add one chunk for good luck. - padding += this.Patch_Margin; - - // Add the prefix. - var prefix = text.substring(patch.start2 - padding, patch.start2); - if (prefix) { - patch.diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, prefix)); - } - // Add the suffix. - var suffix = text.substring(patch.start2 + patch.length1, - patch.start2 + patch.length1 + padding); - if (suffix) { - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, suffix)); - } - - // Roll back the start points. - patch.start1 -= prefix.length; - patch.start2 -= prefix.length; - // Extend the lengths. - patch.length1 += prefix.length + suffix.length; - patch.length2 += prefix.length + suffix.length; -}; - - -/** - * Compute a list of patches to turn text1 into text2. - * Use diffs if provided, otherwise compute it ourselves. - * There are four ways to call this function, depending on what data is - * available to the caller: - * Method 1: - * a = text1, b = text2 - * Method 2: - * a = diffs - * Method 3 (optimal): - * a = text1, b = diffs - * Method 4 (deprecated, use method 3): - * a = text1, b = text2, c = diffs - * - * @param {string|!Array.} a text1 (methods 1,3,4) or - * Array of diff tuples for text1 to text2 (method 2). - * @param {string|!Array.=} opt_b text2 (methods 1,4) or - * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). - * @param {string|!Array.=} opt_c Array of diff tuples - * for text1 to text2 (method 4) or undefined (methods 1,2,3). - * @return {!Array.} Array of Patch objects. - */ -diff_match_patch.prototype.patch_make = function(a, opt_b, opt_c) { - var text1, diffs; - if (typeof a == 'string' && typeof opt_b == 'string' && - typeof opt_c == 'undefined') { - // Method 1: text1, text2 - // Compute diffs from text1 and text2. - text1 = /** @type {string} */(a); - diffs = this.diff_main(text1, /** @type {string} */(opt_b), true); - if (diffs.length > 2) { - this.diff_cleanupSemantic(diffs); - this.diff_cleanupEfficiency(diffs); - } - } else if (a && typeof a == 'object' && typeof opt_b == 'undefined' && - typeof opt_c == 'undefined') { - // Method 2: diffs - // Compute text1 from diffs. - diffs = /** @type {!Array.} */(a); - text1 = this.diff_text1(diffs); - } else if (typeof a == 'string' && opt_b && typeof opt_b == 'object' && - typeof opt_c == 'undefined') { - // Method 3: text1, diffs - text1 = /** @type {string} */(a); - diffs = /** @type {!Array.} */(opt_b); - } else if (typeof a == 'string' && typeof opt_b == 'string' && - opt_c && typeof opt_c == 'object') { - // Method 4: text1, text2, diffs - // text2 is not used. - text1 = /** @type {string} */(a); - diffs = /** @type {!Array.} */(opt_c); - } else { - throw new Error('Unknown call format to patch_make.'); - } - - if (diffs.length === 0) { - return []; // Get rid of the null case. - } - var patches = []; - var patch = new diff_match_patch.patch_obj(); - var patchDiffLength = 0; // Keeping our own length var is faster in JS. - var char_count1 = 0; // Number of characters into the text1 string. - var char_count2 = 0; // Number of characters into the text2 string. - // Start with text1 (prepatch_text) and apply the diffs until we arrive at - // text2 (postpatch_text). We recreate the patches one by one to determine - // context info. - var prepatch_text = text1; - var postpatch_text = text1; - for (var x = 0; x < diffs.length; x++) { - var diff_type = diffs[x][0]; - var diff_text = diffs[x][1]; - - if (!patchDiffLength && diff_type !== DIFF_EQUAL) { - // A new patch starts here. - patch.start1 = char_count1; - patch.start2 = char_count2; - } - - switch (diff_type) { - case DIFF_INSERT: - patch.diffs[patchDiffLength++] = diffs[x]; - patch.length2 += diff_text.length; - postpatch_text = postpatch_text.substring(0, char_count2) + diff_text + - postpatch_text.substring(char_count2); - break; - case DIFF_DELETE: - patch.length1 += diff_text.length; - patch.diffs[patchDiffLength++] = diffs[x]; - postpatch_text = postpatch_text.substring(0, char_count2) + - postpatch_text.substring(char_count2 + - diff_text.length); - break; - case DIFF_EQUAL: - if (diff_text.length <= 2 * this.Patch_Margin && - patchDiffLength && diffs.length != x + 1) { - // Small equality inside a patch. - patch.diffs[patchDiffLength++] = diffs[x]; - patch.length1 += diff_text.length; - patch.length2 += diff_text.length; - } else if (diff_text.length >= 2 * this.Patch_Margin) { - // Time for a new patch. - if (patchDiffLength) { - this.patch_addContext_(patch, prepatch_text); - patches.push(patch); - patch = new diff_match_patch.patch_obj(); - patchDiffLength = 0; - // Unlike Unidiff, our patch lists have a rolling context. - // https://github.com/google/diff-match-patch/wiki/Unidiff - // Update prepatch text & pos to reflect the application of the - // just completed patch. - prepatch_text = postpatch_text; - char_count1 = char_count2; - } - } - break; - } - - // Update the current character count. - if (diff_type !== DIFF_INSERT) { - char_count1 += diff_text.length; - } - if (diff_type !== DIFF_DELETE) { - char_count2 += diff_text.length; - } - } - // Pick up the leftover patch if not empty. - if (patchDiffLength) { - this.patch_addContext_(patch, prepatch_text); - patches.push(patch); - } - - return patches; -}; - - -/** - * Given an array of patches, return another array that is identical. - * @param {!Array.} patches Array of Patch objects. - * @return {!Array.} Array of Patch objects. - */ -diff_match_patch.prototype.patch_deepCopy = function(patches) { - // Making deep copies is hard in JavaScript. - var patchesCopy = []; - for (var x = 0; x < patches.length; x++) { - var patch = patches[x]; - var patchCopy = new diff_match_patch.patch_obj(); - patchCopy.diffs = []; - for (var y = 0; y < patch.diffs.length; y++) { - patchCopy.diffs[y] = - new diff_match_patch.Diff(patch.diffs[y][0], patch.diffs[y][1]); - } - patchCopy.start1 = patch.start1; - patchCopy.start2 = patch.start2; - patchCopy.length1 = patch.length1; - patchCopy.length2 = patch.length2; - patchesCopy[x] = patchCopy; - } - return patchesCopy; -}; - - -/** - * Merge a set of patches onto the text. Return a patched text, as well - * as a list of true/false values indicating which patches were applied. - * @param {!Array.} patches Array of Patch objects. - * @param {string} text Old text. - * @return {!Array.>} Two element Array, containing the - * new text and an array of boolean values. - */ -diff_match_patch.prototype.patch_apply = function(patches, text) { - if (patches.length == 0) { - return [text, []]; - } - - // Deep copy the patches so that no changes are made to originals. - patches = this.patch_deepCopy(patches); - - var nullPadding = this.patch_addPadding(patches); - text = nullPadding + text + nullPadding; - - this.patch_splitMax(patches); - // delta keeps track of the offset between the expected and actual location - // of the previous patch. If there are patches expected at positions 10 and - // 20, but the first patch was found at 12, delta is 2 and the second patch - // has an effective expected position of 22. - var delta = 0; - var results = []; - for (var x = 0; x < patches.length; x++) { - var expected_loc = patches[x].start2 + delta; - var text1 = this.diff_text1(patches[x].diffs); - var start_loc; - var end_loc = -1; - if (text1.length > this.Match_MaxBits) { - // patch_splitMax will only provide an oversized pattern in the case of - // a monster delete. - start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), - expected_loc); - if (start_loc != -1) { - end_loc = this.match_main(text, - text1.substring(text1.length - this.Match_MaxBits), - expected_loc + text1.length - this.Match_MaxBits); - if (end_loc == -1 || start_loc >= end_loc) { - // Can't find valid trailing context. Drop this patch. - start_loc = -1; - } - } - } else { - start_loc = this.match_main(text, text1, expected_loc); - } - if (start_loc == -1) { - // No match found. :( - results[x] = false; - // Subtract the delta for this failed patch from subsequent patches. - delta -= patches[x].length2 - patches[x].length1; - } else { - // Found a match. :) - results[x] = true; - delta = start_loc - expected_loc; - var text2; - if (end_loc == -1) { - text2 = text.substring(start_loc, start_loc + text1.length); - } else { - text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); - } - if (text1 == text2) { - // Perfect match, just shove the replacement text in. - text = text.substring(0, start_loc) + - this.diff_text2(patches[x].diffs) + - text.substring(start_loc + text1.length); - } else { - // Imperfect match. Run a diff to get a framework of equivalent - // indices. - var diffs = this.diff_main(text1, text2, false); - if (text1.length > this.Match_MaxBits && - this.diff_levenshtein(diffs) / text1.length > - this.Patch_DeleteThreshold) { - // The end points match, but the content is unacceptably bad. - results[x] = false; - } else { - this.diff_cleanupSemanticLossless(diffs); - var index1 = 0; - var index2; - for (var y = 0; y < patches[x].diffs.length; y++) { - var mod = patches[x].diffs[y]; - if (mod[0] !== DIFF_EQUAL) { - index2 = this.diff_xIndex(diffs, index1); - } - if (mod[0] === DIFF_INSERT) { // Insertion - text = text.substring(0, start_loc + index2) + mod[1] + - text.substring(start_loc + index2); - } else if (mod[0] === DIFF_DELETE) { // Deletion - text = text.substring(0, start_loc + index2) + - text.substring(start_loc + this.diff_xIndex(diffs, - index1 + mod[1].length)); - } - if (mod[0] !== DIFF_DELETE) { - index1 += mod[1].length; - } - } - } - } - } - } - // Strip the padding off. - text = text.substring(nullPadding.length, text.length - nullPadding.length); - return [text, results]; -}; - - -/** - * Add some padding on text start and end so that edges can match something. - * Intended to be called only from within patch_apply. - * @param {!Array.} patches Array of Patch objects. - * @return {string} The padding string added to each side. - */ -diff_match_patch.prototype.patch_addPadding = function(patches) { - var paddingLength = this.Patch_Margin; - var nullPadding = ''; - for (var x = 1; x <= paddingLength; x++) { - nullPadding += String.fromCharCode(x); - } - - // Bump all the patches forward. - for (var x = 0; x < patches.length; x++) { - patches[x].start1 += paddingLength; - patches[x].start2 += paddingLength; - } - - // Add some padding on start of first diff. - var patch = patches[0]; - var diffs = patch.diffs; - if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) { - // Add nullPadding equality. - diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); - patch.start1 -= paddingLength; // Should be 0. - patch.start2 -= paddingLength; // Should be 0. - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs[0][1].length) { - // Grow first equality. - var extraLength = paddingLength - diffs[0][1].length; - diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]; - patch.start1 -= extraLength; - patch.start2 -= extraLength; - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - // Add some padding on end of last diff. - patch = patches[patches.length - 1]; - diffs = patch.diffs; - if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) { - // Add nullPadding equality. - diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, nullPadding)); - patch.length1 += paddingLength; - patch.length2 += paddingLength; - } else if (paddingLength > diffs[diffs.length - 1][1].length) { - // Grow last equality. - var extraLength = paddingLength - diffs[diffs.length - 1][1].length; - diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength); - patch.length1 += extraLength; - patch.length2 += extraLength; - } - - return nullPadding; -}; - - -/** - * Look through the patches and break up any which are longer than the maximum - * limit of the match algorithm. - * Intended to be called only from within patch_apply. - * @param {!Array.} patches Array of Patch objects. - */ -diff_match_patch.prototype.patch_splitMax = function(patches) { - var patch_size = this.Match_MaxBits; - for (var x = 0; x < patches.length; x++) { - if (patches[x].length1 <= patch_size) { - continue; - } - var bigpatch = patches[x]; - // Remove the big old patch. - patches.splice(x--, 1); - var start1 = bigpatch.start1; - var start2 = bigpatch.start2; - var precontext = ''; - while (bigpatch.diffs.length !== 0) { - // Create one of several smaller patches. - var patch = new diff_match_patch.patch_obj(); - var empty = true; - patch.start1 = start1 - precontext.length; - patch.start2 = start2 - precontext.length; - if (precontext !== '') { - patch.length1 = patch.length2 = precontext.length; - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, precontext)); - } - while (bigpatch.diffs.length !== 0 && - patch.length1 < patch_size - this.Patch_Margin) { - var diff_type = bigpatch.diffs[0][0]; - var diff_text = bigpatch.diffs[0][1]; - if (diff_type === DIFF_INSERT) { - // Insertions are harmless. - patch.length2 += diff_text.length; - start2 += diff_text.length; - patch.diffs.push(bigpatch.diffs.shift()); - empty = false; - } else if (diff_type === DIFF_DELETE && patch.diffs.length == 1 && - patch.diffs[0][0] == DIFF_EQUAL && - diff_text.length > 2 * patch_size) { - // This is a large deletion. Let it pass in one chunk. - patch.length1 += diff_text.length; - start1 += diff_text.length; - empty = false; - patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); - bigpatch.diffs.shift(); - } else { - // Deletion or equality. Only take as much as we can stomach. - diff_text = diff_text.substring(0, - patch_size - patch.length1 - this.Patch_Margin); - patch.length1 += diff_text.length; - start1 += diff_text.length; - if (diff_type === DIFF_EQUAL) { - patch.length2 += diff_text.length; - start2 += diff_text.length; - } else { - empty = false; - } - patch.diffs.push(new diff_match_patch.Diff(diff_type, diff_text)); - if (diff_text == bigpatch.diffs[0][1]) { - bigpatch.diffs.shift(); - } else { - bigpatch.diffs[0][1] = - bigpatch.diffs[0][1].substring(diff_text.length); - } - } - } - // Compute the head context for the next patch. - precontext = this.diff_text2(patch.diffs); - precontext = - precontext.substring(precontext.length - this.Patch_Margin); - // Append the end context for this patch. - var postcontext = this.diff_text1(bigpatch.diffs) - .substring(0, this.Patch_Margin); - if (postcontext !== '') { - patch.length1 += postcontext.length; - patch.length2 += postcontext.length; - if (patch.diffs.length !== 0 && - patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) { - patch.diffs[patch.diffs.length - 1][1] += postcontext; - } else { - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, postcontext)); - } - } - if (!empty) { - patches.splice(++x, 0, patch); - } - } - } -}; - - -/** - * Take a list of patches and return a textual representation. - * @param {!Array.} patches Array of Patch objects. - * @return {string} Text representation of patches. - */ -diff_match_patch.prototype.patch_toText = function(patches) { - var text = []; - for (var x = 0; x < patches.length; x++) { - text[x] = patches[x]; - } - return text.join(''); -}; - - -/** - * Parse a textual representation of patches and return a list of Patch objects. - * @param {string} textline Text representation of patches. - * @return {!Array.} Array of Patch objects. - * @throws {!Error} If invalid input. - */ -diff_match_patch.prototype.patch_fromText = function(textline) { - var patches = []; - if (!textline) { - return patches; - } - var text = textline.split('\n'); - var textPointer = 0; - var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; - while (textPointer < text.length) { - var m = text[textPointer].match(patchHeader); - if (!m) { - throw new Error('Invalid patch string: ' + text[textPointer]); - } - var patch = new diff_match_patch.patch_obj(); - patches.push(patch); - patch.start1 = parseInt(m[1], 10); - if (m[2] === '') { - patch.start1--; - patch.length1 = 1; - } else if (m[2] == '0') { - patch.length1 = 0; - } else { - patch.start1--; - patch.length1 = parseInt(m[2], 10); - } - - patch.start2 = parseInt(m[3], 10); - if (m[4] === '') { - patch.start2--; - patch.length2 = 1; - } else if (m[4] == '0') { - patch.length2 = 0; - } else { - patch.start2--; - patch.length2 = parseInt(m[4], 10); - } - textPointer++; - - while (textPointer < text.length) { - var sign = text[textPointer].charAt(0); - try { - var line = decodeURI(text[textPointer].substring(1)); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in patch_fromText: ' + line); - } - if (sign == '-') { - // Deletion. - patch.diffs.push(new diff_match_patch.Diff(DIFF_DELETE, line)); - } else if (sign == '+') { - // Insertion. - patch.diffs.push(new diff_match_patch.Diff(DIFF_INSERT, line)); - } else if (sign == ' ') { - // Minor equality. - patch.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL, line)); - } else if (sign == '@') { - // Start of next patch. - break; - } else if (sign === '') { - // Blank line? Whatever. - } else { - // WTF? - throw new Error('Invalid patch mode "' + sign + '" in: ' + line); - } - textPointer++; - } - } - return patches; -}; - - -/** - * Class representing one patch operation. - * @constructor - */ -diff_match_patch.patch_obj = function() { - /** @type {!Array.} */ - this.diffs = []; - /** @type {?number} */ - this.start1 = null; - /** @type {?number} */ - this.start2 = null; - /** @type {number} */ - this.length1 = 0; - /** @type {number} */ - this.length2 = 0; -}; - - -/** - * Emulate GNU diff's format. - * Header: @@ -382,8 +481,9 @@ - * Indices are printed as 1-based, not 0-based. - * @return {string} The GNU diff string. - */ -diff_match_patch.patch_obj.prototype.toString = function() { - var coords1, coords2; - if (this.length1 === 0) { - coords1 = this.start1 + ',0'; - } else if (this.length1 == 1) { - coords1 = this.start1 + 1; - } else { - coords1 = (this.start1 + 1) + ',' + this.length1; - } - if (this.length2 === 0) { - coords2 = this.start2 + ',0'; - } else if (this.length2 == 1) { - coords2 = this.start2 + 1; - } else { - coords2 = (this.start2 + 1) + ',' + this.length2; - } - var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']; - var op; - // Escape the body of the patch with %xx notation. - for (var x = 0; x < this.diffs.length; x++) { - switch (this.diffs[x][0]) { - case DIFF_INSERT: - op = '+'; - break; - case DIFF_DELETE: - op = '-'; - break; - case DIFF_EQUAL: - op = ' '; - break; - } - text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'; - } - return text.join('').replace(/%20/g, ' '); -}; - -module.exports = { diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL }; diff --git a/tests/folio/tsconfig.json b/tests/folio/tsconfig.json deleted file mode 100644 index 922a962b50d88..0000000000000 --- a/tests/folio/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "moduleResolution": "node", - "module": "commonjs", - "strict": false, - "sourceMap": true, - "declarationMap": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "declaration": true, - "outDir": "./out", - "rootDir": "./src", - "lib": ["esnext", "dom", "DOM.Iterable"], - }, - "exclude": [ - "node_modules" - ], - "include": [ - "src" - ] -} \ No newline at end of file diff --git a/utils/doclint/test/class-testapi.md b/utils/doclint/test/class-testapi.md deleted file mode 100644 index 087569a3a608a..0000000000000 --- a/utils/doclint/test/class-testapi.md +++ /dev/null @@ -1,20 +0,0 @@ -# class: DoesNotExist - -## method: DoesNotExist.doesNotExist - -# class: Exists - -## method: Exists.doesNotExist - -## method: Exists.exists - -### param: Exists.exists.exists -- `exists` <[boolean]> - -### param: Exists.exists.doesNotExist -- `doesNotExist` <[boolean]> - -### option: Exists.exists.option -- `option` <[int]> - -## method: Exists.exists2 diff --git a/utils/doclint/test/missingDocs.spec.js b/utils/doclint/test/missingDocs.spec.js deleted file mode 100644 index ab2087a6f02a4..0000000000000 --- a/utils/doclint/test/missingDocs.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const fs = require('fs'); -const path = require('path'); -const missingDocs = require('../missingDocs'); -const { folio } = require('folio'); -const { parseApi } = require('../api_parser'); - -const { test, expect } = folio; - -test('missing docs', async ({}) => { - const documentation = parseApi(path.join(__dirname)); - const tsSources = [ - path.join(__dirname, 'test-api.ts'), - path.join(__dirname, 'test-api-class.ts'), - ]; - const errors = missingDocs(documentation, tsSources, path.join(__dirname, 'test-api.ts')); - expect(errors).toEqual([ - 'Missing documentation for "Exists.exists2.extra"', - 'Missing documentation for "Exists.exists2.options"', - 'Missing documentation for "Exists.extra"', - 'Missing documentation for "Extra"', - 'Documented "DoesNotExist" not found in sources', - 'Documented "Exists.doesNotExist" not found is sources', - 'Documented "Exists.exists.doesNotExist" not found is sources', - ]); -}); diff --git a/utils/doclint/test/test-api-class.ts b/utils/doclint/test/test-api-class.ts deleted file mode 100644 index 65295e2bb5c01..0000000000000 --- a/utils/doclint/test/test-api-class.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class Exists { - exists(exists: boolean) { - return true; - } - - exists2(extra: boolean, options: {}) { - return true; - } - - extra() { - return false; - } -} - -export class Extra { - exists() { - return true; - } -} diff --git a/utils/doclint/test/test-api.ts b/utils/doclint/test/test-api.ts deleted file mode 100644 index ff0fc4ca075d4..0000000000000 --- a/utils/doclint/test/test-api.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { Exists } from './test-api-class'; -export { Extra } from './test-api-class';