Merge: Perfize nitcc_runtime
authorJean Privat <jean@pryen.org>
Sat, 5 Dec 2015 21:52:51 +0000 (16:52 -0500)
committerJean Privat <jean@pryen.org>
Sat, 5 Dec 2015 21:52:51 +0000 (16:52 -0500)
Slight improvement of the work of nitcc.

Numbers with raw parsing of a 30MB json file from nitrpg (only the AST creation, no further analysis)

Before: 0m5.700s ; 25.1 GIr
After: 0m3.540s (-38%) ; 20.3 GIr (-19%)

There is still a lot (99?) of improvements to make before having reasonable numbers, but the nitcc_runtime part ain't one.

Pull-Request: #1876
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>

19 files changed:
contrib/nitrpg/Makefile
contrib/nitrpg/src/achievements.nit
contrib/nitrpg/src/events.nit
contrib/nitrpg/src/events_generator.nit [new file with mode: 0644]
contrib/nitrpg/src/game.nit
contrib/nitrpg/src/statistics.nit
contrib/nitrpg/src/templates/templates_base.nit
contrib/nitrpg/src/test_achievements.nit [new file with mode: 0644]
contrib/nitrpg/src/test_events.nit [new file with mode: 0644]
contrib/nitrpg/src/test_game.nit [new file with mode: 0644]
contrib/nitrpg/src/test_helper.nit [new file with mode: 0644]
contrib/nitrpg/src/test_listener.nit [new file with mode: 0644]
contrib/nitrpg/src/test_statistics.nit [new file with mode: 0644]
contrib/nitrpg/src/web.nit
lib/pthreads/concurrent_collections.nit
lib/pthreads/examples/jointask_example.nit [new file with mode: 0644]
lib/pthreads/examples/threadpool_example.nit
lib/pthreads/threadpool.nit
tests/sav/jointask_example.res [new file with mode: 0644]

index 40ebec2..b6c4be9 100644 (file)
@@ -15,6 +15,7 @@
 # limitations under the License.
 
 NITC=../../bin/nitc
+NITU=../../bin/nitunit
 
 all: listener web
 
@@ -24,5 +25,12 @@ listener:
 web:
        $(NITC) src/web.nit
 
+check:
+       $(NITU) src/game.nit
+       $(NITU) src/events.nit
+       $(NITU) src/statistics.nit
+       $(NITU) src/achievements.nit
+       $(NITU) src/listener.nit
+
 clean:
        rm listener web
index 5ce317a..24fac20 100644 (file)
@@ -32,22 +32,28 @@ redef class GameEntity
        #
        # TODO should update the achievement?
        fun add_achievement(achievement: Achievement) do
-               var key = self.key / achievement.key
-               if game.store.has_key(key) then return
                stats.inc("achievements")
-               achievement.save_in(self.key)
-               save
+               achievement.owner = self
+               achievement.save
        end
 
+       # Is `a` unlocked for this `Player`?
+       fun has_achievement(a: Achievement): Bool do return load_achievement(a.id) != null
+
        # Load the event from its `id`.
        #
        # Looks for the event save file in game data.
        # Returns `null` if the event cannot be found.
        fun load_achievement(id: String): nullable Achievement do
-               var key = self.key / "achievements" / id
-               if not game.store.has_key(key) then return null
-               var json = game.store.load_object(key)
-               return new Achievement.from_json(game, json)
+               var req = new JsonObject
+               req["id"] = id
+               req["game"] = game.key
+               req["owner"] = key
+               var obj = game.db.collection("achievements").find(req)
+               if obj isa JsonObject then
+                       return new Achievement.from_json(game, obj)
+               end
+               return null
        end
 
        # List all events registered in this entity.
@@ -56,12 +62,13 @@ redef class GameEntity
        #
        # To add events see `add_event`.
        fun load_achievements: MapRead[String, Achievement] do
+               var req = new JsonObject
+               req["game"] = game.key
+               req["owner"] = key
                var res = new HashMap[String, Achievement]
-               var key = self.key / "achievements"
-               if not game.store.has_collection(key) then return res
-               var coll = game.store.list_collection(key)
-               for id in coll do
-                       res[id.to_s] = load_achievement(id.to_s).as(not null)
+               for obj in game.db.collection("achievements").find_all(req) do
+                       var achievement = new Achievement.from_json(game, obj)
+                       res[achievement.id] = achievement
                end
                return res
        end
@@ -74,10 +81,11 @@ end
 class Achievement
        super GameEntity
 
+       redef var collection_name = "achievements"
 
        redef var game
 
-       redef var key is lazy do
+       redef fun key do
                var owner = self.owner
                if owner == null then return id
                return "{owner.key}-{id}"
@@ -105,7 +113,11 @@ class Achievement
        #
        # Used to load achievements from storage.
        init from_json(game: Game, json: JsonObject) do
-               init(game, json["id"].to_s, json["name"].to_s, json["desc"].to_s, json["reward"].as(Int))
+               init(game,
+                       json["id"].as(String),
+                       json["name"].as(String),
+                       json["desc"].as(String),
+                       json["reward"].as(Int))
        end
 
        redef fun to_json do
@@ -115,18 +127,13 @@ class Achievement
                json["desc"] = desc
                json["reward"] = reward
                json["game"] = game.key
+               var owner = self.owner
                if owner != null then json["owner"] = owner.key
                return json
        end
 end
 
 redef class Player
-
-       # Is `a` unlocked for this `Player`?
-       fun has_achievement(a: Achievement): Bool do
-               return load_achievement(a.id) != null
-       end
-
        # Unlocks an achievement for this Player based on a GithubEvent.
        #
        # Register the achievement and adds the achievement reward to the player
@@ -140,6 +147,7 @@ redef class Player
                nitcoins += a.reward
                add_achievement(a)
                trigger_unlock_event(a, event)
+               save
        end
 
        # Create a new event that marks the achievement unlocking.
index bd1ee58..c540dc5 100644 (file)
@@ -24,6 +24,7 @@ import game
 
 redef class GameEntity
 
+       # Register a new game event for this entity.
        fun add_event(event: GameEvent) do
                event.owner = self
                event.save
@@ -35,13 +36,12 @@ redef class GameEntity
        #
        # To add events see `add_event`.
        fun load_events: Array[GameEvent] do
-               var key = key / "events"
+               var req = new JsonObject
+               req["game"] = game.key
+               req["owner"] = key
                var res = new Array[GameEvent]
-               if not game.store.has_collection(key) then return res
-               var coll = game.store.list_collection(key)
-               for id in coll do
-                       var name = id.to_s
-                       res.add load_event(name).as(not null)
+               for obj in game.db.collection("events").find_all(req) do
+                       res.add new GameEvent.from_json(game, obj)
                end
                (new EventTimeComparator).sort(res)
                return res
