CLI: --strict, and a warnings array in result.json (#14601)

# Description

Add `--strict` for CI and scripted pipelines, and a structured
`warnings`
array in `result.json`.

## `--strict`

A NON_CRITICAL slicing warning is logged and the slice succeeds: return
code
`0`, G-code written. That suits interactive use, but a pipeline then
ships a
slice with a warning nobody saw. With `--strict`, such a warning fails
the run
with `CLI_SLICING_ERROR` before the G-code is exported. Without the
flag,
nothing changes.

In FFF the warning that reaches this path is "support needed but
disabled"
(`PrintObject::generate_support_material`). `--no-check` skips that
check, so
`--strict --no-check` is rejected with `CLI_INVALID_PARAMS`.

`--strict` is read before any work, so it doesn't depend on argument
order and
`result.json` reports it for early failures as well.

## `result.json`

Two new top-level fields:

- `warnings`: `[{"class", ...details}]`. One class is wired:
`slicing_warning_non_critical` with `plate_id` and `text`, recorded
whenever
such a warning fires, with or without `--strict`. The array also fills
on
  runs that succeed, so `return_code` stays the verdict.
- `strict_mode`: whether `--strict` was on.

`record_exit_reson` writes `result.json` on Linux only, so both fields
exist
only there. The non-zero exit works on every platform.

## Tests

- `tests/fff_print/test_support_material.cpp` (all platforms): an
overhang
sliced with support off raises the NON_CRITICAL support-needed status,
and
  the no-check flag suppresses it.
- `tests/cli/test_cli_strict.sh` (Linux only): runs `orca-slicer`
without
flags, with `--strict`, and with `--strict --no-check`, and checks the
shell
status and `result.json` of each. It runs the built binary, so it
carries the
`RequiresApp` label, which `scripts/run_unit_tests.sh` excludes because
the
  unit-test job only receives `build/tests`. Run it with
  `ctest --test-dir build/tests -C Release -L RequiresApp`.
- CI: `unit_tests.yml` now passes `Release` on Linux too.
`build_linux.sh`
configures Ninja Multi-Config, and without a config ctest drops the
labels of
plain `add_test()` tests, so this test ran as "Not Run" instead of being
excluded. The docs that assumed Linux was single-config are corrected
too.

Built and run locally on Linux (GCC 14) on current `main`: both tests
pass,
and the touched files compile clean under Clang with `-Werror`.
This commit is contained in:
packerlschupfer
2026-09-16 12:54:48 +08:00
committed by GitHub
parent 3e1daccd7c
commit 9321f24959
10 changed files with 290 additions and 11 deletions
+42
View File
@@ -189,6 +189,9 @@ typedef struct _sliced_info {
int wall_loops{0};
std::vector<std::string> upward_machines;
std::vector<std::string> downward_machines;
// Structured slicing warnings for result.json, and whether --strict was on.
nlohmann::json warnings = nlohmann::json::array();
bool strict_mode {false};
}sliced_info_t;
std::vector<PrintBase::SlicingStatus> g_slicing_warnings;
@@ -424,6 +427,21 @@ static PrinterTechnology get_printer_technology(const DynamicConfig &config)
return(ret);}
#endif
// Records a structured slicing warning so a CI or scripted consumer can branch on
// a stable `class` string instead of matching stderr. Warnings are kept on the
// run's sliced_info and emitted as the top-level "warnings" array of result.json;
// a non-empty array does not by itself mean the run failed. Under --strict a
// NON_CRITICAL warning additionally ends the run non-zero.
//
// result.json is written on Linux only (see the guard in record_exit_reson), so
// neither "warnings" nor "strict_mode" reaches Windows or macOS.
static void cli_record_warning(sliced_info_t &sliced_info, const std::string &cls,
nlohmann::json details = nlohmann::json::object())
{
details["class"] = cls;
sliced_info.warnings.push_back(std::move(details));
}
void record_exit_reson(std::string outputdir, int code, int plate_id, std::string error_message, sliced_info_t& sliced_info, std::map<std::string, std::string> key_values = std::map<std::string, std::string>())
{
#if defined(__linux__) || defined(__LINUX__)
@@ -462,6 +480,9 @@ void record_exit_reson(std::string outputdir, int code, int plate_id, std::strin
for (auto& iter: key_values)
j[iter.first] = iter.second;
j["warnings"] = sliced_info.warnings;
j["strict_mode"] = sliced_info.strict_mode;
boost::nowide::ofstream c;
c.open(result_file, std::ios::out | std::ios::trunc);
c << j.dump(1, '\t') << std::endl;
@@ -1381,6 +1402,16 @@ int CLI::run(int argc, char **argv)
bool need_skip = (skip_objects.size() > 0)?true:false;
long long global_begin_time = 0, global_current_time;
sliced_info_t sliced_info;
// Read up front so result.json reports it for early failures too.
sliced_info.strict_mode = m_config.opt_bool("strict");
// --no-check skips the check behind the only NON_CRITICAL warning --strict acts on
// (support needed but disabled), from the point it appears among the actions. The pair
// would make --strict a no-op or depend on argument order, so refuse it.
if (sliced_info.strict_mode && m_config.opt_bool("no_check")) {
boost::nowide::cerr << "--strict cannot be combined with --no-check" << std::endl;
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
flush_and_exit(CLI_INVALID_PARAMS);
}
std::map<std::string, std::string> record_key_values;
ConfigOptionBool* downward_check_option = m_config.option<ConfigOptionBool>("downward_check");
@@ -6009,6 +6040,8 @@ int CLI::run(int argc, char **argv)
export_3mf_file = m_config.opt_string(opt_key);
}else if(opt_key=="no_check"){
no_check = m_config.opt_bool(opt_key);
}else if(opt_key=="strict"){
//already read into sliced_info at the start of run()
//} else if (opt_key == "export_gcode" || opt_key == "export_sla" || opt_key == "slice") {
} else if (opt_key == "normative_check") {
//already processed before
@@ -6717,6 +6750,15 @@ int CLI::run(int argc, char **argv)
if (status.warning_level == PrintStateBase::WarningLevel::NON_CRITICAL) {
BOOST_LOG_TRIVIAL(warning) << "plate "<< index+1<< ": found NON_CRITICAL slicing warnings: "<<status.text <<std::endl;
// Always record for AI/CI consumers; under --strict, elevate to a
// non-zero exit so scripted pipelines don't ship a "warning OK" slice.
cli_record_warning(sliced_info, "slicing_warning_non_critical",
nlohmann::json{{"plate_id", index+1}, {"text", status.text}});
if (sliced_info.strict_mode) {
sliced_info.sliced_plates.push_back(sliced_plate_info);
record_exit_reson(outfile_dir, CLI_SLICING_ERROR, index+1, cli_errors[CLI_SLICING_ERROR], sliced_info);
flush_and_exit(CLI_SLICING_ERROR);
}
}
else {
BOOST_LOG_TRIVIAL(warning) << boost::format("plate %1%: found slicing warnings: %2%, no_check=%3%")%(index+1) %status.text %no_check;