sim_citizen/
conformance.rs1use std::sync::Arc;
4
5use sim_kernel::{
6 CapabilitySet, Cx, Error, Expr, ObjectEncoding, Result, Symbol, Value,
7 read_construct_capability,
8};
9
10use crate::{
11 CitizenInfo, CitizenLib, CitizenRegistry, CitizenRuntime, field_error, value_from_expr,
12 values_citizen_eq,
13};
14
15pub fn run_registered_conformance(cx: &mut Cx) -> Result<()> {
21 let registry = CitizenRegistry::from_inventory()?;
22 cx.load_lib(&CitizenLib::all())?;
23 run_conformance_rows(cx, registry.citizens())
24}
25
26pub fn run_registered_conformance_expecting(cx: &mut Cx, expected: &[&str]) -> Result<()> {
33 let registry = CitizenRegistry::from_inventory()?;
34 registry.ensure_contains_symbols(expected)?;
35 cx.load_lib(&CitizenLib::all())?;
36 run_conformance_rows(cx, registry.citizens())
37}
38
39pub fn run_registry_conformance(cx: &mut Cx, registry: &CitizenRegistry) -> Result<()> {
45 cx.load_lib(registry)?;
46 run_conformance_rows(cx, registry.citizens())
47}
48
49pub fn run_registry_conformance_expecting(
54 cx: &mut Cx,
55 registry: &CitizenRegistry,
56 expected: &[&str],
57) -> Result<()> {
58 registry.ensure_contains_symbols(expected)?;
59 run_registry_conformance(cx, registry)
60}
61
62fn run_conformance_rows<'a>(
63 cx: &mut Cx,
64 rows: impl IntoIterator<Item = &'a CitizenInfo>,
65) -> Result<()> {
66 for info in rows {
67 (info.conformance)(cx)?;
68 }
69 Ok(())
70}
71
72pub fn check_default_fixture<T>(cx: &mut Cx) -> Result<()>
77where
78 T: CitizenRuntime,
79{
80 check_fixture(cx, T::example())
81}
82
83pub fn check_fixture<T>(cx: &mut Cx, fixture: T) -> Result<()>
90where
91 T: CitizenRuntime,
92{
93 let original = cx.factory().opaque(Arc::new(fixture))?;
94 let ObjectEncoding::Constructor { args, .. } = object_constructor_encoding(cx, &original)?
95 else {
96 unreachable!("object_constructor_encoding only returns constructor encodings");
97 };
98 let mut wrong_version = args.clone();
99 if let Some(first) = wrong_version.first_mut() {
100 *first = Expr::Symbol(Symbol::new("v999999"));
101 }
102 check_value_fixture_with_wrong_version(cx, original, Some(wrong_version))
103}
104
105pub fn check_value_fixture(cx: &mut Cx, original: Value) -> Result<()> {
110 check_value_fixture_with_wrong_version(cx, original, None)
111}
112
113pub fn check_value_fixture_with_wrong_version(
122 cx: &mut Cx,
123 original: Value,
124 wrong_version: Option<Vec<Expr>>,
125) -> Result<()> {
126 let ObjectEncoding::Constructor { class, args } = object_constructor_encoding(cx, &original)?
127 else {
128 unreachable!("object_constructor_encoding only returns constructor encodings");
129 };
130
131 check_constructor_fixture(cx, original, class, args, wrong_version)
132}
133
134fn object_constructor_encoding(cx: &mut Cx, value: &Value) -> Result<ObjectEncoding> {
135 let Some(encoder) = value.object().as_object_encoder() else {
136 return Err(Error::Eval(
137 "citizen conformance expects constructor encoding".to_owned(),
138 ));
139 };
140 let encoding = encoder.object_encoding(cx)?;
141 if !matches!(encoding, ObjectEncoding::Constructor { .. }) {
142 return Err(Error::Eval(
143 "citizen conformance expects constructor encoding".to_owned(),
144 ));
145 }
146 Ok(encoding)
147}
148
149fn check_constructor_fixture(
150 cx: &mut Cx,
151 original: Value,
152 class: Symbol,
153 args: Vec<Expr>,
154 wrong_version: Option<Vec<Expr>>,
155) -> Result<()> {
156 let text = render_constructor(&class, &args);
157 let prefix = format!("#({class}");
158 if !text.starts_with(&prefix) {
159 return Err(Error::Eval(format!(
160 "citizen constructor text {text:?} does not start with {prefix:?}"
161 )));
162 }
163
164 let values = exprs_to_values(cx, &args)?;
165 cx.with_capabilities(CapabilitySet::default(), |cx| {
166 assert_capability_denied(cx.read_construct(&class, values.clone()))
167 })?;
168
169 let mut allowed = cx.capabilities().clone();
170 allowed.insert(read_construct_capability());
171 cx.with_capabilities(allowed, |cx| {
172 let decoded = cx.read_construct(&class, values)?;
173 if !values_citizen_eq(cx, &original, &decoded)? {
174 return Err(Error::Eval(format!(
175 "citizen {class} failed constructor round-trip equality"
176 )));
177 }
178
179 let malformed = exprs_to_values(cx, &args[..args.len().saturating_sub(1)])?;
180 if cx.read_construct(&class, malformed).is_ok() {
181 return Err(Error::Eval(format!(
182 "citizen {class} accepted malformed arity"
183 )));
184 }
185
186 if let Some(wrong_version) = wrong_version {
187 let wrong_version = exprs_to_values(cx, &wrong_version)?;
188 if cx.read_construct(&class, wrong_version).is_ok() {
189 return Err(Error::Eval(format!(
190 "citizen {class} accepted wrong version"
191 )));
192 }
193 }
194
195 Ok(())
196 })?;
197
198 Ok(())
199}
200
201fn exprs_to_values(cx: &mut Cx, exprs: &[Expr]) -> Result<Vec<Value>> {
202 exprs.iter().map(|expr| value_from_expr(cx, expr)).collect()
203}
204
205fn assert_capability_denied(result: Result<Value>) -> Result<()> {
206 match result {
207 Err(Error::CapabilityDenied { capability })
208 if capability == read_construct_capability() =>
209 {
210 Ok(())
211 }
212 Err(err) => Err(field_error(
213 "read-construct",
214 format!("expected capability denial, found {err}"),
215 )),
216 Ok(_) => Err(field_error(
217 "read-construct",
218 "expected capability denial, found success",
219 )),
220 }
221}
222
223fn render_constructor(class: &Symbol, args: &[Expr]) -> String {
224 let mut out = format!("#({class}");
225 for arg in args {
226 out.push(' ');
227 out.push_str(&render_expr(arg));
228 }
229 out.push(')');
230 out
231}
232
233fn render_expr(expr: &Expr) -> String {
234 match expr {
235 Expr::Nil => "nil".to_owned(),
236 Expr::Bool(value) => value.to_string(),
237 Expr::Number(value) => value.canonical.clone(),
238 Expr::Symbol(value) => value.to_string(),
239 Expr::String(value) => format!("{value:?}"),
240 Expr::List(items) => {
241 let rendered = items.iter().map(render_expr).collect::<Vec<_>>().join(" ");
242 format!("({rendered})")
243 }
244 other => format!("{other:?}"),
245 }
246}