Merge: nitrpg: Move `nitrpg` to its own repository
authorJean Privat <jean@pryen.org>
Wed, 3 Jul 2019 18:50:57 +0000 (14:50 -0400)
committerJean Privat <jean@pryen.org>
Wed, 3 Jul 2019 18:50:57 +0000 (14:50 -0400)
`nitrpg` is broken since a long time. I think https:/api.github.com actually changed twice since it broke. I don't plan on killing it yet but I moved it to its own repository until I worked again on it (or never).

See https://github.com/Morriar/nitrpg.

Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

Pull-Request: #2755

43 files changed:
.gitlab-ci.yml
contrib/nitiwiki/src/wiki_base.nit
lib/config/config.nit
lib/github/api.nit
lib/github/loader.nit
lib/github/tests/mock/errors_404.res [new file with mode: 0644]
lib/github/tests/mock/repo_branches_master.res [new file with mode: 0644]
lib/github/tests/mock/repo_branches_nit.res [new file with mode: 0644]
lib/github/tests/mock/repo_comments_8982707.res [new file with mode: 0644]
lib/github/tests/mock/repo_commits_64ce1f.res [new file with mode: 0644]
lib/github/tests/mock/repo_issues_1000.res [new file with mode: 0644]
lib/github/tests/mock/repo_issues_comments_6020149.res [new file with mode: 0644]
lib/github/tests/mock/repo_issues_events_199674194.res [new file with mode: 0644]
lib/github/tests/mock/repo_labels_ok_will_merge.res [new file with mode: 0644]
lib/github/tests/mock/repo_milestones_4.res [new file with mode: 0644]
lib/github/tests/mock/repo_nit.res [new file with mode: 0644]
lib/github/tests/mock/repo_pulls_1000.res [new file with mode: 0644]
lib/github/tests/mock/repo_pulls_comment_21010363.res [new file with mode: 0644]
lib/github/tests/mock/user_Morriar.res [new file with mode: 0644]
lib/github/tests/test_api.nit [new file with mode: 0644]
lib/ini/README.md [new file with mode: 0644]
lib/ini/ini.nit
lib/ini/package.ini
lib/logger/logger.nit [new file with mode: 0644]
lib/logger/package.ini [new file with mode: 0644]
lib/popcorn/README.md
lib/popcorn/pop_logging.nit
lib/popcorn/pop_tracker.nit
misc/docker/ci/Dockerfile
src/doc/commands/commands_ini.nit
src/loader.nit
src/nitpackage.nit
src/nitpm.nit
src/nitunit.nit
src/nitweb.nit
tests/sav/nitunit_args1.res
tests/sav/nitunit_args10.res
tests/sav/nitunit_args11.res
tests/sav/nitunit_args12.res
tests/sav/nitunit_args13.res
tests/sav/nitunit_args14.res
tests/sav/nitunit_args9.res
tests/tests.sh

index a0711b3..70b91e1 100644 (file)
@@ -77,7 +77,7 @@ test_some: &test_some
   artifacts:
     paths:
       - tests/errlist
