Browse Source

Merge pull request #285 from herby/async-tests

Asynchronous SUnit tests
Nicolas Petton 11 years ago
parent
commit
4b993a8d78
8 changed files with 1637 additions and 165 deletions
  1. 306 0
      js/SUnit-Tests.deploy.js
  2. 386 0
      js/SUnit-Tests.js
  3. 296 57
      js/SUnit.deploy.js
  4. 390 76
      js/SUnit.js
  5. 2 1
      js/amber.js
  6. 130 0
      st/SUnit-Tests.st
  7. 126 30
      st/SUnit.st
  8. 1 1
      test/run_build.sh

+ 306 - 0
js/SUnit-Tests.deploy.js

@@ -0,0 +1,306 @@
+smalltalk.addPackage('SUnit-Tests', {});
+smalltalk.addClass('SUnitAsyncTest', smalltalk.TestCase, ['flag'], 'SUnit-Tests');
+smalltalk.addMethod(
+"_fakeError",
+smalltalk.method({
+selector: "fakeError",
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+self["@flag"]="ok";
+self["@flag"];
+return smalltalk.send(self,"_error_",["Intentional"]);
+})]),"_valueWithTimeout_",[(5)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeErrorFailingInTearDown",
+smalltalk.method({
+selector: "fakeErrorFailingInTearDown",
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_error_",["Intentional"]);
+})]),"_valueWithTimeout_",[(5)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeFailure",
+smalltalk.method({
+selector: "fakeFailure",
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+self["@flag"]="ok";
+self["@flag"];
+return smalltalk.send(self,"_assert_",[false]);
+})]),"_valueWithTimeout_",[(5)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeMultipleGraceTimeFailing",
+smalltalk.method({
+selector: "fakeMultipleGraceTimeFailing",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_graceTime_",[(100)]);
+smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_graceTime_",[(5)]);
+return smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+})]),"_valueWithTimeout_",[(10)]);
+})]),"_valueWithTimeout_",[(5)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeMultipleGraceTimePassing",
+smalltalk.method({
+selector: "fakeMultipleGraceTimePassing",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_graceTime_",[(10)]);
+smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_graceTime_",[(20)]);
+return smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+})]),"_valueWithTimeout_",[(10)]);
+})]),"_valueWithTimeout_",[(5)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeTimeout",
+smalltalk.method({
+selector: "fakeTimeout",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_graceTime_",[(4)]);
+smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+})]),"_valueWithTimeout_",[(5)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_setUp",
+smalltalk.method({
+selector: "setUp",
+fn: function (){
+var self=this;
+self["@flag"]="ok";
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_sortedSelectors_",
+smalltalk.method({
+selector: "sortedSelectors:",
+fn: function (aCollection){
+var self=this;
+var $1;
+$1=smalltalk.send(smalltalk.send(aCollection,"_collect_",[(function(each){
+return smalltalk.send(each,"_selector",[]);
+})]),"_sorted",[]);
+return $1;
+}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_tearDown",
+smalltalk.method({
+selector: "tearDown",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_assert_equals_",["ok",self["@flag"]]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testAsyncErrorsAndFailuresWork",
+smalltalk.method({
+selector: "testAsyncErrorsAndFailuresWork",
+fn: function (){
+var self=this;
+var $1,$2;
+var suite;
+var runner;
+var result;
+var assertBlock;
+suite=smalltalk.send(["fakeError", "fakeErrorFailingInTearDown", "fakeFailure", "testPass"],"_collect_",[(function(each){
+return smalltalk.send(smalltalk.send(self,"_class",[]),"_selector_",[each]);
+})]);
+runner=smalltalk.send((smalltalk.TestSuiteRunner || TestSuiteRunner),"_on_",[suite]);
+smalltalk.send(self,"_graceTime_",[(200)]);
+result=smalltalk.send(runner,"_result",[]);
+assertBlock=smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_assert_equals_",[["fakeError"],smalltalk.send(self,"_sortedSelectors_",[smalltalk.send(result,"_errors",[])])]);
+smalltalk.send(self,"_assert_equals_",[["fakeErrorFailingInTearDown", "fakeFailure"],smalltalk.send(self,"_sortedSelectors_",[smalltalk.send(result,"_failures",[])])]);
+return smalltalk.send(self,"_finished",[]);
+})]);
+smalltalk.send(smalltalk.send(runner,"_announcer",[]),"_on_do_",[(smalltalk.ResultAnnouncement || ResultAnnouncement),(function(ann){
+$1=smalltalk.send(smalltalk.send(ann,"_result",[]),"__eq_eq",[result]);
+if(smalltalk.assert($1)){
+$2=smalltalk.send(smalltalk.send(result,"_runs",[]),"__eq",[smalltalk.send(result,"_total",[])]);
+return smalltalk.send($2,"_ifTrue_",[assertBlock]);
+};
+})]);
+smalltalk.send(runner,"_run",[]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testAsyncNeedsGraceTime",
+smalltalk.method({
+selector: "testAsyncNeedsGraceTime",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_should_raise_",[(function(){
+return smalltalk.send(self,"_async_",[(function(){
+})]);
+}),(smalltalk.Error || Error)]);
+smalltalk.send(self,"_graceTime_",[(0)]);
+smalltalk.send(self,"_shouldnt_raise_",[(function(){
+return smalltalk.send(self,"_async_",[(function(){
+})]);
+}),(smalltalk.Error || Error)]);
+smalltalk.send(self,"_finished",[]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testFinishedNeedsGraceTime",
+smalltalk.method({
+selector: "testFinishedNeedsGraceTime",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_should_raise_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+}),(smalltalk.Error || Error)]);
+smalltalk.send(self,"_graceTime_",[(0)]);
+smalltalk.send(self,"_shouldnt_raise_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+}),(smalltalk.Error || Error)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testIsAsyncReturnsCorrectValues",
+smalltalk.method({
+selector: "testIsAsyncReturnsCorrectValues",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_deny_",[smalltalk.send(self,"_isAsync",[])]);
+smalltalk.send(self,"_graceTime_",[(0)]);
+smalltalk.send(self,"_assert_",[smalltalk.send(self,"_isAsync",[])]);
+smalltalk.send(self,"_finished",[]);
+smalltalk.send(self,"_deny_",[smalltalk.send(self,"_isAsync",[])]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testPass",
+smalltalk.method({
+selector: "testPass",
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_assert_",[true]);
+smalltalk.send(self,"_finished",[]);
+self["@flag"]="ok";
+return self["@flag"];
+})]),"_valueWithTimeout_",[(5)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testTimeoutsWork",
+smalltalk.method({
+selector: "testTimeoutsWork",
+fn: function (){
+var self=this;
+var $1,$2;
+var suite;
+var runner;
+var result;
+var assertBlock;
+suite=smalltalk.send(["fakeTimeout", "fakeMultipleGraceTimeFailing", "fakeMultipleGraceTimePassing", "testPass"],"_collect_",[(function(each){
+return smalltalk.send(smalltalk.send(self,"_class",[]),"_selector_",[each]);
+})]);
+runner=smalltalk.send((smalltalk.TestSuiteRunner || TestSuiteRunner),"_on_",[suite]);
+smalltalk.send(self,"_graceTime_",[(200)]);
+result=smalltalk.send(runner,"_result",[]);
+assertBlock=smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_assert_",[smalltalk.send(smalltalk.send(result,"_errors",[]),"_isEmpty",[])]);
+smalltalk.send(self,"_assert_equals_",[["fakeMultipleGraceTimeFailing", "fakeTimeout"],smalltalk.send(self,"_sortedSelectors_",[smalltalk.send(result,"_failures",[])])]);
+return smalltalk.send(self,"_finished",[]);
+})]);
+smalltalk.send(smalltalk.send(runner,"_announcer",[]),"_on_do_",[(smalltalk.ResultAnnouncement || ResultAnnouncement),(function(ann){
+$1=smalltalk.send(smalltalk.send(ann,"_result",[]),"__eq_eq",[result]);
+if(smalltalk.assert($1)){
+$2=smalltalk.send(smalltalk.send(result,"_runs",[]),"__eq",[smalltalk.send(result,"_total",[])]);
+return smalltalk.send($2,"_ifTrue_",[assertBlock]);
+};
+})]);
+smalltalk.send(runner,"_run",[]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testTwoAsyncPassesWithFinishedOnlyOneIsRun",
+smalltalk.method({
+selector: "testTwoAsyncPassesWithFinishedOnlyOneIsRun",
+fn: function (){
+var self=this;
+var x;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+x=(0);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_finished",[]);
+self["@flag"]="ok";
+self["@flag"];
+x=smalltalk.send(x,"__plus",[(1)]);
+x;
+return smalltalk.send(self,"_assert_equals_",[(1),x]);
+})]),"_valueWithTimeout_",[(0)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_finished",[]);
+self["@flag"]="ok";
+self["@flag"];
+x=smalltalk.send(x,"__plus",[(1)]);
+x;
+return smalltalk.send(self,"_assert_equals_",[(1),x]);
+})]),"_valueWithTimeout_",[(0)]);
+return self}
+}),
+smalltalk.SUnitAsyncTest);
+
+
+

+ 386 - 0
js/SUnit-Tests.js

