mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-12 03:31:57 +00:00
Agents and scripts: Render Image and Check Render Request describe their exact parameters (#13311) (#14827)
* claim: #13311 Co-Authored-By: Claude Code <noreply@anthropic.com> * Render Image and Check Render Request: a test that both describe their parameters (#13311) Fails on main: neither command has a contract, so engine.describe gives no input schema for them. Co-Authored-By: Claude Code <noreply@anthropic.com> * Render Image and Check Render Request: an input contract (#13311) render.start and render.preflight take the same image request, in the GPU form (progressive: true) or the runtime form. The contract makes the handler's checks that look only at the JSON, with its messages, then reads the request with the handler's own serde types (read_request in cmd/render/jobs.rs). The schema publishes both forms; each refuses the other form's keys. The handlers, their messages and their replies don't change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Render Image and Check Render Request: the contract calls the handler's own JSON checks (#13311) The handler's checks that read only the JSON move into two pub(crate) functions in cmd/render/jobs.rs: form_refusal (the other form's keys and values) and saved_job_refusal (setup with settings, an inline job with runtime keys beside it). prepare, prepare_progressive and normalize_saved_job call them as before, and the contract calls the same functions, so the two cannot drift. Strict now also keeps the handler's order for setup with settings, an inline job's runtime keys, and studio weights with the CPU ray tracer. The schema describes the GPU environment by kind with its keys, and an HDR digest as 64 lowercase hex digits (pinned in a test). Descriptions of aging, setup and replace say what the handler does. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -110,7 +110,9 @@ mod parameters;
|
||||
mod plastic;
|
||||
mod print_face;
|
||||
mod render;
|
||||
pub(crate) use render::{PreparedCapture, prepare_capture_with_metadata, render_cancel_params};
|
||||
pub(crate) use render::{
|
||||
PreparedCapture, prepare_capture_with_metadata, read_render_request, render_cancel_params, render_form_refusal, render_saved_job_refusal,
|
||||
};
|
||||
pub(crate) use render_ui::batch_request;
|
||||
mod render_ui;
|
||||
#[cfg(test)]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -153,15 +153,65 @@ struct Normalized {
|
||||
optics: solidcraft_doc::render_optics::OpticalBindings,
|
||||
}
|
||||
|
||||
/// The runtime key beside a saved job, refused: the job already decides the method and its options.
|
||||
fn job_companion_refusal(object: &serde_json::Map<String, Value>) -> Option<String> {
|
||||
let key = object.keys().find(|key| !matches!(key.as_str(), "setup" | "settings" | "replace" | "studio_weights"))?;
|
||||
Some(format!(
|
||||
"saved job already defines the active method and its options; runtime `{key}` cannot override it (use job: null for a runtime request)"
|
||||
))
|
||||
}
|
||||
|
||||
/// What `normalize_saved_job` refuses from the JSON alone, in its order: both `setup` and
|
||||
/// `settings`, then inline `settings` with a `job` that doesn't parse or has a runtime key beside
|
||||
/// it. A named setup's job is in the design, so its companions are checked there. The contract of
|
||||
/// `render.start` and `render.preflight` (`schema::render_start`) calls it too.
|
||||
pub(crate) fn saved_job_refusal(params: &Value) -> Option<String> {
|
||||
let object = params.as_object()?;
|
||||
let inline = object.get("settings").filter(|value| !value.is_null());
|
||||
if object.get("setup").is_some_and(|value| !value.is_null()) && inline.is_some() {
|
||||
return Some("choose either setup or settings".into());
|
||||
}
|
||||
let inline = inline.filter(|value| value.get("job").is_some_and(|job| !job.is_null()))?;
|
||||
if let Err(e) = serde_json::from_value::<RenderSetup>(inline.clone()) {
|
||||
return Some(e.to_string());
|
||||
}
|
||||
job_companion_refusal(object)
|
||||
}
|
||||
|
||||
/// What `prepare` and `prepare_progressive` refuse from the JSON alone before parsing: the runtime
|
||||
/// form (`progressive` not true) refuses the GPU controls, the GPU form the CPU and LED options.
|
||||
/// The contract of `render.start` and `render.preflight` (`schema::render_start`) calls it too.
|
||||
pub(crate) fn form_refusal(params: &Value, progressive: bool) -> Option<&'static str> {
|
||||
let raytrace = params.get("raytrace").and_then(Value::as_object);
|
||||
let environment = params.get("environment");
|
||||
if progressive {
|
||||
let cpu_raytrace = raytrace.is_some_and(|o| o.keys().any(|key| !matches!(key.as_str(), "dielectrics" | "clear_coats" | "textures")));
|
||||
let refused = params.get("integrator").is_some_and(|v| v.as_str() != Some("raster"))
|
||||
|| cpu_raytrace
|
||||
|| params.get("led_strip").is_some()
|
||||
|| params.get("led_samples").is_some()
|
||||
|| environment.is_some_and(|e| e.is_object() && e.get("kind").is_none());
|
||||
return refused.then_some("progressive rendering does not accept CPU raytrace, environment or LED options");
|
||||
}
|
||||
let refused = raytrace.is_some_and(|o| ["clear_coats", "textures"].iter().any(|key| o.contains_key(*key)))
|
||||
|| environment.is_some_and(|e| e.get("kind").is_some())
|
||||
|| ["lens", "lighting", "tone_map", "supersampling", "denoise_settings", "denoise_diagnostics"].iter().any(|key| params.get(*key).is_some())
|
||||
|| params.get("denoise").is_some_and(|value| value != &json!(false))
|
||||
|| params.get("clamp_indirect").is_some_and(|value| !value.is_null());
|
||||
refused.then_some(
|
||||
"runtime GPU transmission, texture, lens, environment, lighting, tone-map, denoise and clamp_indirect controls require progressive rendering",
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_saved_job(s: &Session, params: &Value, command: &str) -> Result<Option<Normalized>> {
|
||||
let Some(object) = params.as_object() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(message) = saved_job_refusal(params) {
|
||||
return Err(bad(command, message));
|
||||
}
|
||||
let named = object.get("setup").filter(|value| !value.is_null());
|
||||
let inline = object.get("settings").filter(|value| !value.is_null());
|
||||
if named.is_some() && inline.is_some() {
|
||||
return Err(bad(command, "choose either setup or settings"));
|
||||
}
|
||||
let mut settings = if let Some(value) = inline {
|
||||
if value.get("job").is_none_or(Value::is_null) {
|
||||
return Ok(None);
|
||||
@@ -175,13 +225,8 @@ fn normalize_saved_job(s: &Session, params: &Value, command: &str) -> Result<Opt
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(key) = object.keys().find(|key| !matches!(key.as_str(), "setup" | "settings" | "replace" | "studio_weights")) {
|
||||
return Err(bad(
|
||||
command,
|
||||
format!(
|
||||
"saved job already defines the active method and its options; runtime `{key}` cannot override it (use job: null for a runtime request)"
|
||||
),
|
||||
));
|
||||
if let Some(message) = job_companion_refusal(object) {
|
||||
return Err(bad(command, message));
|
||||
}
|
||||
settings.validate().map_err(|e| bad(command, e.to_string()))?;
|
||||
s.doc.render.validate_resources(&settings).map_err(|e| bad(command, e.to_string()))?;
|
||||
@@ -259,18 +304,8 @@ pub(crate) fn prepare_with_metadata(
|
||||
retained,
|
||||
);
|
||||
}
|
||||
let gpu_raytrace =
|
||||
params.get("raytrace").and_then(Value::as_object).is_some_and(|o| ["clear_coats", "textures"].iter().any(|key| o.contains_key(*key)));
|
||||
if gpu_raytrace
|
||||
|| params.get("environment").is_some_and(|e| e.get("kind").is_some())
|
||||
|| ["lens", "lighting", "tone_map", "supersampling", "denoise_settings", "denoise_diagnostics"].iter().any(|key| params.get(*key).is_some())
|
||||
|| params.get("denoise").is_some_and(|value| value != &json!(false))
|
||||
|| params.get("clamp_indirect").is_some_and(|value| !value.is_null())
|
||||
{
|
||||
return Err(bad(
|
||||
command,
|
||||
"runtime GPU transmission, texture, lens, environment, lighting, tone-map, denoise and clamp_indirect controls require progressive rendering",
|
||||
));
|
||||
if let Some(message) = form_refusal(params, false) {
|
||||
return Err(bad(command, message));
|
||||
}
|
||||
let mut input: Start = serde_json::from_value(params.clone()).map_err(|e| bad(command, e.to_string()))?;
|
||||
let coats = input.settings.as_mut().map_or_else(Default::default, |settings| std::mem::take(&mut settings.coats));
|
||||
@@ -660,17 +695,8 @@ fn prepare_progressive(
|
||||
cancel: &solidcraft_render::job::CancelToken,
|
||||
retained: Option<&solidcraft_render::job::RetainedMetadata>,
|
||||
) -> Result<Prepared> {
|
||||
let cpu_raytrace = params
|
||||
.get("raytrace")
|
||||
.and_then(Value::as_object)
|
||||
.is_some_and(|o| o.keys().any(|key| !matches!(key.as_str(), "dielectrics" | "clear_coats" | "textures")));
|
||||
if params.get("integrator").is_some_and(|v| v.as_str() != Some("raster"))
|
||||
|| cpu_raytrace
|
||||
|| params.get("led_strip").is_some()
|
||||
|| params.get("led_samples").is_some()
|
||||
|| params.get("environment").is_some_and(|e| e.is_object() && e.get("kind").is_none())
|
||||
{
|
||||
return Err(bad(command, "progressive rendering does not accept CPU raytrace, environment or LED options"));
|
||||
if let Some(message) = form_refusal(params, true) {
|
||||
return Err(bad(command, message));
|
||||
}
|
||||
let mut input: ProgressiveStart = serde_json::from_value(params.clone()).map_err(|e| bad(command, e.to_string()))?;
|
||||
let coats = input.settings.as_mut().map_or_else(Default::default, |settings| std::mem::take(&mut settings.coats));
|
||||
@@ -1118,3 +1144,10 @@ fn validate_studio_weights(weights: Option<[f64; 3]>, command: &str) -> Result<O
|
||||
}
|
||||
Ok(weights)
|
||||
}
|
||||
|
||||
/// Read `params` as `prepare` reads a request: as a `ProgressiveStart` when `progressive`, else as
|
||||
/// a `Start`, with serde's message. The contract of `render.start` and `render.preflight`
|
||||
/// (`schema::render_start`) calls it, so it accepts what these readers accept.
|
||||
pub(crate) fn read_request(params: Value, progressive: bool) -> serde_json::Result<()> {
|
||||
if progressive { serde_json::from_value::<ProgressiveStart>(params).map(drop) } else { serde_json::from_value::<Start>(params).map(drop) }
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ pub mod render_presets;
|
||||
pub mod render_settings;
|
||||
pub mod render_shots;
|
||||
pub mod render_stage;
|
||||
pub mod render_start;
|
||||
pub mod render_surface_resources;
|
||||
pub mod render_ui;
|
||||
pub mod save;
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
//! The contract for Render Image and Check Render Request in `cmd/render/jobs.rs` (#13311). Both
|
||||
//! commands take the same image request; `render.preflight` only checks it.
|
||||
//!
|
||||
//! The handler reads a request in one of two forms. With `progressive: true` it is the GPU form
|
||||
//! (`ProgressiveStart`); otherwise it is the runtime form (`Start`), for the raster and CPU
|
||||
//! raytrace image executors. Before parsing, the handler refuses `setup` with `settings`, an
|
||||
//! inline job with runtime keys beside it, and the keys and values of the other form. The contract
|
||||
//! calls the handler's own functions for these ([`crate::cmd::render_saved_job_refusal`],
|
||||
//! [`crate::cmd::render_form_refusal`]), then parses the request with the handler's serde types
|
||||
//! ([`crate::cmd::read_render_request`]), passing serde's message on whole. So strict refuses what
|
||||
//! the handler refuses before it looks at the design, with today's message, and only an unknown
|
||||
//! key is worded by the check. What the handler checks later (ranges, the saved setup, the host)
|
||||
//! stays the handler's.
|
||||
//!
|
||||
//! Strict words two kinds of call differently from the handler, which still refuses them: a named
|
||||
//! `setup` whose saved job is in the design, with a runtime key beside it (the handler names the
|
||||
//! key, strict the key's own problem, such as sampling without `progressive`), and a request
|
||||
//! wrong in two ways where one is an optics or clear coat binding in `settings` (the handler
|
||||
//! names the binding first).
|
||||
//!
|
||||
//! The render crate has no `JsonSchema`. The schema types each top-level key and publishes the
|
||||
//! render crate's option sections (`raytrace`, `settings`, `lens` and the others) as JSON objects.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use super::check::whole;
|
||||
|
||||
/// The keys only the GPU form takes. The runtime form refuses them.
|
||||
pub(crate) const GPU_ONLY: &[&str] = &[
|
||||
"adaptive",
|
||||
"backdrop",
|
||||
"caustics",
|
||||
"denoise_diagnostics",
|
||||
"denoise_settings",
|
||||
"finishing",
|
||||
"lens",
|
||||
"lighting",
|
||||
"linear_exr",
|
||||
"metal_energy_compensation",
|
||||
"nested_media",
|
||||
"supersampling",
|
||||
"texture_filtering",
|
||||
"tone_map",
|
||||
"transmission_samples",
|
||||
"volume",
|
||||
];
|
||||
|
||||
/// The keys only the runtime form takes. The GPU form refuses them.
|
||||
pub(crate) const RUNTIME_ONLY: &[&str] = &["led_samples", "led_strip"];
|
||||
|
||||
/// An HDR's SHA-256 as the handler reads one (`HdrId`): 64 lowercase hex digits.
|
||||
pub(crate) const DIGEST_PATTERN: &str = "^[0-9a-f]{64}$";
|
||||
|
||||
const WEIGHTS_MESSAGE: &str = "studio visibility weights must be finite in 0..1";
|
||||
const RAYTRACE_WEIGHTS_MESSAGE: &str = "studio_weights apply to raster or the implicit GPU studio; use explicit CPU light intensities";
|
||||
const SAMPLING_MESSAGE: &str = "sampling controls require progressive rendering";
|
||||
|
||||
/// Render Image (`render.start`) and Check Render Request (`render.preflight`): an image request.
|
||||
/// Nothing is read from it here; the handler reads the parameters itself.
|
||||
#[derive(Debug)]
|
||||
pub struct RenderRequest;
|
||||
|
||||
impl<'de> Deserialize<'de> for RenderRequest {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let params = Value::deserialize(d)?;
|
||||
read(¶ms).map_err(|message| D::Error::custom(whole(message)))?;
|
||||
Ok(RenderRequest)
|
||||
}
|
||||
}
|
||||
|
||||
/// What `prepare` in `cmd/render/jobs.rs` refuses in `params` before it looks at the design, in its
|
||||
/// order and with its messages.
|
||||
fn read(params: &Value) -> Result<(), String> {
|
||||
if let Some(message) = crate::cmd::render_saved_job_refusal(params) {
|
||||
return Err(message);
|
||||
}
|
||||
let progressive = params.get("progressive").and_then(Value::as_bool) == Some(true);
|
||||
if let Some(message) = crate::cmd::render_form_refusal(params, progressive) {
|
||||
return Err(message.into());
|
||||
}
|
||||
crate::cmd::read_render_request(params.clone(), progressive).map_err(|e| e.to_string())?;
|
||||
// Parsed: `studio_weights` is three numbers or null, `integrator` is raster or raytrace, and
|
||||
// `samples`, `max_depth` and `seed` are whole numbers or null.
|
||||
let set = |key: &str| params.get(key).is_some_and(|v| !v.is_null());
|
||||
let weights = params.get("studio_weights").and_then(Value::as_array).into_iter().flatten();
|
||||
if weights.filter_map(Value::as_f64).any(|w| !(0.0..=1.0).contains(&w)) {
|
||||
return Err(WEIGHTS_MESSAGE.into());
|
||||
}
|
||||
if progressive {
|
||||
return Ok(());
|
||||
}
|
||||
if params.get("integrator").and_then(Value::as_str) == Some("raytrace") && set("studio_weights") {
|
||||
return Err(RAYTRACE_WEIGHTS_MESSAGE.into());
|
||||
}
|
||||
if ["samples", "max_depth", "seed"].into_iter().any(set) {
|
||||
return Err(SAMPLING_MESSAGE.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A whole number the handler reads as a `u32`, or `null` for its default.
|
||||
fn count(description: &str) -> Value {
|
||||
json!({"description": description, "type": ["integer", "null"], "minimum": 0, "maximum": u32::MAX})
|
||||
}
|
||||
|
||||
/// A section of options the render crate reads as an object, or `null` for none.
|
||||
fn section(description: &str) -> Value {
|
||||
json!({"description": description, "type": ["object", "null"]})
|
||||
}
|
||||
|
||||
/// A switch, false by default.
|
||||
fn switch(description: &str) -> Value {
|
||||
json!({"description": description, "type": "boolean", "default": false})
|
||||
}
|
||||
|
||||
/// `{"type": "object", "required": [key]}` for each key: with `not`, the keys an object may not have.
|
||||
fn any_of_keys(keys: &[&str]) -> Value {
|
||||
json!({"anyOf": keys.iter().map(|key| json!({"type": "object", "required": [key]})).collect::<Vec<_>>()})
|
||||
}
|
||||
|
||||
impl JsonSchema for RenderRequest {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"RenderRequest".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
let mut properties = Map::new();
|
||||
let mut put = |key: &str, schema: Value| properties.insert(key.into(), schema);
|
||||
put(
|
||||
"setup",
|
||||
json!({"description": "A saved render setup, by name. A setup with a saved `job` decides the method and its \
|
||||
options, and only `replace` and `studio_weights` may come with it (`studio_weights` only where its method takes \
|
||||
them). Not with `settings`.", "type": ["string", "null"]}),
|
||||
);
|
||||
put(
|
||||
"settings",
|
||||
section(
|
||||
"A whole render setup, as `render.settings.get` gives one, used without saving it; a setup with \
|
||||
a `job` as for `setup`. Not with `setup`. Without either: the default setup, seen from the current camera or fitted \
|
||||
to the product.",
|
||||
),
|
||||
);
|
||||
put(
|
||||
"replace",
|
||||
switch(
|
||||
"Render Image: cancel the design's active and waiting render jobs before this one starts. Check Render \
|
||||
Request ignores it. Default: false.",
|
||||
),
|
||||
);
|
||||
put("transparent_background", switch("Leave the background transparent. Default: false."));
|
||||
put(
|
||||
"studio_weights",
|
||||
json!({"description": "Three visibility weights, each from 0 to 1, for the studio's sources: with the \
|
||||
raster executor, or on the GPU with its implicit studio (no `lighting` and no environment). Never saved.",
|
||||
"type": ["array", "null"], "items": {"type": "number", "minimum": 0, "maximum": 1}, "minItems": 3, "maxItems": 3}),
|
||||
);
|
||||
put(
|
||||
"progressive",
|
||||
json!({"description": "`true`: the GPU form, rendered progressively by the GPU path tracer; the keys \
|
||||
marked GPU apply. `false` or left out: the runtime form, for the raster executor or, with `integrator: raytrace`, \
|
||||
the CPU ray tracer. Default: false.", "type": "boolean", "default": false}),
|
||||
);
|
||||
put(
|
||||
"integrator",
|
||||
json!({"description": "`raster` or `raytrace` (CPU ray tracing, in a build with it). With `progressive: \
|
||||
true`, only `raster`. Default: raster.", "type": "string", "enum": ["raster", "raytrace"], "default": "raster"}),
|
||||
);
|
||||
put(
|
||||
"raytrace",
|
||||
section(
|
||||
"Without `progressive`: the CPU ray tracer's options (lights, reflections, `path`, `progressive` \
|
||||
sampling, `output`, `budget` and others; see docs/render-jobs.md), with `integrator: raytrace`. With `progressive: \
|
||||
true`: the GPU's `dielectrics`, `clear_coats` and `textures` overrides, each a list.",
|
||||
),
|
||||
);
|
||||
put(
|
||||
"environment",
|
||||
section(
|
||||
"Without `progressive`: a CPU HDR environment, with `integrator: raytrace`: `{path, \
|
||||
expected_sha256?, rotation_radians?, intensity?, samples?}` for a file or `{resource, rotation_radians?, intensity?, \
|
||||
samples?}` for an HDR saved in the design. With `progressive: true`: a GPU environment, by `kind`: \
|
||||
`{kind: builtin_hdr, id, rotation_rad?, intensity?}`, `{kind: hdr, path, expected_sha256?, rotation_rad?, \
|
||||
intensity?, assume_linear_srgb?}`, `{kind: embedded_hdr, resource, rotation_rad?, intensity?, assume_linear_srgb?}` \
|
||||
or `{kind: sky, gradient?, rotation_rad?, intensity?}`. An HDR's `resource` and `expected_sha256` are its SHA-256, \
|
||||
64 lowercase hex digits. Default: none.",
|
||||
),
|
||||
);
|
||||
put(
|
||||
"aging",
|
||||
json!({"description": "Aging layers for the bodies, at most 8: with `integrator: raytrace` on the CPU, or on \
|
||||
the GPU with `progressive: true`; the raster executor refuses them. Default: none.",
|
||||
"type": "array", "items": {"type": "object"}, "maxItems": 8}),
|
||||
);
|
||||
put(
|
||||
"led_strip",
|
||||
section(
|
||||
"CPU only, with `integrator: raytrace`: a continuous LED strip, a rectangle in world mm \
|
||||
(`center_mm`, `length_mm`, `width_mm`, `length_axis`, `width_axis`, `color`, `intensity`), lit as area lights.",
|
||||
),
|
||||
);
|
||||
put("led_samples", count("CPU only: the samples for the whole LED strip: 1, 4, 16 or 64. Needs `led_strip`. Default: 16."));
|
||||
put("denoise", switch("GPU: denoise the image. Without `progressive: true`, only `false`. Default: false."));
|
||||
put(
|
||||
"clamp_indirect",
|
||||
json!({"description": "GPU: cap each contribution from the first bounce on at this value, from \
|
||||
0.000001 to 1000000, keeping its hue. Biased. Without `progressive: true`, only `null`. Default: none.",
|
||||
"type": ["number", "null"]}),
|
||||
);
|
||||
put("samples", count("GPU: samples per pixel, from 1 to 65536. Without `progressive: true`, only `null`."));
|
||||
put("max_depth", count("GPU: the most bounces, from 1 to 64. Without `progressive: true`, only `null`."));
|
||||
put("seed", count("GPU: the random seed. Without `progressive: true`, only `null`."));
|
||||
put("supersampling", count("GPU: 1 or 2. At 2 the image is traced at twice the width and height and averaged down. Default: 1."));
|
||||
put(
|
||||
"denoise_settings",
|
||||
json!({"description": "GPU: the denoiser's `passes` (1 to 3), `strength` (0 to 1), \
|
||||
`firefly_suppression` and `optical_filter`.", "type": "object"}),
|
||||
);
|
||||
put("denoise_diagnostics", switch("GPU: keep the denoiser's guide mask, for `render.export` with `format: denoise_mask`. Default: false."));
|
||||
put(
|
||||
"caustics",
|
||||
section(
|
||||
"GPU: photon caustics: `photons`, `radius_mm`, `max_depth`, `seed` and the optional `area_lights`, \
|
||||
`environment`, `guided_emission` and `refresh_per_pass`. Default: none.",
|
||||
),
|
||||
);
|
||||
put("nested_media", switch("GPU: trace strictly nested solids (glass in glass, an air bubble) as nested media. Default: false."));
|
||||
put(
|
||||
"texture_filtering",
|
||||
switch(
|
||||
"GPU: filter surface maps by the ray's footprint (mip levels and bounded anisotropy). \
|
||||
Default: false, which samples the finest level bilinearly.",
|
||||
),
|
||||
);
|
||||
put("metal_energy_compensation", switch("GPU: restore the energy rough isotropic metals lose. Default: false."));
|
||||
put(
|
||||
"volume",
|
||||
section(
|
||||
"GPU: one homogeneous medium: `min_mm`, `max_mm`, `extinction_per_mm` (0 to 1000), \
|
||||
`scattering_albedo` [r, g, b] and `anisotropy` (-0.95 to 0.95). Default: none.",
|
||||
),
|
||||
);
|
||||
put(
|
||||
"adaptive",
|
||||
section(
|
||||
"GPU: adaptive sampling: `pilot_samples`, `min_samples`, `relative_error`, `absolute_error` and \
|
||||
`coverage_error`. Default: none, the same samples for every pixel.",
|
||||
),
|
||||
);
|
||||
put("transmission_samples", count("GPU: 1, 2 or 4 strata for the first Fresnel branch. Default: 1."));
|
||||
put(
|
||||
"finishing",
|
||||
json!({"description": "GPU: creative finishing, as Regrade Linear Master's `presentation` takes it: \
|
||||
`looks`, `photographic`, `bloom`, `rgb_gain`, `contrast`, `saturation` and `vignette`. Default: none.",
|
||||
"type": "object"}),
|
||||
);
|
||||
put("backdrop", section("GPU: a gradient behind the product, as Regrade Linear Master's `presentation` takes it. Default: none."));
|
||||
put("linear_exr", switch("GPU: keep the raw linear FLOAT32 master, for `render.export` with `format: exr`. Default: false."));
|
||||
put("lens", section("GPU: a thin lens, `{aperture_radius_mm, focus_distance_mm}`. Default: none, a pinhole camera."));
|
||||
put("lighting", section("GPU: studio lighting in place of the implicit studio. Default: none."));
|
||||
put(
|
||||
"tone_map",
|
||||
json!({"description": "GPU: the tone response, `reinhard` or `aces_fitted`. Default: reinhard.",
|
||||
"type": ["string", "null"], "enum": ["reinhard", "aces_fitted", null]}),
|
||||
);
|
||||
|
||||
let null_only = json!({"type": "null"});
|
||||
let digest = json!({"type": "string", "pattern": DIGEST_PATTERN});
|
||||
let digest_or_null = json!({"type": ["string", "null"], "pattern": DIGEST_PATTERN});
|
||||
let runtime = json!({
|
||||
"properties": {
|
||||
"progressive": {"enum": [false]},
|
||||
"raytrace": {"not": any_of_keys(&["clear_coats", "textures"])},
|
||||
"environment": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"resource": digest_or_null,
|
||||
"expected_sha256": digest_or_null,
|
||||
"rotation_radians": {"type": "number"},
|
||||
"intensity": {"type": "number"},
|
||||
"samples": {"type": "integer", "minimum": 0, "maximum": u32::MAX},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
"denoise": {"enum": [false]},
|
||||
"clamp_indirect": null_only,
|
||||
"samples": null_only,
|
||||
"max_depth": null_only,
|
||||
"seed": null_only,
|
||||
},
|
||||
"not": any_of_keys(GPU_ONLY),
|
||||
});
|
||||
let array = json!({"type": "array"});
|
||||
// The GPU environment, as `cmd/render/environment.rs` reads one: by `kind`, each with its keys.
|
||||
let gpu_environment = |kind: &str, required: &[&str], keys: Value| {
|
||||
let mut properties =
|
||||
json!({"kind": {"type": "string", "enum": [kind]}, "rotation_rad": {"type": "number"}, "intensity": {"type": "number"}});
|
||||
if let (Some(all), Some(keys)) = (properties.as_object_mut(), keys.as_object()) {
|
||||
all.extend(keys.clone());
|
||||
}
|
||||
let required: Vec<&str> = std::iter::once("kind").chain(required.iter().copied()).collect();
|
||||
json!({"type": "object", "required": required, "properties": properties, "additionalProperties": false})
|
||||
};
|
||||
let linear = json!({"type": "boolean"});
|
||||
let rgb = json!({"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3});
|
||||
let gpu_environments = json!([
|
||||
{"type": "null"},
|
||||
gpu_environment("builtin_hdr", &["id"], json!({"id": {"type": "string"}})),
|
||||
gpu_environment("hdr", &["path"], json!({"path": {"type": "string"}, "expected_sha256": digest_or_null, "assume_linear_srgb": linear})),
|
||||
gpu_environment("embedded_hdr", &["resource"], json!({"resource": digest, "assume_linear_srgb": linear})),
|
||||
gpu_environment(
|
||||
"sky",
|
||||
&[],
|
||||
json!({"gradient": {"type": "object", "properties": {"zenith": rgb, "horizon": rgb, "ground": rgb}, "additionalProperties": false}}),
|
||||
),
|
||||
]);
|
||||
let gpu = json!({
|
||||
"required": ["progressive"],
|
||||
"properties": {
|
||||
"progressive": {"enum": [true]},
|
||||
"integrator": {"enum": ["raster"]},
|
||||
"raytrace": {
|
||||
"type": "object",
|
||||
"properties": {"dielectrics": array, "clear_coats": array, "textures": array},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
"environment": {"anyOf": gpu_environments},
|
||||
},
|
||||
"not": any_of_keys(RUNTIME_ONLY),
|
||||
});
|
||||
json_schema!({
|
||||
"description": "An image request. With `progressive: true` it is the GPU form, without it the runtime form; each \
|
||||
refuses the other form's keys. Render Image captures the product and queues a job; Check Render Request checks \
|
||||
the request and estimates its cost without queuing anything. Neither changes the design.",
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"additionalProperties": false,
|
||||
"anyOf": [runtime, gpu],
|
||||
"not": {"required": ["setup", "settings"], "properties": {"setup": {"not": null_only}, "settings": {"not": null_only}}},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "render_start_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,315 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_render::job::CpuHost;
|
||||
|
||||
use super::{GPU_ONLY, RUNTIME_ONLY, RenderRequest};
|
||||
use crate::Session;
|
||||
use crate::schema::check::schema_keys;
|
||||
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
|
||||
use crate::schema::{ParamCheck, input_schema_for};
|
||||
|
||||
const IDS: [&str; 2] = ["render.start", "render.preflight"];
|
||||
|
||||
/// #13311: `engine.describe` gives Render Image and Check Render Request their exact parameters.
|
||||
#[test]
|
||||
fn both_commands_describe_their_parameters() {
|
||||
let mut s = Session::default();
|
||||
for id in IDS {
|
||||
let described = s.execute("engine.describe", &json!({"command": id})).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert!(described["input"].is_object(), "{id}: {described}");
|
||||
assert_eq!(described["input"].get("additionalProperties"), Some(&Value::Bool(false)), "{id}: {described}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The fields of the handler's reader for one form, from serde's message for an unknown key,
|
||||
/// which lists them all.
|
||||
fn handler_fields(progressive: bool) -> BTreeSet<String> {
|
||||
let error = crate::cmd::read_render_request(json!({"\u{1}": 0}), progressive).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
let listed = error.split_once("expected one of ").map(|(_, fields)| fields).unwrap_or_default();
|
||||
let fields: BTreeSet<String> = listed.split(", ").map(|field| field.trim_matches('`').to_string()).collect();
|
||||
assert!(fields.len() > 10, "{error}");
|
||||
fields
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schemas_and_the_key_check_agree() {
|
||||
assert_keys(
|
||||
input_schema_for::<RenderRequest>(),
|
||||
&[
|
||||
"adaptive",
|
||||
"aging",
|
||||
"backdrop",
|
||||
"caustics",
|
||||
"clamp_indirect",
|
||||
"denoise",
|
||||
"denoise_diagnostics",
|
||||
"denoise_settings",
|
||||
"environment",
|
||||
"finishing",
|
||||
"integrator",
|
||||
"led_samples",
|
||||
"led_strip",
|
||||
"lens",
|
||||
"lighting",
|
||||
"linear_exr",
|
||||
"max_depth",
|
||||
"metal_energy_compensation",
|
||||
"nested_media",
|
||||
"progressive",
|
||||
"raytrace",
|
||||
"replace",
|
||||
"samples",
|
||||
"seed",
|
||||
"settings",
|
||||
"setup",
|
||||
"studio_weights",
|
||||
"supersampling",
|
||||
"texture_filtering",
|
||||
"tone_map",
|
||||
"transmission_samples",
|
||||
"transparent_background",
|
||||
"volume",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// #13311: the contract's keys are the fields the handler reads, in either form, and each form's
|
||||
/// schema refuses the keys only the other form reads. A field added to the handler's `Start` or
|
||||
/// `ProgressiveStart` fails this test until the contract declares it.
|
||||
#[test]
|
||||
fn the_keys_are_the_fields_the_handler_reads() {
|
||||
let (runtime, gpu) = (handler_fields(false), handler_fields(true));
|
||||
let schema = input_schema_for::<RenderRequest>();
|
||||
let schema = schema.as_value();
|
||||
assert_eq!(schema_keys(schema), runtime.union(&gpu).cloned().collect::<BTreeSet<_>>());
|
||||
let set = |keys: &[&str]| keys.iter().map(|key| key.to_string()).collect::<BTreeSet<_>>();
|
||||
assert_eq!(set(GPU_ONLY), gpu.difference(&runtime).cloned().collect::<BTreeSet<_>>());
|
||||
assert_eq!(set(RUNTIME_ONLY), runtime.difference(&gpu).cloned().collect::<BTreeSet<_>>());
|
||||
let refused = |form: &Value| -> BTreeSet<String> {
|
||||
let keys = form["not"]["anyOf"].as_array().into_iter().flatten().filter_map(|k| k["required"][0].as_str());
|
||||
keys.map(str::to_string).collect()
|
||||
};
|
||||
assert_eq!(refused(&schema["anyOf"][0]), set(GPU_ONLY));
|
||||
assert_eq!(refused(&schema["anyOf"][1]), set(RUNTIME_ONLY));
|
||||
}
|
||||
|
||||
/// #13311: a key of the other form gets the handler's message in strict: its own refusal for the
|
||||
/// GPU controls and the LED options, serde's for the others.
|
||||
#[test]
|
||||
fn a_key_of_the_other_form_keeps_todays_error() {
|
||||
let mut cases: Vec<(&str, Value)> = GPU_ONLY.iter().map(|key| ("render.preflight", json!({*key: null}))).collect();
|
||||
cases.extend(RUNTIME_ONLY.iter().map(|key| ("render.preflight", json!({"progressive": true, *key: null}))));
|
||||
assert_strict_keeps_todays_errors(&cases);
|
||||
}
|
||||
|
||||
/// #13311: calls that fail today get the same message in strict: the handler's own refusals of
|
||||
/// the other form's values, a studio weight out of range and sampling controls without
|
||||
/// `progressive`, and serde's message for a value of the wrong shape (the check passes it on
|
||||
/// whole).
|
||||
#[test]
|
||||
fn strict_keeps_todays_errors() {
|
||||
let mut cases = Vec::new();
|
||||
for id in IDS {
|
||||
cases.extend([
|
||||
(id, json!({"progressive": "yes"})),
|
||||
(id, json!({"progressive": null})),
|
||||
(id, json!({"integrator": "gpu"})),
|
||||
(id, json!({"samples": 4})),
|
||||
(id, json!({"seed": -1})),
|
||||
(id, json!({"denoise": true})),
|
||||
(id, json!({"denoise": null})),
|
||||
(id, json!({"clamp_indirect": 2})),
|
||||
(id, json!({"raytrace": {"textures": []}})),
|
||||
(id, json!({"raytrace": {"no_such_option": 1}})),
|
||||
(id, json!({"environment": {"kind": "sky"}})),
|
||||
(id, json!({"environment": {"path": 5}})),
|
||||
(id, json!({"studio_weights": [0, 0.5, 2]})),
|
||||
(id, json!({"studio_weights": [0, 0.5]})),
|
||||
(id, json!({"setup": 5})),
|
||||
(id, json!({"settings": {"width": "wide"}})),
|
||||
(id, json!({"replace": "no"})),
|
||||
(id, json!({"aging": {}})),
|
||||
(id, json!({"led_samples": 1.5})),
|
||||
(id, json!({"progressive": true, "integrator": "raytrace"})),
|
||||
(id, json!({"progressive": true, "integrator": null})),
|
||||
(id, json!({"progressive": true, "raytrace": {"lights": []}})),
|
||||
(id, json!({"progressive": true, "raytrace": null})),
|
||||
(id, json!({"progressive": true, "environment": {"path": "studio.hdr"}})),
|
||||
(id, json!({"progressive": true, "environment": {"kind": "moon"}})),
|
||||
(id, json!({"progressive": true, "tone_map": "filmic"})),
|
||||
(id, json!({"progressive": true, "supersampling": -1})),
|
||||
(id, json!({"progressive": true, "studio_weights": [2, 0, 0]})),
|
||||
(id, json!({"progressive": true, "finishing": null})),
|
||||
(id, json!({"progressive": true, "denoise_settings": 3})),
|
||||
(id, json!({"progressive": true, "lens": [1, 2]})),
|
||||
(id, json!({"progressive": true, "settings": 5})),
|
||||
(id, json!({"progressive": true, "environment": {"kind": "builtin_hdr"}})),
|
||||
(id, json!({"progressive": true, "environment": {"kind": "sky", "assume_linear_srgb": true}})),
|
||||
(id, json!({"integrator": "raytrace", "environment": {"resource": "ABC"}})),
|
||||
]);
|
||||
}
|
||||
assert_strict_keeps_todays_errors(&cases);
|
||||
}
|
||||
|
||||
/// #13311: a call wrong in two ways gets the handler's first message in strict: `setup` with
|
||||
/// `settings`, and a runtime key beside an inline job, come before the form's own checks, and
|
||||
/// studio weights with the CPU ray tracer before sampling controls.
|
||||
#[test]
|
||||
fn strict_keeps_the_handlers_order() {
|
||||
let mut cases = Vec::new();
|
||||
for id in IDS {
|
||||
cases.extend([
|
||||
(id, json!({"setup": "Studio", "settings": {}, "samples": 4})),
|
||||
(id, json!({"setup": "Studio", "settings": {}, "progressive": true, "led_samples": 4})),
|
||||
(id, json!({"settings": {"job": {"method": "raster"}}, "samples": 4})),
|
||||
(id, json!({"settings": {"job": {"method": "raster"}}, "lens": null})),
|
||||
(id, json!({"settings": {"job": {"method": "raster"}}, "progressive": true})),
|
||||
(id, json!({"settings": {"job": {"method": "moon"}}, "samples": 4})),
|
||||
(id, json!({"integrator": "raytrace", "studio_weights": [1, 1, 1], "samples": 4})),
|
||||
]);
|
||||
}
|
||||
assert_strict_keeps_todays_errors(&cases);
|
||||
}
|
||||
|
||||
/// #13311: an HDR's `resource` and `expected_sha256` are 64 lowercase hex digits, as the
|
||||
/// handler's `HdrId` reads them. `conforms` doesn't read patterns, so the pattern's text is
|
||||
/// pinned here, in both forms' environments.
|
||||
#[test]
|
||||
fn an_hdr_digest_is_64_lowercase_hex_digits() {
|
||||
assert_eq!(super::DIGEST_PATTERN, "^[0-9a-f]{64}$");
|
||||
for (digest, takes) in [("0a".repeat(32), true), ("0A".repeat(32), false), ("0a".repeat(31), false), ("0g".repeat(32), false)] {
|
||||
let runtime = json!({"integrator": "raytrace", "environment": {"resource": digest}});
|
||||
let gpu = json!({"progressive": true, "environment": {"kind": "embedded_hdr", "resource": digest}});
|
||||
for (p, progressive) in [(runtime, false), (gpu, true)] {
|
||||
assert_eq!(crate::cmd::read_render_request(p.clone(), progressive).is_ok(), takes, "{p}");
|
||||
assert_eq!(crate::schema::issues("render.start", &p).is_empty(), takes, "{p}");
|
||||
}
|
||||
}
|
||||
let schema = input_schema_for::<RenderRequest>();
|
||||
let schema = schema.as_value();
|
||||
let runtime = &schema["anyOf"][0]["properties"]["environment"]["properties"];
|
||||
assert_eq!(runtime["resource"]["pattern"], super::DIGEST_PATTERN);
|
||||
assert_eq!(runtime["expected_sha256"]["pattern"], super::DIGEST_PATTERN);
|
||||
let gpu = schema["anyOf"][1]["properties"]["environment"]["anyOf"].as_array().cloned().unwrap_or_default();
|
||||
let patterns: Vec<&Value> = gpu.iter().flat_map(|form| [&form["properties"]["resource"], &form["properties"]["expected_sha256"]]).collect();
|
||||
assert_eq!(patterns.iter().filter(|key| key["pattern"] == super::DIGEST_PATTERN).count(), 2, "{gpu:?}");
|
||||
}
|
||||
|
||||
/// #13311: an unknown key fails in every mode, as the handler's serde structs refuse it: with
|
||||
/// serde's message with the check off, and in strict with the check's, which suggests a key.
|
||||
#[test]
|
||||
fn an_unknown_key_is_refused_in_every_mode() {
|
||||
for id in IDS {
|
||||
for p in [json!({"sampels": 4}), json!({"progressive": true, "sampels": 4})] {
|
||||
let error = |level: ParamCheck| Session { param_check: level, ..Session::default() }.execute(id, &p).err().map(|e| e.to_string());
|
||||
assert!(error(ParamCheck::Off).is_some_and(|e| e.contains("unknown field `sampels`")), "{id} {p}: {:?}", error(ParamCheck::Off));
|
||||
assert!(error(ParamCheck::Strict).is_some_and(|e| e.contains("did you mean `samples`?")), "{id} {p}: {:?}", error(ParamCheck::Strict));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #13311: the contract takes what the input schema allows: each call's check finds no issue
|
||||
/// exactly when it matches the schema. (What the handler refuses after parsing, such as 0
|
||||
/// samples or a setup that doesn't exist, both allow.)
|
||||
#[test]
|
||||
fn the_check_takes_what_the_schema_allows() {
|
||||
let cases = [
|
||||
(json!({}), true),
|
||||
(json!({"setup": "Studio", "replace": true, "studio_weights": [1, 0.5, 0]}), true),
|
||||
(json!({"settings": {"width": 16, "height": 16}, "transparent_background": true}), true),
|
||||
(json!({"setup": null, "settings": null, "studio_weights": null}), true),
|
||||
(json!({"progressive": false, "integrator": "raytrace", "raytrace": {}, "aging": []}), true),
|
||||
(json!({"integrator": "raytrace", "environment": {"path": "studio.hdr", "rotation_radians": 0.5, "samples": 64}}), true),
|
||||
(json!({"integrator": "raytrace", "led_strip": null, "led_samples": 16}), true),
|
||||
(json!({"denoise": false, "clamp_indirect": null, "samples": null, "max_depth": null, "seed": null, "environment": null}), true),
|
||||
(json!({"settings": {"width": 16, "height": 16}, "samples": 0}), false),
|
||||
(json!({"progressive": "yes"}), false),
|
||||
(json!({"progressive": null}), false),
|
||||
(json!({"integrator": "gpu"}), false),
|
||||
(json!({"samples": 4}), false),
|
||||
(json!({"denoise": true}), false),
|
||||
(json!({"clamp_indirect": 2}), false),
|
||||
(json!({"raytrace": {"textures": []}}), false),
|
||||
(json!({"environment": {"kind": "sky"}}), false),
|
||||
(json!({"environment": {"path": 5}}), false),
|
||||
(json!({"studio_weights": [0, 0.5, 2]}), false),
|
||||
(json!({"studio_weights": [0, 0.5]}), false),
|
||||
(json!({"replace": "no"}), false),
|
||||
(json!({"lens": null}), false),
|
||||
(json!({"finishing": {}}), false),
|
||||
(json!({"tone_map": "reinhard"}), false),
|
||||
(
|
||||
json!({"progressive": true, "integrator": "raster", "samples": 64, "max_depth": 8, "seed": 3, "denoise": true,
|
||||
"clamp_indirect": 4.5, "tone_map": "aces_fitted", "supersampling": 2, "transmission_samples": 4,
|
||||
"raytrace": {"dielectrics": [], "clear_coats": [], "textures": []}, "environment": null, "finishing": {},
|
||||
"denoise_settings": {}, "denoise_diagnostics": true, "linear_exr": true, "nested_media": true,
|
||||
"texture_filtering": true, "metal_energy_compensation": true, "lens": null, "lighting": null, "caustics": null,
|
||||
"volume": null, "adaptive": null, "backdrop": null, "aging": [], "studio_weights": [0, 0, 1]}),
|
||||
true,
|
||||
),
|
||||
(json!({"setup": "Studio", "settings": {"width": 16}}), false),
|
||||
(json!({"setup": "Studio", "settings": null}), true),
|
||||
(json!({"progressive": true, "environment": {"kind": "sky"}}), true),
|
||||
(json!({"progressive": true, "environment": {"kind": "sky", "gradient": {"zenith": [0, 0, 1]}, "intensity": 2}}), true),
|
||||
(json!({"progressive": true, "environment": {"kind": "sky", "assume_linear_srgb": true}}), false),
|
||||
(json!({"progressive": true, "environment": {"kind": "builtin_hdr", "id": "studio", "rotation_rad": 0.5}}), true),
|
||||
(json!({"progressive": true, "environment": {"kind": "builtin_hdr"}}), false),
|
||||
(json!({"progressive": true, "environment": {"kind": "hdr", "path": "a.hdr", "expected_sha256": null, "assume_linear_srgb": true}}), true),
|
||||
(json!({"progressive": true, "environment": {"kind": "hdr"}}), false),
|
||||
(json!({"progressive": true, "environment": {"kind": "embedded_hdr", "resource": "0a".repeat(32)}}), true),
|
||||
(json!({"progressive": true, "environment": {"kind": "embedded_hdr", "path": "a.hdr"}}), false),
|
||||
(json!({"progressive": true, "environment": null}), true),
|
||||
(json!({"progressive": true, "samples": 0, "max_depth": 0}), true),
|
||||
(json!({"progressive": true, "tone_map": null, "settings": {"width": 16}}), true),
|
||||
(json!({"progressive": true, "integrator": "raytrace"}), false),
|
||||
(json!({"progressive": true, "integrator": null}), false),
|
||||
(json!({"progressive": true, "raytrace": {"lights": []}}), false),
|
||||
(json!({"progressive": true, "raytrace": null}), false),
|
||||
(json!({"progressive": true, "environment": {"path": "studio.hdr"}}), false),
|
||||
(json!({"progressive": true, "led_strip": null}), false),
|
||||
(json!({"progressive": true, "led_samples": 16}), false),
|
||||
(json!({"progressive": true, "tone_map": "filmic"}), false),
|
||||
(json!({"progressive": true, "supersampling": -1}), false),
|
||||
(json!({"progressive": true, "studio_weights": [2, 0, 0]}), false),
|
||||
(json!({"progressive": true, "finishing": null}), false),
|
||||
(json!({"progressive": true, "denoise_settings": 3}), false),
|
||||
];
|
||||
let schema = input_schema_for::<RenderRequest>();
|
||||
let schema = schema.as_value();
|
||||
for id in IDS {
|
||||
for (p, takes) in &cases {
|
||||
assert_eq!(crate::schema::issues(id, p).is_empty(), *takes, "{id} {p}: {:?}", crate::schema::issues(id, p));
|
||||
assert_eq!(conforms(schema, schema, p), *takes, "{id} {p}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #13311: in Warn, a request the contract takes runs as it did with the check off and gets no
|
||||
/// warnings: Check Render Request's reply is the same, and Render Image queues a job. Among them
|
||||
/// a saved setup with a job and an inline one, which the handler lowers itself.
|
||||
#[test]
|
||||
fn requests_the_contract_takes_run_as_before() {
|
||||
let session = |level: ParamCheck| {
|
||||
let mut s = Session { param_check: level, ..Session::default() };
|
||||
s.set_render_job_host(Some(CpuHost::cooperative())).unwrap_or_else(|e| panic!("{e}"));
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 10, "height": 5})).unwrap_or_else(|e| panic!("{e}"));
|
||||
let raster = json!({"name": "Raster", "set": {"width": 16, "height": 16, "job": {"method": "raster"}}});
|
||||
s.execute("render.settings.set", &raster).unwrap_or_else(|e| panic!("{e}"));
|
||||
s
|
||||
};
|
||||
for p in [
|
||||
json!({}),
|
||||
json!({"settings": {"width": 16, "height": 16}, "replace": false, "transparent_background": true}),
|
||||
json!({"settings": {"width": 16, "height": 16}, "studio_weights": [1, 0.5, 0.25], "integrator": "raster"}),
|
||||
json!({"setup": "Raster", "replace": true}),
|
||||
json!({"settings": {"width": 16, "height": 16, "job": {"method": "raster", "transparent_background": true}}, "studio_weights": [1, 1, 1]}),
|
||||
] {
|
||||
let off = session(ParamCheck::Off).execute("render.preflight", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
|
||||
let warned = session(ParamCheck::Warn).execute("render.preflight", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
|
||||
assert_eq!(warned, off, "{p}");
|
||||
assert!(warned.get("param_warnings").is_none(), "{p}: {warned}");
|
||||
let started = session(ParamCheck::Warn).execute("render.start", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
|
||||
assert!(started["job"].is_object() && started.get("param_warnings").is_none(), "{p}: {started}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user