@@ -52,10 +52,13 @@ redef class GameEntity
        # Looks for the event save file in game data.
        # Returns `null` if the event cannot be found.
        fun load_event(id: String): nullable GameEvent do
-               var key = key / "events" / id
-               if not game.store.has_key(key) then return null
-               var json = game.store.load_object(key)
-               return new GameEvent.from_json(game, json)
+               var req = new JsonObject
+               req["game"] = game.key
+               req["owner"] = key
+               req["internal_id"] = id
+               var res = game.db.collection("events").find(req)
+               if res != null then return new GameEvent.from_json(game, res)
+               return null
        end
 end
 
@@ -63,6 +66,7 @@ end
 class GameEvent
        super GameEntity
 
+       redef var collection_name = "events"
 
        redef var game
 
@@ -95,9 +99,9 @@ class GameEvent
        #
        # Used to load events from json storage.
        init from_json(game: Game, json: JsonObject) do
-               init(game, json["kind"].to_s, json["data"].as(JsonObject))
-               internal_id = json["internal_id"].to_s
-               time = new ISODate.from_string(json["time"].to_s)
+               init(game, json["kind"].as(String), json["data"].as(JsonObject))
+               internal_id = json["internal_id"].as(String)
+               time = new ISODate.from_string(json["time"].as(String))
        end
 
        redef fun to_json do
@@ -107,6 +111,7 @@ class GameEvent
                json["time"] = time.to_s
                json["data"] = data
                json["game"] = game.key
+               var owner = self.owner
                if owner != null then json["owner"] = owner.key
                return json
        end
diff --git a/contrib/nitrpg/src/events_generator.nit b/contrib/nitrpg/src/events_generator.nit
new file mode 100644 (file)
index 0000000..cc8b6a4
--- /dev/null
@@ -0,0 +1,94 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Generate Github events from repo data.
+#
+# Mainly used for testing and history importation.
+module events_generator
+
+import github::events
+
+# Github events generator
+#
+# Generates events from repo data.
+class EventsGenerator
+
+       # API client used to get github data.
+       var api: GithubAPI
+
+       # Issues
+
+       # Generate a new IssuesEvent from an issue.
+       fun issues_event(action: String, issue: Issue): IssuesEvent do
+               var e = new IssuesEvent(api)
+               e.action = action
+               e.repo = issue.repo
+               e.issue = issue
+               return e
+       end
+
+       # Generate a new IssuesEvent with an `opened` action.
+       fun issue_open(issue: Issue): IssuesEvent do return issues_event("opened", issue)
+
+       # Generate a new IssuesEvent with an `closed` action.
+       fun issue_close(issue: Issue): IssuesEvent do return issues_event("closed", issue)
+
+       # Generate a new IssuesEvent with an `reopened` action.
+       fun issue_reopen(issue: Issue): IssuesEvent do return issues_event("reopened", issue)
+
+       # Generate a new IssuesEvent from a IssueEvent.
+       fun issue_raw_event(issue: Issue, event: IssueEvent): IssuesEvent do
+               var e = issues_event(event.event, issue)
+               e.lbl = event.labl
+               e.assignee = event.assignee
+               return e
+       end
+
+       # Pull requests
+
+       # Generate a new PullRequestEvent from a `pull` request.
+       fun pull_event(action: String, pull: PullRequest): PullRequestEvent do
+               var e = new PullRequestEvent(api)
+               e.action = action
+               e.repo = pull.repo
+               e.pull = pull
+               return e
+       end
+
+       # Generate a new PullRequestEvent with an `opened` action.
+       fun pull_open(pull: PullRequest): PullRequestEvent do return pull_event("opened", pull)
+
+       # Generate a new PullRequestEvent with an `closed` action.
+       fun pull_close(pull: PullRequest): PullRequestEvent do return pull_event("closed", pull)
+
+       # Generate a new PullRequestEvent with an `reopened` action.
+       fun pull_reopen(pull: PullRequest): PullRequestEvent do return pull_event("reopened", pull)
+
+       # Generate a new PullRequestEvent from a IssueEvent.
+       fun pull_raw_event(pull: PullRequest, event: IssueEvent): PullRequestEvent do
+               return pull_event(event.event, pull)
+       end
+
+       # Generate a new IssueCommentEvent from a IssueComment.
+       fun issue_comment_event(issue: Issue, comment: IssueComment): IssueCommentEvent do
+               var e = new IssueCommentEvent(api)
+               e.action = "created"
+               e.repo = issue.repo
+               e.issue = issue
+               e.comment = comment
+               return e
+       end
+end
index 2924e46..504c445 100644 (file)
@@ -25,7 +25,7 @@
 # the `GameReactor` abstraction.
 module game
 
-intrude import json::store
+import mongodb
 import github::events
 
 # An entity within a `Game`.
@@ -35,23 +35,21 @@ interface GameEntity
        # The game instance containing `self`.
        fun game: Game is abstract
 
-       # Uniq key used for data storage.
-       fun key: String is abstract
+       # Collection `self` should be saved in.
+       fun collection_name: String is abstract
 
-       # Saves the `self` state in game data.
-       #
-       # Date are stored under `self.key`.
-       fun save do game.store.store_object(key, to_json)
+       # Uniq key of this entity within the collection.
+       fun key: String is abstract
 
-       # Saves `self` state under `key` data.
-       #
-       # Data are stored under `key / self.key`.
-       fun save_in(key: String) do
-               game.store.store_object(key / self.key, to_json)
-       end
+       # Saves `self` in db.
+       fun save do game.db.collection(collection_name).save(to_json)
 
        # Json representation of `self`.
-       fun to_json: JsonObject  do return new JsonObject
+       fun to_json: JsonObject do
+               var json = new JsonObject
+               json["_id"] = key
+               return json
+       end
 
        # Pretty print `self` to be displayed in a terminal.
        fun pretty: String is abstract
@@ -74,18 +72,30 @@ class Game
        # Game name
        var name: String = repo.full_name is lazy
 
-       # Directory where game data are stored.
-       var game_dir: String is lazy do return "nitrpg_data" / repo.full_name
-
        redef var key = name is lazy
 
-       # Used for data storage.
-       #
-       # File are stored in `game_dir`.
-       var store: JsonStore is lazy do return new JsonStore(game_dir)
+       # Mongo server url where this game data are stored.
+       var mongo_url = "mongodb://localhost:27017" is writable
+
+       # Mongo db client.
+       var client = new MongoClient(mongo_url) is lazy
+
+       # Mongo db name where this game data are stored.
+       var db_name = "nitrpg" is writable
+
+       # Mongo db instance for this game.
+       var db: MongoDb is lazy do return client.database(db_name)
+
+       redef var collection_name = "games"
 
        # Init the Game and try to load saved data.
