导出 EXE:实验页工具栏"导出 EXE"把学生的程序编译成一个 exe,拷到机房电脑上双击就能控制真的实训台。编译服务照教材工程编译学生写的原始代码(.NET Framework 4.5 引用程序集,构建时只复制教材工程引用的 13 个到 netfx/;真机版设备类库),不做平台的仿真改写和 API 限制,和 VS 里一样。教材 dll(DeviceCtrlLibrary 及 API 下 7 个、WinFormsDeviceControlLibrary、媒体播放器的两个 Interop)作为资源嵌进 exe,模块初始化器注册 AssemblyResolve 从 exe 里加载,Program.cs 不用改;Properties.Resources 的图片、声音嵌进 exe,代码里按文件名用到的素材(项目 7 的 ring.mp3)启动时放到 exe 旁边的 Resources;媒体播放器的 OcxState 用教材 resx 的默认状态;书上的 ledHelper.Close() 在教材类库里没有,补一个空的扩展方法。23 个实验都能导出(约 0.9 MB),mono + Xvfb 里实测能启动、控件和图片正常。接口 POST /api/export-exe,代码有错时 422,实验页弹框列出报错、点击跳到代码。教材 dll 有版权、GitHub 仓库公开,不进仓库:本机放 textbook-libs/,服务器放 ~/IOT_SIM/textbook-libs/ 由 compose 只读挂载(deploy/README.md),没放时提示原因。使用说明、设计方案、README 同步
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46e0f40cce
commit
5e5d29b2aa
19 files changed
+601
-10
No files matched your search
@@ -1,5 +1,6 @@
|
||||
// HTTP 接口测试:内存数据库,直接调用 app.fetch。
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { createAiQueue, normalizeAiResult } from '../src/ai-review'
|
||||
import { createApp } from '../src/app'
|
||||
@@ -738,3 +739,53 @@ describe('AI 评阅', () => {
|
||||
expect((await t.post(`/api/teacher/assignments/${assignmentId}/adopt-ai`)).body).toEqual({ adopted: 0 })
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
describe('导出 EXE', () => {
|
||||
const hasLibs = existsSync(`${config.textbookLibsDir}/DeviceCtrlLibrary.dll`)
|
||||
|
||||
async function exportExe(browser: Browser, json: unknown) {
|
||||
const response = await app.fetch(
|
||||
new Request('http://localhost/api/export-exe', {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${browser.accessToken}`, 'content-type': 'application/json', 'x-real-ip': browser.ip },
|
||||
body: JSON.stringify(json),
|
||||
}),
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
||||
test.skipIf(!hasLibs)(
|
||||
'照真机版类库编译成 exe;书上的 ledHelper.Close() 也能编译;代码有错时返回报错',
|
||||
async () => {
|
||||
const t = await teacher()
|
||||
const files = (await loadSample('p2-t3-LED推送模拟量'))!
|
||||
const response = await exportExe(t, { files, name: 'LED推送 模拟量/..' })
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-disposition')).toContain(`filename*=UTF-8''${encodeURIComponent('LED推送模拟量.exe')}`)
|
||||
const exe = new Uint8Array(await response.arrayBuffer())
|
||||
expect(String.fromCharCode(exe[0]!, exe[1]!)).toBe('MZ')
|
||||
// 教材 dll 嵌在 exe 里
|
||||
expect(Buffer.from(exe).includes('IotSimExport.lib.DeviceCtrlLibrary.dll')).toBe(true)
|
||||
|
||||
const broken = files.map((f) => (f.path === 'frm_LED.cs' ? { ...f, content: f.content.replace('IsOpen = true;', 'IsOpen = true') } : f))
|
||||
const failed = await exportExe(t, { files: broken, name: 'x' })
|
||||
expect(failed.status).toBe(422)
|
||||
expect(((await failed.json()) as any).diagnostics[0]).toMatchObject({ file: 'frm_LED.cs', severity: 'error' })
|
||||
},
|
||||
60000,
|
||||
)
|
||||
|
||||
test('服务器上没有教材类库时说明原因;格式不对拒绝', async () => {
|
||||
const t = await teacher()
|
||||
const saved = config.textbookLibsDir
|
||||
config.textbookLibsDir = '/nonexistent/textbook-libs'
|
||||
try {
|
||||
const response = await exportExe(t, { files: (await loadSample('p1-t1-登录'))!, name: '登录' })
|
||||
expect(response.status).toBe(503)
|
||||
expect(((await response.json()) as any).message).toContain('设备类库')
|
||||
} finally {
|
||||
config.textbookLibsDir = saved
|
||||
}
|
||||
expect((await exportExe(t, { files: 'x' })).status).toBe(400)
|
||||
}, 60000)
|
||||
})
|
||||
Reference in new issue
Block a user