Skip to content

Commit 229aaaf

Browse files
authored
Merge pull request #35 from bcdev/forman-improve_coverage
Further improve coverage
2 parents f2a9d5a + f955834 commit 229aaaf

19 files changed

+272
-69
lines changed

docs/todo.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@
1414
## Desired
1515

1616
- project logo
17-
- add some more tests so we reach 99% coverage
1817
- apply rule op args/kwargs validation schema
1918
- provide core rule that checks for configurable list of std attributes
2019
- measure time it takes to open a dataset and pass time into rule context
2120
so we can write a configurable rule that checks the opening time
22-
- allow outputting suggestions, if any, that emitted by some rules
21+
- allow outputting suggestions, if any, that are emitted by some rules
22+
- enhance styling of `Result` representation in Jupyter notebooks
23+
(check if we can expand/collapse messages with suggestions)
2324

2425
## Nice to have
2526

27+
- add some more tests so we reach 100% coverage
2628
- support `autofix` feature
2729
- support `md` (markdown) output format
2830
- support formatter op args/kwargs and apply validation schema

tests/formatters/test_simple.py

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77

88

99
class SimpleTest(TestCase):
10-
results = [
10+
errors_and_warnings = [
1111
Result.new(
1212
Config(),
13-
file_path="test.nc",
13+
file_path="test1.nc",
1414
messages=[
1515
Message(message="what", rule_id="rule-1", severity=2),
1616
Message(message="is", fatal=True),
@@ -19,29 +19,65 @@ class SimpleTest(TestCase):
1919
)
2020
]
2121

22+
warnings_only = [
23+
Result.new(
24+
Config(),
25+
file_path="test2.nc",
26+
messages=[
27+
Message(message="what", rule_id="rule-1", severity=1),
28+
Message(message="happened?", rule_id="rule-2", severity=1),
29+
],
30+
)
31+
]
32+
2233
def test_no_color(self):
2334
formatter = Simple(styled=False)
2435
text = formatter.format(
2536
context=get_context(),
26-
results=self.results,
37+
results=self.errors_and_warnings,
38+
)
39+
self.assert_output_1_ok(text)
40+
self.assertNotIn("\033]", text)
41+
42+
formatter = Simple(styled=False)
43+
text = formatter.format(
44+
context=get_context(),
45+
results=self.warnings_only,
2746
)
28-
self.assert_output_ok(text)
47+
self.assert_output_2_ok(text)
2948
self.assertNotIn("\033]", text)
3049

3150
def test_color(self):
3251
formatter = Simple(styled=True)
3352
text = formatter.format(
3453
context=get_context(),
35-
results=self.results,
54+
results=self.errors_and_warnings,
55+
)
56+
self.assert_output_1_ok(text)
57+
self.assertIn("\033]", text)
58+
59+
formatter = Simple(styled=True)
60+
text = formatter.format(
61+
context=get_context(),
62+
results=self.warnings_only,
3663
)
37-
self.assert_output_ok(text)
64+
self.assert_output_2_ok(text)
3865
self.assertIn("\033]", text)
3966

40-
def assert_output_ok(self, text):
67+
def assert_output_1_ok(self, text):
4168
self.assertIsInstance(text, str)
42-
self.assertIn("test.nc", text)
69+
self.assertIn("test1.nc", text)
4370
self.assertIn("happening?", text)
4471
self.assertIn("error", text)
4572
self.assertIn("warn", text)
4673
self.assertIn("rule-1", text)
4774
self.assertIn("rule-2", text)
75+
76+
def assert_output_2_ok(self, text):
77+
self.assertIsInstance(text, str)
78+
self.assertIn("test2.nc", text)
79+
self.assertIn("happened?", text)
80+
self.assertNotIn("error", text)
81+
self.assertIn("warn", text)
82+
self.assertIn("rule-1", text)
83+
self.assertIn("rule-2", text)

tests/test_config.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1+
from typing import Any
12
from unittest import TestCase
23

34
import pytest
5+
import xarray as xr
46

5-
from xrlint.config import Config, ConfigList
6-
from xrlint.rule import RuleConfig
7+
from xrlint.config import Config, ConfigList, get_core_config
8+
from xrlint.constants import CORE_PLUGIN_NAME
9+
from xrlint.plugin import Plugin, new_plugin
10+
from xrlint.processor import define_processor, ProcessorOp
11+
from xrlint.result import Message
12+
from xrlint.rule import RuleConfig, Rule
713
from xrlint.util.filefilter import FileFilter
814

915

1016
# noinspection PyMethodMayBeStatic
1117
class ConfigTest(TestCase):
18+
def test_class_props(self):
19+
self.assertEqual("config", Config.value_name())
20+
self.assertEqual("Config | dict | None", Config.value_type_name())
21+
1222
def test_defaults(self):
1323
config = Config()
1424
self.assertEqual(None, config.name)
@@ -20,6 +30,50 @@ def test_defaults(self):
2030
self.assertEqual(None, config.plugins)
2131
self.assertEqual(None, config.rules)
2232

33+
def test_get_plugin(self):
34+
config = get_core_config()
35+
plugin = config.get_plugin(CORE_PLUGIN_NAME)
36+
self.assertIsInstance(plugin, Plugin)
37+
38+
with pytest.raises(ValueError, match="unknown plugin 'xcube'"):
39+
config.get_plugin("xcube")
40+
41+
def test_get_rule(self):
42+
config = get_core_config()
43+
rule = config.get_rule("flags")
44+
self.assertIsInstance(rule, Rule)
45+
46+
with pytest.raises(ValueError, match="unknown rule 'foo'"):
47+
config.get_rule("foo")
48+
49+
def test_get_processor_op(self):
50+
class MyProc(ProcessorOp):
51+
def preprocess(
52+
self, file_path: str, opener_options: dict[str, Any]
53+
) -> list[tuple[xr.Dataset, str]]:
54+
pass
55+
56+
def postprocess(
57+
self, messages: list[list[Message]], file_path: str
58+
) -> list[Message]:
59+
pass
60+
61+
processor = define_processor("myproc", op_class=MyProc)
62+
config = Config(
63+
plugins=dict(
64+
myplugin=new_plugin("myplugin", processors=dict(myproc=processor))
65+
)
66+
)
67+
68+
processor_op = config.get_processor_op(MyProc())
69+
self.assertIsInstance(processor_op, MyProc)
70+
71+
processor_op = config.get_processor_op("myplugin/myproc")
72+
self.assertIsInstance(processor_op, MyProc)
73+
74+
with pytest.raises(ValueError, match="unknown processor 'myplugin/myproc2'"):
75+
config.get_processor_op("myplugin/myproc2")
76+
2377
def test_from_value_ok(self):
2478
self.assertEqual(Config(), Config.from_value(None))
2579
self.assertEqual(Config(), Config.from_value({}))
@@ -167,7 +221,7 @@ def test_compute_config(self):
167221

168222
config_list = ConfigList(
169223
[
170-
Config(settings={"a": 1, "b": 1}),
224+
Config(ignores=["**/*.yaml"], settings={"a": 1, "b": 1}),
171225
Config(files=["**/datacubes/*.zarr"], settings={"b": 2}),
172226
Config(files=["**/*.txt"], settings={"a": 2}),
173227
]
@@ -185,6 +239,12 @@ def test_compute_config(self):
185239
config_list.compute_config(file_path),
186240
)
187241

242+
file_path = "s3://wq-services/datacubes/config.yaml"
243+
self.assertEqual(
244+
None,
245+
config_list.compute_config(file_path),
246+
)
247+
188248
def test_split_global_filter(self):
189249
config_list = ConfigList(
190250
[

tests/test_operation.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def op_base_class(cls) -> Type[ThingOp]:
3232
return ThingOp
3333

3434
@classmethod
35-
def op_name(cls) -> str:
35+
def value_name(cls) -> str:
3636
return "thing"
3737

3838
@classmethod
@@ -62,10 +62,9 @@ class MyThingOp3(ThingOp):
6262

6363

6464
class OperationTest(TestCase):
65-
def test_defaults(self):
65+
def test_class_props(self):
6666
self.assertEqual(OperationMeta, Operation.meta_class())
6767
self.assertEqual(type, Operation.op_base_class())
68-
self.assertEqual("operation", Operation.op_name())
6968
self.assertEqual("export_operation", Operation.op_import_attr_name())
7069
self.assertEqual("operation", Operation.value_name())
7170
self.assertEqual(

tests/test_plugin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111

1212
class PluginTest(TestCase):
13+
def test_class_props(self):
14+
self.assertEqual("plugin", Plugin.value_name())
15+
self.assertEqual("Plugin | dict | str", Plugin.value_type_name())
16+
1317
def test_new_plugin(self):
1418
plugin = new_plugin(name="hello", version="2.4.5")
1519
self.assertEqual(Plugin(meta=PluginMeta(name="hello", version="2.4.5")), plugin)
@@ -53,6 +57,10 @@ class MyRule2(RuleOp):
5357

5458

5559
class PluginMetaTest(TestCase):
60+
def test_class_props(self):
61+
self.assertEqual("plugin_meta", PluginMeta.value_name())
62+
self.assertEqual("PluginMeta | dict", PluginMeta.value_type_name())
63+
5664
def test_from_value(self):
5765
self.assertEqual(
5866
PluginMeta(name="p", ref="a.b.c:p"),

tests/test_processor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,19 @@
99
from xrlint.result import Message
1010

1111

12+
class ProcessorMetaTest(TestCase):
13+
def test_class_props(self):
14+
self.assertEqual("processor_meta", ProcessorMeta.value_name())
15+
self.assertEqual("ProcessorMeta | dict", ProcessorMeta.value_type_name())
16+
17+
1218
class ProcessorTest(TestCase):
19+
def test_class_props(self):
20+
self.assertEqual("processor", Processor.value_name())
21+
self.assertEqual(
22+
"Processor | Type[ProcessorOp] | dict | str", Processor.value_type_name()
23+
)
24+
1325
def test_define_processor(self):
1426
registry = {}
1527

tests/test_rule.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ def export_rule():
1919

2020

2121
class RuleTest(TestCase):
22+
def test_class_props(self):
23+
self.assertIs(RuleMeta, Rule.meta_class())
24+
self.assertIs(RuleOp, Rule.op_base_class())
25+
self.assertEqual("rule", Rule.value_name())
26+
self.assertEqual("Rule | Type[RuleOp] | dict | str", Rule.value_type_name())
27+
2228
def test_from_value_ok_rule(self):
2329
rule = export_rule()
2430
rule2 = Rule.from_value(rule)
@@ -72,6 +78,10 @@ class MyRule3(RuleOp):
7278

7379

7480
class RuleMetaTest(unittest.TestCase):
81+
def test_class_props(self):
82+
self.assertEqual("rule_meta", RuleMeta.value_name())
83+
self.assertEqual("RuleMeta | dict", RuleMeta.value_type_name())
84+
7585
def test_from_value(self):
7686
rule_meta = RuleMeta.from_value(
7787
{
@@ -133,6 +143,10 @@ def test_fail(self):
133143

134144

135145
class RuleConfigTest(TestCase):
146+
def test_class_props(self):
147+
self.assertEqual("rule_config", RuleConfig.value_name())
148+
self.assertEqual("int | str | list", RuleConfig.value_type_name())
149+
136150
def test_defaults(self):
137151
rule_config = RuleConfig(1)
138152
self.assertEqual(1, rule_config.severity)
@@ -147,6 +161,10 @@ def test_from_value_ok(self):
147161
self.assertEqual(RuleConfig(1), RuleConfig.from_value("warn"))
148162
self.assertEqual(RuleConfig(2), RuleConfig.from_value("error"))
149163
self.assertEqual(RuleConfig(2), RuleConfig.from_value(["error"]))
164+
# YAML "on"/"off" literals
165+
self.assertEqual(RuleConfig(0), RuleConfig.from_value(False))
166+
self.assertEqual(RuleConfig(1), RuleConfig.from_value(True))
167+
150168
self.assertEqual(
151169
RuleConfig(1, ("never",)), RuleConfig.from_value(["warn", "never"])
152170
)
@@ -175,17 +193,19 @@ def test_from_value_ok(self):
175193
)
176194

177195
# noinspection PyMethodMayBeStatic
178-
def test_from_value_fails(self):
196+
def test_from_value_fail(self):
179197
with pytest.raises(
180198
TypeError,
181-
match="rule configuration must be of type int|str|tuple|list, but got None",
199+
match=r"rule_config must be of type int \| str \| list, but got None",
182200
):
183201
RuleConfig.from_value(None)
202+
184203
with pytest.raises(
185204
ValueError,
186205
match="severity must be one of 'error', 'warn', 'off', 2, 1, 0, but got 4",
187206
):
188207
RuleConfig.from_value(4)
208+
189209
with pytest.raises(
190210
ValueError,
191211
match=(
@@ -194,3 +214,9 @@ def test_from_value_fails(self):
194214
),
195215
):
196216
RuleConfig.from_value("debug")
217+
218+
with pytest.raises(
219+
ValueError,
220+
match="rule_config must not be empty",
221+
):
222+
RuleConfig.from_value([])

tests/util/test_filefilter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,26 @@ def test_accept(self):
6262
self.assertEqual(True, file_filter.accept("test-1.zarr"))
6363
self.assertEqual(True, file_filter.accept("test-2.zarr"))
6464

65+
# negated ignores:
6566
file_filter = FileFilter.from_patterns(
6667
["**/*.zarr"], ["**/temp/*", "!**/temp/x.zarr"]
6768
)
6869
self.assertEqual(True, file_filter.accept("./test.zarr"))
6970
self.assertEqual(True, file_filter.accept("./temp/x.zarr"))
7071
self.assertEqual(False, file_filter.accept("./temp/y.zarr"))
72+
73+
# negated ignores:
74+
file_filter = FileFilter.from_patterns([], ["**/temp/*", "!**/temp/*.zarr"])
75+
self.assertEqual(True, file_filter.accept("./ds.nc"))
76+
self.assertEqual(True, file_filter.accept("./temp/ds.zarr"))
77+
self.assertEqual(True, file_filter.accept("./temp/test/ds.zarr"))
78+
self.assertEqual(False, file_filter.accept("./temp/ds.nc"))
79+
self.assertEqual(False, file_filter.accept("./temp/README.md"))
80+
81+
# just ignores:
82+
file_filter = FileFilter.from_patterns([], ["**/*.json", "**/*.yaml"])
83+
self.assertEqual(True, file_filter.accept("./test.nc"))
84+
self.assertEqual(True, file_filter.accept("./temp/x.zarr"))
85+
self.assertEqual(False, file_filter.accept("c.yaml"))
86+
self.assertEqual(False, file_filter.accept("test/config.json"))
87+
self.assertEqual(False, file_filter.accept("test/config.yaml"))

0 commit comments

Comments
 (0)