用 bun fmt 格式化全部代码和文档
- 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:
1 parent
d4b8bf9241
commit
de277d3e9d
102 files changed
+1971
-1059
No files matched your search
+108
-20
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
@@ -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]))
|
||||
|
||||
Reference in new issue
Block a user