-       init do if store.has_key(key) then from_json(store.load_object(key))
+       init from_mongo(api: GithubAPI, repo: Repo) do
+               init(api, repo)
+               var req = new JsonObject
+               req["name"] = repo.full_name
+               var res = db.collection("games").find(req)
+               if res != null then from_json(res)
+       end
 
        # Init `self` from a JsonObject.
        #
@@ -118,10 +128,12 @@ class Game
        # Returns `null` if the player cannot be found.
        # In this case, the player can be created with `add_player`.
        fun load_player(name: String): nullable Player do
-               var key = "players" / name
-               if not store.has_key(key) then return null
-               var json = store.load_object(key)
-               return new Player.from_json(self, json)
+               var req = new JsonObject
+               req["name"] = name
+               req["game"] = game.key
+               var res = db.collection("players").find(req)
+               if res != null then return new Player.from_json(self, res)
+               return null
        end
 
        # List known players.
@@ -130,12 +142,12 @@ class Game
        #
        # To add players see `add_player`.
        fun load_players: MapRead[String, Player] do
+               var req = new JsonObject
+               req["game"] = game.key
                var res = new HashMap[String, Player]
-               if not store.has_collection("players") then return res
-               var coll = store.list_collection("players")
-               for id in coll do
-                       var name = id.to_s
-                       res[name] = load_player(name).as(not null)
+               for obj in db.collection("players").find_all(req) do
+                       var player = new Player.from_json(self, obj)
+                       res[player.name] = player
                end
                return res
        end
@@ -154,7 +166,7 @@ class Game
        end
 
        # Erase all saved data for this game.
-       fun clear do store.clear
+       fun clear do db.collection(collection_name).remove(to_json)
 
        # Verbosity level used fo stdout.
        #
@@ -186,6 +198,9 @@ end
 class Player
        super GameEntity
 
+       # Stored in collection `players`.
+       redef var collection_name = "players"
+
        redef var game
 
        # FIXME contructor should be private
@@ -217,7 +232,7 @@ class Player
        #
        # Used to load players from saved data.
        init from_json(game: Game, json: JsonObject) do
-               init(game, json["name"].to_s)
+               init(game, json["name"].as(String))
                nitcoins = json["nitcoins"].as(Int)
        end
 
index 153b5cd..0bdb421 100644 (file)
@@ -34,11 +34,6 @@ redef class Game
 
        redef var stats is lazy do return new GameStatsManager(game, self)
 
-       redef fun save do
-               super
-               stats.save_in(self.key)
-       end
-
        redef fun pretty do
                var res = new FlatBuffer
                res.append super
@@ -46,17 +41,17 @@ redef class Game
                res.append stats.pretty
                return res.write_to_string
        end
+
+       redef fun save do
+               super
+               stats.save
+       end
 end
 
 redef class Player
 
        redef var stats is lazy do return new GameStatsManager(game, self)
 
-       redef fun save do
-               super
-               stats.save_in(self.key)
-       end
-
        redef fun nitcoins do return stats["nitcoins"]
        redef fun nitcoins=(nc) do stats["nitcoins"] = nc
 
@@ -67,6 +62,11 @@ redef class Player
                res.append stats.pretty
                return res.write_to_string
        end
+
+       redef fun save do
+               super
+               stats.save
+       end
 end
 
 # Store game stats for defined period.
@@ -99,12 +99,15 @@ class GameStatsManager
 
        # Load statistics for a `period` key.
        fun load_stats_for(period: String): GameStats do
-               var key = owner.key / self.key / period
-               if not game.store.has_key(key) then
+               var req = new JsonObject
+               req["period"] = period
+               req["owner"] = owner.key
+               var obj = game.db.collection("statistics").find(req)
+               if obj isa JsonObject then
+                       return new GameStats.from_json(game, period, owner, obj)
+               else
                        return new GameStats(game, period, owner)
                end
-               var json = game.store.load_object(key)
-               return new GameStats.from_json(game, period, owner, json)
        end
 
        redef fun [](key) do return overall[key]
@@ -133,12 +136,12 @@ class GameStatsManager
                weekly.dec(e)
        end
 
-       redef fun save_in(key) do
-               overall.save_in(key / self.key)
-               yearly.save_in(key / self.key)
-               monthly.save_in(key / self.key)
-               daily.save_in(key / self.key)
-               weekly.save_in(key / self.key)
+       redef fun save do
+               overall.save
+               yearly.save
+               monthly.save
+               daily.save
+               weekly.save
        end
 
        redef fun pretty do return overall.pretty
@@ -151,6 +154,8 @@ class GameStats
 
        redef var game
 
+       redef var collection_name = "statistics"
+
        # The period these stats are about.
        var period: String
 
index a4f40d9..470160b 100644 (file)
@@ -21,8 +21,11 @@ import achievements
 
 redef class GameEntity
 
+       # Path to this entity from root.
+       fun path: String do return collection_name / key
+
        # URL to this game entity page.
-       fun url: String do return game.url / key
+       fun url: String do return game.url / path
 end
 
 redef class Game
@@ -32,7 +35,7 @@ redef class Game
        # This must be set before any access to `url`.
        var root_url: String is noinit, writable
 
-       redef fun url do return "{root_url}/games" / key
+       redef fun url do return "{root_url}/{path}"
 
        # Return a HTML link to this Game.
        fun link: String do return "<a href=\"{url}\">{name}</a>"
