Browse Source

SUnit tests can return promise.

No need to use #async:.

You still need to set #timeout:.
Herby Vojčík 3 years ago
parent
commit
704dd5af7f
6 changed files with 838 additions and 28 deletions
  1. 2 0
      CHANGELOG
  2. 2 0
      lang/API-CHANGES.txt
  3. 574 0
      lang/src/SUnit-Tests.js
  4. 115 0
      lang/src/SUnit-Tests.st
  5. 127 20
      lang/src/SUnit.js
  6. 18 8
      lang/src/SUnit.st

+ 2 - 0
CHANGELOG

@@ -2,6 +2,8 @@
 ===================================
 
 * Uncaught exceptions / promises now in node platform as well.
+* SUnit tests can return promises.
+  * They must explicitly send #timeout:, though.
 
 Commits: https://lolg.it/amber/amber/commits/0.29.3.
 

+ 2 - 0
lang/API-CHANGES.txt

@@ -5,6 +5,8 @@
 
 + JSObjectProxy
   + isThenable
++ TestCase >>
+  + delay:
 + TIsInGroup >>
   + isThenable
 + TThenable >>

+ 574 - 0
lang/src/SUnit-Tests.js

@@ -957,4 +957,578 @@ return self;
 $globals.SUnitAsyncTest);
 
 
