From 5c002bc9b1d53f42c12dc0eaf54e8c6d81e775bd Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 15 Aug 2014 12:25:54 +0100 Subject: [PATCH] Use ShareLaTeX conventions --- services/spelling/Gruntfile.coffee | 52 +++++ services/spelling/app.coffee | 9 +- services/spelling/app/coffee/ASpell.coffee | 2 +- services/spelling/app/coffee/Cache.coffee | 35 --- .../spelling/config/settings.defaults.coffee | 14 ++ .../config/settings.development.coffee | 10 - services/spelling/package.json | 15 +- .../test/UnitTests/coffee/CacheTests.coffee | 56 ----- .../coffee/ASpellTests.coffee | 0 .../coffee/LearnedWordsManagerTests.coffee | 7 - .../coffee/SpellingAPIManagerTests.coffee | 0 services/spelling/test/unit/js/ASpellTests.js | 112 ++++++++++ .../test/unit/js/LearnedWordsManagerTests.js | 99 +++++++++ .../test/unit/js/SpellingAPIManagerTests.js | 208 ++++++++++++++++++ 14 files changed, 502 insertions(+), 117 deletions(-) create mode 100644 services/spelling/Gruntfile.coffee delete mode 100644 services/spelling/app/coffee/Cache.coffee create mode 100644 services/spelling/config/settings.defaults.coffee delete mode 100644 services/spelling/config/settings.development.coffee delete mode 100644 services/spelling/test/UnitTests/coffee/CacheTests.coffee rename services/spelling/test/{UnitTests => unit}/coffee/ASpellTests.coffee (100%) rename services/spelling/test/{UnitTests => unit}/coffee/LearnedWordsManagerTests.coffee (95%) rename services/spelling/test/{UnitTests => unit}/coffee/SpellingAPIManagerTests.coffee (100%) create mode 100644 services/spelling/test/unit/js/ASpellTests.js create mode 100644 services/spelling/test/unit/js/LearnedWordsManagerTests.js create mode 100644 services/spelling/test/unit/js/SpellingAPIManagerTests.js diff --git a/services/spelling/Gruntfile.coffee b/services/spelling/Gruntfile.coffee new file mode 100644 index 0000000000..9bf2f98681 --- /dev/null +++ b/services/spelling/Gruntfile.coffee @@ -0,0 +1,52 @@ +module.exports = (grunt) -> + grunt.initConfig + coffee: + app_src: + expand: true, + cwd: "app/coffee" + src: ['**/*.coffee'], + dest: 'app/js/', + ext: '.js' + + app: + src: "app.coffee" + dest: "app.js" + + unit_tests: + expand: true + cwd: "test/unit/coffee" + src: ["**/*.coffee"] + dest: "test/unit/js/" + ext: ".js" + + clean: + app: ["app/js/"] + unit_tests: ["test/unit/js"] + + execute: + app: + src: "app.js" + + mochaTest: + unit: + options: + reporter: grunt.option('reporter') or 'spec' + 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:app', ['clean:app', 'coffee:app', 'coffee:app_src'] + grunt.registerTask 'run', ['compile:app', 'bunyan', 'execute'] + + grunt.registerTask 'compile:unit_tests', ['clean:unit_tests', 'coffee:unit_tests'] + grunt.registerTask 'test:unit', ['compile:app', 'compile:unit_tests', 'mochaTest:unit'] + + grunt.registerTask 'install', 'compile:app' + + grunt.registerTask 'default', ['run'] + + diff --git a/services/spelling/app.coffee b/services/spelling/app.coffee index 1884a845e2..85494a40da 100644 --- a/services/spelling/app.coffee +++ b/services/spelling/app.coffee @@ -20,7 +20,8 @@ server.post "/user/:user_id/learn", SpellingAPIController.learn server.get "/status", (req, res)-> res.send(status:'spelling api is up') -host = Settings.host || "localhost" -port = Settings.port || 3005 -server.listen port, host, () -> - console.log "#{server.name} listening at #{host}:#{port}" +host = Settings.internal?.spelling?.host || "localhost" +port = Settings.internal?.spelling?.port || 3005 +server.listen port, host, (error) -> + throw error if error? + logger.log "spelling-sharelatex listening at #{host}:#{port}" diff --git a/services/spelling/app/coffee/ASpell.coffee b/services/spelling/app/coffee/ASpell.coffee index 4958351458..f64ea8286c 100644 --- a/services/spelling/app/coffee/ASpell.coffee +++ b/services/spelling/app/coffee/ASpell.coffee @@ -47,7 +47,7 @@ class ASpellRunner word = words[i] @sendWord(word) i++ - process.nextTick tick + setTimeout tick, 0 else @close() diff --git a/services/spelling/app/coffee/Cache.coffee b/services/spelling/app/coffee/Cache.coffee deleted file mode 100644 index 132254f0d3..0000000000 --- a/services/spelling/app/coffee/Cache.coffee +++ /dev/null @@ -1,35 +0,0 @@ -redis = require('redis') -settings = require('settings-sharelatex') -rclient = redis.createClient(settings.redis.port, settings.redis.host) -rclient.auth(settings.redis.password) -logger = require('logger-sharelatex') - -thirtyMinutes = (60 * 60 * 30) - -module.exports = - - break: (key, callback)-> - rclient.del buildKey(key), callback - - set :(key, value, callback)-> - value = JSON.stringify value - builtKey = buildKey(key) - multi = rclient.multi() - multi.set builtKey, value - multi.expire builtKey, thirtyMinutes - multi.exec callback - - get :(key, callback)-> - builtKey = buildKey(key) - rclient.get builtKey, (err, result)-> - return callback(err) if err? - if !result? - logger.log key:key, "cache miss" - callback() - else - result = JSON.parse result - logger.log key:key, foundId:result._id, "cache hit" - callback null, result - -buildKey = (key)-> - return "user-learned-words:#{key}" diff --git a/services/spelling/config/settings.defaults.coffee b/services/spelling/config/settings.defaults.coffee new file mode 100644 index 0000000000..f02e51ecc7 --- /dev/null +++ b/services/spelling/config/settings.defaults.coffee @@ -0,0 +1,14 @@ +module.exports = Settings = + internal: + spelling: + port: 3005 + host: "localhost" + + redis: + port:6379 + host:"127.0.0.1" + password:"" + + mongo: + url : 'mongodb://127.0.0.1/sharelatex' + diff --git a/services/spelling/config/settings.development.coffee b/services/spelling/config/settings.development.coffee deleted file mode 100644 index a3183ca0fe..0000000000 --- a/services/spelling/config/settings.development.coffee +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = Settings = - port: 3005 - host: "localhost" - redis: - port:6379 - host:"127.0.0.1" - password:"" - mongo: - url : 'mongodb://127.0.0.1/sharelatexTesting' - diff --git a/services/spelling/package.json b/services/spelling/package.json index a060972d5a..8fb64c268e 100644 --- a/services/spelling/package.json +++ b/services/spelling/package.json @@ -7,8 +7,8 @@ "express": "3.1.0", "async": "0.1.22", "restify": "2.5.1", - "settings": "git+ssh://git@bitbucket.org:sharelatex/settings-sharelatex.git#master", - "logger": "git+ssh://git@bitbucket.org:sharelatex/logger-sharelatex.git#bunyan", + "settings-sharelatex": "git+https://github.com/sharelatex/settings-sharelatex.git#master", + "logger-sharelatex": "git+https://github.com/sharelatex/logger-sharelatex.git#master", "metrics-sharelatex": "git+https://github.com/sharelatex/metrics-sharelatex.git#master", "node-statsd": "0.0.3", "underscore": "1.4.4", @@ -16,8 +16,15 @@ "redis": "~0.8.4" }, "devDependencies": { - "sinon": "", + "bunyan": "^1.0.0", "chai": "", - "sandboxed-module": "" + "grunt": "^0.4.5", + "grunt-bunyan": "^0.5.0", + "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-coffee": "^0.11.0", + "grunt-execute": "^0.2.2", + "grunt-mocha-test": "^0.11.0", + "sandboxed-module": "", + "sinon": "" } } diff --git a/services/spelling/test/UnitTests/coffee/CacheTests.coffee b/services/spelling/test/UnitTests/coffee/CacheTests.coffee deleted file mode 100644 index e4b8e71b7e..0000000000 --- a/services/spelling/test/UnitTests/coffee/CacheTests.coffee +++ /dev/null @@ -1,56 +0,0 @@ -modulePath = "../../../app/js/Cache.js" -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('chai').assert -path = require 'path' - -user_token = "23ionisou90iilkn" -spellings = ["bob", "smith", "words"] - -describe 'Cache', -> - - it 'should save the user into redis', (done)-> - @redis = - expire: (key, value)-> - key.should.equal "user-learned-words:#{user_token}" - (value > 200).should.equal true - set: (key, value)-> - key.should.equal "user-learned-words:#{user_token}" - value.should.equal JSON.stringify(spellings) - exec:-> - done() - @cache = SandboxedModule.require modulePath, requires: - 'redis': createClient :=> {multi:=> @redis} - - @cache.set user_token, spellings, -> - - it 'should get the user from redis', (done)-> - @redis = get: (key, cb)-> - key.should.equal "user-learned-words:#{user_token}" - cb(null, JSON.stringify(spellings)) - - @cache = SandboxedModule.require modulePath, requires: - 'redis': createClient :=> return @redis - - @cache.get user_token, (err, returnedSpellings)-> - assert.deepEqual returnedSpellings, spellings - assert.equal err, null - done() - - it 'should return nothing if the key doesnt exist', (done)-> - @redis = get: (key, cb)-> - cb(null, null) - @cache = SandboxedModule.require modulePath, requires: - 'redis': createClient :=> return @redis - - @cache.get user_token, (err, founduser)-> - assert.equal founduser, undefined - done() - - it 'should be able to delete from redis to break cache', (done)-> - @redis = del: (key, cb)-> - key.should.equal "user-learned-words:#{user_token}" - cb(null) - @cache = SandboxedModule.require modulePath, requires: - 'redis': createClient :=> return @redis - @cache.break user_token, done diff --git a/services/spelling/test/UnitTests/coffee/ASpellTests.coffee b/services/spelling/test/unit/coffee/ASpellTests.coffee similarity index 100% rename from services/spelling/test/UnitTests/coffee/ASpellTests.coffee rename to services/spelling/test/unit/coffee/ASpellTests.coffee diff --git a/services/spelling/test/UnitTests/coffee/LearnedWordsManagerTests.coffee b/services/spelling/test/unit/coffee/LearnedWordsManagerTests.coffee similarity index 95% rename from services/spelling/test/UnitTests/coffee/LearnedWordsManagerTests.coffee rename to services/spelling/test/unit/coffee/LearnedWordsManagerTests.coffee index 22b73c32aa..8d7d9f0b35 100644 --- a/services/spelling/test/UnitTests/coffee/LearnedWordsManagerTests.coffee +++ b/services/spelling/test/unit/coffee/LearnedWordsManagerTests.coffee @@ -11,14 +11,8 @@ describe "LearnedWordsManager", -> @db = spellingPreferences: update: sinon.stub().callsArg(3) - @cache = - get: sinon.stub() - set: sinon.stub() - break: sinon.stub() @LearnedWordsManager = SandboxedModule.require modulePath, requires: "./DB" : @db - "./Cache":@cache - describe "learnWord", -> beforeEach -> @@ -41,7 +35,6 @@ describe "LearnedWordsManager", -> describe "getLearnedWords", -> beforeEach -> - @cache.get.callsArgWith(1) @wordList = ["apples", "bananas", "pears"] @db.spellingPreferences.findOne = (conditions, callback) => callback null, learnedWords: @wordList diff --git a/services/spelling/test/UnitTests/coffee/SpellingAPIManagerTests.coffee b/services/spelling/test/unit/coffee/SpellingAPIManagerTests.coffee similarity index 100% rename from services/spelling/test/UnitTests/coffee/SpellingAPIManagerTests.coffee rename to services/spelling/test/unit/coffee/SpellingAPIManagerTests.coffee diff --git a/services/spelling/test/unit/js/ASpellTests.js b/services/spelling/test/unit/js/ASpellTests.js new file mode 100644 index 0000000000..fbcfddba55 --- /dev/null +++ b/services/spelling/test/unit/js/ASpellTests.js @@ -0,0 +1,112 @@ +(function() { + var chai, should, sinon; + + sinon = require('sinon'); + + chai = require('chai'); + + should = chai.should(); + + describe("ASpell", function() { + beforeEach(function() { + return this.ASpell = require("../../../app/js/ASpell"); + }); + describe("a correctly spelled word", function() { + beforeEach(function(done) { + return this.ASpell.checkWords("en", ["word"], (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should not correct the word", function() { + return this.result.length.should.equal(0); + }); + }); + describe("a misspelled word", function() { + beforeEach(function(done) { + return this.ASpell.checkWords("en", ["bussines"], (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should correct the word", function() { + this.result.length.should.equal(1); + return this.result[0].suggestions.indexOf("business").should.not.equal(-1); + }); + }); + describe("multiple words", function() { + beforeEach(function(done) { + return this.ASpell.checkWords("en", ["bussines", "word", "neccesary"], (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should correct the incorrect words", function() { + this.result[0].index.should.equal(0); + this.result[0].suggestions.indexOf("business").should.not.equal(-1); + this.result[1].index.should.equal(2); + return this.result[1].suggestions.indexOf("necessary").should.not.equal(-1); + }); + }); + describe("without a valid language", function() { + beforeEach(function(done) { + return this.ASpell.checkWords("notALang", ["banana"], (function(_this) { + return function(error, result) { + _this.error = error; + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should return an error", function() { + return should.exist(this.error); + }); + }); + describe("when there are no suggestions", function() { + beforeEach(function(done) { + return this.ASpell.checkWords("en", ["asdkfjalkdjfadhfkajsdhfashdfjhadflkjadhflajsd"], (function(_this) { + return function(error, result) { + _this.error = error; + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should return a blank array", function() { + this.result.length.should.equal(1); + return this.result[0].suggestions.should.deep.equal([]); + }); + }); + return describe("when the request times out", function() { + beforeEach(function(done) { + var i, words; + words = (function() { + var _i, _results; + _results = []; + for (i = _i = 0; _i <= 1000000; i = ++_i) { + _results.push("abcdefg"); + } + return _results; + })(); + this.ASpell.ASPELL_TIMEOUT = 100; + this.start = new Date(); + return this.ASpell.checkWords("en", words, (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should return in reasonable time", function(done) { + return done(); + }); + }); + }); + +}).call(this); diff --git a/services/spelling/test/unit/js/LearnedWordsManagerTests.js b/services/spelling/test/unit/js/LearnedWordsManagerTests.js new file mode 100644 index 0000000000..a4da3013f0 --- /dev/null +++ b/services/spelling/test/unit/js/LearnedWordsManagerTests.js @@ -0,0 +1,99 @@ +(function() { + var SandboxedModule, chai, expect, modulePath, sinon; + + sinon = require('sinon'); + + chai = require('chai'); + + expect = chai.expect; + + SandboxedModule = require('sandboxed-module'); + + modulePath = require('path').join(__dirname, '../../../app/js/LearnedWordsManager'); + + describe("LearnedWordsManager", function() { + beforeEach(function() { + this.token = "a6b3cd919ge"; + this.callback = sinon.stub(); + this.db = { + spellingPreferences: { + update: sinon.stub().callsArg(3) + } + }; + return this.LearnedWordsManager = SandboxedModule.require(modulePath, { + requires: { + "./DB": this.db + } + }); + }); + describe("learnWord", function() { + beforeEach(function() { + this.word = "instanton"; + return this.LearnedWordsManager.learnWord(this.token, this.word, this.callback); + }); + it("should insert the word in the word list in the database", function() { + return expect(this.db.spellingPreferences.update.calledWith({ + token: this.token + }, { + $push: { + learnedWords: this.word + } + }, { + upsert: true + })).to.equal(true); + }); + return it("should call the callback", function() { + return expect(this.callback.called).to.equal(true); + }); + }); + return describe("getLearnedWords", function() { + beforeEach(function() { + this.wordList = ["apples", "bananas", "pears"]; + this.db.spellingPreferences.findOne = (function(_this) { + return function(conditions, callback) { + return callback(null, { + learnedWords: _this.wordList + }); + }; + })(this); + sinon.spy(this.db.spellingPreferences, "findOne"); + return this.LearnedWordsManager.getLearnedWords(this.token, this.callback); + }); + it("should get the word list for the given user", function() { + return expect(this.db.spellingPreferences.findOne.calledWith({ + token: this.token + })).to.equal(true); + }); + return it("should return the word list in the callback", function() { + return expect(this.callback.calledWith(null, this.wordList)).to.equal(true); + }); + }); + + /* + describe "caching the result", -> + it 'should use the cache first if it is primed', (done)-> + @wordList = ["apples", "bananas", "pears"] + @cache.get.callsArgWith(1, null, learnedWords: @wordList) + @db.spellingPreferences.findOne = sinon.stub() + @LearnedWordsManager.getLearnedWords @token, (err, spellings)=> + @db.spellingPreferences.find.called.should.equal false + @wordList.should.equal spellings + done() + + it 'should set the cache after hitting the db', (done)-> + @wordList = ["apples", "bananas", "pears"] + @cache.get.callsArgWith(1) + @db.spellingPreferences.findOne = sinon.stub().callsArgWith(1, null, learnedWords: @wordList) + @LearnedWordsManager.getLearnedWords @token, (err, spellings)=> + @cache.set.calledWith(@token, learnedWords:@wordList).should.equal true + done() + + it 'should break cache when update is called', (done)-> + @word = "instanton" + @LearnedWordsManager.learnWord @token, @word, => + @cache.break.calledWith(@token).should.equal true + done() + */ + }); + +}).call(this); diff --git a/services/spelling/test/unit/js/SpellingAPIManagerTests.js b/services/spelling/test/unit/js/SpellingAPIManagerTests.js new file mode 100644 index 0000000000..4c79859865 --- /dev/null +++ b/services/spelling/test/unit/js/SpellingAPIManagerTests.js @@ -0,0 +1,208 @@ +(function() { + var SandboxedModule, chai, expect, modulePath, sinon; + + sinon = require('sinon'); + + chai = require('chai'); + + expect = chai.expect; + + chai.should(); + + SandboxedModule = require('sandboxed-module'); + + modulePath = require('path').join(__dirname, '../../../app/js/SpellingAPIManager'); + + describe("SpellingAPIManager", function() { + beforeEach(function() { + this.token = "user-id-123"; + this.ASpell = {}; + this.learnedWords = ["lerned"]; + this.LearnedWordsManager = { + getLearnedWords: sinon.stub().callsArgWith(1, null, this.learnedWords), + learnWord: sinon.stub().callsArg(2) + }; + return this.SpellingAPIManager = SandboxedModule.require(modulePath, { + requires: { + "./ASpell": this.ASpell, + "./LearnedWordsManager": this.LearnedWordsManager + } + }); + }); + describe("runRequest", function() { + beforeEach(function() { + this.nonLearnedWords = ["some", "words", "htat", "are", "speled", "rong", "lerned"]; + this.allWords = this.nonLearnedWords.concat(this.learnedWords); + this.misspellings = [ + { + index: 2, + suggestions: ["that"] + }, { + index: 4, + suggestions: ["spelled"] + }, { + index: 5, + suggestions: ["wrong", "ring"] + }, { + index: 6, + suggestions: ["learned"] + } + ]; + this.misspellingsWithoutLearnedWords = this.misspellings.slice(0, 3); + this.ASpell.checkWords = (function(_this) { + return function(lang, word, callback) { + return callback(null, _this.misspellings); + }; + })(this); + return sinon.spy(this.ASpell, "checkWords"); + }); + describe("with sensible JSON", function() { + beforeEach(function(done) { + return this.SpellingAPIManager.runRequest(this.token, { + words: this.allWords + }, (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should return the words that are spelled incorrectly and not learned", function() { + return expect(this.result.misspellings).to.deep.equal(this.misspellingsWithoutLearnedWords); + }); + }); + describe("with a missing words array", function() { + beforeEach(function(done) { + return this.SpellingAPIManager.runRequest(this.token, {}, (function(_this) { + return function(error, result) { + _this.error = error; + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should return an error", function() { + return expect(this.error).to.deep.equal(new Error("malformed JSON")); + }); + }); + describe("with a missing token", function() { + beforeEach(function(done) { + return this.SpellingAPIManager.runRequest(null, { + words: this.allWords + }, (function(_this) { + return function(error, result) { + _this.error = error; + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should spell check without using any learned words", function() { + return this.LearnedWordsManager.getLearnedWords.called.should.equal(false); + }); + }); + describe("without a language", function() { + beforeEach(function(done) { + return this.SpellingAPIManager.runRequest(this.token, { + words: this.allWords + }, (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should use en as the default", function() { + return this.ASpell.checkWords.calledWith("en").should.equal(true); + }); + }); + describe("with a language", function() { + beforeEach(function(done) { + return this.SpellingAPIManager.runRequest(this.token, { + words: this.allWords, + language: this.language = "fr" + }, (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should use the language", function() { + return this.ASpell.checkWords.calledWith(this.language).should.equal(true); + }); + }); + return describe("with a very large collection of words", function() { + beforeEach(function(done) { + var i; + this.manyWords = (function() { + var _i, _results; + _results = []; + for (i = _i = 1; _i <= 100000; i = ++_i) { + _results.push("word"); + } + return _results; + })(); + return this.SpellingAPIManager.runRequest(this.token, { + words: this.manyWords + }, (function(_this) { + return function(error, result) { + _this.result = result; + return done(); + }; + })(this)); + }); + return it("should truncate to 10,000 words", function() { + return this.ASpell.checkWords.calledWith(sinon.match.any, this.manyWords.slice(0, 10000)).should.equal(true); + }); + }); + }); + return describe("learnWord", function() { + describe("without a token", function() { + beforeEach(function(done) { + return this.SpellingAPIManager.learnWord(null, { + word: "banana" + }, (function(_this) { + return function(error) { + _this.error = error; + return done(); + }; + })(this)); + }); + return it("should return an error", function() { + return expect(this.error).to.deep.equal(new Error("malformed JSON")); + }); + }); + describe("without a word", function() { + beforeEach(function(done) { + return this.SpellingAPIManager.learnWord(this.token, {}, (function(_this) { + return function(error) { + _this.error = error; + return done(); + }; + })(this)); + }); + return it("should return an error", function() { + return expect(this.error).to.deep.equal(new Error("no token provided")); + }); + }); + return describe("with a word and a token", function() { + beforeEach(function(done) { + this.word = "banana"; + return this.SpellingAPIManager.learnWord(this.token, { + word: this.word + }, (function(_this) { + return function(error) { + _this.error = error; + return done(); + }; + })(this)); + }); + return it("should call LearnedWordsManager.learnWord", function() { + return this.LearnedWordsManager.learnWord.calledWith(this.token, this.word).should.equal(true); + }); + }); + }); + }); + +}).call(this);