200 lines
23 KiB
JavaScript
200 lines
23 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { execFileSync } from 'node:child_process';
|
||
|
||
export function createRemainingCases(h) {
|
||
const {root,output,fixture,step,open,button,evaluate,send,until,wait,check,box,dimensions,volume,count,upload,save,reopen,idle,hash}=h;
|
||
const objectCount="document.querySelectorAll('[role=treeitem]').length";
|
||
const stage="document.querySelector('.stage > [role=status]')?.textContent";
|
||
const json=(name,data)=>fs.writeFileSync(path.join(output,name),JSON.stringify(data,null,2)+'\n');
|
||
async function memory() {
|
||
const rows=execFileSync('ps',['-eo','pid=,ppid=,rss='],{encoding:'utf8'}).trim().split('\n').map(line=>line.trim().split(/\s+/).map(Number));
|
||
const pids=new Set([h.getBrowserPid()]);
|
||
for(let changed=true;changed;){changed=false;for(const [pid,parent]of rows)if(pids.has(parent)&&!pids.has(pid)){pids.add(pid);changed=true;}}
|
||
const rss=rows.filter(([pid])=>pids.has(pid)).reduce((n,row)=>n+row[2],0)*1024;
|
||
await send('Performance.enable');
|
||
const {metrics}=await send('Performance.getMetrics');
|
||
return {at:new Date().toISOString(),browserTreeRssBytes:rss,processes:pids.size,
|
||
rendererJsHeapBytes:metrics.find(m=>m.name==='JSHeapUsedSize')?.value,
|
||
note:'RSS sums browser descendants and can double-count shared pages; JS heap excludes Worker WASM. Not a physical-memory measurement.'};
|
||
}
|
||
async function importFixture(file) {
|
||
const n=await count();await upload(file);
|
||
await until('Native import complete',`({count:${objectCount},status:${stage},busy:document.querySelector('[aria-label="Import STEP files"]').disabled})`,s=>{
|
||
if(!s.busy&&s.status&&!/^(Importing|Imported)/.test(s.status))throw Error(s.status);
|
||
return !s.busy&&s.count===n+1;
|
||
},60000);
|
||
const v=await evaluate("Number(document.querySelector('[data-native-volume]')?.textContent)");
|
||
check(v>0,'Imported shape has no positive native volume');return v;
|
||
}
|
||
async function chooseLocalFile(file) {
|
||
await until('Open command registration loaded',"window.__freecadWorkbenchState?.selected?.command",s=>s==='Std_Open');
|
||
await send('Page.setInterceptFileChooserDialog',{enabled:true});
|
||
const eventStart=h.getEvents().length;
|
||
const point=await evaluate("(()=>{const b=[...document.querySelectorAll('button')].find(b=>b.textContent==='Choose files');if(!b||b.disabled)throw Error('File chooser button unavailable');b.scrollIntoView({block:'center'});const r=b.getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2};})()");
|
||
await send('Input.dispatchMouseEvent',{type:'mousePressed',...point,button:'left',clickCount:1});
|
||
await send('Input.dispatchMouseEvent',{type:'mouseReleased',...point,button:'left',clickCount:1});
|
||
let chooser;
|
||
for(let i=0;i<100;i++){chooser=h.getEvents().slice(eventStart).find(e=>e.method==='Page.fileChooserOpened');if(chooser)break;await wait(100);}
|
||
check(chooser?.params.backendNodeId,'Native file chooser did not open');
|
||
await send('DOM.setFileInputFiles',{backendNodeId:chooser.params.backendNodeId,files:[file]});
|
||
await send('Page.setInterceptFileChooserDialog',{enabled:false});
|
||
}
|
||
async function openResult() {
|
||
return until('Native open response',"window.__freecadWorkbenchState?.response",r=>{
|
||
if(r?.kind==='error')throw Error(JSON.stringify(r.error));
|
||
return r?.kind==='result';
|
||
},60000);
|
||
}
|
||
const downloadSnapshot=()=>Object.fromEntries(fs.readdirSync(path.join(output,'downloads')).map(n=>[n,fs.statSync(path.join(output,'downloads',n)).mtimeMs]));
|
||
async function downloadedFile(extension,previous={}) {
|
||
for(let i=0;i<200;i++) {
|
||
const names=fs.readdirSync(path.join(output,'downloads')).filter(n=>n.toLowerCase().endsWith('.'+extension.toLowerCase())&&previous[n]!==fs.statSync(path.join(output,'downloads',n)).mtimeMs);
|
||
if(names.length)return path.join(output,'downloads',names.at(-1));
|
||
await wait(100);
|
||
}
|
||
throw Error(`Missing completed .${extension} download`);
|
||
}
|
||
async function nativeExport(format) {
|
||
const previous=downloadSnapshot();
|
||
await h.select('Native export format',format);await button('导出选中对象');
|
||
await until('Native export',stage,s=>{if(s?.startsWith('Error:'))throw Error(s);return s?.startsWith('Exported ');},30000);
|
||
return downloadedFile(format,previous);
|
||
}
|
||
async function command(module,name,args={}) {
|
||
await evaluate(`(()=>{const b=[...document.querySelectorAll('button.module')].find(b=>b.querySelector('strong')?.textContent===${JSON.stringify(module)});if(!b)throw Error('Module missing');b.click();})()`);
|
||
await until('Command registration loaded',`[...document.querySelectorAll('button.command')].some(b=>b.querySelector('span')?.textContent===${JSON.stringify(name)})`);
|
||
await evaluate(`(()=>{const b=[...document.querySelectorAll('button.command')].find(b=>b.querySelector('span')?.textContent===${JSON.stringify(name)});b.click();})()`);
|
||
await h.input('命令 JSON 参数',JSON.stringify(args));
|
||
await until('Selected command ready',"[...document.querySelectorAll('button')].some(b=>b.textContent==='执行选中命令'&&!b.disabled)");
|
||
await button('执行选中命令');
|
||
const response=await until('Native command result',"window.__freecadWorkbenchState?.response",r=>{if(r?.kind==='error')throw Error(JSON.stringify(r.error));return r?.kind==='result';},60000);
|
||
const row=response.result?.records?.[0];
|
||
if(row&&'status' in row)check(['success','exposed','ok'].includes(row.status),JSON.stringify(row));
|
||
const result=row?.result??response.result;
|
||
check(!result?.sceneError,`Scene refresh failed: ${result?.sceneError}`);
|
||
return result;
|
||
}
|
||
async function workbenchDocument() {
|
||
await open();await box();await box([3,4,5]);await save();
|
||
const previous=downloadSnapshot();await button('下载 native FCStd');
|
||
const file=await downloadedFile('fcstd',previous);
|
||
await open('/workbench.html?module=Web&command=Std_Open');await chooseLocalFile(file);
|
||
const r=await openResult(),v=r.result.records[0].result;
|
||
check(!v.sceneError&&v.errors.length===0,'Native open failed');return v.activeDocument;
|
||
}
|
||
async function inspect(documentId,expression) {
|
||
const value=await command('Web','Std_CommandLine',{documentId,runtimeOwner:'PartBox',operation:'submit',source:`import json; print(json.dumps(${expression}))`});
|
||
check(!value.stderr,'Native console returned an error: '+value.stderr);
|
||
return JSON.parse(value.stdout.trim().split('\n').at(-1));
|
||
}
|
||
async function saveAndReopenNative(documentId,name) {
|
||
const saved=await command('Web','Std_SaveAs',{documentId,runtimeOwner:'PartBox',filename:name});
|
||
const file=saved.files?.[0];check(file?.contentsBase64,'Save returned no FCStd bytes');
|
||
const local=path.join(output,name);fs.writeFileSync(local,Buffer.from(file.contentsBase64,'base64'));
|
||
await open('/workbench.html?module=Web&command=Std_Open');await chooseLocalFile(local);
|
||
const r=await openResult(),v=r.result.records[0].result;check(!v.sceneError&&v.errors.length===0,'Restore failed');return v.activeDocument;
|
||
}
|
||
const cases={
|
||
'GEO-01':async()=>{
|
||
await step('创建 2 x 3 x 4 Box',async()=>{await open();return box();},'1 个对象,原生体积约 24');
|
||
await step('读取原生包围盒属性',async()=>{const bounds=await evaluate("JSON.parse(document.querySelector('[data-native-bounds]').dataset.nativeBounds)");check(bounds.length===6&&bounds.every((n,i)=>Math.abs(n-[0,0,0,2,3,4][i])<1e-5),'OCCT bounds differ');return {bounds,volume:await volume(24),tolerance:1e-5};},'原生返回 [0,0,0]-[2,3,4],容差 1e-5 mm');
|
||
},
|
||
'FILE-03':async()=>{
|
||
let downloaded;
|
||
await step('创建和编辑保存文档',async()=>{await open();await box();await h.input('Object Label','User acceptance box');await evaluate("document.querySelector('[aria-label=\"Object Label\"]').blur()");await idle();return save();},'FCStd ZIP 已写入 OPFS');
|
||
await step('下载真实 FCStd 文件',async()=>{await button('下载 native FCStd');for(let i=0;i<100;i++){const files=fs.readdirSync(path.join(output,'downloads')).filter(n=>/\.FCStd$/i.test(n));if(files.length){downloaded=path.join(output,'downloads',files.at(-1));break;}await wait(100);}check(downloaded,'FCStd download missing');return {file:downloaded,bytes:fs.statSync(downloaded).size,sha256:hash(downloaded)};},'下载完成并记录文件指纹');
|
||
await step('关闭后打开 OPFS 文档',async()=>{await button('关闭活动 Document');const state=await reopen();check(state.objects===1,'Object count differs');await volume(24);return state;},'对象数量和体积恢复');
|
||
await step('通过 Std_Open 文件选择器重开下载文件',async()=>{await open('/workbench.html?module=Web&command=Std_Open');await chooseLocalFile(downloaded);const r=await openResult();const v=r.result.records[0].result;check(v.errors.length===0&&v.opened.length===1,'Native file open did not succeed');check(v.opened[0].documents[0].objectCount===1,'FCStd object count differs');check(!v.opened[0].documents[0].partialRestore&&!v.opened[0].documents[0].restoreError&&!v.sceneError,'Partial restore or scene failure');return v;},'本地 FCStd 完整恢复,不以 OPFS 回读代替');
|
||
},
|
||
'FILE-05':async()=>{
|
||
let stl,before;
|
||
await step('准备基准 Box',async()=>{await open();await box();before=await evaluate("JSON.parse(document.querySelector('[data-native-bounds]').dataset.nativeBounds)");return {bounds:before,volume:await volume(24)};},'原生体积 24,包围盒 [0,0,0]-[2,3,4]');
|
||
await step('使用原生 OCCT 导出 STL',async()=>{stl=await nativeExport('stl');return {file:stl,size:fs.statSync(stl).size,sha256:hash(stl)};},'下载非空 STL 文件');
|
||
await step('真实文件回读和误差比较',async()=>{await open();await upload(stl);await until('STL object',objectCount,n=>n===1,30000);await idle();const bounds=await evaluate("JSON.parse(document.querySelector('[data-native-bounds]').dataset.nativeBounds)");check(bounds.every((v,i)=>Math.abs(v-before[i])<1e-5),'STL bounds changed');return {bounds,volume:await volume(24),tolerance:1e-5};},'STL 回读体积约 24,包围盒误差小于 1e-5 mm');
|
||
},
|
||
'FILE-02':async()=>{
|
||
const report=[];
|
||
for(const format of ['step','iges','brep','stl'])await step(`${format.toUpperCase()} 原生文件导出及导入`,async()=>{
|
||
await open();await box();const file=await nativeExport(format);await open();const v=await importFixture(file);
|
||
const bounds=await evaluate("JSON.parse(document.querySelector('[data-native-bounds]').dataset.nativeBounds)");
|
||
check(bounds.every((n,i)=>Math.abs(n-[0,0,0,2,3,4][i])<1e-4),'Imported bounds differ');
|
||
const result={format,file,sha256:hash(file),supported:true,imported:true,volume:v,bounds};report.push(result);json('format-validation.json',report);return result;
|
||
},'有效文件由现有 OCCT 接口生成;导入非空几何,包围盒正确');
|
||
await step('DXF 本地桌面样本导入',async()=>{
|
||
const file=path.join(root,'freecad-source/src/Mod/OpenSCAD/OpenSCADTest/data/Square.dxf');
|
||
await open('/workbench.html?module=Draft');const result=await command('Draft','draft.formatBatch',{schema:'freecad-web-draft/format-batch/v1',module:'Draft',records:[{format:'dxf',direction:'import',documentId:'DxfValidation',payload:fs.readFileSync(file,'utf8')}]});
|
||
const row=result.records[0];check(row.objectCount>0&&row.objects.some(o=>o.length>0||o.area>0),'DXF has no native geometry');
|
||
report.push({format:'dxf',file,sha256:hash(file),supported:true,imported:true,result:row});json('format-validation.json',report);return row;
|
||
},'复用桌面 DXF 导入链路,非空原生对象');
|
||
for(const [format,p]of [
|
||
['obj','FreeCADBaseLib/sources/src/Qt-5.15.13/qt3d/src/3rdparty/assimp/src/test/models/OBJ/box_without_lineending.obj'],
|
||
['ifc','FreeCADBaseLib/sources/src/Qt-5.15.13/qt3d/src/3rdparty/assimp/src/test/models/IFC/AC14-FZK-Haus.ifc'],
|
||
])await step(`${format.toUpperCase()} 实际注册能力及文件处理`,async()=>{
|
||
await open('/workbench.html?module=Web&command=Std_Open');await chooseLocalFile(path.join(root,p));
|
||
const response=await until('File handled',"window.__freecadWorkbenchState?.response",r=>r?.kind==='result'||r?.kind==='error',60000);
|
||
if(response.kind==='error') {
|
||
const error=response.error;
|
||
check(error.code==='file_open_failed'&&error.diagnostic?.openResult?.errors?.length,'Unstructured format failure');
|
||
const value={format,file:p,sha256:hash(path.join(root,p)),supported:false,imported:false,error};report.push(value);json('format-validation.json',report);
|
||
throw new Error(`${format.toUpperCase()} native import is not available: ${error.message}`);
|
||
}
|
||
const value=response.result.records[0].result;
|
||
check(value.opened.length>0&&!value.sceneError,'Imported document or display missing');
|
||
report.push({format,file:p,sha256:hash(path.join(root,p)),supported:true,imported:true,result:value});json('format-validation.json',report);return value;
|
||
},'支持则完整导入;依赖或登记缺口记录失败,不伪装支持');
|
||
},
|
||
'ERR-01':async()=>{
|
||
await step('准备已有文档对象',async()=>{await open();return box();},'已有体积 24 对象');
|
||
await step('在原生导入中请求取消',async()=>{await upload(path.join(root,'freecad-source/data/examples/Schenkel.stp'));await until('Import entered native operation',"document.querySelector('[data-import-phase]')?.dataset.importPhase",v=>v==='native',30000);await button('取消导入');return {phase:await evaluate("document.querySelector('[data-import-phase]')?.dataset.importPhase"),status:await evaluate(stage)};},'原生事务尚未提交,显示取消中');
|
||
await step('等待回滚并核对原文档',async()=>{await until('Import rolled back',stage,s=>s?.startsWith('Import cancelled'),60000);await idle();check(await count()===1,'Cancelled import left an object');return {objects:await count(),volume:await volume(24)};},'取消后无半成品,原对象保留');
|
||
await step('取消后正常导入恢复',async()=>{const v=await importFixture(fixture);check(await count()===2,'Recovery import missing');return {objects:await count(),volume:v};},'Worker 和原生文档可继续使用');
|
||
},
|
||
'DOC-02':async()=>{
|
||
let doc,part,varset;
|
||
await step('准备含两个实体的原生工作台文档',async()=>{doc=await workbenchDocument();return {doc};},'两个实体从 FCStd 打开在 PartBox 宿主');
|
||
await step('创建 Part 并按桌面语义组合选择',async()=>{const v=await command('Web','Std_Part',{documentId:doc,label:'Validation Part',action:'create',selection:['Box1','Box2'].map(object=>({documentId:doc,object}))});part=v.createdObject;check(v.activePart===part&&v.objects.find(o=>o.name===part).children.length===2,'Part grouping or document Tip differs');return v;},'Part 包含两个实体,文档 Tip 指向新 Part');
|
||
await step('移出一个对象再加入',async()=>{for(const names of [['Box1'],['Box1','Box2']]){await command('Web','Std_Properties',{documentId:doc,operation:'apply',object:part,selection:[{documentId:doc,object:part}],property:'Group',value:JSON.stringify(names.map(object=>({documentId:doc,object})))});const actual=await inspect(doc,`[o.Name for o in App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(part)}).Group]`);check(JSON.stringify(actual.sort())===JSON.stringify(names.sort()),'Group edit not applied');}return {part,children:['Box1','Box2']};},'原生 Group 属性执行移出/加入,层级正确');
|
||
await step('创建 VarSet 并设置变量',async()=>{const v=await command('Web','Std_VarSet',{documentId:doc,action:'create',label:'Validation Variables',selection:[{documentId:doc,object:part}]});varset=v.createdObject;await command('Web','Std_VarSet',{documentId:doc,action:'set-variable',object:varset,name:'Width',type:'App::PropertyFloat',value:'7.5',expression:'',selection:[]});const read=await command('Web','Std_VarSet',{documentId:doc,action:'read',object:varset,selection:[]});check(read.objects.find(o=>o.name===varset).properties.some(p=>p.name==='Width'&&p.value===7.5),'Variable not stored');return read;},'VarSet 动态变量为 7.5');
|
||
await step('设置并清除变量表达式',async()=>{await command('Web','Std_VarSet',{documentId:doc,action:'set-variable',object:varset,name:'DoubleWidth',type:'App::PropertyFloat',value:'',expression:'Width * 2',selection:[]});const actual=await inspect(doc,`App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(varset)}).DoubleWidth`);check(actual===15,'Expression not evaluated');await command('Web','Std_VarSet',{documentId:doc,action:'remove-variable',object:varset,name:'DoubleWidth',selection:[]});return {evaluated:actual};},'表达式求值为 15,动态变量可以移除');
|
||
await step('保存刷新重开结构与 Tip',async()=>{doc=await saveAndReopenNative(doc,'Structure.FCStd');const state=await inspect(doc,`{'tip':App.getDocument(${JSON.stringify(doc)}).Tip.Name,'children':[o.Name for o in App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(part)}).Group],'width':App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(varset)}).Width}`);check(state.tip===part&&state.children.includes('Box1')&&state.children.includes(varset)&&state.width===7.5,'Structure not restored');return state;},'FCStd 恢复树层级、文档 Tip 和变量');
|
||
},
|
||
'DOC-03':async()=>{
|
||
let doc,part,link;
|
||
await step('准备可替换引用的原生文档',async()=>{doc=await workbenchDocument();const v=await command('Web','Std_Part',{documentId:doc,action:'create',selection:['Box1','Box2'].map(object=>({documentId:doc,object}))});part=v.createdObject;return {doc,part};},'Part 包含两个原生对象');
|
||
await step('用 Link 替换父容器引用',async()=>{const v=await command('Web','Std_LinkReplace',{documentId:doc,action:'apply',parentDocument:doc,parentObject:part,selection:[{documentId:doc,object:'Box1'}]});const children=await inspect(doc,`[o.Name for o in App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(part)}).Group]`);link=children.find(n=>n!=='Box2');check(link&&link!=='Box1','Reference was not replaced');const target=await inspect(doc,`App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(link)}).getLinkedObject().Name`);check(target==='Box1','Wrong link target');return {v,children,link,target};},'父容器引用 Link,Link 指向原 Box1');
|
||
await step('撤销重做链接替换',async()=>{await command('Web','Std_Undo',{documentId:doc});const before=await inspect(doc,`[o.Name for o in App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(part)}).Group]`);check(before.includes('Box1'),'Undo failed');await command('Web','Std_Redo',{documentId:doc});const after=await inspect(doc,`[o.Name for o in App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(part)}).Group]`);check(after.includes(link),'Redo failed');return {before,after};},'事务准确恢复父容器引用');
|
||
await step('保存重开链接并解除',async()=>{doc=await saveAndReopenNative(doc,'Links.FCStd');await command('Web','Std_LinkUnlink',{documentId:doc,action:'apply',parentDocument:doc,parentObject:part,selection:[{documentId:doc,object:link}]});const children=await inspect(doc,`[o.Name for o in App.getDocument(${JSON.stringify(doc)}).getObject(${JSON.stringify(part)}).Group]`);check(children.includes('Box1')&&!children.includes(link),'Unlink failed');return children;},'链接持久化后可解除,父容器重新引用原对象');
|
||
},
|
||
'PERF-01':async()=>{
|
||
const files=[['small','freecad-source/src/Mod/Import/App/SCL/Product1.stp'],['medium','web/public/samples/freecad-sample.step'],['large','freecad-source/data/examples/Schenkel.stp']];
|
||
const measurements=[];
|
||
await step('登记本机实际样本和性能范围',async()=>{const data=files.map(([tier,p])=>({tier,path:p,bytes:fs.statSync(path.join(root,p)).size,sha256:hash(path.join(root,p))}));json('performance-fixtures.json',{scope:'Relative size tiers of provided desktop STEP examples, not production maximum-capacity certification',maxImportSeconds:60,fixtures:data});return data;},'样本来自本机桌面 FreeCAD;逐项记录字节数和 SHA-256');
|
||
for(const [tier,p]of files)await step(`${tier} 样本加载与交互测量`,async()=>{
|
||
await open();const samples=[await memory()],start=performance.now();
|
||
let sampling=true;const sampler=(async()=>{while(sampling){await wait(250);if(sampling)samples.push(await memory());}})();
|
||
let v;try{v=await importFixture(path.join(root,p));}finally{sampling=false;await sampler;}
|
||
const seconds=(performance.now()-start)/1000;samples.push(await memory());
|
||
check(seconds<60,'Import exceeds 60 second local test budget');
|
||
const frames=await evaluate("new Promise(resolve=>{let n=0;const start=performance.now();const tick=now=>{n++;if(now-start>=1000)resolve({frames:n,seconds:(now-start)/1000});else requestAnimationFrame(tick);};requestAnimationFrame(tick);})");
|
||
const value={tier,file:p,bytes:fs.statSync(path.join(root,p)).size,seconds,volume:v,peakBrowserTreeRssBytes:Math.max(...samples.map(s=>s.browserTreeRssBytes)),samples,animationCallbackHz:frames.frames/frames.seconds,rendering:'headless software WebGL; callback cadence is not CAD draw FPS'};
|
||
measurements.push(value);json('performance-loading.json',measurements);return value;
|
||
},'60 秒内完成导入,原生实体有正体积,记录内存采样和帧回调频率');
|
||
},
|
||
'PERF-02':async()=>{
|
||
const results=[];
|
||
for(let iteration=1;iteration<=20;iteration++)await step(`第 ${iteration}/20 轮导入、修改、保存、刷新重开`,async()=>{
|
||
await open();const start=performance.now();const importedVolume=await importFixture(fixture);
|
||
await box();await dimensions([3,4,5]);await button('应用尺寸并重算');await idle();await volume(60);
|
||
await save();await open();const restored=await reopen();check(restored.objects===2,'Restored object count differs');
|
||
await h.click('[role=treeitem]');await volume(importedVolume);await evaluate("document.querySelectorAll('[role=treeitem]')[1].click()");await volume(60);
|
||
const filenames=await evaluate("[...document.querySelector('[aria-label=\"OPFS document\"]').options].map(o=>o.value).filter(Boolean)");check(new Set(filenames).size===filenames.length,'Duplicate OPFS entries');
|
||
await wait(250);const mem=await memory();
|
||
const result={iteration,seconds:(performance.now()-start)/1000,objects:restored.objects,importedVolume,boxVolume:60,files:filenames,memory:mem};results.push(result);json('stability-cycles.json',results);return result;
|
||
},'每轮真实 STEP 和修改后的 Box 均恢复,无重复文件或对象');
|
||
await step('检查稳定阶段内存趋势',async()=>{const values=results.slice(5).map(r=>r.memory.browserTreeRssBytes);const mean=a=>a.reduce((n,v)=>n+v,0)/a.length;const growth=mean(values.slice(-5))-mean(values.slice(0,5));const allRising=values.every((v,i)=>i===0||v>values[i-1]);const report={cycles:20,warmupCycles:5,earlyMean:mean(values.slice(0,5)),lateMean:mean(values.slice(-5)),growthBytes:growth,allRising,allowedGrowthBytes:256*1024*1024,scope:'Same host/software WebGL, process-tree RSS (shared pages counted per process)'};json('stability-summary.json',report);check(!allRising&&growth<report.allowedGrowthBytes,'Sustained browser memory growth requires investigation');return report;},'预热后不存在持续单调增长,后五轮平均 RSS 增长不超过 256 MiB');
|
||
},
|
||
};
|
||
return cases;
|
||
}
|