Skip to content

Commit

Permalink
Async: Implement assert.async
Browse files Browse the repository at this point in the history
Enforce test-bound start/stop calls

Fixes #534
Closes #653
  • Loading branch information
JamesMGreene committed Sep 11, 2014
1 parent 7b99db9 commit 680901a
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 106 deletions.
4 changes: 3 additions & 1 deletion Gruntfile.js
Expand Up @@ -92,7 +92,8 @@ grunt.initConfig({
},
qunit: [
"test/index.html",
"test/async.html",
"test/autostart.html",
"test/startError.html",
"test/logs.html",
"test/setTimeout.html"
]
Expand Down Expand Up @@ -204,6 +205,7 @@ grunt.registerTask( "test-on-node", function() {

require( "./test/logs" );
require( "./test/test" );
require( "./test/async" );
require( "./test/modules" );
require( "./test/deepEqual" );
require( "./test/globals" );
Expand Down
3 changes: 2 additions & 1 deletion browserstack.json
Expand Up @@ -4,7 +4,8 @@
"test_framework": "qunit",
"test_path": [
"test/index.html",
"test/async.html",
"test/autostart.html",
"test/startError.html",
"test/logs.html",
"test/setTimeout.html"
],
Expand Down
3 changes: 3 additions & 0 deletions reporter/html.js
Expand Up @@ -15,6 +15,8 @@ QUnit.init = function() {
config.autorun = false;
config.filter = "";
config.queue = [];

// DEPRECATED: QUnit.config.semaphore will be removed in QUnit 2.0.
config.semaphore = 1;

// Return on non-browser environments
Expand Down Expand Up @@ -673,6 +675,7 @@ QUnit.testDone(function( details ) {
});

if ( !defined.document || document.readyState === "complete" ) {
config.pageLoaded = true;
config.autorun = true;
}

Expand Down
19 changes: 19 additions & 0 deletions src/assert.js
Expand Up @@ -14,6 +14,25 @@ QUnit.assert = Assert.prototype = {
}
},

// Increment this Test's semaphore counter, then return a single-use function that decrements that counter a maximum of once.
async: function() {
var test = this.test,
popped = false;

test.semaphore += 1;
pauseProcessing();

return function done() {
if ( !popped ) {
test.semaphore -= 1;
popped = true;
resumeProcessing();
} else {
test.pushFailure( "Called the callback returned from `assert.async` more than once", sourceFromStacktrace( 2 ) );
}
};
},

// Exports test.push() to the user API
push: function() {
var assert = this;
Expand Down
158 changes: 99 additions & 59 deletions src/core.js
Expand Up @@ -9,6 +9,7 @@ var QUnit,
now = Date.now || function() {
return new Date().getTime();
},
globalStartCalled = false,
setTimeout = window.setTimeout,
clearTimeout = window.clearTimeout,
defined = {
Expand Down Expand Up @@ -96,6 +97,7 @@ QUnit = {
config.currentModuleTestEnvironment = testEnvironment;
},

// DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
asyncTest: function( testName, expected, callback ) {
if ( arguments.length === 2 ) {
callback = expected;
Expand Down Expand Up @@ -130,74 +132,63 @@ QUnit = {
test.queue();
},

// DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
// In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
start: function( count ) {
var message;

// QUnit hasn't been initialized yet.
// Note: RequireJS (et al) may delay onLoad
if ( config.semaphore === undefined ) {
QUnit.begin(function() {
// This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
setTimeout(function() {
QUnit.start( count );
});
});
return;
}
var globalStartAlreadyCalled = globalStartCalled;

if ( !config.current ) {
globalStartCalled = true;

if ( config.started ) {
throw new Error( "Called start() outside of a test context while already started" );
} else if ( globalStartAlreadyCalled || count > 1 ) {
throw new Error( "Called start() outside of a test context too many times" );
} else if ( config.autostart ) {
throw new Error( "Called start() outside of a test context when QUnit.config.autostart was true" );
} else if ( !config.pageLoaded ) {

// The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
config.autostart = true;
return;
}
} else {

config.semaphore -= count || 1;
// don't start until equal number of stop-calls
if ( config.semaphore > 0 ) {
return;
}
// If a test is running, adjust its semaphore
config.current.semaphore -= count || 1;

// Set the starting time when the first test is run
QUnit.config.started = QUnit.config.started || now();
// ignore if start is called more often then stop
if ( config.semaphore < 0 ) {
config.semaphore = 0;
// Don't start until equal number of stop-calls
if ( config.current.semaphore > 0 ) {
return;
}

message = "Called start() while already started (QUnit.config.semaphore was 0 already)";
// throw an Error if start is called more often than stop
if ( config.current.semaphore < 0 ) {
config.current.semaphore = 0;

if ( config.current ) {
QUnit.pushFailure( message, sourceFromStacktrace( 2 ) );
} else {
throw new Error( message );
QUnit.pushFailure(
"Called start() while already started (test's semaphore was 0 already)",
sourceFromStacktrace( 2 )
);
return;
}

return;
}
// A slight delay, to avoid any current callbacks
if ( defined.setTimeout ) {
setTimeout(function() {
if ( config.semaphore > 0 ) {
return;
}
if ( config.timeout ) {
clearTimeout( config.timeout );
}

config.blocking = false;
process( true );
}, 13 );
} else {
config.blocking = false;
process( true );
}
resumeProcessing();
},

// DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
stop: function( count ) {
config.semaphore += count || 1;
config.blocking = true;

if ( config.testTimeout && defined.setTimeout ) {
clearTimeout( config.timeout );
config.timeout = setTimeout(function() {
QUnit.ok( false, "Test timed out" );
config.semaphore = 1;
QUnit.start();
}, config.testTimeout );

// If there isn't a test running, don't allow QUnit.stop() to be called
if ( !config.current ) {
throw new Error( "Called stop() outside of a test context" );
}

// If a test is running, adjust its semaphore
config.current.semaphore += count || 1;

pauseProcessing();
}
};

Expand Down Expand Up @@ -401,6 +392,8 @@ extend( QUnit.constructor.prototype, {
});

QUnit.load = function() {
config.pageLoaded = true;

runLoggingCallbacks( "begin", {
totalTests: Test.count
});
Expand All @@ -412,14 +405,13 @@ QUnit.load = function() {
started: 0,
updateRate: 1000,
autostart: true,
filter: "",
semaphore: 1
filter: ""
}, true );

config.blocking = false;

if ( config.autostart ) {
QUnit.start();
resumeProcessing();
}
};

Expand Down Expand Up @@ -614,6 +606,54 @@ function process( last ) {
}
}

function resumeProcessing() {

// If the test run hasn't officially begun yet
if ( !config.started ) {

// Record the time of the test run's beginning
config.started = now();

// TODO: Move the "begin" logging callback to here, see Issue #659

}

// A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
if ( defined.setTimeout ) {
setTimeout(function() {
if ( config.current && config.current.semaphore > 0 ) {
return;
}
if ( config.timeout ) {
clearTimeout( config.timeout );
}

config.blocking = false;
process( true );
}, 13 );
} else {
config.blocking = false;
process( true );
}
}

function pauseProcessing() {
config.blocking = true;

if ( config.testTimeout && defined.setTimeout ) {
clearTimeout( config.timeout );
config.timeout = setTimeout(function() {
if ( config.current ) {
config.current.semaphore = 0;
QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
} else {
throw new Error( "Test timed out" );
}
resumeProcessing();
}, config.testTimeout );
}
}

function saveGlobal() {
config.pollution = [];

Expand Down
1 change: 1 addition & 0 deletions src/test.js
Expand Up @@ -3,6 +3,7 @@ function Test( settings ) {
this.assert = new Assert( this );
this.assertions = [];
this.testNumber = ++Test.count;
this.semaphore = 0;
}

Test.count = 0;
Expand Down

0 comments on commit 680901a

Please sign in to comment.