Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions docs/test/unit_test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Unit tests for warlock

Unit tests will be provided over time for all major warlock functions, with continuous integration
as new commits are added. Unit tests use mocha and should.js, with all test scripts written in
coffeescript.


## Running unit tests

To run the unit tests from a newly cloned repository

```sh
npm install -g mocha
npm install
mocha
```


## Writing new unit tests
All newly contributed functions of warlock should have unit tests created. The warlock team recognise
that most open source developers would prefer to write code than to write tests, so as a general rule
the aim is to provide maximum coverage with the minimum number of test cases, and to provide
test cases that don't require ongoing maintenance when minor changes to the API or logic occur. This
will allow future contributors to minimise the amount of testing code that needs modification for any
given extension to the logic.

Test cases generally fall into two categories: synchronous cases and asynchronous cases.

Synchronous cases are easier to write. The general pattern is:

```coffeescript
'use strict'
should = require 'should'
config = require '../lib/warlock/config'

describe 'warlock config: ', ->
describe 'config: ', ->
it 'add a value to the loaded config, it\'s still there when asked for again', ->
config( 'blah.four', 'four' ).should.eql 'four'
config( 'blah.four' ).should.eql 'four'
```

By preference we use eql rather than equal, as this verifies that the content of an object
matches rather than that the instance matches, and is generally good practice.

Describe lines always end with a :, and the outermost describe should provide some indication
of the coffeescript file that is being tested - this is to allow a future developer to quickly
ascertain where they can find a given test that is failing.

It lines should describe what it is that you're testing and what the expected result is, again
so that a future developer can quickly understand what a failing test was intending to prove.

Asynchronous cases leverage mocha's ability to accept a "done()" callback once asynchronous processing
is complete. This requires both ( done ) on the 'it' line and an explicit invocation of done() at
the end of the test case.

Unfortunately at time of writing there was an outstanding issue with mocha and should where failed
expectations in an asynchronous test case result in a timeout rather than an error message - refer:
https://github.com/visionmedia/mocha/pull/278
https://github.com/visionmedia/mocha/pull/985

This can be worked around by enclosing the expectations in a try-catch block, with the catch propogating
the exception to the done call.

The basic pattern is therefore:

```coffeescript
describe 'warlock config: ', ->
describe 'config: ', ->
it 'get values from config that has been loaded', ( done ) ->
bootstrap._readConfig 'test/fixtures/config/user.json', 'test/fixtures/config/package.json'
.then ->
try
config( 'blah.one' ).should.eql true

done()
catch err
done( err )
```

64 changes: 48 additions & 16 deletions lib/warlock/bootstrap.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,57 @@ PATH = require 'path'
# ngbo libraries
warlock = require './../warlock'

module.exports = ( options ) ->

bootstrap = module.exports = ( options ) ->
warlock.debug.log "Bootstrapping warlock..."
# Store the options provided for system-wide access
warlock.options.init options

configPath = FINDUP warlock.options( 'configPath' ), { cwd: process.cwd() }
pkgPath = FINDUP "package.json", { cwd: process.cwd() }

bootstrap._readConfig( configPath, pkgPath )
.then () ->
bootstrap._loadPlugins
.then () ->
bootstrap._setDefaultTask

warlock.util.q true



###
# Find the warlock configuration and package.json, using defaults as necessary,
# read them in, and initialise the configuration using them
###
bootstrap._readConfig = ( configPath, pkgPath, promise ) ->
# Locate the configuration file and save its path for later use
configPath = FINDUP warlock.options( 'configPath' ), { cwd: process.cwd() }
promise = warlock.util.q "{}"

if not configPath?
warlock.log.warning "No config file found. Using empty configuration instead."
else
warlock.debug.log "Config found: #{configPath}"
promise = warlock.file.readFile( configPath )

promise
return promise
.then ( file ) ->
# Save the config file location for later use
warlock.options 'configFile', configPath || 'warlock.json'

# TODO (pml): is it always true that the projectPath is the dir part of configPath?
warlock.options 'projectPath', PATH.dirname( configPath )

# Parse its contents
warlock.util.parseJson file
, ( err ) ->
warlock.fatal "Could not read config file: #{err.toString()}."
.then ( config ) ->
warlock.debug.log "Config loaded."
warlock.debug.log "Config loaded, content was:\n" + JSON.stringify( config )
# Load the config into warlock
warlock.config.init config

# Read in the package.json. It's required.
pkgPath = FINDUP "package.json", { cwd: process.cwd() }
warlock.file.readFile( pkgPath )
, ( err ) ->
warlock.fatal "Could not parse config file: #{err.toString()}"
Expand All @@ -48,11 +68,19 @@ module.exports = ( options ) ->
.then ( pkg ) ->
warlock.debug.log "package.json loaded to `pkg`."
warlock.config "pkg", pkg

# Load all the plugins
warlock.plugins.load()
, ( err ) ->
warlock.fatal "Could not parse package.json: #{err.toString()}"



###
# Load all plugins
###
bootstrap._loadPlugins = () ->
# Load all the plugins
warlock.debug.log "commencing loading plugins"

warlock.plugins.load()
.then () ->
warlock.flow.validate()
flows = warlock.flow.all()
Expand Down Expand Up @@ -92,14 +120,18 @@ module.exports = ( options ) ->
warlock.util.mout.object.forOwn metaTasks, ( deps, task ) ->
warlock.task.add task, deps