diff --git a/contrib/nitrpg/src/test_achievements.nit b/contrib/nitrpg/src/test_achievements.nit
new file mode 100644 (file)
index 0000000..b5d4cb7
--- /dev/null
@@ -0,0 +1,145 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test module for `achievements.nit`
+module test_achievements is test_suite
+
+import test_helper
+import achievements
+
+class TestGame
+       super NitrpgTestHelper
+
+       fun test_add_achievement do
+               var db = load_db("test_add_achievement")
+               var game = load_game("Morriar/nit", db)
+               var a1 = new Achievement(game, "test_id1", "test_name", "test_desc", 15)
+               var a2 = new Achievement(game, "test_id2", "test_name", "test_desc", 15)
+               game.add_achievement(a1)
+               game.add_achievement(a2)
+               assert game.load_achievements.length == 2
+               db.drop
+       end
+
+       fun test_load_achievement do
+               var db = load_db("test_load_achievement")
+               var game = load_game("Morriar/nit", db)
+               var a1 = new Achievement(game, "test_id1", "test_name", "test_desc", 15)
+               var a2 = new Achievement(game, "test_id2", "test_name", "test_desc", 15)
+               game.add_achievement(a1)
+               assert game.load_achievement(a1.id).id == "test_id1"
+               assert game.load_achievement(a2.id) == null
+               db.drop
+       end
+
+       fun test_load_achievements do
+               var db = load_db("test_load_achievements")
+               var game = load_game("Morriar/nit", db)
+               var a1 = new Achievement(game, "test_id1", "test_name", "test_desc", 15)
+               var a2 = new Achievement(game, "test_id2", "test_name", "test_desc", 15)
+               var a3 = new Achievement(game, "test_id3", "test_name", "test_desc", 15)
+               game.add_achievement(a1)
+               game.add_achievement(a2)
+               game.db.collection("achievements").insert(a3.to_json)
+               var ok = [a1.id, a2.id]
+               var res = game.load_achievements
+               assert res.length == 2
+               for a in res.values do assert ok.has(a.id)
+               db.drop
+       end
+end
+
+class TestPlayer
+       super NitrpgTestHelper
+
+       fun test_add_achievement do
+               var db = load_db("test_add_achievement")
+               var game = load_game("Morriar/nit", db)
+               var player1 = new Player(game, "Morriar")
+               var a1 = new Achievement(game, "test_id1", "test_name", "test_desc", 15)
+               var a2 = new Achievement(game, "test_id2", "test_name", "test_desc", 15)
+               player1.add_achievement(a1)
+               player1.add_achievement(a2)
+               assert player1.load_achievements.length == 2
+               db.drop
+       end
+
+       fun test_load_achievement do
+               var db = load_db("test_load_achievement")
+               var game = load_game("Morriar/nit", db)
+               var player1 = new Player(game, "Morriar")
+               var player2 = new Player(game, "xymus")
+               var a1 = new Achievement(game, "test_id1", "test_name", "test_desc", 15)
+               var a2 = new Achievement(game, "test_id2", "test_name", "test_desc", 15)
+               player1.add_achievement(a1)
+               player2.add_achievement(a2)
+               assert player1.load_achievement(a1.id).id == "test_id1"
+               assert player1.load_achievement(a2.id) == null
+               assert player2.load_achievement(a2.id).id == "test_id2"
+               assert player2.load_achievement(a1.id) == null
+               db.drop
+       end
+
+       fun test_load_achievements do
+               var db = load_db("test_load_achievements")
+               var game = load_game("Morriar/nit", db)
+               var player1 = new Player(game, "Morriar")
+               var player2 = new Player(game, "xymus")
+               var a1 = new Achievement(game, "test_id1", "test_name", "test_desc", 15)
+               var a2 = new Achievement(game, "test_id2", "test_name", "test_desc", 15)
+               var a3 = new Achievement(game, "test_id3", "test_name", "test_desc", 15)
+               player1.add_achievement(a1)
+               player1.add_achievement(a2)
+               player2.add_achievement(a3)
+               var ok = [a1.id, a2.id]
+               var res = player1.load_achievements
+               assert res.length == 2
+               for a in res.values do assert ok.has(a.id)
+               db.drop
+       end
+end
+
+class TestAchievement
+       super NitrpgTestHelper
+
+       fun test_init do
+               var db = load_db("test_init")
+               var game = load_game("Morriar/nit", db)
+               var a = new Achievement(game, "test_id", "test_name", "test_desc", 15)
+               assert a.id == "test_id"
+               assert a.name == "test_name"
+               assert a.desc == "test_desc"
+               assert a.reward == 15
+               db.drop
+       end
+
+       fun test_init_from_json do
+               var db = load_db("test_init_from_json")
+               var game = load_game("Morriar/nit", db)
+               var json = """{
+                       "id": "test_id",
+                       "name": "test_name",
+                       "desc": "test_desc",
+                       "reward": 15
+               }""".parse_json.as(JsonObject)
+               var a = new Achievement.from_json(game, json)
+               assert a.id == "test_id"
+               assert a.name == "test_name"
+               assert a.desc == "test_desc"
+               assert a.reward == 15
+               db.drop
+       end
+end
diff --git a/contrib/nitrpg/src/test_events.nit b/contrib/nitrpg/src/test_events.nit
new file mode 100644 (file)
index 0000000..83f17bf
--- /dev/null
@@ -0,0 +1,144 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test module for `events.nit`
+module test_events is test_suite
+
+import test_helper
+import events
+
+class TestGame
+       super NitrpgTestHelper
+
+       fun test_add_event do
+               var db = load_db("test_add_event")
+               var game = load_game("Morriar/nit", db)
+               var event1 = new GameEvent(game, "test_kind", new JsonObject)
+               var event2 = new GameEvent(game, "test_kind", new JsonObject)
+               game.add_event(event1)
+               game.add_event(event2)
+               assert game.load_events.length == 2
+               db.drop
+       end
+
+       fun test_load_event do
+               var db = load_db("test_load_event")
+               var game = load_game("Morriar/nit", db)
+               var event1 = new GameEvent(game, "test_kind", new JsonObject)
+               var event2 = new GameEvent(game, "test_kind", new JsonObject)
+               game.add_event(event1)
+               assert game.load_event(event1.internal_id).kind == "test_kind"
+               assert game.load_event(event2.internal_id) == null
+               db.drop
+       end
+
+       fun test_load_events do
+               var db = load_db("test_load_events")
+               var game = load_game("Morriar/nit", db)
+               var event1 = new GameEvent(game, "test_kind", new JsonObject)
+               var event2 = new GameEvent(game, "test_kind", new JsonObject)
+               var event3 = new GameEvent(game, "test_kind", new JsonObject)
+               game.add_event(event1)
+               game.add_event(event2)
+               game.db.collection("events").insert(event3.to_json)
+               var ok = [event1.internal_id, event2.internal_id]
+               var res = game.load_events
+               assert res.length == 2
+               for event in res do assert ok.has(event.internal_id)
+               db.drop
+       end
+end
+
+class TestPlayer
+       super NitrpgTestHelper
+
+       fun test_add_event do
+               var db = load_db("test_add_event")
+               var game = load_game("Morriar/nit", db)
+               var player1 = new Player(game, "Morriar")
+               var player2 = new Player(game, "xymus")
+               var event1 = new GameEvent(game, "test_kind", new JsonObject)
+               var event2 = new GameEvent(game, "test_kind", new JsonObject)
+               player1.add_event(event1)
+               player1.add_event(event2)
+               assert player1.load_events.length == 2
+               assert player2.load_events.length == 0
+               db.drop
+       end
+
+       fun test_load_event do
+               var db = load_db("test_load_event")
+               var game = load_game("Morriar/nit", db)
+               var player1 = new Player(game, "Morriar")
+               var player2 = new Player(game, "xymus")
+               var event1 = new GameEvent(game, "test_kind", new JsonObject)
+               var event2 = new GameEvent(game, "test_kind", new JsonObject)
+               player1.add_event(event1)
+               player2.add_event(event2)
+               assert player1.load_event(event1.internal_id).kind == "test_kind"
+               assert player1.load_event(event2.internal_id) == null
+               assert player2.load_event(event2.internal_id).kind == "test_kind"
+               assert player2.load_event(event1.internal_id) == null
+               db.drop
+       end
+
+       fun test_load_events do
+               var db = load_db("test_load_events")
+               var game = load_game("Morriar/nit", db)
+               var player1 = new Player(game, "Morriar")
+               var player2 = new Player(game, "xymus")
+               var event1 = new GameEvent(game, "test_kind", new JsonObject)
+               var event2 = new GameEvent(game, "test_kind", new JsonObject)
+               var event3 = new GameEvent(game, "test_kind", new JsonObject)
+               player1.add_event(event1)
+               player1.add_event(event2)
+               player2.add_event(event3)
+               assert player1.load_events.length == 2
+               assert player2.load_events.length == 1
+               var ok = [event1.internal_id, event2.internal_id]
+               for event in player1.load_events do assert ok.has(event.internal_id)
+               db.drop
+       end
+end
+
+class TestGameEvent
+       super NitrpgTestHelper
+
+       fun test_init do
+               var db = load_db("test_init")
+               var game = load_game("Morriar/nit", db)
+               var event = new GameEvent(game, "test_kind", new JsonObject)
+               assert event.to_json["kind"] == "test_kind"
+               db.drop
+       end
+
+       fun test_init_from_json do
+               var db = load_db("test_init_from_json")
+               var game = load_game("Morriar/nit", db)
+               var json = """{
+                       "internal_id": "test_id",
+                       "kind": "test_kind",
+                       "time": "2015-02-05T00:00:00Z",
+                       "data": {"test_field": "test_value"}
+               }""".parse_json.as(JsonObject)
+               var event = new GameEvent.from_json(game, json)
+               assert event.internal_id == "test_id"
+               assert event.kind == "test_kind"
+               assert event.data.to_json == """{"test_field":"test_value"}"""
+               assert event.time.to_s == "2015-02-05T00:00:00Z"
+               db.drop
+       end
+end
diff --git a/contrib/nitrpg/src/test_game.nit b/contrib/nitrpg/src/test_game.nit
new file mode 100644 (file)
index 0000000..78f3bd0
--- /dev/null
@@ -0,0 +1,146 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test module for `game.nit`.
+module test_game is test_suite
+
+import test_helper
+
+class TestGame
+       super NitrpgTestHelper
+
+       fun test_add_player do
+               var db = load_db("test_add_player")
+               var game = load_game("privat/nit", db)
+               var users = ["Morriar", "xymus"]
+               for name in users do
+                       game.add_player(game.api.load_user(name).as(not null))
+               end
+               var res = game.load_players.values
+               assert res.length == 2
+               for player in res do
+                       assert users.has(player.name)
+               end
+               db.drop
+       end
+
+       fun test_load_player do
+               var db = load_db("test_load_player")
+               var game = load_game("privat/nit", db)
+               var ogame = load_game("Morriar/nit", db)
+
+               var player1 = new Player(game, "Morriar")
+               var player2 = new Player(ogame, "privat")
+               game.db.collection("players").insert(player1.to_json)
+               ogame.db.collection("players").insert(player2.to_json)
+
+               assert game.load_player("privat") == null
+               assert game.load_player("Morriar").name == "Morriar"
+               assert ogame.load_player("privat").name == "privat"
+               assert ogame.load_player("Morriar") == null
+               db.drop
+       end
+
+       fun test_load_players do
+               var db = load_db("test_load_players")
+               var game = load_game("privat/nit", db)
+               var ogame = load_game("Morriar/nit", db)
+
+               var player1 = new Player(game, "Morriar")
+               var player2 = new Player(ogame, "privat")
+               var player3 = new Player(game, "xymus")
+               game.db.collection("players").insert(player1.to_json)
+               ogame.db.collection("players").insert(player2.to_json)
+               game.db.collection("players").insert(player3.to_json)
+
+               var players = game.load_players
+               var ok = ["Morriar", "xymus"]
+               for player in players.values do assert ok.has(player.name)
+               db.drop
+       end
+end
+
+class TestPlayer
+       super NitrpgTestHelper
+
+       fun test_init do
+               var db = load_db("test_init")
+               var game = load_game("privat/nit", db)
+               var player = new Player(game, "Morriar")
+               assert player.name == "Morriar"
+               assert player.user.login == "Morriar"
+               assert player.nitcoins == 0
+               db.drop
+       end
+
+       fun test_init_from_json do
+               var db = load_db("test_init_from_json")
+               var game = load_game("privat/nit", db)
+               var json = """{"name": "Morriar", "nitcoins": 10}""".parse_json
+               var player = new Player.from_json(game, json.as(JsonObject))
+               assert player.name == "Morriar"
+               assert player.user.login == "Morriar"
+               assert player.nitcoins == 10
+               db.drop
+       end
+
+       fun test_save do
+               var db = load_db("test_save")
+               var game = load_game("privat/nit", db)
+               var json = """{"name": "Morriar", "nitcoins": 10}""".parse_json.as(JsonObject)
+               var player = new Player.from_json(game, json)
+               player.save
+               assert game.db.collection("players").find(json) != null
+               db.drop
+       end
+
+       fun test_game_add_player do
+               var db = load_db("test_game_add_player")
+               var game = load_game("privat/nit", db)
+               game.add_player(game.api.load_user("Morriar").as(not null))
+               var json = """{"name": "Morriar"}""".parse_json.as(JsonObject)
+               assert game.db.collection("players").find(json) != null
+               db.drop
+       end
+
+       fun test_game_load_player do
+               var db = load_db("test_game_load_player")
+               var game = load_game("privat/nit", db)
+               var json = """{"name": "Morriar", "nitcoins": 10}""".parse_json.as(JsonObject)
+               var player = new Player.from_json(game, json)
+               player.save
+               var oplayer = game.load_player("Morriar")
+               assert oplayer != null
+               assert player.nitcoins == oplayer.nitcoins
+               db.drop
+       end
+end
+
+class TestUser
+       super NitrpgTestHelper
+
+       fun test_player do
+               var db = load_db("test_player")
+               var api = new GithubAPI(get_github_oauth)
+               var game = load_game("privat/nit", db)
+               var user = api.load_user("Morriar")
+               assert user != null
+               var player = user.player(game)
+               assert player.name == "Morriar"
+               game.clear
+               db.drop
+       end
+end
diff --git a/contrib/nitrpg/src/test_helper.nit b/contrib/nitrpg/src/test_helper.nit
new file mode 100644 (file)
index 0000000..d8ba14e
--- /dev/null
@@ -0,0 +1,54 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test tools for NitRPG.
+module test_helper is test_suite
+
+import test_suite
+import game
+import github::cache
+
+# Used to factorize test treatments.
+abstract class NitrpgTestHelper
+       super TestSuite
+
+       # Github API client
+       var api: GithubAPI do
+               var api = new GithubAPI(get_github_oauth)
+               api.enable_cache = true
+               return api
+       end
+
+       # Mongo API client
+       var mongo = new MongoClient("mongodb://localhost:27017/")
+
+       # Load a new test database by with a name
+       fun load_db(name: String): MongoDb do return mongo.database(name)
+
+       # Load a repo by its name.
+       fun load_repo(name: String): Repo do
+               var repo = api.load_repo(name)
+               assert repo != null
+               return repo
+       end
+
+       # Load a game by its name.
+       fun load_game(name: String, db: MongoDb): Game do
+               var game = new Game(api, load_repo(name))
+               game.db_name = db.name
+               return game
+       end
+end
diff --git a/contrib/nitrpg/src/test_listener.nit b/contrib/nitrpg/src/test_listener.nit
new file mode 100644 (file)
index 0000000..edbfe37
--- /dev/null
@@ -0,0 +1,369 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test module for `listener.nit`
+module test_listener is test_suite
+
+import test_helper
+import reactors
+import achievements
+import events_generator
+
+private class DummyListener
+       super NitrpgTestHelper
+
+       var reactors = new Array[GameReactor]
+
+       fun apply_event(event: GithubEvent, db: MongoDb) do
+               var game = load_game(event.repo.full_name, db)
+               for reactor in reactors do
+                       reactor.react_event(game, event)
+               end
+       end
+
+       fun add_reactor(reactors: GameReactor...) do self.reactors.add_all reactors
+end
+
+class TestListener
+       super NitrpgTestHelper
+
+       var generator = new EventsGenerator(api)
+
+       var repo: Repo is lazy do return load_repo("Morriar/nit")
+
+       fun test_game_issue_stats do
+               var db = load_db("test_game_issue_stats")
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+
+               var issue = api.load_issue(repo, 322)
+               assert issue != null
+
+               l.apply_event(generator.issue_open(issue), db)
+               var game = load_game("Morriar/nit", db)
+               assert game.stats.overall["issues"] == 1
+               assert game.stats.overall["issues_open"] == 1
+               l.apply_event(generator.issue_close(issue), db)
+               game = load_game("Morriar/nit", db)
+               assert game.stats.overall["issues"] == 1
+               assert game.stats.overall["issues_open"] == 0
+               l.apply_event(generator.issue_reopen(issue), db)
+               game = load_game("Morriar/nit", db)
+               assert game.stats.overall["issues"] == 1
+               assert game.stats.overall["issues_open"] == 1
+               db.drop
+       end
+
+       fun test_player_issue_stats do
+               var db = load_db("test_player_issue_stats")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+
+               var issue = api.load_issue(repo, 322)
+               assert issue != null
+
+               l.apply_event(generator.issue_open(issue), db)
+               var player = new Player(game, "Morriar")
+               assert player.stats.overall["issues"] == 1
+               assert player.stats.overall["issues_open"] == 1
+               l.apply_event(generator.issue_close(issue), db)
+               player = new Player(game, "Morriar")
+               assert player.stats.overall["issues"] == 1
+               assert player.stats.overall["issues_open"] == 0
+               l.apply_event(generator.issue_reopen(issue), db)
+               player = new Player(game, "Morriar")
+               assert player.stats.overall["issues"] == 1
+               assert player.stats.overall["issues_open"] == 1
+               db.drop
+       end
+
+       fun test_game_pr_stats do
+               var db = load_db("test_game_pr_stats")
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+
+               var pr = api.load_pull(repo, 275)
+               assert pr != null
+
+               l.apply_event(generator.pull_open(pr), db)
+               var game = load_game("Morriar/nit", db)
+               assert game.stats.overall["pulls"] == 1
+               assert game.stats.overall["pulls_open"] == 1
+               assert game.stats.overall["commits"] == 0
+               pr.merged = false
+               l.apply_event(generator.pull_close(pr), db)
+               game = load_game("Morriar/nit", db)
+               assert game.stats.overall["pulls"] == 1
+               assert game.stats.overall["pulls_open"] == 0
+               assert game.stats.overall["commits"] == 0
+               l.apply_event(generator.pull_reopen(pr), db)
+               game = load_game("Morriar/nit", db)
+               assert game.stats.overall["pulls"] == 1
+               assert game.stats.overall["pulls_open"] == 1
+               assert game.stats.overall["commits"] == 0
+               pr.merged = true
+               l.apply_event(generator.pull_close(pr), db)
+               game = load_game("Morriar/nit", db)
+               assert game.stats.overall["pulls"] == 1
+               assert game.stats.overall["pulls_open"] == 0
+               assert game.stats.overall["commits"] == 2
+               db.drop
+       end
+
+       fun test_game_issue_comment_stats do
+               var db = load_db("test_game_issue_comment_stats")
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+
+               var issue = api.load_issue(repo, 322)
+               assert issue != null
+               var comment = api.load_issue_comment(repo, 76119442)
+               assert comment != null
+
+               comment.body = "foo bar"
+               l.apply_event(generator.issue_comment_event(issue, comment), db)
+               var game = load_game("Morriar/nit", db)
+               assert game.stats.overall["comments"] == 1
+               assert game.stats.overall["reviews"] == 0
+               comment.body = "foo +1 bar"
+               l.apply_event(generator.issue_comment_event(issue, comment), db)
+               game = load_game("Morriar/nit", db)
+               assert game.stats.overall["comments"] == 2
+               assert game.stats.overall["reviews"] == 1
+               db.drop
+       end
+
+       fun test_player_pull_reactor do
+               var db = load_db("test_player_pull_reactor")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new PlayerReactor)
+
+               var pull = api.load_pull(repo, 275)
+               assert pull != null
+
+               l.apply_event(generator.pull_open(pull), db)
+               var player = new Player(game, "itch76")
+               assert player.stats.overall["nitcoins"] == 10
+               pull.merged = false
+               l.apply_event(generator.pull_close(pull), db)
+               player = new Player(game, "itch76")
+               assert player.stats.overall["nitcoins"] == 0
+               l.apply_event(generator.pull_reopen(pull), db)
+               player = new Player(game, "itch76")
+               assert player.stats.overall["nitcoins"] == 10
+               pull.merged = true
+               l.apply_event(generator.pull_close(pull), db)
+               player = new Player(game, "itch76")
+               assert player.stats.overall["nitcoins"] == 12
+               db.drop
+       end
+
+       fun test_player_review_reactor do
+               var db = load_db("test_player_review_reactor")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new PlayerReactor)
+
+               var pull = api.load_pull(repo, 275)
+               assert pull != null
+               var comment = api.load_issue_comment(repo, 36961230)
+               assert comment != null
+
+               # TODO handle multiple review by the same user
+
+               # no review in opened issue
+               pull.state = "open"
+               comment.body = "foo bar"
+               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               var player = new Player(game, "Morriar")
+               assert player.stats.overall["nitcoins"] == 0
+
+               # review in opened issue
+               pull.state = "open"
+               comment.body = "foo +1 bar"
+               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               player = new Player(game, "Morriar")
+               print player.stats.overall["nitcoins"]
+               assert player.stats.overall["nitcoins"] == 2
+
+               # review in closed issue
+               pull.state = "closed"
+               comment.body = "foo +1 bar"
+               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               player = new Player(game, "Morriar")
+               assert player.stats.overall["nitcoins"] == 2
+
+               # review in reopened issue
+               pull.state = "open"
+               comment.body = "foo +1 bar"
+               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               player = new Player(game, "Morriar")
+               assert player.stats.overall["nitcoins"] == 4
+               db.drop
+       end
+
+       fun test_X_issues_achievements do
+               var db = load_db("test_X_issues_achievements")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+               l.add_reactor(new Player1Issue, new Player100Issues, new Player1KIssues)
+
+               var issue = api.load_issue(repo, 322)
+               assert issue != null
+
+               for i in [0, 99, 999] do
+                       var id = "player_{i + 1}_issue"
+                       if i > 0 then id = "{id}s"
+                       var player = new Player(game, "Morriar")
+                       player.stats["issues"] = i
+                       player.save
+                       l.apply_event(generator.issue_open(issue), db)
+                       assert player.load_achievements.has_key(id)
+               end
+               var player = new Player(game, "Morriar")
+               assert player.stats.overall["nitcoins"] == 1110
+               db.drop
+       end
+
+       fun test_X_pulls_achievements do
+               var db = load_db("test_X_pulls_achievements")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+               l.add_reactor(new Player1Pull, new Player100Pulls, new Player1KPulls)
+
+               var pull = api.load_pull(repo, 275)
+               assert pull != null
+
+               for i in [0, 99, 999] do
+                       var id = "player_{i + 1}_pull"
+                       if i > 0 then id = "{id}s"
+                       var player = new Player(game, "itch76")
+                       player.stats["pulls"] = i
+                       player.save
+                       l.apply_event(generator.pull_open(pull), db)
+                       assert player.load_achievements.has_key(id)
+               end
+               var player = new Player(game, "itch76")
+               assert player.stats.overall["nitcoins"] == 1110
+               db.drop
+       end
+
+       fun test_X_commits_achievements do
+               var db = load_db("test_X_commits_achievements")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+               l.add_reactor(new Player1Commit, new Player100Commits)
+               l.add_reactor(new Player1KCommits, new Player10KCommits)
+
+               var pull = api.load_pull(repo, 275)
+               assert pull != null
+               pull.state = "closed"
+               pull.merged = true
+
+               for i in [0, 99, 999, 9999] do
+                       var id = "player_{i + 1}_commit"
+                       if i > 0 then id = "{id}s"
+                       var player = new Player(game, "itch76")
+                       player.stats["commits"] = i
+                       player.save
+                       l.apply_event(generator.pull_close(pull), db)
+                       assert player.load_achievements.has_key(id)
+               end
+               var player = new Player(game, "itch76")
+               assert player.stats.overall["nitcoins"] == 11110
+               db.drop
+       end
+
+       fun test_X_comments_achievements do
+               var db = load_db("test_X_comments_achievements")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new StatisticsReactor)
+               l.add_reactor(new Player1Comment, new Player100Comments, new Player1KComments)
+
+               var pull = api.load_pull(repo, 275)
+               assert pull != null
+               var comment = api.load_issue_comment(repo, 36961230)
+               assert comment != null
+
+               for i in [0, 99, 999] do
+                       var id = "player_{i + 1}_comment"
+                       if i > 0 then id = "{id}s"
+                       var player = new Player(game, "Morriar")
+                       player.stats["comments"] = i
+                       player.save
+                       l.apply_event(generator.issue_comment_event(pull, comment), db)
+                       assert player.load_achievements.has_key(id)
+               end
+               var player = new Player(game, "Morriar")
+               assert player.stats.overall["nitcoins"] == 1110
+               db.drop
+       end
+
+    fun test_issues_achievements do
+               var db = load_db("test_issues_achievements")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new IssueAboutNitdoc, new IssueAboutFFI)
+
+               var issue = api.load_issue(repo, 322)
+               assert issue != null
+
+               issue.title = "nitdoc ffi"
+               l.apply_event(generator.issue_open(issue), db)
+               var player = new Player(game, "Morriar")
+               assert player.load_achievements.has_key("issue_about_nitdoc")
+               assert player.load_achievements.has_key("issue_about_ffi")
+               assert player.stats.overall["nitcoins"] == 20
+               db.drop
+       end
+
+       fun test_comments_reactor do
+               var db = load_db("test_comments_reactor")
+               var game = load_game("Morriar/nit", db)
+               var l = new DummyListener
+               l.add_reactor(new PlayerPingGod, new PlayerFirstReview, new PlayerSaysNitcoin)
+
+               var pull = api.load_pull(repo, 275)
+               assert pull != null
+               var comment = api.load_issue_comment(repo, 36961230)
+               assert comment != null
+
+               comment.body = "@{game.repo.owner.login}"
+               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               var player = new Player(game, "Morriar")
+               assert player.load_achievements.has_key("player_ping_god")
+               assert player.stats.overall["nitcoins"] == 50
+
+               comment.body = "+1"
+               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               player = new Player(game, "Morriar")
+               assert player.load_achievements.has_key("player_first_review")
+               assert player.stats.overall["nitcoins"] == 60
+
+               comment.body = "Nitcoins"
+               l.apply_event(generator.issue_comment_event(pull, comment), db)
+               player = new Player(game, "Morriar")
+               assert player.load_achievements.has_key("player_says_nitcoin")
+               assert player.stats.overall["nitcoins"] == 70
+               db.drop
+       end
+end
diff --git a/contrib/nitrpg/src/test_statistics.nit b/contrib/nitrpg/src/test_statistics.nit
new file mode 100644 (file)
index 0000000..aa4c893
--- /dev/null
@@ -0,0 +1,83 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test module for `stats.nit`
+module test_statistics is test_suite
+
+import test_helper
+import statistics
+
+class TestGame
+       super NitrpgTestHelper
+
+       fun test_game_stats do
+               var db = load_db("test_game_stats")
+               var game = load_game("privat/nit", db)
+               var stats = game.stats
+               assert stats.overall["test"] == 0
+               stats.overall.inc("test")
+               assert stats.overall["test"] == 1
+               stats.save
+               var ogame = load_game("privat/nit", db)
+               var ostats = ogame.stats
+               ostats.overall.inc("test")
+               assert ostats.overall["test"] == 2
+               db.drop
+       end
+end
+
+class TestPlayer
+       super NitrpgTestHelper
+
+       fun test_player_stats do
+               var db = load_db("test_player_stats")
+               var game = load_game("privat/nit", db)
+               var player = new Player(game, "Morriar")
+               var stats = player.stats
+               assert stats.overall["test"] == 0
+               stats.overall.inc("test")
+               assert stats.overall["test"] == 1
+               stats.save
+               var oplayer = new Player(game, "Morriar")
+               var ostats = oplayer.stats
+               ostats.overall.inc("test")
+               assert ostats.overall["test"] == 2
+               db.drop
+       end
+end
+
+class TestGameStats
+       super NitrpgTestHelper
+
+       fun test_init_from_json do
+               var db = load_db("test_init_from_json")
+               var game = load_game("privat/nit", db)
+               var owner = new Player(game, "Morriar")
+               var json = """{
+                       "period": "2015",
+                       "owner": "Morriar",
+                       "values": {
+                               "test1": 10,
+                               "test2": 20
+                       }
+               }""".parse_json.as(JsonObject)
+               var stats = new GameStats.from_json(game, "2015", owner, json)
+               assert stats["test0"] == 0
+               assert stats["test1"] == 10
+               assert stats["test2"] == 20
+               db.drop
+       end
+end
index 38ed68e..f624f48 100644 (file)
@@ -57,8 +57,8 @@ class RpgAction
        # Returns the game with `name` or null if no game exists with this name.
        fun load_game(name: String): nullable Game do
                var repo = api.load_repo(name)
