Skip to content

Commit 31c0a9d

Browse files
committed
test: improve framework ide diagnostics
1 parent a932dcd commit 31c0a9d

9 files changed

Lines changed: 387 additions & 65 deletions

e2e/framework-ide-support.test.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import process from 'node:process'
22
import { execa } from 'execa'
33
import { describe, expect, it } from 'vitest'
4+
import { collectFrameworkIdeDiagnostics } from './frameworkIdeDiagnostics'
45
import { getFrameworkIdeCases, getFrameworkIdeExemptCases } from './frameworkSupportMatrix'
56

67
const describeFrameworkIde = process.env['E2E_IDE'] === '1' ? describe : describe.skip
@@ -17,18 +18,22 @@ function readNumberEnv(name: string, fallback: number) {
1718
}
1819

1920
function getProbeTiming() {
20-
const timeoutMs = readNumberEnv('E2E_AUTOMATOR_TIMEOUT_MS', 30_000)
21-
const closeTimeoutMs = readNumberEnv('E2E_IDE_CLOSE_TIMEOUT_MS', 10_000)
21+
const timeoutMs = readNumberEnv('E2E_AUTOMATOR_TIMEOUT_MS', 20_000)
22+
const closeTimeoutMs = readNumberEnv('E2E_IDE_CLOSE_TIMEOUT_MS', 5000)
2223
const hotUpdateTimeoutMs = process.env['E2E_IDE_HOT_UPDATE'] === '0'
2324
? 0
24-
: readNumberEnv('E2E_IDE_HOT_UPDATE_TIMEOUT_MS', readNumberEnv('E2E_WATCH_TIMEOUT_MS', 240_000))
25+
: readNumberEnv('E2E_IDE_HOT_UPDATE_TIMEOUT_MS', readNumberEnv('E2E_WATCH_TIMEOUT_MS', 120_000))
2526
const buildTimeoutMs = process.env['E2E_IDE_BUILD'] === '1'
26-
? readNumberEnv('E2E_IDE_BUILD_TIMEOUT_MS', 120_000)
27+
? readNumberEnv('E2E_IDE_BUILD_TIMEOUT_MS', 90_000)
2728
: 0
28-
const settleTimeoutMs = readNumberEnv('E2E_IDE_SETTLE_MS', 1500)
29-
const maxAttempts = readNumberEnv('E2E_IDE_PROBE_RETRIES', 2) + 1
30-
const attemptTimeoutMs = buildTimeoutMs + hotUpdateTimeoutMs + timeoutMs + closeTimeoutMs + 5000
31-
const testTimeoutMs = (attemptTimeoutMs + settleTimeoutMs) * maxAttempts
29+
const hotUpdateTotalTimeoutMs = process.env['E2E_IDE_HOT_UPDATE'] === '0'
30+
? 0
31+
: readNumberEnv('E2E_IDE_HOT_UPDATE_TOTAL_TIMEOUT_MS', hotUpdateTimeoutMs)
32+
const settleTimeoutMs = readNumberEnv('E2E_IDE_SETTLE_MS', 800)
33+
const maxAttempts = readNumberEnv('E2E_IDE_PROBE_RETRIES', 0) + 1
34+
const attemptTimeoutMs = buildTimeoutMs + hotUpdateTotalTimeoutMs + timeoutMs + closeTimeoutMs + 5000
35+
const parentGraceMs = readNumberEnv('E2E_IDE_PARENT_TIMEOUT_GRACE_MS', 15_000)
36+
const testTimeoutMs = (attemptTimeoutMs + settleTimeoutMs + parentGraceMs) * maxAttempts
3237

3338
return {
3439
attemptTimeoutMs,
@@ -70,18 +75,29 @@ function isTransientIdeError(error: unknown) {
7075
}
7176

7277
async function runFrameworkIdeProbe(entryName: string, timeoutMs: number, testTimeoutMs: number) {
73-
const result = await execa('node', ['--import', 'tsx', './e2e/frameworkIdeProbe.ts', entryName], {
74-
cwd: process.cwd(),
75-
env: {
76-
...process.env,
77-
E2E_IDE_PROBE_TIMEOUT_MS: String(timeoutMs),
78-
E2E_IDE_BUILD: process.env['E2E_IDE_BUILD'] ?? '0',
79-
},
80-
stdio: process.env['E2E_IDE_DEBUG'] === '1' ? 'inherit' : 'pipe',
81-
timeout: testTimeoutMs - 1000,
82-
killSignal: 'SIGKILL',
83-
forceKillAfterDelay: 1000,
84-
})
78+
let result
79+
try {
80+
result = await execa('node', ['--import', 'tsx', './e2e/frameworkIdeProbe.ts', entryName], {
81+
cwd: process.cwd(),
82+
env: {
83+
...process.env,
84+
E2E_IDE_PROBE_TIMEOUT_MS: String(timeoutMs),
85+
E2E_IDE_BUILD: process.env['E2E_IDE_BUILD'] ?? '0',
86+
},
87+
stdio: process.env['E2E_IDE_DEBUG'] === '1' ? 'inherit' : 'pipe',
88+
timeout: testTimeoutMs - 1000,
89+
killSignal: 'SIGKILL',
90+
forceKillAfterDelay: 1000,
91+
})
92+
}
93+
catch (error) {
94+
if (error instanceof Error && !('stderr' in error && typeof error.stderr === 'string' && error.stderr.includes('[e2e:ide] diagnostics'))) {
95+
const diagnostics = await collectFrameworkIdeDiagnostics(entryName)
96+
error.message = `${error.message}\n${diagnostics}`
97+
}
98+
throw error
99+
}
100+
85101
if (process.env['E2E_IDE_DEBUG'] !== '1') {
86102
const visibleLines = result.stdout
87103
?.split(/\r?\n/)
@@ -143,7 +159,7 @@ describeFrameworkIde.sequential('framework support matrix ide', () => {
143159
if (attempt >= maxAttempts || !isTransientIdeError(error)) {
144160
throw error
145161
}
146-
process.stderr.write(`[e2e:ide] retry ${entry.name} after transient DevTools error (${attempt}/${maxAttempts - 1})\n`)
162+
process.stderr.write(`[e2e:ide] retry ${entry.name} after transient DevTools error (${attempt}/${maxAttempts - 1})\n${await collectFrameworkIdeDiagnostics(entry.name)}\n`)
147163
}
148164
finally {
149165
await cleanupDevTools()

e2e/frameworkIdeClassHotUpdate.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ function readNumberEnv(name: string, fallback: number) {
3131
function getDevToolsReadTimeoutMs() {
3232
return readNumberEnv(
3333
'E2E_IDE_DEVTOOLS_READ_TIMEOUT_MS',
34-
Math.min(readNumberEnv('E2E_AUTOMATOR_TIMEOUT_MS', 30_000), 15_000),
34+
Math.min(readNumberEnv('E2E_AUTOMATOR_TIMEOUT_MS', 20_000), 5000),
3535
)
3636
}
3737

38+
function getDevToolsVisibleTimeoutMs(options: CliOptions) {
39+
return Math.min(options.timeoutMs, readNumberEnv('E2E_IDE_VISIBLE_TIMEOUT_MS', 30_000))
40+
}
41+
3842
async function withDevToolsReadTimeout<T>(pageUrl: string, task: Promise<T>) {
3943
let timer: ReturnType<typeof setTimeout> | undefined
4044
try {
@@ -105,6 +109,7 @@ export async function runIdeClassHotUpdate(
105109
) {
106110
const mutation = mutationKind === 'template' ? watchCase.templateMutation : watchCase.scriptMutation
107111
const sourceFile = mutation.sourceFile
112+
process.stdout.write(`[e2e:ide] ${watchCase.label} ${mutationKind} HMR mutate ${sourceFile}\n`)
108113
const { artifacts: baselineArtifacts, mtimes: baselineMtimes } = await collectArtifactMtimes(watchCase)
109114
const liveBefore = await readPageWxml(page, pageUrl).catch(() => undefined)
110115
const scenario = createClassMutationScenario(
@@ -166,6 +171,7 @@ export async function runIdeClassHotUpdate(
166171
await miniProgram.clearCache?.({ clean: 'compile' }).catch(() => undefined)
167172
await miniProgram.compile({ force: true }).catch(() => undefined)
168173
let devtoolsVisible = 'false'
174+
process.stdout.write(`[e2e:ide] ${watchCase.label} ${mutationKind} HMR verify DevTools visibility\n`)
169175
const liveHasMarker = await waitFor(
170176
async () => {
171177
try {
@@ -176,7 +182,7 @@ export async function runIdeClassHotUpdate(
176182
}
177183
},
178184
{
179-
timeoutMs: Math.min(options.timeoutMs, 60_000),
185+
timeoutMs: getDevToolsVisibleTimeoutMs(options),
180186
pollMs: options.pollMs,
181187
message: `[${watchCase.label}] DevTools page did not show IDE ${mutationKind} HMR marker: ${scenario.marker}`,
182188
onTick: session.ensureRunning,
@@ -192,6 +198,7 @@ export async function runIdeClassHotUpdate(
192198
devtoolsVisible = 'live'
193199
}
194200
else if (mutationKind === 'template') {
201+
process.stdout.write(`[e2e:ide] ${watchCase.label} template HMR reopen DevTools for visibility fallback\n`)
195202
const freshWxml = await readFreshDevToolsPageWxml(launchProjectPath, pageUrl)
196203
if (!freshWxml.includes(scenario.marker)) {
197204
throw new Error(`[${watchCase.label}] DevTools page did not show template HMR marker after reopening project: ${scenario.marker}`)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2+
import os from 'node:os'
3+
import path from 'node:path'
4+
import { afterEach, describe, expect, it } from 'vitest'
5+
import { collectFrameworkIdeDiagnostics } from './frameworkIdeDiagnostics'
6+
7+
const tempDirs: string[] = []
8+
9+
afterEach(async () => {
10+
delete process.env['E2E_IDE_DEVTOOLS_SUPPORT_DIR']
11+
delete process.env['E2E_IDE_DIAGNOSTIC_LOG_FILES']
12+
delete process.env['E2E_IDE_DIAGNOSTIC_LOG_LINES']
13+
delete process.env['E2E_IDE_DIAGNOSTIC_PROCESSES']
14+
delete process.env['E2E_IDE_DIAGNOSTIC_PROCESS_CHARS']
15+
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
16+
})
17+
18+
describe('framework IDE diagnostics', () => {
19+
it('collects recent WeChat DevTools log tails for failed IDE probes', async () => {
20+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'weapp-tw-ide-diagnostics-'))
21+
tempDirs.push(tempDir)
22+
const logDir = path.join(tempDir, 'profile-id', 'WeappLog', 'logs')
23+
await mkdir(logDir, { recursive: true })
24+
await writeFile(path.join(tempDir, 'profile-id', 'WeappLog', 'stderr.log'), 'stderr-a\nstderr-b\nstderr-c\n')
25+
await writeFile(path.join(logDir, 'devtools.log'), 'log-a\nlog-b\nlog-c\n')
26+
27+
process.env['E2E_IDE_DEVTOOLS_SUPPORT_DIR'] = tempDir
28+
process.env['E2E_IDE_DIAGNOSTIC_LOG_FILES'] = '2'
29+
process.env['E2E_IDE_DIAGNOSTIC_LOG_LINES'] = '2'
30+
process.env['E2E_IDE_DIAGNOSTIC_PROCESSES'] = '0'
31+
32+
const diagnostics = await collectFrameworkIdeDiagnostics('fixture-case')
33+
34+
expect(diagnostics).toContain('[e2e:ide] diagnostics for fixture-case')
35+
expect(diagnostics).toContain('[devtools log:')
36+
expect(diagnostics).toContain('stderr-b')
37+
expect(diagnostics).toContain('stderr-c')
38+
expect(diagnostics).toContain('log-b')
39+
expect(diagnostics).toContain('log-c')
40+
expect(diagnostics).not.toContain('stderr-a')
41+
expect(diagnostics).not.toContain('log-a')
42+
})
43+
})

e2e/frameworkIdeDiagnostics.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { readdir, readFile, stat } from 'node:fs/promises'
2+
import os from 'node:os'
3+
import process from 'node:process'
4+
import { execa } from 'execa'
5+
import path from 'pathe'
6+
7+
interface DevToolsLogFile {
8+
file: string
9+
mtimeMs: number
10+
}
11+
12+
const LOG_FILE_NAMES = new Set(['stderr.log', 'stdout.log', 'launch.log', 'report.log'])
13+
14+
function readNumberEnv(name: string, fallback: number) {
15+
const raw = process.env[name]
16+
if (!raw) {
17+
return fallback
18+
}
19+
const value = Number(raw)
20+
return Number.isFinite(value) ? value : fallback
21+
}
22+
23+
function getDevToolsSupportDir() {
24+
return process.env['E2E_IDE_DEVTOOLS_SUPPORT_DIR']
25+
?? path.join(os.homedir(), 'Library/Application Support/微信开发者工具')
26+
}
27+
28+
function getMaxLogFiles() {
29+
return Math.max(1, readNumberEnv('E2E_IDE_DIAGNOSTIC_LOG_FILES', 8))
30+
}
31+
32+
function getLogTailLines() {
33+
return Math.max(1, readNumberEnv('E2E_IDE_DIAGNOSTIC_LOG_LINES', 80))
34+
}
35+
36+
function getProcessLineMaxChars() {
37+
return Math.max(80, readNumberEnv('E2E_IDE_DIAGNOSTIC_PROCESS_CHARS', 240))
38+
}
39+
40+
function truncateLine(line: string, maxChars: number) {
41+
if (line.length <= maxChars) {
42+
return line
43+
}
44+
return `${line.slice(0, maxChars - 3)}...`
45+
}
46+
47+
async function pathExists(file: string) {
48+
try {
49+
await stat(file)
50+
return true
51+
}
52+
catch {
53+
return false
54+
}
55+
}
56+
57+
async function collectLogFilesFromDir(dir: string, depth: number, output: DevToolsLogFile[]) {
58+
if (depth < 0) {
59+
return
60+
}
61+
62+
let entries: string[]
63+
try {
64+
entries = await readdir(dir)
65+
}
66+
catch {
67+
return
68+
}
69+
70+
await Promise.all(entries.map(async (entry) => {
71+
const file = path.join(dir, entry)
72+
let stats
73+
try {
74+
stats = await stat(file)
75+
}
76+
catch {
77+
return
78+
}
79+
80+
if (stats.isDirectory()) {
81+
await collectLogFilesFromDir(file, depth - 1, output)
82+
return
83+
}
84+
85+
if (!stats.isFile()) {
86+
return
87+
}
88+
89+
if (LOG_FILE_NAMES.has(entry) || (entry.endsWith('.log') && file.includes(`${path.sep}WeappLog${path.sep}`))) {
90+
output.push({ file, mtimeMs: stats.mtimeMs })
91+
}
92+
}))
93+
}
94+
95+
async function collectRecentDevToolsLogFiles() {
96+
const supportDir = getDevToolsSupportDir()
97+
if (!await pathExists(supportDir)) {
98+
return []
99+
}
100+
101+
const files: DevToolsLogFile[] = []
102+
await collectLogFilesFromDir(supportDir, 4, files)
103+
return files
104+
.sort((left, right) => right.mtimeMs - left.mtimeMs)
105+
.slice(0, getMaxLogFiles())
106+
}
107+
108+
async function tailFile(file: string, lineCount: number) {
109+
try {
110+
const content = await readFile(file, 'utf8')
111+
return content.split(/\r?\n/).filter(line => line.length > 0).slice(-lineCount).join('\n').trim()
112+
}
113+
catch (error) {
114+
return `failed to read ${file}: ${error instanceof Error ? error.message : String(error)}`
115+
}
116+
}
117+
118+
async function collectDevToolsProcesses() {
119+
if (process.env['E2E_IDE_DIAGNOSTIC_PROCESSES'] === '0') {
120+
return ''
121+
}
122+
123+
try {
124+
const result = await execa('ps', ['-Ao', 'pid,ppid,stat,etime,command'], {
125+
timeout: 5000,
126+
})
127+
return result.stdout
128+
.split(/\r?\n/)
129+
.filter(line => /wechatwebdevtools||frameworkIdeProbe|pnpm e2e:ide|vitest run -c \.\/e2e\/vitest\.e2e\.config\.ts/.test(line))
130+
.map(line => truncateLine(line, getProcessLineMaxChars()))
131+
.join('\n')
132+
.trim()
133+
}
134+
catch (error) {
135+
return `failed to collect process list: ${error instanceof Error ? error.message : String(error)}`
136+
}
137+
}
138+
139+
export async function collectFrameworkIdeDiagnostics(caseName: string) {
140+
const sections = [`[e2e:ide] diagnostics for ${caseName}`]
141+
const processSummary = await collectDevToolsProcesses()
142+
if (processSummary) {
143+
sections.push(`\n[processes]\n${processSummary}`)
144+
}
145+
146+
const logFiles = await collectRecentDevToolsLogFiles()
147+
if (logFiles.length === 0) {
148+
sections.push(`\n[devtools logs]\nNo WeChat DevTools log files found under ${getDevToolsSupportDir()}`)
149+
}
150+
else {
151+
const tails = await Promise.all(logFiles.map(async ({ file, mtimeMs }) => {
152+
const relative = path.relative(getDevToolsSupportDir(), file)
153+
const content = await tailFile(file, getLogTailLines())
154+
return `\n[devtools log: ${relative} mtime=${new Date(mtimeMs).toISOString()}]\n${content || '(empty)'}`
155+
}))
156+
sections.push(...tails)
157+
}
158+
159+
return sections.join('\n')
160+
}

0 commit comments

Comments
 (0)