导出 VS 工程:实验页工具栏的"导出 EXE"改成下拉按钮"导出 ▾",里面是"导出 EXE"和"导出 VS 工程"。导出的 zip 是教材那样的 VS 2012 窗体应用工程(.sln + .csproj,.NET Framework 4.5),在机房电脑上解压、双击 .sln 用 VS 打开接着改,按 F5 运行。导出前照机房环境编译一遍要放进 zip 的代码:C# 5(VS 2012 的编译器)、真机版设备类库,有错不导出;新版 C# 写法报 CS8026,按功能名给出改法(字符串插值改 string.Format、?. 改先判断 null、=> 改大括号等 12 种)。导出 EXE 仍按最新 C#。平台的 Properties/Resources.Designer.cs 换成 VS 生成的写法和 Resources.resx,图片放 Resources\;代码里按文件名用的素材(ring.mp3)复制到输出目录,运行时在 bin\Debug\Resources\;每个窗体带 .resx(媒体播放器 OcxState 用教材默认状态),SubType、DependentUpon 和 VS 一样;补上 AssemblyInfo、Settings、App.config。教材 dll 全放 lib\,只引用代码直接用到的(和教材工程一样),只带这些编译不过时引用全部;用到书上的 ledHelper.Close() 时带上 LedHelperCompat.cs。文本文件用带 BOM 的 UTF-8 和 CRLF(VS 2012 打开不带 BOM 的文件会把中文当 GBK);zip 文件名用 GBK,另附 Info-ZIP Unicode 文件名字段(0x7075),中文 Windows 自带解压和 7-Zip 都显示正确(ZipWriter.cs)。23 个示例都能导出,导出的工程在 mono 的 msbuild 里全部编译通过、输出目录和教材 bin\Debug 一样、程序能启动;VS 2012 还要在机房实测。接口 POST /api/export-project,代码有错时 422。export-exe.ts 改名 export.ts(服务器和网页各一个)。使用说明、设计方案、README、部署说明同步
Deploy / deploy (push) Failing after 1m8s
Deploy / deploy (push) Failing after 1m8s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c3ce31fe1
commit
2aa267839f
17 files changed
+1209
-80
No files matched your search
+107
-3
@@ -740,12 +740,14 @@ describe('AI 评阅', () => {
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
describe('导出 EXE', () => {
|
||||
describe('导出 EXE、VS 工程', () => {
|
||||
const hasLibs = existsSync(`${config.textbookLibsDir}/DeviceCtrlLibrary.dll`)
|
||||
|
||||
async function exportExe(browser: Browser, json: unknown) {
|
||||
const exportExe = (browser: Browser, json: unknown) => exportRequest(browser, 'exe', json)
|
||||
|
||||
async function exportRequest(browser: Browser, kind: 'exe' | 'project', json: unknown) {
|
||||
const response = await app.fetch(
|
||||
new Request('http://localhost/api/export-exe', {
|
||||
new Request(`http://localhost/api/export-${kind}`, {
|
||||
method: 'POST',
|
||||
headers: { authorization: `Bearer ${browser.accessToken}`, 'content-type': 'application/json', 'x-real-ip': browser.ip },
|
||||
body: JSON.stringify(json),
|
||||
@@ -775,6 +777,104 @@ describe('导出 EXE', () => {
|
||||
60000,
|
||||
)
|
||||
|
||||
/** 读出 zip 里的文件:文件名是 GBK 编码,0x7075 扩展字段里是 UTF-8 文件名,两者要一致 */
|
||||
function unzip(zip: Uint8Array<ArrayBuffer>) {
|
||||
const view = new DataView(zip.buffer, zip.byteOffset, zip.byteLength)
|
||||
let end = zip.length - 22
|
||||
while (view.getUint32(end, true) !== 0x06054b50) end--
|
||||
const count = view.getUint16(end + 10, true)
|
||||
let p = view.getUint32(end + 16, true)
|
||||
const files = new Map<string, Uint8Array<ArrayBuffer>>()
|
||||
for (let i = 0; i < count; i++) {
|
||||
expect(view.getUint32(p, true)).toBe(0x02014b50)
|
||||
const flags = view.getUint16(p + 8, true)
|
||||
const method = view.getUint16(p + 10, true)
|
||||
const compressedSize = view.getUint32(p + 20, true)
|
||||
const nameLength = view.getUint16(p + 28, true)
|
||||
const extraLength = view.getUint16(p + 30, true)
|
||||
const commentLength = view.getUint16(p + 32, true)
|
||||
const offset = view.getUint32(p + 42, true)
|
||||
const nameBytes = zip.subarray(p + 46, p + 46 + nameLength)
|
||||
const extra = zip.subarray(p + 46 + nameLength, p + 46 + nameLength + extraLength)
|
||||
expect(flags & 0x800).toBe(0)
|
||||
const name = new TextDecoder('gbk').decode(nameBytes)
|
||||
if (extra.length > 0) {
|
||||
expect(extra[0]! | (extra[1]! << 8)).toBe(0x7075)
|
||||
expect(new TextDecoder().decode(extra.subarray(9))).toBe(name)
|
||||
} else {
|
||||
expect(/^[\x20-\x7e]+$/.test(name)).toBe(true)
|
||||
}
|
||||
const local = offset + 30 + view.getUint16(offset + 26, true) + view.getUint16(offset + 28, true)
|
||||
const data = zip.subarray(local, local + compressedSize)
|
||||
files.set(name, method === 8 ? Bun.inflateSync(data) : data)
|
||||
p += 46 + nameLength + extraLength + commentLength
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
test.skipIf(!hasLibs)(
|
||||
'导出 VS 2012 工程:资源换成 resx、素材复制到输出目录、教材 dll 放在 lib;照 C# 5 检查',
|
||||
async () => {
|
||||
const t = await teacher()
|
||||
const response = await exportRequest(t, 'project', { files: (await loadSample('p7-t2-智慧寝室'))!, name: '寝室' })
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('application/zip')
|
||||
expect(response.headers.get('content-disposition')).toContain(`filename*=UTF-8''${encodeURIComponent('寝室.zip')}`)
|
||||
const files = unzip(new Uint8Array(await response.arrayBuffer()))
|
||||
const text = (name: string) => {
|
||||
const bytes = files.get(name)
|
||||
expect(bytes, name).toBeDefined()
|
||||
// VS 2012 要带 BOM 才把文件当成 UTF-8;Windows 的换行
|
||||
expect([...bytes!.subarray(0, 3)]).toEqual([0xef, 0xbb, 0xbf])
|
||||
const content = new TextDecoder().decode(bytes!.subarray(3))
|
||||
expect(content.replace(/\r\n/g, '')).not.toContain('\n')
|
||||
return content
|
||||
}
|
||||
expect(text('寝室/寝室.sln')).toContain('# Visual Studio 2012\r\nProject("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "寝室", "寝室\\寝室.csproj"')
|
||||
const csproj = text('寝室/寝室/寝室.csproj')
|
||||
expect(csproj).toContain('<RootNamespace>寝室情景模式</RootNamespace>')
|
||||
expect(csproj).toContain('<AssemblyName>寝室</AssemblyName>')
|
||||
expect(csproj).toContain('<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>')
|
||||
// 和教材工程一样只引用直接用到的教材 dll,其他的作为依赖复制到输出目录
|
||||
expect(csproj).toContain('<HintPath>lib\\DeviceCtrlLibrary.dll</HintPath>')
|
||||
expect(csproj).toContain('<HintPath>lib\\AxInterop.WMPLib.dll</HintPath>')
|
||||
expect(csproj).not.toContain('ICS.Models')
|
||||
expect(files.has('寝室/寝室/lib/ICS.Models.dll')).toBe(true)
|
||||
expect(csproj).toContain('<Compile Include="Form1.cs">\r\n <SubType>Form</SubType>')
|
||||
expect(csproj).toContain('<Content Include="Resources\\ring.mp3">\r\n <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>')
|
||||
expect(csproj).toContain('<None Include="Resources\\switch_on.png" />')
|
||||
expect(files.has('寝室/寝室/Resources/ring.mp3')).toBe(true)
|
||||
expect(text('寝室/寝室/Properties/Resources.resx')).toContain('<value>..\\Resources\\switch_on.png;System.Drawing.Bitmap, System.Drawing')
|
||||
const resources = text('寝室/寝室/Properties/Resources.Designer.cs')
|
||||
expect(resources).toContain('new global::System.Resources.ResourceManager("寝室情景模式.Properties.Resources"')
|
||||
expect(resources).not.toContain('Sim.Host')
|
||||
expect(text('寝室/寝室/Form1.resx')).toContain('<data name="axWindowsMediaPlayer1.OcxState" mimetype="application/x-microsoft.net.object.binary.base64">')
|
||||
expect(text('寝室/寝室/Form1.cs')).toContain('namespace 寝室情景模式')
|
||||
expect(files.has('寝室/寝室/LedHelperCompat.cs')).toBe(false)
|
||||
|
||||
// 书上的 ledHelper.Close() 教材类库里没有,用到时带上补的方法
|
||||
const led = unzip(
|
||||
new Uint8Array(await (await exportRequest(t, 'project', { files: (await loadSample('p2-t3-LED推送模拟量'))!, name: 'LED' })).arrayBuffer()),
|
||||
)
|
||||
expect(new TextDecoder().decode(led.get('LED/LED/LedHelperCompat.cs'))).toContain('Close(this global::DeviceCtrlLibrary.LEDHelper helper)')
|
||||
expect(new TextDecoder().decode(led.get('LED/LED/LED.csproj'))).toContain('<Compile Include="LedHelperCompat.cs" />')
|
||||
|
||||
// 机房的 VS 2012 只支持 C# 5
|
||||
const login = (await loadSample('p1-t1-登录'))!
|
||||
const newer = login.map((f) =>
|
||||
f.path === 'Program.cs' ? { ...f, content: f.content.replace('[STAThread]', 'static string S => $"{1}";\n [STAThread]') } : f,
|
||||
)
|
||||
const failed = await exportRequest(t, 'project', { files: newer, name: '登录' })
|
||||
expect(failed.status).toBe(422)
|
||||
const diagnostics = ((await failed.json()) as any).diagnostics
|
||||
expect(diagnostics.map((d: any) => d.code)).toEqual(['CS8026', 'CS8026'])
|
||||
expect(diagnostics[1]).toMatchObject({ file: 'Program.cs', hint: expect.stringContaining('string.Format') })
|
||||
// 导出 EXE 不受这个限制
|
||||
expect((await exportExe(t, { files: newer, name: '登录' })).status).toBe(200)
|
||||
},
|
||||
60000,
|
||||
)
|
||||
|
||||
test('服务器上没有教材类库时说明原因;格式不对拒绝', async () => {
|
||||
const t = await teacher()
|
||||
const saved = config.textbookLibsDir
|
||||
@@ -783,9 +883,13 @@ describe('导出 EXE', () => {
|
||||
const response = await exportExe(t, { files: (await loadSample('p1-t1-登录'))!, name: '登录' })
|
||||
expect(response.status).toBe(503)
|
||||
expect(((await response.json()) as any).message).toContain('设备类库')
|
||||
const project = await exportRequest(t, 'project', { files: (await loadSample('p1-t1-登录'))!, name: '登录' })
|
||||
expect(project.status).toBe(503)
|
||||
expect(((await project.json()) as any).message).toContain('不能导出工程')
|
||||
} finally {
|
||||
config.textbookLibsDir = saved
|
||||
}
|
||||
expect((await exportExe(t, { files: 'x' })).status).toBe(400)
|
||||
expect((await exportRequest(t, 'project', { files: 'x' })).status).toBe(400)
|
||||
}, 60000)
|
||||
})
|
||||
Reference in new issue
Block a user