-               if api.was_error or repo == null then return null
-               var game = new Game(api, repo)
+               if repo == null then return null
+               var game = new Game.from_mongo(api, repo)
                game.root_url = root_url
                return game
        end
@@ -66,13 +66,16 @@ class RpgAction
        # Returns the list of saved games from NitRPG data.
        fun load_games: Array[Game] do
                var res = new Array[Game]
-               var rpgdir = "nitrpg_data"
-               if not rpgdir.file_exists then return res
-               for user in rpgdir.files do
-                       for repo in "{rpgdir}/{user}".files do
-                               var game = load_game("{user}/{repo}")
-                               if game != null then res.add game
-                       end
+               # TODO should be option
+               var mongo = new MongoClient("mongodb://localhost:27017")
+               var db = mongo.database("nitrpg")
+               for obj in db.collection("games").find_all(new JsonObject) do
+                       var repo = api.load_repo(obj["name"].to_s)
+                       assert repo != null
+                       var game = new Game(api, repo)
+                       game.from_json(obj)
+                       game.root_url = root_url
+                       res.add game
                end
                return res
        end
index 67e6765..d55bad0 100644 (file)
@@ -499,4 +499,12 @@ class ConcurrentList[E]
                real_collection.push(e)
                mutex.unlock
        end
+
+       redef fun shift
+       do
+               mutex.lock
+               var value = real_collection.shift
+               mutex.unlock
+               return value
+       end
 end