warlock.debug.log "plugins loaded"

warlock.util.q true
.then () ->
# Ensure we have a default task defined, if none has been specified elsewhere
tasks = warlock.config( "default" )
if tasks? and tasks?.length
warlock.task.add "default", tasks
else
warlock.log.warning "There is no default task defined. This is usually handed by plugins or spells."

warlock.util.q true

bootstrap._setDefaultTask = () ->
# Set the default task(s)
tasks = warlock.config( "default" )

if tasks? and tasks?.length
warlock.task.add "default", tasks
else
warlock.log.warning "There is no default task defined. This is usually handed by plugins or spells."


22 changes: 17 additions & 5 deletions lib/warlock/config.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,14 @@ _defaultConfig =
plugins: []

# Tasks to prevent from running.
prevent: [ 'something1' ]
prevent: []

# Tasks to inject into one of the flows.
inject: []

# The default tasks to run when none are specified.
default: []

myval: "Hello!"

###
# The current warlock-wide configuration.
###
Expand All @@ -41,6 +39,17 @@ _config = {}
###
_userConfig = {}


###
# A multi-purpose function, allows merging config into the cached config, setting
# a key to a given value, or retrieving a value given a key
#
# key should always be provided. If val is provided, then key is set to val
# overwriting any previous value. If merge is true, then val is merged into the config,
# being added to any array or object already in the config at that key.
#
# In all instances, the value associated with the requested key is returned.
###
config = module.exports = ( key, val, merge ) ->
if key
if val
Expand All @@ -61,7 +70,10 @@ config.init = ( conf ) ->
_config = warlock.util.merge {}, _defaultConfig

###
# Process every property of an object recursively as a template.
# Process every property of an object recursively as a template, if the
# attribute is a string, and in the format <%= xxxxx %> then get the value
# of xxx from the config, and substitute it in to the resulting object.
#
# Ripped from Grunt nearly directly.
###
propStringTmplRe = /^<%=\s*([a-z0-9_$]+(?:\.[a-z0-9_$]+)*)\s*%>$/i
Expand Down Expand Up @@ -110,7 +122,7 @@ config.getRaw = ( key ) ->
if key? then MOUT.object.get( _getCombinedConfig(), key ) else _getCombinedConfig()

###
# The user's configuration, containing only those variables set of manipulated by the user. All of
# The user's configuration, containing only those variables set or manipulated by the user. All of
# these are also in _config, but this is what should be synchronized to warlock.json.
###

Expand Down
3 changes: 2 additions & 1 deletion lib/warlock/util.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ util.forEveryProperty = ( value, fn, fnContinue ) ->
obj = {}
MOUT.object.forIn value, ( val, key ) ->
obj[ key ] = util.forEveryProperty val, fn, fnContinue

return true

obj
else
# Otherwise pass value into fn and return.
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,9 @@
"q": "~0.9.7",
"rimraf": "~2.2.6",
"vinyl-fs": "0.0.2"
},
"devDependencies": {
"mocha": "^1.18.2",
"should": "^3.3.1"
}
}
62 changes: 62 additions & 0 deletions test/bootstrap.spec.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'use strict'
should = require 'should'
join = ( require 'path' ).join
options = require '../lib/warlock/options'
warlock = require '../lib/warlock'
bootstrap = require '../lib/warlock/bootstrap'
config = require '../lib/warlock/config'

require 'mocha'

beforeEach ->
options.init { 'debug': false }

describe 'warlock bootstrap: ', ->
describe 'read_config ', ->
# these tests focus on the fact that config routines are called, detailed testing of config is in the config module
it 'config, empty package.json, loaded correctly', ( done )->
bootstrap._readConfig 'test/fixtures/bootstrap/user.json', 'test/fixtures/bootstrap/empty.json'
.then ->
try
config.get( 'standard_values.one' ).should.eql false
config.get( 'standard_values.extra_1' ).should.eql 1
config.get( 'standard_values.extra_2' ).should.eql 'fred'
done()
catch err
done( err )


it 'empty config, package.json, loaded correctly', ( done ) ->
bootstrap._readConfig 'test/fixtures/bootstrap/empty.json', 'test/fixtures/bootstrap/package.json'
.then ->
try
config.get( 'pkg.standard_values.one' ).should.eql true
config.get( 'pkg.standard_values.two' ).should.eql false
config.get( 'pkg.standard_values.three' ).should.eql "three"
config.get( 'pkg.structures.a_hash' ).should.eql { "value_1": "value", "value_2": true, "value_3": 3 }
config.get( 'pkg.structures.an_array' ).should.eql ["test", 1, true, "john"]
done()
catch err
done( err )

# TODO(pml): not clear how to test exceptions when asynch - mocha doesn't have great support
# ideally we'd test a file that cannot parse, a missing file etc


describe 'loadPlugins ', ->
# these tests focus on the fact that load plugins is called, detailed testing of plugins is in the plugin module
# TODO(pml): still need to work out what this actually does so as to verify it

describe 'setDefaultTask', ->
it 'establishes default tasks', ( done ) ->
try
config.set( 'default', [ 'a_task' ] )

bootstrap._setDefaultTask()
done()
catch err
done( err )

# TODO(pml): this doesn't work - task setup needs more content
# warlock.task.getTasks().should.eql( [ 'a_task' ] )

Loading