Merge: nitcorn server for nitiwiki
authorJean Privat <jean@pryen.org>
Tue, 8 Dec 2015 21:18:45 +0000 (16:18 -0500)
committerJean Privat <jean@pryen.org>
Tue, 8 Dec 2015 21:18:45 +0000 (16:18 -0500)
This PR contains the initial work of @ablondin and integrates it further with nitiwiki.

Intro a simple nitcorn server to publish the static files generated by nitiwiki and allow modifications from a web form. It is already applied on http://xymus.net/ to manage the main page.

The server reads the `config.ini` of the target to find the path to the public files. It writes the new markdown to the source folder and regenerates the wiki on each modification. The modification forms apply the template of the current wiki. (with some imperfections)

I've added a basic password authentication system using a list of hashed passwords in a simple text file. It should be enough for simple deployment of the server and this file can be ignored by git.

Limitations:
* Not integrated with all configuration of a nitiwiki server.
* Creating new files is supported by manually changing the path in the URI, but creating parent folders of a file is not supported.
* Some paths configuration may cause problems, the `edit/` page must be at the root of the server and other similar restrictions were not fully tested.
* There's no integration with git.

Closes #1832

Pull-Request: #1877
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>
Reviewed-by: Jean Privat <jean@pryen.org>

27 files changed:
contrib/nitcc/src/autom.nit
contrib/nitcc/src/nitcc_semantic.nit
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/gamnit/examples/triangle/src/portable_triangle.nit
lib/gamnit/examples/triangle/src/standalone_triangle.nit
lib/glesv2/examples/opengles2_hello_triangle.nit
lib/glesv2/glesv2.nit
lib/json/json_lexer.nit
lib/nitcc_runtime.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 946b420..84df722 100644 (file)
@@ -688,17 +688,28 @@ private class DFAGenerator
                                        token = null
                                end
                                add("\tredef fun is_accept do return true\n")
-                               add("\tredef fun make_token(position, text) do\n")
+                               var is_ignored = false
                                if token != null and token.name == "Ignored" then
+                                       is_ignored = true
+                                       add("\tredef fun is_ignored do return true\n")
+                               end
+                               add("\tredef fun make_token(position, source) do\n")
+                               if is_ignored then
                                        add("\t\treturn null\n")
                                else
                                        if token == null then
                                                add("\t\tvar t = new MyNToken\n")
+                                               add("\t\tt.text = position.extract(source)\n")
                                        else
                                                add("\t\tvar t = new {token.cname}\n")
+                                               var ttext = token.text
+                                               if ttext == null then
+                                                       add("\t\tt.text = position.extract(source)\n")
+                                               else
+                                                       add("\t\tt.text = \"{ttext.escape_to_nit}\"\n")
+                                               end
                                        end
                                        add("\t\tt.position = position\n")
-                                       add("\t\tt.text = text\n")
                                        add("\t\treturn t\n")
                                end
                                add("\tend\n")
@@ -715,22 +726,50 @@ private class DFAGenerator
                                add("\tredef fun trans(char) do\n")
 
                                add("\t\tvar c = char.code_point\n")
-                               var haslast = false
+
+                               # Collect the sequence of tests in the dispatch sequence
+                               # The point here is that for each transition, there is a first and a last
+                               # So holes hare to be identified
+                               var dispatch = new HashMap[Int, nullable State]
+                               var haslast: nullable State = null
+
                                var last = -1
                                for sym, next in trans do
-                                       assert not haslast
+                                       assert haslast == null
                                        assert sym.first > last
-                                       if sym.first > last + 1 then add("\t\tif c <= {sym.first-1} then return null\n")
+                                       if sym.first > last + 1 then
+                                               dispatch[sym.first-1] = null
+                                       end
                                        var l = sym.last
                                        if l == null then
-                                               add("\t\treturn dfastate_{names[next]}\n")
-                                               haslast= true
+                                               haslast = next
                                        else
-                                               add("\t\tif c <= {l} then return dfastate_{names[next]}\n")
+                                               dispatch[l] = next
                                                last = l
                                        end
                                end
-                               if not haslast then add("\t\treturn null\n")
+
+                               # Generate a sequence of `if` for the dispatch
+                               if haslast != null then
+                                       # Special case: handle up-bound first if not an error
+                                       add("\t\tif c > {last} then return dfastate_{names[haslast]}\n")
+                                       # previous become the new last case
+                                       haslast = dispatch[last]
+                                       dispatch.keys.remove(last)
+                               end
+                               for c, next in dispatch do
+                                       if next == null then
+                                               add("\t\tif c <= {c} then return null\n")
+                                       else
+                                               add("\t\tif c <= {c} then return dfastate_{names[next]}\n")
+                                       end
+                               end
+                               if haslast == null then
+                                       add("\t\treturn null\n")
+                               else
+                                       add("\t\treturn dfastate_{names[haslast]}\n")
+                               end
+
                                add("\tend\n")
                        end
                        add("end\n")
