自动评分:评分规则和执行引擎
- 每个示例的 checks.yaml:书上评价表的功能项转成操作步骤和期望结果(10 个示例共 23 项) - 引擎:编译一次,每项在沙箱里单独启动一次程序,按控件名操作、调节实训台、核对结果; 提示框自动点第一个按钮;全站同时最多跑 2 项 - 不通过时写明期望和实际,如"期望6 秒内 txt_even 的文字包含"发现入侵",实际是"正常""; 找不到书上规定的控件名时直接指出 - 规则写错时指出第几项第几步 - 测试:参考答案全部通过;编译错误、控件改名、判断写反、忘停定时器、接错 DO 口各自报出对应原因 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5bb2940969
commit
37fc747b49
16 files changed
+824
No files matched your search
@@ -0,0 +1,171 @@
|
||||
// 评分时驱动一个学生程序:在沙箱里启动,像浏览器一样维护控件树,按控件名操作控件、调节实训台。
|
||||
// 提示框自动点第一个按钮("确定"/"是"),免得程序一直等在 MessageBox.Show 上。
|
||||
import type { Subprocess } from 'bun'
|
||||
import type { ControlNode, RunnerInput, RunnerMessage, UserAction } from '@iot/shared'
|
||||
import { Board } from '../board'
|
||||
import { readLines } from '../runner-process'
|
||||
import { runCommand } from '../sandbox'
|
||||
import type { Step } from './checks'
|
||||
|
||||
type BoardStep = Extract<Step, { kind: 'board' }>
|
||||
|
||||
export class ProgramDriver {
|
||||
readonly board = new Board()
|
||||
/** 当前显示的窗体,按显示先后排列 */
|
||||
readonly forms: ControlNode[] = []
|
||||
private readonly nodes = new Map<string, ControlNode>()
|
||||
private readonly parents = new Map<string, ControlNode>()
|
||||
readonly messageBoxes: { text: string; caption: string }[] = []
|
||||
readonly exceptions: string[] = []
|
||||
console = ''
|
||||
exited = false
|
||||
private proc?: Subprocess<'pipe', 'pipe', 'pipe'>
|
||||
private listeners = new Set<() => void>()
|
||||
private lastMessageAt = Date.now()
|
||||
|
||||
start(projectDir: string) {
|
||||
const { cmd, env } = runCommand(projectDir, { SIM_BOARD: JSON.stringify(this.board.runnerInputs()) })
|
||||
const proc = Bun.spawn(cmd, { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe', env })
|
||||
this.proc = proc
|
||||
void readLines(proc.stdout, (line) => {
|
||||
try {
|
||||
this.receive(JSON.parse(line) as RunnerMessage)
|
||||
} catch {
|
||||
this.console += `${line}\n`
|
||||
}
|
||||
})
|
||||
void new Response(proc.stderr).text()
|
||||
void proc.exited.then(() => {
|
||||
this.exited = true
|
||||
this.notify()
|
||||
})
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.proc?.kill()
|
||||
}
|
||||
|
||||
private send(message: RunnerInput) {
|
||||
if (this.exited || !this.proc) return
|
||||
this.proc.stdin.write(`${JSON.stringify(message)}\n`)
|
||||
this.proc.stdin.flush()
|
||||
}
|
||||
|
||||
private register(node: ControlNode, parent?: ControlNode) {
|
||||
this.nodes.set(node.id, node)
|
||||
if (parent) this.parents.set(node.id, parent)
|
||||
for (const child of node.children) this.register(child, node)
|
||||
}
|
||||
|
||||
private unregister(node: ControlNode) {
|
||||
this.nodes.delete(node.id)
|
||||
this.parents.delete(node.id)
|
||||
for (const child of node.children) this.unregister(child)
|
||||
}
|
||||
|
||||
private receive(message: RunnerMessage) {
|
||||
this.lastMessageAt = Date.now()
|
||||
switch (message.type) {
|
||||
case 'formShown':
|
||||
this.forms.push(message.form)
|
||||
this.register(message.form)
|
||||
break
|
||||
case 'formClosed': {
|
||||
const index = this.forms.findIndex((f) => f.id === message.formId)
|
||||
if (index >= 0) this.unregister(this.forms.splice(index, 1)[0])
|
||||
break
|
||||
}
|
||||
case 'patch':
|
||||
for (const change of message.changes) {
|
||||
const node = this.nodes.get(change.id)
|
||||
if (node) node.props[change.prop] = change.value
|
||||
}
|
||||
break
|
||||
case 'controlAdded': {
|
||||
const parent = this.nodes.get(message.parentId)
|
||||
if (!parent) break
|
||||
parent.children.splice(message.index, 0, message.node)
|
||||
this.register(message.node, parent)
|
||||
break
|
||||
}
|
||||
case 'controlRemoved': {
|
||||
const node = this.nodes.get(message.id)
|
||||
const parent = this.parents.get(message.id)
|
||||
if (!node) break
|
||||
if (parent) parent.children = parent.children.filter((c) => c.id !== node.id)
|
||||
this.unregister(node)
|
||||
break
|
||||
}
|
||||
case 'messageBox': {
|
||||
this.messageBoxes.push({ text: message.text, caption: message.caption })
|
||||
// 自动点第一个按钮
|
||||
const first = /^(OK|Yes|Retry|Abort)/.exec(message.buttons)?.[1] ?? 'OK'
|
||||
this.send({ type: 'reply', requestId: message.requestId, result: first })
|
||||
break
|
||||
}
|
||||
case 'device':
|
||||
this.board.applyDevice(message)
|
||||
break
|
||||
case 'console':
|
||||
this.console += message.text
|
||||
break
|
||||
case 'exception':
|
||||
this.exceptions.push(message.message)
|
||||
break
|
||||
}
|
||||
this.notify()
|
||||
}
|
||||
|
||||
private notify() {
|
||||
for (const listener of this.listeners) listener()
|
||||
}
|
||||
|
||||
/** 按控件名找控件;多个窗体里有同名控件时取最后显示的窗体里的 */
|
||||
find(name: string): ControlNode | undefined {
|
||||
for (const form of [...this.forms].reverse()) {
|
||||
const found = findIn(form, name)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
|
||||
act(node: ControlNode, action: UserAction) {
|
||||
this.send({ type: 'action', id: node.id, ...action })
|
||||
}
|
||||
|
||||
setBoard(step: BoardStep) {
|
||||
this.board.applyInput({ type: 'boardInput', analog: step.analog, digital: step.digital })
|
||||
this.send(this.board.runnerInputs())
|
||||
}
|
||||
|
||||
/** 等到条件成立(立即检查,之后每收到一条消息检查一次),超时返回 false */
|
||||
waitFor(condition: () => boolean, timeoutMs: number): Promise<boolean> {
|
||||
if (condition()) return Promise.resolve(true)
|
||||
return new Promise((resolve) => {
|
||||
const done = (result: boolean) => {
|
||||
clearTimeout(timer)
|
||||
this.listeners.delete(check)
|
||||
resolve(result)
|
||||
}
|
||||
const check = () => {
|
||||
if (condition()) done(true)
|
||||
}
|
||||
const timer = setTimeout(() => done(condition()), timeoutMs)
|
||||
this.listeners.add(check)
|
||||
})
|
||||
}
|
||||
|
||||
/** 等程序处理完刚才的操作:从现在起一段时间内没有新消息(最多等 maxMs) */
|
||||
async settle(quietMs = 150, maxMs = 2000) {
|
||||
const start = Date.now()
|
||||
const quietFor = () => Date.now() - Math.max(this.lastMessageAt, start)
|
||||
while (Date.now() - start < maxMs && quietFor() < quietMs) await Bun.sleep(quietMs - quietFor())
|
||||
}
|
||||
}
|
||||
|
||||
function findIn(node: ControlNode, name: string): ControlNode | undefined {
|
||||
if (node.name === name) return node
|
||||
for (const child of node.children) {
|
||||
const found = findIn(child, name)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user