1 // Copyright (C) 2023 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 //! Code for reading configuration json files, usually called `cargo_embargo.json`.
16 //!
17 //! A single configuration file may cover several Rust packages under its directory tree, and may
18 //! have multiple "variants". A variant is a particular configuration for building a set of
19 //! packages, such as what feature flags to enable. Multiple variants are most often used to build
20 //! both a std variant of a library and a no_std variant. Each variant generates one or more modules
21 //! for each package, usually distinguished by a suffix.
22 //!
23 //! The [`Config`] struct has a map of `PackageConfig`s keyed by package name (for options that
24 //! apply across all variants of a package), and a vector of `VariantConfig`s. There must be at
25 //! least one variant for the configuration to generate any output. Each `VariantConfig` has the
26 //! options that apply to that variant across all packages, and then a map of
27 //! `PackageVariantConfig`s for options specific to a particular package of the variant.
28
29 use anyhow::{bail, Context, Result};
30 use serde::{Deserialize, Serialize};
31 use serde_json::{Map, Value};
32 use std::collections::BTreeMap;
33 use std::path::{Path, PathBuf};
34
default_apex_available() -> Vec<String>35 fn default_apex_available() -> Vec<String> {
36 vec!["//apex_available:platform".to_string(), "//apex_available:anyapex".to_string()]
37 }
38
is_default_apex_available(apex_available: &[String]) -> bool39 fn is_default_apex_available(apex_available: &[String]) -> bool {
40 apex_available == default_apex_available()
41 }
42
default_true() -> bool43 fn default_true() -> bool {
44 true
45 }
46
is_true(value: &bool) -> bool47 fn is_true(value: &bool) -> bool {
48 *value
49 }
50
is_false(value: &bool) -> bool51 fn is_false(value: &bool) -> bool {
52 !*value
53 }
54
55 /// Options that apply to everything.
56 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57 #[serde(deny_unknown_fields)]
58 pub struct Config {
59 pub variants: Vec<VariantConfig>,
60 /// Package specific config options across all variants.
61 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
62 pub package: BTreeMap<String, PackageConfig>,
63 }
64
65 /// Inserts entries from `defaults` into `variant` if neither it nor `ignored_fields` contain
66 /// matching keys.
add_defaults_to_variant( variant: &mut Map<String, Value>, defaults: &Map<String, Value>, ignored_fields: &[&str], )67 fn add_defaults_to_variant(
68 variant: &mut Map<String, Value>,
69 defaults: &Map<String, Value>,
70 ignored_fields: &[&str],
71 ) {
72 for (key, value) in defaults {
73 if !ignored_fields.contains(&key.as_str()) && !variant.contains_key(key) {
74 variant.insert(key.to_owned(), value.to_owned());
75 }
76 }
77 }
78
79 impl Config {
80 /// Names of all fields in [`Config`] other than `variants` (which is treated specially).
81 const FIELD_NAMES: [&'static str; 1] = ["package"];
82
83 /// Parses an instance of this config from the given JSON file.
from_file(filename: &Path) -> Result<Self>84 pub fn from_file(filename: &Path) -> Result<Self> {
85 let json_string = std::fs::read_to_string(filename)
86 .with_context(|| format!("failed to read file: {:?}", filename))?;
87 Self::from_json_str(&json_string)
88 }
89
90 /// Parses an instance of this config from a string of JSON.
from_json_str(json_str: &str) -> Result<Self>91 pub fn from_json_str(json_str: &str) -> Result<Self> {
92 // Ignore comments.
93 let json_str: String =
94 json_str.lines().filter(|l| !l.trim_start().starts_with("//")).collect();
95 // First parse into untyped map.
96 let mut config: Map<String, Value> =
97 serde_json::from_str(&json_str).context("failed to parse config")?;
98
99 // Flatten variants. First, get the variants from the config file.
100 let mut variants = match config.remove("variants") {
101 Some(Value::Array(v)) => v,
102 Some(_) => bail!("Failed to parse config: variants is not an array"),
103 None => {
104 // There are no variants, so just put everything into a single variant.
105 vec![Value::Object(Map::new())]
106 }
107 };
108 // Set default values in variants from top-level config.
109 for variant in &mut variants {
110 let variant = variant
111 .as_object_mut()
112 .context("Failed to parse config: variant is not an object")?;
113 add_defaults_to_variant(variant, &config, &Config::FIELD_NAMES);
114
115 if let Some(packages) = config.get("package") {
116 // Copy package entries across.
117 let variant_packages = variant
118 .entry("package")
119 .or_insert_with(|| Map::new().into())
120 .as_object_mut()
121 .context("Failed to parse config: variant package is not an object")?;
122 for (package_name, package_config) in packages
123 .as_object()
124 .context("Failed to parse config: package is not an object")?
125 {
126 let variant_package = variant_packages
127 .entry(package_name)
128 .or_insert_with(|| Map::new().into())
129 .as_object_mut()
130 .context(
131 "Failed to parse config: variant package config is not an object",
132 )?;
133 add_defaults_to_variant(
134 variant_package,
135 package_config
136 .as_object()
137 .context("Failed to parse config: package is not an object")?,
138 &PackageConfig::FIELD_NAMES,
139 );
140 }
141 }
142 }
143 // Remove other entries from the top-level config, and put variants back.
144 config.retain(|key, _| Self::FIELD_NAMES.contains(&key.as_str()));
145 if let Some(package) = config.get_mut("package") {
146 for value in package
147 .as_object_mut()
148 .context("Failed to parse config: package is not an object")?
149 .values_mut()
150 {
151 let package_config = value
152 .as_object_mut()
153 .context("Failed to parse config: package is not an object")?;
154 package_config.retain(|key, _| PackageConfig::FIELD_NAMES.contains(&key.as_str()))
155 }
156 }
157 config.insert("variants".to_string(), Value::Array(variants));
158
159 // Parse into `Config` struct.
160 serde_json::from_value(Value::Object(config)).context("failed to parse config")
161 }
162
163 /// Serializes an instance of this config to a string of pretty-printed JSON.
to_json_string(&self) -> Result<String>164 pub fn to_json_string(&self) -> Result<String> {
165 // First convert to an untyped map.
166 let Value::Object(mut config) = serde_json::to_value(self)? else {
167 panic!("Config wasn't a map.");
168 };
169
170 // Factor out common options which are set for all variants.
171 let Value::Array(mut variants) = config.remove("variants").unwrap() else {
172 panic!("variants wasn't an array.")
173 };
174 let mut packages = if let Some(Value::Object(packages)) = config.remove("package") {
175 packages
176 } else {
177 Map::new()
178 };
179 for (key, value) in variants[0].as_object().unwrap() {
180 if key == "package" {
181 for (package_name, package_config) in value.as_object().unwrap() {
182 for (package_key, package_value) in package_config.as_object().unwrap() {
183 // Check whether all other variants have the same entry for the same package.
184 if variants[1..variants.len()].iter().all(|variant| {
185 if let Some(Value::Object(variant_packages)) =
186 variant.as_object().unwrap().get("package")
187 {
188 if let Some(Value::Object(variant_package_config)) =
189 variant_packages.get(package_name)
190 {
191 return variant_package_config.get(package_key)
192 == Some(package_value);
193 }
194 }
195 false
196 }) {
197 packages
198 .entry(package_name)
199 .or_insert_with(|| Map::new().into())
200 .as_object_mut()
201 .unwrap()
202 .insert(package_key.to_owned(), package_value.to_owned());
203 }
204 }
205 }
206 } else {
207 // Check whether all the other variants have the same entry.
208 if variants[1..variants.len()]
209 .iter()
210 .all(|variant| variant.as_object().unwrap().get(key) == Some(value))
211 {
212 // Add it to the top-level config.
213 config.insert(key.to_owned(), value.to_owned());
214 }
215 }
216 }
217 // Remove factored out common options from all variants.
218 for key in config.keys() {
219 for variant in &mut variants {
220 variant.as_object_mut().unwrap().remove(key);
221 }
222 }
223 // Likewise, remove package options factored out from variants.
224 for (package_name, package_config) in &packages {
225 for package_key in package_config.as_object().unwrap().keys() {
226 for variant in &mut variants {
227 if let Some(Value::Object(variant_packages)) = variant.get_mut("package") {
228 if let Some(Value::Object(variant_package_config)) =
229 variant_packages.get_mut(package_name)
230 {
231 variant_package_config.remove(package_key);
232 }
233 }
234 }
235 }
236 }
237 // Remove any variant packages which are now empty.
238 for variant in &mut variants {
239 if let Some(Value::Object(variant_packages)) = variant.get_mut("package") {
240 variant_packages
241 .retain(|_, package_config| !package_config.as_object().unwrap().is_empty());
242 if variant_packages.is_empty() {
243 variant.as_object_mut().unwrap().remove("package");
244 }
245 }
246 }
247 // Put packages and variants back into the top-level config.
248 if variants.len() > 1 || !variants[0].as_object().unwrap().is_empty() {
249 config.insert("variants".to_string(), Value::Array(variants));
250 }
251 if !packages.is_empty() {
252 config.insert("package".to_string(), Value::Object(packages));
253 }
254
255 // Serialise the map into a JSON string.
256 serde_json::to_string_pretty(&config).context("failed to serialize config")
257 }
258 }
259
260 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
261 #[serde(deny_unknown_fields)]
262 pub struct VariantConfig {
263 /// Whether to output `rust_test` modules.
264 #[serde(default, skip_serializing_if = "is_false")]
265 pub tests: bool,
266 /// Set of features to enable. If not set, uses the default crate features.
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub features: Option<Vec<String>>,
269 /// Whether to build with `--workspace`.
270 #[serde(default, skip_serializing_if = "is_false")]
271 pub workspace: bool,
272 /// When workspace is enabled, list of `--exclude` crates.
273 #[serde(default, skip_serializing_if = "Vec::is_empty")]
274 pub workspace_excludes: Vec<String>,
275 /// Value to use for every generated module's `defaults` field.
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub global_defaults: Option<String>,
278 /// Value to use for every generated library module's `apex_available` field.
279 #[serde(default = "default_apex_available", skip_serializing_if = "is_default_apex_available")]
280 pub apex_available: Vec<String>,
281 /// Value to use for every generated library module's `native_bridge_supported` field.
282 #[serde(default, skip_serializing_if = "is_false")]
283 pub native_bridge_supported: bool,
284 /// Value to use for every generated library module's `product_available` field.
285 #[serde(default = "default_true", skip_serializing_if = "is_true")]
286 pub product_available: bool,
287 /// Value to use for every generated library module's `ramdisk_available` field.
288 #[serde(default, skip_serializing_if = "is_false")]
289 pub ramdisk_available: bool,
290 /// Value to use for every generated library module's `recovery_available` field.
291 #[serde(default, skip_serializing_if = "is_false")]
292 pub recovery_available: bool,
293 /// Value to use for every generated library module's `vendor_available` field.
294 #[serde(default = "default_true", skip_serializing_if = "is_true")]
295 pub vendor_available: bool,
296 /// Value to use for every generated library module's `vendor_ramdisk_available` field.
297 #[serde(default, skip_serializing_if = "is_false")]
298 pub vendor_ramdisk_available: bool,
299 /// Minimum SDK version.
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub min_sdk_version: Option<String>,
302 /// Map of renames for modules. For example, if a "libfoo" would be generated and there is an
303 /// entry ("libfoo", "libbar"), the generated module will be called "libbar" instead.
304 ///
305 /// Also, affects references to dependencies (e.g. in a "static_libs" list), even those outside
306 /// the project being processed.
307 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
308 pub module_name_overrides: BTreeMap<String, String>,
309 /// Package specific config options.
310 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
311 pub package: BTreeMap<String, PackageVariantConfig>,
312 /// `cfg` flags in this list will not be included.
313 #[serde(default, skip_serializing_if = "Vec::is_empty")]
314 pub cfg_blocklist: Vec<String>,
315 /// Extra `cfg` flags to enable in output modules.
316 #[serde(default, skip_serializing_if = "Vec::is_empty")]
317 pub extra_cfg: Vec<String>,
318 /// Modules in this list will not be generated.
319 #[serde(default, skip_serializing_if = "Vec::is_empty")]
320 pub module_blocklist: Vec<String>,
321 /// Modules name => Soong "visibility" property.
322 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
323 pub module_visibility: BTreeMap<String, Vec<String>>,
324 /// Whether to run the cargo build and parse its output, rather than just figuring things out
325 /// from the cargo metadata.
326 #[serde(default = "default_true", skip_serializing_if = "is_true")]
327 pub run_cargo: bool,
328 /// Generate Android build rules at Android.bp for this variant if true.
329 #[serde(default = "default_true", skip_serializing_if = "is_true")]
330 pub generate_androidbp: bool,
331 /// Generate Trusty build rules at rules.mk and Android.bp if true.
332 #[serde(default, skip_serializing_if = "is_false")]
333 pub generate_rulesmk: bool,
334 }
335
336 impl Default for VariantConfig {
default() -> Self337 fn default() -> Self {
338 Self {
339 tests: false,
340 features: Default::default(),
341 workspace: false,
342 workspace_excludes: Default::default(),
343 global_defaults: None,
344 apex_available: default_apex_available(),
345 native_bridge_supported: false,
346 product_available: true,
347 ramdisk_available: false,
348 recovery_available: false,
349 vendor_available: true,
350 vendor_ramdisk_available: false,
351 min_sdk_version: None,
352 module_name_overrides: Default::default(),
353 package: Default::default(),
354 cfg_blocklist: Default::default(),
355 extra_cfg: Default::default(),
356 module_blocklist: Default::default(),
357 module_visibility: Default::default(),
358 run_cargo: true,
359 generate_androidbp: true,
360 generate_rulesmk: false,
361 }
362 }
363 }
364
365 /// Options that apply to everything in a package (i.e. everything associated with a particular
366 /// Cargo.toml file), for all variants.
367 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
368 #[serde(deny_unknown_fields)]
369 pub struct PackageConfig {
370 /// File with content to append to the end of the generated Android.bp.
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub add_toplevel_block: Option<PathBuf>,
373 /// Patch file to apply after Android.bp is generated.
374 #[serde(skip_serializing_if = "Option::is_none")]
375 pub patch: Option<PathBuf>,
376 /// Patch file to apply after rules.mk is generated.
377 #[serde(skip_serializing_if = "Option::is_none")]
378 pub rulesmk_patch: Option<PathBuf>,
379 /// `license_text` to use for `license` module, overriding the `license_file` given by the
380 /// package or the default "LICENSE".
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub license_text: Option<Vec<String>>,
383 }
384
385 impl PackageConfig {
386 /// Names of all the fields on `PackageConfig`.
387 const FIELD_NAMES: [&'static str; 4] =
388 ["add_toplevel_block", "license_text", "patch", "rulesmk_patch"];
389 }
390
391 /// Options that apply to everything in a package (i.e. everything associated with a particular
392 /// Cargo.toml file), for a particular variant.
393 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
394 #[serde(deny_unknown_fields)]
395 pub struct PackageVariantConfig {
396 /// Link against `alloc`. Only valid if `no_std` is also true.
397 #[serde(default, skip_serializing_if = "is_false")]
398 pub alloc: bool,
399 /// Whether to compile for device. Defaults to true.
400 #[serde(default = "default_true", skip_serializing_if = "is_true")]
401 pub device_supported: bool,
402 /// Whether to compile for host. Defaults to true.
403 #[serde(default = "default_true", skip_serializing_if = "is_true")]
404 pub host_supported: bool,
405 /// Whether to compile for non-build host targets. Defaults to true.
406 #[serde(default = "default_true", skip_serializing_if = "is_true")]
407 pub host_cross_supported: bool,
408 /// Add a `compile_multilib: "first"` property to host modules.
409 #[serde(default, skip_serializing_if = "is_false")]
410 pub host_first_multilib: bool,
411 /// Generate "rust_library_rlib" instead of "rust_library".
412 #[serde(default, skip_serializing_if = "is_false")]
413 pub force_rlib: bool,
414 /// Whether to disable "unit_test" for "rust_test" modules.
415 // TODO: Should probably be a list of modules or crates. A package might have a mix of unit and
416 // integration tests.
417 #[serde(default, skip_serializing_if = "is_false")]
418 pub no_presubmit: bool,
419 /// File with content to append to the end of each generated module.
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub add_module_block: Option<PathBuf>,
422 /// Modules in this list will not be added as dependencies of generated modules.
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
424 pub dep_blocklist: Vec<String>,
425 /// Don't link against `std`, only `core`.
426 #[serde(default, skip_serializing_if = "is_false")]
427 pub no_std: bool,
428 /// Copy build.rs output to ./out/* and add a genrule to copy ./out/* to genrule output.
429 /// For crates with code pattern:
430 /// include!(concat!(env!("OUT_DIR"), "/<some_file>.rs"))
431 #[serde(default, skip_serializing_if = "is_false")]
432 pub copy_out: bool,
433 /// Add the given files to the given tests' `data` property. The key is the test source filename
434 /// relative to the crate root.
435 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
436 pub test_data: BTreeMap<String, Vec<String>>,
437 /// Static libraries in this list will instead be added as whole_static_libs.
438 #[serde(default, skip_serializing_if = "Vec::is_empty")]
439 pub whole_static_libs: Vec<String>,
440 /// Directories with headers to export for C usage.
441 #[serde(default, skip_serializing_if = "Vec::is_empty")]
442 pub exported_c_header_dir: Vec<PathBuf>,
443 }
444
445 impl Default for PackageVariantConfig {
default() -> Self446 fn default() -> Self {
447 Self {
448 alloc: false,
449 device_supported: true,
450 host_supported: true,
451 host_cross_supported: true,
452 host_first_multilib: false,
453 force_rlib: false,
454 no_presubmit: false,
455 add_module_block: None,
456 dep_blocklist: Default::default(),
457 no_std: false,
458 copy_out: false,
459 test_data: Default::default(),
460 whole_static_libs: Default::default(),
461 exported_c_header_dir: Default::default(),
462 }
463 }
464 }
465
466 #[cfg(test)]
467 mod tests {
468 use super::*;
469
470 #[test]
variant_config()471 fn variant_config() {
472 let config = Config::from_json_str(
473 r#"{
474 "tests": true,
475 "package": {
476 "argh": {
477 "patch": "patches/Android.bp.patch"
478 },
479 "another": {
480 "add_toplevel_block": "block.bp",
481 "device_supported": false,
482 "force_rlib": true
483 },
484 "rulesmk": {
485 "rulesmk_patch": "patches/rules.mk.patch"
486 }
487 },
488 "variants": [
489 {},
490 {
491 "generate_androidbp": false,
492 "generate_rulesmk": true,
493 "tests": false,
494 "features": ["feature"],
495 "vendor_available": false,
496 "package": {
497 "another": {
498 "alloc": false,
499 "force_rlib": false
500 },
501 "variant_package": {
502 "add_module_block": "variant_module_block.bp"
503 }
504 }
505 }
506 ]
507 }"#,
508 )
509 .unwrap();
510
511 assert_eq!(
512 config,
513 Config {
514 variants: vec![
515 VariantConfig {
516 generate_androidbp: true,
517 generate_rulesmk: false,
518 tests: true,
519 features: None,
520 vendor_available: true,
521 package: [
522 ("argh".to_string(), PackageVariantConfig { ..Default::default() }),
523 (
524 "another".to_string(),
525 PackageVariantConfig {
526 device_supported: false,
527 force_rlib: true,
528 ..Default::default()
529 },
530 ),
531 ("rulesmk".to_string(), PackageVariantConfig { ..Default::default() }),
532 ]
533 .into_iter()
534 .collect(),
535 ..Default::default()
536 },
537 VariantConfig {
538 generate_androidbp: false,
539 generate_rulesmk: true,
540 tests: false,
541 features: Some(vec!["feature".to_string()]),
542 vendor_available: false,
543 package: [
544 ("argh".to_string(), PackageVariantConfig { ..Default::default() }),
545 (
546 "another".to_string(),
547 PackageVariantConfig {
548 alloc: false,
549 device_supported: false,
550 force_rlib: false,
551 ..Default::default()
552 },
553 ),
554 ("rulesmk".to_string(), PackageVariantConfig { ..Default::default() }),
555 (
556 "variant_package".to_string(),
557 PackageVariantConfig {
558 add_module_block: Some("variant_module_block.bp".into()),
559 ..Default::default()
560 },
561 ),
562 ]
563 .into_iter()
564 .collect(),
565 ..Default::default()
566 },
567 ],
568 package: [
569 (
570 "argh".to_string(),
571 PackageConfig {
572 patch: Some("patches/Android.bp.patch".into()),
573 ..Default::default()
574 },
575 ),
576 (
577 "another".to_string(),
578 PackageConfig {
579 add_toplevel_block: Some("block.bp".into()),
580 ..Default::default()
581 },
582 ),
583 (
584 "rulesmk".to_string(),
585 PackageConfig {
586 rulesmk_patch: Some("patches/rules.mk.patch".into()),
587 ..Default::default()
588 },
589 ),
590 ]
591 .into_iter()
592 .collect(),
593 }
594 );
595 }
596
597 /// Tests that variant configuration options are factored out to the top level where possible.
598 #[test]
factor_variants()599 fn factor_variants() {
600 let config = Config {
601 variants: vec![
602 VariantConfig {
603 features: Some(vec![]),
604 tests: true,
605 vendor_available: false,
606 package: [(
607 "argh".to_string(),
608 PackageVariantConfig {
609 dep_blocklist: vec!["bad_dep".to_string()],
610 ..Default::default()
611 },
612 )]
613 .into_iter()
614 .collect(),
615 ..Default::default()
616 },
617 VariantConfig {
618 features: Some(vec![]),
619 tests: true,
620 product_available: false,
621 module_name_overrides: [("argh".to_string(), "argh_nostd".to_string())]
622 .into_iter()
623 .collect(),
624 vendor_available: false,
625 package: [(
626 "argh".to_string(),
627 PackageVariantConfig {
628 dep_blocklist: vec!["bad_dep".to_string()],
629 no_std: true,
630 ..Default::default()
631 },
632 )]
633 .into_iter()
634 .collect(),
635 ..Default::default()
636 },
637 ],
638 package: [(
639 "argh".to_string(),
640 PackageConfig { add_toplevel_block: Some("block.bp".into()), ..Default::default() },
641 )]
642 .into_iter()
643 .collect(),
644 };
645
646 assert_eq!(
647 config.to_json_string().unwrap(),
648 r#"{
649 "features": [],
650 "package": {
651 "argh": {
652 "add_toplevel_block": "block.bp",
653 "dep_blocklist": [
654 "bad_dep"
655 ]
656 }
657 },
658 "tests": true,
659 "variants": [
660 {},
661 {
662 "module_name_overrides": {
663 "argh": "argh_nostd"
664 },
665 "package": {
666 "argh": {
667 "no_std": true
668 }
669 },
670 "product_available": false
671 }
672 ],
673 "vendor_available": false
674 }"#
675 );
676 }
677
678 #[test]
factor_trivial_variant()679 fn factor_trivial_variant() {
680 let config = Config {
681 variants: vec![VariantConfig {
682 tests: true,
683 package: [("argh".to_string(), Default::default())].into_iter().collect(),
684 ..Default::default()
685 }],
686 package: Default::default(),
687 };
688
689 assert_eq!(
690 config.to_json_string().unwrap(),
691 r#"{
692 "tests": true
693 }"#
694 );
695 }
696 }
697