diff --git a/raster/r.geomorphon/Makefile b/raster/r.geomorphon/Makefile index 8845a0297fb..b2130722c10 100644 --- a/raster/r.geomorphon/Makefile +++ b/raster/r.geomorphon/Makefile @@ -3,7 +3,7 @@ MODULE_TOPDIR = ../.. PGM = r.geomorphon LIBES = $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PARSONLIB) -DEPENDENCIES = $(RASTERDEP) $(GISDEP) +DEPENDENCIES = $(RASTERDEP) $(GISDEP) $(PARSONDEP) include $(MODULE_TOPDIR)/include/Make/Module.make diff --git a/raster/r.geomorphon/profile.c b/raster/r.geomorphon/profile.c index d3600bccd28..06ca94816e2 100644 --- a/raster/r.geomorphon/profile.c +++ b/raster/r.geomorphon/profile.c @@ -2,6 +2,7 @@ #include "local_proto.h" #include +#define JSON_MIN_INDENT 1 #define YAML_MIN_INDENT 0 #define XML_MIN_INDENT 1 #define MAX_STR_LEN GNAME_MAX @@ -252,90 +253,127 @@ static const char *format_token_common(const struct token *t) static unsigned write_json(FILE *f) { unsigned i; - G_JSON_Value *root_val; - G_JSON_Object *obj_stack[MAX_STACK_ELEMS]; - int depth = 0; - - root_val = G_json_value_init_object(); - if (root_val == NULL) { - G_fatal_error(_("Failed to initialize JSON object. Out of memory?")); - } - - obj_stack[0] = G_json_value_get_object(root_val); + G_JSON_Value *stack_vals[MAX_STACK_ELEMS]; + unsigned stack_top = 0; + G_JSON_Value *root = G_json_value_init_object(); + if (!root) + return 0; + stack_vals[stack_top++] = root; for (i = 0; i < size; i++) { - G_JSON_Object *cur = obj_stack[depth]; + G_JSON_Object *parent = G_json_object(stack_vals[stack_top - 1]); + if (!parent) { + G_json_value_free(root); + return 0; + } switch (token[i].type) { case T_SSO: { - G_JSON_Value *sub_val = G_json_value_init_object(); - if (sub_val == NULL) { - G_fatal_error( - _("Failed to initialize JSON object. Out of memory?")); + if (stack_top >= MAX_STACK_ELEMS) { + G_json_value_free(root); + return 0; } - G_JSON_Object *sub_obj = G_json_value_get_object(sub_val); - G_json_object_set_value(cur, token[i].key, sub_val); - - if (depth < MAX_STACK_ELEMS - 1) { - obj_stack[++depth] = sub_obj; + G_JSON_Value *obj = G_json_value_init_object(); + if (!obj) { + G_json_value_free(root); + return 0; } - else { - G_warning(_("JSON nesting exceeds maximum depth")); - G_json_value_free(root_val); + if (G_json_object_set_value(parent, token[i].key, obj) != + G_JSONSuccess) { + G_json_value_free(obj); + G_json_value_free(root); return 0; } - break; - } + stack_vals[stack_top++] = obj; + } break; case T_ESO: - if (depth > 0) { - depth--; + if (stack_top <= 1) { + /* can't pop root */ + G_json_value_free(root); + return 0; } - else if (depth == 0) { - G_json_value_free(root_val); + stack_top--; + break; + case T_BLN: + if (G_json_object_set_boolean(parent, token[i].key, + token[i].int_val ? 1 : 0) != + G_JSONSuccess) { + G_json_value_free(root); return 0; } break; - case T_INT: - G_json_object_set_number(cur, token[i].key, - (double)token[i].int_val); - break; - - case T_BLN: - G_json_object_set_boolean(cur, token[i].key, token[i].int_val); + if (G_json_object_set_number(parent, token[i].key, + (double)token[i].int_val) != + G_JSONSuccess) { + G_json_value_free(root); + return 0; + } break; - case T_DBL: + if (isnan(token[i].dbl_val)) { + if (G_json_object_set_null(parent, token[i].key) != + G_JSONSuccess) { + G_json_value_free(root); + return 0; + } + } + else { + if (G_json_object_set_number(parent, token[i].key, + token[i].dbl_val) != + G_JSONSuccess) { + G_json_value_free(root); + return 0; + } + } + break; case T_MTR: - if (isnan(token[i].dbl_val)) - G_json_object_set_null(cur, token[i].key); - else - G_json_object_set_number(cur, token[i].key, token[i].dbl_val); + if (isnan(token[i].dbl_val)) { + if (G_json_object_set_null(parent, token[i].key) != + G_JSONSuccess) { + G_json_value_free(root); + return 0; + } + } + else { + if (G_json_object_set_number(parent, token[i].key, + token[i].dbl_val) != + G_JSONSuccess) { + G_json_value_free(root); + return 0; + } + } break; - case T_STR: - G_json_object_set_string(cur, token[i].key, token[i].str_val); + if (G_json_object_set_string(parent, token[i].key, + token[i].str_val) != G_JSONSuccess) { + G_json_value_free(root); + return 0; + } break; default: - G_json_value_free(root_val); + G_json_value_free(root); return 0; } } - if (depth != 0 || overflow) { - G_json_value_free(root_val); + + if (stack_top != 1 || overflow) { + G_json_value_free(root); return 0; } - char *json_str = G_json_serialize_to_string_pretty(root_val); - if (!json_str) { - G_json_value_free(root_val); + + char *s = G_json_serialize_to_string_pretty(root); + if (!s) { + G_json_value_free(root); return 0; } - - fputs(json_str, f); - fputc('\n', f); - - G_json_free_serialized_string(json_str); - G_json_value_free(root_val); + if (fprintf(f, "%s\n", s) < 0) { + G_json_free_serialized_string(s); + G_json_value_free(root); + return 0; + } + G_json_free_serialized_string(s); + G_json_value_free(root); return 1; } diff --git a/raster/r.geomorphon/testsuite/test_r_geom.py b/raster/r.geomorphon/testsuite/test_r_geom.py index a071e51a062..c8e137b570e 100644 --- a/raster/r.geomorphon/testsuite/test_r_geom.py +++ b/raster/r.geomorphon/testsuite/test_r_geom.py @@ -14,6 +14,11 @@ from grass.gunittest.case import TestCase from grass.gunittest.main import test from grass.script.core import read_command +import json +import os +from grass.script import core as gcore +import tempfile +import pathlib from grass.tools import Tools synth_out = """1 flat @@ -299,47 +304,39 @@ def test_sint(self): self.assertEqual(first=synth_out, second=category) def test_json_profile(self): - """Test JSON profile output structure and key values""" - result = self.tools.r_geomorphon( - elevation=self.inele, - coordinates="636400,221100", - profiledata="-", - profileformat="json", - search=10, - ) - - out = result.stdout - + # ensure region is set to the test elevation + self.runModule("g.region", raster=self.inele) + region = gcore.region() + east = float(region.get("east", region["e"])) + west = float(region.get("west", region["w"])) + north = float(region.get("north", region["n"])) + south = float(region.get("south", region["s"])) + e = (east + west) / 2.0 + n = (north + south) / 2.0 + fd, tmp = tempfile.mkstemp(prefix="rgeom_profile_", suffix=".json") + os.close(fd) try: - json_out = json.loads(out) - except ValueError: - self.fail(msg="r.geomorphon produced invalid JSON:\n" + out) - - expected = json.loads(expected_json) - for block in expected: - if isinstance(expected[block], dict): - for key in expected[block]: - if isinstance(expected[block][key], dict): - for sub_key in expected[block][key]: - if isinstance(expected[block][key][sub_key], float): - self.assertAlmostEqual( - expected[block][key][sub_key], - json_out[block][key][sub_key], - places=4, - ) - else: - self.assertEqual( - expected[block][key][sub_key], - json_out[block][key][sub_key], - ) - elif isinstance(expected[block][key], float): - self.assertAlmostEqual( - expected[block][key], json_out[block][key], places=4 - ) - else: - self.assertEqual(expected[block][key], json_out[block][key]) - else: - self.assertEqual(expected[block], json_out[block]) + self.runModule( + "r.geomorphon", + elevation=self.inele, + search=3, + profiledata=tmp, + profileformat="json", + coordinates=(e, n), + overwrite=True, + ) + self.assertTrue(pathlib.Path(tmp).exists()) + self.assertGreater(pathlib.Path(tmp).stat().st_size, 0) + with open(tmp, encoding="utf-8") as fh: + data = json.load(fh) + # basic sanity checks on JSON structure + self.assertIn("final_results", data) + self.assertIn("computation_parameters", data) + self.assertIsInstance(data["final_results"], dict) + self.assertIsInstance(data["computation_parameters"], dict) + finally: + if pathlib.Path(tmp).exists(): + os.remove(tmp) if __name__ == "__main__":