| | |
| | | } |
| | | |
| | | const webRepo = resolve(import.meta.dirname, '../../..') |
| | | const backendRepo = resolve(webRepo, '../BatteryDetection') |
| | | const backendRepo = resolve(process.env.TICKET04_BACKEND_REPO || resolve(webRepo, '../BatteryDetection')) |
| | | const artifactDir = resolve(webRepo, 'tests/e2e/.artifacts') |
| | | const backendJar = resolve(backendRepo, 'ruoyi-admin/target/ruoyi-admin.jar') |
| | | const apiPort = Number(process.env.TICKET04_API_PORT || 18001) |
| | |
| | | |
| | | readonly backendUrl = `http://127.0.0.1:${apiPort}` |
| | | |
| | | async start() { |
| | | async start(profiles = 'druid,dev-simulator') { |
| | | await Promise.all([mysqlPort, redisPort, apiPort, simulatorPort].map(assertPortAvailable)) |
| | | this.docker = resolveDocker() |
| | | mkdirSync(artifactDir, { recursive: true }) |
| | |
| | | ]) |
| | | await this.waitForContainers() |
| | | this.buildBackend() |
| | | await this.startBackend() |
| | | await this.startBackend(profiles) |
| | | this.disableCaptchaInIsolatedDatabase() |
| | | } catch (error) { |
| | | await this.stop() |
| | |
| | | } |
| | | } |
| | | |
| | | async restartBackend() { |
| | | async restartBackend(profiles = 'druid,dev-simulator') { |
| | | await this.stopBackend() |
| | | await this.startBackend() |
| | | await this.startBackend(profiles) |
| | | } |
| | | |
| | | queryDatabase(sql: string) { |
| | | if (!/^SELECT\s/i.test(sql)) throw new Error('验收查询只允许 SELECT') |
| | | return this.dockerRun(['exec', this.mysqlContainer, 'mysql', '-uroot', `-p${mysqlPassword}`, |
| | | '--database=battery_detection', '--batch', '--skip-column-names', '-e', sql]).stdout.trim() |
| | | } |
| | | |
| | | // 仅操作本次启动的临时容器,用于受控验收数据及事务故障注入。 |
| | | executeIsolatedSql(sql: string) { |
| | | return this.dockerRun(['exec', this.mysqlContainer, 'mysql', '-uroot', `-p${mysqlPassword}`, |
| | | '--database=battery_detection', '--default-character-set=utf8mb4', '-e', sql]).stdout |
| | | } |
| | | |
| | | redisCommand(...args: string[]) { |
| | |
| | | if (!this.docker) return |
| | | this.dockerRun(['rm', '--force', this.mysqlContainer], true) |
| | | this.dockerRun(['rm', '--force', this.redisContainer], true) |
| | | // WSL 的端口转发可能晚于容器退出释放;确认本次端口已释放后才交给下一份验收。 |
| | | await waitUntil(async () => { |
| | | try { |
| | | await Promise.all([mysqlPort, redisPort, apiPort, simulatorPort].map(assertPortAvailable)) |
| | | return true |
| | | } catch { |
| | | return false |
| | | } |
| | | }, '本次隔离环境的端口未在清理后释放', 30_000) |
| | | } |
| | | |
| | | private dockerRun(args: string[], allowFailure = false) { |
| | |
| | | } |
| | | |
| | | private async waitForContainers() { |
| | | await waitUntil(() => { |
| | | const logs = this.dockerRun(['logs', this.mysqlContainer], true) |
| | | return `${logs.stdout}\n${logs.stderr}`.includes('MySQL init process done. Ready for start up.') |
| | | }, '隔离 MySQL 8 初始化未完成') |
| | | await waitUntil(() => this.dockerRun([ |
| | | 'exec', this.mysqlContainer, 'mysqladmin', 'ping', '-uroot', `-p${mysqlPassword}`, '--silent', |
| | | 'exec', this.mysqlContainer, 'mysql', '-uroot', `-p${mysqlPassword}`, |
| | | '--database=battery_detection', '--skip-column-names', '-e', 'SELECT 1', |
| | | ], true).status === 0, '隔离 MySQL 8 未就绪') |
| | | await waitUntil(() => this.dockerRun([ |
| | | 'exec', this.redisContainer, 'redis-cli', '-a', redisPassword, 'ping', |
| | |
| | | if (!existsSync(backendJar)) throw new Error(`未生成后端可执行包: ${backendJar}`) |
| | | } |
| | | |
| | | private async startBackend() { |
| | | private async startBackend(profiles = 'druid,dev-simulator') { |
| | | const logPath = resolve(artifactDir, 'backend.log') |
| | | const log = createWriteStream(logPath, { flags: 'a' }) |
| | | const jdbcUrl = `jdbc:mysql://127.0.0.1:${mysqlPort}/battery_detection?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8` |
| | | const jdbcUrl = `jdbc:mysql://127.0.0.1:${mysqlPort}/battery_detection?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=true` |
| | | this.backend = spawn('java', [ |
| | | '-jar', backendJar, |
| | | `--server.port=${apiPort}`, |
| | |
| | | `--spring.data.redis.port=${redisPort}`, |
| | | `--spring.data.redis.password=${redisPassword}`, |
| | | '--spring.data.redis.database=0', |
| | | `--spring.profiles.active=${profiles}`, |
| | | '--spring.devtools.restart.enabled=false', |
| | | `--battery.dev-simulator.port=${simulatorPort}`, |
| | | `--ruoyi.profile=${artifactDir}`, |
| | |
| | | await waitUntil(async () => { |
| | | if (this.backend?.exitCode !== null) throw new Error(`后端提前退出,详见 ${logPath}`) |
| | | try { |
| | | return (await fetch(`${this.backendUrl}/dev/bts-simulator/state`)).ok |
| | | return (await fetch(`${this.backendUrl}/captchaImage`)).ok |
| | | } catch { |
| | | return false |
| | | } |