@@ -740,6 +779,11 @@ private class DFAGenerator
        end
 end
 
+redef class Token
+       # The associated text (if any, ie defined in the parser part)
+       var text: nullable String is noautoinit, writable
+end
+
 # A state in a finite automaton
 class State
        # Outgoing transitions
index 795cee0..299b46c 100644 (file)
@@ -572,8 +572,6 @@ end
 redef class Token
        # The associated expression (if any, ie defined in the lexer part)
        var nexpr: nullable Nexpr
-       # The associated text (if any, ie defined in the parser part)
-       var text: nullable String
 
        # Build a NFA according to nexpr or text
        # Does not tag it!
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 624ec47..8903c9c 100644 (file)
@@ -49,17 +49,17 @@ redef class App
                print "Width: {display.width}"
                print "Height: {display.height}"
 
-               assert_no_gl_error
+               assert glGetError == gl_NO_ERROR
                assert gl.shader_compiler else print "Cannot compile shaders"
 
                # GL program
                program = new GLProgram
                if not glIsProgram(program) then
                        print "Program is not ok: {glGetError.to_s}\nLog:"
-                       print program.info_log
+                       print glGetProgramInfoLog(program)
                        abort
                end
-               assert_no_gl_error
+               assert glGetError == gl_NO_ERROR
 
                # Vertex shader
                vertex_shader = new GLVertexShader
@@ -72,8 +72,8 @@ redef class App
                }
                """@glsl_vertex_shader.to_cstring)
                glCompileShader(vertex_shader)
-               assert vertex_shader.is_compiled else print "Vertex shader compilation failed with: {vertex_shader.info_log} {program.info_log}"
-               assert_no_gl_error
+               assert vertex_shader.is_compiled else print "Vertex shader compilation failed with: {glGetShaderInfoLog(vertex_shader)} {glGetProgramInfoLog(program)}"
+               assert glGetError == gl_NO_ERROR
 
                # Fragment shader
                fragment_shader = new GLFragmentShader
@@ -86,16 +86,16 @@ redef class App
                }
                """@glsl_fragment_shader.to_cstring)
                glCompileShader(fragment_shader)
-               assert fragment_shader.is_compiled else print "Fragment shader compilation failed with: {fragment_shader.info_log}"
-               assert_no_gl_error
+               assert fragment_shader.is_compiled else print "Fragment shader compilation failed with: {glIsShader(fragment_shader)}"
+               assert glGetError == gl_NO_ERROR
 
                # Attach to program
                glAttachShader(program, vertex_shader)
                glAttachShader(program, fragment_shader)
                program.bind_attrib_location(0, "vPosition")
                glLinkProgram program
-               assert program.is_linked else print "Linking failed: {program.info_log}"
-               assert_no_gl_error
+               assert program.is_linked else print "Linking failed: {glGetProgramInfoLog(program)}"
+               assert glGetError == gl_NO_ERROR
 
                # Draw!
                var vertices = [0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0]
@@ -111,7 +111,7 @@ redef class App
                if display != null then
                        glClearColor(t, t, t, 1.0)
 
-                       assert_no_gl_error
+                       assert glGetError == gl_NO_ERROR
                        glViewport(0, 0, display.width, display.height)
                        glClear gl_COLOR_BUFFER_BIT
                        glUseProgram program
index 209597f..9840d15 100644 (file)
@@ -36,7 +36,7 @@ print "Width: {width}"
 print "Height: {height}"
 
 # Custom calls to OpenGL ES 2.0
-assert_no_gl_error
+assert glGetError == gl_NO_ERROR
 assert gl.shader_compiler else print "Cannot compile shaders"
 
 # GL program
@@ -44,10 +44,10 @@ print glGetError.to_s
 var program = new GLProgram
 if not glIsProgram(program) then
        print "Program is not ok: {glGetError.to_s}\nLog:"
-       print program.info_log
+       print glGetProgramInfoLog(program)
        abort
 end
-assert_no_gl_error
+assert glGetError == gl_NO_ERROR
 
 # Vertex shader
 var vertex_shader = new GLVertexShader
@@ -60,8 +60,8 @@ void main()
 }
 """@glsl_vertex_shader.to_cstring)
 glCompileShader vertex_shader
-assert vertex_shader.is_compiled else print "Vertex shader compilation failed with: {vertex_shader.info_log} {program.info_log}"
-assert_no_gl_error
+assert vertex_shader.is_compiled else print "Vertex shader compilation failed with: {glGetShaderInfoLog(vertex_shader)} {glGetProgramInfoLog(program)}"
+assert glGetError == gl_NO_ERROR
 
 # Fragment shader
 var fragment_shader = new GLFragmentShader
@@ -74,16 +74,16 @@ void main()
 }
 """@glsl_fragment_shader.to_cstring)
 glIsShader fragment_shader