+
+$core.addClass("SUnitPromiseTest", $globals.TestCase, "SUnit-Tests");
+$core.setSlots($globals.SUnitPromiseTest, ["flag"]);
+$core.addMethod(
+$core.method({
+selector: "fakeError",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeError\x0a\x09flag := 'bad'.\x0a\x09self timeout: 30.\x0a\x09flag := self delay: 20.\x0a\x09^ flag then: [ flag := 'ok'. self error: 'Intentional' ]",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "delay:", "then:", "error:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self.flag="bad";
+$self._timeout_((30));
+$self.flag=$self._delay_((20));
+return $recv($self.flag)._then_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$self.flag="ok";
+return $self._error_("Intentional");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeError",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "fakeErrorFailingInTearDown",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeErrorFailingInTearDown\x0a\x09flag := 'bad'.\x0a\x09self timeout: 30.\x0a\x09flag := self delay: 20.\x0a\x09^ flag then: [ self error: 'Intentional' ]",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "delay:", "then:", "error:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self.flag="bad";
+$self._timeout_((30));
+$self.flag=$self._delay_((20));
+return $recv($self.flag)._then_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $self._error_("Intentional");
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeErrorFailingInTearDown",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "fakeFailure",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeFailure\x0a\x09flag := 'bad'.\x0a\x09self timeout: 30.\x0a\x09flag := self delay: 20.\x0a\x09^ flag then: [ flag := 'ok'. self assert: false ]",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "delay:", "then:", "assert:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self.flag="bad";
+$self._timeout_((30));
+$self.flag=$self._delay_((20));
+return $recv($self.flag)._then_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$self.flag="ok";
+return $self._assert_(false);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeFailure",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "fakeMultipleTimeoutFailing",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeMultipleTimeoutFailing\x0a\x09self timeout: 100.\x0a\x09^ (self delay: 20) then: [ self timeout: 20. self delay: 30 ]",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "then:", "delay:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+[$self._timeout_((100))
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["timeout:"]=1
+//>>excludeEnd("ctx");
+][0];
+return $recv([$self._delay_((20))
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["delay:"]=1
+//>>excludeEnd("ctx");
+][0])._then_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$self._timeout_((20));
+return $self._delay_((30));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeMultipleTimeoutFailing",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "fakeMultipleTimeoutPassing",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeMultipleTimeoutPassing\x0a\x09self timeout: 20.\x0a\x09^ (self delay: 10) then: [ self timeout: 40. self delay: 20 ]",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "then:", "delay:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+[$self._timeout_((20))
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["timeout:"]=1
+//>>excludeEnd("ctx");
+][0];
+return $recv([$self._delay_((10))
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["delay:"]=1
+//>>excludeEnd("ctx");
+][0])._then_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$self._timeout_((40));
+return $self._delay_((20));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeMultipleTimeoutPassing",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "fakePromiseWithoutTimeout",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakePromiseWithoutTimeout\x0a\x09^ self delay: 10",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["delay:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+return $self._delay_((10));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakePromiseWithoutTimeout",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "fakeTimeout",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeTimeout\x0a\x09self timeout: 10.\x0a\x09^ self delay: 20",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "delay:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self._timeout_((10));
+return $self._delay_((20));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeTimeout",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "fakeTimeoutSendOnly",
+protocol: "helpers",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "fakeTimeoutSendOnly\x0a\x09self timeout: 10",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self._timeout_((10));
+return self;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"fakeTimeoutSendOnly",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "selectorSetOf:",
+protocol: "private",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: ["aCollection"],
+source: "selectorSetOf: aCollection\x0a\x09^ (aCollection collect: [ :each | each selector ]) asSet",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["asSet", "collect:", "selector"]
+}, function ($methodClass){ return function (aCollection){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+return $recv($recv(aCollection)._collect_((function(each){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv(each)._selector();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({each:each},$ctx1,1)});
+//>>excludeEnd("ctx");
+})))._asSet();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"selectorSetOf:",{aCollection:aCollection})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "setUp",
+protocol: "running",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "setUp\x0a\x09flag := 'ok'",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: []
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+$self.flag="ok";
+return self;
+
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "tearDown",
+protocol: "running",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "tearDown\x0a\x09self assert: 'ok' equals: flag",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["assert:equals:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self._assert_equals_("ok",$self.flag);
+return self;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"tearDown",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "testIsAsyncReturnsCorrectValues",
+protocol: "tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testIsAsyncReturnsCorrectValues\x0a\x09self deny: self isAsync.\x0a\x09self timeout: 0.\x0a\x09self assert: self isAsync.\x0a\x09\x22self finished.\x0a\x09self deny: self isAsync\x22\x0a\x09^ Promise new",
+referencedClasses: ["Promise"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["deny:", "isAsync", "timeout:", "assert:", "new"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self._deny_([$self._isAsync()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["isAsync"]=1
+//>>excludeEnd("ctx");
+][0]);
+$self._timeout_((0));
+$self._assert_($self._isAsync());
+return $recv($globals.Promise)._new();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testIsAsyncReturnsCorrectValues",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "testPass",
+protocol: "tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testPass\x0a\x09flag := 'bad'.\x0a\x09self timeout: 10.\x0a\x09flag := self delay: 5.\x0a\x09^ flag then: [ self assert: true. flag := 'ok' ]",
+referencedClasses: [],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["timeout:", "delay:", "then:", "assert:"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+$self.flag="bad";
+$self._timeout_((10));
+$self.flag=$self._delay_((5));
+return $recv($self.flag)._then_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$self._assert_(true);
+$self.flag="ok";
+return $self.flag;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testPass",{})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "testPromiseErrorsAndFailures",
+protocol: "tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testPromiseErrorsAndFailures\x0a\x09| suite runner result |\x0a\x09suite := #(fakeError fakeErrorFailingInTearDown fakeFailure testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09^ Promise new: [ :model |\x0a\x09\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: [\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result errors) equals: #(fakeError) asSet.\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result failures) equals: #(fakeErrorFailingInTearDown fakeFailure) asSet.\x0a\x09\x09\x09\x09model value: nil ] ].\x0a\x09\x09runner run ]",
+referencedClasses: ["TestSuiteRunner", "Promise", "ResultAnnouncement"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "new:", "on:do:", "announcer", "ifTrue:", "and:", "==", "=", "runs", "total", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "value:", "run"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+var suite,runner,result;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $1;
+suite=["fakeError", "fakeErrorFailingInTearDown", "fakeFailure", "testPass"]._collect_((function(each){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv($self._class())._selector_(each);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({each:each},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+runner=$recv($globals.TestSuiteRunner)._on_(suite);
+$self._timeout_((200));
+result=[$recv(runner)._result()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["result"]=1
+//>>excludeEnd("ctx");
+][0];
+return $recv($globals.Promise)._new_((function(model){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$recv($recv(runner)._announcer())._on_do_($globals.ResultAnnouncement,(function(ann){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+if($core.assert($recv($recv(ann)._result()).__eq_eq(result))){
+$1=$recv($recv(result)._runs()).__eq($recv(result)._total());
+} else {
+$1=false;
+}
+if($core.assert($1)){
+[$self._assert_equals_([$self._selectorSetOf_($recv(result)._errors())
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["selectorSetOf:"]=1
+//>>excludeEnd("ctx");
+][0],[["fakeError"]._asSet()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["asSet"]=1
+//>>excludeEnd("ctx");
+][0])
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["assert:equals:"]=1
+//>>excludeEnd("ctx");
+][0];
+$self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),["fakeErrorFailingInTearDown", "fakeFailure"]._asSet());
+return $recv(model)._value_(nil);
+}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({ann:ann},$ctx2,3)});
+//>>excludeEnd("ctx");
+}));
+return $recv(runner)._run();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({model:model},$ctx1,2)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testPromiseErrorsAndFailures",{suite:suite,runner:runner,result:result})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+$core.addMethod(
+$core.method({
+selector: "testTimeouts",
+protocol: "tests",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: [],
+source: "testTimeouts\x0a\x09| suite runner result |\x0a\x09suite := #(fakeTimeout fakeMultipleTimeoutFailing fakeMultipleTimeoutPassing fakeTimeoutSendOnly fakePromiseWithoutTimeout testPass) collect: [ :each | self class selector: each ].\x0a\x09runner := TestSuiteRunner on: suite.\x0a\x09self timeout: 200.\x0a\x09result := runner result.\x0a\x09^ Promise new: [ :model |\x0a\x09\x09runner announcer on: ResultAnnouncement do: [ :ann |\x0a\x09\x09\x09console log: ann; log: ann result runs.\x0a\x09\x09\x09(ann result == result and: [ result runs = result total ]) ifTrue: [\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result errors) equals: #() asSet.\x0a\x09\x09\x09\x09self assert: (self selectorSetOf: result failures) equals: #(fakeMultipleTimeoutFailing fakeTimeout fakeTimeoutSendOnly fakePromiseWithoutTimeout) asSet.\x0a\x09\x09\x09\x09model value: nil ] ].\x0a\x09\x09runner run ]",
+referencedClasses: ["TestSuiteRunner", "Promise", "ResultAnnouncement"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["collect:", "selector:", "class", "on:", "timeout:", "result", "new:", "on:do:", "announcer", "log:", "runs", "ifTrue:", "and:", "==", "=", "total", "assert:equals:", "selectorSetOf:", "errors", "asSet", "failures", "value:", "run"]
+}, function ($methodClass){ return function (){
+var self=this,$self=this;
+var suite,runner,result;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+var $1,$2;
+suite=["fakeTimeout", "fakeMultipleTimeoutFailing", "fakeMultipleTimeoutPassing", "fakeTimeoutSendOnly", "fakePromiseWithoutTimeout", "testPass"]._collect_((function(each){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv($self._class())._selector_(each);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({each:each},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+runner=$recv($globals.TestSuiteRunner)._on_(suite);
+$self._timeout_((200));
+result=[$recv(runner)._result()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx1.sendIdx["result"]=1
+//>>excludeEnd("ctx");
+][0];
+return $recv($globals.Promise)._new_((function(model){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+$recv($recv(runner)._announcer())._on_do_($globals.ResultAnnouncement,(function(ann){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+$1=console;
+[$recv($1)._log_(ann)
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["log:"]=1
+//>>excludeEnd("ctx");
+][0];
+$recv($1)._log_([$recv([$recv(ann)._result()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["result"]=2
+//>>excludeEnd("ctx");
+][0])._runs()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["runs"]=1
+//>>excludeEnd("ctx");
+][0]);
+if($core.assert($recv($recv(ann)._result()).__eq_eq(result))){
+$2=$recv($recv(result)._runs()).__eq($recv(result)._total());
+} else {
+$2=false;
+}
+if($core.assert($2)){
+[$self._assert_equals_([$self._selectorSetOf_($recv(result)._errors())
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["selectorSetOf:"]=1
+//>>excludeEnd("ctx");
+][0],[[]._asSet()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["asSet"]=1
+//>>excludeEnd("ctx");
+][0])
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["assert:equals:"]=1
+//>>excludeEnd("ctx");
+][0];
+$self._assert_equals_($self._selectorSetOf_($recv(result)._failures()),["fakeMultipleTimeoutFailing", "fakeTimeout", "fakeTimeoutSendOnly", "fakePromiseWithoutTimeout"]._asSet());
+return $recv(model)._value_(nil);
+}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({ann:ann},$ctx2,3)});
+//>>excludeEnd("ctx");
+}));
+return $recv(runner)._run();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({model:model},$ctx1,2)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"testTimeouts",{suite:suite,runner:runner,result:result})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.SUnitPromiseTest);
+
+
 });

+ 115 - 0
lang/src/SUnit-Tests.st

@@ -184,3 +184,118 @@ testTwoAsyncPassesWithFinishedOnlyOneIsRun
 	flag := (self async: [ self finished. flag := 'ok'. x := x+1. self assert: x equals: 1 ]) valueWithTimeout: 0.
 ! !
 
+TestCase subclass: #SUnitPromiseTest
+	slots: {#flag}
+	package: 'SUnit-Tests'!
+
+!SUnitPromiseTest methodsFor: 'helpers'!
+
+fakeError
+	flag := 'bad'.
+	self timeout: 30.
+	flag := self delay: 20.
+	^ flag then: [ flag := 'ok'. self error: 'Intentional' ]
+!
+
+fakeErrorFailingInTearDown
+	flag := 'bad'.
+	self timeout: 30.
+	flag := self delay: 20.
+	^ flag then: [ self error: 'Intentional' ]
+!
+
+fakeFailure
+	flag := 'bad'.
+	self timeout: 30.
+	flag := self delay: 20.
+	^ flag then: [ flag := 'ok'. self assert: false ]
+!
+
+fakeMultipleTimeoutFailing
+	self timeout: 100.
+	^ (self delay: 20) then: [ self timeout: 20. self delay: 30 ]
+!
+
+fakeMultipleTimeoutPassing
+	self timeout: 20.
+	^ (self delay: 10) then: [ self timeout: 40. self delay: 20 ]
+!
+
+fakePromiseWithoutTimeout
+	^ self delay: 10
+!
+
+fakeTimeout
+	self timeout: 10.
+	^ self delay: 20
+!
+
+fakeTimeoutSendOnly
+	self timeout: 10
+! !
+
+!SUnitPromiseTest methodsFor: 'private'!
+
+selectorSetOf: aCollection
+	^ (aCollection collect: [ :each | each selector ]) asSet
+! !
+
+!SUnitPromiseTest methodsFor: 'running'!
+
+setUp
+	flag := 'ok'
+!
+
+tearDown
+	self assert: 'ok' equals: flag
+! !
+
+!SUnitPromiseTest methodsFor: 'tests'!
+
+testIsAsyncReturnsCorrectValues
+	self deny: self isAsync.
+	self timeout: 0.
+	self assert: self isAsync.
+	"self finished.
+	self deny: self isAsync"
+	^ Promise new
+!
+
+testPass
+	flag := 'bad'.
+	self timeout: 10.
+	flag := self delay: 5.
+	^ flag then: [ self assert: true. flag := 'ok' ]
+!
+
+testPromiseErrorsAndFailures
+	| suite runner result |
+	suite := #(fakeError fakeErrorFailingInTearDown fakeFailure testPass) collect: [ :each | self class selector: each ].
+	runner := TestSuiteRunner on: suite.
+	self timeout: 200.
+	result := runner result.
+	^ Promise new: [ :model |
+		runner announcer on: ResultAnnouncement do: [ :ann |
+			(ann result == result and: [ result runs = result total ]) ifTrue: [
+				self assert: (self selectorSetOf: result errors) equals: #(fakeError) asSet.
+				self assert: (self selectorSetOf: result failures) equals: #(fakeErrorFailingInTearDown fakeFailure) asSet.
+				model value: nil ] ].
+		runner run ]
+!
+
+testTimeouts
+	| suite runner result |
+	suite := #(fakeTimeout fakeMultipleTimeoutFailing fakeMultipleTimeoutPassing fakeTimeoutSendOnly fakePromiseWithoutTimeout testPass) collect: [ :each | self class selector: each ].
+	runner := TestSuiteRunner on: suite.
+	self timeout: 200.
+	result := runner result.
+	^ Promise new: [ :model |
+		runner announcer on: ResultAnnouncement do: [ :ann |
+			console log: ann; log: ann result runs.
+			(ann result == result and: [ result runs = result total ]) ifTrue: [
+				self assert: (self selectorSetOf: result errors) equals: #() asSet.
+				self assert: (self selectorSetOf: result failures) equals: #(fakeMultipleTimeoutFailing fakeTimeout fakeTimeoutSendOnly fakePromiseWithoutTimeout) asSet.
+				model value: nil ] ].
+		runner run ]
+! !
+

+ 127 - 20
lang/src/SUnit.js

@@ -426,6 +426,45 @@ return $self._runCase();
 }; }),
 $globals.TestCase);
 
+$core.addMethod(
+$core.method({
+selector: "delay:",
+protocol: "async",
+//>>excludeStart("ide", pragmas.excludeIdeData);
+args: ["millis"],
+source: "delay: millis\x0a\x09^ Promise new: [ :model | [ model value: nil ] valueWithTimeout: millis ]",
+referencedClasses: ["Promise"],
+//>>excludeEnd("ide");
+pragmas: [],
+messageSends: ["new:", "valueWithTimeout:", "value:"]
+}, function ($methodClass){ return function (millis){
+var self=this,$self=this;
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx1) {
+//>>excludeEnd("ctx");
+return $recv($globals.Promise)._new_((function(model){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx2) {
+//>>excludeEnd("ctx");
+return $recv((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+return $recv(model)._value_(nil);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({},$ctx2,2)});
+//>>excludeEnd("ctx");
+}))._valueWithTimeout_(millis);
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx2) {$ctx2.fillBlock({model:model},$ctx1,1)});
+//>>excludeEnd("ctx");
+}));
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx1) {$ctx1.fill(self,"delay:",{millis:millis})});
+//>>excludeEnd("ctx");
+}; }),
+$globals.TestCase);
+
 $core.addMethod(
 $core.method({
 selector: "deny:",
@@ -482,17 +521,24 @@ selector: "finished",
 protocol: "async",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: [],
-source: "finished\x0a\x09self errorIfNotAsync: '#finished'.\x0a\x09asyncTimeout := nil",
+source: "finished\x0a\x09self errorIfNotAsync: '#finished'.\x0a\x09asyncTimeout ifNotNil: [ asyncTimeout clearTimeout ].\x0a\x09asyncTimeout := nil",
 referencedClasses: [],
 //>>excludeEnd("ide");
 pragmas: [],
-messageSends: ["errorIfNotAsync:"]
+messageSends: ["errorIfNotAsync:", "ifNotNil:", "clearTimeout"]
 }, function ($methodClass){ return function (){
 var self=this,$self=this;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 return $core.withContext(function($ctx1) {
 //>>excludeEnd("ctx");
+var $1;
 $self._errorIfNotAsync_("#finished");
+$1=$self.asyncTimeout;
+if($1 == null || $1.a$nil){
+$1;
+} else {
+$recv($self.asyncTimeout)._clearTimeout();
+}
 $self.asyncTimeout=nil;
 return self;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
@@ -530,7 +576,7 @@ selector: "performTest",
 protocol: "running",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: [],
-source: "performTest\x0a\x09asyncTimeout := nil.\x0a\x09self perform: self selector",
+source: "performTest\x0a\x09asyncTimeout := nil.\x0a\x09^ self perform: self selector",
 referencedClasses: [],
 //>>excludeEnd("ide");
 pragmas: [],
@@ -541,8 +587,7 @@ var self=this,$self=this;
 return $core.withContext(function($ctx1) {
 //>>excludeEnd("ctx");
 $self.asyncTimeout=nil;
-$self._perform_($self._selector());
-return self;
+return $self._perform_($self._selector());
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 }, function($ctx1) {$ctx1.fill(self,"performTest",{})});
 //>>excludeEnd("ctx");
@@ -1047,29 +1092,36 @@ selector: "execute:",
 protocol: "running",
 //>>excludeStart("ide", pragmas.excludeIdeData);
 args: ["aBlock"],
-source: "execute: aBlock\x0a\x09| failed |\x0a\x09\x0a\x09testCase context: self.\x0a\x09[\x0a\x09\x09failed := true.\x0a\x09\x09aBlock value.\x0a\x09\x09failed := false\x0a\x09]\x0a\x09\x09ensure: [\x0a\x09\x09\x09testCase context: nil.\x0a\x09\x09\x09\x0a\x09\x09\x09(failed and: [ testCase isAsync ]) ifTrue: [\x0a\x09\x09\x09\x09testCase finished ].\x0a\x09\x09\x09testCase isAsync ifFalse: [\x0a\x09\x09\x09\x09testCase tearDown ] ]",
+source: "execute: aBlock\x0a\x09| failed result |\x0a\x09\x0a\x09testCase context: self.\x0a\x09[\x0a\x09\x09failed := true.\x0a\x09\x09result := aBlock value.\x0a\x09\x09testCase isAsync ifFalse: [\x0a\x09\x09\x09testCase assert: result isThenable not description: testCase asString, ' returned promise without sending #timeout:' ].\x0a\x09\x09failed := false\x0a\x09]\x0a\x09\x09ensure: [\x0a\x09\x09\x09\x22testCase context: nil.\x22\x0a\x09\x09\x09\x0a\x09\x09\x09(failed and: [ testCase isAsync ]) ifTrue: [ testCase finished ].\x0a\x09\x09\x09testCase isAsync\x0a\x09\x09\x09\x09ifFalse: [ testCase tearDown ]\x0a\x09\x09\x09\x09ifTrue: [ result isThenable ifTrue: [\x0a\x09\x09\x09\x09\x09result\x0a\x09\x09\x09\x09\x09\x09then: [ testCase isAsync ifTrue: [ self execute: [ testCase finished ] ] ]\x0a\x09\x09\x09\x09\x09\x09catch: [ :error | testCase isAsync ifTrue: [ self execute: [ error signal ] ] ] ] ] ]",
 referencedClasses: [],
 //>>excludeEnd("ide");
 pragmas: [],
-messageSends: ["context:", "ensure:", "value", "ifTrue:", "and:", "isAsync", "finished", "ifFalse:", "tearDown"]
+messageSends: ["context:", "ensure:", "value", "ifFalse:", "isAsync", "assert:description:", "not", "isThenable", ",", "asString", "ifTrue:", "and:", "finished", "ifFalse:ifTrue:", "tearDown", "then:catch:", "execute:", "signal"]
 }, function ($methodClass){ return function (aBlock){
 var self=this,$self=this;
-var failed;
+var failed,result;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 return $core.withContext(function($ctx1) {
 //>>excludeEnd("ctx");
 var $1;
-[$recv($self.testCase)._context_(self)
-//>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx1.sendIdx["context:"]=1
-//>>excludeEnd("ctx");
-][0];
+$recv($self.testCase)._context_(self);
 $recv((function(){
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 return $core.withContext(function($ctx2) {
 //>>excludeEnd("ctx");
 failed=true;
-$recv(aBlock)._value();
+result=$recv(aBlock)._value();
+if(!$core.assert([$recv($self.testCase)._isAsync()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx2.sendIdx["isAsync"]=1
+//>>excludeEnd("ctx");
+][0])){
+$recv($self.testCase)._assert_description_($recv([$recv(result)._isThenable()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx2.sendIdx["isThenable"]=1
+//>>excludeEnd("ctx");
+][0])._not(),$recv($recv($self.testCase)._asString()).__comma(" returned promise without sending #timeout:"));
+}
 failed=false;
 return failed;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
@@ -1079,29 +1131,84 @@ return failed;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
 return $core.withContext(function($ctx2) {
 //>>excludeEnd("ctx");
-$recv($self.testCase)._context_(nil);
 if($core.assert(failed)){
 $1=[$recv($self.testCase)._isAsync()
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-,$ctx2.sendIdx["isAsync"]=1
+,$ctx2.sendIdx["isAsync"]=2
 //>>excludeEnd("ctx");
 ][0];
 } else {
 $1=false;
 }
 if($core.assert($1)){
-$recv($self.testCase)._finished();
+[$recv($self.testCase)._finished()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx2.sendIdx["finished"]=1
+//>>excludeEnd("ctx");
+][0];
 }
-if(!$core.assert($recv($self.testCase)._isAsync())){
+if($core.assert([$recv($self.testCase)._isAsync()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx2.sendIdx["isAsync"]=3
+//>>excludeEnd("ctx");
+][0])){
+if($core.assert($recv(result)._isThenable())){
+return $recv(result)._then_catch_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+if($core.assert([$recv($self.testCase)._isAsync()
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["isAsync"]=4
+//>>excludeEnd("ctx");
+][0])){
+return [$self._execute_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx4) {
+//>>excludeEnd("ctx");
+return $recv($self.testCase)._finished();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx4) {$ctx4.fillBlock({},$ctx3,11)});
+//>>excludeEnd("ctx");
+}))
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+,$ctx3.sendIdx["execute:"]=1
+//>>excludeEnd("ctx");
+][0];
+}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({},$ctx2,9)});
+//>>excludeEnd("ctx");
+}),(function(error){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx3) {
+//>>excludeEnd("ctx");
+if($core.assert($recv($self.testCase)._isAsync())){
+return $self._execute_((function(){
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+return $core.withContext(function($ctx4) {
+//>>excludeEnd("ctx");
+return $recv(error)._signal();
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx4) {$ctx4.fillBlock({},$ctx3,14)});
+//>>excludeEnd("ctx");
+}));
+}
+//>>excludeStart("ctx", pragmas.excludeDebugContexts);
+}, function($ctx3) {$ctx3.fillBlock({error:error},$ctx2,12)});
+//>>excludeEnd("ctx");
+}));
+}
+} else {
 return $recv($self.testCase)._tearDown();
 }
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-}, function($ctx2) {$ctx2.fillBlock({},$ctx1,2)});
+}, function($ctx2) {$ctx2.fillBlock({},$ctx1,3)});
 //>>excludeEnd("ctx");
 }));
 return self;
 //>>excludeStart("ctx", pragmas.excludeDebugContexts);
-}, function($ctx1) {$ctx1.fill(self,"execute:",{aBlock:aBlock,failed:failed})});
+}, function($ctx1) {$ctx1.fill(self,"execute:",{aBlock:aBlock,failed:failed,result:result})});
 //>>excludeEnd("ctx");
 }; }),
 $globals.TestContext);