diff --git a/lib/pthreads/examples/jointask_example.nit b/lib/pthreads/examples/jointask_example.nit
new file mode 100644 (file)
index 0000000..3d12f6f
--- /dev/null
@@ -0,0 +1,52 @@
+# This file is part of NIT (http://www.nitlanguage.org).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Simple example of joinable task using threadpool
+module jointask_example
+
+import threadpool
+
+# Task computing a string
+class StringTask
+       super JoinTask
+
+       # Sleeping time
+       var sec: Int
+
+       # result of `self` execution
+       var value: String
+
+       # ID for printing
+       var id: Int
+
+       redef fun main do
+               nanosleep(sec, 0)
+               value += " id: {id}"
+       end
+end
+
+var tp = new ThreadPool
+var t0 = new StringTask(10, "First, long task", 0)
+var tasks = new Array[StringTask]
+for i in 5.times do
+       tasks.add(new StringTask(1, "Small task", i + 1))
+end
+tp.execute(t0)
+for t in tasks do tp.execute(t)
+for t in tasks do
+       t.join
+       print t.value
+end
+t0.join
+print t0.value
index f7fafd7..fec1ae2 100644 (file)
@@ -19,7 +19,7 @@ import threadpool
 
 # Task printing "hello world" on standard output
 class HWTask
