test: replace sendSIGINTAfter with interactWithTestRunner (#24411)

This way we can send multiple SIGINTs in tests.
This commit is contained in:
Dmitry Gozman 2023-07-25 15:46:39 -07:00 committed by GitHub
parent 9d0bba9c99
commit ed14bf2103
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 172 additions and 144 deletions

View File

@ -204,8 +204,8 @@ export class TestChildProcess {
throw new Error(`Process received signal: ${r.signal}`);
}
async waitForOutput(substring: string) {
while (!stripAnsi(this.output).includes(substring))
async waitForOutput(substring: string, count = 1) {
while (countTimes(stripAnsi(this.output), substring) < count)
await new Promise<void>(f => this._outputCallbacks.add(f));
}
@ -275,3 +275,15 @@ export const commonFixtures: Fixtures<CommonFixtures, CommonWorkerFixtures> = {
token.canceled = true;
},
};
export function countTimes(s: string, sub: string): number {
let result = 0;
for (let index = 0; index !== -1;) {
index = s.indexOf(sub, index);
if (index !== -1) {
result++;
index += sub.length;
}
}
return result;
}

View File

@ -15,7 +15,7 @@
*/
import path from 'path';
import { test, expect } from './playwright-test-fixtures';
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures';
test('should be able to call expect.extend in config', async ({ runInlineTest }) => {
const result = await runInlineTest({
@ -588,10 +588,10 @@ test('should print pending operations for toHaveText', async ({ runInlineTest })
expect(output).toContain('waiting for locator(\'no-such-thing\')');
});
test('should print expected/received on Ctrl+C', async ({ runInlineTest }) => {
test('should print expected/received on Ctrl+C', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'a.test.ts': `
import { test, expect } from '@playwright/test';
@ -603,8 +603,13 @@ test('should print expected/received on Ctrl+C', async ({ runInlineTest }) => {
await promise;
});
`,
}, { workers: 1 }, {}, { sendSIGINTAfter: 1 });
expect(result.exitCode).toBe(130);
}, { workers: 1 });
await testProcess.waitForOutput('%%SEND-SIGINT%%');
process.kill(testProcess.process.pid!, 'SIGINT');
const { exitCode } = await testProcess.exited;
expect(exitCode).toBe(130);
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
expect(result.interrupted).toBe(1);
expect(result.output).toContain('Expected string: "Text 2"');

View File

@ -26,8 +26,9 @@ import { serverFixtures } from '../config/serverFixtures';
import type { TestInfo } from './stable-test-runner';
import { expect } from './stable-test-runner';
import { test as base } from './stable-test-runner';
export { countTimes } from '../config/commonFixtures';
export type CliRunResult = {
type CliRunResult = {
exitCode: number,
output: string,
};
@ -92,13 +93,7 @@ const configFile = (baseDir: string, files: Files): string | undefined => {
return undefined;
};
async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], baseDir: string, params: any, env: NodeJS.ProcessEnv, options: RunOptions, files: Files, mergeReports: (reportFolder: string, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<CliRunResult>, useIntermediateMergeReport: boolean): Promise<RunResult> {
let reporter;
if (useIntermediateMergeReport) {
reporter = params.reporter;
params.reporter = 'blob';
}
function startPlaywrightTest(childProcess: CommonFixtures['childProcess'], baseDir: string, params: any, env: NodeJS.ProcessEnv, options: RunOptions): TestChildProcess {
const paramList: string[] = [];
for (const key of Object.keys(params)) {
for (const value of Array.isArray(params[key]) ? params[key] : [params[key]]) {
@ -106,7 +101,6 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
paramList.push(params[key] === true ? `${k}` : `${k}=${value}`);
}
}
const reportFile = path.join(baseDir, 'report.json');
const args = ['test'];
args.push(
'--workers=2',
@ -114,14 +108,27 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
);
if (options.additionalArgs)
args.push(...options.additionalArgs);
return childProcess({
command: ['node', cliEntrypoint, ...args],
env: cleanEnv(env),
cwd: options.cwd ? path.resolve(baseDir, options.cwd) : baseDir,
});
}
const cwd = options.cwd ? path.resolve(baseDir, options.cwd) : baseDir;
// eslint-disable-next-line prefer-const
let { exitCode, output } = await runPlaywrightCommand(childProcess, cwd, args, {
async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], baseDir: string, params: any, env: NodeJS.ProcessEnv, options: RunOptions, files: Files, mergeReports: (reportFolder: string, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<CliRunResult>, useIntermediateMergeReport: boolean): Promise<RunResult> {
let reporter;
if (useIntermediateMergeReport) {
reporter = params.reporter;
params.reporter = 'blob';
}
const reportFile = path.join(baseDir, 'report.json');
const testProcess = startPlaywrightTest(childProcess, baseDir, params, {
PW_TEST_REPORTER: path.join(__dirname, '../../packages/playwright-test/lib/reporters/json.js'),
PLAYWRIGHT_JSON_OUTPUT_NAME: reportFile,
...env,
}, options.sendSIGINTAfter);
}, options);
const { exitCode } = await testProcess.exited;
let output = testProcess.output.toString();
if (useIntermediateMergeReport) {
const additionalArgs = [];
@ -130,25 +137,14 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
const config = configFile(baseDir, files);
if (config)
additionalArgs.push('--config', config);
const cwd = options.cwd ? path.resolve(baseDir, options.cwd) : baseDir;
const mergeResult = await mergeReports('blob-report', env, { cwd, additionalArgs });
expect(mergeResult.exitCode).toBe(0);
output = mergeResult.output;
}
const summary = (re: RegExp) => {
let result = 0;
let match = re.exec(output);
while (match) {
result += (+match[1]);
match = re.exec(output);
}
return result;
};
const passed = summary(/(\d+) passed/g);
const failed = summary(/(\d+) failed/g);
const flaky = summary(/(\d+) flaky/g);
const skipped = summary(/(\d+) skipped/g);
const interrupted = summary(/(\d+) interrupted/g);
const parsed = parseTestRunnerOutput(output);
let report;
try {
report = JSON.parse(fs.readFileSync(reportFile).toString());
@ -171,66 +167,23 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
if (report)
visitSuites(report.suites);
const strippedOutput = stripAnsi(output);
return {
...parsed,
exitCode,
output: strippedOutput,
outputLines: strippedOutput.split('\n').filter(line => line.startsWith('%%')).map(line => line.substring(2).trim()),
rawOutput: output,
passed,
failed,
flaky,
skipped,
interrupted,
report,
results,
};
}
async function runPlaywrightListFiles(childProcess: CommonFixtures['childProcess'], baseDir: string, env: NodeJS.ProcessEnv): Promise<{ output: string, exitCode: number }> {
const reportFile = path.join(baseDir, 'report.json');
// eslint-disable-next-line prefer-const
let { exitCode, output } = await runPlaywrightCommand(childProcess, baseDir, ['list-files'], {
PW_TEST_REPORTER: path.join(__dirname, '../../packages/playwright-test/lib/reporters/json.js'),
PLAYWRIGHT_JSON_OUTPUT_NAME: reportFile,
...env,
});
return { exitCode, output };
}
function watchPlaywrightTest(childProcess: CommonFixtures['childProcess'], baseDir: string, env: NodeJS.ProcessEnv, options: RunOptions): TestChildProcess {
const args = ['test', '--workers=2'];
if (options.additionalArgs)
args.push(...options.additionalArgs);
const cwd = options.cwd ? path.resolve(baseDir, options.cwd) : baseDir;
const command = ['node', cliEntrypoint];
command.push(...args);
const testProcess = childProcess({
command,
env: cleanEnv({ PWTEST_WATCH: '1', ...env }),
cwd,
});
return testProcess;
}
async function runPlaywrightCommand(childProcess: CommonFixtures['childProcess'], cwd: string, commandWithArguments: string[], env: NodeJS.ProcessEnv, sendSIGINTAfter?: number): Promise<CliRunResult> {
const command = ['node', cliEntrypoint];
command.push(...commandWithArguments);
const testProcess = childProcess({
command,
command: ['node', cliEntrypoint, 'list-files'],
env: cleanEnv(env),
cwd,
cwd: baseDir,
});
let didSendSigint = false;
testProcess.onOutput = () => {
if (sendSIGINTAfter && !didSendSigint && countTimes(testProcess.output, '%%SEND-SIGINT%%') >= sendSIGINTAfter) {
didSendSigint = true;
process.kill(testProcess.process.pid!, 'SIGINT');
}
};
const { exitCode } = await testProcess.exited;
return { exitCode, output: testProcess.output.toString() };
return { exitCode, output: testProcess.output };
}
export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
@ -261,7 +214,6 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
}
export type RunOptions = {
sendSIGINTAfter?: number;
additionalArgs?: string[];
cwd?: string,
};
@ -271,6 +223,7 @@ type Fixtures = {
runInlineTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<RunResult>;
runListFiles: (files: Files) => Promise<{ output: string, exitCode: number }>;
runWatchTest: (files: Files, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
interactWithTestRunner: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
runTSC: (files: Files) => Promise<TSCResult>;
mergeReports: (reportFolder: string, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<CliRunResult>
useIntermediateMergeReport: boolean;
@ -310,12 +263,16 @@ export const test = base
await rimraf(cacheDir);
},
runWatchTest: async ({ childProcess }, use, testInfo: TestInfo) => {
runWatchTest: async ({ interactWithTestRunner }, use, testInfo: TestInfo) => {
await use((files, env, options) => interactWithTestRunner(files, {}, { ...env, PWTEST_WATCH: '1' }, options));
},
interactWithTestRunner: async ({ childProcess }, use, testInfo: TestInfo) => {
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
let testProcess: TestChildProcess | undefined;
await use(async (files: Files, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => {
await use(async (files: Files, params: Params = {}, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => {
const baseDir = await writeFiles(testInfo, files, true);
testProcess = watchPlaywrightTest(childProcess, baseDir, { ...env, PWTEST_CACHE_DIR: cacheDir }, options);
testProcess = startPlaywrightTest(childProcess, baseDir, params, { ...env, PWTEST_CACHE_DIR: cacheDir }, options);
return testProcess;
});
await testProcess?.kill();
@ -388,18 +345,6 @@ export function stripAnsi(str: string): string {
return str.replace(asciiRegex, '');
}
export function countTimes(s: string, sub: string): number {
let result = 0;
for (let index = 0; index !== -1;) {
index = s.indexOf(sub, index);
if (index !== -1) {
result++;
index += sub.length;
}
}
return result;
}
export function createImage(width: number, height: number, r: number = 0, g: number = 0, b: number = 0, a: number = 255): Buffer {
const image = new PNG({ width, height });
// Make both images red.
@ -446,3 +391,32 @@ export function expectTestHelper(result: RunResult) {
}
};
}
export function parseTestRunnerOutput(output: string) {
const summary = (re: RegExp) => {
let result = 0;
let match = re.exec(output);
while (match) {
result += (+match[1]);
match = re.exec(output);
}
return result;
};
const passed = summary(/(\d+) passed/g);
const failed = summary(/(\d+) failed/g);
const flaky = summary(/(\d+) flaky/g);
const skipped = summary(/(\d+) skipped/g);
const interrupted = summary(/(\d+) interrupted/g);
const strippedOutput = stripAnsi(output);
return {
output: strippedOutput,
outputLines: strippedOutput.split('\n').filter(line => line.startsWith('%%')).map(line => line.substring(2).trim()),
rawOutput: output,
passed,
failed,
flaky,
skipped,
interrupted,
};
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { test, expect } from './playwright-test-fixtures';
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures';
import fs from 'fs';
import path from 'path';
import { spawnSync } from 'child_process';
@ -431,10 +431,10 @@ test('should throw when using page in beforeAll', async ({ runInlineTest }) => {
expect(result.output).toContain(`Error: "context" and "page" fixtures are not supported in "beforeAll"`);
});
test('should report click error on sigint', async ({ runInlineTest }) => {
test('should report click error on sigint', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('timedout', async ({ page }) => {
@ -445,9 +445,13 @@ test('should report click error on sigint', async ({ runInlineTest }) => {
await promise;
});
`,
}, { workers: 1 }, {}, { sendSIGINTAfter: 1 });
}, { workers: 1 });
await testProcess.waitForOutput('%%SEND-SIGINT%%');
process.kill(testProcess.process.pid!, 'SIGINT');
const { exitCode } = await testProcess.exited;
expect(exitCode).toBe(130);
expect(result.exitCode).toBe(130);
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
expect(result.failed).toBe(0);
expect(result.interrupted).toBe(1);

View File

@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import path from 'path';
import { test, expect } from './playwright-test-fixtures';
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures';
test('it should not allow multiple tests with the same name per suite', async ({ runInlineTest }) => {
const result = await runInlineTest({
@ -93,10 +95,10 @@ test('should continue with other tests after worker process suddenly exits', asy
expect(result.output).toContain('Internal error: worker process exited unexpectedly');
});
test('sigint should stop workers', async ({ runInlineTest }) => {
test('sigint should stop workers', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'a.spec.js': `
import { test, expect } from '@playwright/test';
test('interrupted1', async () => {
@ -117,8 +119,16 @@ test('sigint should stop workers', async ({ runInlineTest }) => {
console.log('\\n%%skipped2');
});
`,
}, { 'workers': 2, 'reporter': 'line,json' }, {}, { sendSIGINTAfter: 2 });
expect(result.exitCode).toBe(130);
}, { 'workers': 2, 'reporter': 'line,json' }, {
PW_TEST_REPORTER: path.join(__dirname, '../../packages/playwright-test/lib/reporters/json.js'),
PLAYWRIGHT_JSON_OUTPUT_NAME: 'report.json',
});
await testProcess.waitForOutput('%%SEND-SIGINT%%', 2);
process.kill(testProcess.process.pid!, 'SIGINT');
const { exitCode } = await testProcess.exited;
expect(exitCode).toBe(130);
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
expect(result.failed).toBe(0);
expect(result.skipped).toBe(2);
@ -130,11 +140,12 @@ test('sigint should stop workers', async ({ runInlineTest }) => {
expect(result.output).toContain('Test was interrupted.');
expect(result.output).not.toContain('Test timeout of');
const interrupted2 = result.report.suites[1].specs[0];
const report = JSON.parse(fs.readFileSync(test.info().outputPath('report.json'), 'utf8'));
const interrupted2 = report.suites[1].specs[0];
expect(interrupted2.title).toBe('interrupted2');
expect(interrupted2.tests[0].results[0].workerIndex === 0 || interrupted2.tests[0].results[0].workerIndex === 1).toBe(true);
const skipped2 = result.report.suites[1].specs[1];
const skipped2 = report.suites[1].specs[1];
expect(skipped2.title).toBe('skipped2');
expect(skipped2.tests[0].results[0].workerIndex).toBe(-1);
});
@ -169,10 +180,10 @@ test('should use the first occurring error when an unhandled exception was throw
expect(result.report.suites[0].specs[0].tests[0].results[0].error!.message).toBe('first error');
});
test('worker interrupt should report errors', async ({ runInlineTest }) => {
test('worker interrupt should report errors', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
@ -187,8 +198,13 @@ test('worker interrupt should report errors', async ({ runInlineTest }) => {
await throwOnTeardown;
});
`,
}, {}, {}, { sendSIGINTAfter: 1 });
expect(result.exitCode).toBe(130);
});
await testProcess.waitForOutput('%%SEND-SIGINT%%');
process.kill(testProcess.process.pid!, 'SIGINT');
const { exitCode } = await testProcess.exited;
expect(exitCode).toBe(130);
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
expect(result.failed).toBe(0);
expect(result.interrupted).toBe(1);
@ -334,10 +350,10 @@ test('should not hang if test suites in worker are inconsistent with runner', as
expect(result.report.suites[0].specs[1].tests[0].results[0].error!.message).toBe('Test(s) not found in the worker process. Make sure test titles do not change:\nproject-name > a.spec.js > Test 1 - bar\nproject-name > a.spec.js > Test 2 - baz');
});
test('sigint should stop global setup', async ({ runInlineTest }) => {
test('sigint should stop global setup', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'playwright.config.ts': `
module.exports = {
globalSetup: './globalSetup',
@ -360,18 +376,22 @@ test('sigint should stop global setup', async ({ runInlineTest }) => {
import { test, expect } from '@playwright/test';
test('test', async () => { });
`,
}, { 'workers': 1 }, {}, { sendSIGINTAfter: 1 });
expect(result.exitCode).toBe(130);
}, { 'workers': 1 });
await testProcess.waitForOutput('%%SEND-SIGINT%%');
process.kill(testProcess.process.pid!, 'SIGINT');
const { exitCode } = await testProcess.exited;
expect(exitCode).toBe(130);
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
const output = result.output;
expect(output).toContain('Global setup');
expect(output).not.toContain('Global teardown');
expect(result.output).toContain('Global setup');
expect(result.output).not.toContain('Global teardown');
});
test('sigint should stop plugins', async ({ runInlineTest }) => {
test('sigint should stop plugins', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'playwright.config.ts': `
const _plugins = [];
_plugins.push(() => ({
@ -403,21 +423,25 @@ test('sigint should stop plugins', async ({ runInlineTest }) => {
console.log('testing!');
});
`,
}, { 'workers': 1 }, {}, { sendSIGINTAfter: 1 });
expect(result.exitCode).toBe(130);
}, { 'workers': 1 });
await testProcess.waitForOutput('%%SEND-SIGINT%%');
process.kill(testProcess.process.pid!, 'SIGINT');
const { exitCode } = await testProcess.exited;
expect(exitCode).toBe(130);
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
const output = result.output;
expect(output).toContain('Plugin1 setup');
expect(output).toContain('Plugin1 teardown');
expect(output).not.toContain('Plugin2 setup');
expect(output).not.toContain('Plugin2 teardown');
expect(output).not.toContain('testing!');
expect(result.output).toContain('Plugin1 setup');
expect(result.output).toContain('Plugin1 teardown');
expect(result.output).not.toContain('Plugin2 setup');
expect(result.output).not.toContain('Plugin2 teardown');
expect(result.output).not.toContain('testing!');
});
test('sigint should stop plugins 2', async ({ runInlineTest }) => {
test('sigint should stop plugins 2', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'playwright.config.ts': `
const _plugins = [];
_plugins.push(() => ({
@ -447,15 +471,19 @@ test('sigint should stop plugins 2', async ({ runInlineTest }) => {
console.log('testing!');
});
`,
}, { 'workers': 1 }, {}, { sendSIGINTAfter: 1 });
expect(result.exitCode).toBe(130);
}, { 'workers': 1 });
await testProcess.waitForOutput('%%SEND-SIGINT%%');
process.kill(testProcess.process.pid!, 'SIGINT');
const { exitCode } = await testProcess.exited;
expect(exitCode).toBe(130);
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
const output = result.output;
expect(output).toContain('Plugin1 setup');
expect(output).toContain('Plugin2 setup');
expect(output).toContain('Plugin1 teardown');
expect(output).toContain('Plugin2 teardown');
expect(output).not.toContain('testing!');
expect(result.output).toContain('Plugin1 setup');
expect(result.output).toContain('Plugin2 setup');
expect(result.output).toContain('Plugin1 teardown');
expect(result.output).toContain('Plugin2 teardown');
expect(result.output).not.toContain('testing!');
});
test('should not crash with duplicate titles and .only', async ({ runInlineTest }) => {

View File

@ -16,7 +16,7 @@
import type http from 'http';
import path from 'path';
import { test, expect } from './playwright-test-fixtures';
import { test, expect, parseTestRunnerOutput } from './playwright-test-fixtures';
import { createHttpServer } from '../../packages/playwright-core/lib/utils/network';
const SIMPLE_SERVER_PATH = path.join(__dirname, 'assets', 'simple-server.js');
@ -705,10 +705,10 @@ test('should be able to ignore "stderr"', async ({ runInlineTest }, { workerInde
expect(result.output).not.toContain('error from server');
});
test('should forward stdout when set to "pipe" before server is ready', async ({ runInlineTest }, { workerIndex }) => {
test('should forward stdout when set to "pipe" before server is ready', async ({ interactWithTestRunner }) => {
test.skip(process.platform === 'win32', 'No sending SIGINT on Windows');
const result = await runInlineTest({
const testProcess = await interactWithTestRunner({
'web-server.js': `
console.log('output from server');
console.log('\\n%%SEND-SIGINT%%');
@ -728,7 +728,12 @@ test('should forward stdout when set to "pipe" before server is ready', async ({
},
};
`,
}, { workers: 1 }, {}, { sendSIGINTAfter: 1 });
}, { workers: 1 });
await testProcess.waitForOutput('%%SEND-SIGINT%%');
process.kill(testProcess.process.pid!, 'SIGINT');
await testProcess.exited;
const result = parseTestRunnerOutput(testProcess.output);
expect(result.passed).toBe(0);
expect(result.output).toContain('[WebServer] output from server');
expect(result.output).not.toContain('Timed out waiting 3000ms');