@@ -0,0 +1,386 @@
+smalltalk.addPackage('SUnit-Tests', {});
+smalltalk.addClass('SUnitAsyncTest', smalltalk.TestCase, ['flag'], 'SUnit-Tests');
+smalltalk.addMethod(
+"_fakeError",
+smalltalk.method({
+selector: "fakeError",
+category: 'tests',
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+self["@flag"]="ok";
+self["@flag"];
+return smalltalk.send(self,"_error_",["Intentional"]);
+})]),"_valueWithTimeout_",[(5)]);
+return self},
+args: [],
+source: "fakeError\x0a\x09flag := 'bad'.\x0a\x09self graceTime: 10.\x0a    flag := (self async: [ flag := 'ok'. self error: 'Intentional' ]) valueWithTimeout: 5\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "error:"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeErrorFailingInTearDown",
+smalltalk.method({
+selector: "fakeErrorFailingInTearDown",
+category: 'tests',
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_error_",["Intentional"]);
+})]),"_valueWithTimeout_",[(5)]);
+return self},
+args: [],
+source: "fakeErrorFailingInTearDown\x0a\x09flag := 'bad'.\x0a\x09self graceTime: 10.\x0a    flag := (self async: [ self error: 'Intentional' ]) valueWithTimeout: 5\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "error:"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeFailure",
+smalltalk.method({
+selector: "fakeFailure",
+category: 'tests',
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+self["@flag"]="ok";
+self["@flag"];
+return smalltalk.send(self,"_assert_",[false]);
+})]),"_valueWithTimeout_",[(5)]);
+return self},
+args: [],
+source: "fakeFailure\x0a\x09flag := 'bad'.\x0a\x09self graceTime: 10.\x0a    flag := (self async: [ flag := 'ok'. self assert: false ]) valueWithTimeout: 5\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "assert:"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeMultipleGraceTimeFailing",
+smalltalk.method({
+selector: "fakeMultipleGraceTimeFailing",
+category: 'tests',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_graceTime_",[(100)]);
+smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_graceTime_",[(5)]);
+return smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+})]),"_valueWithTimeout_",[(10)]);
+})]),"_valueWithTimeout_",[(5)]);
+return self},
+args: [],
+source: "fakeMultipleGraceTimeFailing\x0a\x09self graceTime: 100.\x0a    (self async: [\x0a\x09\x09self graceTime: 5.\x0a        (self async: [ self finished ]) valueWithTimeout: 10\x0a\x09]) valueWithTimeout: 5\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "finished"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeMultipleGraceTimePassing",
+smalltalk.method({
+selector: "fakeMultipleGraceTimePassing",
+category: 'tests',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_graceTime_",[(10)]);
+smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_graceTime_",[(20)]);
+return smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+})]),"_valueWithTimeout_",[(10)]);
+})]),"_valueWithTimeout_",[(5)]);
+return self},
+args: [],
+source: "fakeMultipleGraceTimePassing\x0a\x09self graceTime: 10.\x0a    (self async: [\x0a\x09\x09self graceTime: 20.\x0a        (self async: [ self finished ]) valueWithTimeout: 10\x0a\x09]) valueWithTimeout: 5\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "finished"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_fakeTimeout",
+smalltalk.method({
+selector: "fakeTimeout",
+category: 'tests',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_graceTime_",[(4)]);
+smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+})]),"_valueWithTimeout_",[(5)]);
+return self},
+args: [],
+source: "fakeTimeout\x0a\x09self graceTime: 4.\x0a    (self async: [ self finished ]) valueWithTimeout: 5\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "finished"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_setUp",
+smalltalk.method({
+selector: "setUp",
+category: 'running',
+fn: function (){
+var self=this;
+self["@flag"]="ok";
+return self},
+args: [],
+source: "setUp\x0a\x09flag := 'ok'\x0a",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_sortedSelectors_",
+smalltalk.method({
+selector: "sortedSelectors:",
+category: 'private',
+fn: function (aCollection){
+var self=this;
+var $1;
+$1=smalltalk.send(smalltalk.send(aCollection,"_collect_",[(function(each){
+return smalltalk.send(each,"_selector",[]);
+})]),"_sorted",[]);
+return $1;
+},
+args: ["aCollection"],
+source: "sortedSelectors: aCollection\x0a\x09^(aCollection collect: [:each | each selector]) sorted",
+messageSends: ["sorted", "collect:", "selector"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_tearDown",
+smalltalk.method({
+selector: "tearDown",
+category: 'running',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_assert_equals_",["ok",self["@flag"]]);
+return self},
+args: [],
+source: "tearDown\x0a\x09self assert: 'ok' equals: flag\x0a",
+messageSends: ["assert:equals:"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testAsyncErrorsAndFailuresWork",
+smalltalk.method({
+selector: "testAsyncErrorsAndFailuresWork",
+category: 'tests',
+fn: function (){
+var self=this;
+var $1,$2;
+var suite;
+var runner;
+var result;
+var assertBlock;
+suite=smalltalk.send(["fakeError", "fakeErrorFailingInTearDown", "fakeFailure", "testPass"],"_collect_",[(function(each){
+return smalltalk.send(smalltalk.send(self,"_class",[]),"_selector_",[each]);
+})]);
+runner=smalltalk.send((smalltalk.TestSuiteRunner || TestSuiteRunner),"_on_",[suite]);
+smalltalk.send(self,"_graceTime_",[(200)]);
+result=smalltalk.send(runner,"_result",[]);
+assertBlock=smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_assert_equals_",[["fakeError"],smalltalk.send(self,"_sortedSelectors_",[smalltalk.send(result,"_errors",[])])]);
+smalltalk.send(self,"_assert_equals_",[["fakeErrorFailingInTearDown", "fakeFailure"],smalltalk.send(self,"_sortedSelectors_",[smalltalk.send(result,"_failures",[])])]);
+return smalltalk.send(self,"_finished",[]);
+})]);
+smalltalk.send(smalltalk.send(runner,"_announcer",[]),"_on_do_",[(smalltalk.ResultAnnouncement || ResultAnnouncement),(function(ann){
+$1=smalltalk.send(smalltalk.send(ann,"_result",[]),"__eq_eq",[result]);
+if(smalltalk.assert($1)){
+$2=smalltalk.send(smalltalk.send(result,"_runs",[]),"__eq",[smalltalk.send(result,"_total",[])]);
+return smalltalk.send($2,"_ifTrue_",[assertBlock]);
+};
+})]);
+smalltalk.send(runner,"_run",[]);
+return self},
+args: [],
+source: "testAsyncErrorsAndFailuresWork\x0a\x09| suite runner result assertBlock |\x0a\x09suite := #('fakeError' 'fakeErrorFailingInTearDown' 'fakeFailure' 'testPass') collect: [ :each | self class selector: each ].\x0a    runner := TestSuiteRunner on: suite.\x0a    self graceTime: 200.\x0a\x09result := runner result.\x0a    assertBlock := self async: [\x0a\x09\x09self assert: #('fakeError') equals: (self sortedSelectors: result errors).\x0a\x09\x09self assert: #('fakeErrorFailingInTearDown' 'fakeFailure') equals: (self sortedSelectors: result failures).\x0a\x09\x09self finished\x0a  \x09].\x0a    runner announcer on: ResultAnnouncement do: [:ann |\x0a    \x09ann result == result  ifTrue: [ result runs = result total ifTrue: assertBlock ]].\x0a\x09runner run",
+messageSends: ["collect:", "selector:", "class", "on:", "graceTime:", "result", "async:", "assert:equals:", "sortedSelectors:", "errors", "failures", "finished", "on:do:", "ifTrue:", "=", "total", "runs", "==", "announcer", "run"],
+referencedClasses: ["TestSuiteRunner", "ResultAnnouncement"]
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testAsyncNeedsGraceTime",
+smalltalk.method({
+selector: "testAsyncNeedsGraceTime",
+category: 'tests',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_should_raise_",[(function(){
+return smalltalk.send(self,"_async_",[(function(){
+})]);
+}),(smalltalk.Error || Error)]);
+smalltalk.send(self,"_graceTime_",[(0)]);
+smalltalk.send(self,"_shouldnt_raise_",[(function(){
+return smalltalk.send(self,"_async_",[(function(){
+})]);
+}),(smalltalk.Error || Error)]);
+smalltalk.send(self,"_finished",[]);
+return self},
+args: [],
+source: "testAsyncNeedsGraceTime\x0a    self should: [ self async: [ ] ] raise: Error.\x0a    self graceTime: 0.\x0a    self shouldnt: [ self async: [ ] ] raise: Error.\x0a    self finished\x0a",
+messageSends: ["should:raise:", "async:", "graceTime:", "shouldnt:raise:", "finished"],
+referencedClasses: ["Error"]
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testFinishedNeedsGraceTime",
+smalltalk.method({
+selector: "testFinishedNeedsGraceTime",
+category: 'tests',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_should_raise_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+}),(smalltalk.Error || Error)]);
+smalltalk.send(self,"_graceTime_",[(0)]);
+smalltalk.send(self,"_shouldnt_raise_",[(function(){
+return smalltalk.send(self,"_finished",[]);
+}),(smalltalk.Error || Error)]);
+return self},
+args: [],
+source: "testFinishedNeedsGraceTime\x0a    self should: [ self finished ] raise: Error.\x0a    self graceTime: 0.\x0a    self shouldnt: [ self finished ] raise: Error.\x0a",
+messageSends: ["should:raise:", "finished", "graceTime:", "shouldnt:raise:"],
+referencedClasses: ["Error"]
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testIsAsyncReturnsCorrectValues",
+smalltalk.method({
+selector: "testIsAsyncReturnsCorrectValues",
+category: 'tests',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_deny_",[smalltalk.send(self,"_isAsync",[])]);
+smalltalk.send(self,"_graceTime_",[(0)]);
+smalltalk.send(self,"_assert_",[smalltalk.send(self,"_isAsync",[])]);
+smalltalk.send(self,"_finished",[]);
+smalltalk.send(self,"_deny_",[smalltalk.send(self,"_isAsync",[])]);
+return self},
+args: [],
+source: "testIsAsyncReturnsCorrectValues\x0a    self deny: self isAsync.\x0a    self graceTime: 0.\x0a    self assert: self isAsync.\x0a    self finished.\x0a    self deny: self isAsync\x0a",
+messageSends: ["deny:", "isAsync", "graceTime:", "assert:", "finished"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testPass",
+smalltalk.method({
+selector: "testPass",
+category: 'tests',
+fn: function (){
+var self=this;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_assert_",[true]);
+smalltalk.send(self,"_finished",[]);
+self["@flag"]="ok";
+return self["@flag"];
+})]),"_valueWithTimeout_",[(5)]);
+return self},
+args: [],
+source: "testPass\x0a\x09flag := 'bad'.\x0a\x09self graceTime: 10.\x0a    flag := (self async: [ self assert: true. self finished. flag := 'ok' ]) valueWithTimeout: 5\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "assert:", "finished"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testTimeoutsWork",
+smalltalk.method({
+selector: "testTimeoutsWork",
+category: 'tests',
+fn: function (){
+var self=this;
+var $1,$2;
+var suite;
+var runner;
+var result;
+var assertBlock;
+suite=smalltalk.send(["fakeTimeout", "fakeMultipleGraceTimeFailing", "fakeMultipleGraceTimePassing", "testPass"],"_collect_",[(function(each){
+return smalltalk.send(smalltalk.send(self,"_class",[]),"_selector_",[each]);
+})]);
+runner=smalltalk.send((smalltalk.TestSuiteRunner || TestSuiteRunner),"_on_",[suite]);
+smalltalk.send(self,"_graceTime_",[(200)]);
+result=smalltalk.send(runner,"_result",[]);
+assertBlock=smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_assert_",[smalltalk.send(smalltalk.send(result,"_errors",[]),"_isEmpty",[])]);
+smalltalk.send(self,"_assert_equals_",[["fakeMultipleGraceTimeFailing", "fakeTimeout"],smalltalk.send(self,"_sortedSelectors_",[smalltalk.send(result,"_failures",[])])]);
+return smalltalk.send(self,"_finished",[]);
+})]);
+smalltalk.send(smalltalk.send(runner,"_announcer",[]),"_on_do_",[(smalltalk.ResultAnnouncement || ResultAnnouncement),(function(ann){
+$1=smalltalk.send(smalltalk.send(ann,"_result",[]),"__eq_eq",[result]);
+if(smalltalk.assert($1)){
+$2=smalltalk.send(smalltalk.send(result,"_runs",[]),"__eq",[smalltalk.send(result,"_total",[])]);
+return smalltalk.send($2,"_ifTrue_",[assertBlock]);
+};
+})]);
+smalltalk.send(runner,"_run",[]);
+return self},
+args: [],
+source: "testTimeoutsWork\x0a\x09| suite runner result assertBlock |\x0a\x09suite := #('fakeTimeout' 'fakeMultipleGraceTimeFailing' 'fakeMultipleGraceTimePassing' 'testPass') collect: [ :each | self class selector: each ].\x0a    runner := TestSuiteRunner on: suite.\x0a    self graceTime: 200.\x0a\x09result := runner result.\x0a    assertBlock := self async: [\x0a\x09\x09self assert: result errors isEmpty.\x0a\x09\x09self assert: #('fakeMultipleGraceTimeFailing' 'fakeTimeout') equals: (self sortedSelectors: result failures).\x0a\x09\x09self finished\x0a  \x09].\x0a    runner announcer on: ResultAnnouncement do: [:ann |\x0a    \x09ann result == result  ifTrue: [ result runs = result total ifTrue: assertBlock ]].\x0a\x09runner run",
+messageSends: ["collect:", "selector:", "class", "on:", "graceTime:", "result", "async:", "assert:", "isEmpty", "errors", "assert:equals:", "sortedSelectors:", "failures", "finished", "on:do:", "ifTrue:", "=", "total", "runs", "==", "announcer", "run"],
+referencedClasses: ["TestSuiteRunner", "ResultAnnouncement"]
+}),
+smalltalk.SUnitAsyncTest);
+
+smalltalk.addMethod(
+"_testTwoAsyncPassesWithFinishedOnlyOneIsRun",
+smalltalk.method({
+selector: "testTwoAsyncPassesWithFinishedOnlyOneIsRun",
+category: 'tests',
+fn: function (){
+var self=this;
+var x;
+self["@flag"]="bad";
+smalltalk.send(self,"_graceTime_",[(10)]);
+x=(0);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_finished",[]);
+self["@flag"]="ok";
+self["@flag"];
+x=smalltalk.send(x,"__plus",[(1)]);
+x;
+return smalltalk.send(self,"_assert_equals_",[(1),x]);
+})]),"_valueWithTimeout_",[(0)]);
+self["@flag"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+smalltalk.send(self,"_finished",[]);
+self["@flag"]="ok";
+self["@flag"];
+x=smalltalk.send(x,"__plus",[(1)]);
+x;
+return smalltalk.send(self,"_assert_equals_",[(1),x]);
+})]),"_valueWithTimeout_",[(0)]);
+return self},
+args: [],
+source: "testTwoAsyncPassesWithFinishedOnlyOneIsRun\x0a\x09| x |\x0a\x09flag := 'bad'.\x0a\x09self graceTime: 10.\x0a    x := 0.\x0a    flag := (self async: [ self finished. flag := 'ok'. x := x+1. self assert: 1 equals: x ]) valueWithTimeout: 0.\x0a    flag := (self async: [ self finished. flag := 'ok'. x := x+1. self assert: 1 equals: x ]) valueWithTimeout: 0.\x0a",
+messageSends: ["graceTime:", "valueWithTimeout:", "async:", "finished", "+", "assert:equals:"],
+referencedClasses: []
+}),
+smalltalk.SUnitAsyncTest);
+
+
+

