Преглед изворни кода

Use ShareLaTeX conventions

James Allen пре 12 година
родитељ
комит
5c002bc9b1

+ 52 - 0
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']
+
+

+ 5 - 4
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}"

+ 1 - 1
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()
 

+ 0 - 35
services/spelling/app/coffee/Cache.coffee

@@ -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}"

+ 14 - 0
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'
+

+ 0 - 10
services/spelling/config/settings.development.coffee

@@ -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'
-

+ 11 - 4
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": ""
   }
 }

+ 0 - 56
services/spelling/test/UnitTests/coffee/CacheTests.coffee

@@ -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

+ 0 - 0
services/spelling/test/UnitTests/coffee/ASpellTests.coffee → services/spelling/test/unit/coffee/ASpellTests.coffee


+ 0 - 7
services/spelling/test/UnitTests/coffee/LearnedWordsManagerTests.coffee → 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

+ 0 - 0
services/spelling/test/UnitTests/coffee/SpellingAPIManagerTests.coffee → services/spelling/test/unit/coffee/SpellingAPIManagerTests.coffee


+ 112 - 0
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);

+ 99 - 0
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);

+ 208 - 0
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);