-assert fragment_shader.is_compiled else print "Fragment shader compilation failed with: {fragment_shader.info_log}"
-assert_no_gl_error
+assert fragment_shader.is_compiled else print "Fragment shader compilation failed with: {glGetShaderInfoLog(fragment_shader)}"
+assert glGetError == gl_NO_ERROR
 
 # Attach to program
 glAttachShader(program, vertex_shader)
 glAttachShader(program, fragment_shader)
 program.bind_attrib_location(0, "vPosition")
 glLinkProgram program
-assert program.is_linked else print "Linking failed: {program.info_log}"
-assert_no_gl_error
+assert program.is_linked else print "Linking failed: {glGetProgramInfoLog(program)}"
+assert glGetError == gl_NO_ERROR
 
 # Draw!
 var vertices = [0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0]
@@ -92,7 +92,7 @@ vertex_array.attrib_pointer
 glClearColor(0.5, 0.0, 0.5, 1.0)
 for i in [0..1000[ do
        printn "."
-       assert_no_gl_error
+       assert glGetError == gl_NO_ERROR
        glViewport(0, 0, width, height)
        glClear gl_COLOR_BUFFER_BIT
        glUseProgram program
index 340c307..abf21b3 100644 (file)
@@ -116,7 +116,7 @@ print glGetError.to_s
 var program = new GLProgram
 if not glIsProgram(program) then
        print "Program is not ok: {glGetError.to_s}\nLog:"
-       print program.info_log
+       print glGetProgramInfoLog(program)
        abort
 end
 assert_no_gl_error
@@ -132,7 +132,7 @@ void main()
 }
 """.to_cstring)
 glCompileShader vertex_shader
-assert vertex_shader.is_compiled else print "Vertex shader compilation failed with: {vertex_shader.info_log} {program.info_log}"
+assert vertex_shader.is_compiled else print "Vertex shader compilation failed with: {glGetShaderInfoLog(vertex_shader)} {glGetProgramInfoLog(program)}"
 assert_no_gl_error
 
 # fragment shader
@@ -146,14 +146,14 @@ void main()
 }
 """.to_cstring)
 glCompileShader(fragment_shader)
-assert fragment_shader.is_compiled else print "Fragment shader compilation failed with: {fragment_shader.info_log}"
+assert fragment_shader.is_compiled else print "Fragment shader compilation failed with: {glGetShaderInfoLog(fragment_shader)}"
 assert_no_gl_error
 
 glAttachShader(program, vertex_shader)
 glAttachShader(program, fragment_shader)
 program.bind_attrib_location(0, "vPosition")
 glLinkProgram program
-assert program.is_linked else print "Linking failed: {program.info_log}"
+assert program.is_linked else print "Linking failed: {glGetProgramInfoLog(program)}"
 assert_no_gl_error
 
 # draw!