-       super Task
+       super JoinTask
 
        # Sleeping time
        var sec: Int
index 42931d3..02042ba 100644 (file)
@@ -20,7 +20,7 @@ import concurrent_collections
 
 # A simple ThreadPool implemented with an array
 class ThreadPool
-       private var queue = new ConcurrentList[Task].wrap(new List[Task])
+       private var queue = new ConcurrentList[JoinTask].wrap(new List[JoinTask])
        private var mutex = new Mutex
        private var cond = new NativePthreadCond
 
@@ -37,7 +37,7 @@ class ThreadPool
        private fun set_nb_threads(nb: nullable Int) is autoinit do nb_threads = nb or else 5
 
        # Adds a Task into the queue
-       fun execute(task: Task) do
+       fun execute(task: JoinTask) do
                queue.push(task)
                cond.signal
        end
@@ -47,20 +47,49 @@ end
 private class PoolThread
        super Thread
 
-       var queue: ConcurrentList[Task]
+       var queue: ConcurrentList[JoinTask]
        var mutex: Mutex
        var cond : NativePthreadCond
 
        redef fun main do
                loop
-                       var t: nullable Task = null
+                       var t: nullable JoinTask = null
                        mutex.lock
                        if queue.is_empty then cond.wait(mutex.native.as(not null))
                        if not queue.is_empty then
