This PR adds a mock to GithubAPI so we can avoid sending requests to the API on CI.
For each API call we save the actual Github response body from the API and reuse it during the tests.
The attribute `update_responses_cache` can be set to `true` so the cache files are updated from the API when `nitunit` is called.
Pull-Request: #2753
Reviewed-by: Jean Privat <jean@pryen.org>
artifacts:
paths:
- tests/errlist
- - tests/*.xml
+ - tests/*.xml*
when: always
reports:
junit: tests/*.xml
- 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*
- 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*
- 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*
- 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:
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:
#
# 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
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
# 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
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")
redef fun parse_options(args) do
super
- ini = new ConfigTree(config_file)
+ ini = new IniFile.from_file(config_file)
end
# Default config file path
# 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
# 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
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
--- /dev/null
+# `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."
+~~~
# 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
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
--- /dev/null
+# 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
--- /dev/null
+[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
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
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).
module pop_logging
import pop_handlers
-import console
+import logger
import realtime
# Initialize a clock for the resquest.
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
import popcorn
import popcorn::pop_config
-import popcorn::pop_logging
import popcorn::pop_json
import popcorn::pop_repos
# Saves logs into a MongoDB collection
class PopTracker
- super ConsoleLog
super TrackerHandler
redef fun all(req, res) do
graphviz \
libunwind-dev \
pkg-config \
+ libicu-dev \
# Get the code!
git \
ca-certificates \
super CmdEntity
# Ini file
- var ini: nullable ConfigTree = null
+ var ini: nullable IniFile = null
redef fun init_command do
var res = super
# 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
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
# 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
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)
# 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", "")
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
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}`")
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."
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"
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
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
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
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
</system-out></testcase><testcase classname="nitunit.test_nitunit.X" name="foo1" time="0.0"><failure message="Syntax Error: unexpected operator '!'."></failure><system-out>assert !@#$%^&*()
</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/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/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
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
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
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
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
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
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>"></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/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/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/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/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/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/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/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/gen_test_nitunit4.nit">Before Test
Tested method
After Test
Runtime assert: <TestTestSuite>.before
</error></testcase><testcase classname="nitunit.test_nitunit4.TestTestSuite" name="test_sav_conflict" time="0.0"><error message="Conflicting expected output: test_nitunit4/test_nitunit4.sav/test_sav_conflict.res, test_nitunit4/sav/test_sav_conflict.res and test_nitunit4/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
echo >>$xml "</testsuite></testsuites>"
+if type junit2html >/dev/null; then
+ junit2html "$xml"
+fi
+
if [ -n "$nok" ]; then
exit 1
else