diff --git a/CHANGELOG.md b/CHANGELOG.md index 2335cb34f..d8ec308fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [0.1.4] Next release ### Added +- Show display example of `NUMERIC-EDITED` data on hover [#337](https://github.com/OCamlPro/superbol-studio-oss/pull/337) - Support for dump and listing files, along with a task attribute for outputting the latter [#347](https://github.com/OCamlPro/superbol-studio-oss/pull/347) - Improved information shown on completion [#336](https://github.com/OCamlPro/superbol-studio-oss/pull/336) - Configuration flag for caching in storage provided by Visual Studio Code [#167](https://github.com/OCamlPro/superbol-studio-oss/pull/167) diff --git a/src/lsp/cobol_data/data_picture.ml b/src/lsp/cobol_data/data_picture.ml index cc08118ad..a890e1ebf 100644 --- a/src/lsp/cobol_data/data_picture.ml +++ b/src/lsp/cobol_data/data_picture.ml @@ -122,7 +122,6 @@ module TYPES = struct and special_insertion = { special_insertion_offset: int; - special_insertion_length: int; } and fixed_insertion = @@ -325,14 +324,13 @@ let data_size: category -> int = function let edited_size: category -> int = let simple_insertion_size { simple_insertion_symbols = symbols; _ } = - symbols.symbol_occurences - and special_insertion_size { special_insertion_length = n; _ } = n in + symbols.symbol_occurences in let simple_insertions_size = List.fold_left (fun s i -> s + simple_insertion_size i) 0 and basic_editions_size basics = List.fold_left begin fun s -> function | SimpleInsertion i -> s + simple_insertion_size i - | SpecialInsertion i -> s + special_insertion_size i + | SpecialInsertion _ | FixedInsertion _ -> s + 1 end 0 basics in @@ -561,10 +559,9 @@ let append category ~after_v ({ symbol; symbol_occurences = n } as symbols) = Ok (numeric ~with_sign ~editions digits scale) | Error () -> error) | _ -> error - and append_special_insertion offset = function + and append_special_insertion special_insertion_offset = function | FixedNum { digits; scale; with_sign; editions } -> - let special = SpecialInsertion { special_insertion_offset = offset; - special_insertion_length = n } in + let special = SpecialInsertion { special_insertion_offset } in Ok (numeric ~with_sign digits scale ~editions:{ editions with basics = special :: editions.basics }) | _ -> error @@ -767,12 +764,11 @@ let char_order_checker_for_pic_string config = (* Maybe not in ISO/IEC 2014: Z/CS *) let mutual_exclusions = SymbolsMap.of_seq @@ List.to_seq [ - CS, Symbols.singleton Z; DecimalSep, Symbols.of_list [P; V]; P, Symbols.singleton DecimalSep; Star, Symbols.singleton Z; V, Symbols.singleton DecimalSep; - Z, Symbols.of_list [Star; CS]; + Z, Symbols.singleton Star; ] type exp_sequence_state = diff --git a/src/lsp/cobol_data/data_picture.mli b/src/lsp/cobol_data/data_picture.mli index cb4e81cbe..baba58387 100644 --- a/src/lsp/cobol_data/data_picture.mli +++ b/src/lsp/cobol_data/data_picture.mli @@ -40,6 +40,9 @@ module TYPES: sig | Z | Zero + val pp_symbol: symbol Pretty.printer + val pp_symbol_cobolized: symbol Pretty.printer + type symbols = { symbol: symbol; @@ -102,7 +105,6 @@ module TYPES: sig and special_insertion = { special_insertion_offset: int; - special_insertion_length: int; } and fixed_insertion = diff --git a/src/lsp/cobol_lsp/cobol_lsp.ml b/src/lsp/cobol_lsp/cobol_lsp.ml index d94955588..b3c3a8438 100644 --- a/src/lsp/cobol_lsp/cobol_lsp.ml +++ b/src/lsp/cobol_lsp/cobol_lsp.ml @@ -36,6 +36,7 @@ module INTERNAL = struct module Document = Lsp_document module Server = Lsp_server module Loop = Lsp_server_loop + module Picture_interp = Lsp_picture_interp module Request = Lsp_request.INTERNAL module Utils = Lsp_utils module Debug = Lsp_debug diff --git a/src/lsp/cobol_lsp/lsp_data_info_printer.ml b/src/lsp/cobol_lsp/lsp_data_info_printer.ml index c2e02a435..1c62761db 100644 --- a/src/lsp/cobol_lsp/lsp_data_info_printer.ml +++ b/src/lsp/cobol_lsp/lsp_data_info_printer.ml @@ -25,16 +25,42 @@ let pp_cobol_block: _ Fmt.t -> _ Fmt.t = fun pp -> (* usage *) +let max_value digits scale = + let s = "123456789123456789123456789123456789" in + let whole = (digits - scale) in + let scale = if scale < 0 then 0 else scale in + let whole_part = Str.string_before s whole in + let decimal_part = Str.string_before (Str.string_after s whole) scale in + float_of_string (whole_part ^ "." ^ decimal_part) + +let nbsp_repl = Str.global_replace (Str.regexp " ") " " (* <- utf8 nbsp *) + +let pp_example_of ppf (picture: Cobol_data.Picture.t) = + try + match picture.category with + | FixedNum { digits; scale; _ } -> + let max = max_value digits scale in + let max_str = + if Float.is_integer max + then string_of_int (int_of_float max) + else string_of_float max in + Fmt.pf ppf "\n\n*e.g,* [`%s`] (0), [`%s`] (%s)" + (Lsp_picture_interp.example_of ~picture 0. |> nbsp_repl) + (Lsp_picture_interp.example_of ~picture max |> nbsp_repl) + max_str + | _ -> () + + with Invalid_argument _ -> () + let pp_usage: usage Pretty.printer = let pp_usage_with_picture ppf name (picture: Cobol_data.Picture.t) = - Fmt.( - pp_cobol_block (fun ppf _ -> - pf ppf "PIC %a USAGE %s" - Cobol_data.Picture.pp_picture_symbols picture.pic - name) - ++ const string "\n\n" - ++ const Cobol_data.Picture.pp_category picture.category) - ppf () + Fmt.pf ppf "%a\n\n%a%a" + (pp_cobol_block (fun ppf _ -> + Fmt.pf ppf "PIC %a USAGE %s" + Cobol_data.Picture.pp_picture_symbols picture.pic + name)) () + Cobol_data.Picture.pp_category picture.category + pp_example_of picture and pp_usage_with_sign ppf name signed = pp_cobol_block Fmt.(any "USAGE " ++ any name ++ any (if signed then " SIGNED" else " UNSIGNED")) ppf () diff --git a/src/lsp/cobol_lsp/lsp_picture_interp.ml b/src/lsp/cobol_lsp/lsp_picture_interp.ml new file mode 100644 index 000000000..0c8b9c857 --- /dev/null +++ b/src/lsp/cobol_lsp/lsp_picture_interp.ml @@ -0,0 +1,202 @@ +(**************************************************************************) +(* *) +(* SuperBOL OSS Studio *) +(* *) +(* Copyright (c) 2022-2023 OCamlPro SAS *) +(* *) +(* All rights reserved. *) +(* This source code is licensed under the GNU Affero General Public *) +(* License version 3 found in the LICENSE.md file in the root directory *) +(* of this source tree. *) +(* *) +(**************************************************************************) + +open Cobol_data.Picture +open TYPES + + +let simple_insertion_char_of ~symbol = + match symbol with + | B -> ' ' + | Zero -> '0' + | Slant -> '/' + | DecimalSep -> '.' + | GroupingSep -> ',' + | _ -> Pretty.invalid_arg + "Not a simple insertion symbol '%a'" + pp_symbol symbol + +let fixed_insertion_str_of ~symbol ~is_negative = + match symbol with + | CS -> "$" + | Plus | Minus when is_negative -> "-" + | Plus -> "+" + | Minus -> " " + | CR | DB when not is_negative -> " " + | CR -> "CR" + | DB -> "DB" + | _ -> Pretty.invalid_arg + "Not a fixed insertion symbol '%a'" + pp_symbol_cobolized symbol + +let do_basic_edit_on ~is_negative basic s = + let (offset, insertion) = + match basic with + | SimpleInsertion + { simple_insertion_symbols = { symbol_occurences = n; symbol }; + simple_insertion_offset = offset } -> + offset, String.make n @@ simple_insertion_char_of ~symbol + | SpecialInsertion { special_insertion_offset = offset } -> + offset, "." + | FixedInsertion { fixed_insertion_symbol = symbol; fixed_insertion_offset = offset } -> + offset, fixed_insertion_str_of ~symbol ~is_negative + in + Str.string_before s offset + ^ insertion + ^ Str.string_after s offset + +let all_repl_indexes_from ~ranges s digits = + let indexes = + ranges + |> List.rev_map begin fun { floating_range_offset = offset; + floating_range_length = len } -> + List.init len (fun i -> offset + i) + end + |> List.flatten |> List.sort Int.compare + in + let is_only_repl_char = List.length indexes >= digits in + let min_index = List.hd indexes in + let (_, (all_indexes, all_zero)) = String.fold_left + begin fun ((idx, (acc_repl, should_continue_repl)) as acc) ch -> + if not should_continue_repl then acc else + if List.mem idx indexes + then (idx+1, + if ch == '0' + then (idx::acc_repl, true) + else (acc_repl, false)) + else + if min_index < idx && List.mem ch [' '; ','] + then (idx+1, (idx::acc_repl, true)) + else (idx+1, (acc_repl, should_continue_repl)) + end (0, ([], true)) s + in all_indexes, all_zero && is_only_repl_char + +let do_floatedit_n_zerorepl_on digits is_negative + symbol ranges s = + if ranges == [] then s else + let floating_last_ch = match symbol with + | Plus | Minus when is_negative -> '-' + | Plus -> '+' + | Minus -> ' ' + | CS -> '$' + | Z -> ' ' + | Star -> '*' + | _ -> Pretty.invalid_arg + "Floating edit or zero replacement symbol '%a' is invalid" + pp_symbol_cobolized symbol + in + let repl_ch = match symbol with + | Minus | Plus | CS | Z -> ' ' + | Star -> '*' + | _ -> Pretty.invalid_arg + "Floating edit or zero replacement symbol '%a' is invalid" + pp_symbol_cobolized symbol + in + let repl_str = String.make 1 repl_ch in + let all_repl_indexes, repl_everything = + all_repl_indexes_from ~ranges s digits in + if repl_everything + then + String.map begin fun ch -> + if ch == '.' && symbol == Star + then '.' + else repl_ch + end s + else + let (_, _, last_repl_idx, res) = String.fold_left + begin fun (i, after_decimal_point, last_repl_idx, res) ch -> + let orig_str = String.make 1 ch in + if after_decimal_point + then (i+1, after_decimal_point, last_repl_idx, res ^ orig_str) + else + if ch == '.' + then (i+1, true, last_repl_idx, res ^ ".") + else + if List.mem i all_repl_indexes + then (i+1, after_decimal_point, i, res ^ repl_str) + else (i+1, after_decimal_point, last_repl_idx, res ^ orig_str) + end (0, false, -1, "") s + in + String.mapi begin fun i ch -> + if i == last_repl_idx + then floating_last_ch + else ch + end + res + +let rec edit_basics ~is_negative basics s = + match basics with + | [] -> s + | hd::tl -> + do_basic_edit_on ~is_negative hd s + |> edit_basics ~is_negative tl + +let simple_example_of ~digits ~scale ~with_dot value = + let str_val = string_of_float (Float.abs value) in + let i = String.index str_val '.' in + let whole_part = Str.string_before str_val i in + let whole_len = String.length whole_part in + let floating_part = Str.string_after str_val (i+1) in + let required_len = digits - scale in + (String.init required_len + (fun i -> + if i < required_len - whole_len + then '0' + else whole_part.[i - (required_len - whole_len)]) + ) + ^ (if scale > 0 + then + (if with_dot then "." else "") + ^ String.init scale + (fun i -> + if i < String.length floating_part + then floating_part.[i] + else '0') + else "") + +let example_of ~picture value = + if List.exists (fun { symbol; _ } -> symbol == P) picture.pic + then raise @@ Invalid_argument "No example with P yet" (* /!\ scale can be negative: PIC 9P *) + else + match picture.category with + | Alphabetic _ | Boolean _ | National _ | Alphanumeric _ -> "" + | FloatNum _ -> raise @@ Invalid_argument "No example for floatnum yet" + | FixedNum { digits; scale; with_sign; _ } + when not @@ is_edited picture -> + (if with_sign then "+" else "") + ^ simple_example_of ~digits ~scale ~with_dot:true value + | FixedNum { digits; scale; with_sign; + editions = { basics; floating; zerorepl } } -> + ignore (with_sign); + let is_negative = value < 0. in + let edit_zerorepl = Option.fold ~none:Fun.id + ~some:(fun { zero_replacement_symbol = symbol; + zero_replacement_ranges = ranges } -> + do_floatedit_n_zerorepl_on digits is_negative symbol ranges) + zerorepl in + let edit_floating = Option.fold ~none:Fun.id + ~some:(fun { floating_insertion_symbol = symbol; + floating_insertion_ranges = ranges } -> + do_floatedit_n_zerorepl_on digits is_negative symbol ranges) + floating in + try + (if Option.is_some floating + then "0" + else "") + ^ simple_example_of ~digits ~scale ~with_dot:false value + |> edit_basics ~is_negative:(value < 0.) basics + |> edit_zerorepl + |> edit_floating + with Invalid_argument e -> + Pretty.invalid_arg + "Unable to build example of picture, error '%s'" e diff --git a/test/cobol_parsing/test_picture_parsing.ml b/test/cobol_parsing/test_picture_parsing.ml index cf76a07c0..9fa58a557 100644 --- a/test/cobol_parsing/test_picture_parsing.ml +++ b/test/cobol_parsing/test_picture_parsing.ml @@ -328,8 +328,7 @@ module Pictures = struct simple_insertion_offset = 0 }; SimpleInsertion { simple_insertion_symbols = comma 1; simple_insertion_offset = 5 }; - SpecialInsertion { special_insertion_offset = 9; - special_insertion_length = 1 } ] + SpecialInsertion { special_insertion_offset = 9 } ] in { category = fixednum 9 3 ~basics; pic = [comma 2; nine 3; comma 1; nine 3; dot 1; nine 3] } @@ -361,8 +360,7 @@ module Pictures = struct and basics = [ SimpleInsertion { simple_insertion_symbols = comma 1; simple_insertion_offset = 3 }; - SpecialInsertion { special_insertion_offset = 7; - special_insertion_length = 1 } ] + SpecialInsertion { special_insertion_offset = 7 } ] in { category = fixednum 9 3 ~basics ~zerorepl; pic = [z 3; comma 1; z 3; dot 1; z 3] } @@ -413,8 +411,7 @@ module Pictures = struct let basics = [ SimpleInsertion { simple_insertion_symbols = comma 1; simple_insertion_offset = 3 }; - SpecialInsertion { special_insertion_offset = 7; - special_insertion_length = 1 } ] + SpecialInsertion { special_insertion_offset = 7 } ] in { category = floatnum 9 3 3 ~basics; pic = [nine 3; comma 1; nine 3; dot 1; nine 3; e 1; plus 1; nine 3] } @@ -446,8 +443,7 @@ module Pictures = struct let pic_ppvpp = let basics = - [ SpecialInsertion { special_insertion_offset = 2; - special_insertion_length = 1 } ] + [ SpecialInsertion { special_insertion_offset = 2 } ] and floating = { floating_insertion_symbol = Plus; floating_insertion_ranges = [{ floating_range_offset = 0; diff --git a/test/lsp/lsp_hover.ml b/test/lsp/lsp_hover.ml index 9b43a7fdd..7ee599d0e 100644 --- a/test/lsp/lsp_hover.ml +++ b/test/lsp/lsp_hover.ml @@ -292,6 +292,7 @@ let%expect_test "hover-typedef-vars" = PIC 999 USAGE DISPLAY ``` NUMERIC(digits = 3, scale = 0, with_sign = false) + *e.g,* [`000`] (0), [`123`] (123) VALUE 123 (line 8, character 16): __rootdir__/prog.cob:9.13-9.21: @@ -390,6 +391,7 @@ let%expect_test "hover-typedef-vars" = PIC 999 USAGE DISPLAY ``` NUMERIC(digits = 3, scale = 0, with_sign = false) + *e.g,* [`000`] (0), [`123`] (123) VALUE 123 (line 12, character 47): __rootdir__/prog.cob:13.44-13.52: @@ -440,6 +442,7 @@ let%expect_test "hover-typedef-vars-usage" = 01 _|_VAR6 PIC 111 USAGE BIT. 01 _|_VAR7 USAGE POINTER. 01 _|_VAR8 PIC 9 USAGE PACKED-DECIMAL. + 01 _|_VAR9 PIC $++/+.+B+. PROCEDURE DIVISION. STOP RUN. |cobol}; @@ -462,6 +465,7 @@ let%expect_test "hover-typedef-vars-usage" = PIC -BZZZ,ZZ9.99 USAGE DISPLAY ``` NUMERIC(digits = 8, scale = 2, with_sign = false) + *e.g,* [`        0.00`] (0), [`  123,456.78`] (123456.78) (line 6, character 11): __rootdir__/prog.cob:7.11-7.15: 4 DATA DIVISION. @@ -478,6 +482,7 @@ let%expect_test "hover-typedef-vars-usage" = PIC 9 USAGE BINARY ``` NUMERIC(digits = 1, scale = 0, with_sign = false) + *e.g,* [`0`] (0), [`1`] (1) (line 7, character 11): __rootdir__/prog.cob:8.11-8.15: 5 WORKING-STORAGE SECTION. @@ -532,7 +537,7 @@ let%expect_test "hover-typedef-vars-usage" = 11 > 01 VAR7 USAGE POINTER. ---- ^^^^ 12 01 VAR8 PIC 9 USAGE PACKED-DECIMAL. - 13 PROCEDURE DIVISION. + 13 01 VAR9 PIC $++/+.+B+. ```cobol VAR7 ``` @@ -544,16 +549,33 @@ let%expect_test "hover-typedef-vars-usage" = 11 01 VAR7 USAGE POINTER. 12 > 01 VAR8 PIC 9 USAGE PACKED-DECIMAL. ---- ^^^^ - 13 PROCEDURE DIVISION. - 14 STOP RUN. + 13 01 VAR9 PIC $++/+.+B+. + 14 PROCEDURE DIVISION. ```cobol VAR8 ``` ```cobol PIC 9 USAGE PACKED-DECIMAL ``` - NUMERIC(digits = 1, scale = 0, with_sign = false) |}];; - + NUMERIC(digits = 1, scale = 0, with_sign = false) + *e.g,* [`0`] (0), [`1`] (1) + (line 12, character 11): + __rootdir__/prog.cob:13.11-13.15: + 10 01 VAR6 PIC 111 USAGE BIT. + 11 01 VAR7 USAGE POINTER. + 12 01 VAR8 PIC 9 USAGE PACKED-DECIMAL. + 13 > 01 VAR9 PIC $++/+.+B+. + ---- ^^^^ + 14 PROCEDURE DIVISION. + 15 STOP RUN. + ```cobol + VAR9 + ``` + ```cobol + PIC $++/+.+B+ USAGE DISPLAY + ``` + NUMERIC(digits = 4, scale = 2, with_sign = false) + *e.g,* [`         `] (0), [`$+1/2.3 4`] (12.34) |}];; let%expect_test "hover-typedef-filler-vars" = let { projdir; end_with_postproc }, server = make_lsp_project () in @@ -791,6 +813,7 @@ let%expect_test "hover-typedef-renames" = PIC 9 USAGE DISPLAY ``` NUMERIC(digits = 1, scale = 0, with_sign = false) + *e.g,* [`0`] (0), [`1`] (1) (line 8, character 16): __rootdir__/prog.cob:9.10-9.25: 6 01 X. @@ -808,6 +831,7 @@ let%expect_test "hover-typedef-renames" = PIC 9 USAGE DISPLAY ``` NUMERIC(digits = 1, scale = 0, with_sign = false) + *e.g,* [`0`] (0), [`1`] (1) (line 8, character 23): __rootdir__/prog.cob:9.23-9.24: 6 01 X. @@ -824,6 +848,7 @@ let%expect_test "hover-typedef-renames" = PIC 9 USAGE DISPLAY ``` NUMERIC(digits = 1, scale = 0, with_sign = false) + *e.g,* [`0`] (0), [`1`] (1) (line 9, character 22): __rootdir__/prog.cob:10.13-10.22: 7 05 Y PIC 9. @@ -858,7 +883,8 @@ let%expect_test "hover-typedef-renames" = ```cobol PIC 9 USAGE DISPLAY ``` - NUMERIC(digits = 1, scale = 0, with_sign = false) |}];; + NUMERIC(digits = 1, scale = 0, with_sign = false) + *e.g,* [`0`] (0), [`1`] (1) |}];; let%expect_test "hover-typedef-redefines" = let { projdir; end_with_postproc }, server = make_lsp_project () in @@ -913,6 +939,7 @@ let%expect_test "hover-typedef-redefines" = PIC 9 USAGE DISPLAY ``` NUMERIC(digits = 1, scale = 0, with_sign = false) + *e.g,* [`0`] (0), [`1`] (1) (line 9, character 20): __rootdir__/prog.cob:10.20-10.21: 7 05 Y PIC 9. diff --git a/test/lsp/lsp_picture_interp.ml b/test/lsp/lsp_picture_interp.ml new file mode 100644 index 000000000..94123eec1 --- /dev/null +++ b/test/lsp/lsp_picture_interp.ml @@ -0,0 +1,74 @@ +(**************************************************************************) +(* *) +(* SuperBOL OSS Studio *) +(* *) +(* Copyright (c) 2022-2023 OCamlPro SAS *) +(* *) +(* All rights reserved. *) +(* This source code is licensed under the GNU Affero General Public *) +(* License version 3 found in the LICENSE.md file in the root directory *) +(* of this source tree. *) +(* *) +(**************************************************************************) + + +open Lsp_testing +module CHARS = Cobol_common.Basics.CharSet + +let unit_tests = + [ ("99,B999,B000", 1234., "01, 234, 000"); + ("99,999", 12345., "12,345"); + ("999.99", 1.234, "001.23"); + ("999.99", 12.34, "012.34"); + ("999.99", 123.45, "123.45"); + ("999.99", 1234.5, "234.50"); + (* ("+999.99E+99", 12345., "+123.45E+02"); *) + ("999.99+", +6555.556, "555.55+"); + ("+9999.99", -6555.555, "-6555.55"); + ("9999.99", +1234.56, "1234.56"); + ("$999.99", -123.45, "$123.45"); + ("-$999.99", -123.456, "-$123.45"); + ("-$999.99", +123.456, " $123.45"); + ("$9999.99CR", +123.45, "$0123.45 "); + ("$9999.99DB", -123.45, "$0123.45DB"); + (* ("U999.99", -123.45, "EUR123.45"); *) + (* ("-u999.99", -123.456, "-USD123.45"); *) + ("$$$$.99", 0.123, " $.12"); + ("$$$9.99", 0.12, " $0.12"); + ("$,$$$,999.99", -1234.56, " $1,234.56"); + (* ("U,UUU,UU9.99-", -1234.56, "EUR1,234.56-"); *) + (* ("u,uuu,uu9.99", 1234.56, "USD1,234.56"); *) + ("+,+++,999.99", -123456.789, " -123,456.78"); + ("$$,$$$,$$$.99CR", -1234567., "$1,234,567.00CR"); + ("++,+++,+++.+++", 0000.00, " "); + ("$B++/+++.+", 0000.00, " "); + ("****.**", 0000.00, "****.**"); + ("ZZZZ.ZZ", 0000.00, " "); + ("ZZZZ.99", 0000.00, " .00"); + ("****.99", 0000.00, "****.00"); + ("ZZ99.99", 0000.00, " 00.00"); + ("Z,ZZZ.ZZ+", +123.456, " 123.45+"); + ("*,***.**+", -123.45, "**123.45-"); + ("**,***,***.**", +12345678.9, "12,345,678.90"); + ("$Z,ZZZ,ZZZ.ZZCR", +12345.67, "$ 12,345.67 "); + ("$B*,***.**BBDB", 0., "*******.******"); + ("$B*,***,***.**BBDB", -12345.67, "$ ***12,345.67 DB");] + +let () = + let config: Cobol_data.Picture.TYPES.config = + { max_pic_length = 100; decimal_char = '.'; + currency_signs = CHARS.add '$' CHARS.empty } + in + let rec test = function + | [] -> () + | (pic, value, expected)::tl -> + match Cobol_data.Picture.of_string config pic with + | Ok picture -> + let example = LSP.Picture_interp.example_of ~picture value in + let error_msg = Pretty.to_string "ERROR: different result (%s, %f) -> '%s' (expected: '%s')" pic value example expected in + if String.equal example expected + then test tl + else failwith error_msg + | Error _ -> + failwith @@ Pretty.to_string "ERROR: Unable to form picture with picture-string '%s'\n" pic + in test unit_tests