483 строки
19 KiB
TypeScript
483 строки
19 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
|
|
module TDev.TestMgr
|
|
{
|
|
export class TestHost
|
|
extends EditorHost
|
|
implements RuntimeHost
|
|
{
|
|
constructor()
|
|
{
|
|
super()
|
|
}
|
|
|
|
scriptId = "?";
|
|
actionName = "none";
|
|
exceptionThrown = false;
|
|
|
|
|
|
public showWall()
|
|
{
|
|
this.wallVisible = true
|
|
this.getWall().style.opacity = "0";
|
|
elt("testHostFrame").setChildren([ this.getWall() ]);
|
|
}
|
|
|
|
public liveMode() { return false; }
|
|
|
|
public exceptionHandler(e:any)
|
|
{
|
|
this.exceptionThrown = true;
|
|
|
|
if (this.scriptId != "?") {
|
|
var bug = Ticker.mkBugReport(e, "autotest-" + this.scriptId + "-" + this.actionName);
|
|
this.attachScriptStackTrace(bug);
|
|
if (!/http:\/\/localhost/.test(bug.jsUrl))
|
|
Util.sendErrorReport(bug);
|
|
}
|
|
}
|
|
|
|
wall = div("sideWall");
|
|
public getWall() { return this.wall; }
|
|
public newWall() {
|
|
this.wall = div("sideWall");
|
|
}
|
|
public notifyStopAsync() : Promise {
|
|
this.onStop();
|
|
return Promise.as();
|
|
}
|
|
public notifyHideWall() {}
|
|
public notifyPagePush() {}
|
|
public notifyPagePop(p:WallPage) {}
|
|
public dontWaitForEvents() { return true; }
|
|
public wallShown() { }
|
|
public wallHidden() { }
|
|
public updateButtonsVisibility() { }
|
|
public notifyPageButtonUpdate() { }
|
|
}
|
|
|
|
export interface ScriptTestResult {
|
|
id:string;
|
|
name:string;
|
|
numErrors:number;
|
|
actions:ActionTestResult[];
|
|
totalTime:number;
|
|
normalizedTotalTime:number;
|
|
downloadError:boolean;
|
|
}
|
|
|
|
export interface ActionTestResult {
|
|
name:string;
|
|
error:string; // empty if none
|
|
time:number; // run time in ms
|
|
}
|
|
|
|
var rt:Runtime;
|
|
var testHost:TestHost;
|
|
|
|
function compileForTestsAsync(flags: AST.CompilerOptions = null)
|
|
{
|
|
rt = new Runtime();
|
|
|
|
AST.TypeChecker.tcApp(Script);
|
|
|
|
if (flags == null) {
|
|
var flags: AST.CompilerOptions = {
|
|
tracing: false,
|
|
replaying: false,
|
|
profiling: false,
|
|
optimizeLoops: /optimizeLoops/.test(document.URL),
|
|
inlining: Browser.compilerInlining || /inlining/.test(document.URL),
|
|
okElimination: Browser.compilerOkElimination || /okElimination/.test(document.URL),
|
|
blockChaining: Browser.compilerBlockChaining || /blockChaining/.test(document.URL),
|
|
commonSubexprElim: /commonSubexprElim/.test(document.URL),
|
|
constantPropagation: /constantPropagation/.test(document.URL),
|
|
coverage: false,
|
|
crashOnInvalid: /crashOnInvalid/.test(document.URL),
|
|
};
|
|
}
|
|
var compileCounter = TDev.RT.Perf.start("compile." + testHost.scriptId);
|
|
var cs = AST.Compiler.getCompiledScript(Script, flags);
|
|
TDev.RT.Perf.stop(compileCounter);
|
|
rt.initFrom(cs);
|
|
Runtime.theRuntime = rt;
|
|
RT.ArtCache.resetProgress();
|
|
rt.setHost(testHost);
|
|
return rt.initDataAsync();
|
|
}
|
|
|
|
function emptyResult(id:string)
|
|
{
|
|
var testRes:ScriptTestResult = {
|
|
id: id,
|
|
name: Script.getName(),
|
|
numErrors: 0,
|
|
actions: [],
|
|
totalTime: 0,
|
|
normalizedTotalTime: 0,
|
|
downloadError: false
|
|
}
|
|
return testRes
|
|
}
|
|
|
|
export function runTestsAsync(id: string, flags:AST.CompilerOptions = null)
|
|
{
|
|
var totalTime = 0;
|
|
var testRes = emptyResult(id);
|
|
var res = new PromiseInv();
|
|
var tests = Script.orderedThings().filter((t) => {
|
|
if (t instanceof AST.Action) {
|
|
var a = <AST.Action>t;
|
|
return a.isTest();
|
|
} else return false;
|
|
});
|
|
|
|
testHost = new TestHost();
|
|
testHost.scriptId = id;
|
|
|
|
if (Script.getName() === undefined) {
|
|
testRes.downloadError = true;
|
|
res.success(testRes);
|
|
return res;
|
|
}
|
|
|
|
RT.ArtCache.runningTests = true;
|
|
compileForTestsAsync(flags).done(() => {
|
|
|
|
var idx = 0;
|
|
var doTest = () => {
|
|
if (idx >= tests.length) {
|
|
testRes.totalTime = totalTime;
|
|
RT.ArtCache.runningTests = false;
|
|
res.success(testRes);
|
|
} else {
|
|
var act = <AST.Action>tests[idx++];
|
|
Util.log("Testing {0}", act.getName())
|
|
var actStart = TDev.RT.Perf.start(id+"."+act.getName());
|
|
var actRes:ActionTestResult = {
|
|
name: act.getName(),
|
|
error: "",
|
|
time: 0
|
|
}
|
|
testRes.actions.push(actRes)
|
|
|
|
testHost.exceptionThrown = false;
|
|
testHost.actionName = act.getName();
|
|
|
|
testHost.showWall();
|
|
|
|
testHost.onStop = () => {
|
|
actRes.time = TDev.RT.Perf.stop(actStart);
|
|
totalTime += actRes.time;
|
|
if (testHost.exceptionThrown) {
|
|
testRes.numErrors++;
|
|
actRes.error = "exception";
|
|
}
|
|
Util.setTimeout(0, doTest);
|
|
};
|
|
|
|
if (act.isCompilerTest()) {
|
|
if (!act._errorsOK)
|
|
testHost.exceptionHandler(new Error("error mismatch"));
|
|
testHost.onStop();
|
|
} else {
|
|
rt.initPageStack();
|
|
rt.devMode = true;
|
|
rt.currentScriptId = id;
|
|
rt.baseScriptId = "unknown";
|
|
rt.testMode = true;
|
|
|
|
ProgressOverlay.bumpShow(); // rt.run is calling hide
|
|
|
|
if (act.isPage()) {
|
|
rt.run(Runtime.syntheticFrame((s) => {
|
|
s.rt.postAutoPage("this", actRes.name);
|
|
}), []);
|
|
} else {
|
|
rt.run(rt.compiled.actionsByStableName[act.getStableName()], []);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
doTest();
|
|
|
|
})
|
|
|
|
return res;
|
|
}
|
|
|
|
function displayResults(res:ScriptTestResult[], betaMode = false, msg = "")
|
|
{
|
|
elt("testHostFrame").setChildren([]);
|
|
|
|
var m = new ModalDialog();
|
|
var d = div("test-results");
|
|
m.add(d);
|
|
m.noChrome();
|
|
m.setScroll();
|
|
|
|
var ok = res.every((r) => r.numErrors == 0);
|
|
|
|
var h = "";
|
|
|
|
var numTotal = 0;
|
|
var numFailed = 0;
|
|
|
|
var perScript = "";
|
|
res.forEach((r) => {
|
|
perScript += "<div><b class='" + (r.numErrors ? "test-error" : "test-ok") + "'>" + r.name + "</b> (" +
|
|
r.normalizedTotalTime.toFixed(3) + ' - ' + r.totalTime.toFixed(0) + "ms, " + r.id + "): ";
|
|
if (r.actions.length == 0) {
|
|
numTotal++;
|
|
if (r.numErrors) numFailed++;
|
|
}
|
|
r.actions.forEach((a) => {
|
|
numTotal++;
|
|
perScript += "<span class='" + (a.error ? "test-error" : "test-ok") + "'>" + a.name + "</span>";
|
|
if (a.error) numFailed++;
|
|
if (a.time > 20) {
|
|
perScript += " (" + a.time.toFixed(0) + "ms)";
|
|
}
|
|
perScript += ", ";
|
|
})
|
|
perScript += "</div>\n";
|
|
});
|
|
|
|
if (res.length > 1) {
|
|
|
|
if (Benchmarker.jsProgramsTested.aggregatesCount() > 0) {
|
|
perScript += "<div><br/>performance benchmarks (JavaScript)</div>"
|
|
}
|
|
Benchmarker.jsProgramsTested.forEachAggregate((mes: Benchmarker.SumMeasurement) => {
|
|
perScript += "<div><b class='" + (!mes.correct ? "test-error" : "test-ok") + "'>" + mes.name + "</b>"
|
|
+ " (" + (mes.average / TDev.RT.Perf.unit()).toFixed(3) + ' - '
|
|
+ mes.average.toFixed(0) + "ms)</div>\n";
|
|
if (!mes.correct) {
|
|
numFailed++;
|
|
ok = false;
|
|
}
|
|
numTotal++;
|
|
});
|
|
if (Benchmarker.jsProgramsTested.aggregatesCount() > 0) {
|
|
perScript += "<div><br/>performance benchmarks (TouchDevelop)</div>"
|
|
}
|
|
Benchmarker.tdProgramsTested.forEachAggregate((mes: Benchmarker.SumMeasurement) => {
|
|
perScript += "<div><b class='" + (!mes.correct ? "test-error" : "test-ok") + "'>" + mes.name + "</b>"
|
|
+ " (" + (mes.average / TDev.RT.Perf.unit()).toFixed(3) + ' - '
|
|
+ mes.average.toFixed(0) + "ms)</div>\n";
|
|
if (!mes.correct) {
|
|
numFailed++;
|
|
ok = false;
|
|
}
|
|
numTotal++;
|
|
});
|
|
}
|
|
|
|
if (ok && isBeta) return; // skip confirmation dialog in beta unless something fails
|
|
if (ok) {
|
|
h += "<h2 class='test-ok'>tests ok</h2>\n"
|
|
if (betaMode)
|
|
h += "<div class='test-important'>Thank you for your help in making TouchDevelop better! " +
|
|
"If you find anything fishy about this beta version, please let " +
|
|
"us know at touchdevelop@microsoft.com.</div>";
|
|
} else {
|
|
h += "<h2 class='test-error'>" + numFailed + " of " + numTotal + " tests failed</h2>\n"
|
|
if (betaMode)
|
|
h += "<div class='test-important'>Thank you for finding these issues! Developers have been notified. " +
|
|
"You may want to leave the beta version and go back to the main version.</div>";
|
|
}
|
|
|
|
h += "<div>" + msg + "</div>";
|
|
|
|
Browser.setInnerHTML(d, h + (betaMode ? "" : perScript));
|
|
|
|
var leave = betaMode && !ok
|
|
|
|
d.appendChild(div("wall-dialog-buttons",
|
|
leave ? HTML.mkButton(lf("leave beta now"), () => {
|
|
Util.navigateInWindow("https://www.touchdevelop.com/app/")
|
|
}) : null,
|
|
HTML.mkButton(leave ? "keep using beta" : "ok", () => { m.dismiss() })))
|
|
|
|
m.show();
|
|
}
|
|
|
|
export function testCurrentScript()
|
|
{
|
|
ProgressOverlay.lockAndShow(lf("testing current script"), () => {
|
|
runTestsAsync("?").done((res:ScriptTestResult) => {
|
|
ProgressOverlay.hide();
|
|
displayResults([res]);
|
|
})
|
|
})
|
|
}
|
|
|
|
var testsFromTag = false;
|
|
var numTests = 0;
|
|
|
|
export function getNumScripts(): number {
|
|
return numTests;
|
|
}
|
|
|
|
export function testAllScripts(isBeta = false)
|
|
{
|
|
return Cloud.isOnlineWithPingAsync()
|
|
.then((isOnline: boolean) => {
|
|
if (!isOnline) {
|
|
TDev.ModalDialog.info(lf("Tests cancelled"), lf("You seem to be offline. Tests can only run when you are online."));
|
|
return;
|
|
}
|
|
|
|
var scriptsToRun = []
|
|
var startTime = TDev.RT.Perf.startPaused('tests', true);
|
|
var tryAgain = 5;
|
|
|
|
var fetch = (cont: string) => {
|
|
Browser.TheApiCacheMgr.getAsync("test/scripts?applyupdates=true&etagsmode=etagsonly&count=103" +
|
|
(cont ? "&continuation=" + cont : "")).then((resp: JsonList) =>
|
|
{
|
|
resp.etags.forEach((it) => scriptsToRun.push(it.id))
|
|
if (resp.continuation) fetch(resp.continuation);
|
|
else finish();
|
|
})
|
|
}
|
|
|
|
var finishCore = () => {
|
|
}
|
|
|
|
var finish = () => {
|
|
var results: ScriptTestResult[] = []
|
|
var scriptCache: any = {}
|
|
numTests = scriptsToRun.length;
|
|
ProgressOverlay.lockAndShow(isBeta ? lf("thank you for trying the beta!") : lf("running platform tests"), () => {
|
|
var idx = 0;
|
|
var lastBreak = Util.now();
|
|
|
|
if (isBeta)
|
|
ProgressOverlay.setAddInfo([lf("we're running some tests; thanks for waiting")]);
|
|
|
|
var doNext = () => {
|
|
if (idx >= scriptsToRun.length) {
|
|
ProgressOverlay.hide();
|
|
numTests = 0;
|
|
setGlobalScript(null);
|
|
var totalTime = TDev.RT.Perf.stop(startTime);
|
|
totalTime += Benchmarker.jsProgramsTested.totalTime();
|
|
totalTime += Benchmarker.tdProgramsTested.totalTime();
|
|
var msg = "";
|
|
if (isBeta)
|
|
msg = "total test run time: " + totalTime.toFixed(0) + "ms";
|
|
else
|
|
msg = "total test run time: " + (totalTime / TDev.RT.Perf.unit()).toFixed(3) + " - " + totalTime.toFixed(0) + "ms";
|
|
displayResults(results, isBeta, msg);
|
|
return;
|
|
}
|
|
|
|
var id = scriptsToRun[idx];
|
|
ProgressOverlay.setProgress("script " + id + " (" + (idx + Benchmarker.getNumScripts() + 1).toString()
|
|
+ " of " + (scriptsToRun.length + Benchmarker.getNumScripts()).toString() + ")");
|
|
idx++;
|
|
|
|
var getScript = (s) => {
|
|
if (!s) s = id;
|
|
var r = scriptCache[s];
|
|
if (r) return Promise.as(r);
|
|
return ScriptCache.getScriptAsync(s).then((text) =>(scriptCache[s] = text));
|
|
}
|
|
|
|
var start = Util.now();
|
|
AST.loadScriptAsync(getScript).done((resp: AST.LoadScriptResult) => {
|
|
if (resp.numErrors > 0 && Script.actions().every((a) => !a.isCompilerTest())) {
|
|
var res = emptyResult(id);
|
|
res.numErrors = 1;
|
|
res.name += " (parse errors)";
|
|
results.push(res);
|
|
|
|
var h = new TestHost();
|
|
h.scriptId = id;
|
|
h.exceptionHandler(new Error(resp.status));
|
|
|
|
doNext();
|
|
} else {
|
|
TDev.RT.Perf.resume(startTime);
|
|
var timeToken = TDev.RT.Perf.start(id);
|
|
runTestsAsync(id).done((res: ScriptTestResult) => {
|
|
var n = Util.now();
|
|
TDev.RT.Perf.pause(startTime);
|
|
var ellapsed = n - start;
|
|
res.totalTime = TDev.RT.Perf.stop(timeToken);
|
|
res.normalizedTotalTime = res.totalTime / TDev.RT.Perf.unit();
|
|
if (res.downloadError) {
|
|
if (--tryAgain == 0) {
|
|
ProgressOverlay.hide();
|
|
HTML.showProgressNotification(lf("Failed to perform tests. Are you online?"));
|
|
return;
|
|
}
|
|
--idx;
|
|
doNext();
|
|
return;
|
|
}
|
|
results.push(res);
|
|
if (n - lastBreak > 500) {
|
|
lastBreak = n;
|
|
Util.setTimeout(1, doNext);
|
|
} else {
|
|
doNext();
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
var displayError = (err: any) => {
|
|
ProgressOverlay.hide();
|
|
numTests = 0;
|
|
HTML.showProgressNotification(lf("Failed to perform tests. Are you online?"));
|
|
};
|
|
|
|
var runUnitBench = (TDev.RT.Perf.unit() < 0) ? true : false;
|
|
Benchmarker.runUnitBenchmarksAsync(/*testOnly=*/!runUnitBench,/*updateOverlay=*/true).then(() => {
|
|
return Benchmarker.runTDBenchmarksAsync(/*test=*/true,/*update=*/true);
|
|
}).then(() => {
|
|
return Promise.join(scriptsToRun.map((id) =>
|
|
ScriptCache.getScriptAsync(id).then((text) => {
|
|
scriptCache[id] = text;
|
|
})));
|
|
}).done(doNext, displayError);
|
|
|
|
})
|
|
}
|
|
|
|
if (testsFromTag)
|
|
fetch(null)
|
|
else {
|
|
scriptsToRun = testScripts;
|
|
finish();
|
|
}
|
|
});
|
|
}
|
|
|
|
export function runBetaTests() {
|
|
return; // note: we don't have enough tests and it is currently breaking down the tutorial exprience
|
|
|
|
if (!Cloud.getAccessToken() || // the login will interupt the beta tests anyways
|
|
dbg) // don't run tests for dbg users, as those do not represent general population anyway
|
|
return;
|
|
|
|
var betaFriendlyId = (<any>window).betaFriendlyId;
|
|
if (!Browser.isWP8app
|
|
&& betaFriendlyId
|
|
&& window.localStorage["betaTestsRunFor"] != betaFriendlyId) {
|
|
window.localStorage["betaTestsRunFor"] = betaFriendlyId;
|
|
testAllScripts(true);
|
|
}
|
|
}
|
|
|
|
export function dumpCurrentScript()
|
|
{
|
|
var s = new CopyRenderer().dispatch(TDev.Script);
|
|
Browser.setInnerHTML(elt("root"), CopyRenderer.css + s);
|
|
}
|
|
}
|