From 9c0f8134fcbeff8c6f4c3b6615ae285289873102 Mon Sep 17 00:00:00 2001 From: John Bartholomew Date: Thu, 19 Feb 2026 12:00:56 +0000 Subject: [PATCH 1/3] test_cmd: add support for updating golden files --- test_cmd/cmd_tests.source | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/test_cmd/cmd_tests.source b/test_cmd/cmd_tests.source index 012c4d22..908c4647 100644 --- a/test_cmd/cmd_tests.source +++ b/test_cmd/cmd_tests.source @@ -25,6 +25,8 @@ JSONNETFMT_BIN="${JSONNETFMT_BIN:-../jsonnetfmt}" OVERRIDE_DIR="${OVERRIDE_DIR:-.}" +UPDATE_GOLDENS="${UPDATE_GOLDENS:-0}" + separator() { echo -e "\n-----------------------------\n" } @@ -43,22 +45,27 @@ check_file_sed() { RESULT_CONTENT="$(sed --silent -E -e "$SED_SCRIPT" "$RESULT_FILENAME")" fi if [ "${RESULT_CONTENT}" != "${GOLDEN_CONTENT}" ] ; then - FAILED=$((FAILED + 1)) - if [ -z "${SUMMARY_ONLY:-}" ]; then - printf "\033[31;1mFAIL\033[0m \033[1m(stdout mismatch)\033[0m: \033[36m%s\033[0m\n" "$TEST" - echo "Result output:" - echo "${RESULT_CONTENT}" - echo "Expected output:" - echo "${GOLDEN_CONTENT}" - echo "Diff:" - # Using git diff for pretty format and pretty colors - git --no-pager diff --color --no-index "$GOLDEN_FILENAME" <(echo "${RESULT_CONTENT}") - # The output is often quite long, let's repeat the filename once more - # to avoid wasting time on looking for it - printf "\nTEST ABOVE: \033[36m%s\033[0m\n" "$TEST" - separator + if [[ "${UPDATE_GOLDENS}" -eq 1 ]]; then + printf "\033[31;1mUPDATING\033[0m \033[1m(stdout mismatch)\033[0m: \033[36m%s\033[0m\n" "$TEST" + > "$GOLDEN_FILENAME" echo -n "${RESULT_CONTENT}" + else + FAILED=$((FAILED + 1)) + if [ -z "${SUMMARY_ONLY:-}" ]; then + printf "\033[31;1mFAIL\033[0m \033[1m(stdout mismatch)\033[0m: \033[36m%s\033[0m\n" "$TEST" + echo "Result output:" + echo "${RESULT_CONTENT}" + echo "Expected output:" + echo "${GOLDEN_CONTENT}" + echo "Diff:" + # Using git diff for pretty format and pretty colors + git --no-pager diff --color --no-index "$GOLDEN_FILENAME" <(echo "${RESULT_CONTENT}") + # The output is often quite long, let's repeat the filename once more + # to avoid wasting time on looking for it + printf "\nTEST ABOVE: \033[36m%s\033[0m\n" "$TEST" + separator + fi + return 1 fi - return 1 fi } From 534ae464acd3ef91fd49254e2c9181de54837607 Mon Sep 17 00:00:00 2001 From: John Bartholomew Date: Thu, 19 Feb 2026 12:17:36 +0000 Subject: [PATCH 2/3] test_cmd: don't rely on being able to read and compare files byte-for-byte in bash I thought I was being smart by using some bash stuff like $(< some_file) to read files and compare them within the script, but that was a bad idea because bash tends to mess with whitespace whenever it can. It strips trailing newlines, for example. So, best to use `cmp` to compare the files directly where possible. --- test_cmd/cmd_tests.source | 25 +++++++++++++++---------- test_cmd/fmt_simple5.golden.stderr | 1 + 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/test_cmd/cmd_tests.source b/test_cmd/cmd_tests.source index 908c4647..c725981e 100644 --- a/test_cmd/cmd_tests.source +++ b/test_cmd/cmd_tests.source @@ -36,29 +36,34 @@ check_file_sed() { local RESULT_FILENAME="$2" local GOLDEN_FILENAME="$3" local SED_SCRIPT="$4" + local HAS_DIFF - local GOLDEN_CONTENT="$(< "$GOLDEN_FILENAME")" - local RESULT_CONTENT if [[ -z "$SED_SCRIPT" ]]; then - RESULT_CONTENT="$(< "$RESULT_FILENAME")" - else - RESULT_CONTENT="$(sed --silent -E -e "$SED_SCRIPT" "$RESULT_FILENAME")" + SED_SCRIPT='p' fi - if [ "${RESULT_CONTENT}" != "${GOLDEN_CONTENT}" ] ; then + + cmp --quiet "$GOLDEN_FILENAME" <(sed --silent -E -e "$SED_SCRIPT" "$RESULT_FILENAME") + HAS_DIFF="$?" + + if [ "${HAS_DIFF}" -eq 2 ]; then + FAILED=$((FAILED + 1)) + printf "\033[31;1mERROR\033[0m \033[1m(cmp failed)\033[0m: \033[36m%s\033[0m\n" "$TEST" + return 1 + elif [ "${HAS_DIFF}" -ne 0 ] ; then if [[ "${UPDATE_GOLDENS}" -eq 1 ]]; then printf "\033[31;1mUPDATING\033[0m \033[1m(stdout mismatch)\033[0m: \033[36m%s\033[0m\n" "$TEST" - > "$GOLDEN_FILENAME" echo -n "${RESULT_CONTENT}" + > "${GOLDEN_FILENAME}" sed --silent -E -e "$SED_SCRIPT" "$RESULT_FILENAME" else FAILED=$((FAILED + 1)) if [ -z "${SUMMARY_ONLY:-}" ]; then printf "\033[31;1mFAIL\033[0m \033[1m(stdout mismatch)\033[0m: \033[36m%s\033[0m\n" "$TEST" echo "Result output:" - echo "${RESULT_CONTENT}" + sed --silent -E -e "$SED_SCRIPT" "$RESULT_FILENAME" echo "Expected output:" - echo "${GOLDEN_CONTENT}" + cat "${GOLDEN_FILENAME}" echo "Diff:" # Using git diff for pretty format and pretty colors - git --no-pager diff --color --no-index "$GOLDEN_FILENAME" <(echo "${RESULT_CONTENT}") + git --no-pager diff --color --no-index "$GOLDEN_FILENAME" <(sed --silent -E -e "$SED_SCRIPT" "$RESULT_FILENAME") # The output is often quite long, let's repeat the filename once more # to avoid wasting time on looking for it printf "\nTEST ABOVE: \033[36m%s\033[0m\n" "$TEST" diff --git a/test_cmd/fmt_simple5.golden.stderr b/test_cmd/fmt_simple5.golden.stderr index 4895264a..44e26d66 100644 --- a/test_cmd/fmt_simple5.golden.stderr +++ b/test_cmd/fmt_simple5.golden.stderr @@ -1 +1,2 @@ ERROR: only one filename is allowed + From 3f56d4c3d5fba06919f2229d99e2ddf623a71385 Mon Sep 17 00:00:00 2001 From: John Bartholomew Date: Thu, 19 Feb 2026 12:34:28 +0000 Subject: [PATCH 3/3] feat: add option --no-trailing-newline to disable the final newline in output --- cmd/jsonnet.cpp | 12 +++++++ core/libjsonnet.cpp | 21 +++++++++-- cpp/libjsonnet++.cpp | 5 +++ include/libjsonnet++.h | 3 ++ include/libjsonnet.h | 3 ++ test_cmd/help.golden.stdout | 1 + test_cmd/no_args.golden.stderr | 1 + test_cmd/nonewline1.golden.stdout | 1 + test_cmd/nonewline2.golden.stdout | 5 +++ test_cmd/nonewline3.golden.stdout | 1 + test_cmd/nonewline_multi1.golden.file1 | 1 + test_cmd/nonewline_multi1.golden.file2 | 1 + test_cmd/nonewline_multi1.golden.stdout | 2 ++ test_cmd/nonewline_multi2.golden.file1 | 1 + test_cmd/nonewline_multi2.golden.file2 | 1 + test_cmd/nonewline_multi2.golden.stdout | 2 ++ test_cmd/nonewline_yaml1.golden.stderr | 46 +++++++++++++++++++++++++ test_cmd/run_cmd_tests.sh | 19 ++++++++-- 18 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 test_cmd/nonewline1.golden.stdout create mode 100644 test_cmd/nonewline2.golden.stdout create mode 100644 test_cmd/nonewline3.golden.stdout create mode 100644 test_cmd/nonewline_multi1.golden.file1 create mode 100644 test_cmd/nonewline_multi1.golden.file2 create mode 100644 test_cmd/nonewline_multi1.golden.stdout create mode 100644 test_cmd/nonewline_multi2.golden.file1 create mode 100644 test_cmd/nonewline_multi2.golden.file2 create mode 100644 test_cmd/nonewline_multi2.golden.stdout create mode 100644 test_cmd/nonewline_yaml1.golden.stderr diff --git a/cmd/jsonnet.cpp b/cmd/jsonnet.cpp index fc8d507b..7cbeeda4 100644 --- a/cmd/jsonnet.cpp +++ b/cmd/jsonnet.cpp @@ -58,6 +58,7 @@ void usage(std::ostream &o) o << " -m / --multi Write multiple files to the directory, list files on stdout\n"; o << " -y / --yaml-stream Write output as a YAML stream of JSON documents\n"; o << " -S / --string Expect a string, manifest as plain text\n"; + o << " --no-trailing-newline Do not add a trailing newline to the output\n"; o << " -s / --max-stack Number of allowed stack frames\n"; o << " -t / --max-trace Max length of stack trace before cropping\n"; o << " --gc-min-objects Do not run garbage collector until this many\n"; @@ -167,6 +168,7 @@ static ArgStatus process_args(int argc, const char **argv, JsonnetConfig *config std::vector remaining_args; unsigned i = 0; + bool trailing_newline = true; for (; i < args.size(); ++i) { const std::string &arg = args[i]; @@ -323,6 +325,10 @@ static ArgStatus process_args(int argc, const char **argv, JsonnetConfig *config config->evalStream = true; } else if (arg == "-S" || arg == "--string") { jsonnet_string_output(vm, 1); + } else if (arg == "--no-trailing-newline") { + // Keep track so we can check for mutually incompatible args. + trailing_newline = false; + jsonnet_set_trailing_newline(vm, 0); } else if (arg.length() > 1 && arg[0] == '-') { std::cerr << "ERROR: unrecognized argument: " << arg << std::endl; return ARG_FAILURE; @@ -331,6 +337,12 @@ static ArgStatus process_args(int argc, const char **argv, JsonnetConfig *config } } + if (config->evalStream && !trailing_newline) { + std::cerr << "ERROR: cannot use --no-trailing-newline with --yaml-stream" << std::endl; + usage(std::cerr); + return ARG_FAILURE; + } + const char *want = config->filenameIsCode ? "code" : "filename"; if (remaining_args.size() == 0) { std::cerr << "ERROR: must give " << want << "\n" << std::endl; diff --git a/core/libjsonnet.cpp b/core/libjsonnet.cpp index 730cef8e..375cf00b 100644 --- a/core/libjsonnet.cpp +++ b/core/libjsonnet.cpp @@ -218,6 +218,7 @@ struct JsonnetVm { VmNativeCallbackMap nativeCallbacks; void *importCallbackContext; bool stringOutput; + bool trailingNewline; std::vector jpaths; FmtOpts fmtOpts; @@ -231,6 +232,7 @@ struct JsonnetVm { importCallback(default_import_callback), importCallbackContext(this), stringOutput(false), + trailingNewline(true), fmtDebugDesugaring(false) { jpaths.emplace_back("/usr/share/jsonnet-" + std::string(jsonnet_version()) + "/"); @@ -368,6 +370,11 @@ void jsonnet_string_output(struct JsonnetVm *vm, int v) vm->stringOutput = bool(v); } +void jsonnet_set_trailing_newline(struct JsonnetVm *vm, int enable) +{ + vm->trailingNewline = bool(enable); +} + void jsonnet_import_callback(struct JsonnetVm *vm, JsonnetImportCallback *cb, void *ctx) { vm->importCallback = cb; @@ -562,7 +569,9 @@ static char *jsonnet_evaluate_snippet_aux(JsonnetVm *vm, const char *filename, c vm->importCallback, vm->importCallbackContext, vm->stringOutput); - json_str += "\n"; + if (vm->trailingNewline) { + json_str += "\n"; + } *error = false; return from_string(vm, json_str); } break; @@ -593,8 +602,10 @@ static char *jsonnet_evaluate_snippet_aux(JsonnetVm *vm, const char *filename, c i += pair.first.length() + 1; memcpy(&buf[i], pair.second.c_str(), pair.second.length()); i += pair.second.length(); - buf[i] = '\n'; - i++; + if (vm->trailingNewline) { + buf[i] = '\n'; + i++; + } buf[i] = '\0'; i++; } @@ -604,6 +615,10 @@ static char *jsonnet_evaluate_snippet_aux(JsonnetVm *vm, const char *filename, c } break; case STREAM: { + if (!vm->trailingNewline) { + *error = true; + return from_string(vm, "INTERNAL: trailing-newline is required for streamed output"); + } std::vector documents = jsonnet_vm_execute_stream(&alloc, expr, diff --git a/cpp/libjsonnet++.cpp b/cpp/libjsonnet++.cpp index 2b1a8483..28c9efae 100644 --- a/cpp/libjsonnet++.cpp +++ b/cpp/libjsonnet++.cpp @@ -58,6 +58,11 @@ void Jsonnet::setStringOutput(bool string_output) ::jsonnet_string_output(vm_, string_output); } +void Jsonnet::setTrailingNewline(bool enable) +{ + ::jsonnet_set_trailing_newline(vm_, enable); +} + void Jsonnet::addImportPath(const std::string& path) { ::jsonnet_jpath_add(vm_, path.c_str()); diff --git a/include/libjsonnet++.h b/include/libjsonnet++.h index 9a2ccc3e..cf26c3c3 100644 --- a/include/libjsonnet++.h +++ b/include/libjsonnet++.h @@ -60,6 +60,9 @@ class Jsonnet { /// Set whether to expect a string as output and don't JSON encode it. void setStringOutput(bool string_output); + /// Set whether to include a trailing newline in manifested/string output. + void setTrailingNewline(bool enable); + /// Set the number of lines of stack trace to display (0 to display all). void setMaxTrace(uint32_t lines); diff --git a/include/libjsonnet.h b/include/libjsonnet.h index 64ed91cb..85931441 100644 --- a/include/libjsonnet.h +++ b/include/libjsonnet.h @@ -57,6 +57,9 @@ void jsonnet_gc_growth_trigger(struct JsonnetVm *vm, double v); /** Expect a string as output and don't JSON encode it. */ void jsonnet_string_output(struct JsonnetVm *vm, int v); +/** Enable/disable trailing newline in manifested/string output. */ +void jsonnet_set_trailing_newline(struct JsonnetVm *vm, int enable); + /** Callback used to load imports. * * The returned char* should be allocated with jsonnet_realloc. It will be cleaned up by diff --git a/test_cmd/help.golden.stdout b/test_cmd/help.golden.stdout index fd820394..60e26ee8 100644 --- a/test_cmd/help.golden.stdout +++ b/test_cmd/help.golden.stdout @@ -10,6 +10,7 @@ Available options: -m / --multi Write multiple files to the directory, list files on stdout -y / --yaml-stream Write output as a YAML stream of JSON documents -S / --string Expect a string, manifest as plain text + --no-trailing-newline Do not add a trailing newline to the output -s / --max-stack Number of allowed stack frames -t / --max-trace Max length of stack trace before cropping --gc-min-objects Do not run garbage collector until this many diff --git a/test_cmd/no_args.golden.stderr b/test_cmd/no_args.golden.stderr index 099fa8c3..9c913635 100644 --- a/test_cmd/no_args.golden.stderr +++ b/test_cmd/no_args.golden.stderr @@ -12,6 +12,7 @@ Available options: -m / --multi Write multiple files to the directory, list files on stdout -y / --yaml-stream Write output as a YAML stream of JSON documents -S / --string Expect a string, manifest as plain text + --no-trailing-newline Do not add a trailing newline to the output -s / --max-stack Number of allowed stack frames -t / --max-trace Max length of stack trace before cropping --gc-min-objects Do not run garbage collector until this many diff --git a/test_cmd/nonewline1.golden.stdout b/test_cmd/nonewline1.golden.stdout new file mode 100644 index 00000000..f70d7bba --- /dev/null +++ b/test_cmd/nonewline1.golden.stdout @@ -0,0 +1 @@ +42 \ No newline at end of file diff --git a/test_cmd/nonewline2.golden.stdout b/test_cmd/nonewline2.golden.stdout new file mode 100644 index 00000000..18d664bb --- /dev/null +++ b/test_cmd/nonewline2.golden.stdout @@ -0,0 +1,5 @@ +{ + "a": 1, + "b": 2, + "c": 3 +} \ No newline at end of file diff --git a/test_cmd/nonewline3.golden.stdout b/test_cmd/nonewline3.golden.stdout new file mode 100644 index 00000000..b6fc4c62 --- /dev/null +++ b/test_cmd/nonewline3.golden.stdout @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/test_cmd/nonewline_multi1.golden.file1 b/test_cmd/nonewline_multi1.golden.file1 new file mode 100644 index 00000000..cffcb609 --- /dev/null +++ b/test_cmd/nonewline_multi1.golden.file1 @@ -0,0 +1 @@ +"file1" \ No newline at end of file diff --git a/test_cmd/nonewline_multi1.golden.file2 b/test_cmd/nonewline_multi1.golden.file2 new file mode 100644 index 00000000..edb69cd7 --- /dev/null +++ b/test_cmd/nonewline_multi1.golden.file2 @@ -0,0 +1 @@ +"file2" \ No newline at end of file diff --git a/test_cmd/nonewline_multi1.golden.stdout b/test_cmd/nonewline_multi1.golden.stdout new file mode 100644 index 00000000..ae4999cf --- /dev/null +++ b/test_cmd/nonewline_multi1.golden.stdout @@ -0,0 +1,2 @@ +out/nonewline_multi1/file1 +out/nonewline_multi1/file2 diff --git a/test_cmd/nonewline_multi2.golden.file1 b/test_cmd/nonewline_multi2.golden.file1 new file mode 100644 index 00000000..08219db9 --- /dev/null +++ b/test_cmd/nonewline_multi2.golden.file1 @@ -0,0 +1 @@ +file1 \ No newline at end of file diff --git a/test_cmd/nonewline_multi2.golden.file2 b/test_cmd/nonewline_multi2.golden.file2 new file mode 100644 index 00000000..30d67d46 --- /dev/null +++ b/test_cmd/nonewline_multi2.golden.file2 @@ -0,0 +1 @@ +file2 \ No newline at end of file diff --git a/test_cmd/nonewline_multi2.golden.stdout b/test_cmd/nonewline_multi2.golden.stdout new file mode 100644 index 00000000..c5a178cc --- /dev/null +++ b/test_cmd/nonewline_multi2.golden.stdout @@ -0,0 +1,2 @@ +out/nonewline_multi2/file1 +out/nonewline_multi2/file2 diff --git a/test_cmd/nonewline_yaml1.golden.stderr b/test_cmd/nonewline_yaml1.golden.stderr new file mode 100644 index 00000000..05896ea0 --- /dev/null +++ b/test_cmd/nonewline_yaml1.golden.stderr @@ -0,0 +1,46 @@ +ERROR: cannot use --no-trailing-newline with --yaml-stream +Jsonnet commandline interpreter v0.21.0 + +jsonnet {