Repository: 42School/norminette (language: python)

Issue:
feat: add --format flag and *ErrorsFormatter classes
I'm adding a rich way of handling errors to format it as "short" (currently), "verbose" (with highlights and hints, like GCC does on errors/warnings), or others like JSON (#448).

Example of errors in JSON format:
```bash
norminette test.c --format=json
```
```json
{
  "files": [
    {
      "path": "/home/nium/OpenSource/norminette/test.c",
      "status": "Error",
      "errors": [
        {
          "name": "INVALID_HEADER",
          "text": "Missing or invalid 42 header",
          "level": "Error",
          "highlights": [{"lineno": 1, "column": 1, "length": null, "hint": null}]
        },
        {
          "name": "SPACE_AFTER_KW",
          "text": "Missing space after keyword",
          "level": "Error",
          "highlights": [{"lineno": 3, "column": 5, "length": null, "hint": null}]
        }
      ]
    }
  ]
}
```
Some considerations (just for now):
- `length` and `hint` is always `null`
- `highlights` always contains just one element



Discussion / hints:

Fix patch:
diff --git a/norminette/__main__.py b/norminette/__main__.py
index 2b6509e..60143a6 100644
--- a/norminette/__main__.py
+++ b/norminette/__main__.py
@@ -136,7 +136,7 @@ def main():
         except KeyboardInterrupt:
             sys.exit(1)
     errors = format(files)
-    print(errors)
+    print(errors, end='')
     sys.exit(1 if len(file.errors) else 0)
 
 
diff --git a/norminette/errors.py b/norminette/errors.py
index dbe436d..e3972c8 100644
--- a/norminette/errors.py
+++ b/norminette/errors.py
@@ -86,6 +86,7 @@ def __str__(self) -> str:
                 highlight = error.highlights[0]
                 output += f"\n{error.level}: {error.name:<20} "
                 output += f"(line: {highlight.lineno:>3}, col: {highlight.column:>3}):\t{brief}"
+            output += '\n'
         return output
 
 
@@ -101,7 +102,7 @@ def __str__(self):
         output = {
             "files": files,
         }
-        return json.dumps(output, separators=",:")
+        return json.dumps(output, separators=",:") + '\n'
 
 
 formatters = (


Test patch:
diff --git a/tests/rules/rules_generator_test.py b/tests/rules/rules_generator_test.py
index f2049ad..d8aa6c6 100644
--- a/tests/rules/rules_generator_test.py
+++ b/tests/rules/rules_generator_test.py
@@ -25,7 +25,7 @@ def test_rule_for_file(file, capsys):
     context = Context(file, lexer.get_tokens(), debug=2)
     registry.run(context)
     errors = HumanizedErrorsFormatter(file)
-    print(errors)
+    print(errors, end='')
     captured = capsys.readouterr()
 
     assert captured.out == out_content
diff --git a/tests/test_errors.py b/tests/test_errors.py
index 9f61515..ac47ae3 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -1,12 +1,68 @@
 import json
 
 import pytest
+from unittest.mock import patch
 
 from norminette.file import File
 from norminette.lexer import Lexer
 from norminette.context import Context
 from norminette.registry import Registry
 from norminette.errors import JSONErrorsFormatter
+from norminette.errors import HumanizedErrorsFormatter
+
+
+@pytest.mark.parametrize("files, expected_result, ", [it.values() for it in [
+        {
+            "files": [
+                File("/nium/a.c", "#include <stdio.h>"),
+                File("/nium/b.c", "int\tmain(void)\n{\n\treturn (1);\n}\n"),
+                File("/nium/c.c", "int\tfn(int n);\n"),
+            ],
+            "expected_result": "a.c: OK!\nb.c: OK!\nc.c: OK!\n",
+        },
+        {
+            "files": [
+                File("skyfall.c", "// Hello"),
+            ],
+            "expected_result": "skyfall.c: OK!\n",
+        },
+        {
+            "files": [
+                File("/nium/mortari.c", "#define TRUE 1"),
+                File("/nium/gensler.c", "int\tmain();\n"),
+            ],
+            "expected_result": (
+                "mortari.c: OK!\n"
+                "gensler.c: Error!\n"
+                "Error: NO_ARGS_VOID         (line:   1, col:  10):\tEmpty function argument requires void\n"
+            )
+        },
+        {
+            "files": [
+                File("/nium/john.c", "#define x"),
+                File("/nium/galt.c", "#define x"),
+            ],
+            "expected_result": (
+                "john.c: Error!\n"
+                "Error: MACRO_NAME_CAPITAL   (line:   1, col:   9):\tMacro name must be capitalized\n"
+                "galt.c: Error!\n"
+                "Error: MACRO_NAME_CAPITAL   (line:   1, col:   9):\tMacro name must be capitalized\n"
+            )
+        },
+    ]
+])
+def test_humanized_formatter_errored_file(files, expected_result):
+    registry = Registry()
+
+    with patch("norminette.rules.check_header.CheckHeader.run") as _:
+        for file in files:
+            lexer = Lexer(file)
+            context = Context(file, lexer.get_tokens())
+            registry.run(context)
+
+    formatter = HumanizedErrorsFormatter(files)
+    assert str(formatter) == expected_result
+
 
 tests = [
     {
@@ -44,4 +100,4 @@ def test_json_formatter_errored_file(file, test):
     Registry().run(context)
 
     formatter = JSONErrorsFormatter(file)
-    assert str(formatter) == json.dumps(test, separators=",:")
+    assert str(formatter) == json.dumps(test, separators=",:") + '\n'