用 bun fmt 格式化全部代码和文档
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- bunfig.toml 的 [fmt]:不写分号、单引号、一行最长 160,和原来的写法一致
- .prettierignore:docs/教材摘要/ 是书上的摘录,不格式化
- 模板里一行写两条语句的事件处理(@keydown.delete、@dblclick)改成调用方法:去掉分号后拆成两行,Vue 编译不过

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 14:02:33 +08:00
1 parent d4b8bf9241
commit de277d3e9d
102 files changed
+1971 -1059

No files matched your search

+108 -20
View File
@@ -33,9 +33,7 @@ class Browser {
const headers: Record<string, string> = { 'x-real-ip': this.ip }
if (this.accessToken) headers.authorization = `Bearer ${this.accessToken}`
if (json !== undefined) headers['content-type'] = 'application/json'
const response = await app.fetch(
new Request(`http://localhost${path}`, { method, headers, body: json === undefined ? undefined : JSON.stringify(json) }),
)
const response = await app.fetch(new Request(`http://localhost${path}`, { method, headers, body: json === undefined ? undefined : JSON.stringify(json) }))
const body = (await response.json()) as any
if (body?.accessToken) {
this.accessToken = body.accessToken
@@ -418,7 +416,13 @@ describe('作业', () => {
describe('评阅与成绩', () => {
const SAMPLE = 'p4-t3-报警灯'
const scoring = { auto: [10, 10], manual: [{ title: '窗体界面', points: 10 }, { title: '注释', points: 5 }] }
const scoring = {
auto: [10, 10],
manual: [
{ title: '窗体界面', points: 10 },
{ title: '注释', points: 5 },
],
}
async function submitted() {
const t = await teacher()
@@ -427,7 +431,15 @@ describe('评阅与成绩', () => {
{ username: 's2', name: '乙', password: 'p2' },
])
const files = (await loadSample(SAMPLE))!
const { body } = await t.post('/api/teacher/assignments', { classId, sampleId: SAMPLE, title: '报警灯', startMode: 'ui', template: files, scoring, deadline: null })
const { body } = await t.post('/api/teacher/assignments', {
classId,
sampleId: SAMPLE,
title: '报警灯',
startMode: 'ui',
template: files,
scoring,
deadline: null,
})
const student = new Browser()
await student.login('s1', 'p1')
await student.post(`/api/assignments/${body.id}/submit`, { files })
@@ -452,7 +464,11 @@ describe('评阅与成绩', () => {
expect((await student.get(`/api/assignments/${assignmentId}`)).body.review).toMatchObject({ scores: [8, null], comment: '界面整齐,注释还要补' })
expect((await student.get('/api/assignments')).body[0].manualScore).toBe(8)
expect((await t.get(`/api/teacher/assignments/${assignmentId}/submissions`)).body[0]).toMatchObject({ username: 's1', manualScore: 8, review: { comment: '界面整齐,注释还要补' } })
expect((await t.get(`/api/teacher/assignments/${assignmentId}/submissions`)).body[0]).toMatchObject({
username: 's1',
manualScore: 8,
review: { comment: '界面整齐,注释还要补' },
})
}, 60000)
test('成绩表:总分 = 自动评分 + 老师评分;没提交的没有总分', async () => {
@@ -469,7 +485,11 @@ describe('评阅与成绩', () => {
test('老师减少评分项后,已打的分按新的评分项对齐', async () => {
const { t, assignmentId, userId } = await submitted()
await t.put(`/api/teacher/assignments/${assignmentId}/students/${userId}/review`, { scores: [8, 4], comment: '' })
await t.patch(`/api/teacher/assignments/${assignmentId}`, { title: '报警灯', deadline: null, scoring: { auto: [10, 10], manual: [{ title: '窗体界面', points: 5 }] } })
await t.patch(`/api/teacher/assignments/${assignmentId}`, {
title: '报警灯',
deadline: null,
scoring: { auto: [10, 10], manual: [{ title: '窗体界面', points: 5 }] },
})
expect((await t.get(`/api/teacher/assignments/${assignmentId}/students/${userId}`)).body.review.scores).toEqual([5])
}, 60000)
@@ -487,7 +507,12 @@ describe('语言服务', () => {
test('代码补全、边写边检查(中文提示);请求格式不对拒绝;没登录不能用', async () => {
const t = await teacher()
const files = [{ path: 'Program.cs', content: 'using System.Windows.Forms;\nstatic class P\n{\n static void Main()\n {\n var b = new Button();\n b.\n }\n}\n' }]
const files = [
{
path: 'Program.cs',
content: 'using System.Windows.Forms;\nstatic class P\n{\n static void Main()\n {\n var b = new Button();\n b.\n }\n}\n',
},
]
const completions = await t.post('/api/language', { type: 'complete', session: 'tab1', files, file: 'Program.cs', line: 7, column: 11 })
expect(completions.body.items.map((i: any) => i.label)).toEqual(expect.arrayContaining(['Text', 'Click', 'Enabled']))
@@ -500,7 +525,13 @@ describe('语言服务', () => {
test('平台支持的旧版线程 API(Suspend / Resume / Abort)不提示"已过时"', async () => {
const t = await teacher()
const files = [{ path: 'Program.cs', content: 'using System.Threading;\nstatic class P\n{\n static void Main()\n {\n var t = new Thread(() => Thread.Sleep(10));\n t.Start();\n t.Suspend();\n t.Resume();\n t.Abort();\n }\n}\n' }]
const files = [
{
path: 'Program.cs',
content:
'using System.Threading;\nstatic class P\n{\n static void Main()\n {\n var t = new Thread(() => Thread.Sleep(10));\n t.Start();\n t.Suspend();\n t.Resume();\n t.Abort();\n }\n}\n',
},
]
const check = await t.post('/api/language', { type: 'diagnostics', session: 'tab2', files })
expect(check.body.diagnostics).toEqual([])
}, 60000)
@@ -529,16 +560,40 @@ describe('数据库页面', () => {
database: '测试库',
sql: "CREATE TABLE T (ID int IDENTITY(1,1) PRIMARY KEY, name nvarchar(20)); INSERT INTO T (name) VALUES (N'张三'), (N'李四'); SELECT * FROM T WHERE name LIKE N'张%'",
})
expect(created.body).toMatchObject({ type: 'result', database: '测试库', affected: 2, results: [{ columns: [{ name: 'ID', type: 'int' }, { name: 'name', type: 'nvarchar(20)' }], rows: [[1, '张三']], total: 1 }] })
expect(created.body).toMatchObject({
type: 'result',
database: '测试库',
affected: 2,
results: [
{
columns: [
{ name: 'ID', type: 'int' },
{ name: 'name', type: 'nvarchar(20)' },
],
rows: [[1, '张三']],
total: 1,
},
],
})
const error = await t.post('/api/db/query', { database: '测试库', sql: 'SELECT * FROM 不存在的表' })
expect(error.body).toEqual({ type: 'error', number: 208, message: "对象名 '不存在的表' 无效。", line: 1 })
const explorer = (await t.get('/api/db')).body
expect(explorer.databases).toEqual([{ name: '测试库', tables: [{ name: 'T', columns: [
{ name: 'ID', type: 'int', nullable: false, identity: true, primaryKey: true },
{ name: 'name', type: 'nvarchar(20)', nullable: true, identity: false, primaryKey: false },
] }] }])
expect(explorer.databases).toEqual([
{
name: '测试库',
tables: [
{
name: 'T',
columns: [
{ name: 'ID', type: 'int', nullable: false, identity: true, primaryKey: true },
{ name: 'name', type: 'nvarchar(20)', nullable: true, identity: false, primaryKey: false },
],
},
],
},
])
expect((await student.get('/api/db')).body.databases).toEqual([])
expect((await new Browser().get('/api/db')).status).toBe(401)
}, 60000)
@@ -573,7 +628,13 @@ describe('数据库页面', () => {
describe('AI 评阅', () => {
const SAMPLE = 'p4-t3-报警灯'
const scoring = { auto: [10, 10], manual: [{ title: '窗体界面', points: 10 }, { title: '注释规范', points: 5 }] }
const scoring = {
auto: [10, 10],
manual: [
{ title: '窗体界面', points: 10 },
{ title: '注释规范', points: 5 },
],
}
/** 假的模型服务:记下收到的请求,返回固定的评分(第二项故意超出满分,检查服务器会限制) */
const requests: any[] = []
let fake: ReturnType<typeof Bun.serve>
@@ -584,7 +645,13 @@ describe('AI 评阅', () => {
port: 0,
async fetch(req) {
requests.push(await req.json())
const content = JSON.stringify({ scores: [{ item: '窗体界面', score: 8.6, reason: '布局整齐' }, { item: '注释规范', score: 9, reason: '注释充分' }], comment: '界面整齐,继续加油。' })
const content = JSON.stringify({
scores: [
{ item: '窗体界面', score: 8.6, reason: '布局整齐' },
{ item: '注释规范', score: 9, reason: '注释充分' },
],
comment: '界面整齐,继续加油。',
})
return Response.json({ choices: [{ message: { content } }] })
},
})
@@ -599,9 +666,15 @@ describe('AI 评阅', () => {
})
test('分数限制在 0 到满分并取整;评分项个数不对时报错', () => {
const manual = [{ title: 'A', points: 10 }, { title: 'B', points: 5 }]
const manual = [
{ title: 'A', points: 10 },
{ title: 'B', points: 5 },
]
expect(normalizeAiResult({ scores: [{ score: 8.6, reason: 'x' }, { score: -1 }], comment: ' 好 ' }, manual)).toEqual({
scores: [{ score: 9, reason: 'x' }, { score: 0, reason: '' }],
scores: [
{ score: 9, reason: 'x' },
{ score: 0, reason: '' },
],
comment: '好',
})
expect(() => normalizeAiResult({ scores: [{ score: 1 }] }, manual)).toThrow('评分项个数不对')
@@ -611,7 +684,15 @@ describe('AI 评阅', () => {
const t = await teacher()
const classId = await classWithStudents(t, [{ username: 's1', name: '甲同学', password: 'p1' }])
const files = (await loadSample(SAMPLE))!
const { body } = await t.post('/api/teacher/assignments', { classId, sampleId: SAMPLE, title: '报警灯', startMode: 'ui', template: files, scoring, deadline: null })
const { body } = await t.post('/api/teacher/assignments', {
classId,
sampleId: SAMPLE,
title: '报警灯',
startMode: 'ui',
template: files,
scoring,
deadline: null,
})
const assignmentId = body.id as number
const student = new Browser()
await student.login('s1', 'p1')
@@ -622,7 +703,14 @@ describe('AI 评阅', () => {
const userId = (await student.get('/api/me')).body.user.id
const detail = (await t.get(`/api/teacher/assignments/${assignmentId}/students/${userId}`)).body
expect(detail.aiReview).toMatchObject({ status: 'done', scores: [{ score: 9, reason: '布局整齐' }, { score: 5, reason: '注释充分' }], comment: '界面整齐,继续加油。' })
expect(detail.aiReview).toMatchObject({
status: 'done',
scores: [
{ score: 9, reason: '布局整齐' },
{ score: 5, reason: '注释充分' },
],
comment: '界面整齐,继续加油。',
})
expect(detail.screenshots).toEqual([{ form: 'Form1', png }])
expect((await t.get(`/api/teacher/assignments/${assignmentId}/submissions`)).body[0]).toMatchObject({ aiStatus: 'done', aiManual: 14, review: null })
expect((await t.get(`/api/teacher/classes/${classId}/gradebook`)).body.students[0].scores[assignmentId]).toMatchObject({ manual: null, aiManual: 14 })
+17 -14
View File
@@ -195,9 +195,7 @@ try {
await s.client.request('PUT', `/api/works/${encodeURIComponent(`assignment:${assignment.id}`)}`, { files: s.files })
})
await phase('边写边检查', students, (s) =>
s.client.request('POST', '/api/language', { type: 'diagnostics', session: 'tab', files: s.files }),
)
await phase('边写边检查', students, (s) => s.client.request('POST', '/api/language', { type: 'diagnostics', session: 'tab', files: s.files }))
// 老师说"运行看看":全班 SPREAD 内先后按 F5
await phase('运行到窗体出现', students, async (s) => {
@@ -209,17 +207,22 @@ try {
window.form = ((await shown) as Extract<ServerMessage, { type: 'formShown' }>).form
})
if (!process.env.SAMPLE) await phase('点按钮到实训台', students.filter((s) => s.window?.form), async (s) => {
const window = s.window!
window.action('com_lamp', { action: 'dropDown' })
window.action('com_lamp', { action: 'select', index: 3 })
window.action('btn_open', { action: 'click' })
await Bun.sleep(300)
// 设备消息由服务器记进实训台状态,再以 board 消息推给浏览器
const lit = window.waitFor((m) => m.type === 'board' && m.state.zigbee[1] === true, 30_000)
window.action('btn_lampOpen', { action: 'click' })
await lit
})
if (!process.env.SAMPLE)
await phase(
'点按钮到实训台',
students.filter((s) => s.window?.form),
async (s) => {
const window = s.window!
window.action('com_lamp', { action: 'dropDown' })
window.action('com_lamp', { action: 'select', index: 3 })
window.action('btn_open', { action: 'click' })
await Bun.sleep(300)
// 设备消息由服务器记进实训台状态,再以 board 消息推给浏览器
const lit = window.waitFor((m) => m.type === 'board' && m.state.zigbee[1] === true, 30_000)
window.action('btn_lampOpen', { action: 'click' })
await lit
},
)
// 程序都还开着,再自己检查一遍(评分进程和正在运行的程序抢 CPU)
await phase('自己检查', students, async (s) => {
+29 -9
View File
@@ -37,11 +37,15 @@ describe('参考答案全部通过', async () => {
const ids = (await Array.fromAsync(new Bun.Glob('*/checks.yaml').scan({ cwd: SAMPLES }))).map((p) => p.split('/')[0]).sort()
test('每个示例都有评分规则', () => expect(ids.length).toBe(23))
for (const id of ids) {
test(id, async () => {
const result = await grade(await prepare(id, id), await checksOf(id), id)
expect(result.compiled).toBe(true)
expect(result.results.filter((r) => !r.passed)).toEqual([])
}, 60000)
test(
id,
async () => {
const result = await grade(await prepare(id, id), await checksOf(id), id)
expect(result.compiled).toBe(true)
expect(result.results.filter((r) => !r.passed)).toEqual([])
},
60000,
)
}
})
@@ -66,7 +70,11 @@ describe('改坏的代码', () => {
test('判断写反:入侵时显示"正常"', async () => {
const id = 'p4-t1-红外对射'
const dir = await prepare(id, 'reversed', { file: 'Form1.cs', from: 'if (DeviceCtrlHelper.adam4150Helper.SetData().infraredValue)', to: 'if (!DeviceCtrlHelper.adam4150Helper.SetData().infraredValue)' })
const dir = await prepare(id, 'reversed', {
file: 'Form1.cs',
from: 'if (DeviceCtrlHelper.adam4150Helper.SetData().infraredValue)',
to: 'if (!DeviceCtrlHelper.adam4150Helper.SetData().infraredValue)',
})
const result = await grade(dir, await checksOf(id), id)
const realtime = result.results.find((r) => r.title === '能实时显示入侵数据')!
expect(realtime.passed).toBe(false)
@@ -94,7 +102,11 @@ describe('改坏的代码', () => {
test('数据库:照书上 INSERT 不写列名,温度和光照存反了', async () => {
const id = 'p6-t2-存储环境数据'
const dir = await prepare(id, 'insert-order', { file: 'Form1.cs', from: 'insert into CampusEnvironment (temperature, humidity, light, addtime) values', to: 'insert into CampusEnvironment values' })
const dir = await prepare(id, 'insert-order', {
file: 'Form1.cs',
from: 'insert into CampusEnvironment (temperature, humidity, light, addtime) values',
to: 'insert into CampusEnvironment values',
})
const result = await grade(dir, await checksOf(id), id)
const save = result.results.find((r) => r.title === '能把数据保存到数据库,并显示在表格中')!
expect(save.passed).toBe(false)
@@ -157,7 +169,11 @@ describe('改坏的代码', () => {
test('管理平台:开灯时换错了背景图片', async () => {
const id = 'p8-t3-环境采集与分析'
const dir = await prepare(id, 'lamp-image', { file: 'Environment.cs', from: 'panel_bg.BackgroundImage = Properties.Resources.day_01;', to: 'panel_bg.BackgroundImage = Properties.Resources.day_02;' })
const dir = await prepare(id, 'lamp-image', {
file: 'Environment.cs',
from: 'panel_bg.BackgroundImage = Properties.Resources.day_01;',
to: 'panel_bg.BackgroundImage = Properties.Resources.day_02;',
})
const result = await grade(dir, await checksOf(id), id)
const lamp = result.results.find((r) => r.title === '路灯能手动控制')!
expect(lamp).toMatchObject({ passed: false, message: '期望 panel_bg 背景换成亮灯的场景(day_01),实际是别的图片' })
@@ -165,7 +181,11 @@ describe('改坏的代码', () => {
test('报警灯接错了 DO 口', async () => {
const id = 'p4-t3-报警灯'
const dir = await prepare(id, 'relay', { file: 'Form1.cs', from: 'DeviceCtrlHelper.adam4150Helper.OnOff(0, IsOpen);', to: 'DeviceCtrlHelper.adam4150Helper.OnOff(1, IsOpen);' })
const dir = await prepare(id, 'relay', {
file: 'Form1.cs',
from: 'DeviceCtrlHelper.adam4150Helper.OnOff(0, IsOpen);',
to: 'DeviceCtrlHelper.adam4150Helper.OnOff(1, IsOpen);',
})
const result = await grade(dir, await checksOf(id), id)
expect(result.results[0]).toMatchObject({ passed: false, message: '期望实训台的报警灯(ADAM-4150 DO0)打开,实际是关闭的' })
}, 60000)
+70 -43
View File
@@ -176,7 +176,13 @@ test('编译错误:书上常见的 True 大写给出中文提示', async () =>
const sample = resolve(ROOT, 'samples/p2-t1-LED显示屏')
for (const name of ['Program.cs', 'Form1.Designer.cs']) await Bun.write(`${dir}/${name}`, Bun.file(`${sample}/${name}`))
const form1 = await Bun.file(`${sample}/Form1.cs`).text()
await Bun.write(`${dir}/Form1.cs`, form1.replace('private void btn_Open_Click(object sender, EventArgs e)\n {', 'private void btn_Open_Click(object sender, EventArgs e)\n {\n bool IsOpen = True;'))
await Bun.write(
`${dir}/Form1.cs`,
form1.replace(
'private void btn_Open_Click(object sender, EventArgs e)\n {',
'private void btn_Open_Click(object sender, EventArgs e)\n {\n bool IsOpen = True;',
),
)
runner = await Runner.start(dir)
const compiled = await runner.waitFor('compiled')
@@ -193,9 +199,9 @@ test('项目2 任务2:温度读数来自实训台,滑块变化后再次获
runner.send({ type: 'action', id: port, action: 'select', index: 1 })
runner.send({ type: 'action', id: id('btn_Open'), action: 'click' })
const textOf = (name: string) =>
runner.next('patch', (m) => m.changes.some((c) => c.id === id(name) && c.prop === 'Text')).then(
(m) => m.changes.findLast((c) => c.id === id(name) && c.prop === 'Text')!.value,
)
runner
.next('patch', (m) => m.changes.some((c) => c.id === id(name) && c.prop === 'Text'))
.then((m) => m.changes.findLast((c) => c.id === id(name) && c.prop === 'Text')!.value)
let temperature = textOf('txt_temperature')
runner.send({ type: 'action', id: id('btn_get'), action: 'click' })
@@ -279,19 +285,23 @@ test('项目1 任务1:多窗体——打开增加用户窗体、添加用户
describe('所有示例都能编译通过', async () => {
const ids = (await Array.fromAsync(new Bun.Glob('*/meta.json').scan({ cwd: resolve(ROOT, 'samples') }))).map((p) => p.split('/')[0]).sort()
for (const id of ids) {
test(id, async () => {
const result = await compileService.compile(await prepareSample(id))
expect(result.diagnostics.filter((d) => d.severity === 'error')).toEqual([])
}, 30000)
test(
id,
async () => {
const result = await compileService.compile(await prepareSample(id))
expect(result.diagnostics.filter((d) => d.severity === 'error')).toEqual([])
},
30000,
)
}
})
test('项目1 任务2:计算器(装箱拆箱)和多态', async () => {
const { runner, id } = await startSample('p1-t2-面向对象特性')
const textOf = (name: string) =>
runner.next('patch', (m) => m.changes.some((c) => c.id === id(name) && c.prop === 'Text')).then(
(m) => m.changes.findLast((c) => c.id === id(name) && c.prop === 'Text')!.value,
)
runner
.next('patch', (m) => m.changes.some((c) => c.id === id(name) && c.prop === 'Text'))
.then((m) => m.changes.findLast((c) => c.id === id(name) && c.prop === 'Text')!.value)
runner.send({ type: 'action', id: id('txtNum1'), action: 'text', value: '7' })
runner.send({ type: 'action', id: id('txtNum2'), action: 'text', value: '2' })
runner.send({ type: 'action', id: id('cmboxType'), action: 'select', index: 3 })
@@ -483,8 +493,17 @@ test('旧版线程 API:Suspend / Resume / Abort、线程自己挂起、休眠
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
expect(runner.messages.filter((m) => m.type === 'exception')).toEqual([])
for (const line of [
'state0=Unstarted', 'state1=Suspended', 'frozen=True', 'abortSuspended=error', 'joined=True,Stopped',
'selfState=Suspended', 'self=resumed', 'resumeRunning=error', 'sleeperJoined=True', 'unstarted=Unstarted, AbortRequested', 'param=42',
'state0=Unstarted',
'state1=Suspended',
'frozen=True',
'abortSuspended=error',
'joined=True,Stopped',
'selfState=Suspended',
'self=resumed',
'resumeRunning=error',
'sleeperJoined=True',
'unstarted=Unstarted, AbortRequested',
'param=42',
]) {
expect(output).toContain(line)
}
@@ -677,15 +696,17 @@ test('TrackBar:拖动时先 ValueChanged 再 Scroll,超出范围的值截到
runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '99' })
await runner.waitFor('console', (m) => m.text === 'scroll=32\n')
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
expect(output).toContain("error=“40”的值对于“Value”无效。")
expect(output).toContain('error=“40”的值对于“Value”无效。')
expect(output).toContain('changed=26\nscroll=26\nchanged=32\nscroll=32\n')
}, 30000)
test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别的运行目录、环境变量和其他进程,工程目录只读', async () => {
process.env.SIM_TEST_SECRET = '不该被看到'
const { runner } = await runMain(
'sandbox',
`
test.skipIf(config.sandbox === 'none')(
'沙箱:看不到服务器文件、别的运行目录、环境变量和其他进程,工程目录只读',
async () => {
process.env.SIM_TEST_SECRET = '不该被看到'
const { runner } = await runMain(
'sandbox',
`
Console.WriteLine("passwd=" + File.Exists("/etc/passwd"));
Console.WriteLine("runs=" + Directory.Exists(${JSON.stringify(config.runsDir)}));
Console.WriteLine("home=" + Directory.Exists("/home"));
@@ -695,19 +716,21 @@ test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别
try { File.WriteAllText("/sim/app/hack.txt", "x"); Console.WriteLine("write=ok"); } catch (Exception) { Console.WriteLine("write=denied"); }
File.WriteAllText("/tmp/ok.txt", "x");
Console.WriteLine("tmp=" + File.Exists("/tmp/ok.txt"));`,
)
expect((await runner.waitFor('compiled')).ok).toBe(true)
await runner.finished()
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
expect(output).toContain('passwd=False')
expect(output).toContain('runs=False')
expect(output).toContain('home=False')
expect(output).toContain('secret=null')
// 只能看到 bwrap 和自己
expect(Number(/procs=(\d+)/.exec(output)?.[1])).toBeLessThanOrEqual(2)
expect(output).toContain('write=denied')
expect(output).toContain('tmp=True')
}, 30000)
)
expect((await runner.waitFor('compiled')).ok).toBe(true)
await runner.finished()
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
expect(output).toContain('passwd=False')
expect(output).toContain('runs=False')
expect(output).toContain('home=False')
expect(output).toContain('secret=null')
// 只能看到 bwrap 和自己
expect(Number(/procs=(\d+)/.exec(output)?.[1])).toBeLessThanOrEqual(2)
expect(output).toContain('write=denied')
expect(output).toContain('tmp=True')
},
30000,
)
test('内存上限:不停分配内存的程序得到 OutOfMemoryException,而不是吃光服务器内存', async () => {
const { runner } = await runMain(
@@ -768,14 +791,18 @@ using System.Runtime.InteropServices;
expect(result.diagnostics.filter((d) => d.file.includes('AssemblyInfo'))).toEqual([])
}, 30000)
test.skipIf(config.sandbox === 'none')('CPU 时间上限:死循环的程序到时被结束,并提示检查死循环', async () => {
const limit = config.cpuLimitSeconds
config.cpuLimitSeconds = 2
try {
const { runner } = await runMain('cpu', ' long i = 0;\n while (true) i++;')
const { code, signal } = await runner.finished()
expect(cpuLimitMessage(code, signal)).toContain('死循环')
} finally {
config.cpuLimitSeconds = limit
}
}, 30000)
test.skipIf(config.sandbox === 'none')(
'CPU 时间上限:死循环的程序到时被结束,并提示检查死循环',
async () => {
const limit = config.cpuLimitSeconds
config.cpuLimitSeconds = 2
try {
const { runner } = await runMain('cpu', ' long i = 0;\n while (true) i++;')
const { code, signal } = await runner.finished()
expect(cpuLimitMessage(code, signal)).toContain('死循环')
} finally {
config.cpuLimitSeconds = limit
}
},
30000,
)
+4 -1
View File
@@ -13,7 +13,10 @@ function tableNames(task: string): string[] {
previous = []
continue
}
const cells = line.trim().replace(/^\||\|$/g, '').split('|')
const cells = line
.trim()
.replace(/^\||\|$/g, '')
.split('|')
// 表头是分隔行(|---|)上面那一行;数据行里出现"名称"两个字不算表头
if (cells.every((c) => /^\s*:?-+:?\s*$/.test(c))) column = previous.findIndex((c) => c.includes('名称'))
else if (column >= 0) names.push(...[...(cells[column] ?? '').matchAll(/`([^`]+)`/g)].map((m) => m[1]))