This repository has been archived by the owner on Aug 6, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from sharelatex/sk-time-async-method
Time-async-method
- Loading branch information
Showing
7 changed files
with
209 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
node_modules | ||
test/unit/js/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
6.9 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
module.exports = (grunt) -> | ||
grunt.initConfig | ||
coffee: | ||
unit_tests: | ||
expand: true | ||
cwd: "test/unit/coffee" | ||
src: ["**/*.coffee"] | ||
dest: "test/unit/js/" | ||
ext: ".js" | ||
|
||
clean: | ||
unit_tests: ["test/unit/js"] | ||
|
||
mochaTest: | ||
unit: | ||
options: | ||
reporter: grunt.option('reporter') or 'spec' | ||
grep: grunt.option("grep") | ||
|
||
src: ["test/unit/js/**/*.js"] | ||
|
||
grunt.loadNpmTasks 'grunt-contrib-coffee' | ||
grunt.loadNpmTasks 'grunt-contrib-clean' | ||
grunt.loadNpmTasks 'grunt-mocha-test' | ||
grunt.loadNpmTasks 'grunt-execute' | ||
grunt.loadNpmTasks 'grunt-bunyan' | ||
|
||
grunt.registerTask 'compile:unit_tests', ['clean:unit_tests', 'coffee:unit_tests'] | ||
grunt.registerTask 'test:unit', ['compile:unit_tests', 'mochaTest:unit'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
require('coffee-script') | ||
chai = require('chai') | ||
should = chai.should() | ||
expect = chai.expect | ||
path = require('path') | ||
modulePath = path.join __dirname, '../../../timeAsyncMethod.coffee' | ||
SandboxedModule = require('sandboxed-module') | ||
sinon = require("sinon") | ||
|
||
|
||
describe 'timeAsyncMethod', -> | ||
|
||
beforeEach -> | ||
@Timer = {done: sinon.stub()} | ||
@TimerConstructor = sinon.stub().returns(@Timer) | ||
@metrics = { | ||
Timer: @TimerConstructor | ||
inc: sinon.stub() | ||
} | ||
@timeAsyncMethod = SandboxedModule.require modulePath, requires: | ||
'./metrics': @metrics | ||
|
||
@testObject = { | ||
nextNumber: (n, callback=(err, result)->) -> | ||
setTimeout( | ||
() -> | ||
callback(null, n+1) | ||
, 100 | ||
) | ||
} | ||
|
||
it 'should have the testObject behave correctly before wrapping', (done) -> | ||
@testObject.nextNumber 2, (err, result) -> | ||
expect(err).to.not.exist | ||
expect(result).to.equal 3 | ||
done() | ||
|
||
it 'should wrap method without error', (done) -> | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject' | ||
done() | ||
|
||
it 'should transparently wrap method invocation in timer', (done) -> | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject' | ||
@testObject.nextNumber 2, (err, result) => | ||
expect(err).to.not.exist | ||
expect(result).to.equal 3 | ||
expect(@TimerConstructor.callCount).to.equal 1 | ||
expect(@Timer.done.callCount).to.equal 1 | ||
done() | ||
|
||
it 'should increment success count', (done) -> | ||
@metrics.inc = sinon.stub() | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject' | ||
@testObject.nextNumber 2, (err, result) => | ||
expect(@metrics.inc.callCount).to.equal 1 | ||
expect(@metrics.inc.calledWith('someContext.TestObject.nextNumber.success')).to.equal true | ||
done() | ||
|
||
describe 'when base method produces an error', -> | ||
beforeEach -> | ||
@metrics.inc = sinon.stub() | ||
@testObject.nextNumber = (n, callback=(err, result)->) -> | ||
setTimeout( | ||
() -> | ||
callback(new Error('woops')) | ||
, 100 | ||
) | ||
|
||
it 'should propagate the error transparently', (done) -> | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject' | ||
@testObject.nextNumber 2, (err, result) => | ||
expect(err).to.exist | ||
expect(err).to.be.instanceof Error | ||
expect(result).to.not.exist | ||
done() | ||
|
||
it 'should increment failure count', (done) -> | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject' | ||
@testObject.nextNumber 2, (err, result) => | ||
expect(@metrics.inc.callCount).to.equal 1 | ||
expect(@metrics.inc.calledWith('someContext.TestObject.nextNumber.failure')).to.equal true | ||
done() | ||
|
||
describe 'when a logger is supplied', -> | ||
beforeEach -> | ||
@logger = {log: sinon.stub()} | ||
|
||
it 'should also call logger.log', (done) -> | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject', @logger | ||
@testObject.nextNumber 2, (err, result) => | ||
expect(err).to.not.exist | ||
expect(result).to.equal 3 | ||
expect(@TimerConstructor.callCount).to.equal 1 | ||
expect(@Timer.done.callCount).to.equal 1 | ||
expect(@logger.log.callCount).to.equal 1 | ||
done() | ||
|
||
describe 'when the wrapper cannot be applied', -> | ||
beforeEach -> | ||
|
||
it 'should raise an error', -> | ||
badWrap = () => | ||
@timeAsyncMethod @testObject, 'DEFINITELY_NOT_A_REAL_METHOD', 'someContext.TestObject' | ||
expect(badWrap).to.throw( | ||
/^.*expected object property 'DEFINITELY_NOT_A_REAL_METHOD' to be a function.*$/ | ||
) | ||
|
||
describe 'when the wrapped function is not using a callback', -> | ||
beforeEach -> | ||
@realMethod = sinon.stub().returns(42) | ||
@testObject.nextNumber = @realMethod | ||
|
||
it 'should not throw an error', -> | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject' | ||
badCall = () => | ||
@testObject.nextNumber 2 | ||
expect(badCall).to.not.throw(Error) | ||
|
||
it 'should call the underlying method', -> | ||
@timeAsyncMethod @testObject, 'nextNumber', 'someContext.TestObject' | ||
result = @testObject.nextNumber(12) | ||
expect(@realMethod.callCount).to.equal 1 | ||
expect(@realMethod.calledWith(12)).to.equal true | ||
expect(result).to.equal 42 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
|
||
module.exports = (obj, methodName, prefix, logger) -> | ||
metrics = require('./metrics') | ||
|
||
if typeof obj[methodName] != 'function' | ||
throw new Error("[Metrics] expected object property '#{methodName}' to be a function") | ||
|
||
realMethod = obj[methodName] | ||
key = "#{prefix}.#{methodName}" | ||
|
||
obj[methodName] = (originalArgs...) -> | ||
|
||
[firstArgs..., callback] = originalArgs | ||
|
||
if !callback? || typeof callback != 'function' | ||
if logger? | ||
logger.log "[Metrics] expected wrapped method '#{methodName}' to be invoked with a callback" | ||
return realMethod.apply this, originalArgs | ||
|
||
timer = new metrics.Timer(key) | ||
|
||
realMethod.call this, firstArgs..., (callbackArgs...) -> | ||
elapsedTime = timer.done() | ||
possibleError = callbackArgs[0] | ||
if possibleError? | ||
metrics.inc "#{key}.failure" | ||
else | ||
metrics.inc "#{key}.success" | ||
if logger? | ||
loggableArgs = {} | ||
try | ||
for arg, idx in firstArgs | ||
if arg.toString().match(/^[0-9a-f]{24}$/) | ||
loggableArgs["#{idx}"] = arg | ||
logger.log {key, args: loggableArgs, elapsedTime}, "[Metrics] timed async method call" | ||
callback.apply this, callbackArgs |