index caea237..38ebc89 100644 (file)
@@ -72,69 +72,52 @@ extern class GLProgram `{GLuint`}
                return glGetUniformLocation(self, c_name);
        `}
 
-       # Query information on this program
-       fun query(pname: Int): Int `{
-               int val;
-               glGetProgramiv(self, pname, &val);
-               return val;
-       `}
-
        # Is this program linked?
-       fun is_linked: Bool do return query(0x8B82) != 0
+       fun is_linked: Bool do return glGetProgramiv(self, gl_LINK_STATUS) != 0
 
        # Has this program been deleted?
-       fun is_deleted: Bool do return query(0x8B80) != 0
+       fun is_deleted: Bool do return glGetProgramiv(self, gl_DELETE_STATUS) != 0
 
        # Boolean result of `validate`, must be called after `validate`
-       fun is_validated: Bool do return query(0x8B83) != 0
-
-       # Retrieve the information log of this program
-       #
-       # Useful with `link` and `validate`
-       fun info_log: String import NativeString.to_s `{
-               int size;
-               glGetProgramiv(self, GL_INFO_LOG_LENGTH, &size);
-               GLchar *msg = malloc(size);
-               glGetProgramInfoLog(self, size, NULL, msg);
-               return NativeString_to_s(msg);
-       `}
+       fun is_validated: Bool do return glGetProgramiv(self, gl_VALIDATE_STATUS) != 0
 
        # Number of active uniform in this program
        #
        # This should be the number of uniforms declared in all shader, except
        # unused uniforms which may have been optimized out.
-       fun n_active_uniforms: Int do return query(0x8B86)
+       fun n_active_uniforms: Int do return glGetProgramiv(self, gl_ACTIVE_UNIFORMS)
 
-       # Length of the longest uniform name in this program, including `\n`
-       fun active_uniform_max_length: Int do return query(0x8B87)
+       # Length of the longest uniform name in this program, including the null byte
+       fun active_uniform_max_length: Int do return glGetProgramiv(self, gl_ACTIVE_UNIFORM_MAX_LENGTH)
 
        # Number of active attributes in this program
        #
        # This should be the number of uniforms declared in all shader, except
        # unused uniforms which may have been optimized out.
-       fun n_active_attributes: Int do return query(0x8B89)
+       fun n_active_attributes: Int do return glGetProgramiv(self, gl_ACTIVE_ATTRIBUTES)
 
-       # Length of the longest uniform name in this program, including `\n`
-       fun active_attribute_max_length: Int do return query(0x8B8A)
+       # Length of the longest attribute name in this program, including the null byte
+       fun active_attribute_max_length: Int do return glGetProgramiv(self, gl_ACTIVE_ATTRIBUTE_MAX_LENGTH)
 
        # Number of shaders attached to this program
-       fun n_attached_shaders: Int do return query(0x8B85)
+       fun n_attached_shaders: Int do return glGetProgramiv(self, gl_ATTACHED_SHADERS)
 
        # Name of the active attribute at `index`
        fun active_attrib_name(index: Int): String
        do
                var max_size = active_attribute_max_length
-               return active_attrib_name_native(index, max_size).to_s
+               var cname = new NativeString(max_size)
+               active_attrib_name_native(index, max_size, cname)
+               return cname.to_s
        end
-       private fun active_attrib_name_native(index, max_size: Int): NativeString `{
+
+       private fun active_attrib_name_native(index, max_size: Int, name: NativeString) `{
                // We get more values than we need, for compatibility. At least the
                // NVidia driver tries to fill them even if NULL.
 
-               char *name = malloc(max_size);
                int size;
                GLenum type;
                glGetActiveAttrib(self, index, max_size, NULL, &size, &type, name);
-               return name;
        `}
 
        # Size of the active attribute at `index`
@@ -148,7 +131,7 @@ extern class GLProgram `{GLuint`}
        # Type of the active attribute at `index`
        #
        # May only be float related data types (single float, vectors and matrix).
-       fun active_attrib_type(index: Int): GLFloatDataType `{
+       fun active_attrib_type(index: Int): GLDataType `{
                int size;
                GLenum type;
                glGetActiveAttrib(self, index, 0, NULL, &size, &type, NULL);
@@ -158,15 +141,16 @@ extern class GLProgram `{GLuint`}
        # Name of the active uniform at `index`
        fun active_uniform_name(index: Int): String
        do
-               var max_size = active_attribute_max_length
-               return active_uniform_name_native(index, max_size).to_s
+               var max_size = active_uniform_max_length
+               var cname = new NativeString(max_size)
+               active_uniform_name_native(index, max_size, cname)
+               return cname.to_s
        end
-       private fun active_uniform_name_native(index, max_size: Int): NativeString `{
-               char *name = malloc(max_size);
+
+       private fun active_uniform_name_native(index, max_size: Int, name: NativeString) `{
                int size;
                GLenum type;
                glGetActiveUniform(self, index, max_size, NULL, &size, &type, name);
-               return name;
        `}
 
        # Size of the active uniform at `index`
@@ -212,6 +196,29 @@ fun glAttachShader(program: GLProgram, shader: GLShader) `{ glAttachShader(progr
 # Detach `shader` from `program`
 fun glDetachShader(program: GLProgram, shader: GLShader) `{ glDetachShader(program, shader); `}
 
+# Parameter value from a `program` object
+fun glGetProgramiv(program: GLProgram, pname: GLGetParameterName): Int `{
+       int value;
+       glGetProgramiv(program, pname, &value);
+       return value;
+`}
+
+# The information log for the `program` object
+fun glGetProgramInfoLog(program: GLProgram): String
+do
+       var size = glGetProgramiv(program, gl_INFO_LOG_LENGTH)
+       var buf = new NativeString(size)
+       native_glGetProgramInfoLog(program, size, buf)
+       return buf.to_s_with_length(size)
+end
+
+# Return the program information log in `buf`
+private fun native_glGetProgramInfoLog(program: GLProgram, buf_size: Int, buf: NativeString): Int `{
+       int length;
+       glGetProgramInfoLog(program, buf_size, &length, buf);
+       return length;
+`}
+
 # Abstract OpenGL ES shader object, implemented by `GLFragmentShader` and `GLVertexShader`
 extern class GLShader `{GLuint`}
 
@@ -221,7 +228,7 @@ extern class GLShader `{GLuint`}
        # was created from a binary file.
        fun source: nullable String
        do
-               var size = query(0x8B88)
+               var size = glGetShaderiv(self, gl_SHADER_SOURCE_LENGTH)
                if size == 0 then return null
                return source_native(size).to_s
        end
