项目 4 任务 2 网络摄像机:PictureBox、Image / Bitmap、照片列表
- 摄像头:自绘东围墙场景(Camera/scene.svg),按云台位置切成 640×480 帧(build-frames.ts); Pan 每次转一格,红外对射被遮挡时画面出现翻墙的人;类名、委托、事件参数与教材 dll 一致, ImageRead 每 250 ms 在后台线程触发 - Image / Bitmap 保存真实图片数据:Save、FromFile、FromStream,文件夹不存在时报 GDI+ 一般性错误; 图片每张只发给浏览器一次(按内容哈希缓存) - 程序所在目录(AppDomain.CurrentDomain.BaseDirectory,编译检查单独放行)和当前目录指向可写的 bin/Debug, 沙箱里在沙箱的临时目录 - PictureBox(SizeMode、BorderStyle);设计器工具箱加"图片框";运行区显示程序保存的图片,点开看大图; 实训台摄像头图标显示开启状态 - 评分规则加图片框显示图片、摄像头开关与转向、保存图片三种检查;示例 p4-t2-网络摄像机 - 测试:摄像头端到端、图片读写与程序目录、AppDomain 编译检查、评分出错情况 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ed9450c02
commit
6b088240da
76 files changed
+1290
-14
No files matched your search
@@ -31,7 +31,7 @@ async function prepare(id: string, name: string, edit?: { file: string; from: st
|
||||
|
||||
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(12))
|
||||
test('每个示例都有评分规则', () => expect(ids.length).toBe(13))
|
||||
for (const id of ids) {
|
||||
test(id, async () => {
|
||||
const result = await grade(await prepare(id, id), await checksOf(id))
|
||||
@@ -80,6 +80,14 @@ describe('改坏的代码', () => {
|
||||
expect(stop.message).toStartWith('期望 txt_time 不再更新时间(定时器应已停止),实际从')
|
||||
}, 60000)
|
||||
|
||||
test('摄像机:忘了绑定画面事件', async () => {
|
||||
const id = 'p4-t2-网络摄像机'
|
||||
const dir = await prepare(id, 'camera', { file: 'Form1.cs', from: 'DeviceCtrlHelper.ipCameraHelper.ImageRead += ipCameraHelper_ImageRead;', to: '' })
|
||||
const result = await grade(dir, await checksOf(id))
|
||||
expect(result.results.map((r) => r.passed)).toEqual([true, false, true, false])
|
||||
expect(result.results[1].message).toBe('期望3 秒内 pictureBox1 显示摄像机拍到的图像,实际是空的')
|
||||
}, 60000)
|
||||
|
||||
test('关闭时忘了挂起线程', async () => {
|
||||
const id = 'p5-t2-烟雾'
|
||||
const dir = await prepare(id, 'thread', { file: 'Form1.cs', from: 'thd.Suspend();//挂起线程', to: '' })
|
||||
|
||||
@@ -318,6 +318,41 @@ test('项目4 任务1:红外对射被遮挡时显示"发现入侵"', async ()
|
||||
await even('发现入侵')
|
||||
}, 30000)
|
||||
|
||||
test('项目4 任务2:打开摄像机显示画面,转动后画面变化,有人翻墙时画面变化,拍照保存到 Camera 文件夹', async () => {
|
||||
const { runner, id } = await startSample('p4-t2-网络摄像机')
|
||||
const pictures = () => runner.next('patch', (m) => m.changes.some((c) => c.id === id('pictureBox1') && c.prop === 'Image'), 5000)
|
||||
const imageOf = (patch: Extract<RunnerMessage, { type: 'patch' }>) =>
|
||||
patch.changes.find((c) => c.id === id('pictureBox1') && c.prop === 'Image')!.value as { key: string; width: number; height: number }
|
||||
|
||||
runner.send({ type: 'action', id: id('txt_ip'), action: 'text', value: '192.168.2.50' })
|
||||
const first = pictures()
|
||||
const cameraOn = runner.next('device', (m) => m.device === 'camera' && m.action === 'on' && m.data === true)
|
||||
runner.send({ type: 'action', id: id('btn_open'), action: 'click' })
|
||||
await cameraOn
|
||||
const center = imageOf(await first)
|
||||
expect(center).toMatchObject({ width: 640, height: 480 })
|
||||
// 图片数据先于(或随)属性变化发给浏览器,同一张图只发一次
|
||||
const sent = runner.messages.filter((m) => m.type === 'image' && m.key === center.key)
|
||||
expect(sent.length).toBe(1)
|
||||
expect(sent[0]).toMatchObject({ mime: 'image/jpeg' })
|
||||
|
||||
const up = pictures()
|
||||
const pan = runner.next('device', (m) => m.device === 'camera' && m.action === 'pan')
|
||||
runner.send({ type: 'action', id: id('btn_up'), action: 'click' })
|
||||
expect((await pan).data).toEqual([3, 0])
|
||||
expect(imageOf(await up).key).not.toBe(center.key)
|
||||
|
||||
const intruder = pictures()
|
||||
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: [false, false, false, true] })
|
||||
const withIntruder = imageOf(await intruder)
|
||||
|
||||
const saved = runner.next('imageSaved')
|
||||
runner.send({ type: 'action', id: id('btn_camera'), action: 'click' })
|
||||
const photo = await saved
|
||||
expect(photo.path).toMatch(/^Camera\/\d{8}_\d{2} \d{2} \d{2}\.jpg$/)
|
||||
expect(photo.image).toBe(withIntruder.key)
|
||||
}, 30000)
|
||||
|
||||
test('项目4 任务3:开报警灯——实训台 DO0 动作,窗体上的报警灯亮', async () => {
|
||||
const { runner, id } = await startSample('p4-t3-报警灯')
|
||||
await openAdam4150(runner, id)
|
||||
@@ -483,6 +518,32 @@ test('ListBox:点某一项先选中(SelectedIndexChanged)再触发 Click
|
||||
expect(output).toBe('changed=1,乙\nclick\ndouble=乙\n')
|
||||
}, 30000)
|
||||
|
||||
test('图片:程序所在目录可写,Image.Save / FromFile 往返,文件夹不存在时和 GDI+ 一样报一般性错误', async () => {
|
||||
const { runner } = await runMain(
|
||||
'image',
|
||||
`
|
||||
string dir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
Console.WriteLine("base=" + dir.EndsWith("/bin/Debug/") + "," + (Directory.GetCurrentDirectory() + "/" == dir));
|
||||
// 1×1 的 PNG
|
||||
byte[] png = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==");
|
||||
Image image = Image.FromStream(new MemoryStream(png));
|
||||
Directory.CreateDirectory(dir + "Pics");
|
||||
image.Save(dir + "Pics/a.png", ImageFormat.Png);
|
||||
Image back = Image.FromFile("Pics/a.png");
|
||||
Console.WriteLine("size=" + back.Width + "x" + back.Height + "," + back.RawFormat);
|
||||
try { image.Save(dir + "NoSuchDir/b.png"); }
|
||||
catch (System.Runtime.InteropServices.ExternalException ex) { Console.WriteLine("error=" + ex.Message); }`,
|
||||
'using System.Drawing;\nusing System.Drawing.Imaging;',
|
||||
)
|
||||
expect((await runner.waitFor('compiled')).diagnostics.filter((d) => d.severity === 'error')).toEqual([])
|
||||
await runner.finished()
|
||||
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||
expect(output).toContain('base=True,True')
|
||||
expect(output).toContain('size=1x1,Png')
|
||||
expect(output).toContain('error=GDI+ 中发生一般性错误。')
|
||||
expect(runner.messages.filter((m) => m.type === 'imageSaved').map((m) => (m as { path: string }).path)).toEqual(['Pics/a.png'])
|
||||
}, 30000)
|
||||
|
||||
test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别的运行目录、环境变量和其他进程,工程目录只读', async () => {
|
||||
process.env.SIM_TEST_SECRET = '不该被看到'
|
||||
const { runner } = await runMain(
|
||||
@@ -493,7 +554,8 @@ test.skipIf(config.sandbox === 'none')('沙箱:看不到服务器文件、别
|
||||
Console.WriteLine("home=" + Directory.Exists("/home"));
|
||||
Console.WriteLine("secret=" + (Environment.GetEnvironmentVariable("SIM_TEST_SECRET") ?? "null"));
|
||||
Console.WriteLine("procs=" + Directory.GetDirectories("/proc").Count(d => char.IsDigit(Path.GetFileName(d)[0])));
|
||||
try { File.WriteAllText("hack.txt", "x"); Console.WriteLine("write=ok"); } catch (Exception) { Console.WriteLine("write=denied"); }
|
||||
// 工程目录在沙箱里挂在 /sim/app(当前目录是可写的程序目录 bin/Debug,不是工程目录)
|
||||
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"));`,
|
||||
)
|
||||
@@ -525,7 +587,7 @@ test('内存上限:不停分配内存的程序得到 OutOfMemoryException,
|
||||
expect(megabytes).toBeLessThanOrEqual(config.heapLimitMb)
|
||||
}, 30000)
|
||||
|
||||
test('编译检查:网络、启动程序、反射、dynamic 给出中文报错;AssemblyInfo.cs 里的特性不受影响', async () => {
|
||||
test('编译检查:网络、启动程序、反射、dynamic、AppDomain 给出中文报错;AssemblyInfo.cs 里的特性、BaseDirectory 不受影响', async () => {
|
||||
const dir = resolve(ROOT, '.runs/test-policy')
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
await Bun.write(
|
||||
@@ -541,6 +603,8 @@ static class Program
|
||||
var client = new HttpClient();
|
||||
typeof(string).GetMethod("Trim", Type.EmptyTypes).Invoke("x", null);
|
||||
dynamic d = 1;
|
||||
string path = AppDomain.CurrentDomain.BaseDirectory + "Camera/";
|
||||
AppDomain.CurrentDomain.GetAssemblies();
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -562,6 +626,7 @@ using System.Runtime.InteropServices;
|
||||
[9, '平台不允许使用 System.Net.Http.HttpClient(网络访问)'],
|
||||
[10, '平台不允许使用 System.Reflection.MethodBase.Invoke(反射)'],
|
||||
[11, '平台不允许使用 dynamic(dynamic 动态调用)'],
|
||||
[13, '平台不允许使用 System.AppDomain.GetAssemblies(加载程序集)'],
|
||||
])
|
||||
expect(result.diagnostics.filter((d) => d.file.includes('AssemblyInfo'))).toEqual([])
|
||||
}, 30000)
|
||||
|
||||
Reference in new issue
Block a user