+ 296 - 57
js/SUnit.deploy.js

@@ -24,7 +24,170 @@ smalltalk.ResultAnnouncement);
 
 
 
-smalltalk.addClass('TestCase', smalltalk.Object, ['testSelector'], 'SUnit');
+smalltalk.addClass('RunningTestContext', smalltalk.Object, ['finished', 'testCase', 'result', 'step'], 'SUnit');
+smalltalk.addMethod(
+"_exception_ifNotAsync_",
+smalltalk.method({
+selector: "exception:ifNotAsync:",
+fn: function (anException,aBlock){
+var self=this;
+var $1;
+$1=smalltalk.send(self["@testCase"],"_isAsync",[]);
+if(smalltalk.assert($1)){
+self["@step"]=(function(){
+smalltalk.send(self["@testCase"],"_finished",[]);
+return smalltalk.send(anException,"_signal",[]);
+});
+self["@step"];
+} else {
+smalltalk.send(aBlock,"_value",[]);
+};
+return self}
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_execute_",
+smalltalk.method({
+selector: "execute:",
+fn: function (aBlock){
+var self=this;
+var $1,$2;
+self["@step"]=aBlock;
+smalltalk.send((function(){
+return smalltalk.send(self["@step"],"_isNil",[]);
+}),"_whileFalse_",[(function(){
+smalltalk.send(self["@testCase"],"_context_",[self]);
+return smalltalk.send((function(){
+return smalltalk.send((function(){
+return smalltalk.send((function(){
+return smalltalk.send(self["@step"],"_ensure_",[(function(){
+smalltalk.send(self["@testCase"],"_context_",[nil]);
+self["@step"]=nil;
+self["@step"];
+$1=smalltalk.send(self["@testCase"],"_isAsync",[]);
+if(! smalltalk.assert($1)){
+return smalltalk.send(self["@testCase"],"_tearDown",[]);
+};
+})]);
+}),"_on_do_",[(smalltalk.TestFailure || TestFailure),(function(ex){
+return smalltalk.send(self,"_exception_ifNotAsync_",[ex,(function(){
+return smalltalk.send(self["@result"],"_addFailure_",[self["@testCase"]]);
+})]);
+})]);
+}),"_on_do_",[(smalltalk.Error || Error),(function(ex){
+return smalltalk.send(self,"_exception_ifNotAsync_",[ex,(function(){
+return smalltalk.send(self["@result"],"_addError_",[self["@testCase"]]);
+})]);
+})]);
+}),"_ensure_",[(function(){
+$2=smalltalk.send(self["@testCase"],"_isAsync",[]);
+if(! smalltalk.assert($2)){
+smalltalk.send(self["@result"],"_increaseRuns",[]);
+return smalltalk.send(self["@finished"],"_value",[]);
+};
+})]);
+})]);
+return self}
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_finished_",
+smalltalk.method({
+selector: "finished:",
+fn: function (aBlock){
+var self=this;
+self["@finished"]=aBlock;
+return self}
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_result_",
+smalltalk.method({
+selector: "result:",
+fn: function (aTestResult){
+var self=this;
+self["@result"]=aTestResult;
+return self}
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_start",
+smalltalk.method({
+selector: "start",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_execute_",[(function(){
+smalltalk.send(self["@testCase"],"_setUp",[]);
+return smalltalk.send(self["@testCase"],"_performTest",[]);
+})]);
+return self}
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_testCase_",
+smalltalk.method({
+selector: "testCase:",
+fn: function (aTestCase){
+var self=this;
+self["@testCase"]=aTestCase;
+return self}
+}),
+smalltalk.RunningTestContext);
+
+
+smalltalk.addMethod(
+"_testCase_result_finished_",
+smalltalk.method({
+selector: "testCase:result:finished:",
+fn: function (aTestCase,aTestResult,aBlock){
+var self=this;
+var $2,$3,$1;
+$2=smalltalk.send(self,"_new",[]);
+smalltalk.send($2,"_testCase_",[aTestCase]);
+smalltalk.send($2,"_result_",[aTestResult]);
+smalltalk.send($2,"_finished_",[aBlock]);
+$3=smalltalk.send($2,"_yourself",[]);
+$1=$3;
+return $1;
+}
+}),
+smalltalk.RunningTestContext.klass);
+
+
+smalltalk.addClass('ErroringTestContext', smalltalk.RunningTestContext, [], 'SUnit');
+smalltalk.addMethod(
+"_exception_ifNotAsync_",
+smalltalk.method({
+selector: "exception:ifNotAsync:",
+fn: function (anException,aBlock){
+var self=this;
+smalltalk.send(anException,"_signal",[]);
+return self}
+}),
+smalltalk.ErroringTestContext);
+
+
+smalltalk.addMethod(
+"_testCase_",
+smalltalk.method({
+selector: "testCase:",
+fn: function (aTestCase){
+var self=this;
+var $1;
+$1=smalltalk.send(self,"_testCase_result_finished_",[aTestCase,smalltalk.send((smalltalk.TestResult || TestResult),"_new",[]),(function(){
+})]);
+return $1;
+}
+}),
+smalltalk.ErroringTestContext.klass);
+
+
+smalltalk.addClass('TestCase', smalltalk.Object, ['testSelector', 'asyncTimeout', 'context'], 'SUnit');
 smalltalk.addMethod(
 "_assert_",
 smalltalk.method({
@@ -64,6 +227,38 @@ fn: function (expected, actual) {
 }),
 smalltalk.TestCase);
 
+smalltalk.addMethod(
+"_async_",
+smalltalk.method({
+selector: "async:",
+fn: function (aBlock){
+var self=this;
+var $2,$1;
+var c;
+smalltalk.send(self,"_mustBeAsync_",["#async"]);
+c=self["@context"];
+$1=(function(){
+$2=smalltalk.send(self,"_isAsync",[]);
+if(smalltalk.assert($2)){
+return smalltalk.send(c,"_execute_",[aBlock]);
+};
+});
+return $1;
+}
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_context_",
+smalltalk.method({
+selector: "context:",
+fn: function (aRunningTestContext){
+var self=this;
+self["@context"]=aRunningTestContext;
+return self}
+}),
+smalltalk.TestCase);
+
 smalltalk.addMethod(
 "_deny_",
 smalltalk.method({
@@ -76,12 +271,72 @@ fn: function (aBoolean) {
 }),
 smalltalk.TestCase);
 
+smalltalk.addMethod(
+"_finished",
+smalltalk.method({
+selector: "finished",
+fn: function (){
+var self=this;
+smalltalk.send(self,"_mustBeAsync_",["#finished"]);
+self["@asyncTimeout"]=nil;
+return self}
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_graceTime_",
+smalltalk.method({
+selector: "graceTime:",
+fn: function (millis){
+var self=this;
+if(($receiver = self["@asyncTimeout"]) == nil || $receiver == undefined){
+self["@asyncTimeout"];
+} else {
+smalltalk.send(self["@asyncTimeout"],"_clearTimeout",[]);
+};
+self["@asyncTimeout"]=true;
+self["@asyncTimeout"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_assert_description_",[false,"SUnit grace time exhausted"]);
+})]),"_valueWithTimeout_",[millis]);
+return self}
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_isAsync",
+smalltalk.method({
+selector: "isAsync",
+fn: function (){
+var self=this;
+var $1;
+$1=smalltalk.send(self["@asyncTimeout"],"_notNil",[]);
+return $1;
+}
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_mustBeAsync_",
+smalltalk.method({
+selector: "mustBeAsync:",
+fn: function (aString){
+var self=this;
+var $1;
+$1=smalltalk.send(self,"_isAsync",[]);
+if(! smalltalk.assert($1)){
+smalltalk.send(self,"_error_",[smalltalk.send(aString,"__comma",[" used without prior #graceTime:"])]);
+};
+return self}
+}),
+smalltalk.TestCase);
+
 smalltalk.addMethod(
 "_performTest",
 smalltalk.method({
 selector: "performTest",
 fn: function (){
 var self=this;
+self["@asyncTimeout"]=nil;
 smalltalk.send(self,"_perform_",[smalltalk.send(self,"_selector",[])]);
 return self}
 }),
@@ -93,12 +348,7 @@ smalltalk.method({
 selector: "runCase",
 fn: function (){
 var self=this;
-smalltalk.send((function(){
-smalltalk.send(self,"_setUp",[]);
-return smalltalk.send(self,"_performTest",[]);
-}),"_ensure_",[(function(){
-return smalltalk.send(self,"_tearDown",[]);
-})]);
+smalltalk.send(smalltalk.send((smalltalk.ErroringTestContext || ErroringTestContext),"_testCase_",[self]),"_start",[]);
 return self}
 }),
 smalltalk.TestCase);
@@ -377,42 +627,6 @@ fn: function () {
 }),
 smalltalk.TestResult);
 
-smalltalk.addMethod(
-"_nextRunDo_",
-smalltalk.method({
-selector: "nextRunDo:",
-fn: function (aBlock){
-var self=this;
-var $2,$1;
-$2=smalltalk.send(smalltalk.send(self,"_runs",[]),"__eq_eq",[smalltalk.send(self,"_total",[])]);
-if(! smalltalk.assert($2)){
-$1=smalltalk.send(aBlock,"_value_",[smalltalk.send(smalltalk.send(self,"_runs",[]),"__plus",[(1)])]);
-};
-return $1;
-}
-}),
-smalltalk.TestResult);
-
-smalltalk.addMethod(
-"_runCase_",
-smalltalk.method({
-selector: "runCase:",
-fn: function (aTestCase){
-var self=this;
-smalltalk.send((function(){
-return smalltalk.send((function(){
-smalltalk.send(self,"_increaseRuns",[]);
-return smalltalk.send(aTestCase,"_runCase",[]);
-}),"_on_do_",[(smalltalk.TestFailure || TestFailure),(function(ex){
-return smalltalk.send(self,"_addFailure_",[aTestCase]);
-})]);
-}),"_on_do_",[(smalltalk.Error || Error),(function(ex){
-return smalltalk.send(self,"_addError_",[aTestCase]);
-})]);
-return self}
-}),
-smalltalk.TestResult);
-
 smalltalk.addMethod(
 "_runs",
 smalltalk.method({
@@ -483,7 +697,7 @@ smalltalk.TestResult);
 
 
 
-smalltalk.addClass('TestSuiteRunner', smalltalk.Object, ['suite', 'result', 'announcer'], 'SUnit');
+smalltalk.addClass('TestSuiteRunner', smalltalk.Object, ['suite', 'result', 'announcer', 'runNextTest'], 'SUnit');
 smalltalk.addMethod(
 "_announcer",
 smalltalk.method({
@@ -495,15 +709,40 @@ return self["@announcer"];
 }),
 smalltalk.TestSuiteRunner);
 
+smalltalk.addMethod(
+"_contextOf_",
+smalltalk.method({
+selector: "contextOf:",
+fn: function (anInteger){
+var self=this;
+var $1;
+$1=smalltalk.send((smalltalk.RunningTestContext || RunningTestContext),"_testCase_result_finished_",[smalltalk.send(self["@suite"],"_at_",[anInteger]),self["@result"],(function(){
+return smalltalk.send(self,"_resume",[]);
+})]);
+return $1;
+}
+}),
+smalltalk.TestSuiteRunner);
+
 smalltalk.addMethod(
 "_initialize",
 smalltalk.method({
 selector: "initialize",
 fn: function (){
 var self=this;
+var $1;
 smalltalk.send(self,"_initialize",[],smalltalk.Object);
 self["@announcer"]=smalltalk.send((smalltalk.Announcer || Announcer),"_new",[]);
 self["@result"]=smalltalk.send((smalltalk.TestResult || TestResult),"_new",[]);
+self["@runNextTest"]=(function(){
+var runs;
+runs=smalltalk.send(self["@result"],"_runs",[]);
+runs;
+$1=smalltalk.send(runs,"__lt",[smalltalk.send(self["@result"],"_total",[])]);
+if(smalltalk.assert($1)){
+return smalltalk.send(smalltalk.send(self,"_contextOf_",[smalltalk.send(runs,"__plus",[(1)])]),"_start",[]);
+};
+});
 return self}
 }),
 smalltalk.TestSuiteRunner);
@@ -519,26 +758,26 @@ return self["@result"];
 }),
 smalltalk.TestSuiteRunner);
 
+smalltalk.addMethod(
+"_resume",
+smalltalk.method({
+selector: "resume",
+fn: function (){
+var self=this;
+smalltalk.send(self["@runNextTest"],"_fork",[]);
+smalltalk.send(self["@announcer"],"_announce_",[smalltalk.send(smalltalk.send((smalltalk.ResultAnnouncement || ResultAnnouncement),"_new",[]),"_result_",[self["@result"]])]);
+return self}
+}),
+smalltalk.TestSuiteRunner);
+
 smalltalk.addMethod(
 "_run",
 smalltalk.method({
 selector: "run",
 fn: function (){
 var self=this;
-var worker;
 smalltalk.send(self["@result"],"_total_",[smalltalk.send(self["@suite"],"_size",[])]);
-smalltalk.send(self["@announcer"],"_announce_",[smalltalk.send(smalltalk.send((smalltalk.ResultAnnouncement || ResultAnnouncement),"_new",[]),"_result_",[self["@result"]])]);
-worker=(function(){
-return smalltalk.send(self["@result"],"_nextRunDo_",[(function(index){
-return smalltalk.send((function(){
-return smalltalk.send(self["@result"],"_runCase_",[smalltalk.send(self["@suite"],"_at_",[index])]);
-}),"_ensure_",[(function(){
-smalltalk.send(worker,"_fork",[]);
-return smalltalk.send(self["@announcer"],"_announce_",[smalltalk.send(smalltalk.send((smalltalk.ResultAnnouncement || ResultAnnouncement),"_new",[]),"_result_",[self["@result"]])]);
-})]);
-})]);
-});
-smalltalk.send(worker,"_fork",[]);
+smalltalk.send(self,"_resume",[]);
 return self}
 }),
 smalltalk.TestSuiteRunner);

+ 390 - 76
js/SUnit.js

@@ -34,7 +34,215 @@ smalltalk.ResultAnnouncement);
 
 
 
-smalltalk.addClass('TestCase', smalltalk.Object, ['testSelector'], 'SUnit');
+smalltalk.addClass('RunningTestContext', smalltalk.Object, ['finished', 'testCase', 'result', 'step'], 'SUnit');
+smalltalk.addMethod(
+"_exception_ifNotAsync_",
+smalltalk.method({
+selector: "exception:ifNotAsync:",
+category: 'private',
+fn: function (anException,aBlock){
+var self=this;
+var $1;
+$1=smalltalk.send(self["@testCase"],"_isAsync",[]);
+if(smalltalk.assert($1)){
+self["@step"]=(function(){
+smalltalk.send(self["@testCase"],"_finished",[]);
+return smalltalk.send(anException,"_signal",[]);
+});
+self["@step"];
+} else {
+smalltalk.send(aBlock,"_value",[]);
+};
+return self},
+args: ["anException", "aBlock"],
+source: "exception: anException ifNotAsync: aBlock\x0a\x09testCase isAsync\x0a\x09\x09ifTrue: [ step := [ testCase finished. anException signal ]]\x0a\x09\x09ifFalse: [ aBlock value ]\x0a",
+messageSends: ["ifTrue:ifFalse:", "finished", "signal", "value", "isAsync"],
+referencedClasses: []
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_execute_",
+smalltalk.method({
+selector: "execute:",
+category: 'running',
+fn: function (aBlock){
+var self=this;
+var $1,$2;
+self["@step"]=aBlock;
+smalltalk.send((function(){
+return smalltalk.send(self["@step"],"_isNil",[]);
+}),"_whileFalse_",[(function(){
+smalltalk.send(self["@testCase"],"_context_",[self]);
+return smalltalk.send((function(){
+return smalltalk.send((function(){
+return smalltalk.send((function(){
+return smalltalk.send(self["@step"],"_ensure_",[(function(){
+smalltalk.send(self["@testCase"],"_context_",[nil]);
+self["@step"]=nil;
+self["@step"];
+$1=smalltalk.send(self["@testCase"],"_isAsync",[]);
+if(! smalltalk.assert($1)){
+return smalltalk.send(self["@testCase"],"_tearDown",[]);
+};
+})]);
+}),"_on_do_",[(smalltalk.TestFailure || TestFailure),(function(ex){
+return smalltalk.send(self,"_exception_ifNotAsync_",[ex,(function(){
+return smalltalk.send(self["@result"],"_addFailure_",[self["@testCase"]]);
+})]);
+})]);
+}),"_on_do_",[(smalltalk.Error || Error),(function(ex){
+return smalltalk.send(self,"_exception_ifNotAsync_",[ex,(function(){
+return smalltalk.send(self["@result"],"_addError_",[self["@testCase"]]);
+})]);
+})]);
+}),"_ensure_",[(function(){
+$2=smalltalk.send(self["@testCase"],"_isAsync",[]);
+if(! smalltalk.assert($2)){
+smalltalk.send(self["@result"],"_increaseRuns",[]);
+return smalltalk.send(self["@finished"],"_value",[]);
+};
+})]);
+})]);
+return self},
+args: ["aBlock"],
+source: "execute: aBlock\x0a    step := aBlock.\x0a\x09[ step isNil ] whileFalse: [\x0a\x09    testCase context: self.\x0a\x09\x09[[[\x09step\x0a        \x09\x09ensure: [ testCase context: nil. step := nil. testCase isAsync ifFalse: [ testCase tearDown ]]]\x0a\x09\x09\x09\x09on: TestFailure do: [:ex | self exception: ex ifNotAsync: [ result addFailure: testCase]]]\x0a\x09\x09\x09\x09on: Error do: [:ex | self exception: ex ifNotAsync: [ result addError: testCase]]]\x0a\x09\x09\x09\x09ensure: [ testCase isAsync ifFalse: [ result increaseRuns. finished value ]]]",
+messageSends: ["whileFalse:", "context:", "ensure:", "ifFalse:", "increaseRuns", "value", "isAsync", "on:do:", "exception:ifNotAsync:", "addError:", "addFailure:", "tearDown", "isNil"],
+referencedClasses: ["Error", "TestFailure"]
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_finished_",
+smalltalk.method({
+selector: "finished:",
+category: 'accessing',
+fn: function (aBlock){
+var self=this;
+self["@finished"]=aBlock;
+return self},
+args: ["aBlock"],
+source: "finished: aBlock\x0a\x09finished := aBlock",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_result_",
+smalltalk.method({
+selector: "result:",
+category: 'accessing',
+fn: function (aTestResult){
+var self=this;
+self["@result"]=aTestResult;
+return self},
+args: ["aTestResult"],
+source: "result: aTestResult\x0a\x09result := aTestResult",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_start",
+smalltalk.method({
+selector: "start",
+category: 'running',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_execute_",[(function(){
+smalltalk.send(self["@testCase"],"_setUp",[]);
+return smalltalk.send(self["@testCase"],"_performTest",[]);
+})]);
+return self},
+args: [],
+source: "start\x0a\x09self execute: [ testCase setUp. testCase performTest ]",
+messageSends: ["execute:", "setUp", "performTest"],
+referencedClasses: []
+}),
+smalltalk.RunningTestContext);
+
+smalltalk.addMethod(
+"_testCase_",
+smalltalk.method({
+selector: "testCase:",
+category: 'accessing',
+fn: function (aTestCase){
+var self=this;
+self["@testCase"]=aTestCase;
+return self},
+args: ["aTestCase"],
+source: "testCase: aTestCase\x0a\x09testCase := aTestCase",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.RunningTestContext);
+
+
+smalltalk.addMethod(
+"_testCase_result_finished_",
+smalltalk.method({
+selector: "testCase:result:finished:",
+category: 'instance creation',
+fn: function (aTestCase,aTestResult,aBlock){
+var self=this;
+var $2,$3,$1;
+$2=smalltalk.send(self,"_new",[]);
+smalltalk.send($2,"_testCase_",[aTestCase]);
+smalltalk.send($2,"_result_",[aTestResult]);
+smalltalk.send($2,"_finished_",[aBlock]);
+$3=smalltalk.send($2,"_yourself",[]);
+$1=$3;
+return $1;
+},
+args: ["aTestCase", "aTestResult", "aBlock"],
+source: "testCase: aTestCase result: aTestResult finished: aBlock\x0a\x09^self new\x0a        testCase: aTestCase;\x0a        result: aTestResult;\x0a        finished: aBlock;\x0a        yourself",
+messageSends: ["testCase:", "new", "result:", "finished:", "yourself"],
+referencedClasses: []
+}),
+smalltalk.RunningTestContext.klass);
+
+
+smalltalk.addClass('ErroringTestContext', smalltalk.RunningTestContext, [], 'SUnit');
+smalltalk.addMethod(
+"_exception_ifNotAsync_",
+smalltalk.method({
+selector: "exception:ifNotAsync:",
+category: 'private',
+fn: function (anException,aBlock){
+var self=this;
+smalltalk.send(anException,"_signal",[]);
+return self},
+args: ["anException", "aBlock"],
+source: "exception: anException ifNotAsync: aBlock\x0a\x09anException signal",
+messageSends: ["signal"],
+referencedClasses: []
+}),
+smalltalk.ErroringTestContext);
+
+
+smalltalk.addMethod(
+"_testCase_",
+smalltalk.method({
+selector: "testCase:",
+category: 'instance creation',
+fn: function (aTestCase){
+var self=this;
+var $1;
+$1=smalltalk.send(self,"_testCase_result_finished_",[aTestCase,smalltalk.send((smalltalk.TestResult || TestResult),"_new",[]),(function(){
+})]);
+return $1;
+},
+args: ["aTestCase"],
+source: "testCase: aTestCase\x0a\x09^self\x0a        testCase: aTestCase\x0a        result: TestResult new\x0a        finished: []",
+messageSends: ["testCase:result:finished:", "new"],
+referencedClasses: ["TestResult"]
+}),
+smalltalk.ErroringTestContext.klass);
+
+
+smalltalk.addClass('TestCase', smalltalk.Object, ['testSelector', 'asyncTimeout', 'context'], 'SUnit');
 smalltalk.addMethod(
 "_assert_",
 smalltalk.method({
@@ -89,6 +297,48 @@ referencedClasses: []
 }),
 smalltalk.TestCase);
 
+smalltalk.addMethod(
+"_async_",
+smalltalk.method({
+selector: "async:",
+category: 'async',
+fn: function (aBlock){
+var self=this;
+var $2,$1;
+var c;
+smalltalk.send(self,"_mustBeAsync_",["#async"]);
+c=self["@context"];
+$1=(function(){
+$2=smalltalk.send(self,"_isAsync",[]);
+if(smalltalk.assert($2)){
+return smalltalk.send(c,"_execute_",[aBlock]);
+};
+});
+return $1;
+},
+args: ["aBlock"],
+source: "async: aBlock\x0a\x09| c |\x0a\x09self mustBeAsync: '#async'.\x0a    c := context.\x0a    ^[ self isAsync ifTrue: [ c execute: aBlock ]]",
+messageSends: ["mustBeAsync:", "ifTrue:", "execute:", "isAsync"],
+referencedClasses: []
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_context_",
+smalltalk.method({
+selector: "context:",
+category: 'accessing',
+fn: function (aRunningTestContext){
+var self=this;
+self["@context"]=aRunningTestContext;
+return self},
+args: ["aRunningTestContext"],
+source: "context: aRunningTestContext\x0a\x09context := aRunningTestContext",
+messageSends: [],
+referencedClasses: []
+}),
+smalltalk.TestCase);
+
 smalltalk.addMethod(
 "_deny_",
 smalltalk.method({
@@ -106,6 +356,85 @@ referencedClasses: []
 }),
 smalltalk.TestCase);
 
+smalltalk.addMethod(
+"_finished",
+smalltalk.method({
+selector: "finished",
+category: 'async',
+fn: function (){
+var self=this;
+smalltalk.send(self,"_mustBeAsync_",["#finished"]);
+self["@asyncTimeout"]=nil;
+return self},
+args: [],
+source: "finished\x0a\x09self mustBeAsync: '#finished'.\x0a\x09asyncTimeout := nil",
+messageSends: ["mustBeAsync:"],
+referencedClasses: []
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_graceTime_",
+smalltalk.method({
+selector: "graceTime:",
+category: 'async',
+fn: function (millis){
+var self=this;
+if(($receiver = self["@asyncTimeout"]) == nil || $receiver == undefined){
+self["@asyncTimeout"];
+} else {
+smalltalk.send(self["@asyncTimeout"],"_clearTimeout",[]);
+};
+self["@asyncTimeout"]=true;
+self["@asyncTimeout"]=smalltalk.send(smalltalk.send(self,"_async_",[(function(){
+return smalltalk.send(self,"_assert_description_",[false,"SUnit grace time exhausted"]);
+})]),"_valueWithTimeout_",[millis]);
+return self},
+args: ["millis"],
+source: "graceTime: millis\x0a\x09asyncTimeout ifNotNil: [ asyncTimeout clearTimeout ].\x0a\x09asyncTimeout := true. \x22to allow async:\x22\x0a\x09asyncTimeout :=\x0a\x09\x09(self async: [ self assert: false description: 'SUnit grace time exhausted' ])\x0a        valueWithTimeout: millis",
+messageSends: ["ifNotNil:", "clearTimeout", "valueWithTimeout:", "async:", "assert:description:"],
+referencedClasses: []
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_isAsync",
+smalltalk.method({
+selector: "isAsync",
+category: 'async',
+fn: function (){
+var self=this;
+var $1;
+$1=smalltalk.send(self["@asyncTimeout"],"_notNil",[]);
+return $1;
+},
+args: [],
+source: "isAsync\x0a\x09^asyncTimeout notNil",
+messageSends: ["notNil"],
+referencedClasses: []
+}),
+smalltalk.TestCase);
+
+smalltalk.addMethod(
+"_mustBeAsync_",
+smalltalk.method({
+selector: "mustBeAsync:",
+category: 'async',
+fn: function (aString){
+var self=this;
+var $1;
+$1=smalltalk.send(self,"_isAsync",[]);
+if(! smalltalk.assert($1)){
+smalltalk.send(self,"_error_",[smalltalk.send(aString,"__comma",[" used without prior #graceTime:"])]);
+};
+return self},
+args: ["aString"],
+source: "mustBeAsync: aString\x0a\x09self isAsync ifFalse: [ self error: aString, ' used without prior #graceTime:' ]",
+messageSends: ["ifFalse:", "error:", ",", "isAsync"],
+referencedClasses: []
+}),
+smalltalk.TestCase);
+
 smalltalk.addMethod(
 "_performTest",
 smalltalk.method({
@@ -113,10 +442,11 @@ selector: "performTest",
 category: 'running',
 fn: function (){
 var self=this;
+self["@asyncTimeout"]=nil;
 smalltalk.send(self,"_perform_",[smalltalk.send(self,"_selector",[])]);
 return self},
 args: [],
-source: "performTest\x0a\x09self perform: self selector\x0a",
+source: "performTest\x0a\x09asyncTimeout := nil.\x0a\x09self perform: self selector\x0a",
 messageSends: ["perform:", "selector"],
 referencedClasses: []
 }),
@@ -129,17 +459,12 @@ selector: "runCase",
 category: 'running',
 fn: function (){
 var self=this;
-smalltalk.send((function(){
-smalltalk.send(self,"_setUp",[]);
-return smalltalk.send(self,"_performTest",[]);
-}),"_ensure_",[(function(){
-return smalltalk.send(self,"_tearDown",[]);
-})]);
+smalltalk.send(smalltalk.send((smalltalk.ErroringTestContext || ErroringTestContext),"_testCase_",[self]),"_start",[]);
 return self},
 args: [],
-source: "runCase\x0a\x09[\x09self setUp.\x0a\x09\x09self performTest ] ensure: [\x0a\x09\x09self tearDown.\x0a\x09\x09\x22self cleanUpInstanceVariables\x22 ]\x0a",
-messageSends: ["ensure:", "tearDown", "setUp", "performTest"],
-referencedClasses: []
+source: "runCase\x0a\x09\x22Runs a test case in isolated context, leaking all errors.\x22\x0a\x0a\x09(ErroringTestContext testCase: self) start",
+messageSends: ["start", "testCase:"],
+referencedClasses: ["ErroringTestContext"]
 }),
 smalltalk.TestCase);
 
@@ -522,52 +847,6 @@ referencedClasses: ["Date", "Array"]
 }),
 smalltalk.TestResult);
 
-smalltalk.addMethod(
-"_nextRunDo_",
-smalltalk.method({
-selector: "nextRunDo:",
-category: 'running',
-fn: function (aBlock){
-var self=this;
-var $2,$1;
-$2=smalltalk.send(smalltalk.send(self,"_runs",[]),"__eq_eq",[smalltalk.send(self,"_total",[])]);
-if(! smalltalk.assert($2)){
-$1=smalltalk.send(aBlock,"_value_",[smalltalk.send(smalltalk.send(self,"_runs",[]),"__plus",[(1)])]);
-};
-return $1;
-},
-args: ["aBlock"],
-source: "nextRunDo: aBlock\x0a\x22Runs aBlock with index of next run\x0aor does nothing if no more runs\x22\x0a^self runs == self total\x0a\x09ifFalse: [ aBlock value: self runs + 1 ]",
-messageSends: ["ifFalse:", "value:", "+", "runs", "==", "total"],
-referencedClasses: []
-}),
-smalltalk.TestResult);
-
-smalltalk.addMethod(
-"_runCase_",
-smalltalk.method({
-selector: "runCase:",
-category: 'running',
-fn: function (aTestCase){
-var self=this;
-smalltalk.send((function(){
-return smalltalk.send((function(){
-smalltalk.send(self,"_increaseRuns",[]);
-return smalltalk.send(aTestCase,"_runCase",[]);
-}),"_on_do_",[(smalltalk.TestFailure || TestFailure),(function(ex){
-return smalltalk.send(self,"_addFailure_",[aTestCase]);
-})]);
-}),"_on_do_",[(smalltalk.Error || Error),(function(ex){
-return smalltalk.send(self,"_addError_",[aTestCase]);
-})]);
-return self},
-args: ["aTestCase"],
-source: "runCase: aTestCase\x0a\x09[[\x09self increaseRuns.\x0a    \x09aTestCase runCase]\x0a\x09on: TestFailure do: [:ex | self addFailure: aTestCase]]\x0a\x09on: Error do: [:ex | self addError: aTestCase]\x0a",
-messageSends: ["on:do:", "addError:", "addFailure:", "increaseRuns", "runCase"],
-referencedClasses: ["Error", "TestFailure"]
-}),
-smalltalk.TestResult);
-
 smalltalk.addMethod(
 "_runs",
 smalltalk.method({
@@ -663,7 +942,7 @@ smalltalk.TestResult);
 
 
 
-smalltalk.addClass('TestSuiteRunner', smalltalk.Object, ['suite', 'result', 'announcer'], 'SUnit');
+smalltalk.addClass('TestSuiteRunner', smalltalk.Object, ['suite', 'result', 'announcer', 'runNextTest'], 'SUnit');
 smalltalk.addMethod(
 "_announcer",
 smalltalk.method({
@@ -680,6 +959,26 @@ referencedClasses: []
 }),
 smalltalk.TestSuiteRunner);
 
+smalltalk.addMethod(
+"_contextOf_",
+smalltalk.method({
+selector: "contextOf:",
+category: 'private',
+fn: function (anInteger){
+var self=this;
+var $1;
+$1=smalltalk.send((smalltalk.RunningTestContext || RunningTestContext),"_testCase_result_finished_",[smalltalk.send(self["@suite"],"_at_",[anInteger]),self["@result"],(function(){
+return smalltalk.send(self,"_resume",[]);
+})]);
+return $1;
+},
+args: ["anInteger"],
+source: "contextOf: anInteger\x0a   \x09^RunningTestContext testCase: (suite at: anInteger) result: result finished: [ self resume ]\x0a",
+messageSends: ["testCase:result:finished:", "at:", "resume"],
+referencedClasses: ["RunningTestContext"]
+}),
+smalltalk.TestSuiteRunner);
+
 smalltalk.addMethod(
 "_initialize",
 smalltalk.method({
@@ -687,13 +986,23 @@ selector: "initialize",
 category: 'initialization',
 fn: function (){
 var self=this;
+var $1;
 smalltalk.send(self,"_initialize",[],smalltalk.Object);
 self["@announcer"]=smalltalk.send((smalltalk.Announcer || Announcer),"_new",[]);
 self["@result"]=smalltalk.send((smalltalk.TestResult || TestResult),"_new",[]);
+self["@runNextTest"]=(function(){
+var runs;
+runs=smalltalk.send(self["@result"],"_runs",[]);
+runs;
+$1=smalltalk.send(runs,"__lt",[smalltalk.send(self["@result"],"_total",[])]);
+if(smalltalk.assert($1)){
+return smalltalk.send(smalltalk.send(self,"_contextOf_",[smalltalk.send(runs,"__plus",[(1)])]),"_start",[]);
+};
+});
 return self},
 args: [],
-source: "initialize\x0a\x09super initialize.\x0a\x09announcer := Announcer new.\x0a    result := TestResult new",
-messageSends: ["initialize", "new"],
+source: "initialize\x0a\x09super initialize.\x0a\x09announcer := Announcer new.\x0a    result := TestResult new.\x0a    runNextTest := [ | runs | runs := result runs. runs < result total ifTrue: [ (self contextOf: runs + 1) start ]].\x0a",
+messageSends: ["initialize", "new", "runs", "ifTrue:", "start", "contextOf:", "+", "<", "total"],
 referencedClasses: ["Announcer", "TestResult"]
 }),
 smalltalk.TestSuiteRunner);
@@ -714,6 +1023,23 @@ referencedClasses: []
 }),
 smalltalk.TestSuiteRunner);
 
+smalltalk.addMethod(
+"_resume",
+smalltalk.method({
+selector: "resume",
+category: 'actions',
+fn: function (){
+var self=this;
+smalltalk.send(self["@runNextTest"],"_fork",[]);
+smalltalk.send(self["@announcer"],"_announce_",[smalltalk.send(smalltalk.send((smalltalk.ResultAnnouncement || ResultAnnouncement),"_new",[]),"_result_",[self["@result"]])]);
+return self},
+args: [],
+source: "resume\x0a\x09runNextTest fork.\x0a    announcer announce: (ResultAnnouncement new result: result)\x0a",
+messageSends: ["fork", "announce:", "result:", "new"],
+referencedClasses: ["ResultAnnouncement"]
+}),
+smalltalk.TestSuiteRunner);
+
 smalltalk.addMethod(
 "_run",
 smalltalk.method({
@@ -721,25 +1047,13 @@ selector: "run",
 category: 'actions',
 fn: function (){
 var self=this;
-var worker;
 smalltalk.send(self["@result"],"_total_",[smalltalk.send(self["@suite"],"_size",[])]);
-smalltalk.send(self["@announcer"],"_announce_",[smalltalk.send(smalltalk.send((smalltalk.ResultAnnouncement || ResultAnnouncement),"_new",[]),"_result_",[self["@result"]])]);
-worker=(function(){
-return smalltalk.send(self["@result"],"_nextRunDo_",[(function(index){
-return smalltalk.send((function(){
-return smalltalk.send(self["@result"],"_runCase_",[smalltalk.send(self["@suite"],"_at_",[index])]);
-}),"_ensure_",[(function(){
-smalltalk.send(worker,"_fork",[]);
-return smalltalk.send(self["@announcer"],"_announce_",[smalltalk.send(smalltalk.send((smalltalk.ResultAnnouncement || ResultAnnouncement),"_new",[]),"_result_",[self["@result"]])]);
-})]);
-})]);
-});
-smalltalk.send(worker,"_fork",[]);
+smalltalk.send(self,"_resume",[]);
 return self},
 args: [],
-source: "run\x0a\x09| worker |\x0a\x09result total: suite size.\x0a    announcer announce: (ResultAnnouncement new result: result).\x0a    worker := [ result nextRunDo: [ :index |\x0a\x09\x09[ result runCase: (suite at: index) ]\x0a\x09\x09ensure: [ worker fork.\x0a        \x09announcer announce: (ResultAnnouncement new result: result) ]]].\x0a\x09worker fork",
-messageSends: ["total:", "size", "announce:", "result:", "new", "nextRunDo:", "ensure:", "fork", "runCase:", "at:"],
-referencedClasses: ["ResultAnnouncement"]
+source: "run\x0a\x09result total: suite size.\x0a\x09self resume",
+messageSends: ["total:", "size", "resume"],
+referencedClasses: []
 }),
 smalltalk.TestSuiteRunner);
 

+ 2 - 1
js/amber.js

@@ -97,7 +97,8 @@ amber = (function() {
 				'IDE',
 				'Examples',
 				'Benchfib',
-				'Kernel-Tests'
+				'Kernel-Tests',
+                'SUnit-Tests'
 			]);
 		}
 

+ 130 - 0
st/SUnit-Tests.st

@@ -0,0 +1,130 @@
+Smalltalk current createPackage: 'SUnit-Tests' properties: #{}!
+TestCase subclass: #SUnitAsyncTest
+	instanceVariableNames: 'flag'
+	package: 'SUnit-Tests'!
+
+!SUnitAsyncTest methodsFor: 'private'!
+
+sortedSelectors: aCollection
+	^(aCollection collect: [:each | each selector]) sorted
+! !
+
+!SUnitAsyncTest methodsFor: 'running'!
+
+setUp
+	flag := 'ok'
+!
+
+tearDown
+	self assert: 'ok' equals: flag
+! !
+
+!SUnitAsyncTest methodsFor: 'tests'!
+
+fakeError
+	flag := 'bad'.
+	self graceTime: 10.
+    flag := (self async: [ flag := 'ok'. self error: 'Intentional' ]) valueWithTimeout: 5
+!
+
+fakeErrorFailingInTearDown
+	flag := 'bad'.
+	self graceTime: 10.
+    flag := (self async: [ self error: 'Intentional' ]) valueWithTimeout: 5
+!
+
+fakeFailure
+	flag := 'bad'.
+	self graceTime: 10.
+    flag := (self async: [ flag := 'ok'. self assert: false ]) valueWithTimeout: 5
+!
+
+fakeMultipleGraceTimeFailing
+	self graceTime: 100.
+    (self async: [
+		self graceTime: 5.
+        (self async: [ self finished ]) valueWithTimeout: 10
+	]) valueWithTimeout: 5
+!
+
+fakeMultipleGraceTimePassing
+	self graceTime: 10.
+    (self async: [
+		self graceTime: 20.
+        (self async: [ self finished ]) valueWithTimeout: 10
+	]) valueWithTimeout: 5
+!
+
+fakeTimeout
+	self graceTime: 4.
+    (self async: [ self finished ]) valueWithTimeout: 5
+!
+
+testAsyncErrorsAndFailuresWork
+	| suite runner result assertBlock |
+	suite := #('fakeError' 'fakeErrorFailingInTearDown' 'fakeFailure' 'testPass') collect: [ :each | self class selector: each ].
+    runner := TestSuiteRunner on: suite.
+    self graceTime: 200.
+	result := runner result.
+    assertBlock := self async: [
+		self assert: #('fakeError') equals: (self sortedSelectors: result errors).
+		self assert: #('fakeErrorFailingInTearDown' 'fakeFailure') equals: (self sortedSelectors: result failures).
+		self finished
+  	].
+    runner announcer on: ResultAnnouncement do: [:ann |
+    	ann result == result  ifTrue: [ result runs = result total ifTrue: assertBlock ]].
+	runner run
+!
+
+testAsyncNeedsGraceTime
+    self should: [ self async: [ ] ] raise: Error.
+    self graceTime: 0.
+    self shouldnt: [ self async: [ ] ] raise: Error.
+    self finished
+!
+
+testFinishedNeedsGraceTime
+    self should: [ self finished ] raise: Error.
+    self graceTime: 0.
+    self shouldnt: [ self finished ] raise: Error.
+!
+
+testIsAsyncReturnsCorrectValues
+    self deny: self isAsync.
+    self graceTime: 0.
+    self assert: self isAsync.
+    self finished.
+    self deny: self isAsync
+!
+
+testPass
+	flag := 'bad'.
+	self graceTime: 10.
+    flag := (self async: [ self assert: true. self finished. flag := 'ok' ]) valueWithTimeout: 5
+!
+
+testTimeoutsWork
+	| suite runner result assertBlock |
+	suite := #('fakeTimeout' 'fakeMultipleGraceTimeFailing' 'fakeMultipleGraceTimePassing' 'testPass') collect: [ :each | self class selector: each ].
+    runner := TestSuiteRunner on: suite.
+    self graceTime: 200.
+	result := runner result.
+    assertBlock := self async: [
+		self assert: result errors isEmpty.
+		self assert: #('fakeMultipleGraceTimeFailing' 'fakeTimeout') equals: (self sortedSelectors: result failures).
+		self finished
+  	].
+    runner announcer on: ResultAnnouncement do: [:ann |
+    	ann result == result  ifTrue: [ result runs = result total ifTrue: assertBlock ]].
+	runner run
+!
+
+testTwoAsyncPassesWithFinishedOnlyOneIsRun
+	| x |
+	flag := 'bad'.
+	self graceTime: 10.
+    x := 0.
+    flag := (self async: [ self finished. flag := 'ok'. x := x+1. self assert: 1 equals: x ]) valueWithTimeout: 0.
+    flag := (self async: [ self finished. flag := 'ok'. x := x+1. self assert: 1 equals: x ]) valueWithTimeout: 0.
+! !
+

+ 126 - 30
st/SUnit.st

@@ -13,12 +13,88 @@ result: aTestResult
 	result := aTestResult
 ! !
 
+Object subclass: #RunningTestContext
+	instanceVariableNames: 'finished testCase result step'
+	package: 'SUnit'!
+
+!RunningTestContext methodsFor: 'accessing'!
+
+finished: aBlock
+	finished := aBlock
+!
+
+result: aTestResult
+	result := aTestResult
+!
+
+testCase: aTestCase
+	testCase := aTestCase
+! !
+
+!RunningTestContext methodsFor: 'private'!
+
+exception: anException ifNotAsync: aBlock
+	testCase isAsync
+		ifTrue: [ step := [ testCase finished. anException signal ]]
+		ifFalse: [ aBlock value ]
+! !
+
+!RunningTestContext methodsFor: 'running'!
+
+execute: aBlock
+    step := aBlock.
+	[ step isNil ] whileFalse: [
+	    testCase context: self.
+		[[[	step
+        		ensure: [ testCase context: nil. step := nil. testCase isAsync ifFalse: [ testCase tearDown ]]]
+				on: TestFailure do: [:ex | self exception: ex ifNotAsync: [ result addFailure: testCase]]]
+				on: Error do: [:ex | self exception: ex ifNotAsync: [ result addError: testCase]]]
+				ensure: [ testCase isAsync ifFalse: [ result increaseRuns. finished value ]]]
+!
+
+start
+	self execute: [ testCase setUp. testCase performTest ]
+! !
+
+!RunningTestContext class methodsFor: 'instance creation'!
+
+testCase: aTestCase result: aTestResult finished: aBlock
+	^self new
+        testCase: aTestCase;
+        result: aTestResult;
+        finished: aBlock;
+        yourself
+! !
+
+RunningTestContext subclass: #ErroringTestContext
+	instanceVariableNames: ''
+	package: 'SUnit'!
+
+!ErroringTestContext methodsFor: 'private'!
+
+exception: anException ifNotAsync: aBlock
+	anException signal
+! !
+
+!ErroringTestContext class methodsFor: 'instance creation'!
+
+testCase: aTestCase
+	^self
+        testCase: aTestCase
+        result: TestResult new
+        finished: []
+! !
+
 Object subclass: #TestCase
-	instanceVariableNames: 'testSelector'
+	instanceVariableNames: 'testSelector asyncTimeout context'
 	package: 'SUnit'!
 
 !TestCase methodsFor: 'accessing'!
 
+context: aRunningTestContext
+	context := aRunningTestContext
+!
+
 selector
 	^testSelector
 !
@@ -27,6 +103,36 @@ setTestSelector: aSelector
 	testSelector := aSelector
 ! !
 
+!TestCase methodsFor: 'async'!
+
+async: aBlock
+	| c |
+	self mustBeAsync: '#async'.
+    c := context.
+    ^[ self isAsync ifTrue: [ c execute: aBlock ]]
+!
+
+finished
+	self mustBeAsync: '#finished'.
+	asyncTimeout := nil
+!
+
+graceTime: millis
+	asyncTimeout ifNotNil: [ asyncTimeout clearTimeout ].
+	asyncTimeout := true. "to allow async:"
+	asyncTimeout :=
+		(self async: [ self assert: false description: 'SUnit grace time exhausted' ])
+        valueWithTimeout: millis
+!
+
+isAsync
+	^asyncTimeout notNil
+!
+
+mustBeAsync: aString
+	self isAsync ifFalse: [ self error: aString, ' used without prior #graceTime:' ]
+! !
+
 !TestCase methodsFor: 'private'!
 
 signalFailure: aString
@@ -38,14 +144,14 @@ signalFailure: aString
 !TestCase methodsFor: 'running'!
 
 performTest
+	asyncTimeout := nil.
 	self perform: self selector
 !
 
 runCase
-	[	self setUp.
-		self performTest ] ensure: [
-		self tearDown.
-		"self cleanUpInstanceVariables" ]
+	"Runs a test case in isolated context, leaking all errors."
+
+	(ErroringTestContext testCase: self) start
 !
 
 setUp
@@ -192,24 +298,8 @@ initialize
 	total := 0
 ! !
 
-!TestResult methodsFor: 'running'!
-
-nextRunDo: aBlock
-"Runs aBlock with index of next run
-or does nothing if no more runs"
-^self runs == self total
-	ifFalse: [ aBlock value: self runs + 1 ]
-!
-
-runCase: aTestCase
-	[[	self increaseRuns.
-    	aTestCase runCase]
-	on: TestFailure do: [:ex | self addFailure: aTestCase]]
-	on: Error do: [:ex | self addError: aTestCase]
-! !
-
 Object subclass: #TestSuiteRunner
-	instanceVariableNames: 'suite result announcer'
+	instanceVariableNames: 'suite result announcer runNextTest'
 	package: 'SUnit'!
 
 !TestSuiteRunner methodsFor: 'accessing'!
@@ -228,15 +318,14 @@ suite: aCollection
 
 !TestSuiteRunner methodsFor: 'actions'!
 
+resume
+	runNextTest fork.
+    announcer announce: (ResultAnnouncement new result: result)
+!
+
 run
-	| worker |
 	result total: suite size.
-    announcer announce: (ResultAnnouncement new result: result).
-    worker := [ result nextRunDo: [ :index |
-		[ result runCase: (suite at: index) ]
-		ensure: [ worker fork.
-        	announcer announce: (ResultAnnouncement new result: result) ]]].
-	worker fork
+	self resume
 ! !
 
 !TestSuiteRunner methodsFor: 'initialization'!
@@ -244,7 +333,14 @@ run
 initialize
 	super initialize.
 	announcer := Announcer new.
-    result := TestResult new
+    result := TestResult new.
+    runNextTest := [ | runs | runs := result runs. runs < result total ifTrue: [ (self contextOf: runs + 1) start ]].
+! !
+
+!TestSuiteRunner methodsFor: 'private'!
+
+contextOf: anInteger
+   	^RunningTestContext testCase: (suite at: anInteger) result: result finished: [ self resume ]
 ! !
 
 !TestSuiteRunner class methodsFor: 'instance creation'!

+ 1 - 1
test/run_build.sh

@@ -3,7 +3,7 @@ cd `dirname ${0}`/..
 
 
 # Create the build file
-cat js/boot.js js/Kernel-Announcements.js js/Kernel-Classes.js js/Kernel-Collections.js  js/Kernel-Exceptions.js js/Kernel-Methods.js js/Kernel-Objects.js js/Kernel-Transcript.js js/Compiler-AST.js js/Compiler-Core.js js/Compiler-Exceptions.js js/Compiler-IR.js js/Compiler-Inlining.js js/Compiler-Semantic.js js/SUnit.js js/Kernel-Tests.js js/Compiler-Tests.js test/Test.js js/parser.js js/init.js > test/run.js
+cat js/boot.js js/Kernel-Announcements.js js/Kernel-Classes.js js/Kernel-Collections.js  js/Kernel-Exceptions.js js/Kernel-Methods.js js/Kernel-Objects.js js/Kernel-Transcript.js js/Compiler-AST.js js/Compiler-Core.js js/Compiler-Exceptions.js js/Compiler-IR.js js/Compiler-Inlining.js js/Compiler-Semantic.js js/SUnit.js js/Kernel-Tests.js js/Compiler-Tests.js js/SUnit-Tests.js test/Test.js js/parser.js js/init.js > test/run.js
 
 
 #run it!