1use crate::{get_keypair, is_hidden, keys_sync};
2use anchor_client::Cluster;
3use anchor_lang_idl::types::Idl;
4use anyhow::{anyhow, bail, Context, Error, Result};
5use clap::{Parser, ValueEnum};
6use dirs::home_dir;
7use heck::ToSnakeCase;
8use reqwest::Url;
9use serde::de::{self, MapAccess, Visitor};
10use serde::ser::SerializeMap;
11use serde::{Deserialize, Deserializer, Serialize, Serializer};
12use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE};
13use solana_sdk::clock::Slot;
14use solana_sdk::pubkey::Pubkey;
15use solana_sdk::signature::{Keypair, Signer};
16use solang_parser::pt::{ContractTy, SourceUnitPart};
17use std::collections::{BTreeMap, HashMap};
18use std::convert::TryFrom;
19use std::ffi::OsStr;
20use std::fs::{self, File};
21use std::io::prelude::*;
22use std::marker::PhantomData;
23use std::ops::Deref;
24use std::path::Path;
25use std::path::PathBuf;
26use std::str::FromStr;
27use std::{fmt, io};
28use walkdir::WalkDir;
29
30pub trait Merge: Sized {
31 fn merge(&mut self, _other: Self) {}
32}
33
34#[derive(Default, Debug, Parser)]
35pub struct ConfigOverride {
36 #[clap(global = true, long = "provider.cluster")]
38 pub cluster: Option<Cluster>,
39 #[clap(global = true, long = "provider.wallet")]
41 pub wallet: Option<WalletPath>,
42}
43
44#[derive(Debug)]
45pub struct WithPath<T> {
46 inner: T,
47 path: PathBuf,
48}
49
50impl<T> WithPath<T> {
51 pub fn new(inner: T, path: PathBuf) -> Self {
52 Self { inner, path }
53 }
54
55 pub fn path(&self) -> &PathBuf {
56 &self.path
57 }
58
59 pub fn into_inner(self) -> T {
60 self.inner
61 }
62}
63
64impl<T> std::convert::AsRef<T> for WithPath<T> {
65 fn as_ref(&self) -> &T {
66 &self.inner
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub struct Manifest(cargo_toml::Manifest);
72
73impl Manifest {
74 pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
75 cargo_toml::Manifest::from_path(&p)
76 .map(Manifest)
77 .map_err(anyhow::Error::from)
78 .with_context(|| format!("Error reading manifest from path: {}", p.as_ref().display()))
79 }
80
81 pub fn lib_name(&self) -> Result<String> {
82 match &self.lib {
83 Some(cargo_toml::Product {
84 name: Some(name), ..
85 }) => Ok(name.to_owned()),
86 _ => self
87 .package
88 .as_ref()
89 .ok_or_else(|| anyhow!("package section not provided"))
90 .map(|pkg| pkg.name.to_snake_case()),
91 }
92 }
93
94 pub fn version(&self) -> String {
95 match &self.package {
96 Some(package) => package.version().to_string(),
97 _ => "0.0.0".to_string(),
98 }
99 }
100
101 pub fn discover() -> Result<Option<WithPath<Manifest>>> {
103 Manifest::discover_from_path(std::env::current_dir()?)
104 }
105
106 pub fn discover_from_path(start_from: PathBuf) -> Result<Option<WithPath<Manifest>>> {
108 let mut cwd_opt = Some(start_from.as_path());
109
110 while let Some(cwd) = cwd_opt {
111 let mut anchor_toml = false;
112
113 for f in fs::read_dir(cwd).with_context(|| {
114 format!("Error reading the directory with path: {}", cwd.display())
115 })? {
116 let p = f
117 .with_context(|| {
118 format!("Error reading the directory with path: {}", cwd.display())
119 })?
120 .path();
121 if let Some(filename) = p.file_name().and_then(|name| name.to_str()) {
122 if filename == "Cargo.toml" {
123 return Ok(Some(WithPath::new(Manifest::from_path(&p)?, p)));
124 }
125 if filename == "Anchor.toml" {
126 anchor_toml = true;
127 }
128 }
129 }
130
131 if anchor_toml {
133 break;
134 }
135
136 cwd_opt = cwd.parent();
137 }
138
139 Ok(None)
140 }
141}
142
143impl Deref for Manifest {
144 type Target = cargo_toml::Manifest;
145
146 fn deref(&self) -> &Self::Target {
147 &self.0
148 }
149}
150
151impl WithPath<Config> {
152 pub fn get_rust_program_list(&self) -> Result<Vec<PathBuf>> {
153 let (members, exclude) = self.canonicalize_workspace()?;
155
156 let program_paths: Vec<PathBuf> = {
161 if members.is_empty() {
162 let path = self.path().parent().unwrap().join("programs");
163 if let Ok(entries) = fs::read_dir(path) {
164 entries
165 .filter(|entry| entry.as_ref().map(|e| e.path().is_dir()).unwrap_or(false))
166 .map(|dir| dir.map(|d| d.path().canonicalize().unwrap()))
167 .collect::<Vec<Result<PathBuf, std::io::Error>>>()
168 .into_iter()
169 .collect::<Result<Vec<PathBuf>, std::io::Error>>()?
170 } else {
171 Vec::new()
172 }
173 } else {
174 members
175 }
176 };
177
178 Ok(program_paths
180 .into_iter()
181 .filter(|m| !exclude.contains(m))
182 .collect())
183 }
184
185 pub fn get_solidity_program_list(&self) -> Result<Vec<(String, PathBuf)>> {
189 let path = self.path().parent().unwrap().join("solidity");
190 let mut res = Vec::new();
191
192 if let Ok(entries) = fs::read_dir(path) {
193 for entry in entries {
194 let path = entry?.path();
195
196 if !path.is_file() || path.extension() != Some(OsStr::new("sol")) {
197 continue;
198 }
199
200 let source = fs::read_to_string(&path)?;
201
202 let tree = match solang_parser::parse(&source, 0) {
203 Ok((tree, _)) => tree,
204 Err(diag) => {
205 bail!(
208 "{}: {}: {}",
209 path.display(),
210 diag[0].level.to_string(),
211 diag[0].message
212 );
213 }
214 };
215
216 tree.0.iter().for_each(|part| {
217 if let SourceUnitPart::ContractDefinition(contract) = part {
218 if matches!(&contract.ty, ContractTy::Contract(..)) {
220 if let Some(name) = &contract.name {
221 res.push((name.name.clone(), path.clone()));
222 }
223 }
224 }
225 });
226 }
227 }
228
229 Ok(res)
230 }
231
232 pub fn read_all_programs(&self) -> Result<Vec<Program>> {
233 let mut r = vec![];
234 for path in self.get_rust_program_list()? {
235 let cargo = Manifest::from_path(path.join("Cargo.toml"))?;
236 let lib_name = cargo.lib_name()?;
237
238 let idl_filepath = Path::new("target")
239 .join("idl")
240 .join(&lib_name)
241 .with_extension("json");
242 let idl = fs::read(idl_filepath)
243 .ok()
244 .map(|bytes| serde_json::from_reader(&*bytes))
245 .transpose()?;
246
247 r.push(Program {
248 lib_name,
249 solidity: false,
250 path,
251 idl,
252 });
253 }
254 for (lib_name, path) in self.get_solidity_program_list()? {
255 let idl_filepath = Path::new("target")
256 .join("idl")
257 .join(&lib_name)
258 .with_extension("json");
259 let idl = fs::read(idl_filepath)
260 .ok()
261 .map(|bytes| serde_json::from_reader(&*bytes))
262 .transpose()?;
263
264 r.push(Program {
265 lib_name,
266 solidity: true,
267 path,
268 idl,
269 });
270 }
271 Ok(r)
272 }
273
274 pub fn get_programs(&self, name: Option<String>) -> Result<Vec<Program>> {
278 let programs = self.read_all_programs()?;
279 let programs = match name {
280 Some(name) => vec![programs
281 .into_iter()
282 .find(|program| {
283 name == program.lib_name
284 || name == program.path.file_name().unwrap().to_str().unwrap()
285 })
286 .ok_or_else(|| anyhow!("Program {name} not found"))?],
287 None => programs,
288 };
289
290 Ok(programs)
291 }
292
293 pub fn get_program(&self, name: &str) -> Result<Program> {
295 self.get_programs(Some(name.to_owned()))?
296 .into_iter()
297 .next()
298 .ok_or_else(|| anyhow!("Expected a program"))
299 }
300
301 pub fn canonicalize_workspace(&self) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
302 let members = self.process_paths(&self.workspace.members)?;
303 let exclude = self.process_paths(&self.workspace.exclude)?;
304 Ok((members, exclude))
305 }
306
307 fn process_paths(&self, paths: &[String]) -> Result<Vec<PathBuf>, Error> {
308 let base_path = self.path().parent().unwrap();
309 paths
310 .iter()
311 .flat_map(|m| {
312 let path = base_path.join(m);
313 if m.ends_with("/*") {
314 let dir = path.parent().unwrap();
315 match fs::read_dir(dir) {
316 Ok(entries) => entries
317 .filter_map(|entry| entry.ok())
318 .map(|entry| self.process_single_path(&entry.path()))
319 .collect(),
320 Err(e) => vec![Err(Error::new(io::Error::new(
321 io::ErrorKind::Other,
322 format!("Error reading directory {:?}: {}", dir, e),
323 )))],
324 }
325 } else {
326 vec![self.process_single_path(&path)]
327 }
328 })
329 .collect()
330 }
331
332 fn process_single_path(&self, path: &PathBuf) -> Result<PathBuf, Error> {
333 path.canonicalize().map_err(|e| {
334 Error::new(io::Error::new(
335 io::ErrorKind::Other,
336 format!("Error canonicalizing path {:?}: {}", path, e),
337 ))
338 })
339 }
340}
341
342impl<T> std::ops::Deref for WithPath<T> {
343 type Target = T;
344 fn deref(&self) -> &Self::Target {
345 &self.inner
346 }
347}
348
349impl<T> std::ops::DerefMut for WithPath<T> {
350 fn deref_mut(&mut self) -> &mut Self::Target {
351 &mut self.inner
352 }
353}
354
355#[derive(Debug, Default)]
356pub struct Config {
357 pub toolchain: ToolchainConfig,
358 pub features: FeaturesConfig,
359 pub registry: RegistryConfig,
360 pub provider: ProviderConfig,
361 pub programs: ProgramsConfig,
362 pub scripts: ScriptsConfig,
363 pub workspace: WorkspaceConfig,
364 pub test_validator: Option<TestValidator>,
368 pub test_config: Option<TestConfig>,
369}
370
371#[derive(Default, Clone, Debug, Serialize, Deserialize)]
372pub struct ToolchainConfig {
373 pub anchor_version: Option<String>,
374 pub solana_version: Option<String>,
375 pub package_manager: Option<PackageManager>,
376}
377
378#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum, Serialize, Deserialize)]
380#[serde(rename_all = "lowercase")]
381pub enum PackageManager {
382 NPM,
384 #[default]
386 Yarn,
387 PNPM,
389}
390
391impl std::fmt::Display for PackageManager {
392 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
393 let pkg_manager_str = match self {
394 PackageManager::NPM => "npm",
395 PackageManager::Yarn => "yarn",
396 PackageManager::PNPM => "pnpm",
397 };
398
399 write!(f, "{pkg_manager_str}")
400 }
401}
402
403#[derive(Clone, Debug, Serialize, Deserialize)]
404pub struct FeaturesConfig {
405 #[serde(default = "FeaturesConfig::get_default_resolution")]
409 pub resolution: bool,
410 #[serde(default, rename = "skip-lint")]
412 pub skip_lint: bool,
413}
414
415impl FeaturesConfig {
416 fn get_default_resolution() -> bool {
417 true
418 }
419}
420
421impl Default for FeaturesConfig {
422 fn default() -> Self {
423 Self {
424 resolution: Self::get_default_resolution(),
425 skip_lint: false,
426 }
427 }
428}
429
430#[derive(Clone, Debug, Serialize, Deserialize)]
431pub struct RegistryConfig {
432 pub url: String,
433}
434
435impl Default for RegistryConfig {
436 fn default() -> Self {
437 Self {
438 url: "https://api.apr.dev".to_string(),
439 }
440 }
441}
442
443#[derive(Debug, Default)]
444pub struct ProviderConfig {
445 pub cluster: Cluster,
446 pub wallet: WalletPath,
447}
448
449pub type ScriptsConfig = BTreeMap<String, String>;
450
451pub type ProgramsConfig = BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>;
452
453#[derive(Debug, Default, Clone, Serialize, Deserialize)]
454pub struct WorkspaceConfig {
455 #[serde(default, skip_serializing_if = "Vec::is_empty")]
456 pub members: Vec<String>,
457 #[serde(default, skip_serializing_if = "Vec::is_empty")]
458 pub exclude: Vec<String>,
459 #[serde(default, skip_serializing_if = "String::is_empty")]
460 pub types: String,
461}
462
463#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug)]
464pub enum BootstrapMode {
465 None,
466 Debian,
467}
468
469#[derive(ValueEnum, Parser, Clone, PartialEq, Eq, Debug)]
470pub enum ProgramArch {
471 Bpf,
472 Sbf,
473}
474impl ProgramArch {
475 pub fn build_subcommand(&self) -> &str {
476 match self {
477 Self::Bpf => "build-bpf",
478 Self::Sbf => "build-sbf",
479 }
480 }
481}
482
483#[derive(Debug, Clone)]
484pub struct BuildConfig {
485 pub verifiable: bool,
486 pub solana_version: Option<String>,
487 pub docker_image: String,
488 pub bootstrap: BootstrapMode,
489}
490
491impl Config {
492 pub fn add_test_config(
493 &mut self,
494 root: impl AsRef<Path>,
495 test_paths: Vec<PathBuf>,
496 ) -> Result<()> {
497 self.test_config = TestConfig::discover(root, test_paths)?;
498 Ok(())
499 }
500
501 pub fn docker(&self) -> String {
502 let version = self
503 .toolchain
504 .anchor_version
505 .as_deref()
506 .unwrap_or(crate::DOCKER_BUILDER_VERSION);
507 format!("backpackapp/build:v{version}")
508 }
509
510 pub fn discover(cfg_override: &ConfigOverride) -> Result<Option<WithPath<Config>>> {
511 Config::_discover().map(|opt| {
512 opt.map(|mut cfg| {
513 if let Some(cluster) = cfg_override.cluster.clone() {
514 cfg.provider.cluster = cluster;
515 }
516 if let Some(wallet) = cfg_override.wallet.clone() {
517 cfg.provider.wallet = wallet;
518 }
519 cfg
520 })
521 })
522 }
523
524 fn _discover() -> Result<Option<WithPath<Config>>> {
526 let _cwd = std::env::current_dir()?;
527 let mut cwd_opt = Some(_cwd.as_path());
528
529 while let Some(cwd) = cwd_opt {
530 for f in fs::read_dir(cwd).with_context(|| {
531 format!("Error reading the directory with path: {}", cwd.display())
532 })? {
533 let p = f
534 .with_context(|| {
535 format!("Error reading the directory with path: {}", cwd.display())
536 })?
537 .path();
538 if let Some(filename) = p.file_name() {
539 if filename.to_str() == Some("Anchor.toml") {
540 let mut cfg = Config::from_path(&p)?;
542 let deploy_dir = p.parent().unwrap().join("target").join("deploy");
543 if !deploy_dir.exists() && !cfg.programs.contains_key(&Cluster::Localnet) {
544 println!("Updating program ids...");
545 fs::create_dir_all(deploy_dir)?;
546 keys_sync(&ConfigOverride::default(), None)?;
547 cfg = Config::from_path(&p)?;
548 }
549
550 return Ok(Some(WithPath::new(cfg, p)));
551 }
552 }
553 }
554
555 cwd_opt = cwd.parent();
556 }
557
558 Ok(None)
559 }
560
561 fn from_path(p: impl AsRef<Path>) -> Result<Self> {
562 fs::read_to_string(&p)
563 .with_context(|| format!("Error reading the file with path: {}", p.as_ref().display()))?
564 .parse::<Self>()
565 }
566
567 pub fn wallet_kp(&self) -> Result<Keypair> {
568 get_keypair(&self.provider.wallet.to_string())
569 }
570}
571
572#[derive(Debug, Serialize, Deserialize)]
573struct _Config {
574 toolchain: Option<ToolchainConfig>,
575 features: Option<FeaturesConfig>,
576 programs: Option<BTreeMap<String, BTreeMap<String, serde_json::Value>>>,
577 registry: Option<RegistryConfig>,
578 provider: Provider,
579 workspace: Option<WorkspaceConfig>,
580 scripts: Option<ScriptsConfig>,
581 test: Option<_TestValidator>,
582}
583
584#[derive(Debug, Serialize, Deserialize)]
585struct Provider {
586 #[serde(serialize_with = "ser_cluster", deserialize_with = "des_cluster")]
587 cluster: Cluster,
588 wallet: String,
589}
590
591fn ser_cluster<S: Serializer>(cluster: &Cluster, s: S) -> Result<S::Ok, S::Error> {
592 match cluster {
593 Cluster::Custom(http, ws) => {
594 match (Url::parse(http), Url::parse(ws)) {
595 (Ok(h), Ok(w)) if h.domain() == w.domain() => s.serialize_str(http),
597 _ => {
598 let mut map = s.serialize_map(Some(2))?;
599 map.serialize_entry("http", http)?;
600 map.serialize_entry("ws", ws)?;
601 map.end()
602 }
603 }
604 }
605 _ => s.serialize_str(&cluster.to_string()),
606 }
607}
608
609fn des_cluster<'de, D>(deserializer: D) -> Result<Cluster, D::Error>
610where
611 D: Deserializer<'de>,
612{
613 struct StringOrCustomCluster(PhantomData<fn() -> Cluster>);
614
615 impl<'de> Visitor<'de> for StringOrCustomCluster {
616 type Value = Cluster;
617
618 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
619 formatter.write_str("string or map")
620 }
621
622 fn visit_str<E>(self, value: &str) -> Result<Cluster, E>
623 where
624 E: de::Error,
625 {
626 value.parse().map_err(de::Error::custom)
627 }
628
629 fn visit_map<M>(self, mut map: M) -> Result<Cluster, M::Error>
630 where
631 M: MapAccess<'de>,
632 {
633 if let (Some((http_key, http_value)), Some((ws_key, ws_value))) = (
635 map.next_entry::<String, String>()?,
636 map.next_entry::<String, String>()?,
637 ) {
638 if http_key != "http" || ws_key != "ws" {
640 return Err(de::Error::custom("Invalid key"));
641 }
642
643 Url::parse(&http_value).map_err(de::Error::custom)?;
645 Url::parse(&ws_value).map_err(de::Error::custom)?;
646
647 Ok(Cluster::Custom(http_value, ws_value))
648 } else {
649 Err(de::Error::custom("Invalid entry"))
650 }
651 }
652 }
653 deserializer.deserialize_any(StringOrCustomCluster(PhantomData))
654}
655
656impl fmt::Display for Config {
657 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
658 let programs = {
659 let c = ser_programs(&self.programs);
660 if c.is_empty() {
661 None
662 } else {
663 Some(c)
664 }
665 };
666 let cfg = _Config {
667 toolchain: Some(self.toolchain.clone()),
668 features: Some(self.features.clone()),
669 registry: Some(self.registry.clone()),
670 provider: Provider {
671 cluster: self.provider.cluster.clone(),
672 wallet: self.provider.wallet.stringify_with_tilde(),
673 },
674 test: self.test_validator.clone().map(Into::into),
675 scripts: match self.scripts.is_empty() {
676 true => None,
677 false => Some(self.scripts.clone()),
678 },
679 programs,
680 workspace: (!self.workspace.members.is_empty() || !self.workspace.exclude.is_empty())
681 .then(|| self.workspace.clone()),
682 };
683
684 let cfg = toml::to_string(&cfg).expect("Must be well formed");
685 write!(f, "{}", cfg)
686 }
687}
688
689impl FromStr for Config {
690 type Err = Error;
691
692 fn from_str(s: &str) -> Result<Self, Self::Err> {
693 let cfg: _Config =
694 toml::from_str(s).map_err(|e| anyhow!("Unable to deserialize config: {e}"))?;
695 Ok(Config {
696 toolchain: cfg.toolchain.unwrap_or_default(),
697 features: cfg.features.unwrap_or_default(),
698 registry: cfg.registry.unwrap_or_default(),
699 provider: ProviderConfig {
700 cluster: cfg.provider.cluster,
701 wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
702 },
703 scripts: cfg.scripts.unwrap_or_default(),
704 test_validator: cfg.test.map(Into::into),
705 test_config: None,
706 programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
707 workspace: cfg.workspace.unwrap_or_default(),
708 })
709 }
710}
711
712pub fn get_solana_cfg_url() -> Result<String, io::Error> {
713 let config_file = CONFIG_FILE.as_ref().ok_or_else(|| {
714 io::Error::new(
715 io::ErrorKind::NotFound,
716 "Default Solana config was not found",
717 )
718 })?;
719 SolanaConfig::load(config_file).map(|config| config.json_rpc_url)
720}
721
722fn ser_programs(
723 programs: &BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>,
724) -> BTreeMap<String, BTreeMap<String, serde_json::Value>> {
725 programs
726 .iter()
727 .map(|(cluster, programs)| {
728 let cluster = cluster.to_string();
729 let programs = programs
730 .iter()
731 .map(|(name, deployment)| {
732 (
733 name.clone(),
734 to_value(&_ProgramDeployment::from(deployment)),
735 )
736 })
737 .collect::<BTreeMap<String, serde_json::Value>>();
738 (cluster, programs)
739 })
740 .collect::<BTreeMap<String, BTreeMap<String, serde_json::Value>>>()
741}
742
743fn to_value(dep: &_ProgramDeployment) -> serde_json::Value {
744 if dep.path.is_none() && dep.idl.is_none() {
745 return serde_json::Value::String(dep.address.to_string());
746 }
747 serde_json::to_value(dep).unwrap()
748}
749
750fn deser_programs(
751 programs: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
752) -> Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>> {
753 programs
754 .iter()
755 .map(|(cluster, programs)| {
756 let cluster: Cluster = cluster.parse()?;
757 let programs = programs
758 .iter()
759 .map(|(name, program_id)| {
760 Ok((
761 name.clone(),
762 ProgramDeployment::try_from(match &program_id {
763 serde_json::Value::String(address) => _ProgramDeployment {
764 address: address.parse()?,
765 path: None,
766 idl: None,
767 },
768
769 serde_json::Value::Object(_) => {
770 serde_json::from_value(program_id.clone())
771 .map_err(|_| anyhow!("Unable to read toml"))?
772 }
773 _ => return Err(anyhow!("Invalid toml type")),
774 })?,
775 ))
776 })
777 .collect::<Result<BTreeMap<String, ProgramDeployment>>>()?;
778 Ok((cluster, programs))
779 })
780 .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
781}
782
783#[derive(Default, Debug, Clone, Serialize, Deserialize)]
784pub struct TestValidator {
785 pub genesis: Option<Vec<GenesisEntry>>,
786 pub validator: Option<Validator>,
787 pub startup_wait: i32,
788 pub shutdown_wait: i32,
789 pub upgradeable: bool,
790}
791
792#[derive(Default, Debug, Clone, Serialize, Deserialize)]
793pub struct _TestValidator {
794 #[serde(skip_serializing_if = "Option::is_none")]
795 pub genesis: Option<Vec<GenesisEntry>>,
796 #[serde(skip_serializing_if = "Option::is_none")]
797 pub validator: Option<_Validator>,
798 #[serde(skip_serializing_if = "Option::is_none")]
799 pub startup_wait: Option<i32>,
800 #[serde(skip_serializing_if = "Option::is_none")]
801 pub shutdown_wait: Option<i32>,
802 #[serde(skip_serializing_if = "Option::is_none")]
803 pub upgradeable: Option<bool>,
804}
805
806pub const STARTUP_WAIT: i32 = 5000;
807pub const SHUTDOWN_WAIT: i32 = 2000;
808
809impl From<_TestValidator> for TestValidator {
810 fn from(_test_validator: _TestValidator) -> Self {
811 Self {
812 shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
813 startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT),
814 genesis: _test_validator.genesis,
815 validator: _test_validator.validator.map(Into::into),
816 upgradeable: _test_validator.upgradeable.unwrap_or(false),
817 }
818 }
819}
820
821impl From<TestValidator> for _TestValidator {
822 fn from(test_validator: TestValidator) -> Self {
823 Self {
824 shutdown_wait: Some(test_validator.shutdown_wait),
825 startup_wait: Some(test_validator.startup_wait),
826 genesis: test_validator.genesis,
827 validator: test_validator.validator.map(Into::into),
828 upgradeable: Some(test_validator.upgradeable),
829 }
830 }
831}
832
833#[derive(Debug, Clone)]
834pub struct TestConfig {
835 pub test_suite_configs: HashMap<PathBuf, TestToml>,
836}
837
838impl Deref for TestConfig {
839 type Target = HashMap<PathBuf, TestToml>;
840
841 fn deref(&self) -> &Self::Target {
842 &self.test_suite_configs
843 }
844}
845
846impl TestConfig {
847 pub fn discover(root: impl AsRef<Path>, test_paths: Vec<PathBuf>) -> Result<Option<Self>> {
848 let walker = WalkDir::new(root).into_iter();
849 let mut test_suite_configs = HashMap::new();
850 for entry in walker.filter_entry(|e| !is_hidden(e)) {
851 let entry = entry?;
852 if entry.file_name() == "Test.toml" {
853 let entry_path = entry.path();
854 let test_toml = TestToml::from_path(entry_path)?;
855 if test_paths.is_empty() || test_paths.iter().any(|p| entry_path.starts_with(p)) {
856 test_suite_configs.insert(entry.path().into(), test_toml);
857 }
858 }
859 }
860
861 Ok(match test_suite_configs.is_empty() {
862 true => None,
863 false => Some(Self { test_suite_configs }),
864 })
865 }
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize)]
871pub struct _TestToml {
872 pub extends: Option<Vec<String>>,
873 pub test: Option<_TestValidator>,
874 pub scripts: Option<ScriptsConfig>,
875}
876
877impl _TestToml {
878 fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
879 let s = fs::read_to_string(&path)?;
880 let parsed_toml: Self = toml::from_str(&s)?;
881 let mut current_toml = _TestToml {
882 extends: None,
883 test: None,
884 scripts: None,
885 };
886 if let Some(bases) = &parsed_toml.extends {
887 for base in bases {
888 let mut canonical_base = base.clone();
889 canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?;
890 current_toml.merge(_TestToml::from_path(&canonical_base)?);
891 }
892 }
893 current_toml.merge(parsed_toml);
894
895 if let Some(test) = &mut current_toml.test {
896 if let Some(genesis_programs) = &mut test.genesis {
897 for entry in genesis_programs {
898 entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?;
899 }
900 }
901 if let Some(validator) = &mut test.validator {
902 if let Some(ledger_dir) = &mut validator.ledger {
903 *ledger_dir = canonicalize_filepath_from_origin(&ledger_dir, &path)?;
904 }
905 if let Some(accounts) = &mut validator.account {
906 for entry in accounts {
907 entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?;
908 }
909 }
910 }
911 }
912 Ok(current_toml)
913 }
914}
915
916fn canonicalize_filepath_from_origin(
921 file_path: impl AsRef<Path>,
922 origin: impl AsRef<Path>,
923) -> Result<String> {
924 let previous_dir = std::env::current_dir()?;
925 std::env::set_current_dir(origin.as_ref().parent().unwrap())?;
926 let result = fs::canonicalize(&file_path)
927 .with_context(|| {
928 format!(
929 "Error reading (possibly relative) path: {}. If relative, this is the path that was used as the current path: {}",
930 &file_path.as_ref().display(),
931 &origin.as_ref().display()
932 )
933 })?
934 .display()
935 .to_string();
936 std::env::set_current_dir(previous_dir)?;
937 Ok(result)
938}
939
940#[derive(Debug, Clone, Serialize, Deserialize)]
941pub struct TestToml {
942 #[serde(skip_serializing_if = "Option::is_none")]
943 pub test: Option<TestValidator>,
944 pub scripts: ScriptsConfig,
945}
946
947impl TestToml {
948 pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
949 WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into()
950 }
951}
952
953impl Merge for _TestToml {
954 fn merge(&mut self, other: Self) {
955 let mut my_scripts = self.scripts.take();
956 match &mut my_scripts {
957 None => my_scripts = other.scripts,
958 Some(my_scripts) => {
959 if let Some(other_scripts) = other.scripts {
960 for (name, script) in other_scripts {
961 my_scripts.insert(name, script);
962 }
963 }
964 }
965 }
966
967 let mut my_test = self.test.take();
968 match &mut my_test {
969 Some(my_test) => {
970 if let Some(other_test) = other.test {
971 if let Some(startup_wait) = other_test.startup_wait {
972 my_test.startup_wait = Some(startup_wait);
973 }
974 if let Some(other_genesis) = other_test.genesis {
975 match &mut my_test.genesis {
976 Some(my_genesis) => {
977 for other_entry in other_genesis {
978 match my_genesis
979 .iter()
980 .position(|g| *g.address == other_entry.address)
981 {
982 None => my_genesis.push(other_entry),
983 Some(i) => my_genesis[i] = other_entry,
984 }
985 }
986 }
987 None => my_test.genesis = Some(other_genesis),
988 }
989 }
990 let mut my_validator = my_test.validator.take();
991 match &mut my_validator {
992 None => my_validator = other_test.validator,
993 Some(my_validator) => {
994 if let Some(other_validator) = other_test.validator {
995 my_validator.merge(other_validator)
996 }
997 }
998 }
999
1000 my_test.validator = my_validator;
1001 }
1002 }
1003 None => my_test = other.test,
1004 };
1005
1006 *self = Self {
1010 test: my_test,
1011 scripts: my_scripts,
1012 extends: self.extends.take(),
1013 };
1014 }
1015}
1016
1017impl TryFrom<WithPath<_TestToml>> for TestToml {
1018 type Error = Error;
1019
1020 fn try_from(mut value: WithPath<_TestToml>) -> Result<Self, Self::Error> {
1021 Ok(Self {
1022 test: value.test.take().map(Into::into),
1023 scripts: value
1024 .scripts
1025 .take()
1026 .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?,
1027 })
1028 }
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize)]
1032pub struct GenesisEntry {
1033 pub address: String,
1035 pub program: String,
1037 pub upgradeable: Option<bool>,
1039}
1040
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042pub struct CloneEntry {
1043 pub address: String,
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1048pub struct AccountEntry {
1049 pub address: String,
1051 pub filename: String,
1053}
1054
1055#[derive(Debug, Clone, Serialize, Deserialize)]
1056pub struct AccountDirEntry {
1057 pub directory: String,
1059}
1060
1061#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1062pub struct _Validator {
1063 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub account: Option<Vec<AccountEntry>>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1068 pub account_dir: Option<Vec<AccountDirEntry>>,
1069 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub bind_address: Option<String>,
1072 #[serde(skip_serializing_if = "Option::is_none")]
1074 pub clone: Option<Vec<CloneEntry>>,
1075 #[serde(skip_serializing_if = "Option::is_none")]
1077 pub dynamic_port_range: Option<String>,
1078 #[serde(skip_serializing_if = "Option::is_none")]
1080 pub faucet_port: Option<u16>,
1081 #[serde(skip_serializing_if = "Option::is_none")]
1083 pub faucet_sol: Option<String>,
1084 #[serde(skip_serializing_if = "Option::is_none")]
1086 pub geyser_plugin_config: Option<String>,
1087 #[serde(skip_serializing_if = "Option::is_none")]
1089 pub gossip_host: Option<String>,
1090 #[serde(skip_serializing_if = "Option::is_none")]
1092 pub gossip_port: Option<u16>,
1093 #[serde(skip_serializing_if = "Option::is_none")]
1095 pub url: Option<String>,
1096 #[serde(skip_serializing_if = "Option::is_none")]
1098 pub ledger: Option<String>,
1099 #[serde(skip_serializing_if = "Option::is_none")]
1101 pub limit_ledger_size: Option<String>,
1102 #[serde(skip_serializing_if = "Option::is_none")]
1104 pub rpc_port: Option<u16>,
1105 #[serde(skip_serializing_if = "Option::is_none")]
1107 pub slots_per_epoch: Option<String>,
1108 #[serde(skip_serializing_if = "Option::is_none")]
1110 pub ticks_per_slot: Option<u16>,
1111 #[serde(skip_serializing_if = "Option::is_none")]
1113 pub warp_slot: Option<Slot>,
1114 #[serde(skip_serializing_if = "Option::is_none")]
1116 pub deactivate_feature: Option<Vec<String>>,
1117}
1118
1119#[derive(Debug, Default, Clone, Serialize, Deserialize)]
1120pub struct Validator {
1121 #[serde(skip_serializing_if = "Option::is_none")]
1122 pub account: Option<Vec<AccountEntry>>,
1123 #[serde(skip_serializing_if = "Option::is_none")]
1124 pub account_dir: Option<Vec<AccountDirEntry>>,
1125 pub bind_address: String,
1126 #[serde(skip_serializing_if = "Option::is_none")]
1127 pub clone: Option<Vec<CloneEntry>>,
1128 #[serde(skip_serializing_if = "Option::is_none")]
1129 pub dynamic_port_range: Option<String>,
1130 #[serde(skip_serializing_if = "Option::is_none")]
1131 pub faucet_port: Option<u16>,
1132 #[serde(skip_serializing_if = "Option::is_none")]
1133 pub faucet_sol: Option<String>,
1134 #[serde(skip_serializing_if = "Option::is_none")]
1135 pub geyser_plugin_config: Option<String>,
1136 #[serde(skip_serializing_if = "Option::is_none")]
1137 pub gossip_host: Option<String>,
1138 #[serde(skip_serializing_if = "Option::is_none")]
1139 pub gossip_port: Option<u16>,
1140 #[serde(skip_serializing_if = "Option::is_none")]
1141 pub url: Option<String>,
1142 pub ledger: String,
1143 #[serde(skip_serializing_if = "Option::is_none")]
1144 pub limit_ledger_size: Option<String>,
1145 pub rpc_port: u16,
1146 #[serde(skip_serializing_if = "Option::is_none")]
1147 pub slots_per_epoch: Option<String>,
1148 #[serde(skip_serializing_if = "Option::is_none")]
1149 pub ticks_per_slot: Option<u16>,
1150 #[serde(skip_serializing_if = "Option::is_none")]
1151 pub warp_slot: Option<Slot>,
1152 #[serde(skip_serializing_if = "Option::is_none")]
1153 pub deactivate_feature: Option<Vec<String>>,
1154}
1155
1156impl From<_Validator> for Validator {
1157 fn from(_validator: _Validator) -> Self {
1158 Self {
1159 account: _validator.account,
1160 account_dir: _validator.account_dir,
1161 bind_address: _validator
1162 .bind_address
1163 .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
1164 clone: _validator.clone,
1165 dynamic_port_range: _validator.dynamic_port_range,
1166 faucet_port: _validator.faucet_port,
1167 faucet_sol: _validator.faucet_sol,
1168 geyser_plugin_config: _validator.geyser_plugin_config,
1169 gossip_host: _validator.gossip_host,
1170 gossip_port: _validator.gossip_port,
1171 url: _validator.url,
1172 ledger: _validator
1173 .ledger
1174 .unwrap_or_else(|| get_default_ledger_path().display().to_string()),
1175 limit_ledger_size: _validator.limit_ledger_size,
1176 rpc_port: _validator
1177 .rpc_port
1178 .unwrap_or(solana_sdk::rpc_port::DEFAULT_RPC_PORT),
1179 slots_per_epoch: _validator.slots_per_epoch,
1180 ticks_per_slot: _validator.ticks_per_slot,
1181 warp_slot: _validator.warp_slot,
1182 deactivate_feature: _validator.deactivate_feature,
1183 }
1184 }
1185}
1186
1187impl From<Validator> for _Validator {
1188 fn from(validator: Validator) -> Self {
1189 Self {
1190 account: validator.account,
1191 account_dir: validator.account_dir,
1192 bind_address: Some(validator.bind_address),
1193 clone: validator.clone,
1194 dynamic_port_range: validator.dynamic_port_range,
1195 faucet_port: validator.faucet_port,
1196 faucet_sol: validator.faucet_sol,
1197 geyser_plugin_config: validator.geyser_plugin_config,
1198 gossip_host: validator.gossip_host,
1199 gossip_port: validator.gossip_port,
1200 url: validator.url,
1201 ledger: Some(validator.ledger),
1202 limit_ledger_size: validator.limit_ledger_size,
1203 rpc_port: Some(validator.rpc_port),
1204 slots_per_epoch: validator.slots_per_epoch,
1205 ticks_per_slot: validator.ticks_per_slot,
1206 warp_slot: validator.warp_slot,
1207 deactivate_feature: validator.deactivate_feature,
1208 }
1209 }
1210}
1211
1212pub fn get_default_ledger_path() -> PathBuf {
1213 Path::new(".anchor").join("test-ledger")
1214}
1215
1216const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0";
1217
1218impl Merge for _Validator {
1219 fn merge(&mut self, other: Self) {
1220 *self = Self {
1224 account: match self.account.take() {
1225 None => other.account,
1226 Some(mut entries) => match other.account {
1227 None => Some(entries),
1228 Some(other_entries) => {
1229 for other_entry in other_entries {
1230 match entries
1231 .iter()
1232 .position(|my_entry| *my_entry.address == other_entry.address)
1233 {
1234 None => entries.push(other_entry),
1235 Some(i) => entries[i] = other_entry,
1236 };
1237 }
1238 Some(entries)
1239 }
1240 },
1241 },
1242 account_dir: match self.account_dir.take() {
1243 None => other.account_dir,
1244 Some(mut entries) => match other.account_dir {
1245 None => Some(entries),
1246 Some(other_entries) => {
1247 for other_entry in other_entries {
1248 match entries
1249 .iter()
1250 .position(|my_entry| *my_entry.directory == other_entry.directory)
1251 {
1252 None => entries.push(other_entry),
1253 Some(i) => entries[i] = other_entry,
1254 };
1255 }
1256 Some(entries)
1257 }
1258 },
1259 },
1260 bind_address: other.bind_address.or_else(|| self.bind_address.take()),
1261 clone: match self.clone.take() {
1262 None => other.clone,
1263 Some(mut entries) => match other.clone {
1264 None => Some(entries),
1265 Some(other_entries) => {
1266 for other_entry in other_entries {
1267 match entries
1268 .iter()
1269 .position(|my_entry| *my_entry.address == other_entry.address)
1270 {
1271 None => entries.push(other_entry),
1272 Some(i) => entries[i] = other_entry,
1273 };
1274 }
1275 Some(entries)
1276 }
1277 },
1278 },
1279 dynamic_port_range: other
1280 .dynamic_port_range
1281 .or_else(|| self.dynamic_port_range.take()),
1282 faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()),
1283 faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()),
1284 geyser_plugin_config: other
1285 .geyser_plugin_config
1286 .or_else(|| self.geyser_plugin_config.take()),
1287 gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()),
1288 gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()),
1289 url: other.url.or_else(|| self.url.take()),
1290 ledger: other.ledger.or_else(|| self.ledger.take()),
1291 limit_ledger_size: other
1292 .limit_ledger_size
1293 .or_else(|| self.limit_ledger_size.take()),
1294 rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()),
1295 slots_per_epoch: other
1296 .slots_per_epoch
1297 .or_else(|| self.slots_per_epoch.take()),
1298 ticks_per_slot: other.ticks_per_slot.or_else(|| self.ticks_per_slot.take()),
1299 warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()),
1300 deactivate_feature: other
1301 .deactivate_feature
1302 .or_else(|| self.deactivate_feature.take()),
1303 };
1304 }
1305}
1306
1307#[derive(Debug, Clone)]
1308pub struct Program {
1309 pub lib_name: String,
1310 pub solidity: bool,
1311 pub path: PathBuf,
1313 pub idl: Option<Idl>,
1314}
1315
1316impl Program {
1317 pub fn pubkey(&self) -> Result<Pubkey> {
1318 self.keypair().map(|kp| kp.pubkey())
1319 }
1320
1321 pub fn keypair(&self) -> Result<Keypair> {
1322 let file = self.keypair_file()?;
1323 get_keypair(file.path().to_str().unwrap())
1324 }
1325
1326 pub fn keypair_file(&self) -> Result<WithPath<File>> {
1328 let deploy_dir_path = Path::new("target").join("deploy");
1329 fs::create_dir_all(&deploy_dir_path)
1330 .with_context(|| format!("Error creating directory with path: {deploy_dir_path:?}"))?;
1331 let path = std::env::current_dir()
1332 .expect("Must have current dir")
1333 .join(deploy_dir_path.join(format!("{}-keypair.json", self.lib_name)));
1334 if path.exists() {
1335 return Ok(WithPath::new(
1336 File::open(&path)
1337 .with_context(|| format!("Error opening file with path: {}", path.display()))?,
1338 path,
1339 ));
1340 }
1341 let program_kp = Keypair::new();
1342 let mut file = File::create(&path)
1343 .with_context(|| format!("Error creating file with path: {}", path.display()))?;
1344 file.write_all(format!("{:?}", &program_kp.to_bytes()).as_bytes())?;
1345 Ok(WithPath::new(file, path))
1346 }
1347
1348 pub fn binary_path(&self, verifiable: bool) -> PathBuf {
1349 let path = Path::new("target")
1350 .join(if verifiable { "verifiable" } else { "deploy" })
1351 .join(&self.lib_name)
1352 .with_extension("so");
1353
1354 std::env::current_dir()
1355 .expect("Must have current dir")
1356 .join(path)
1357 }
1358}
1359
1360#[derive(Debug, Default)]
1361pub struct ProgramDeployment {
1362 pub address: Pubkey,
1363 pub path: Option<String>,
1364 pub idl: Option<String>,
1365}
1366
1367impl TryFrom<_ProgramDeployment> for ProgramDeployment {
1368 type Error = anyhow::Error;
1369 fn try_from(pd: _ProgramDeployment) -> Result<Self, Self::Error> {
1370 Ok(ProgramDeployment {
1371 address: pd.address.parse()?,
1372 path: pd.path,
1373 idl: pd.idl,
1374 })
1375 }
1376}
1377
1378#[derive(Debug, Default, Serialize, Deserialize)]
1379pub struct _ProgramDeployment {
1380 pub address: String,
1381 pub path: Option<String>,
1382 pub idl: Option<String>,
1383}
1384
1385impl From<&ProgramDeployment> for _ProgramDeployment {
1386 fn from(pd: &ProgramDeployment) -> Self {
1387 Self {
1388 address: pd.address.to_string(),
1389 path: pd.path.clone(),
1390 idl: pd.idl.clone(),
1391 }
1392 }
1393}
1394
1395pub struct ProgramWorkspace {
1396 pub name: String,
1397 pub program_id: Pubkey,
1398 pub idl: Idl,
1399}
1400
1401#[derive(Debug, Serialize, Deserialize)]
1402pub struct AnchorPackage {
1403 pub name: String,
1404 pub address: String,
1405 pub idl: Option<String>,
1406}
1407
1408impl AnchorPackage {
1409 pub fn from(name: String, cfg: &WithPath<Config>) -> Result<Self> {
1410 let cluster = &cfg.provider.cluster;
1411 if cluster != &Cluster::Mainnet {
1412 return Err(anyhow!("Publishing requires the mainnet cluster"));
1413 }
1414 let program_details = cfg
1415 .programs
1416 .get(cluster)
1417 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?
1418 .get(&name)
1419 .ok_or_else(|| anyhow!("Program not provided in Anchor.toml"))?;
1420 let idl = program_details.idl.clone();
1421 let address = program_details.address.to_string();
1422 Ok(Self { name, address, idl })
1423 }
1424}
1425
1426#[macro_export]
1427macro_rules! home_path {
1428 ($my_struct:ident, $path:literal) => {
1429 #[derive(Clone, Debug)]
1430 pub struct $my_struct(String);
1431
1432 impl Default for $my_struct {
1433 fn default() -> Self {
1434 $my_struct(
1435 home_dir()
1436 .unwrap()
1437 .join($path.replace('/', std::path::MAIN_SEPARATOR_STR))
1438 .display()
1439 .to_string(),
1440 )
1441 }
1442 }
1443
1444 impl $my_struct {
1445 fn stringify_with_tilde(&self) -> String {
1446 self.0
1447 .replacen(home_dir().unwrap().to_str().unwrap(), "~", 1)
1448 }
1449 }
1450
1451 impl FromStr for $my_struct {
1452 type Err = anyhow::Error;
1453
1454 fn from_str(s: &str) -> Result<Self, Self::Err> {
1455 Ok(Self(s.to_owned()))
1456 }
1457 }
1458
1459 impl fmt::Display for $my_struct {
1460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1461 write!(f, "{}", self.0)
1462 }
1463 }
1464 };
1465}
1466
1467home_path!(WalletPath, ".config/solana/id.json");
1468
1469#[cfg(test)]
1470mod tests {
1471 use super::*;
1472
1473 const BASE_CONFIG: &str = "
1474 [provider]
1475 cluster = \"localnet\"
1476 wallet = \"id.json\"
1477 ";
1478
1479 #[test]
1480 fn parse_custom_cluster_str() {
1481 let config = Config::from_str(
1482 "
1483 [provider]
1484 cluster = \"http://my-url.com\"
1485 wallet = \"id.json\"
1486 ",
1487 )
1488 .unwrap();
1489 assert!(!config.features.skip_lint);
1490
1491 assert!(config
1493 .to_string()
1494 .contains(r#"cluster = "http://my-url.com""#));
1495 }
1496
1497 #[test]
1498 fn parse_custom_cluster_map() {
1499 let config = Config::from_str(
1500 "
1501 [provider]
1502 cluster = { http = \"http://my-url.com\", ws = \"ws://my-url.com\" }
1503 wallet = \"id.json\"
1504 ",
1505 )
1506 .unwrap();
1507 assert!(!config.features.skip_lint);
1508 }
1509
1510 #[test]
1511 fn parse_skip_lint_no_section() {
1512 let config = Config::from_str(BASE_CONFIG).unwrap();
1513 assert!(!config.features.skip_lint);
1514 }
1515
1516 #[test]
1517 fn parse_skip_lint_no_value() {
1518 let string = BASE_CONFIG.to_owned() + "[features]";
1519 let config = Config::from_str(&string).unwrap();
1520 assert!(!config.features.skip_lint);
1521 }
1522
1523 #[test]
1524 fn parse_skip_lint_true() {
1525 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = true";
1526 let config = Config::from_str(&string).unwrap();
1527 assert!(config.features.skip_lint);
1528 }
1529
1530 #[test]
1531 fn parse_skip_lint_false() {
1532 let string = BASE_CONFIG.to_owned() + "[features]\nskip-lint = false";
1533 let config = Config::from_str(&string).unwrap();
1534 assert!(!config.features.skip_lint);
1535 }
1536}