@@ -232,31 +239,55 @@ extern class GLShader `{GLuint`}
                return code;
        `}
 
-       # Query information on this shader
-       protected fun query(pname: Int): Int `{
-               int val;
-               glGetShaderiv(self, pname, &val);
-               return val;
-       `}
-
        # Has this shader been compiled?
-       fun is_compiled: Bool do return query(0x8B81) != 0
+       fun is_compiled: Bool do return glGetShaderiv(self, gl_COMPILE_STATUS) != 0
 
        # Has this shader been deleted?
-       fun is_deleted: Bool do return query(0x8B80) != 0
+       fun is_deleted: Bool do return glGetShaderiv(self, gl_DELETE_STATUS) != 0
+end
 
-       # Retrieve the information log of this shader
-       #
-       # Useful with `link` and `validate`
-       fun info_log: String import NativeString.to_s `{
-               int size;
-               glGetShaderiv(self, GL_INFO_LOG_LENGTH, &size);
-               GLchar *msg = malloc(size);
-               glGetShaderInfoLog(self, size, NULL, msg);
-               return NativeString_to_s(msg);
-       `}
+# Get a parameter value from a `shader` object
+fun glGetShaderiv(shader: GLShader, pname: GLGetParameterName): Int `{
+       int val;
+       glGetShaderiv(shader, pname, &val);
+       return val;
+`}
+
+# Shader parameter
+extern class GLGetParameterName
+       super GLEnum
+end
+
+fun gl_INFO_LOG_LENGTH: GLGetParameterName `{ return GL_INFO_LOG_LENGTH; `}
+fun gl_DELETE_STATUS: GLGetParameterName `{ return GL_DELETE_STATUS; `}
+
+fun gl_SHADER_TYPE: GLGetParameterName `{ return GL_SHADER_TYPE; `}
+fun gl_COMPILE_STATUS: GLGetParameterName `{ return GL_COMPILE_STATUS; `}
+fun gl_SHADER_SOURCE_LENGTH: GLGetParameterName `{ return GL_SHADER_SOURCE_LENGTH; `}
+
+fun gl_ACTIVE_ATTRIBUTES: GLGetParameterName `{ return GL_ACTIVE_ATTRIBUTES; `}
+fun gl_ACTIVE_ATTRIBUTE_MAX_LENGTH: GLGetParameterName `{ return GL_ACTIVE_ATTRIBUTE_MAX_LENGTH; `}
+fun gl_ACTIVE_UNIFORMS: GLGetParameterName `{ return GL_ACTIVE_UNIFORMS; `}
+fun gl_ACTIVE_UNIFORM_MAX_LENGTH: GLGetParameterName `{ return GL_ACTIVE_UNIFORM_MAX_LENGTH; `}
+fun gl_ATTACHED_SHADERS: GLGetParameterName `{ return GL_ATTACHED_SHADERS; `}
+fun gl_LINK_STATUS: GLGetParameterName `{ return GL_LINK_STATUS; `}
+fun gl_VALIDATE_STATUS: GLGetParameterName `{ return GL_VALIDATE_STATUS; `}
+
+# The information log for the `shader` object
+fun glGetShaderInfoLog(shader: GLShader): String
+do
+       var size = glGetShaderiv(shader, gl_INFO_LOG_LENGTH)
+       var buf = new NativeString(size)
+       native_glGetShaderInfoLog(shader, size, buf)
+       return buf.to_s_with_length(size)
 end
 
+private fun native_glGetShaderInfoLog(shader: GLShader, buf_size: Int, buffer: NativeString): Int `{
+       int length;
+       glGetShaderInfoLog(shader, buf_size, &length, buffer);
+       return length;
+`}
+
 # Shader type
 extern class GLShaderType
        super GLEnum
@@ -344,6 +375,11 @@ fun glDisableVertexAttribArray(index: Int) `{ glDisableVertexAttribArray(index);
 # Render primitives from array data
 fun glDrawArrays(mode: GLDrawMode, from, count: Int) `{ glDrawArrays(mode, from, count); `}
 
+# Render primitives from array data by their index
+fun glDrawElements(mode: GLDrawMode, count: Int, typ: GLDataType, indices: Pointer) `{
+       glDrawElements(mode, count, typ, indices);
+`}
+
 # Define an array of generic vertex attribute data
 fun glVertexAttribPointer(index, size: Int, typ: GLDataType, normalized: Bool, stride: Int, array: NativeGLfloatArray) `{
        glVertexAttribPointer(index, size, typ, normalized, stride, array);
@@ -373,7 +409,17 @@ fun glUniform3i(index, x, y, z: Int) `{ glUniform3i(index, x, y, z); `}
 # Specify the value of a uniform variable for the current program object
 fun glUniform4i(index, x, y, z, w: Int) `{ glUniform4i(index, x, y, z, w); `}
 
-# TODO glUniform*f
+# Specify the value of a uniform variable for the current program object
+fun glUniform1f(index: Int, x: Float) `{ glUniform1f(index, x); `}
+
+# Specify the value of a uniform variable for the current program object
+fun glUniform2f(index: Int, x, y: Float) `{ glUniform2f(index, x, y); `}
+
+# Specify the value of a uniform variable for the current program object
+fun glUniform3f(index: Int, x, y, z: Float) `{ glUniform3f(index, x, y, z); `}
+
+# Specify the value of a uniform variable for the current program object
+fun glUniform4f(index: Int, x, y, z, w: Float) `{ glUniform4f(index, x, y, z, w); `}
 
 # Low level array of `Float`
 class GLfloatArray
@@ -558,14 +604,14 @@ fun gl_UNPACK_ALIGNEMENT: GLPack `{ return GL_UNPACK_ALIGNMENT; `}
 # Specify a two-dimensional texture image
 fun glTexImage2D(target: GLTextureTarget, level: Int, internalformat: GLPixelFormat,
                  width, height, border: Int,
-                 format: GLPixelFormat, typ: GLPixelType, data: Pointer) `{
+                 format: GLPixelFormat, typ: GLDataType, data: Pointer) `{
        glTexImage2D(target, level, internalformat, width, height, border, format, typ, data);
 `}
 
 # Specify a two-dimensional texture subimage
 fun glTexSubImage2D(target: GLTextureTarget,
                     level, xoffset, yoffset, width, height, border: Int,
-                    format: GLPixelFormat, typ: GLPixelType, data: Pointer) `{
+                    format: GLPixelFormat, typ: GLDataType, data: Pointer) `{
        glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, typ, data);
 `}
 
@@ -581,7 +627,7 @@ fun glCopyTexSubImage2D(target: GLTextureTarget, level, xoffset, yoffset, x, y,
 `}
 
 # Copy a block of pixels from the framebuffer of `fomat` and `typ` at `data`
-fun glReadPixels(x, y, width, height: Int, format: GLPixelFormat, typ: GLPixelType, data: Pointer) `{
+fun glReadPixels(x, y, width, height: Int, format: GLPixelFormat, typ: GLDataType, data: Pointer) `{
        glReadPixels(x, y, width, height, format, typ, data);
 `}
 
@@ -998,47 +1044,43 @@ class GLCapabilities
        var stencil_test: GLCap is lazy do return new GLCap(0x0B90)
 end
 
-# Float related data types of OpenGL ES 2.0 shaders
-#
-# Only data types supported by shader attributes, as seen with
-# `GLProgram::active_attrib_type`.
-extern class GLFloatDataType
-       super GLEnum
-
-       fun is_float: Bool `{ return self == GL_FLOAT; `}
-       fun is_float_vec2: Bool `{ return self == GL_FLOAT_VEC2; `}
-       fun is_float_vec3: Bool `{ return self == GL_FLOAT_VEC3; `}
-       fun is_float_vec4: Bool `{ return self == GL_FLOAT_VEC4; `}
-       fun is_float_mat2: Bool `{ return self == GL_FLOAT_MAT2; `}
-       fun is_float_mat3: Bool `{ return self == GL_FLOAT_MAT3; `}
-       fun is_float_mat4: Bool `{ return self == GL_FLOAT_MAT4; `}
-
-       # Instances of `GLFloatDataType` can be equal to instances of `GLDataType`
-       redef fun ==(o)
-       do
-               return o != null and o isa GLFloatDataType and o.hash == self.hash
-       end
-end
-
 # All data types of OpenGL ES 2.0 shaders
 #
 # These types can be used by shader uniforms, as seen with
 # `GLProgram::active_uniform_type`.
 extern class GLDataType
-       super GLFloatDataType
-
-       fun is_int: Bool `{ return self == GL_INT; `}
-       fun is_int_vec2: Bool `{ return self == GL_INT_VEC2; `}
-       fun is_int_vec3: Bool `{ return self == GL_INT_VEC3; `}
-       fun is_int_vec4: Bool `{ return self == GL_INT_VEC4; `}
-       fun is_bool: Bool `{ return self == GL_BOOL; `}
-       fun is_bool_vec2: Bool `{ return self == GL_BOOL_VEC2; `}
-       fun is_bool_vec3: Bool `{ return self == GL_BOOL_VEC3; `}
-       fun is_bool_vec4: Bool `{ return self == GL_BOOL_VEC4; `}
-       fun is_sampler_2d: Bool `{ return self == GL_SAMPLER_2D; `}
-       fun is_sampler_cube: Bool `{ return self == GL_SAMPLER_CUBE; `}
+       super GLEnum
 end
 
+fun gl_FLOAT: GLDataType `{ return GL_FLOAT; `}
+fun gl_FLOAT_VEC2: GLDataType `{ return GL_FLOAT_VEC2; `}
+fun gl_FLOAT_VEC3: GLDataType `{ return GL_FLOAT_VEC3; `}
+fun gl_FLOAT_VEC4: GLDataType `{ return GL_FLOAT_VEC4; `}
+fun gl_FLOAT_MAT2: GLDataType `{ return GL_FLOAT_MAT2; `}
+fun gl_FLOAT_MAT3: GLDataType `{ return GL_FLOAT_MAT3; `}
+fun gl_FLOAT_MAT4: GLDataType `{ return GL_FLOAT_MAT4; `}
+
+fun gl_BYTE: GLDataType `{ return GL_BYTE; `}
+fun gl_UNSIGNED_BYTE: GLDataType `{ return GL_UNSIGNED_BYTE; `}
+fun gl_SHORT: GLDataType `{ return GL_SHORT; `}
+fun gl_UNSIGNED_SHORT: GLDataType `{ return GL_UNSIGNED_SHORT; `}
+fun gl_INT: GLDataType `{ return GL_INT; `}
+fun gl_UNSIGNED_INT: GLDataType `{ return GL_UNSIGNED_INT; `}
+fun gl_FIXED: GLDataType `{ return GL_FIXED; `}
+fun gl_INT_VEC2: GLDataType `{ return GL_INT_VEC2; `}
+fun gl_INT_VEC3: GLDataType `{ return GL_INT_VEC3; `}
+fun gl_INT_VEC4: GLDataType `{ return GL_INT_VEC4; `}
+fun gl_BOOL: GLDataType `{ return GL_BOOL; `}
+fun gl_BOOL_VEC2: GLDataType `{ return GL_BOOL_VEC2; `}
+fun gl_BOOL_VEC3: GLDataType `{ return GL_BOOL_VEC3; `}
+fun gl_BOOL_VEC4: GLDataType `{ return GL_BOOL_VEC4; `}
+fun gl_SAMPLER_2D: GLDataType `{ return GL_SAMPLER_2D; `}
+fun gl_SAMPLER_CUBE: GLDataType `{ return GL_SAMPLER_CUBE; `}
+
+fun gl_UNSIGNED_SHORT_5_6_5: GLDataType `{ return GL_UNSIGNED_SHORT_5_6_5; `}
+fun gl_UNSIGNED_SHORT_4_4_4_4: GLDataType `{ return GL_UNSIGNED_SHORT_4_4_4_4; `}
+fun gl_UNSIGNED_SHORT_5_5_5_1: GLDataType `{ return GL_UNSIGNED_SHORT_5_5_5_1; `}
+
 # Kind of primitives to render
 extern class GLDrawMode
        super GLEnum
@@ -1092,16 +1134,6 @@ fun gl_ALPHA: GLPixelFormat `{ return GL_ALPHA; `}
 fun gl_RGB: GLPixelFormat `{ return GL_RGB; `}
 fun gl_RGBA: GLPixelFormat `{ return GL_RGBA; `}
 
-# Data type of pixel data
-extern class GLPixelType
-       super GLEnum
-end
-
-fun gl_UNSIGNED_BYTE: GLPixelType `{ return GL_UNSIGNED_BYTE; `}
-fun gl_UNSIGNED_SHORT_5_6_5: GLPixelType `{ return GL_UNSIGNED_SHORT_5_6_5; `}
-fun gl_UNSIGNED_SHORT_4_4_4_4: GLPixelType `{ return GL_UNSIGNED_SHORT_4_4_4_4; `}
-fun gl_UNSIGNED_SHORT_5_5_5_1: GLPixelType `{ return GL_UNSIGNED_SHORT_5_5_5_1; `}
-
 # Set of buffers as a bitwise OR mask
 extern class GLBuffer `{ GLbitfield `}
        # Bitwise OR with `other`
index b6bf2c1..8ec28ff 100644 (file)
@@ -80,7 +80,8 @@ end
 private class DFAState1
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun is_ignored do return true
+       redef fun make_token(position, source) do
                return null
        end
        redef fun trans(char) do
@@ -96,20 +97,20 @@ private class DFAState2
        super DFAState
        redef fun trans(char) do
                var c = char.code_point
+               if c > 92 then return dfastate_2
                if c <= 33 then return dfastate_2
                if c <= 34 then return dfastate_29
                if c <= 91 then return dfastate_2
-               if c <= 92 then return dfastate_30
-               return dfastate_2
+               return dfastate_30
        end
 end
 private class DFAState3
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39d_44d_39d
+               t.text = ","
                t.position = position
-               t.text = text
                return t
        end
 end
@@ -125,10 +126,10 @@ end
 private class DFAState5
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new Nnumber
+               t.text = position.extract(source)
                t.position = position
-               t.text = text
                return t
        end
        redef fun trans(char) do
@@ -147,30 +148,30 @@ end
 private class DFAState6
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39d_58d_39d
+               t.text = ":"
                t.position = position
-               t.text = text
                return t
        end
 end
 private class DFAState7
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39d_91d_39d
+               t.text = "["
                t.position = position
-               t.text = text
                return t
        end
 end
 private class DFAState8
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39d_93d_39d
+               t.text = "]"
                t.position = position
-               t.text = text
                return t
        end
 end
@@ -204,20 +205,20 @@ end
 private class DFAState12
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39d_123d_39d
+               t.text = "\{"
                t.position = position
-               t.text = text
                return t
        end
 end
 private class DFAState13
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39d_125d_39d
+               t.text = "\}"
                t.position = position
-               t.text = text
                return t
        end
 end
@@ -242,10 +243,10 @@ end
 private class DFAState16
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39dtrue_39d
+               t.text = "true"
                t.position = position
-               t.text = text
                return t
        end
 end
@@ -270,10 +271,10 @@ end
 private class DFAState19
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39dnull_39d
+               t.text = "null"
                t.position = position
-               t.text = text
                return t
        end
 end
@@ -307,10 +308,10 @@ end
 private class DFAState23
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new N_39dfalse_39d
+               t.text = "false"
                t.position = position
-               t.text = text
                return t
        end
 end
@@ -348,10 +349,10 @@ end
 private class DFAState27
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new Nnumber
+               t.text = position.extract(source)
                t.position = position
-               t.text = text
                return t
        end
        redef fun trans(char) do
@@ -364,10 +365,10 @@ end
 private class DFAState28
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new Nnumber
+               t.text = position.extract(source)
                t.position = position
-               t.text = text
                return t
        end
        redef fun trans(char) do
@@ -384,10 +385,10 @@ end
 private class DFAState29
        super DFAState
        redef fun is_accept do return true
-       redef fun make_token(position, text) do
+       redef fun make_token(position, source) do
                var t = new Nstring
+               t.text = position.extract(source)
                t.position = position
-               t.text = text
                return t
        end
 end
index bfd137d..ac75d8b 100644 (file)
@@ -19,7 +19,7 @@ module nitcc_runtime
 abstract class Parser
        # The list of tokens
        # FIXME: provide something better, like a lexer?
-       var tokens = new List[NToken]
+       var tokens = new CircularArray[NToken]
 
        # Look at the next token
        # Used by generated parsers
@@ -76,7 +76,7 @@ abstract class Parser
                #print "  expected: {state.error_msg}"
                #print "  node_stack={node_stack.join(", ")}"
                #print "  state_stack={state_stack.join(", ")}"
-               node_stack.add(token)
+               node_stack.push(token)
                var error: NError
                if token isa NLexerError then
                        error = token
@@ -162,9 +162,9 @@ abstract class Lexer
        protected fun start_state: DFAState is abstract
 
        # Lexize a stream of characters and return a sequence of tokens
-       fun lex: List[NToken]
+       fun lex: CircularArray[NToken]
        do
-               var res = new List[NToken]
+               var res = new CircularArray[NToken]
                var state = start_state
                var pos = 0
                var pos_start = 0
@@ -201,19 +201,21 @@ abstract class Lexer
                                                var position = new Position(pos_start, pos, line_start, line, col_start, col)
                                                token.position = position
                                                token.text = text.substring(pos_start, pos-pos_start+1)
-                                               res.add token
+                                               res.push token
                                                break
                                        end
-                                       var position = new Position(pos_start, pos_end, line_start, line_end, col_start, col_end)
-                                       var token = last_state.make_token(position, text.substring(pos_start, pos_end-pos_start+1))
-                                       if token != null then res.add(token)
+                                       if not last_state.is_ignored then
+                                               var position = new Position(pos_start, pos_end, line_start, line_end, col_start, col_end)
+                                               var token = last_state.make_token(position, text)
+                                               if token != null then res.push(token)
+                                       end
                                end
                                if pos >= length then
                                        var token = new NEof
                                        var position = new Position(pos, pos, line, line, col, col)
                                        token.position = position
                                        token.text = ""
-                                       res.add token
+                                       res.push token
                                        break
                                end
                                state = start_state
@@ -244,7 +246,8 @@ end
 interface DFAState
        fun is_accept: Bool do return false
        fun trans(c: Char): nullable DFAState do return null
-       fun make_token(position: Position, text: String): nullable NToken is abstract
+       fun make_token(position: Position, source: String): nullable NToken is abstract
+       fun is_ignored: Bool do return false
 end
 
 ###
@@ -299,6 +302,12 @@ class Position
 
        redef fun to_s do return "{line_start}:{col_start}-{line_end}:{col_end}"
 
+       # Extract the content from the given source
+       fun extract(source: String): String
+       do
+               return source.substring(pos_start, pos_end-pos_start+1)
+       end
+
        # Get the lines covered by `self` and underline the target columns.
        #
        # This is useful for pretty printing errors or debug the output
@@ -343,7 +352,7 @@ abstract class Node
        fun children: SequenceRead[nullable Node] is abstract
 
        # A point of view of a depth-first visit of all non-null children
-       var depth: Collection[Node] = new DephCollection(self)
+       var depth: Collection[Node] = new DephCollection(self) is lazy
 
        # Visit all the children of the node with the visitor `v`
        protected fun visit_children(v: Visitor)
@@ -410,10 +419,10 @@ end
 private class DephIterator
        super Iterator[Node]
 
-       var stack = new List[Iterator[nullable Node]]
+       var stack = new Array[Iterator[nullable Node]]
 
        init(i: Iterator[nullable Node]) is old_style_init do
-               stack.add i
+               stack.push i
        end
 
        redef fun is_ok do return not stack.is_empty
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