+ 18 - 8
lang/src/SUnit.st

@@ -117,8 +117,13 @@ async: aBlock
 	^ [ self isAsync ifTrue: [ c execute: aBlock ] ]
 !
 
+delay: millis
+	^ Promise new: [ :model | [ model value: nil ] valueWithTimeout: millis ]
+!
+
 finished
 	self errorIfNotAsync: '#finished'.
+	asyncTimeout ifNotNil: [ asyncTimeout clearTimeout ].
 	asyncTimeout := nil
 !
 
@@ -159,7 +164,7 @@ debugCase
 
 performTest
 	asyncTimeout := nil.
-	self perform: self selector
+	^ self perform: self selector
 !
 
 runCase
@@ -279,21 +284,26 @@ testCase: aTestCase
 !TestContext methodsFor: 'running'!
 
 execute: aBlock
-	| failed |
+	| failed result |
 	
 	testCase context: self.
 	[
 		failed := true.
-		aBlock value.
+		result := aBlock value.
+		testCase isAsync ifFalse: [
+			testCase assert: result isThenable not description: testCase asString, ' returned promise without sending #timeout:' ].
 		failed := false
 	]
 		ensure: [
-			testCase context: nil.
+			"testCase context: nil."
 			
-			(failed and: [ testCase isAsync ]) ifTrue: [
-				testCase finished ].
-			testCase isAsync ifFalse: [
-				testCase tearDown ] ]
+			(failed and: [ testCase isAsync ]) ifTrue: [ testCase finished ].
+			testCase isAsync
+				ifFalse: [ testCase tearDown ]
+				ifTrue: [ result isThenable ifTrue: [
+					result
+						then: [ testCase isAsync ifTrue: [ self execute: [ testCase finished ] ] ]
+						catch: [ :error | testCase isAsync ifTrue: [ self execute: [ error signal ] ] ] ] ] ]
 !
 
 start