-                               t = queue.pop
+                               t = queue.shift
                        end
                        mutex.unlock
-                       if t != null then t.main
+                       if t != null then
+                               t.main
+                               t.mutex.lock
+                               t.is_done = true
+                               var tcond = t.cond
+                               if tcond != null then tcond.signal
+                               t.mutex.unlock
+                       end
+               end
+       end
+end
+
+# A Task which is joinable, meaning it can return a value and if the value is not set yet, it blocks the execution
+class JoinTask
+       super Task
+
+       # Is `self` done ?
+       var is_done = false
+
+       private var mutex = new Mutex
+       private var cond: nullable NativePthreadCond = null
+
+       # Return immediatly if the task terminated, or block waiting for `self` to terminate
+       fun join do
+               mutex.lock
+               if not is_done then
+                       var cond = new NativePthreadCond
+                       self.cond = cond
+                       cond.wait(mutex.native.as(not null))
                end
+               mutex.unlock
        end
 end
diff --git a/tests/sav/jointask_example.res b/tests/sav/jointask_example.res
new file mode 100644 (file)
index 0000000..a0f7bca
--- /dev/null
@@ -0,0 +1,6 @@
+Small task id: 1
+Small task id: 2
+Small task id: 3
+Small task id: 4
+Small task id: 5
+First, long task id: 0