项目 7 情景控制:虚拟时钟、项目资源、Windows Media Player、TrackBar

- 虚拟时钟:实训台时钟面板可拨时间、1×/10×/60× 倍速、恢复真实时间;编译时把
  DateTime.Now / Today / UtcNow 改写成 SimClock;评分规则加 clock 步骤
- 项目资源(Properties.Resources):平台素材库(自绘开关图片、合成门铃声),"项目资源"对话框勾选,
  平台生成 Properties/Resources.Designer.cs;设计器图片框的"图片"属性可选资源;程序目录下的 Resources
  指向素材库;未添加资源时给中文提示
- Windows Media Player(AxWMPLib):设置 URL 自动播放、Ctlcontrols.play/stop,浏览器里播放声音,
  实训台加广播喇叭;编译时给窗体嵌空资源,OcxState 取到 null 不报错;图片和声音统一按内容缓存(blob)
- TrackBar:拖动、点轨道、方向键,先 ValueChanged 再 Scroll;设计器加"滑动条""媒体播放器"
- 评分规则加 value、media 检查;示例项目 7 任务 1–3(修正书上开关灯判断写反、漏填串口列表)
- 修正:Properties/ 下的资源类不当作窗体;带命名空间的 System.AppDomain.CurrentDomain.BaseDirectory 放行
- 测试:虚拟时钟、TrackBar、项目 7 示例评分、作业起点带资源类

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-23 14:11:05 +08:00
1 parent a9af26c2d6
commit eedf1aa2fb
66 files changed
+3538 -43

No files matched your search

+17 -1
View File
@@ -35,7 +35,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(15))
test('每个示例都有评分规则', () => expect(ids.length).toBe(18))
for (const id of ids) {
test(id, async () => {
const result = await grade(await prepare(id, id), await checksOf(id), id)
@@ -110,6 +110,22 @@ describe('改坏的代码', () => {
expect(result.results[0].message).toStartWith('程序出错:System.ArgumentNullException')
}, 60000)
test('情景控制:照书上把手动开关灯的判断写反,点了灯不亮', async () => {
const id = 'p7-t1-智慧教室'
const dir = await prepare(id, 'lamp-reversed', { file: '教室情景模式.cs', from: ' if (IsLampOpen)\n', to: ' if (!IsLampOpen)\n' })
const checks = [
...(await checksOf(id)),
...parseChecks(`
- title: 能手动开关灯
steps:
- click: pic_lightsStatus
- expect: { control: lamp, on: true, desc: 点开关后灯亮 }
`),
]
const result = await grade(dir, checks, id)
expect(result.results.at(-1)).toMatchObject({ passed: false, message: '期望窗体上的 lamp 点开关后灯亮,实际是灭的' })
}, 60000)
test('关闭时忘了挂起线程', async () => {
const id = 'p5-t2-烟雾'
const dir = await prepare(id, 'thread', { file: 'Form1.cs', from: 'thd.Suspend();//挂起线程', to: '' })
+52 -1
View File
@@ -4,6 +4,7 @@ import { afterAll, afterEach, describe, expect, test } from 'bun:test'
import { cp, mkdir, rm } from 'node:fs/promises'
import { resolve } from 'node:path'
import type { ControlNode, RunnerInput, RunnerMessage } from '@iot/shared'
import { wallNow } from '../src/board'
import { compileService } from '../src/compile-service'
import { config, ROOT } from '../src/config'
import { cpuLimitMessage, runCommand } from '../src/sandbox'
@@ -332,7 +333,7 @@ test('项目4 任务2:打开摄像机显示画面,转动后画面变化,
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)
const sent = runner.messages.filter((m) => m.type === 'blob' && m.key === center.key)
expect(sent.length).toBe(1)
expect(sent[0]).toMatchObject({ mime: 'image/jpeg' })
@@ -630,6 +631,56 @@ test('数据库:照书上的写法建库建表、插入、查询、SqlCommandB
}
}, 30000)
test('虚拟时钟:拨时间、倍速后 DateTime.Now / Today 跟着走,恢复后是真实时间', async () => {
const { runner } = await runMain(
'clock',
`
for (int i = 0; i < 40; i++)
{
Console.WriteLine("now=" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " today=" + DateTime.Today.ToString("yyyy-MM-dd"));
System.Threading.Thread.Sleep(100);
}`,
)
expect((await runner.waitFor('compiled')).ok).toBe(true)
await runner.waitFor('console')
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: { wall: '2024-01-16T06:44:58', realAt: Date.now(), speed: 60 } })
// 60 倍速:真实的 1 秒是虚拟的 1 分钟
await runner.next('console', (m) => /now=2024-01-16 06:4[6-9]/.test(m.text), 5000)
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: DIGITAL_OFF, clock: null })
const real = await runner.next('console', (m) => !m.text.includes('2024-01-16'), 3000)
// 学生程序按服务器配置的时区(config.timeZone)计时,CI 机器本身是 UTC
expect(real.text).toContain(`today=${wallNow().slice(0, 10)}`)
expect(runner.messages.some((m) => m.type === 'console' && m.text.includes('today=2024-01-16'))).toBe(true)
}, 30000)
test('TrackBar:拖动时先 ValueChanged 再 Scroll,超出范围的值截到两端;代码设超出范围的 Value 报错', async () => {
const { runner } = await runMain(
'trackbar',
`
var form = new Form();
var bar = new TrackBar { Name = "bar", Minimum = 18, Maximum = 32 };
bar.ValueChanged += (s, e) => Console.WriteLine("changed=" + bar.Value);
bar.Scroll += (s, e) => Console.WriteLine("scroll=" + bar.Value);
form.Load += (s, e) =>
{
try { bar.Value = 40; } catch (ArgumentOutOfRangeException ex) { Console.WriteLine("error=" + ex.Message); }
};
form.Controls.Add(bar);
Application.Run(form);`,
'using System.Windows.Forms;',
)
const { form } = await runner.waitFor('formShown')
const bar = findByName(form, 'bar')
expect(bar.props).toMatchObject({ Minimum: 18, Maximum: 32, Value: 18 })
runner.send({ type: 'action', id: bar.id, action: 'scroll', value: '26' })
await runner.waitFor('console', (m) => m.text === 'scroll=26\n')
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('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(