fix: restart dev-server on config change (#21212)

* fix: restart dev-server on config change

* close dev server before cp is spawned

* fix test that is failing... not sure why

* cleanup unsued close events

* remove wait

* add back in close for unit tests

Co-authored-by: Tim Griesser <tgriesser10@gmail.com>
This commit is contained in:
Zachary Williams
2022-05-01 21:05:01 -05:00
committed by GitHub
parent 1289b01947
commit 00a0f5a0e9
14 changed files with 264 additions and 201 deletions

View File

@@ -33,6 +33,7 @@ export async function devServer (config: ViteDevServerConfig): Promise<Cypress.R
return {
port,
// Close is for unit testing only. We kill this child process which will handle the closing of the server
close (cb) {
return server.close().then(() => cb?.()).catch(cb)
},

View File

@@ -76,6 +76,7 @@ export function devServer (devServerConfig: WebpackDevServerConfig): Promise<Cyp
resolve({
port,
// Close is for unit testing only. We kill this child process which will handle the closing of the server
close: (done) => {
srv.close((err) => {
if (err) {
@@ -101,7 +102,8 @@ export function devServer (devServerConfig: WebpackDevServerConfig): Promise<Cyp
resolve({
port: result.server.options.port as number,
close: (done) => {
// Close is for unit testing only. We kill this child process which will handle the closing of the server
close: async (done) => {
debug('closing dev server')
result.server.stop().then(() => done?.()).catch(done).finally(() => {
debug('closed dev server')

View File

@@ -4,156 +4,192 @@ import { getPathForPlatform } from '../../src/paths'
import { snapshotAUTPanel } from './support/snapshot-aut-panel'
describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: 10000 }, () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
cy.findBrowsers()
cy.openProject('cypress-in-cypress')
cy.startAppServer('component')
})
it('test component', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.location().should((location) => {
expect(location.hash).to.contain('TestComponent.spec')
context('default config', () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
cy.findBrowsers()
cy.openProject('cypress-in-cypress')
cy.startAppServer('component')
})
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
it('test component', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.location().should((location) => {
expect(location.hash).to.contain('TestComponent.spec')
})
cy.findByTestId('aut-url').should('not.exist')
cy.findByTestId('select-browser').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
cy.contains('Canary').should('be.visible')
cy.findByTestId('viewport').click()
cy.findByTestId('aut-url').should('not.exist')
cy.findByTestId('select-browser').click()
snapshotAUTPanel('browsers open')
cy.contains('Canary').should('be.hidden')
cy.contains('The viewport determines the width and height of your application. By default the viewport will be 500px by 500px for Component Testing unless specified by a cy.viewport command.')
.should('be.visible')
cy.contains('Canary').should('be.visible')
cy.findByTestId('viewport').click()
snapshotAUTPanel('viewport info open')
snapshotAUTPanel('browsers open')
cy.contains('Canary').should('be.hidden')
cy.contains('The viewport determines the width and height of your application. By default the viewport will be 500px by 500px for Component Testing unless specified by a cy.viewport command.')
.should('be.visible')
cy.get('body').click()
snapshotAUTPanel('viewport info open')
cy.findByTestId('playground-activator').click()
cy.findByTestId('playground-selector').clear().type('[data-cy-root]')
cy.get('body').click()
snapshotAUTPanel('cy.get selector')
cy.findByTestId('playground-activator').click()
cy.findByTestId('playground-selector').clear().type('[data-cy-root]')
cy.findByTestId('playground-num-elements').contains('1 Match')
snapshotAUTPanel('cy.get selector')
cy.window().then((win) => cy.spy(win.console, 'log'))
cy.findByTestId('playground-print').click().window().then((win) => {
expect(win.console.log).to.have.been.calledWith('%cCommand: ', 'font-weight: bold', 'cy.get(\'[data-cy-root]\')')
cy.findByTestId('playground-num-elements').contains('1 Match')
cy.window().then((win) => cy.spy(win.console, 'log'))
cy.findByTestId('playground-print').click().window().then((win) => {
expect(win.console.log).to.have.been.calledWith('%cCommand: ', 'font-weight: bold', 'cy.get(\'[data-cy-root]\')')
})
cy.findByLabelText('Selector Methods').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click()
cy.findByTestId('playground-selector').clear().type('Component Test')
snapshotAUTPanel('cy.contains selector')
cy.findByTestId('playground-num-elements').contains('1 Match')
})
cy.findByLabelText('Selector Methods').click()
cy.findByRole('menuitem', { name: 'cy.contains' }).click()
it('navigation between specs and other parts of the app works', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
cy.findByTestId('playground-selector').clear().type('Component Test')
// go to Settings page and back to spec runner
cy.contains('a', 'Settings').click()
cy.contains(defaultMessages.settingsPage.device.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
snapshotAUTPanel('cy.contains selector')
// go to Runs page and back to spec runner
cy.contains('a', 'Runs').click()
cy.contains(defaultMessages.runs.connect.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
})
cy.findByTestId('playground-num-elements').contains('1 Match')
})
it('redirects to the specs list with error if a spec is not found', () => {
cy.visitApp()
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage
const badFilePath = 'src/DoesNotExist.spec.js'
it('navigation between specs and other parts of the app works', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
// go to Settings page and back to spec runner
cy.contains('a', 'Settings').click()
cy.contains(defaultMessages.settingsPage.device.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
// go to Runs page and back to spec runner
cy.contains('a', 'Runs').click()
cy.contains(defaultMessages.runs.connect.title).should('be.visible')
cy.contains('a', 'Specs').click()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
})
it('redirects to the specs list with error if a spec is not found', () => {
cy.visitApp()
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage
const badFilePath = 'src/DoesNotExist.spec.js'
cy.visitApp(`/specs/runner?file=${badFilePath}`)
cy.contains(noSpecErrorTitle).should('be.visible')
cy.contains(noSpecErrorIntro).should('be.visible')
cy.contains(noSpecErrorExplainer).should('be.visible')
cy.contains(getPathForPlatform(badFilePath)).should('be.visible')
cy.location()
.its('href')
.should('eq', 'http://localhost:4455/__/#/specs')
// should clear after reload
cy.reload()
cy.contains(noSpecErrorTitle).should('not.exist')
})
it('redirects to the specs list with error if an open spec is not found when specs list updates', () => {
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage
const goodFilePath = 'src/TestComponent.spec.jsx'
cy.visitApp(`/specs/runner?file=${goodFilePath}`)
cy.contains('renders the test component').should('be.visible')
cy.withCtx((ctx, o) => {
ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path)))
}, { path: goodFilePath }).then(() => {
cy.visitApp(`/specs/runner?file=${badFilePath}`)
cy.contains(noSpecErrorTitle).should('be.visible')
cy.contains(noSpecErrorIntro).should('be.visible')
cy.contains(noSpecErrorExplainer).should('be.visible')
cy.contains(getPathForPlatform(goodFilePath)).should('be.visible')
cy.contains(getPathForPlatform(badFilePath)).should('be.visible')
cy.location()
.its('href')
.should('eq', 'http://localhost:4455/__/#/specs')
// should clear after reload
cy.reload()
cy.contains(noSpecErrorTitle).should('not.exist')
})
it('redirects to the specs list with error if an open spec is not found when specs list updates', () => {
const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage
const goodFilePath = 'src/TestComponent.spec.jsx'
cy.visitApp(`/specs/runner?file=${goodFilePath}`)
cy.contains('renders the test component').should('be.visible')
cy.withCtx((ctx, o) => {
ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path)))
}, { path: goodFilePath }).then(() => {
cy.contains(noSpecErrorTitle).should('be.visible')
cy.contains(noSpecErrorIntro).should('be.visible')
cy.contains(noSpecErrorExplainer).should('be.visible')
cy.contains(getPathForPlatform(goodFilePath)).should('be.visible')
cy.location()
.its('href')
.should('eq', 'http://localhost:4455/__/#/specs')
})
})
it('browser picker in runner calls mutation with current spec path', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.app, 'setActiveBrowserById')
o.sinon.stub(ctx.actions.project, 'launchProject').resolves()
})
cy.get('[data-cy="select-browser"]')
.click()
cy.contains('Firefox')
.click()
cy.withCtx((ctx, o) => {
const browserId = (ctx.actions.app.setActiveBrowserById as SinonStub).args[0][0]
const genId = ctx.fromId(browserId, 'Browser')
expect(ctx.actions.app.setActiveBrowserById).to.have.been.calledWith(browserId)
expect(genId).to.eql('firefox-firefox-stable')
expect(ctx.actions.project.launchProject).to.have.been.calledWith(
ctx.coreData.currentTestingType, {}, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')),
)
})
})
it('restarts dev server on config change', () => {
cy.visitApp()
cy.withCtx(async (ctx, { testState, sinon }) => {
sinon.stub(ctx._apis.projectApi.getDevServer(), 'close')
const devServerReady =
new Promise<void>((res) => {
ctx._apis.projectApi.getDevServer().emitter.on('dev-server:compile:success', () => res())
})
testState.originalCypressConfig = await ctx.file.readFileInProject('cypress.config.js')
const newCypressConfig = testState.originalCypressConfig.replace(`webpackConfig: require('./webpack.config.js')`, `webpackConfig: {}`)
await ctx.actions.file.writeFileInProject('cypress.config.js', newCypressConfig)
await devServerReady
})
cy.contains('TestComponent.spec').click()
cy.get('.failed > .num').should('contain', 1)
cy.withCtx(async (ctx, { testState }) => {
await ctx.actions.file.writeFileInProject('cypress.config.js', testState.originalCypressConfig)
})
cy.get('.passed > .num').should('contain', 1)
})
})
it('browser picker in runner calls mutation with current spec path', () => {
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('[data-model-state="passed"]').should('contain', 'renders the test component')
cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.app, 'setActiveBrowserById')
o.sinon.stub(ctx.actions.project, 'launchProject').resolves()
context('custom config', () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
cy.findBrowsers()
})
cy.get('[data-cy="select-browser"]')
.click()
it('set the correct viewport values from CLI', () => {
cy.openProject('cypress-in-cypress', ['--config', 'viewportWidth=333,viewportHeight=333'])
cy.startAppServer('component')
cy.contains('Firefox')
.click()
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.withCtx((ctx, o) => {
const browserId = (ctx.actions.app.setActiveBrowserById as SinonStub).args[0][0]
const genId = ctx.fromId(browserId, 'Browser')
expect(ctx.actions.app.setActiveBrowserById).to.have.been.calledWith(browserId)
expect(genId).to.eql('firefox-firefox-stable')
expect(ctx.actions.project.launchProject).to.have.been.calledWith(
ctx.coreData.currentTestingType, {}, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')),
)
cy.get('#unified-runner').should('have.css', 'width', '333px')
cy.get('#unified-runner').should('have.css', 'height', '333px')
})
})
it('set the correct viewport values from CLI', () => {
cy.openProject('cypress-in-cypress', ['--config', 'viewportWidth=333,viewportHeight=333'])
cy.startAppServer('component')
cy.visitApp()
cy.contains('TestComponent.spec').click()
cy.get('#unified-runner').should('have.css', 'width', '333px')
cy.get('#unified-runner').should('have.css', 'height', '333px')
})
})

View File

@@ -2,10 +2,9 @@ describe('Spec List - Git Status', () => {
beforeEach(() => {
cy.scaffoldProject('cypress-in-cypress')
.then((projectPath) => {
cy.task('initGitRepoForTestProject', projectPath)
cy.wait(500)
cy.openProject('cypress-in-cypress')
cy.startAppServer('e2e')
cy.task('initGitRepoForTestProject', projectPath)
cy.visitApp()
})
})

View File

@@ -1,5 +1,5 @@
import type { CodeGenType, MutationSetProjectPreferencesArgs, NexusGenObjects, NexusGenUnions, TestingTypeEnum } from '@packages/graphql/src/gen/nxs.gen'
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject } from '@packages/types'
import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject, FullConfig } from '@packages/types'
import execa from 'execa'
import path from 'path'
import assert from 'assert'
@@ -34,7 +34,10 @@ export interface ProjectApiShape {
getCurrentProjectSavedState(): {} | undefined
setPromptShown(slug: string): void
getDevServer (): {
updateSpecs: (specs: FoundSpec[]) => void
updateSpecs(specs: FoundSpec[]): void
start(options: {specs: Cypress.Spec[], config: FullConfig}): Promise<{port: number}>
close(): void
emitter: EventEmitter
}
isListening: (url: string) => Promise<void>
}

View File

@@ -219,6 +219,26 @@ export class ProjectLifecycleManager {
this.ctx.emitter.toLaunchpad()
},
onFinalConfigLoaded: async (finalConfig: FullConfig) => {
if (this._currentTestingType && finalConfig.specPattern) {
await this.ctx.actions.project.setSpecsFoundBySpecPattern({
path: this.projectRoot,
testingType: this._currentTestingType,
specPattern: this.ctx.modeOptions.spec || finalConfig.specPattern,
excludeSpecPattern: finalConfig.excludeSpecPattern,
additionalIgnorePattern: finalConfig.additionalIgnorePattern,
})
}
if (this._currentTestingType === 'component') {
const devServerOptions = await this.ctx._apis.projectApi.getDevServer().start({ specs: this.ctx.project.specs, config: finalConfig })
if (!devServerOptions?.port) {
this.ctx.onError(getError('CONFIG_FILE_DEV_SERVER_INVALID_RETURN', devServerOptions))
}
finalConfig.baseUrl = `http://localhost:${devServerOptions?.port}`
}
this._cachedFullConfig = finalConfig
// This happens automatically with openProjectCreate in run mode
@@ -230,16 +250,6 @@ export class ProjectLifecycleManager {
await this.setInitialActiveBrowser()
if (this._currentTestingType && finalConfig.specPattern) {
await this.ctx.actions.project.setSpecsFoundBySpecPattern({
path: this.projectRoot,
testingType: this._currentTestingType,
specPattern: this.ctx.modeOptions.spec || finalConfig.specPattern,
excludeSpecPattern: finalConfig.excludeSpecPattern,
additionalIgnorePattern: finalConfig.additionalIgnorePattern,
})
}
this._pendingInitialize?.resolve(finalConfig)
this.ctx.emitter.configChange()
},
@@ -297,6 +307,12 @@ export class ProjectLifecycleManager {
await this.initializeConfig()
if (this._currentTestingType && this.isTestingTypeConfigured(this._currentTestingType)) {
if (this._currentTestingType === 'component') {
// Since we refresh the dev-server on config changes, we need to close it and clean up it's listeners
// before we can start a new one. This needs to happen before we have registered the events of the child process
this.ctx._apis.projectApi.getDevServer().close()
}
this._configManager.loadTestingType()
} else {
this.setAndLoadCurrentTestingType(null)

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap" rel="stylesheet">
<style>
body {
font-family: "Courier Prime", Courier, monospace;
padding: 0 1em;
line-height: 1.4;
color: #eee;
background-color: #111;
}
pre {
padding: 0 0;
margin: 0 0;
font-family: "Courier Prime", Courier, monospace;
}
body {
margin: 5px;
padding: 0;
overflow: hidden;
}
pre {
white-space: pre-wrap;
word-break: break-word;
-webkit-font-smoothing: antialiased;
}
</style>
</head>
<body><pre><span style="color:#e05561">The returned value of the <span style="color:#e5e510">devServer<span style="color:#e05561"> function is not valid.<span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561">The returned value must be an object with a <span style="color:#e5e510">port<span style="color:#e05561"> property of the dev-server that is running.<span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561">Instead, we saw:<span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#de73ff">{}<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561">Learn more: https://on.cypress.io/dev-server<span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
</pre></body></html>

View File

@@ -1370,6 +1370,20 @@ export const AllCypressErrors = {
`
},
CONFIG_FILE_DEV_SERVER_INVALID_RETURN: (devServerOptions: any) => {
return errTemplate`
The returned value of the ${fmt.highlight('devServer')} function is not valid.
The returned value must be an object with a ${fmt.highlight('port')} property of the dev-server that is running.
Instead, we saw:
${fmt.stringify(devServerOptions)}
Learn more: https://on.cypress.io/dev-server
`
},
UNEXPECTED_MUTATION_ERROR: (mutationField: string, args: any, err: Error) => {
return errTemplate`
An unexpected internal error occurred while executing the ${fmt.highlight(mutationField)} operation with payload:

View File

@@ -1051,6 +1051,11 @@ describe('visual error templates', () => {
default: ['/path/to/plugins/file.js'],
}
},
CONFIG_FILE_DEV_SERVER_INVALID_RETURN: () => {
return {
default: [{}],
}
},
PLUGINS_RUN_EVENT_ERROR: () => {
return {
default: ['before:spec', makeErr()],

View File

@@ -513,6 +513,7 @@ enum ErrorTypeEnum {
CHROME_WEB_SECURITY_NOT_SUPPORTED
COMPONENT_FOLDER_REMOVED
CONFIG_FILES_LANGUAGE_CONFLICT
CONFIG_FILE_DEV_SERVER_INVALID_RETURN
CONFIG_FILE_DEV_SERVER_IS_NOT_VALID
CONFIG_FILE_INVALID_DEV_START_EVENT
CONFIG_FILE_INVALID_ROOT_CONFIG

View File

@@ -266,15 +266,7 @@ export class OpenProject {
})
try {
const cfg = await this.projectBase.initializeConfig()
await this._ctx.actions.project.setSpecsFoundBySpecPattern({
path,
testingType,
specPattern: options.spec || cfg.specPattern,
excludeSpecPattern: cfg.excludeSpecPattern,
additionalIgnorePattern: cfg.additionalIgnorePattern,
})
await this.projectBase.initializeConfig()
await this.projectBase.open()
} catch (err: any) {

View File

@@ -19,12 +19,6 @@ plugins.registerHandler((ipc) => {
ipc.on('dev-server:compile:success', ({ specFile } = {}) => {
baseEmitter.emit('dev-server:compile:success', { specFile })
})
baseEmitter.on('dev-server:close', () => {
debug('base emitter plugin close event')
ipc.send('dev-server:close')
})
})
// for simpler stubbing from unit tests
@@ -45,8 +39,6 @@ const API = {
close () {
debug('close dev-server')
baseEmitter.emit('close')
baseEmitter.removeAllListeners()
},
}

View File

@@ -8,7 +8,6 @@ import { Automation } from './automation'
import browsers from './browsers'
import * as config from './config'
import * as errors from './errors'
import devServer from './plugins/dev-server'
import preprocessor from './plugins/preprocessor'
import runEvents from './plugins/run_events'
import { checkSupportFile } from './project_utils'
@@ -156,12 +155,6 @@ export class ProjectBase<TServer extends Server> extends EE {
this._server = this.createServer(this.testingType)
const { ctDevServerPort } = await this.initializeSpecsAndDevServer(cfg)
if (this.testingType === 'component') {
cfg.baseUrl = `http://localhost:${ctDevServerPort}`
}
const [port, warning] = await this._server.open(cfg, {
getCurrentBrowser: () => this.browser,
getSpec: () => this.spec,
@@ -266,7 +259,6 @@ export class ProjectBase<TServer extends Server> extends EE {
__reset () {
preprocessor.close()
devServer.close()
process.chdir(localCwd)
}
@@ -308,46 +300,6 @@ export class ProjectBase<TServer extends Server> extends EE {
options.onError(err)
}
async initializeSpecsAndDevServer (updatedConfig: Cfg): Promise<{
ctDevServerPort: number | undefined
}> {
const specs = this.ctx.project.specs || []
let ctDevServerPort: number | undefined
if (!this.ctx.currentProject) {
throw new Error('Cannot set specs without current project')
}
updatedConfig.specs = specs
if (this.testingType === 'component' && !this.options.skipPluginInitializeForTesting) {
const { port } = await this.startCtDevServer(specs, updatedConfig)
ctDevServerPort = port
}
return {
ctDevServerPort,
}
}
async startCtDevServer (specs: Cypress.Cypress['spec'][], config: any) {
// CT uses a dev-server to build the bundle.
// We start the dev server here.
const devServerOptions = await devServer.start({ specs, config })
if (!devServerOptions) {
throw new Error([
'It looks like nothing was returned from on(\'dev-server:start\', {here}).',
'Make sure that the dev-server:start function returns an object.',
'For example: on("dev-server:start", () => startWebpackDevServer({ webpackConfig }))',
].join('\n'))
}
return { port: devServerOptions.port }
}
initializeReporter ({
report,
reporter,

View File

@@ -14,5 +14,8 @@ module.exports = {
}
}
}
}
},
// These tests should run quickly / fail quickly,
// since we intentionally causing error states for testing
defaultCommandTimeout: 1000
}