-      - tests/*.xml
+      - tests/*.xml*
     when: always
     reports:
       junit: tests/*.xml
@@ -90,6 +90,7 @@ nitunit_some:
     - git diff --name-only origin/master..HEAD -- "*.nit" "*.res" "README.*" | grep -v "^tests/" > list0.txt || true
     - xargs nitls -pP < list0.txt > list.txt
     - xargs nitunit < list.txt
+    - junit2html nitunit.xml
   artifacts:
     paths:
       - nitunit.xml*
@@ -221,6 +222,7 @@ nitunit_lib:
     - xargs nitunit -v < list.txt| tee log.txt
     - grep -e KO log.txt > status.txt || true
     - tail -3 log.txt >> status.txt
+    - junit2html nitunit.xml
   artifacts:
     paths:
       - nitunit.xml*
@@ -238,6 +240,7 @@ nitunit_src:
     - xargs nitunit -v < list.txt| tee log.txt
     - grep -e KO log.txt > status.txt || true
     - tail -3 log.txt >> status.txt
+    - junit2html nitunit.xml
   artifacts:
     paths:
       - nitunit.xml*
@@ -329,6 +332,21 @@ build_more_tools:
       - src/version.nit
       - src/nitc_0
 
+valgrind:
+  stage: more_test
+  dependencies:
+    - build_more_tools
+  script:
+    - mkdir -p valgrind.out
+    - nitc src/nitc.nit # To warm-up the cache
+    - src/valgrind.sh --callgrind-out-file=valgrind.out/nitc.nitc.out nitc src/nitc.nit -vv
+    - callgrind_annotate valgrind.out/nitc.nitc.out > valgrind.out/nitc.nitc.txt
+    - src/valgrind.sh --callgrind-out-file=valgrind.out/niti.niti.out nit -- src/nit.nit tests/base_simple3.nit -vv
+    - callgrind_annotate valgrind.out/niti.niti.out > valgrind.out/niti.niti.txt
+  artifacts:
+    paths:
+      - valgrind.out
+
 build_doc:
   stage: more_test
   dependencies:
@@ -340,6 +358,16 @@ build_doc:
     paths:
       - nitdoc.out
 
+nitmetrics:
+  stage: more_test
+  dependencies:
+    - build_more_tools
+  script:
+    - nitmetrics --all --log --log-dir nitmetrics.out --dir nitmetrics.out --keep-going lib src
+  artifacts:
+    paths:
+      - nitmetrics.out
+
 build_catalog:
   stage: more_test
   dependencies:
index 2e93a74..f1fb6ae 100644 (file)
@@ -612,7 +612,13 @@ end
 #
 # This class provides services that ensure static typing when accessing the `config.ini` file.
 class WikiConfig
-       super ConfigTree
+       super IniFile
+       autoinit ini_file
+
+       # Path to this file
+       var ini_file: String
+
+       init do load_file(ini_file)
 
        # Returns the config value at `key` or return `default` if no key was found.
        protected fun value_or_default(key: String, default: String): String do
@@ -779,8 +785,8 @@ class WikiConfig
        var sidebar_blocks: Array[String] is lazy do
                var res = new Array[String]
                if not has_key("wiki.sidebar.blocks") then return res
-               for val in at("wiki.sidebar.blocks").as(not null).values do
-                       res.add val
+               for val in section("wiki.sidebar.blocks").as(not null).values do
+                       res.add val.as(not null)
                end
                return res
        end
@@ -834,7 +840,13 @@ end
 # Each section can provide its own config file to customize
 # appearance or behavior.
 class SectionConfig
-       super ConfigTree
+       super IniFile
+       autoinit ini_file
+
+       # Path to this file
+       var ini_file: String
+
+       init do load_file(ini_file)
 
        # Returns the config value at `key` or `null` if no key was found.
        private fun value_or_null(key: String): nullable String do
index da03f7c..f07ec47 100644 (file)
@@ -299,7 +299,7 @@ class IniConfig
        super Config
 
        # Config tree used to store config options
-       var ini: ConfigTree is noinit
+       var ini: IniFile is noinit
 
        # Path to app config file
        var opt_config = new OptionString("Path to config file", "--config")
@@ -311,7 +311,7 @@ class IniConfig
 
        redef fun parse_options(args) do
                super
-               ini = new ConfigTree(config_file)
+               ini = new IniFile.from_file(config_file)
        end
 
        # Default config file path
index 4eb9a97..fb6fff9 100644 (file)
@@ -102,20 +102,24 @@ class GithubAPI
        # This method returns raw json data.
        # See other `load_*` methods to use more expressive types.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var obj = api.get("/repos/nitlang/nit")
-       #     assert obj isa JsonObject
-       #     assert obj["name"] == "nit"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var obj = api.get("/repos/nitlang/nit")
+       # assert obj isa JsonObject
+       # assert obj["name"] == "nit"
+       # ~~~
        #
        # Returns `null` in case of `error`.
        #
-       #     obj = api.get("/foo/bar/baz")
-       #     assert obj == null
-       #     assert api.was_error
-       #     var err = api.last_error
-       #     assert err isa GithubError
-       #     assert err.name == "GithubAPIError"
-       #     assert err.message == "Not Found"
+       # ~~~nitish
+       # obj = api.get("/foo/bar/baz")
+       # assert obj == null
+       # assert api.was_error
+       # var err = api.last_error
+       # assert err isa GithubError
+       # assert err.name == "GithubAPIError"
+       # assert err.message == "Not Found"
+       # ~~~
        fun get(path: String): nullable Serializable do
                path = sanitize_uri(path)
                var res = ghcurl.get_and_parse("{api_url}{path}")
@@ -173,10 +177,12 @@ class GithubAPI
        #
        # Loads the `User` from the API or returns `null` if the user cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var user = api.load_user("Morriar")
-       #     print user or else "null"
-       #     assert user.login == "Morriar"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var user = api.load_user("Morriar")
+       # print user or else "null"
+       # assert user.login == "Morriar"
+       # ~~~
        fun load_user(login: String): nullable User do
                return load_from_github("/users/{login}").as(nullable User)
        end
@@ -185,11 +191,13 @@ class GithubAPI
        #
        # Loads the `Repo` from the API or returns `null` if the repo cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo.name == "nit"
-       #     assert repo.owner.login == "nitlang"
-       #     assert repo.default_branch == "master"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo.name == "nit"
+       # assert repo.owner.login == "nitlang"
+       # assert repo.default_branch == "master"
+       # ~~~
        fun load_repo(full_name: String): nullable Repo do
                return load_from_github("/repos/{full_name}").as(nullable Repo)
        end
@@ -201,8 +209,12 @@ class GithubAPI
                var res = new Array[Branch]
                if not array isa JsonArray then return res
                var deser = deserialize(array.to_json)
-               if deser isa Array[Object] then return res # empty array
-               return deser.as(Array[Branch])
+               if not deser isa Array[Object] then return res # empty array
+               for branch in deser do
+                       if not branch isa Branch then continue
+                       res.add branch
+               end
+               return res
        end
 
        # List of issues associated with their ids.
@@ -301,12 +313,14 @@ class GithubAPI
        #
        # Returns `null` if the branch cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var branch = api.load_branch(repo, "master")
-       #     assert branch.name == "master"
-       #     assert branch.commit isa Commit
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var branch = api.load_branch(repo, "master")
+       # assert branch.name == "master"
+       # assert branch.commit isa Commit
+       # ~~~
        fun load_branch(repo: Repo, name: String): nullable Branch do
                return load_from_github("/repos/{repo.full_name}/branches/{name}").as(nullable Branch)
        end
@@ -339,11 +353,13 @@ class GithubAPI
        #
        # Returns `null` if the commit cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var commit = api.load_commit(repo, "64ce1f")
-       #     assert commit isa Commit
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var commit = api.load_commit(repo, "64ce1f")
+       # assert commit isa Commit
+       # ~~~
        fun load_commit(repo: Repo, sha: String): nullable Commit do
                return load_from_github("/repos/{repo.full_name}/commits/{sha}").as(nullable Commit)
        end
@@ -352,11 +368,13 @@ class GithubAPI
        #
        # Returns `null` if the issue cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var issue = api.load_issue(repo, 1)
-       #     assert issue.title == "Doc"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var issue = api.load_issue(repo, 1)
+       # assert issue.title == "Doc"
+       # ~~~
        fun load_issue(repo: Repo, number: Int): nullable Issue do
                return load_from_github("/repos/{repo.full_name}/issues/{number}").as(nullable Issue)
        end
@@ -406,12 +424,14 @@ class GithubAPI
        #
        # Returns `null` if the pull request cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var pull = api.load_pull(repo, 1)
-       #     assert pull.title == "Doc"
-       #     assert pull.user.login == "Morriar"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var pull = api.load_pull(repo, 1)
+       # assert pull.title == "Doc"
+       # assert pull.user.login == "Morriar"
+       # ~~~
        fun load_pull(repo: Repo, number: Int): nullable PullRequest do
                return load_from_github("/repos/{repo.full_name}/pulls/{number}").as(nullable PullRequest)
        end
@@ -420,11 +440,13 @@ class GithubAPI
        #
        # Returns `null` if the label cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var labl = api.load_label(repo, "ok_will_merge")
-       #     assert labl != null
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var labl = api.load_label(repo, "ok_will_merge")
+       # assert labl != null
+       # ~~~
        fun load_label(repo: Repo, name: String): nullable Label do
                return load_from_github("/repos/{repo.full_name}/labels/{name}").as(nullable Label)
        end
@@ -433,11 +455,13 @@ class GithubAPI
        #
        # Returns `null` if the milestone cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var stone = api.load_milestone(repo, 4)
-       #     assert stone.title == "v1.0prealpha"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var stone = api.load_milestone(repo, 4)
+       # assert stone.title == "v1.0prealpha"
+       # ~~~
        fun load_milestone(repo: Repo, id: Int): nullable Milestone do
                return load_from_github("/repos/{repo.full_name}/milestones/{id}").as(nullable Milestone)
        end
@@ -446,15 +470,17 @@ class GithubAPI
        #
        # Returns `null` if the event cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo isa Repo
-       #     var event = api.load_issue_event(repo, 199674194)
-       #     assert event isa IssueEvent
-       #     assert event.actor.login == "privat"
-       #     assert event.event == "labeled"
-       #     assert event.labl isa Label
-       #     assert event.labl.name == "need_review"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo isa Repo
+       # var event = api.load_issue_event(repo, 199674194)
+       # assert event isa IssueEvent
+       # assert event.actor.login == "privat"
+       # assert event.event == "labeled"
+       # assert event.labl isa Label
+       # assert event.labl.name == "need_review"
+       # ~~~
        fun load_issue_event(repo: Repo, id: Int): nullable IssueEvent do
                return load_from_github("/repos/{repo.full_name}/issues/events/{id}").as(nullable IssueEvent)
        end
@@ -463,13 +489,15 @@ class GithubAPI
        #
        # Returns `null` if the comment cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var comment = api.load_commit_comment(repo, 8982707)
-       #     assert comment.user.login == "Morriar"
-       #     assert comment.body == "For testing purposes...\n"
-       #     assert comment.commit_id == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var comment = api.load_commit_comment(repo, 8982707)
+       # assert comment.user.login == "Morriar"
+       # assert comment.body == "For testing purposes...\n"
+       # assert comment.commit_id == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
+       # ~~~
        fun load_commit_comment(repo: Repo, id: Int): nullable CommitComment do
                return load_from_github("/repos/{repo.full_name}/comments/{id}").as(nullable CommitComment)
        end
@@ -478,13 +506,15 @@ class GithubAPI
        #
        # Returns `null` if the comment cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var comment = api.load_issue_comment(repo, 6020149)
-       #     assert comment.user.login == "privat"
-       #     assert comment.created_at.to_s == "2012-05-30T20:16:54Z"
-       #     assert comment.issue_number == 10
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var comment = api.load_issue_comment(repo, 6020149)
+       # assert comment.user.login == "privat"
+       # assert comment.created_at.to_s == "2012-05-30T20:16:54Z"
+       # assert comment.issue_number == 10
+       # ~~~
        fun load_issue_comment(repo: Repo, id: Int): nullable IssueComment do
                return load_from_github("/repos/{repo.full_name}/issues/comments/{id}").as(nullable IssueComment)
        end
@@ -493,13 +523,15 @@ class GithubAPI
        #
        # Returns `null` if the comment cannot be found.
        #
-       #     var api = new GithubAPI(get_github_oauth)
-       #     var repo = api.load_repo("nitlang/nit")
-       #     assert repo != null
-       #     var comment = api.load_review_comment(repo, 21010363)
-       #     assert comment.path == "src/modelize/modelize_property.nit"
-       #     assert comment.original_position == 26
-       #     assert comment.pull_number == 945
+       # ~~~nitish
+       # var api = new GithubAPI(get_github_oauth)
+       # var repo = api.load_repo("nitlang/nit")
+       # assert repo != null
+       # var comment = api.load_review_comment(repo, 21010363)
+       # assert comment.path == "src/modelize/modelize_property.nit"
+       # assert comment.original_position == 26
+       # assert comment.pull_number == 945
+       # ~~~
        fun load_review_comment(repo: Repo, id: Int): nullable ReviewComment do
                return load_from_github("/repos/{repo.full_name}/pulls/comments/{id}").as(nullable ReviewComment)
        end
index 4ffa501..6a60c5d 100644 (file)
@@ -104,12 +104,18 @@ class LoaderConfig
 
        # Github tokens used to access data.
        var tokens: Array[String] is lazy do
-               var arr = opt_tokens.value
-               if arr.is_empty then
-                       var iarr = ini.at("tokens")
-                       if iarr != null then arr = iarr.values.to_a
+               var opt_tokens = self.opt_tokens.value
+               if opt_tokens.not_empty then return opt_tokens
+
+               var res = new Array[String]
+               var ini_tokens = ini.section("tokens")
+               if ini_tokens == null then return res
+
+               for token in ini_tokens.values do
+                       if token == null then continue
+                       res.add token
                end
-               return arr or else new Array[String]
+               return res
        end
 
        # Github tokens wallet\13
@@ -128,15 +134,19 @@ class LoaderConfig
        # Verbosity level (the higher the more verbose)
        fun verbose_level: Int do
                var opt = opt_start.value
-               if opt > 0 then return opt
+               if opt > 0 then
+                       return info_level
+               end
                var v = ini["loader.verbose"]
-               if v != null then return v.to_i
-               return 4
+               if v != null and v.to_i > 0 then
+                       return info_level
+               end
+               return warn_level
        end
 
        # Logger used to print things
-       var logger: ConsoleLog is lazy do
-               var logger = new ConsoleLog
+       var logger: PopLogger is lazy do
+               var logger = new PopLogger
                logger.level = verbose_level
                return logger
        end
@@ -412,7 +422,7 @@ class Loader
        end
 
        # Logger shortcut
-       fun log: ConsoleLog do return config.logger
+       fun log: PopLogger do return config.logger
 
        # Display a error and exit
        fun error(msg: String) do
diff --git a/lib/github/tests/mock/errors_404.res b/lib/github/tests/mock/errors_404.res
new file mode 100644 (file)
index 0000000..e0a95cb
--- /dev/null
@@ -0,0 +1 @@
+{"message":"Not Found","documentation_url":"https://developer.github.com/v3"}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_branches_master.res b/lib/github/tests/mock/repo_branches_master.res
new file mode 100644 (file)
index 0000000..dcb088a
--- /dev/null
@@ -0,0 +1 @@
+{"name":"master","commit":{"sha":"9248f1c81c08b6c0ec7785678dbb1d7440b885d9","node_id":"MDY6Q29tbWl0MzI4NTk3OjkyNDhmMWM4MWMwOGI2YzBlYzc3ODU2NzhkYmIxZDc0NDBiODg1ZDk=","commit":{"author":{"name":"Jean Privat","email":"jean@pryen.org","date":"2019-06-13T14:14:24Z"},"committer":{"name":"Jean Privat","email":"jean@pryen.org","date":"2019-06-13T14:14:24Z"},"message":"Merge: Some more small improvements on gitlab-ci\n\nPull-Request: #2744","tree":{"sha":"1c40bdc143d18c628fb7939b8258fa65f6ada2e7","url":"https://api.github.com/repos/nitlang/nit/git/trees/1c40bdc143d18c628fb7939b8258fa65f6ada2e7"},"url":"https://api.github.com/repos/nitlang/nit/git/commits/9248f1c81c08b6c0ec7785678dbb1d7440b885d9","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/nitlang/nit/commits/9248f1c81c08b6c0ec7785678dbb1d7440b885d9","html_url":"https://github.com/nitlang/nit/commit/9248f1c81c08b6c0ec7785678dbb1d7440b885d9","comments_url":"https://api.github.com/repos/nitlang/nit/commits/9248f1c81c08b6c0ec7785678dbb1d7440b885d9/comments","author":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"committer":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"parents":[{"sha":"afc737008b2b10b9317f0e528dedf27dcf9926ab","url":"https://api.github.com/repos/nitlang/nit/commits/afc737008b2b10b9317f0e528dedf27dcf9926ab","html_url":"https://github.com/nitlang/nit/commit/afc737008b2b10b9317f0e528dedf27dcf9926ab"},{"sha":"61e9c7897630bfe23a2bf6c2a02803c1fc8dd51d","url":"https://api.github.com/repos/nitlang/nit/commits/61e9c7897630bfe23a2bf6c2a02803c1fc8dd51d","html_url":"https://github.com/nitlang/nit/commit/61e9c7897630bfe23a2bf6c2a02803c1fc8dd51d"}]},"_links":{"self":"https://api.github.com/repos/nitlang/nit/branches/master","html":"https://github.com/nitlang/nit/tree/master"},"protected":true,"protection":{"enabled":true,"required_status_checks":{"enforcement_level":"non_admins","contexts":[]}},"protection_url":"https://api.github.com/repos/nitlang/nit/branches/master/protection"}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_branches_nit.res b/lib/github/tests/mock/repo_branches_nit.res
new file mode 100644 (file)
index 0000000..ff67214
--- /dev/null
@@ -0,0 +1 @@
+[{"name":"master","commit":{"sha":"9248f1c81c08b6c0ec7785678dbb1d7440b885d9","url":"https://api.github.com/repos/nitlang/nit/commits/9248f1c81c08b6c0ec7785678dbb1d7440b885d9"},"protected":true},{"name":"next","commit":{"sha":"9248f1c81c08b6c0ec7785678dbb1d7440b885d9","url":"https://api.github.com/repos/nitlang/nit/commits/9248f1c81c08b6c0ec7785678dbb1d7440b885d9"},"protected":false}]
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_comments_8982707.res b/lib/github/tests/mock/repo_comments_8982707.res
new file mode 100644 (file)
index 0000000..e8ec2f0
--- /dev/null
@@ -0,0 +1 @@
+{"url":"https://api.github.com/repos/nitlang/nit/comments/8982707","html_url":"https://github.com/nitlang/nit/commit/7eacb86d1e24b7e72bc9ac869bf7182c0300ceca#commitcomment-8982707","id":8982707,"node_id":"MDEzOkNvbW1pdENvbW1lbnQ4OTgyNzA3","user":{"login":"Morriar","id":583144,"node_id":"MDQ6VXNlcjU4MzE0NA==","avatar_url":"https://avatars2.githubusercontent.com/u/583144?v=4","gravatar_id":"","url":"https://api.github.com/users/Morriar","html_url":"https://github.com/Morriar","followers_url":"https://api.github.com/users/Morriar/followers","following_url":"https://api.github.com/users/Morriar/following{/other_user}","gists_url":"https://api.github.com/users/Morriar/gists{/gist_id}","starred_url":"https://api.github.com/users/Morriar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Morriar/subscriptions","organizations_url":"https://api.github.com/users/Morriar/orgs","repos_url":"https://api.github.com/users/Morriar/repos","events_url":"https://api.github.com/users/Morriar/events{/privacy}","received_events_url":"https://api.github.com/users/Morriar/received_events","type":"User","site_admin":false},"position":null,"line":null,"path":null,"commit_id":"7eacb86d1e24b7e72bc9ac869bf7182c0300ceca","created_at":"2014-12-16T00:37:24Z","updated_at":"2014-12-16T00:37:24Z","author_association":"MEMBER","body":"For testing purposes...\n"}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_commits_64ce1f.res b/lib/github/tests/mock/repo_commits_64ce1f.res
new file mode 100644 (file)
index 0000000..e90dbfc
--- /dev/null
@@ -0,0 +1 @@
+{"sha":"64ce1f587209024f5de46d06c70526a569ff537f","node_id":"MDY6Q29tbWl0MzI4NTk3OjY0Y2UxZjU4NzIwOTAyNGY1ZGU0NmQwNmM3MDUyNmE1NjlmZjUzN2Y=","commit":{"author":{"name":"Jean Privat","email":"jean@pryen.org","date":"2014-12-03T15:16:13Z"},"committer":{"name":"Jean Privat","email":"jean@pryen.org","date":"2014-12-03T15:33:19Z"},"message":"lib/string: add `chomp`\n\nSigned-off-by: Jean Privat <jean@pryen.org>","tree":{"sha":"66ac4b2bd9247d98afbb5309db97dda06991ed77","url":"https://api.github.com/repos/nitlang/nit/git/trees/66ac4b2bd9247d98afbb5309db97dda06991ed77"},"url":"https://api.github.com/repos/nitlang/nit/git/commits/64ce1f587209024f5de46d06c70526a569ff537f","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null}},"url":"https://api.github.com/repos/nitlang/nit/commits/64ce1f587209024f5de46d06c70526a569ff537f","html_url":"https://github.com/nitlang/nit/commit/64ce1f587209024f5de46d06c70526a569ff537f","comments_url":"https://api.github.com/repos/nitlang/nit/commits/64ce1f587209024f5de46d06c70526a569ff537f/comments","author":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"committer":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"parents":[{"sha":"a882d5602264623f9275698b5abe73d95b127b9f","url":"https://api.github.com/repos/nitlang/nit/commits/a882d5602264623f9275698b5abe73d95b127b9f","html_url":"https://github.com/nitlang/nit/commit/a882d5602264623f9275698b5abe73d95b127b9f"}],"stats":{"total":13,"additions":13,"deletions":0},"files":[{"sha":"664ecbcb0652892b7659a98e5fb24c995211563c","filename":"lib/standard/string.nit","status":"modified","additions":13,"deletions":0,"changes":13,"blob_url":"https://github.com/nitlang/nit/blob/64ce1f587209024f5de46d06c70526a569ff537f/lib/standard/string.nit","raw_url":"https://github.com/nitlang/nit/raw/64ce1f587209024f5de46d06c70526a569ff537f/lib/standard/string.nit","contents_url":"https://api.github.com/repos/nitlang/nit/contents/lib/standard/string.nit?ref=64ce1f587209024f5de46d06c70526a569ff537f","patch":"@@ -385,6 +385,19 @@ abstract class Text\n \t#     assert \"\\na\\nb\\tc\\t\".trim          == \"a\\nb\\tc\"\n \tfun trim: SELFTYPE do return (self.l_trim).r_trim\n \n+\t# Returns `self` removed from its last `\\n` (if any).\n+\t#\n+\t#    assert \"Hello\\n\".chomp == \"Hello\"\n+\t#    assert \"Hello\".chomp   == \"Hello\"\n+\t#    assert \"\\n\\n\\n\".chomp  == \"\\n\\n\"\n+\t#\n+\t# This method is mainly used to remove the LINE_FEED character from lines of text.\n+\tfun chomp: SELFTYPE\n+\tdo\n+\t\tif self.chars.last != '\\n' then return self\n+\t\treturn substring(0, length-1)\n+\tend\n+\n \t# Justify a self in a space of `length`\n \t#\n \t# `left` is the space ratio on the left side."}]}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_issues_1000.res b/lib/github/tests/mock/repo_issues_1000.res
new file mode 100644 (file)
index 0000000..24294cf
--- /dev/null
@@ -0,0 +1 @@
+{"url":"https://api.github.com/repos/nitlang/nit/issues/1000","repository_url":"https://api.github.com/repos/nitlang/nit","labels_url":"https://api.github.com/repos/nitlang/nit/issues/1000/labels{/name}","comments_url":"https://api.github.com/repos/nitlang/nit/issues/1000/comments","events_url":"https://api.github.com/repos/nitlang/nit/issues/1000/events","html_url":"https://github.com/nitlang/nit/pull/1000","id":51639845,"node_id":"MDExOlB1bGxSZXF1ZXN0MjU4NzM0Mzg=","number":1000,"title":"Raise nitc from the dead","user":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"labels":[{"id":81916206,"node_id":"MDU6TGFiZWw4MTkxNjIwNg==","url":"https://api.github.com/repos/nitlang/nit/labels/ok_will_merge","name":"ok_will_merge","color":"009800","default":false}],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":7,"created_at":"2014-12-11T02:55:09Z","updated_at":"2014-12-18T14:14:33Z","closed_at":"2014-12-13T15:38:09Z","author_association":"MEMBER","pull_request":{"url":"https://api.github.com/repos/nitlang/nit/pulls/1000","html_url":"https://github.com/nitlang/nit/pull/1000","diff_url":"https://github.com/nitlang/nit/pull/1000.diff","patch_url":"https://github.com/nitlang/nit/pull/1000.patch"},"body":"Raise dead on `nitc`.\nIt's super effective...\n","closed_by":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false}}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_issues_comments_6020149.res b/lib/github/tests/mock/repo_issues_comments_6020149.res
new file mode 100644 (file)
index 0000000..8b2f691
--- /dev/null
@@ -0,0 +1 @@
+{"url":"https://api.github.com/repos/nitlang/nit/issues/comments/6020149","html_url":"https://github.com/nitlang/nit/pull/10#issuecomment-6020149","issue_url":"https://api.github.com/repos/nitlang/nit/issues/10","id":6020149,"node_id":"MDEyOklzc3VlQ29tbWVudDYwMjAxNDk=","user":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"created_at":"2012-05-30T20:16:54Z","updated_at":"2012-05-30T20:16:54Z","author_association":"MEMBER","body":"Rebased e766cde to drop the ugly github merge commit 0e3a614.\nThe result is 8f221e3.\n"}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_issues_events_199674194.res b/lib/github/tests/mock/repo_issues_events_199674194.res
new file mode 100644 (file)
index 0000000..5e6b941
--- /dev/null
@@ -0,0 +1 @@
+{"id":199674194,"node_id":"MDEyOkxhYmVsZWRFdmVudDE5OTY3NDE5NA==","url":"https://api.github.com/repos/nitlang/nit/issues/events/199674194","actor":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"event":"labeled","commit_id":null,"commit_url":null,"created_at":"2014-11-27T20:32:30Z","label":{"name":"need_review","color":"fbca04"},"issue":{"url":"https://api.github.com/repos/nitlang/nit/issues/945","repository_url":"https://api.github.com/repos/nitlang/nit","labels_url":"https://api.github.com/repos/nitlang/nit/issues/945/labels{/name}","comments_url":"https://api.github.com/repos/nitlang/nit/issues/945/comments","events_url":"https://api.github.com/repos/nitlang/nit/issues/945/events","html_url":"https://github.com/nitlang/nit/pull/945","id":50322007,"node_id":"MDExOlB1bGxSZXF1ZXN0MjUxNjg4ODY=","number":945,"title":"Useless type","user":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"labels":[{"id":81916206,"node_id":"MDU6TGFiZWw4MTkxNjIwNg==","url":"https://api.github.com/repos/nitlang/nit/labels/ok_will_merge","name":"ok_will_merge","color":"009800","default":false}],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":1,"created_at":"2014-11-27T20:32:27Z","updated_at":"2014-12-03T20:22:42Z","closed_at":"2014-12-01T13:53:03Z","author_association":"MEMBER","pull_request":{"url":"https://api.github.com/repos/nitlang/nit/pulls/945","html_url":"https://github.com/nitlang/nit/pull/945","diff_url":"https://github.com/nitlang/nit/pull/945.diff","patch_url":"https://github.com/nitlang/nit/pull/945.patch"},"body":"Fix a wrong `useless-type` warning for attributes.\nExtends the `useless-type` warning to local variables.\n"}}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_labels_ok_will_merge.res b/lib/github/tests/mock/repo_labels_ok_will_merge.res
new file mode 100644 (file)
index 0000000..e75432c
--- /dev/null
@@ -0,0 +1 @@
+{"id":81916206,"node_id":"MDU6TGFiZWw4MTkxNjIwNg==","url":"https://api.github.com/repos/nitlang/nit/labels/ok_will_merge","name":"ok_will_merge","color":"009800","default":false}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_milestones_4.res b/lib/github/tests/mock/repo_milestones_4.res
new file mode 100644 (file)
index 0000000..69abdc0
--- /dev/null
@@ -0,0 +1 @@
+{"url":"https://api.github.com/repos/nitlang/nit/milestones/4","html_url":"https://github.com/nitlang/nit/milestone/4","labels_url":"https://api.github.com/repos/nitlang/nit/milestones/4/labels","id":795157,"node_id":"MDk6TWlsZXN0b25lNzk1MTU3","number":4,"title":"v1.0prealpha","description":"The first public version that we are proud off and can be used sanely by non Nit people.","creator":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"open_issues":22,"closed_issues":22,"state":"open","created_at":"2014-09-19T00:16:45Z","updated_at":"2017-06-02T12:43:15Z","due_on":null,"closed_at":null}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_nit.res b/lib/github/tests/mock/repo_nit.res
new file mode 100644 (file)
index 0000000..8acf396
--- /dev/null
@@ -0,0 +1 @@
+{"id":328597,"node_id":"MDEwOlJlcG9zaXRvcnkzMjg1OTc=","name":"nit","full_name":"nitlang/nit","private":false,"owner":{"login":"nitlang","id":5420298,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU0MjAyOTg=","avatar_url":"https://avatars1.githubusercontent.com/u/5420298?v=4","gravatar_id":"","url":"https://api.github.com/users/nitlang","html_url":"https://github.com/nitlang","followers_url":"https://api.github.com/users/nitlang/followers","following_url":"https://api.github.com/users/nitlang/following{/other_user}","gists_url":"https://api.github.com/users/nitlang/gists{/gist_id}","starred_url":"https://api.github.com/users/nitlang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nitlang/subscriptions","organizations_url":"https://api.github.com/users/nitlang/orgs","repos_url":"https://api.github.com/users/nitlang/repos","events_url":"https://api.github.com/users/nitlang/events{/privacy}","received_events_url":"https://api.github.com/users/nitlang/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/nitlang/nit","description":"Nit language","fork":false,"url":"https://api.github.com/repos/nitlang/nit","forks_url":"https://api.github.com/repos/nitlang/nit/forks","keys_url":"https://api.github.com/repos/nitlang/nit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nitlang/nit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nitlang/nit/teams","hooks_url":"https://api.github.com/repos/nitlang/nit/hooks","issue_events_url":"https://api.github.com/repos/nitlang/nit/issues/events{/number}","events_url":"https://api.github.com/repos/nitlang/nit/events","assignees_url":"https://api.github.com/repos/nitlang/nit/assignees{/user}","branches_url":"https://api.github.com/repos/nitlang/nit/branches{/branch}","tags_url":"https://api.github.com/repos/nitlang/nit/tags","blobs_url":"https://api.github.com/repos/nitlang/nit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nitlang/nit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nitlang/nit/git/refs{/sha}","trees_url":"https://api.github.com/repos/nitlang/nit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nitlang/nit/statuses/{sha}","languages_url":"https://api.github.com/repos/nitlang/nit/languages","stargazers_url":"https://api.github.com/repos/nitlang/nit/stargazers","contributors_url":"https://api.github.com/repos/nitlang/nit/contributors","subscribers_url":"https://api.github.com/repos/nitlang/nit/subscribers","subscription_url":"https://api.github.com/repos/nitlang/nit/subscription","commits_url":"https://api.github.com/repos/nitlang/nit/commits{/sha}","git_commits_url":"https://api.github.com/repos/nitlang/nit/git/commits{/sha}","comments_url":"https://api.github.com/repos/nitlang/nit/comments{/number}","issue_comment_url":"https://api.github.com/repos/nitlang/nit/issues/comments{/number}","contents_url":"https://api.github.com/repos/nitlang/nit/contents/{+path}","compare_url":"https://api.github.com/repos/nitlang/nit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nitlang/nit/merges","archive_url":"https://api.github.com/repos/nitlang/nit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nitlang/nit/downloads","issues_url":"https://api.github.com/repos/nitlang/nit/issues{/number}","pulls_url":"https://api.github.com/repos/nitlang/nit/pulls{/number}","milestones_url":"https://api.github.com/repos/nitlang/nit/milestones{/number}","notifications_url":"https://api.github.com/repos/nitlang/nit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nitlang/nit/labels{/name}","releases_url":"https://api.github.com/repos/nitlang/nit/releases{/id}","deployments_url":"https://api.github.com/repos/nitlang/nit/deployments","created_at":"2009-10-06T15:03:00Z","updated_at":"2019-06-17T13:13:03Z","pushed_at":"2019-06-19T00:02:11Z","git_url":"git://github.com/nitlang/nit.git","ssh_url":"git@github.com:nitlang/nit.git","clone_url":"https://github.com/nitlang/nit.git","svn_url":"https://github.com/nitlang/nit","homepage":"http://nitlanguage.org","size":123083,"stargazers_count":187,"watchers_count":187,"language":"C","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":56,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":171,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":56,"open_issues":171,"watchers":187,"default_branch":"master","permissions":{"admin":false,"push":true,"pull":true},"organization":{"login":"nitlang","id":5420298,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU0MjAyOTg=","avatar_url":"https://avatars1.githubusercontent.com/u/5420298?v=4","gravatar_id":"","url":"https://api.github.com/users/nitlang","html_url":"https://github.com/nitlang","followers_url":"https://api.github.com/users/nitlang/followers","following_url":"https://api.github.com/users/nitlang/following{/other_user}","gists_url":"https://api.github.com/users/nitlang/gists{/gist_id}","starred_url":"https://api.github.com/users/nitlang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nitlang/subscriptions","organizations_url":"https://api.github.com/users/nitlang/orgs","repos_url":"https://api.github.com/users/nitlang/repos","events_url":"https://api.github.com/users/nitlang/events{/privacy}","received_events_url":"https://api.github.com/users/nitlang/received_events","type":"Organization","site_admin":false},"network_count":56,"subscribers_count":18}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_pulls_1000.res b/lib/github/tests/mock/repo_pulls_1000.res
new file mode 100644 (file)
index 0000000..76f6935
--- /dev/null
@@ -0,0 +1 @@
+{"url":"https://api.github.com/repos/nitlang/nit/pulls/1000","id":25873438,"node_id":"MDExOlB1bGxSZXF1ZXN0MjU4NzM0Mzg=","html_url":"https://github.com/nitlang/nit/pull/1000","diff_url":"https://github.com/nitlang/nit/pull/1000.diff","patch_url":"https://github.com/nitlang/nit/pull/1000.patch","issue_url":"https://api.github.com/repos/nitlang/nit/issues/1000","number":1000,"state":"closed","locked":false,"title":"Raise nitc from the dead","user":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"body":"Raise dead on `nitc`.\nIt's super effective...\n","created_at":"2014-12-11T02:55:09Z","updated_at":"2014-12-18T14:14:33Z","closed_at":"2014-12-13T15:38:09Z","merged_at":"2014-12-13T15:38:09Z","merge_commit_sha":"49af656d278987d3a09f8500bcbe019e3c0f6367","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":81916206,"node_id":"MDU6TGFiZWw4MTkxNjIwNg==","url":"https://api.github.com/repos/nitlang/nit/labels/ok_will_merge","name":"ok_will_merge","color":"009800","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/nitlang/nit/pulls/1000/commits","review_comments_url":"https://api.github.com/repos/nitlang/nit/pulls/1000/comments","review_comment_url":"https://api.github.com/repos/nitlang/nit/pulls/comments{/number}","comments_url":"https://api.github.com/repos/nitlang/nit/issues/1000/comments","statuses_url":"https://api.github.com/repos/nitlang/nit/statuses/273b078ecc1a395f260992ec9fb08a31e8c338d9","head":{"label":"nitlang:raise-nitc","ref":"raise-nitc","sha":"273b078ecc1a395f260992ec9fb08a31e8c338d9","user":{"login":"nitlang","id":5420298,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU0MjAyOTg=","avatar_url":"https://avatars1.githubusercontent.com/u/5420298?v=4","gravatar_id":"","url":"https://api.github.com/users/nitlang","html_url":"https://github.com/nitlang","followers_url":"https://api.github.com/users/nitlang/followers","following_url":"https://api.github.com/users/nitlang/following{/other_user}","gists_url":"https://api.github.com/users/nitlang/gists{/gist_id}","starred_url":"https://api.github.com/users/nitlang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nitlang/subscriptions","organizations_url":"https://api.github.com/users/nitlang/orgs","repos_url":"https://api.github.com/users/nitlang/repos","events_url":"https://api.github.com/users/nitlang/events{/privacy}","received_events_url":"https://api.github.com/users/nitlang/received_events","type":"Organization","site_admin":false},"repo":{"id":328597,"node_id":"MDEwOlJlcG9zaXRvcnkzMjg1OTc=","name":"nit","full_name":"nitlang/nit","private":false,"owner":{"login":"nitlang","id":5420298,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU0MjAyOTg=","avatar_url":"https://avatars1.githubusercontent.com/u/5420298?v=4","gravatar_id":"","url":"https://api.github.com/users/nitlang","html_url":"https://github.com/nitlang","followers_url":"https://api.github.com/users/nitlang/followers","following_url":"https://api.github.com/users/nitlang/following{/other_user}","gists_url":"https://api.github.com/users/nitlang/gists{/gist_id}","starred_url":"https://api.github.com/users/nitlang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nitlang/subscriptions","organizations_url":"https://api.github.com/users/nitlang/orgs","repos_url":"https://api.github.com/users/nitlang/repos","events_url":"https://api.github.com/users/nitlang/events{/privacy}","received_events_url":"https://api.github.com/users/nitlang/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/nitlang/nit","description":"Nit language","fork":false,"url":"https://api.github.com/repos/nitlang/nit","forks_url":"https://api.github.com/repos/nitlang/nit/forks","keys_url":"https://api.github.com/repos/nitlang/nit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nitlang/nit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nitlang/nit/teams","hooks_url":"https://api.github.com/repos/nitlang/nit/hooks","issue_events_url":"https://api.github.com/repos/nitlang/nit/issues/events{/number}","events_url":"https://api.github.com/repos/nitlang/nit/events","assignees_url":"https://api.github.com/repos/nitlang/nit/assignees{/user}","branches_url":"https://api.github.com/repos/nitlang/nit/branches{/branch}","tags_url":"https://api.github.com/repos/nitlang/nit/tags","blobs_url":"https://api.github.com/repos/nitlang/nit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nitlang/nit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nitlang/nit/git/refs{/sha}","trees_url":"https://api.github.com/repos/nitlang/nit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nitlang/nit/statuses/{sha}","languages_url":"https://api.github.com/repos/nitlang/nit/languages","stargazers_url":"https://api.github.com/repos/nitlang/nit/stargazers","contributors_url":"https://api.github.com/repos/nitlang/nit/contributors","subscribers_url":"https://api.github.com/repos/nitlang/nit/subscribers","subscription_url":"https://api.github.com/repos/nitlang/nit/subscription","commits_url":"https://api.github.com/repos/nitlang/nit/commits{/sha}","git_commits_url":"https://api.github.com/repos/nitlang/nit/git/commits{/sha}","comments_url":"https://api.github.com/repos/nitlang/nit/comments{/number}","issue_comment_url":"https://api.github.com/repos/nitlang/nit/issues/comments{/number}","contents_url":"https://api.github.com/repos/nitlang/nit/contents/{+path}","compare_url":"https://api.github.com/repos/nitlang/nit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nitlang/nit/merges","archive_url":"https://api.github.com/repos/nitlang/nit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nitlang/nit/downloads","issues_url":"https://api.github.com/repos/nitlang/nit/issues{/number}","pulls_url":"https://api.github.com/repos/nitlang/nit/pulls{/number}","milestones_url":"https://api.github.com/repos/nitlang/nit/milestones{/number}","notifications_url":"https://api.github.com/repos/nitlang/nit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nitlang/nit/labels{/name}","releases_url":"https://api.github.com/repos/nitlang/nit/releases{/id}","deployments_url":"https://api.github.com/repos/nitlang/nit/deployments","created_at":"2009-10-06T15:03:00Z","updated_at":"2019-06-17T13:13:03Z","pushed_at":"2019-06-19T00:02:11Z","git_url":"git://github.com/nitlang/nit.git","ssh_url":"git@github.com:nitlang/nit.git","clone_url":"https://github.com/nitlang/nit.git","svn_url":"https://github.com/nitlang/nit","homepage":"http://nitlanguage.org","size":123083,"stargazers_count":187,"watchers_count":187,"language":"C","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":56,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":171,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":56,"open_issues":171,"watchers":187,"default_branch":"master"}},"base":{"label":"nitlang:master","ref":"master","sha":"8bd95517ec64090da1356ee1a88af82a9ccf2847","user":{"login":"nitlang","id":5420298,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU0MjAyOTg=","avatar_url":"https://avatars1.githubusercontent.com/u/5420298?v=4","gravatar_id":"","url":"https://api.github.com/users/nitlang","html_url":"https://github.com/nitlang","followers_url":"https://api.github.com/users/nitlang/followers","following_url":"https://api.github.com/users/nitlang/following{/other_user}","gists_url":"https://api.github.com/users/nitlang/gists{/gist_id}","starred_url":"https://api.github.com/users/nitlang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nitlang/subscriptions","organizations_url":"https://api.github.com/users/nitlang/orgs","repos_url":"https://api.github.com/users/nitlang/repos","events_url":"https://api.github.com/users/nitlang/events{/privacy}","received_events_url":"https://api.github.com/users/nitlang/received_events","type":"Organization","site_admin":false},"repo":{"id":328597,"node_id":"MDEwOlJlcG9zaXRvcnkzMjg1OTc=","name":"nit","full_name":"nitlang/nit","private":false,"owner":{"login":"nitlang","id":5420298,"node_id":"MDEyOk9yZ2FuaXphdGlvbjU0MjAyOTg=","avatar_url":"https://avatars1.githubusercontent.com/u/5420298?v=4","gravatar_id":"","url":"https://api.github.com/users/nitlang","html_url":"https://github.com/nitlang","followers_url":"https://api.github.com/users/nitlang/followers","following_url":"https://api.github.com/users/nitlang/following{/other_user}","gists_url":"https://api.github.com/users/nitlang/gists{/gist_id}","starred_url":"https://api.github.com/users/nitlang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nitlang/subscriptions","organizations_url":"https://api.github.com/users/nitlang/orgs","repos_url":"https://api.github.com/users/nitlang/repos","events_url":"https://api.github.com/users/nitlang/events{/privacy}","received_events_url":"https://api.github.com/users/nitlang/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/nitlang/nit","description":"Nit language","fork":false,"url":"https://api.github.com/repos/nitlang/nit","forks_url":"https://api.github.com/repos/nitlang/nit/forks","keys_url":"https://api.github.com/repos/nitlang/nit/keys{/key_id}","collaborators_url":"https://api.github.com/repos/nitlang/nit/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/nitlang/nit/teams","hooks_url":"https://api.github.com/repos/nitlang/nit/hooks","issue_events_url":"https://api.github.com/repos/nitlang/nit/issues/events{/number}","events_url":"https://api.github.com/repos/nitlang/nit/events","assignees_url":"https://api.github.com/repos/nitlang/nit/assignees{/user}","branches_url":"https://api.github.com/repos/nitlang/nit/branches{/branch}","tags_url":"https://api.github.com/repos/nitlang/nit/tags","blobs_url":"https://api.github.com/repos/nitlang/nit/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/nitlang/nit/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/nitlang/nit/git/refs{/sha}","trees_url":"https://api.github.com/repos/nitlang/nit/git/trees{/sha}","statuses_url":"https://api.github.com/repos/nitlang/nit/statuses/{sha}","languages_url":"https://api.github.com/repos/nitlang/nit/languages","stargazers_url":"https://api.github.com/repos/nitlang/nit/stargazers","contributors_url":"https://api.github.com/repos/nitlang/nit/contributors","subscribers_url":"https://api.github.com/repos/nitlang/nit/subscribers","subscription_url":"https://api.github.com/repos/nitlang/nit/subscription","commits_url":"https://api.github.com/repos/nitlang/nit/commits{/sha}","git_commits_url":"https://api.github.com/repos/nitlang/nit/git/commits{/sha}","comments_url":"https://api.github.com/repos/nitlang/nit/comments{/number}","issue_comment_url":"https://api.github.com/repos/nitlang/nit/issues/comments{/number}","contents_url":"https://api.github.com/repos/nitlang/nit/contents/{+path}","compare_url":"https://api.github.com/repos/nitlang/nit/compare/{base}...{head}","merges_url":"https://api.github.com/repos/nitlang/nit/merges","archive_url":"https://api.github.com/repos/nitlang/nit/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/nitlang/nit/downloads","issues_url":"https://api.github.com/repos/nitlang/nit/issues{/number}","pulls_url":"https://api.github.com/repos/nitlang/nit/pulls{/number}","milestones_url":"https://api.github.com/repos/nitlang/nit/milestones{/number}","notifications_url":"https://api.github.com/repos/nitlang/nit/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/nitlang/nit/labels{/name}","releases_url":"https://api.github.com/repos/nitlang/nit/releases{/id}","deployments_url":"https://api.github.com/repos/nitlang/nit/deployments","created_at":"2009-10-06T15:03:00Z","updated_at":"2019-06-17T13:13:03Z","pushed_at":"2019-06-19T00:02:11Z","git_url":"git://github.com/nitlang/nit.git","ssh_url":"git@github.com:nitlang/nit.git","clone_url":"https://github.com/nitlang/nit.git","svn_url":"https://github.com/nitlang/nit","homepage":"http://nitlanguage.org","size":123083,"stargazers_count":187,"watchers_count":187,"language":"C","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":56,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":171,"license":{"key":"other","name":"Other","spdx_id":"NOASSERTION","url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":56,"open_issues":171,"watchers":187,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/nitlang/nit/pulls/1000"},"html":{"href":"https://github.com/nitlang/nit/pull/1000"},"issue":{"href":"https://api.github.com/repos/nitlang/nit/issues/1000"},"comments":{"href":"https://api.github.com/repos/nitlang/nit/issues/1000/comments"},"review_comments":{"href":"https://api.github.com/repos/nitlang/nit/pulls/1000/comments"},"review_comment":{"href":"https://api.github.com/repos/nitlang/nit/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/nitlang/nit/pulls/1000/commits"},"statuses":{"href":"https://api.github.com/repos/nitlang/nit/statuses/273b078ecc1a395f260992ec9fb08a31e8c338d9"}},"author_association":"MEMBER","merged":true,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":{"login":"privat","id":135828,"node_id":"MDQ6VXNlcjEzNTgyOA==","avatar_url":"https://avatars1.githubusercontent.com/u/135828?v=4","gravatar_id":"","url":"https://api.github.com/users/privat","html_url":"https://github.com/privat","followers_url":"https://api.github.com/users/privat/followers","following_url":"https://api.github.com/users/privat/following{/other_user}","gists_url":"https://api.github.com/users/privat/gists{/gist_id}","starred_url":"https://api.github.com/users/privat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/privat/subscriptions","organizations_url":"https://api.github.com/users/privat/orgs","repos_url":"https://api.github.com/users/privat/repos","events_url":"https://api.github.com/users/privat/events{/privacy}","received_events_url":"https://api.github.com/users/privat/received_events","type":"User","site_admin":false},"comments":7,"review_comments":0,"maintainer_can_modify":false,"commits":11,"additions":282,"deletions":268,"changed_files":67}
\ No newline at end of file
diff --git a/lib/github/tests/mock/repo_pulls_comment_21010363.res b/lib/github/tests/mock/repo_pulls_comment_21010363.res
new file mode 100644 (file)
index 0000000..c6f62ee
--- /dev/null
@@ -0,0 +1 @@
+{"url":"https://api.github.com/repos/nitlang/nit/pulls/comments/21010363","pull_request_review_id":null,"id":21010363,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxMDEwMzYz","diff_hunk":"@@ -981,11 +983,11 @@ redef class AAttrPropdef\n \n \t\t\t\tif mtype == null then return\n \t\t\tend\n-\t\telse if ntype != null then\n+\t\telse if ntype != null and inherited_type == mtype then\n \t\t\tif nexpr isa ANewExpr then\n \t\t\t\tvar xmtype = modelbuilder.resolve_mtype(mmodule, mclassdef, nexpr.n_type)\n \t\t\t\tif xmtype == mtype then\n-\t\t\t\t\tmodelbuilder.advice(ntype, \"useless-type\", \"Warning: useless type definition\")\n+\t\t\t\t\tmodelbuilder.advice(ntype, \"useless-type\", \"Warning: useless type definition {inherited_type or else \"?\"}\")","path":"src/modelize/modelize_property.nit","position":null,"original_position":26,"commit_id":"ce5e187a87ed5c41144ea5637188a0677d840fdc","original_commit_id":"5f0ab1c7f3c560a67867d5eb08f5c3082f251c20","user":{"login":"jcbrinfo","id":6044484,"node_id":"MDQ6VXNlcjYwNDQ0ODQ=","avatar_url":"https://avatars0.githubusercontent.com/u/6044484?v=4","gravatar_id":"","url":"https://api.github.com/users/jcbrinfo","html_url":"https://github.com/jcbrinfo","followers_url":"https://api.github.com/users/jcbrinfo/followers","following_url":"https://api.github.com/users/jcbrinfo/following{/other_user}","gists_url":"https://api.github.com/users/jcbrinfo/gists{/gist_id}","starred_url":"https://api.github.com/users/jcbrinfo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jcbrinfo/subscriptions","organizations_url":"https://api.github.com/users/jcbrinfo/orgs","repos_url":"https://api.github.com/users/jcbrinfo/repos","events_url":"https://api.github.com/users/jcbrinfo/events{/privacy}","received_events_url":"https://api.github.com/users/jcbrinfo/received_events","type":"User","site_admin":false},"body":"Warning: `inherited_type` is always non null here.\n","created_at":"2014-11-27T20:39:29Z","updated_at":"2014-11-28T01:05:12Z","html_url":"https://github.com/nitlang/nit/pull/945#discussion_r21010363","pull_request_url":"https://api.github.com/repos/nitlang/nit/pulls/945","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/nitlang/nit/pulls/comments/21010363"},"html":{"href":"https://github.com/nitlang/nit/pull/945#discussion_r21010363"},"pull_request":{"href":"https://api.github.com/repos/nitlang/nit/pulls/945"}}}
\ No newline at end of file
diff --git a/lib/github/tests/mock/user_Morriar.res b/lib/github/tests/mock/user_Morriar.res
new file mode 100644 (file)
index 0000000..5a491b1
--- /dev/null
@@ -0,0 +1 @@
+{"login":"Morriar","id":583144,"node_id":"MDQ6VXNlcjU4MzE0NA==","avatar_url":"https://avatars2.githubusercontent.com/u/583144?v=4","gravatar_id":"","url":"https://api.github.com/users/Morriar","html_url":"https://github.com/Morriar","followers_url":"https://api.github.com/users/Morriar/followers","following_url":"https://api.github.com/users/Morriar/following{/other_user}","gists_url":"https://api.github.com/users/Morriar/gists{/gist_id}","starred_url":"https://api.github.com/users/Morriar/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Morriar/subscriptions","organizations_url":"https://api.github.com/users/Morriar/orgs","repos_url":"https://api.github.com/users/Morriar/repos","events_url":"https://api.github.com/users/Morriar/events{/privacy}","received_events_url":"https://api.github.com/users/Morriar/received_events","type":"User","site_admin":false,"name":"Alexandre Terrasa","company":null,"blog":"moz-code.org","location":null,"email":"alexandre@moz-code.org","hireable":null,"bio":null,"public_repos":96,"public_gists":1,"followers":42,"following":10,"created_at":"2011-01-25T17:50:36Z","updated_at":"2019-06-15T01:41:56Z"}
\ No newline at end of file
diff --git a/lib/github/tests/test_api.nit b/lib/github/tests/test_api.nit
new file mode 100644 (file)
index 0000000..7176aaa
--- /dev/null
@@ -0,0 +1,346 @@
+# 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.
+
+module test_api is test
+
+intrude import api
+
+# GithubAPI testing
+#
+# To avoid test flakyness we test the GithubAPI against a mock of the real one.
+# For each api request we return a cache file of the real API response body.
+#
+# Cache files can be automatically created and updated by setting
+# `update_responses_cache` to `true` then running `nitunit`.
+class MockGithubCurl
+       super GithubCurl
+
+       # Mock so it returns the response from a file
+       #
+       # See `update_responses_cache`.
+       redef fun get_and_parse(uri) do
+               print uri # for debugging
+
+               var path = uri.replace("https://api.github.com/", "/")
+               assert has_response(path)
+
+               if update_responses_cache then
+                       var file = response_file(path)
+                       save_actual_response(uri, file)
+               end
+
+               var response = response_string(path).parse_json
+               if response_is_error(path) then
+                       var title = "GithubAPIError"
+                       var msg = response.as(JsonObject)["message"].as(String)
+                       var err = new GithubError(msg, title)
+                       err.json["requested_uri"] = uri
+                       err.json["status_code"] = response_code(path)
+                       return err
+               end
+               return response
+       end
+
+       var test_responses: Map[String, String] do
+               var map = new HashMap[String, String]
+               map["/user"] = "user_Morriar"
+               map["/users/Morriar"] = "user_Morriar"
+               map["/repos/nitlang/nit"] = "repo_nit"
+               map["/repos/nitlang/nit/labels/ok_will_merge"] = "repo_labels_ok_will_merge"
+               map["/repos/nitlang/nit/milestones/4"] = "repo_milestones_4"
+               map["/repos/nitlang/nit/branches"] = "repo_branches_nit"
+               map["/repos/nitlang/nit/branches/master"] = "repo_branches_master"
+               map["/repos/nitlang/nit/issues/1000"] = "repo_issues_1000"
+               map["/repos/nitlang/nit/issues/comments/6020149"] = "repo_issues_comments_6020149"
+               map["/repos/nitlang/nit/issues/events/199674194"] = "repo_issues_events_199674194"
+               map["/repos/nitlang/nit/pulls/1000"] = "repo_pulls_1000"
+               map["/repos/nitlang/nit/commits/64ce1f"] = "repo_commits_64ce1f"
+               map["/repos/nitlang/nit/comments/8982707"] = "repo_comments_8982707"
+               map["/repos/nitlang/nit/pulls/comments/21010363"] = "repo_pulls_comment_21010363"
+               # errors
+               map["/users/not_found/not_found"] = "errors_404"
+               return map
+       end
+
+       # Does `self` have a mock response for Github `path`?
+       fun has_response(path: String): Bool do
+               return test_responses.has_key(path)
+       end
+
+       # Root responses cache directory
+       var responses_dir: String is lazy do
+               var path = "NIT_TESTING_PATH".environ.dirname / "mock"
+               path.mkdir
+               return path
+       end
+
+       # Returns the response file path for a Github `path`
+       fun response_file(path: String): String do
+               assert has_response(path)
+               return "{responses_dir / test_responses[path]}.res"
+       end
+
+       # Returns the response body string for a Github `path`
+       fun response_string(path: String): String do
+               var file = response_file(path)
+               assert file.file_exists
+               return file.to_path.read_all
+       end
+
+       # Is this response a simulated error?
+       fun response_is_error(path: String): Bool do
+               assert has_response(path)
+               return test_responses[path].has_prefix("errors_")
+       end
+
+       # Status code of a simulated error
+       #
+       # See `response_is_error`.
+       fun response_code(path: String): String do
+               assert response_is_error(path)
+               return test_responses[path].split("_").last
+       end
+
+       # Response caching
+
+       # Activate caching
+       #
+       # Change this value to `true` then run nitunit to cache the responses
+       # from the Github API.
+       #
+       # Default is `false`.
+       var update_responses_cache = false
+
+       # Save the actual Github API response body for `uri` to a `file`
+       private fun save_actual_response(uri, file: String) do
+               assert update_responses_cache
+
+               var request = new CurlHTTPRequest(uri)
+               request.user_agent = actual_curl.user_agent
+               request.headers = actual_curl.header
+               var response = request.execute
+
+               if response isa CurlResponseSuccess then
+                       response.body_str.write_to_file(file)
+               else if response isa CurlResponseFailed then
+                       response.error_msg.write_to_file(file)
+               else abort
+
+               print "Response to `{uri}` saved at `{file}`"
+       end
+
+       # Actual GithubCurl instance used for caching
+       private var actual_curl = new GithubCurl(get_github_oauth, "nitunit")
+end
+
+class TestGithubAPI
+       test
+
+       var mock = new MockGithubCurl("test", "test")
+
+       fun api: GithubAPI do
+               var api = new GithubAPI("test")
+               api.ghcurl = mock
+               return api
+       end
+
+       fun test_deserialize is test do
+               var response = mock.response_string("/users/Morriar")
+               var obj = api.deserialize(response)
+               assert obj isa User
+               assert obj.login == "Morriar"
+       end
+
+       fun test_sanitize_url is test do
+               # TODO better tests
+               assert api.sanitize_uri("/repos/Nit with spaces/") == "/repos/Nit%20with%20spaces/"
+       end
+
+       fun test_get is test do
+               var api = self.api
+               var obj = api.get("/users/Morriar")
+               assert not api.was_error
+               assert api.last_error == null
+               assert obj isa JsonObject
+               assert obj["login"] == "Morriar"
+       end
+
+       fun test_get_404 is test do
+               var api = self.api
+               var res = api.get("/users/not_found/not_found")
+               assert res == null
+               assert api.was_error
+               var err = api.last_error
+               assert err isa GithubError
+               assert err.name == "GithubAPIError"
+               assert err.message == "Not Found"
+       end
+
+       fun test_load_from_github is test do
+               var api = self.api
+               var obj = api.load_from_github("/users/Morriar")
+               assert not api.was_error
+               assert api.last_error == null
+               assert obj isa User
+               assert obj.login == "Morriar"
+       end
+
+       fun test_load_from_github_404 is test do
+               var api = self.api
+               var res = api.load_from_github("/users/not_found/not_found")
+               assert res == null
+               assert api.was_error
+               var err = api.last_error
+               assert err isa GithubError
+               assert err.name == "GithubAPIError"
+               assert err.message == "Not Found"
+       end
+
+       # TODO test more error cases
+
+       fun test_get_auth_user is test do
+               var user = api.load_auth_user
+               assert user isa User
+               assert user.login == "Morriar"
+               assert user.avatar_url == "https://avatars2.githubusercontent.com/u/583144?v=4"
+               assert user.name == "Alexandre Terrasa"
+               assert user.email == "alexandre@moz-code.org"
+               assert user.blog == "moz-code.org"
+       end
+
+       fun test_get_user is test do
+               var user = api.load_user("Morriar")
+               assert user isa User
+               assert user.login == "Morriar"
+               assert user.avatar_url == "https://avatars2.githubusercontent.com/u/583144?v=4"
+               assert user.name == "Alexandre Terrasa"
+               assert user.email == "alexandre@moz-code.org"
+               assert user.blog == "moz-code.org"
+       end
+
+       fun test_get_repo is test do
+               var repo = api.load_repo("nitlang/nit")
+               assert repo isa Repo
+               assert repo.full_name == "nitlang/nit"
+               assert repo.name == "nit"
+               assert repo.owner.login == "nitlang"
+               assert repo.default_branch == "master"
+       end
+
+       private var repo: Repo is lazy do return api.load_repo("nitlang/nit").as(not null)
+
+       fun test_get_branches is test do
+               var branches = api.load_repo_branches(repo)
+               assert branches.length == 2
+               assert branches.first.name == "master"
+               assert branches.last.name == "next"
+       end
+
+       # TODO issues
+       # TODO repo_last_issue
+       # TODO labels
+       # TODO milestones
+       # TODO pulls
+       # TODO contrib_stats
+
+       fun test_get_branch is test do
+               var branch = api.load_branch(repo, "master")
+               assert branch isa Branch
+               assert branch.name == "master"
+       end
+
+       # TODO branch commits
+
+       fun test_get_commit is test do
+               var commit = api.load_commit(repo, "64ce1f")
+               assert commit isa Commit
+               assert commit.sha == "64ce1f587209024f5de46d06c70526a569ff537f"
+               # TODO other fields
+       end
+
+       fun test_get_issue is test do
+               var issue = api.load_issue(repo, 1000)
+               assert issue isa Issue
+               assert issue.number == 1000
+               assert issue.title == "Raise nitc from the dead"
+               assert issue.user.as(User).login == "privat"
+               assert issue.comments == 7
+               assert issue.created_at == "2014-12-11T02:55:09Z"
+               assert issue.closed_at == "2014-12-13T15:38:09Z"
+               assert issue.closed_by.as(User).login == "privat"
+               assert issue.body == "Raise dead on `nitc`.\nIt's super effective...\n"
+               assert issue.is_pull_request
+       end
+
+       # TODO issue comments
+       # TODO issue events
+
+       fun test_get_pull is test do
+               var pull = api.load_pull(repo, 1000)
+               assert pull isa Issue
+               assert pull.number == 1000
+               assert pull.title == "Raise nitc from the dead"
+               assert pull.user.as(User).login == "privat"
+               assert pull.comments == 7
+               assert pull.created_at == "2014-12-11T02:55:09Z"
+               assert pull.closed_at == "2014-12-13T15:38:09Z"
+               assert pull.merged_by.as(User).login == "privat"
+               assert pull.body == "Raise dead on `nitc`.\nIt's super effective...\n"
+       end
+
+       fun test_get_label is test do
+               var labl = api.load_label(repo, "ok_will_merge")
+               assert labl isa Label
+               assert labl.name == "ok_will_merge"
+       end
+
+       fun test_get_milestone is test do
+               var milestone = api.load_milestone(repo, 4)
+               assert milestone isa Milestone
+               assert milestone.title == "v1.0prealpha"
+               # TODO other fields
+       end
+
+       fun test_get_issue_event is test do
+               var event = api.load_issue_event(repo, 199674194)
+               assert event isa IssueEvent
+               assert event.actor.login == "privat"
+               assert event.event == "labeled"
+               assert event.labl.as(Label).name == "need_review"
+       end
+
+       fun test_get_issue_comment is test do
+               var comment = api.load_issue_comment(repo, 6020149)
+               assert comment isa IssueComment
+               assert comment.user.login == "privat"
+               assert comment.created_at.to_s == "2012-05-30T20:16:54Z"
+               assert comment.issue_number == 10
+       end
+
+       fun test_get_comment is test do
+               var comment = api.load_commit_comment(repo, 8982707)
+               assert comment isa CommitComment
+               assert comment.user.login == "Morriar"
+               assert comment.body == "For testing purposes...\n"
+               assert comment.commit_id == "7eacb86d1e24b7e72bc9ac869bf7182c0300ceca"
+       end
+
+       fun test_get_review_comments is test do
+               var comment = api.load_review_comment(repo, 21010363)
+               assert comment isa ReviewComment
+               assert comment.path == "src/modelize/modelize_property.nit"
+               assert comment.original_position == 26
+               assert comment.pull_number == 945
+       end
+end
diff --git a/lib/ini/README.md b/lib/ini/README.md
new file mode 100644 (file)
index 0000000..4f9a478
--- /dev/null
@@ -0,0 +1,316 @@
+# `ini` - Read and write INI configuration files
+
+[INI files](https://en.wikipedia.org/wiki/INI_file) are simple text files with
+a basic structure composed of sections, properties and values used to store
+configuration parameters.
+
+Here's an example from the `package.ini` of this package:
+
+~~~
+import ini
+
+var package_ini = """
+[package]
+name=ini
+desc=Read and write INI configuration files.
+[upstream]
+git=https://github.com/nitlang/nit.git
+git.directory=lib/ini/
+"""
+~~~
+
+## Basic usage
+
+`IniFile` is used to parse INI strings and access their content:
+
+~~~
+var ini = new IniFile.from_string(package_ini)
+assert ini["package.name"] == "ini"
+assert ini["upstream.git.directory"] == "lib/ini/"
+assert ini["unknown.unknown"] == null
+~~~
+
+`IniFile` can also load INI configuration from a file:
+
+~~~
+package_ini.write_to_file("my_package.ini")
+
+ini = new IniFile.from_file("my_package.ini")
+assert ini["package.name"] == "ini"
+assert ini["upstream.git.directory"] == "lib/ini/"
+
+"my_package.ini".to_path.delete
+~~~
+
+INI content can be added or edited through the `IniFile` API then written to
+a stream or a file.
+
+~~~
+ini["package.name"] = "new name"
+ini["upstream.git.directory"] = "/dev/null"
+ini["section.key"] = "value"
+
+var stream = new StringWriter
+ini.write_to(stream)
+
+assert stream.to_s == """
+[package]
+name=new name
+desc=Read and write INI configuration files.
+[upstream]
+git=https://github.com/nitlang/nit.git
+git.directory=/dev/null
+[section]
+key=value
+"""
+~~~
+
+## INI content
+
+### Properties
+
+Properties are the basic element of the INI format.
+Every property correspond to a *key* associated to a *value* thanks to the equal (`=`) sign.
+
+~~~
+ini = new IniFile.from_string("""
+key1=value1
+key2=value2
+""")
+assert ini["key1"] == "value1"
+assert ini["key2"] == "value2"
+assert ini.length == 2
+~~~
+
+Accessing an unknown property returns `null`:
+
+~~~
+assert ini["unknown"] == null
+~~~
+
+Properties can be iterated over:
+
+~~~
+var i = 1
+for key, value in ini do
+       assert key == "key{i}"
+       assert value == "value{i}"
+       i += 1
+end
+~~~
+
+Property keys cannot contain the character `=`.
+Values can contain any character.
+Spaces are trimmed.
+
+~~~
+ini = new IniFile.from_string("""
+prop=erty1=value1
+ property2 =  value2
+property3=value3 ; with semicolon
+""")
+assert ini[";property1"] == null
+assert ini["prop=erty1"] == null
+assert ini["prop"] == "erty1=value1"
+assert ini["property2"] == "value2"
+assert ini[" property2 "] == "value2"
+assert ini["property3"] == "value3 ; with semicolon"
+~~~
+
+Both keys and values are case sensitive.
+
+~~~
+ini = new IniFile.from_string("""
+Property1=value1
+property2=Value2
+""")
+assert ini["property1"] == null
+assert ini["Property1"] == "value1"
+assert ini["property2"] != "value2"
+assert ini["property2"] == "Value2"
+~~~
+
+### Sections
+
+Properties may be grouped into arbitrary sections.
+The section name appears on a line by itself between square brackets (`[` and `]`).
+
+All keys after the section declaration are associated with that section.
+The is no explicit "end of section" delimiter; sections end at the next section
+declaration or the end of the file.
+Sections cannot be nested.
+
+~~~
+var content = """
+key1=value1
+key2=value2
+[section1]
+key1=value3
+key2=value4
+[section2]
+key1=value5
+"""
+
+ini = new IniFile.from_string(content)
+assert ini["key1"] == "value1"
+assert ini["unknown"] == null
+assert ini["section1.key1"] == "value3"
+assert ini["section1.unknown"] == null
+assert ini["section2.key1"] == "value5"
+~~~
+
+Sections can be iterated over:
+
+~~~
+i = 1
+for section in ini.sections do
+       assert section.name == "section{i}"
+       assert section["key1"].has_prefix("value")
+       i += 1
+end
+~~~
+
+When iterating over a file properties, only properties at root are returned.
+`flatten` can be used to iterate over all properties including the one from
+sections.
+
+~~~
+assert ini.join(", ", ": ") == "key1: value1, key2: value2"
+assert ini.flatten.join(", ", ": ") ==
+       "key1: value1, key2: value2, section1.key1: value3, section1.key2: value4, section2.key1: value5"
+
+i = 0
+for key, value in ini do
+       i += 1
+       assert key == "key{i}" and value == "value{i}"
+end
+assert i == 2
+
+~~~
+
+Sections name may contain any character including brackets (`[` and `]`).
+Spaces are trimmed.
+
+~~~
+ini = new IniFile.from_string("""
+[[section1]]
+key=value1
+[ section 2 ]
+key=value2
+[section1.section3]
+key=value3
+""")
+assert ini.sections.length == 3
+assert ini["[section1].key"] == "value1"
+assert ini["section 2.key"] == "value2"
+assert ini["section1.section3.key"] == "value3"
+assert ini.sections.last.name == "section1.section3"
+~~~
+
+The dot `.` notation is used to create new sections with `[]=`.
+Unknown sections will be created on the fly.
+
+~~~
+ini = new IniFile
+ini["key"] = "value1"
+ini["section1.key"] = "value2"
+ini["section2.key"] = "value3"
+
+stream = new StringWriter
+ini.write_to(stream)
+assert stream.to_s == """
+key=value1
+[section1]
+key=value2
+[section2]
+key=value3
+"""
+~~~
+
+Sections can also be created manually:
+
+~~~
+ini = new IniFile
+ini["key"] = "value1"
+
+var section = new IniSection("section1")
+section["key"] = "value2"
+ini.sections.add section
+
+stream = new StringWriter
+ini.write_to(stream)
+assert stream.to_s == """
+key=value1
+[section1]
+key=value2
+"""
+~~~
+
+### Comments
+
+Comments are indicated by semicolon (`;`) or a number sign (`#`) at the begining
+of the line. Commented lines are ignored as well as empty lines.
+
+~~~
+ini = new IniFile.from_string("""
+; This is a comment.
+; property1=value1
+
+# This is another comment.
+# property2=value2
+""")
+assert ini.is_empty
+~~~
+
+### Unicode support
+
+INI files support Unicode:
+
+~~~
+ini = new IniFile.from_string("""
+property❤=héhé
+""")
+assert ini["property❤"] == "héhé"
+~~~
+
+## Error handling
+
+By default `IniFile` does not stop when it cannot parse a line in a string (loaded
+by `from_string` or `load_string`) or a file (loaded by `from_file` or `load_file`).
+
+~~~
+ini = new IniFile.from_string("""
+key1=value1
+key2
+key3=value3
+""")
+
+assert ini.length == 2
+assert ini["key1"] == "value1"
+assert ini["key2"] == null
+assert ini["key3"] == "value3"
+~~~
+
+
+This behaviour can be modified by setting `stop_on_first_error` to `true`.
+
+~~~
+ini = new IniFile.from_string("""
+key1=value1
+key2
+key3=value3
+""", stop_on_first_error = true)
+
+assert ini.length == 1
+assert ini["key1"] == "value1"
+assert ini["key2"] == null
+assert ini["key3"] == null
+~~~
+
+Wathever the value set for `stop_on_first_error`, errors can be checked thanks
+to the `errors` array:
+
+~~~
+assert ini.errors.length == 1
+assert ini.errors.first.message == "Unexpected string `key2` at line 2."
+~~~
index 815995f..e965323 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# Handle ini config files.
+# Read and write INI configuration files
 module ini
 
-# A configuration tree that can read and store data in ini format
+import core
+intrude import core::collection::hash_collection
+
+# Read and write INI configuration files
+#
+# In an INI file, properties (or keys) are associated to values thanks to the
+# equals symbol (`=`).
+# Properties may be grouped into section marked between brackets (`[` and `]`).
+#
+# ~~~
+# var ini_string = """
+# ; Example INI
+# key=value1
+# [section1]
+# key=value2
+# [section2]
+# key=value3
+# """
+# ~~~
+#
+# The main class, `IniFile`, can be created from an INI string and allows easy
+# access to its content.
+#
+# ~~~
+# # Read INI from string
+# var ini = new IniFile.from_string(ini_string)
+#
+# # Check keys presence
+# assert ini.has_key("key")
+# assert ini.has_key("section1.key")
+# assert not ini.has_key("not.found")
+#
+# # Access values
+# assert ini["key"] == "value1"
+# assert ini["section2.key"] == "value3"
+# assert ini["not.found"] == null
+#
+# # Access sections
+# assert ini.sections.length == 2
+# assert ini.section("section1")["key"] == "value2"
+# ~~~
 #
-# Write example:
+# `IniFile` can also be used to create new INI files from scratch, or edit
+# existing ones through its API.
 #
-#     var config = new ConfigTree("config.ini")
-#     config["goo"] = "goo"
-#     config["foo.bar"] = "foobar"
-#     config["foo.baz"] = "foobaz"
-#     config.save
-#     assert config.to_map.length == 3
+# ~~~
+# # Create a new INI file and write it to disk
+# ini = new IniFile
+# ini["key"] = "value1"
+# ini["section1.key"] = "value2"
+# ini["section2.key"] = "value3"
+# ini.write_to_file("my_config.ini")
 #
-# Read example:
+# # Load the INI file from disk
+# ini = new IniFile.from_file("my_config.ini")
+# assert ini["key"] == "value1"
+# assert ini["section1.key"] == "value2"
+# assert ini["section2.key"] == "value3"
 #
-#     config = new ConfigTree("config.ini")
-#     assert config.has_key("foo.bar")
-#     assert config["foo.bar"] == "foobar"
-class ConfigTree
+# "my_config.ini".to_path.delete
+# ~~~
+class IniFile
        super Writable
+       super HashMap[String, nullable String]
 
-       # The ini file used to read/store data
-       var ini_file: String
-
-       init do if ini_file.file_exists then load
+       # Create a IniFile from a `string` content
+       #
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # key1=value1
+       # [section1]
+       # key2=value2
+       # """)
+       # assert ini["key1"] == "value1"
+       # assert ini["section1.key2"] == "value2"
+       # ~~~
+       #
+       # See also `stop_on_first_error` and `errors`.
+       init from_string(string: String, stop_on_first_error: nullable Bool) do
+               init stop_on_first_error or else false
+               load_string(string)
+       end
 
-       # Get the config value for `key`
-       #
-       #     var config = new ConfigTree("config.ini")
-       #     assert config["goo"] == "goo"
-       #     assert config["foo.bar"] == "foobar"
-       #     assert config["foo.baz"] == "foobaz"
-       #     assert config["fail.fail"] == null
-       fun [](key: String): nullable String do
-               var node = get_node(key)
-               if node == null then return null
-               return node.value
+       # Create a IniFile from a `file` content
+       #
+       # ~~~
+       # """
+       # key1=value1
+       # [section1]
+       # key2=value2
+       # """.write_to_file("my_config.ini")
+       #
+       # var ini = new IniFile.from_file("my_config.ini")
+       # assert ini["key1"] == "value1"
+       # assert ini["section1.key2"] == "value2"
+       #
+       # "my_config.ini".to_path.delete
+       # ~~~
+       #
+       # See also `stop_on_first_error` and `errors`.
+       init from_file(file: String, stop_on_first_error: nullable Bool) do
+               init stop_on_first_error or else false
+               load_file(file)
        end
 
-       # Get the config values under `key`
+       # Sections composing this IniFile
        #
-       #     var config = new ConfigTree("config.ini")
-       #     var values = config.at("foo")
-       #     assert values.has_key("bar")
-       #     assert values.has_key("baz")
-       #     assert not values.has_key("goo")
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # [section1]
+       # key1=value1
+       # [ section 2 ]
+       # key2=value2
+       # """)
+       # assert ini.sections.length == 2
+       # assert ini.sections.first.name == "section1"
+       # assert ini.sections.last.name == "section 2"
+       # ~~~
+       var sections = new Array[IniSection]
+
+       # Get a section by its `name`
        #
-       # Return null if the key does not exists.
+       # Returns `null` if the section is not found.
        #
-       #     assert config.at("fail.fail") == null
-       fun at(key: String): nullable Map[String, String] do
-               var node = get_node(key)
-               if node == null then return null
-               var map = new HashMap[String, String]
-               for k, child in node.children do
-                       var value = child.value
-                       if value == null then continue
-                       map[k] = value
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # [section1]
+       # key1=value1
+       # [section2]
+       # key2=value2
+       # """)
+       # assert ini.section("section1") isa IniSection
+       # assert ini.section("section2").name == "section2"
+       # assert ini.section("not.found") == null
+       # ~~~
+       fun section(name: String): nullable IniSection do
+               for section in sections do
+                       if section.name == name then return section
                end
-               return map
+               return null
        end
 
-       # Set `value` at `key`
-       #
-       #     var config = new ConfigTree("config.ini")
-       #     assert config["foo.bar"] == "foobar"
-       #     config["foo.bar"] = "baz"
-       #     assert config["foo.bar"] == "baz"
-       fun []=(key: String, value: nullable String) do
-               set_node(key, value)
+       # Does this file contains no properties and no sections?
+       #
+       # ~~~
+       # var ini = new IniFile.from_string("")
+       # assert ini.is_empty
+       #
+       # ini = new IniFile.from_string("""
+       # key=value
+       # """)
+       # assert not ini.is_empty
+       #
+       # ini = new IniFile.from_string("""
+       # [section]
+       # """)
+       # assert not ini.is_empty
+       # ~~~
+       redef fun is_empty do return super and sections.is_empty
+
+       # Is there a property located at `key`?
+       #
+       # Returns `true` if the `key` is not found of if its associated value is `null`.
+       #
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # key=value1
+       # [section1]
+       # key=value2
+       # [section2]
+       # key=value3
+       # """)
+       # assert ini.has_key("key")
+       # assert ini.has_key("section1.key")
+       # assert ini.has_key("section2.key")
+       # assert not ini.has_key("section1")
+       # assert not ini.has_key("not.found")
+       # ~~~
+       redef fun has_key(key) do return self[key] != null
+
+       # Get the value associated with a property (`key`)
+       #
+       # Returns `null` if the key is not found.
+       # Section properties can be accessed with the `.` notation.
+       #
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # key=value1
+       # [section1]
+       # key=value2
+       # [section2]
+       # key=value3
+       # """)
+       # assert ini["key"] == "value1"
+       # assert ini["section1.key"] == "value2"
+       # assert ini["section2.key"] == "value3"
+       # assert ini["section1"] == null
+       # assert ini["not.found"] == null
+       # ~~~
+       redef fun [](key) do
+               if key == null then return null
+               key = key.to_s.trim
+
+               # Look in root
+               var node = node_at(key)
+               if node != null then return node.value
+
+               # Look in sections
+               for section in sections do
+                       # Matched if the section name is a prefix of the key
+                       if not key.has_prefix(section.name) then continue
+                       var skey = key.substring(section.name.length + 1, key.length)
+                       if section.has_key(skey) then return section[skey]
+               end
+               return null
        end
 
-       # Is `key` in the config?
-       #
-       #     var config = new ConfigTree("config.ini")
-       #     assert config.has_key("goo")
-       #     assert config.has_key("foo.bar")
-       #     assert not config.has_key("zoo")
-       fun has_key(key: String): Bool do
-               var parts = key.split(".").reversed
-               var node = get_root(parts.pop)
-               if node == null then return false
-               while not parts.is_empty do
-                       node = node.get_child(parts.pop)
-                       if node == null then return false
+       # Set the `value` for the property locaated at `key`
+       #
+       # ~~~
+       # var ini = new IniFile
+       # ini["key"] = "value1"
+       # ini["section1.key"] = "value2"
+       # ini["section2.key"] = "value3"
+       #
+       # assert ini["key"] == "value1"
+       # assert ini["section1.key"] == "value2"
+       # assert ini["section2.key"] == "value3"
+       # assert ini.section("section1").name == "section1"
+       # assert ini.section("section2")["key"] == "value3"
+       # ~~~
+       redef fun []=(key, value) do
+               if value == null then return
+               var parts = key.split_once_on(".")
+
+               # No dot notation, store value in root
+               if parts.length == 1 then
+                       super(key.trim, value.trim)
+                       return
+               end
+
+               # First part matches a section, store value in it
+               var section = self.section(parts.first.trim)
+               if section != null then
+                       section[parts.last.trim] = value.trim
+                       return
                end
-               return true
+
+               # No section matched, create a new one and store value in it
+               section = new IniSection(parts.first.trim)
+               section[parts.last.trim] = value.trim
+               sections.add section
        end
 
-       # Get `self` as a Map of `key`, `value`
-       #
-       #     var config = new ConfigTree("config.ini")
-       #     var map = config.to_map
-       #     assert map.has_key("goo")
-       #     assert map.has_key("foo.bar")
-       #     assert map.has_key("foo.baz")
-       #     assert map.length == 3
-       fun to_map: Map[String, String] do
+       # Flatten `self` and its subsection in a `Map` of keys => values
+       #
+       # Properties from section are prefixed with their section names with the
+       # dot (`.`) notation.
+       #
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # key=value1
+       # [section]
+       # key=value2
+       # """)
+       # assert ini.flatten.join(", ", ": ") == "key: value1, section.key: value2"
+       # ~~~
+       fun flatten: Map[String, String] do
                var map = new HashMap[String, String]
-               for node in leaves do
-                       var value = node.value
+               for key, value in self do
                        if value == null then continue
-                       map[node.key] = value
+                       map[key] = value
+               end
+               for section in sections do
+                       for key, value in section do
+                               if value == null then continue
+                               map["{section.name}.{key}"] = value
+                       end
                end
                return map
        end
 
-       redef fun to_s do return to_map.join(", ", ":")
-
-       # Write `self` in `stream`
-       #
-       #     var config = new ConfigTree("config.ini")
-       #     var out = new StringWriter
-       #     config.write_to(out)
-       #     assert out.to_s == """
-       #     goo=goo
-       #     [foo]
-       #     bar=foobar
-       #     baz=foobaz
-       #     """
+       # Write `self` to a `stream`
+       #
+       # Key with `null` values are ignored.
+       # The empty string can be used to represent an empty value.
+       #
+       # ~~~
+       # var ini = new IniFile
+       # ini["key"] = "value1"
+       # ini["key2"] = null
+       # ini["key3"] = ""
+       # ini["section1.key"] = "value2"
+       # ini["section1.key2"] = null
+       # ini["section2.key"] = "value3"
+       #
+       # var stream = new StringWriter
+       # ini.write_to(stream)
+       #
+       # assert stream.to_s == """
+       # key=value1
+       # key3=
+       # [section1]
+       # key=value2
+       # [section2]
+       # key=value3
+       # """
+       # ~~~
        redef fun write_to(stream) do
-               var todo = new Array[ConfigNode].from(roots.reversed)
-               while not todo.is_empty do
-                       var node = todo.pop
-                       if node.children.not_empty then
-                               todo.add_all node.children.values.to_a.reversed
-                       end
-                       if node.children.not_empty and node.parent == null then
-                               stream.write("[{node.name}]\n")
-                       end
-                       var value = node.value
+               for key, value in self do
                        if value == null then continue
-                       var path = node.path
-                       if path.length > 1 then path.shift
-                       stream.write("{path.join(".")}={value}\n")
+                       stream.write "{key}={value}\n"
+               end
+               for section in sections do
+                       stream.write "[{section.name}]\n"
+                       for key, value in section do
+                               if value == null then continue
+                               stream.write "{key}={value}\n"
+                       end
                end
        end
 
-       # Reload config from file
-       # Done automatically at init
-       #
-       # Example with hierarchical ini file:
-       #
-       #     # init file
-       #     var str = """
-       #     foo.bar=foobar
-       #     foo.baz=foobaz
-       #     goo=goo"""
-       #     str.write_to_file("config1.ini")
-       #     # load file
-       #     var config = new ConfigTree("config1.ini")
-       #     assert config["foo.bar"] == "foobar"
-       #
-       # Example with sections:
-       #
-       #     # init file
-       #     str = """
-       #     goo=goo
-       #     [foo]
-       #     bar=foobar
-       #     baz=foobaz
-       #     [boo]
-       #     bar=boobar"""
-       #     str.write_to_file("config2.ini")
-       #     # load file
-       #     config = new ConfigTree("config2.ini")
-       #     assert config["foo.bar"] == "foobar"
-       #     assert config["boo.bar"] == "boobar"
-       #
-       # Example with both hierarchy and section:
-       #
-       #     # init file
-       #     str = """
-       #     goo=goo
-       #     [foo]
-       #     bar.baz=foobarbaz
-       #     [goo.boo]
-       #     bar=gooboobar
-       #     baz.bar=gooboobazbar"""
-       #     str.write_to_file("config3.ini")
-       #     # load file
-       #     config = new ConfigTree("config3.ini")
-       #     assert config["goo"] == "goo"
-       #     assert config["foo.bar.baz"] == "foobarbaz"
-       #     assert config["goo.boo.bar"] == "gooboobar"
-       #     assert config["goo.boo.baz.bar"] == "gooboobazbar"
-       #
-       # Using the array notation
-       #
-       #     str = """
-       #     foo[]=a
-       #     foo[]=b
-       #     foo[]=c"""
-       #     str.write_to_file("config4.ini")
-       #     # load file
-       #     config = new ConfigTree("config4.ini")
-       #     print config.to_map.join(":", ",")
-       #     assert config["foo.0"] == "a"
-       #     assert config["foo.1"] == "b"
-       #     assert config["foo.2"] == "c"
-       #     assert config.at("foo").values.join(",") == "a,b,c"
-       fun load do
-               roots.clear
-               var stream = new FileReader.open(ini_file)
-               var path: nullable String = null
-               var line_number = 0
+       # Read INI content from `string`
+       #
+       # ~~~
+       # var ini = new IniFile
+       # ini.load_string("""
+       # section1.key1=value1
+       # section1.key2=value2
+       # [section2]
+       # key=value3
+       # """)
+       # assert ini["section1.key1"] == "value1"
+       # assert ini["section1.key2"] == "value2"
+       # assert ini["section2.key"] == "value3"
+       # ~~~
+       #
+       # Returns `true` if the parsing finished correctly.
+       #
+       # See also `stop_on_first_error` and `errors`.
+       fun load_string(string: String): Bool do
+               var stream = new StringReader(string)
+               var last_section = null
+               var was_error = false
+               var i = 0
                while not stream.eof do
-                       var line = stream.read_line
-                       line_number += 1
+                       i += 1
+                       var line = stream.read_line.trim
                        if line.is_empty then
                                continue
                        else if line.has_prefix(";") then
                                continue
+                       else if line.has_prefix("#") then
+                               continue
                        else if line.has_prefix("[") then
-                               line = line.trim
-                               var key = line.substring(1, line.length - 2)
-                               path = key
-                               set_node(path, null)
+                               var section = new IniSection(line.substring(1, line.length - 2).trim)
+                               sections.add section
+                               last_section = section
+                               continue
                        else
                                var parts = line.split_once_on("=")
-                               if parts.length == 1 then
+                               if parts.length != 2 then
+                                       # FIXME silent skip?
+                                       # we definitely need exceptions...
+                                       was_error = true
+                                       errors.add new IniError("Unexpected string `{line}` at line {i}.")
+                                       if stop_on_first_error then return was_error
                                        continue
                                end
                                var key = parts[0].trim
-                               var val = parts[1].trim
-                               if path != null then key = "{path}.{key}"
-                               if key.has_suffix("[]") then
-                                       set_array(key, val)
+                               var value = parts[1].trim
+
+                               if last_section != null then
+                                       last_section[key] = value
                                else
-                                       set_node(key,val)
+                                       self[key] = value
                                end
                        end
                end
                stream.close
+               return was_error
        end
 
-       # Save config to file
-       fun save do write_to_file(ini_file)
-
-       private var roots = new Array[ConfigNode]
-
-       # Append `value` to array at `key`
-       private fun set_array(key: String, value: nullable String) do
-               key = key.substring(0, key.length - 2)
-               var len = 0
-               var node = get_node(key)
-               if node != null then len = node.children.length
-               set_node("{key}.{len.to_s}", value)
-       end
-
-       private fun set_node(key: String, value: nullable String) do
-               var parts = key.split(".").reversed
-               var k = parts.pop
-               var root = get_root(k)
-               if root == null then
-                       root = new ConfigNode(k)
-                       if parts.is_empty then
-                               root.value = value
-                       end
-                       roots.add root
-               end
-               while not parts.is_empty do
-                       k = parts.pop
-                       var node = root.get_child(k)
-                       if node == null then
-                               node = new ConfigNode(k)
-                               node.parent = root
-                               root.children[node.name] = node
-                       end
-                       if parts.is_empty then
-                               node.value = value
-                       end
-                       root = node
-               end
-       end
-
-       private fun get_node(key: String): nullable ConfigNode do
-               var parts = key.split(".").reversed
-               var node = get_root(parts.pop)
-               while node != null and not parts.is_empty do
-                       node = node.get_child(parts.pop)
-               end
-               return node
-       end
+       # Load a `file` content as INI
+       #
+       # New properties will be appended to the `self`, existing properties will be
+       # overwrote by the values contained in `file`.
+       #
+       # ~~~
+       # var ini = new IniFile
+       # ini["key1"] = "value1"
+       # ini["key2"] = "value2"
+       #
+       # """
+       # key2=changed
+       # key3=added
+       # """.write_to_file("load_config.ini")
+       #
+       # ini.load_file("load_config.ini")
+       # assert ini["key1"] == "value1"
+       # assert ini["key2"] == "changed"
+       # assert ini["key3"] == "added"
+       #
+       # "load_config.ini".to_path.delete
+       # ~~~
+       #
+       # The process fails silently if the file does not exist.
+       #
+       # ~~~
+       # ini = new IniFile
+       # ini.load_file("ini_not_found.ini")
+       # assert ini.is_empty
+       # ~~~
+       #
+       # Returns `true` if the parsing finished correctly.
+       #
+       # See also `stop_on_first_error` and `errors`.
+       fun load_file(file: String): Bool do return load_string(file.to_path.read_all)
 
-       private fun get_root(name: String): nullable ConfigNode do
-               for root in roots do
-                       if root.name == name then return root
-               end
-               return null
-       end
+       # Stop parsing on the first error
+       #
+       # By default, `load_string` will skip unparsable properties so the string can
+       # be loaded.
+       #
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # key1=value1
+       # key2
+       # key3=value3
+       # """)
+    #
+       # assert ini.length == 2
+       # assert ini["key1"] == "value1"
+       # assert ini["key2"] == null
+       # assert ini["key3"] == "value3"
+       # ~~~
+       #
+       # Set `stop_on_first_error` to `true` to force the parsing to stop.
+       #
+       # ~~~
+       # ini = new IniFile
+       # ini.stop_on_first_error = true
+       # ini.load_string("""
+       # key1=value1
+       # key2
+       # key3=value3
+       # """)
+    #
+       # assert ini.length == 1
+       # assert ini["key1"] == "value1"
+       # assert ini["key2"] == null
+       # assert ini["key3"] == null
+       # ~~~
+       #
+       # See also `errors`.
+       var stop_on_first_error = false is optional, writable
 
-       private fun leaves: Array[ConfigNode] do
-               var res = new Array[ConfigNode]
-               var todo = new Array[ConfigNode]
-               todo.add_all roots
-               while not todo.is_empty do
-                       var node = todo.pop
-                       if node.children.is_empty then
-                               res.add node
-                       else
-                               todo.add_all node.children.values
-                       end
-               end
-               return res
-       end
+       # Errors found during parsing
+       #
+       # Wathever the value of `stop_on_first_error`, errors from parsing a string
+       # or a file are logged into `errors`.
+       #
+       # ~~~
+       # var ini = new IniFile.from_string("""
+       # key1=value1
+       # key2
+       # key3=value3
+       # """)
+    #
+       # assert ini.errors.length == 1
+       # assert ini.errors.first.message == "Unexpected string `key2` at line 2."
+       # ~~~
+       #
+       # `errors` is not cleared between two parsing:
+       #
+       # ~~~
+       # ini.load_string("""
+       # key4
+       # key5=value5
+       # """)
+    #
+       # assert ini.errors.length == 2
+       # assert ini.errors.last.message == "Unexpected string `key4` at line 1."
+       # ~~~
+       #
+       # See also `stop_on_first_error`.
+       var errors = new Array[IniError]
 end
 
-private class ConfigNode
-
-       var parent: nullable ConfigNode = null
-       var children = new HashMap[String, ConfigNode]
-       var name: String is writable
-       var value: nullable String = null
+# A section in a IniFile
+#
+# Section properties values are strings associated keys.
+# Sections cannot be nested.
+#
+# ~~~
+# var section = new IniSection("section")
+# section["key1"] = "value1"
+# section["key2"] = "value2"
+#
+# assert section.length == 2
+# assert section["key1"] == "value1"
+# assert section["not.found"] == null
+# assert section.join(", ", ": ") == "key1: value1, key2: value2"
+#
+# var i = 0
+# for key, value in section do
+#      assert key.has_prefix("key")
+#      assert value.has_prefix("value")
+#      i += 1
+# end
+# assert i == 2
+# ~~~
+class IniSection
+       super HashMap[String, nullable String]
 
-       fun key: String do
-               var parent = self.parent
-               if parent == null then
-                       return name
-               end
-               return "{parent.key}.{name}"
-       end
+       # Section name
+       var name: String
 
-       fun path: Array[String] do
-               var parent = self.parent
-               if parent == null then
-                       return [name]
-               end
-               var res = new Array[String].from(parent.path)
-               res.add name
-               return res
+       # Get the value associated with `key`
+       #
+       # Returns `null` if the `key` is not found.
+       #
+       # ~~~
+       # var section = new IniSection("section")
+       # section["key"] = "value1"
+       # section["sub.key"] = "value2"
+       #
+       # assert section["key"] == "value1"
+       # assert section["sub.key"] == "value2"
+       # assert section["not.found"] == null
+       # ~~~
+       redef fun [](key) do
+               if not has_key(key) then return null
+               return super
        end
+end
 
-       fun get_child(name: String): nullable ConfigNode do
-               if children.has_key(name) then
-                       return children[name]
-               end
-               return null
-       end
+# Error for `IniFile` parsing
+class IniError
+       super Error
 end
index 9611f06..07864ce 100644 (file)
@@ -3,7 +3,7 @@ name=ini
 tags=format,lib
 maintainer=Alexandre Terrasa <alexandre@moz-code.org>
 license=Apache-2.0
-desc=Handle ini config files
+desc=Read and write INI configuration files
 [upstream]
 browse=https://github.com/nitlang/nit/tree/master/lib/ini/
 git=https://github.com/nitlang/nit.git
diff --git a/lib/logger/logger.nit b/lib/logger/logger.nit
new file mode 100644 (file)
index 0000000..974ff9c
--- /dev/null
@@ -0,0 +1,402 @@
+# 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.
+
+# A simple logger for Nit
+#
+# ## Basic Usage
+#
+# Create a new `Logger` with a severity level threshold set to `warn_level`:
+#
+# ~~~
+# var logger = new Logger(warn_level)
+# ~~~
+#
+# Messages with a severity equal or higher than `warn_level` will be displayed:
+#
+# ~~~
+# logger.error "Displays an error."
+# logger.warn "Displays a warning."
+# ~~~
+#
+# Messages with a lower severity are silenced:
+#
+# ~~~
+# logger.info "Displays nothing."
+# ~~~
+#
+# `FileLogger` can be used to output the messages into a file:
+#
+# ~~~
+# var log_file = "my.log"
+#
+# logger = new FileLogger(warn_level, log_file, append = false)
+# logger.error("An error")
+# logger.info("Some info")
+# logger.close
+#
+# assert log_file.to_path.read_all == "An error\n"
+# log_file.to_path.delete
+# ~~~
+#
+# ## Severity levels
+#
+# Each message is associated with a level that indicate its severity.
+# Only messages with a severity equal to or higher than the logger `level`
+# threshold will be displayed.
+#
+# Severity levels from the most severe to the least severe:
+#
+# * `unknown_level`: An unknown message that should always be outputted.
+# * `fatal_level`: An unhandleable error that results in a program crash.
+# * `error_level`: A handleable error condition.
+# * `warn_level`: A warning.
+# * `info_level`: Generic (useful) information about system operation.
+# * `debug_level`: Low-level information for developpers.
+#
+# ## Formatting messages
+#
+# You can create custom formatters by implementing the `Formatter` interface.
+#
+# ~~~
+# class MyFormatter
+#      super Formatter
+#
+#      redef fun format(level, message) do
+#              if level < warn_level then return super
+#              return "!!!{message}!!!"
+#      end
+# end
+# ~~~
+#
+# See `DefaultFormatter` for a more advanced implementation example.
+#
+# Each Logger can be given a default formatter used to format the every messages
+# before outputting them:
+#
+# ~~~
+# var formatter = new MyFormatter
+# var stderr = new StringWriter
+# var logger = new Logger(warn_level, stderr, formatter)
+#
+# logger.warn("This is a warning.")
+# assert stderr.to_s.trim.split("\n").last == "!!!This is a warning.!!!"
+# ~~~
+#
+# Optionally, a `Formatter` can be given to replace the `default_formatter`
+# used by default:
+#
+# ~~~
+# # Create a formatter with no default decorator
+# logger = new Logger(warn_level, stderr, null)
+#
+# # Display a message without any formatter
+# logger.warn("This is a warning.")
+# assert stderr.to_s.trim.split("\n").last == "This is a warning."
+#
+# # Display a message with a custom formatter
+# logger.warn("This is a warning.", formatter)
+# assert stderr.to_s.trim.split("\n").last == "!!!This is a warning.!!!"
+# ~~~
+module logger
+
+import console
+
+# A simple logging utility
+#
+# `Logger` provides a simple way to output messages from applications.
+#
+# Each message is associated with a level that indicate its severity.
+# Only messages with a severity equal to or higher than the logger `level`
+# threshold will be displayed.
+#
+# ~~~
+# var logger = new Logger(warn_level)
+# assert logger.unknown("unkown")
+# assert logger.fatal("fatal")
+# assert logger.error("error")
+# assert logger.warn("warn")
+# assert not logger.info("info")
+# assert not logger.debug("debug")
+# ~~~
+class Logger
+
+       # Severity threshold
+       #
+       # Messages with a severity level greater than or equal to `level` will be displayed.
+       # Default is `warn_level`.
+       #
+       # See `unknown_level`, `fatal_level`, error_level``, `warn_level`,
+       # `info_level` and `debug_level`.
+       var level: Int = warn_level is optional, writable
+
+       # Kind of `Writer` used to output messages
+       type OUT: Writer
+
+       # Writer used to output messages
+       #
+       # Default is `stderr`.
+       var out: OUT = stderr is optional
+
+       # Formatter used to format messages before outputting them
+       #
+       # By default no formatter is used.
+       #
+       # See `DefaultFormatter`.
+       var default_formatter: nullable Formatter = null is optional, writable
+
+       # Output a message with `level` severity
+       #
+       # Only output messages with `level` severity greater than of equal to `self.level`.
+       #
+       # ~~~
+       # var stderr = new StringWriter
+       # var logger = new Logger(warn_level, stderr, null)
+       #
+       # # This message will be displayed:
+       # assert logger.warn("This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       #
+       # # This message will not:
+       # assert not logger.info("This is some info.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       # ~~~
+       #
+       # Each logger can be given a default formatter used to format the messages
+       # before outputting them:
+       #
+       # ~~~
+       # var formatter = new DefaultFormatter(no_color = true)
+       # logger = new Logger(warn_level, stderr, formatter)
+       # logger.warn("This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "Warning: This is a warning."
+       # ~~~
+       #
+       # Optionally, a `Formatter` can be given to replace the `default_formatter`
+       # used by default.
+       #
+       # ~~~
+       # # Create a formatter with no default decorator
+       # logger = new Logger(warn_level, stderr, null)
+       #
+       # # Display a message without any formatter
+       # logger.warn("This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       #
+       # # Display a message with a custom formatter
+       # logger.warn("This is a warning.", formatter)
+       # assert stderr.to_s.trim.split("\n").last == "Warning: This is a warning."
+       # ~~~
+       fun add(level: Int, message: Writable, formatter: nullable Formatter): Bool do
+               var format = formatter or else default_formatter
+               if format == null then
+                       return add_raw(level, message)
+               end
+               return add_raw(level, format.format(level, message))
+       end
+
+       # Output a message with `level` severity without formatting it
+       #
+       # Only output messages with `level` severity greater than of equal to `self.level`.
+       #
+       # ~~~
+       # var stderr = new StringWriter
+       # var logger = new Logger(warn_level, stderr, null)
+       #
+       # # This message will be displayed:
+       # assert logger.add_raw(warn_level, "This is a warning.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       #
+       # # This message will not:
+       # assert not logger.add_raw(info_level, "This is some info.")
+       # assert stderr.to_s.trim.split("\n").last == "This is a warning."
+       # ~~~
+       fun add_raw(level: Int, message: Writable): Bool do
+               if level < self.level then return false
+               out.write(message.write_to_string)
+               out.write("\n")
+               return true
+       end
+
+       # Output a message with `unknown_level` severity
+       #
+       # Unkown severity messages are always outputted.
+       fun unknown(message: String, formatter: nullable Formatter): Bool do
+               return add(unknown_level, message, formatter)
+       end
+
+       # Output a message with `fatal_level` severity
+       fun fatal(message: String, formatter: nullable Formatter): Bool do
+               return add(fatal_level, message, formatter)
+       end
+
+       # Output a message with `error_level` severity
+       fun error(message: String, formatter: nullable Formatter): Bool do
+               return add(error_level, message, formatter)
+       end
+
+       # Output a message with `warn_level` severity
+       fun warn(message: String, formatter: nullable Formatter): Bool do
+               return add(warn_level, message, formatter)
+       end
+
+       # Output a message with `info_level` severity
+       fun info(message: String, formatter: nullable Formatter): Bool do
+               return add(info_level, message, formatter)
+       end
+
+       # Output a message with `debug` severity
+       fun debug(message: String, formatter: nullable Formatter): Bool do
+               return add(debug_level, message, formatter)
+       end
+end
+
+# Log messages to a file
+#
+# ~~~
+# var log_file = "my_file.log"
+# var logger = new FileLogger(warn_level, log_file, append = false)
+# logger.error("An error")
+# logger.info("Some info")
+# logger.close
+# assert log_file.to_path.read_all == "An error\n"
+#
+# logger = new FileLogger(warn_level, log_file, append = true)
+# logger.error("Another error")
+# logger.close
+# assert log_file.to_path.read_all == "An error\nAnother error\n"
+#
+# log_file.to_path.delete
+# ~~~
+class FileLogger
+       super Logger
+       autoinit level, file, append, default_formatter
+
+       redef type OUT: FileWriter
+
+       # File where messages will be written
+       var file: String
+
+       # Append messages to `file`
+       #
+       # If `append` is `false`, the `file` will be overwritten.
+       var append: Bool = true is optional
+
+       init do
+               var old = null
+               if append then
+                       old = file.to_path.read_all
+               end
+               out = new FileWriter.open(file)
+               out.set_buffering_mode(0, buffer_mode_line)
+               if old != null then
+                       out.write(old)
+               end
+       end
+
+       # Close the logger and its `file`
+       fun close do out.close
+end
+
+# Format messages before outputing them
+#
+# A `Logger` can use a `Formatter` to format the messages before outputting them.
+#
+# See `DefaultFormatter`.
+interface Formatter
+
+       # Format `message` depending of its severity `level`
+       fun format(level: Int, message: Writable): Writable do return message
+end
+
+# Default `Logger` formatter
+#
+# The default formatter decorates the messages with severity labels and colors.
+class DefaultFormatter
+       super Formatter
+
+       # Do not decorate messages with colors
+       #
+       # ~~~
+       # var formatter = new DefaultFormatter(no_color = true)
+       # assert formatter.format(error_level, "My message.") == "Error: My message."
+       # ~~~
+       var no_color = false is optional, writable
+
+       redef fun format(level, message) do
+               var string = message.write_to_string
+
+               if level == fatal_level then
+                       string = "Fatal: {string}"
+               else if level == error_level then
+                       string = "Error: {string}"
+               else if level == warn_level then
+                       string = "Warning: {string}"
+               else if level == info_level then
+                       string = "Info: {string}"
+               else if level == debug_level then
+                       string = "Debug: {string}"
+               end
+
+               if no_color then return string
+
+               if level == fatal_level then
+                       return string.red
+               else if level == error_level then
+                       return string.red
+               else if level == warn_level then
+                       return string.yellow
+               else if level == info_level then
+                       return string.purple
+               else if level == debug_level then
+                       return string.blue
+               end
+
+               return string
+       end
+end
+
+redef class Sys
+
+       # Unknown severity level
+       #
+       # These messages are always displayed.
+       #
+       # See `Logger`.
+       var unknown_level = 5
+
+       # Fatal severity level
+       #
+       # See `Logger`.
+       var fatal_level = 4
+
+       # Error severity level
+       #
+       # See `Logger`.
+       var error_level = 3
+
+       # Warning severity level
+       #
+       # See `Logger`.
+       var warn_level = 2
+
+       # Info severity level
+       #
+       # See `Logger`.
+       var info_level = 1
+
+       # Debug severity level
+       #
+       # See `Logger`.
+       var debug_level = 0
+end
diff --git a/lib/logger/package.ini b/lib/logger/package.ini
new file mode 100644 (file)
index 0000000..f4ba7bd
--- /dev/null
@@ -0,0 +1,12 @@
+[package]
+name=logger
+tags=logging,lib
+maintainer=Alexandre Terrasa <alexandre@moz-code.org>
+license=Apache-2.0
+desc=A simple logger for Nit
+[upstream]
+browse=https://github.com/nitlang/nit/tree/master/lib/logger/
+git=https://github.com/nitlang/nit.git
+git.directory=lib/logger/
+homepage=http://nitlanguage.org
+issues=https://github.com/nitlang/nit/issues
index f7c6346..50a0de6 100644 (file)
@@ -511,7 +511,7 @@ with the `use_before` method.
 Next, we’ll create a middleware handler called “LogHandler” that prints the requested
 uri, the response status and the time it took to Popcorn to process the request.
 
-This example gives a simplified version of the `RequestClock` and `ConsoleLog` middlewares.
+This example gives a simplified version of the `RequestClock` and `PopLogger` middlewares.
 
 ~~~
 import popcorn
@@ -584,7 +584,7 @@ Starting with version 0.1, Popcorn provide a set of built-in middleware that can
 be used to develop your app faster.
 
 * `RequestClock`: initializes requests clock.
-* `ConsoleLog`: displays resquest and response status in console (can be used with `RequestClock`).
+* `PopLogger`: displays resquest and response status in console (can be used with `RequestClock`).
 * `SessionInit`: initializes requests session (see the `Sessions` section).
 * `StaticServer`: serves static files (see the `Serving static files with Popcorn` section).
 * `Router`: a mountable mini-app (see the `Mountable routers` section).
index ba3418f..6f0d154 100644 (file)
@@ -17,7 +17,7 @@
 module pop_logging
 
 import pop_handlers
-import console
+import logger
 import realtime
 
 # Initialize a clock for the resquest.
@@ -30,73 +30,71 @@ class RequestClock
 end
 
 # Display log info about request processing.
-class ConsoleLog
+class PopLogger
+       super Logger
        super Handler
 
-       # Logger level
-       #
-       # * `0`: silent
-       # * `1`: errors
-       # * `2`: warnings
-       # * `3`: info
-       # * `4`: debug
-       #
-       # Request status are always logged, whatever the logger level is.
-       var level = 4 is writable
-
        # Do we want colors in the console output?
-       var no_colors = false
+       var no_color = false is optional
+
+       redef var default_formatter = new PopFormatter(no_color) is optional
 
        redef fun all(req, res) do
                var clock = req.clock
                if clock != null then
-                       log "{req.method} {req.url} {status(res)} ({clock.total}s)"
+                       add_raw(info_level, "{req.method} {req.url} {status(res)} ({clock.total}s)")
                else
-                       log "{req.method} {req.url} {status(res)}"
+                       add_raw(info_level, "{req.method} {req.url} {status(res)}")
                end
        end
 
        # Colorize the request status.
        private fun status(res: HttpResponse): String do
-               if no_colors then return res.status_code.to_s
+               if no_color then return res.status_code.to_s
                return res.color_status
        end
+end
 
-       # Display a `message` with `level`.
-       #
-       # Message will only be displayed if `level <= self.level`.
-       # Colors will be used depending on `colors`.
-       #
-       # Use `0` for no coloration.
-       private fun display(level: Int, message: String) do
-               if level > self.level then return
-               if no_colors then
-                       print message
-                       return
+class PopFormatter
+       super Formatter
+
+       # Do not decorate messages with colors
+       var no_color = false is optional, writable
+
+       redef fun format(level, message) do
+               var string = message.write_to_string
+
+               if level == fatal_level then
+                       string = "[FATAL] {string}"
+               else if level == error_level then
+                       string = "[ERROR] {string}"
+               else if level == warn_level then
+                       string = "[WARN] {string}"
+               else if level == info_level then
+                       string = "[INFO] {string}"
+               else if level == debug_level then
+                       string = "[DEBUG] {string}"
                end
-               if level == 0 then print message
-               if level == 1 then print message.red
-               if level == 2 then print message.yellow
-               if level == 3 then print message.blue
-               if level == 4 then print message.gray
-       end
-
-       # Display a message wathever the `level`
-       fun log(message: String) do display(0, message)
 
-       # Display a red error `message`.
-       fun error(message: String) do display(1, "[ERROR] {message}")
-
-       # Display a yellow warning `message`.
-       fun warning(message: String) do display(2, "[WARN] {message}")
-
-       # Display a blue info `message`.
-       fun info(message: String) do display(3, "[INFO] {message}")
+               if no_color then return string
+
+               if level == fatal_level then
+                       return string.red
+               else if level == error_level then
+                       return string.red
+               else if level == warn_level then
+                       return string.yellow
+               else if level == info_level then
+                       return string.blue
+               else if level == debug_level then
+                       return string.gray
+               end
 
-       # Display a gray debug `message`.
-       fun debug(message: String) do display(4, "[DEBUG] {message}")
+               return string
+       end
 end
 
+
 redef class HttpRequest
        # Time that request was received by the Popcorn app.
        var clock: nullable Clock = null
index 9ecf5b7..f43b100 100644 (file)
@@ -46,7 +46,6 @@ module pop_tracker
 
 import popcorn
 import popcorn::pop_config
-import popcorn::pop_logging
 import popcorn::pop_json
 import popcorn::pop_repos
 
@@ -91,7 +90,6 @@ end
 
 # Saves logs into a MongoDB collection
 class PopTracker
-       super ConsoleLog
        super TrackerHandler
 
        redef fun all(req, res) do
index e4ba62b..69dbc45 100644 (file)
@@ -13,6 +13,7 @@ RUN dpkg --add-architecture i386 \
                graphviz \
                libunwind-dev \
                pkg-config \
+               libicu-dev \
                # Get the code!
                git \
                ca-certificates \
index df312c4..d7c3457 100644 (file)
@@ -21,7 +21,7 @@ abstract class CmdIni
        super CmdEntity
 
        # Ini file
-       var ini: nullable ConfigTree = null
+       var ini: nullable IniFile = null
 
        redef fun init_command do
                var res = super
index 8bdfcb2..169d52e 100644 (file)
@@ -477,7 +477,7 @@ redef class ModelBuilder
                        # Attach homonymous `ini` file to the package
                        var inipath = path.dirname / "{pn}.ini"
                        if inipath.file_exists then
-                               var ini = new ConfigTree(inipath)
+                               var ini = new IniFile.from_file(inipath)
                                mpackage.ini = ini
                        end
                end
@@ -543,7 +543,7 @@ redef class ModelBuilder
                var parent = null
                var inipath = dirpath / "package.ini"
                if inipath.file_exists then
-                       ini = new ConfigTree(inipath)
+                       ini = new IniFile.from_file(inipath)
                end
 
                if ini == null then
@@ -1178,7 +1178,7 @@ redef class MPackage
        # The `ini` file is given as is and might contain invalid or missing information.
        #
        # Some packages, like stand-alone packages or virtual packages have no `ini` file associated.
-       var ini: nullable ConfigTree = null
+       var ini: nullable IniFile = null
 
        # Array of relative source paths excluded according to the `source.exclude` key of the `ini`
        var excludes: nullable Array[String] is lazy do
index a81ded9..7f4c5f2 100644 (file)
@@ -253,14 +253,14 @@ redef class MPackage
                var ini_path = ini_path
                if ini_path == null then return
 
-               var ini = new ConfigTree(ini_path)
+               var ini = new IniFile.from_file(ini_path)
 
-               ini.check_key(toolcontext, self, "package.name", name)
-               ini.check_key(toolcontext, self, "package.desc")
-               ini.check_key(toolcontext, self, "package.tags")
+               ini.check_key(ini_path, toolcontext, self, "package.name", name)
+               ini.check_key(ini_path, toolcontext, self, "package.desc")
+               ini.check_key(ini_path, toolcontext, self, "package.tags")
 
                # FIXME since `git reflog --follow` seems bugged
-               ini.check_key(toolcontext, self, "package.maintainer")
+               ini.check_key(ini_path, toolcontext, self, "package.maintainer")
                # var maint = mpackage.maintainer
                # if maint != null then
                        # ini.check_key(toolcontext, self, "package.maintainer", maint)
@@ -272,24 +272,24 @@ redef class MPackage
                        # ini.check_key(toolcontext, self, "package.more_contributors", contribs.join(", "))
                # end
 
-               ini.check_key(toolcontext, self, "package.license", license)
-               ini.check_key(toolcontext, self, "upstream.browse", browse_url)
-               ini.check_key(toolcontext, self, "upstream.git", git_url)
-               ini.check_key(toolcontext, self, "upstream.git.directory", git_dir)
-               ini.check_key(toolcontext, self, "upstream.homepage", homepage_url)
-               ini.check_key(toolcontext, self, "upstream.issues", issues_url)
+               ini.check_key(ini_path, toolcontext, self, "package.license", license)
+               ini.check_key(ini_path, toolcontext, self, "upstream.browse", browse_url)
+               ini.check_key(ini_path, toolcontext, self, "upstream.git", git_url)
+               ini.check_key(ini_path, toolcontext, self, "upstream.git.directory", git_dir)
+               ini.check_key(ini_path, toolcontext, self, "upstream.homepage", homepage_url)
+               ini.check_key(ini_path, toolcontext, self, "upstream.issues", issues_url)
 
-               for key in ini.to_map.keys do
+               for key in ini.flatten.keys do
                        if not allowed_ini_keys.has(key) then
                                toolcontext.warning(location, "unknown-ini-key",
-                                       "Warning: ignoring unknown `{key}` key in `{ini.ini_file}`")
+                                       "Warning: ignoring unknown `{key}` key in `{ini_path}`")
                        end
                end
        end
 
        private fun gen_ini: String do
                var ini_path = self.ini_path.as(not null)
-               var ini = new ConfigTree(ini_path)
+               var ini = new IniFile.from_file(ini_path)
 
                ini.update_value("package.name", name)
                ini.update_value("package.desc", "")
@@ -304,7 +304,7 @@ redef class MPackage
                ini.update_value("upstream.homepage", homepage_url)
                ini.update_value("upstream.issues", issues_url)
 
-               ini.save
+               ini.write_to_file(ini_path)
                return ini_path
        end
 
@@ -528,8 +528,8 @@ redef class MModule
        end
 end
 
-redef class ConfigTree
-       private fun check_key(toolcontext: ToolContext, mpackage: MPackage, key: String, value: nullable String) do
+redef class IniFile
+       private fun check_key(ini_file: String, toolcontext: ToolContext, mpackage: MPackage, key: String, value: nullable String) do
                if not has_key(key) then
                        toolcontext.warning(mpackage.location, "missing-ini-key",
                                "Warning: missing `{key}` key in `{ini_file}`")
index 1e9de9c..0c6ed5f 100644 (file)
@@ -78,7 +78,7 @@ class CommandInstall
                                exit 1
                        end
 
-                       var ini = new ConfigTree(ini_path)
+                       var ini = new IniFile.from_file(ini_path)
                        var import_line = ini["package.import"]
                        if import_line == null then
                                print_error "The local `package.ini` declares no external dependencies."
@@ -135,7 +135,7 @@ class CommandInstall
                                print ini_path.to_path.read_all
                        end
 
-                       var ini = new ConfigTree(ini_path)
+                       var ini = new IniFile.from_file(ini_path)
                        var git_repo = ini["upstream.git"]
                        if git_repo == null then
                                print_error "Package description invalid, or it does not declare a Git repository"
@@ -195,7 +195,7 @@ class CommandInstall
                end
 
                # Recursive install
-               var ini = new ConfigTree(target_dir/"package.ini")
+               var ini = new IniFile.from_file(target_dir/"package.ini")
                var import_line = ini["package.import"]
                if import_line != null then
                        install_packages import_line
@@ -320,7 +320,7 @@ class CommandList
                for file in files do
                        var ini_path = nitpm_lib_dir / file / "package.ini"
                        if verbose then print "- Reading ini file at {ini_path}"
-                       var ini = new ConfigTree(ini_path)
+                       var ini = new IniFile.from_file(ini_path)
                        var tags = ini["package.tags"]
 
                        name_to_desc[file] = tags
index fe8fb7b..125c514 100644 (file)
@@ -84,19 +84,23 @@ for a in args do
        end
        # Try to load the file as a markdown document
        var mdoc = modelbuilder.load_markdown(a)
-       page.add modelbuilder.test_mdoc(mdoc)
+       var ts = modelbuilder.test_mdoc(mdoc)
+       if not ts.children.is_empty then page.add ts
 end
 
 for a in module_files do
        var g = modelbuilder.identify_group(a)
        if g == null then continue
-       page.add modelbuilder.test_group(g)
+       var ts = modelbuilder.test_group(g)
+       if not ts.children.is_empty then page.add ts
 end
 
 for m in mmodules do
-       page.add modelbuilder.test_markdown(m)
-       var ts = modelbuilder.test_unit(m)
-       if ts != null then page.add ts
+       var ts
+       ts = modelbuilder.test_markdown(m)
+       if not ts.children.is_empty then page.add ts
+       ts = modelbuilder.test_unit(m)
+       if ts != null and not ts.children.is_empty then page.add ts
 end
 
 var file = toolcontext.opt_output.value
index 9e9d54f..d3c8c63 100644 (file)
@@ -99,7 +99,7 @@ private class NitwebPhase
                app.use("/oauth", new GithubOAuthCallBack(config.github_client_id, config.github_client_secret))
                app.use("/logout", new GithubLogout)
                app.use("/*", new StaticHandler(toolcontext.share_dir / "nitweb", "index.html"))
-               app.use_after("/*", new ConsoleLog)
+               app.use_after("/*", new PopLogger(info_level))
 
                app.listen(config.app_host, config.app_port)
        end
index f1851ee..7cec735 100644 (file)
@@ -35,5 +35,5 @@ Test suites: Classes: 1; Test Cases: 3; Failures: 1
 </system-out></testcase><testcase classname="nitunit.test_nitunit.X" name="foo1" time="0.0"><failure message="Syntax Error: unexpected operator &#39;!&#39;."></failure><system-out>assert !@#$%^&amp;*()
 </system-out></testcase><testcase classname="nitunit.test_nitunit.X" name="foo2" time="0.0"><system-err></system-err><system-out>var x = new X
 assert x.foo2
-</system-out></testcase></testsuite><testsuite package="test_test_nitunit::test_test_nitunit"></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit.TestX" name="test_foo" time="0.0"><system-err></system-err></testcase><testcase classname="nitunit.test_test_nitunit.TestX" name="test_foo1" time="0.0"><error message="Runtime Error in file nitunit.out&#47;gen_test_test_nitunit.nit">Runtime error: Assert failed (test_test_nitunit.nit:38)
+</system-out></testcase></testsuite><testsuite package="test_test_nitunit"><testcase classname="nitunit.test_test_nitunit.TestX" name="test_foo" time="0.0"><system-err></system-err></testcase><testcase classname="nitunit.test_test_nitunit.TestX" name="test_foo1" time="0.0"><error message="Runtime Error in file nitunit.out&#47;gen_test_test_nitunit.nit">Runtime error: Assert failed (test_test_nitunit.nit:38)
 </error></testcase><testcase classname="nitunit.test_test_nitunit.TestX" name="test_foo2" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
index 5fdb314..cbfc0cd 100644 (file)
@@ -5,4 +5,4 @@
 Docunits: Entities: 4; Documented ones: 0; With nitunits: 0
 Test suites: Classes: 1; Test Cases: 2; Failures: 0
 [SUCCESS] All 2 tests passed.
-<testsuites><testsuite package="test_nitunit5::test_nitunit5"></testsuite><testsuite package="test_nitunit5"><testcase classname="nitunit.test_nitunit5.TestNitunit5" name="test_path_is_set" time="0.0"><system-err></system-err></testcase><testcase classname="nitunit.test_nitunit5.TestNitunit5" name="test_path_is_suite_path" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_nitunit5"><testcase classname="nitunit.test_nitunit5.TestNitunit5" name="test_path_is_set" time="0.0"><system-err></system-err></testcase><testcase classname="nitunit.test_nitunit5.TestNitunit5" name="test_path_is_suite_path" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
index 0d1829d..a640323 100644 (file)
@@ -13,4 +13,4 @@ Docunits: Entities: 5; Documented ones: 0; With nitunits: 0
 Test suites: Classes: 1; Test Cases: 3; Failures: 3
 [FAILURE] 3/3 tests failed.
 `nitunit.out` is not removed for investigation.
-<testsuites><testsuite package="test_nitunit6::test_nitunit6"></testsuite><testsuite package="test_nitunit6"><testcase classname="nitunit.test_nitunit6.TestNitunit6" name="test_foo" time="0.0"><failure message="Nitunit Error: before module test failed"></failure></testcase></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_nitunit6"><testcase classname="nitunit.test_nitunit6.TestNitunit6" name="test_foo" time="0.0"><failure message="Nitunit Error: before module test failed"></failure></testcase></testsuite></testsuites>
\ No newline at end of file
index 9015b4a..4932785 100644 (file)
@@ -11,4 +11,4 @@ Docunits: Entities: 5; Documented ones: 0; With nitunits: 0
 Test suites: Classes: 1; Test Cases: 3; Failures: 1
 [FAILURE] 1/3 tests failed.
 `nitunit.out` is not removed for investigation.
-<testsuites><testsuite package="test_nitunit7::test_nitunit7"></testsuite><testsuite package="test_nitunit7"><testcase classname="nitunit.test_nitunit7.TestNitunit7" name="test_foo" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_nitunit7"><testcase classname="nitunit.test_nitunit7.TestNitunit7" name="test_foo" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
index 8c8d3f9..84dbc07 100644 (file)
@@ -11,4 +11,4 @@ Docunits: Entities: 3; Documented ones: 0; With nitunits: 0
 Test suites: Classes: 1; Test Cases: 3; Failures: 1
 [FAILURE] 1/3 tests failed.
 `nitunit.out` is not removed for investigation.
-<testsuites><testsuite package="test_nitunit8::test_nitunit8"></testsuite><testsuite package="test_nitunit8"><testcase classname="nitunit.test_nitunit8.TestNitunit8" name="test_foo" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_nitunit8"><testcase classname="nitunit.test_nitunit8.TestNitunit8" name="test_foo" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
index c0f1e23..3cb2d30 100644 (file)
@@ -15,4 +15,4 @@ Docunits: Entities: 7; Documented ones: 0; With nitunits: 0
 Test suites: Classes: 1; Test Cases: 7; Failures: 1
 [FAILURE] 1/7 tests failed.
 `nitunit.out` is not removed for investigation.
-<testsuites><testsuite package="test_nitunit11::test_nitunit11"></testsuite><testsuite package="test_nitunit11"><testcase classname="nitunit.test_nitunit11.TestNitunit11" name="test_baz" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
+<testsuites><testsuite package="test_nitunit11"><testcase classname="nitunit.test_nitunit11.TestNitunit11" name="test_baz" time="0.0"><system-err></system-err></testcase></testsuite></testsuites>
\ No newline at end of file
index 404c270..7682e35 100644 (file)
@@ -59,11 +59,11 @@ Docunits: Entities: 22; Documented ones: 0; With nitunits: 0
 Test suites: Classes: 3; Test Cases: 8; Failures: 7
 [FAILURE] 7/8 tests failed.
 `nitunit.out` is not removed for investigation.
-<testsuites><testsuite package="test_nitunit4&gt;"></testsuite><testsuite package="test_nitunit4::nitunit4"></testsuite><testsuite package="test_nitunit4::test_bad_comp"></testsuite><testsuite package="test_bad_comp"><testcase classname="nitunit.test_nitunit4.TestSuiteBadComp" name="test_good" time="0.0"><failure message="Compilation Error">test_nitunit4&#47;test_bad_comp.nit:25,10--19: Error: method or variable `bad_method` unknown in `TestSuiteBadComp`.
+<testsuites><testsuite package="test_bad_comp"><testcase classname="nitunit.test_nitunit4.TestSuiteBadComp" name="test_good" time="0.0"><failure message="Compilation Error">test_nitunit4&#47;test_bad_comp.nit:25,10--19: Error: method or variable `bad_method` unknown in `TestSuiteBadComp`.
 </failure></testcase><testcase classname="nitunit.test_nitunit4.TestSuiteBadComp" name="test_bad" time="0.0"><failure message="Compilation Error">test_nitunit4&#47;test_bad_comp.nit:25,10--19: Error: method or variable `bad_method` unknown in `TestSuiteBadComp`.
-</failure></testcase></testsuite><testsuite package="test_nitunit4::test_bad_comp2"></testsuite><testsuite package="test_bad_comp2"><testcase classname="nitunit.test_nitunit4.TestSuiteBadComp" name="test_good" time="0.0"><failure message="Compilation Error">nitunit.out&#47;gen_test_bad_comp2.nit:11,10--17: Error: expected 1 argument(s) for `test_bad(param: Bool)`; got 0. See introduction at `test_nitunit4::TestSuiteBadComp::test_bad`.
+</failure></testcase></testsuite><testsuite package="test_bad_comp2"><testcase classname="nitunit.test_nitunit4.TestSuiteBadComp" name="test_good" time="0.0"><failure message="Compilation Error">nitunit.out&#47;gen_test_bad_comp2.nit:11,10--17: Error: expected 1 argument(s) for `test_bad(param: Bool)`; got 0. See introduction at `test_nitunit4::TestSuiteBadComp::test_bad`.
 </failure></testcase><testcase classname="nitunit.test_nitunit4.TestSuiteBadComp" name="test_bad" time="0.0"><failure message="Compilation Error">nitunit.out&#47;gen_test_bad_comp2.nit:11,10--17: Error: expected 1 argument(s) for `test_bad(param: Bool)`; got 0. See introduction at `test_nitunit4::TestSuiteBadComp::test_bad`.
-</failure></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4"></testsuite><testsuite package="test_nitunit4"><testcase classname="nitunit.test_nitunit4.TestTestSuite" name="test_foo" time="0.0"><error message="Runtime Error in file nitunit.out&#47;gen_test_nitunit4.nit">Before Test
+</failure></testcase></testsuite><testsuite package="test_nitunit4"><testcase classname="nitunit.test_nitunit4.TestTestSuite" name="test_foo" time="0.0"><error message="Runtime Error in file nitunit.out&#47;gen_test_nitunit4.nit">Before Test
 Tested method
 After Test
 Runtime assert: &lt;TestTestSuite&gt;.before
@@ -82,4 +82,4 @@ After Test
 </error></testcase><testcase classname="nitunit.test_nitunit4.TestTestSuite" name="test_sav_conflict" time="0.0"><error message="Conflicting expected output: test_nitunit4&#47;test_nitunit4.sav&#47;test_sav_conflict.res, test_nitunit4&#47;sav&#47;test_sav_conflict.res and test_nitunit4&#47;test_sav_conflict.res all exist">Before Test
 Tested method
 After Test
-</error></testcase></testsuite><testsuite package="test_nitunit4::test_nitunit4_base"></testsuite></testsuites>
\ No newline at end of file
+</error></testcase></testsuite></testsuites>
\ No newline at end of file
index 721171d..c19a9a8 100755 (executable)
@@ -846,6 +846,10 @@ fi
 
 echo >>$xml "</testsuite></testsuites>"
 
+if type junit2html >/dev/null; then
+       junit2html "$xml"
+fi
+
 if [ -n "$nok" ]; then
        exit 1
 else