1use crate::config::FoundryConfig;
8use crate::runner::RunnerError;
9use serde_json::{Map, Value, json};
10use std::path::{Path, PathBuf};
11use std::sync::{Mutex, OnceLock};
12use tokio::process::Command;
13
14static INSTALLED_VERSIONS: OnceLock<Mutex<Vec<SemVer>>> = OnceLock::new();
17
18fn get_installed_versions() -> Vec<SemVer> {
19 let mutex = INSTALLED_VERSIONS.get_or_init(|| Mutex::new(scan_installed_versions()));
20 mutex.lock().unwrap().clone()
21}
22
23fn invalidate_installed_versions() {
24 if let Some(mutex) = INSTALLED_VERSIONS.get() {
25 *mutex.lock().unwrap() = scan_installed_versions();
26 }
27}
28
29pub async fn resolve_solc_binary(
42 config: &FoundryConfig,
43 file_source: Option<&str>,
44 client: Option<&tower_lsp::Client>,
45) -> PathBuf {
46 if let Some(source) = file_source
48 && let Some(constraint) = parse_pragma(source)
49 {
50 if !matches!(constraint, PragmaConstraint::Exact(_)) {
56 if let Some(ref config_ver) = config.solc_version
57 && let Some(parsed) = SemVer::parse(config_ver)
58 && version_satisfies(&parsed, &constraint)
59 && let Some(path) = find_solc_binary(config_ver)
60 {
61 if let Some(c) = client {
62 c.log_message(
63 tower_lsp::lsp_types::MessageType::INFO,
64 format!(
65 "solc: foundry.toml {config_ver} satisfies pragma {constraint:?} → {}",
66 path.display()
67 ),
68 )
69 .await;
70 }
71 return path;
72 }
73 }
74
75 let installed = get_installed_versions();
76 if let Some(version) = find_matching_version(&constraint, &installed)
77 && let Some(path) = find_solc_binary(&version.to_string())
78 {
79 if let Some(c) = client {
80 c.log_message(
81 tower_lsp::lsp_types::MessageType::INFO,
82 format!(
83 "solc: pragma {constraint:?} → {version} → {}",
84 path.display()
85 ),
86 )
87 .await;
88 }
89 return path;
90 }
91
92 let install_version = version_to_install(&constraint);
94 if let Some(ref ver_str) = install_version {
95 if let Some(c) = client {
96 c.show_message(
97 tower_lsp::lsp_types::MessageType::INFO,
98 format!("Installing solc {ver_str}..."),
99 )
100 .await;
101 }
102
103 if svm_install(ver_str).await {
104 invalidate_installed_versions();
106
107 if let Some(c) = client {
108 c.show_message(
109 tower_lsp::lsp_types::MessageType::INFO,
110 format!("Installed solc {ver_str}"),
111 )
112 .await;
113 }
114 if let Some(path) = find_solc_binary(ver_str) {
115 return path;
116 }
117 } else if let Some(c) = client {
118 c.show_message(
119 tower_lsp::lsp_types::MessageType::WARNING,
120 format!(
121 "Failed to install solc {ver_str}. \
122 Install it manually: svm install {ver_str}"
123 ),
124 )
125 .await;
126 }
127 }
128 }
129
130 if let Some(ref version) = config.solc_version
132 && let Some(path) = find_solc_binary(version)
133 {
134 if let Some(c) = client {
135 c.log_message(
136 tower_lsp::lsp_types::MessageType::INFO,
137 format!(
138 "solc: no pragma, using foundry.toml version {version} → {}",
139 path.display()
140 ),
141 )
142 .await;
143 }
144 return path;
145 }
146
147 if let Some(c) = client {
149 c.log_message(
150 tower_lsp::lsp_types::MessageType::INFO,
151 "solc: no pragma match, falling back to system solc",
152 )
153 .await;
154 }
155 PathBuf::from("solc")
156}
157
158fn version_to_install(constraint: &PragmaConstraint) -> Option<String> {
165 match constraint {
166 PragmaConstraint::Exact(v) => Some(v.to_string()),
167 PragmaConstraint::Caret(v) => Some(v.to_string()),
168 PragmaConstraint::Gte(v) => Some(v.to_string()),
169 PragmaConstraint::Range(lower, _) => Some(lower.to_string()),
170 }
171}
172
173async fn svm_install(version: &str) -> bool {
177 let result = Command::new("svm")
178 .arg("install")
179 .arg(version)
180 .stdout(std::process::Stdio::piped())
181 .stderr(std::process::Stdio::piped())
182 .status()
183 .await;
184
185 matches!(result, Ok(status) if status.success())
186}
187
188fn svm_data_dirs() -> Vec<PathBuf> {
200 let mut dirs = Vec::new();
201
202 if let Some(home) = home_dir() {
203 #[cfg(target_os = "macos")]
205 dirs.push(home.join("Library/Application Support/svm"));
206
207 #[cfg(target_os = "linux")]
208 dirs.push(home.join(".svm"));
209
210 #[cfg(target_os = "windows")]
211 if let Some(appdata) = std::env::var_os("APPDATA") {
212 dirs.push(PathBuf::from(appdata).join("svm"));
213 }
214
215 let dot_svm = home.join(".svm");
217 if !dirs.contains(&dot_svm) {
218 dirs.push(dot_svm);
219 }
220
221 dirs.push(home.join(".solc-select").join("artifacts"));
223 }
224
225 dirs
226}
227
228fn find_solc_binary(version: &str) -> Option<PathBuf> {
230 for dir in svm_data_dirs() {
231 let svm_path = dir.join(version).join(format!("solc-{version}"));
233 if svm_path.is_file() {
234 return Some(svm_path);
235 }
236
237 let select_path = dir
239 .join(format!("solc-{version}"))
240 .join(format!("solc-{version}"));
241 if select_path.is_file() {
242 return Some(select_path);
243 }
244 }
245
246 None
247}
248
249fn home_dir() -> Option<PathBuf> {
251 std::env::var_os("HOME")
253 .or_else(|| std::env::var_os("USERPROFILE"))
254 .map(PathBuf::from)
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
261pub struct SemVer {
262 pub major: u32,
263 pub minor: u32,
264 pub patch: u32,
265}
266
267impl SemVer {
268 fn parse(s: &str) -> Option<SemVer> {
269 let parts: Vec<&str> = s.split('.').collect();
270 if parts.len() != 3 {
271 return None;
272 }
273 Some(SemVer {
274 major: parts[0].parse().ok()?,
275 minor: parts[1].parse().ok()?,
276 patch: parts[2].parse().ok()?,
277 })
278 }
279}
280
281impl std::fmt::Display for SemVer {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
284 }
285}
286
287#[derive(Debug, Clone, PartialEq)]
289pub enum PragmaConstraint {
290 Exact(SemVer),
292 Caret(SemVer),
295 Gte(SemVer),
297 Range(SemVer, SemVer),
299}
300
301pub fn parse_pragma(source: &str) -> Option<PragmaConstraint> {
309 let pragma_line = source
311 .lines()
312 .take(20)
313 .find(|line| line.trim_start().starts_with("pragma solidity"))?;
314
315 let after_keyword = pragma_line
317 .trim_start()
318 .strip_prefix("pragma solidity")?
319 .trim();
320 let constraint_str = after_keyword
321 .strip_suffix(';')
322 .unwrap_or(after_keyword)
323 .trim();
324
325 if constraint_str.is_empty() {
326 return None;
327 }
328
329 if let Some(rest) = constraint_str.strip_prefix(">=") {
331 let rest = rest.trim();
332 if let Some(space_idx) = rest.find(|c: char| c.is_whitespace() || c == '<') {
333 let lower_str = rest[..space_idx].trim();
334 let upper_part = rest[space_idx..].trim();
335 if let Some(upper_str) = upper_part.strip_prefix('<') {
336 let upper_str = upper_str.trim();
337 if let (Some(lower), Some(upper)) =
338 (SemVer::parse(lower_str), SemVer::parse(upper_str))
339 {
340 return Some(PragmaConstraint::Range(lower, upper));
341 }
342 }
343 }
344 if let Some(ver) = SemVer::parse(rest) {
346 return Some(PragmaConstraint::Gte(ver));
347 }
348 }
349
350 if let Some(rest) = constraint_str.strip_prefix('^')
352 && let Some(ver) = SemVer::parse(rest.trim())
353 {
354 return Some(PragmaConstraint::Caret(ver));
355 }
356
357 if let Some(ver) = SemVer::parse(constraint_str) {
359 return Some(PragmaConstraint::Exact(ver));
360 }
361
362 None
363}
364
365pub fn list_installed_versions() -> Vec<SemVer> {
367 get_installed_versions()
368}
369
370fn scan_installed_versions() -> Vec<SemVer> {
374 let mut versions = Vec::new();
375
376 for dir in svm_data_dirs() {
377 if let Ok(entries) = std::fs::read_dir(&dir) {
378 for entry in entries.flatten() {
379 let name = match entry.file_name().into_string() {
380 Ok(n) => n,
381 Err(_) => continue,
382 };
383 let version_str = name.strip_prefix("solc-").unwrap_or(&name);
386 if let Some(ver) = SemVer::parse(version_str) {
387 versions.push(ver);
388 }
389 }
390 }
391 }
392
393 versions.sort();
394 versions.dedup();
395 versions
396}
397
398pub fn find_matching_version(
403 constraint: &PragmaConstraint,
404 installed: &[SemVer],
405) -> Option<SemVer> {
406 let candidates: Vec<&SemVer> = installed
407 .iter()
408 .filter(|v| version_satisfies(v, constraint))
409 .collect();
410
411 candidates.last().cloned().cloned()
413}
414
415pub fn version_satisfies(version: &SemVer, constraint: &PragmaConstraint) -> bool {
417 match constraint {
418 PragmaConstraint::Exact(v) => version == v,
419 PragmaConstraint::Caret(v) => {
420 version.major == v.major && version >= v && version.minor < v.minor + 1
423 }
424 PragmaConstraint::Gte(v) => version >= v,
425 PragmaConstraint::Range(lower, upper) => version >= lower && version < upper,
426 }
427}
428
429pub async fn resolve_remappings(config: &FoundryConfig) -> Vec<String> {
433 let output = Command::new("forge")
436 .arg("remappings")
437 .current_dir(&config.root)
438 .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
439 .output()
440 .await;
441
442 if let Ok(output) = output
443 && output.status.success()
444 {
445 let stdout = String::from_utf8_lossy(&output.stdout);
446 let remappings: Vec<String> = stdout
447 .lines()
448 .filter(|l| !l.trim().is_empty())
449 .map(|l| l.to_string())
450 .collect();
451 if !remappings.is_empty() {
452 return remappings;
453 }
454 }
455
456 if !config.remappings.is_empty() {
458 return config.remappings.clone();
459 }
460
461 let remappings_txt = config.root.join("remappings.txt");
463 if let Ok(content) = std::fs::read_to_string(&remappings_txt) {
464 return content
465 .lines()
466 .filter(|l| !l.trim().is_empty())
467 .map(|l| l.to_string())
468 .collect();
469 }
470
471 Vec::new()
472}
473
474pub fn build_standard_json_input(
483 file_path: &str,
484 remappings: &[String],
485 config: &FoundryConfig,
486) -> Value {
487 let mut settings = json!({
488 "remappings": remappings,
489 "outputSelection": {
490 "*": {
491 "*": [
492 "abi",
493 "devdoc",
494 "userdoc",
495 "evm.methodIdentifiers",
496 "evm.gasEstimates"
497 ],
498 "": ["ast"]
499 }
500 }
501 });
502
503 if config.optimizer {
505 settings["optimizer"] = json!({
506 "enabled": true,
507 "runs": config.optimizer_runs
508 });
509 }
510
511 if config.via_ir {
513 settings["viaIR"] = json!(true);
514 }
515
516 if let Some(ref evm_version) = config.evm_version {
518 settings["evmVersion"] = json!(evm_version);
519 }
520
521 json!({
522 "language": "Solidity",
523 "sources": {
524 file_path: {
525 "urls": [file_path]
526 }
527 },
528 "settings": settings
529 })
530}
531
532pub async fn run_solc(
534 solc_binary: &Path,
535 input: &Value,
536 project_root: &Path,
537) -> Result<Value, RunnerError> {
538 let input_str = serde_json::to_string(input)?;
539
540 let mut child = Command::new(solc_binary)
541 .arg("--standard-json")
542 .current_dir(project_root)
543 .stdin(std::process::Stdio::piped())
544 .stdout(std::process::Stdio::piped())
545 .stderr(std::process::Stdio::piped())
546 .spawn()?;
547
548 if let Some(mut stdin) = child.stdin.take() {
550 use tokio::io::AsyncWriteExt;
551 stdin
552 .write_all(input_str.as_bytes())
553 .await
554 .map_err(RunnerError::CommandError)?;
555 }
557
558 let output = child
559 .wait_with_output()
560 .await
561 .map_err(RunnerError::CommandError)?;
562
563 let stdout = String::from_utf8_lossy(&output.stdout);
565 if stdout.trim().is_empty() {
566 let stderr = String::from_utf8_lossy(&output.stderr);
567 return Err(RunnerError::CommandError(std::io::Error::other(format!(
568 "solc produced no output, stderr: {stderr}"
569 ))));
570 }
571
572 let parsed: Value = serde_json::from_str(&stdout)?;
573 Ok(parsed)
574}
575
576pub fn normalize_solc_output(mut solc_output: Value, project_root: Option<&Path>) -> Value {
594 let mut result = Map::new();
595
596 let errors = solc_output
598 .get_mut("errors")
599 .map(Value::take)
600 .unwrap_or_else(|| json!([]));
601 result.insert("errors".to_string(), errors);
602
603 let resolve = |p: &str| -> String {
606 if let Some(root) = project_root {
607 let path = Path::new(p);
608 if path.is_relative() {
609 return root.join(path).to_string_lossy().into_owned();
610 }
611 }
612 p.to_string()
613 };
614
615 let mut source_id_to_path = Map::new();
618 let mut resolved_sources = Map::new();
619
620 if let Some(sources) = solc_output
621 .get_mut("sources")
622 .and_then(|s| s.as_object_mut())
623 {
624 let keys: Vec<String> = sources.keys().cloned().collect();
626 for key in keys {
627 if let Some(mut source_data) = sources.remove(&key) {
628 let abs_key = resolve(&key);
629
630 if let Some(ast) = source_data.get_mut("ast") {
632 if let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str()) {
633 let resolved = resolve(abs_path);
634 ast.as_object_mut()
635 .unwrap()
636 .insert("absolutePath".to_string(), json!(resolved));
637 }
638 }
639
640 if let Some(id) = source_data.get("id") {
641 source_id_to_path.insert(id.to_string(), json!(&abs_key));
642 }
643
644 resolved_sources.insert(abs_key, source_data);
645 }
646 }
647 }
648
649 result.insert("sources".to_string(), Value::Object(resolved_sources));
650
651 let mut resolved_contracts = Map::new();
653 if let Some(contracts) = solc_output
654 .get_mut("contracts")
655 .and_then(|c| c.as_object_mut())
656 {
657 let keys: Vec<String> = contracts.keys().cloned().collect();
658 for key in keys {
659 if let Some(contract_data) = contracts.remove(&key) {
660 resolved_contracts.insert(resolve(&key), contract_data);
661 }
662 }
663 }
664 result.insert("contracts".to_string(), Value::Object(resolved_contracts));
665
666 result.insert(
668 "source_id_to_path".to_string(),
669 Value::Object(source_id_to_path),
670 );
671
672 Value::Object(result)
673}
674
675pub fn normalize_forge_output(mut forge_output: Value) -> Value {
687 let mut result = Map::new();
688
689 let errors = forge_output
691 .get_mut("errors")
692 .map(Value::take)
693 .unwrap_or_else(|| json!([]));
694 result.insert("errors".to_string(), errors);
695
696 let mut normalized_sources = Map::new();
698 if let Some(sources) = forge_output
699 .get_mut("sources")
700 .and_then(|s| s.as_object_mut())
701 {
702 for (path, entries) in sources.iter_mut() {
703 if let Some(arr) = entries.as_array_mut()
704 && let Some(first) = arr.first_mut()
705 && let Some(sf) = first.get_mut("source_file")
706 {
707 normalized_sources.insert(path.clone(), sf.take());
708 }
709 }
710 }
711 result.insert("sources".to_string(), Value::Object(normalized_sources));
712
713 let mut normalized_contracts = Map::new();
715 if let Some(contracts) = forge_output
716 .get_mut("contracts")
717 .and_then(|c| c.as_object_mut())
718 {
719 for (path, names) in contracts.iter_mut() {
720 let mut path_contracts = Map::new();
721 if let Some(names_obj) = names.as_object_mut() {
722 for (name, entries) in names_obj.iter_mut() {
723 if let Some(arr) = entries.as_array_mut()
724 && let Some(first) = arr.first_mut()
725 && let Some(contract) = first.get_mut("contract")
726 {
727 path_contracts.insert(name.clone(), contract.take());
728 }
729 }
730 }
731 normalized_contracts.insert(path.clone(), Value::Object(path_contracts));
732 }
733 }
734 result.insert("contracts".to_string(), Value::Object(normalized_contracts));
735
736 let source_id_to_path = forge_output
738 .get_mut("build_infos")
739 .and_then(|bi| bi.as_array_mut())
740 .and_then(|arr| arr.first_mut())
741 .and_then(|info| info.get_mut("source_id_to_path"))
742 .map(Value::take)
743 .unwrap_or_else(|| json!({}));
744 result.insert("source_id_to_path".to_string(), source_id_to_path);
745
746 Value::Object(result)
747}
748
749pub async fn solc_ast(
754 file_path: &str,
755 config: &FoundryConfig,
756 client: Option<&tower_lsp::Client>,
757) -> Result<Value, RunnerError> {
758 let file_source = std::fs::read_to_string(file_path).ok();
760 let solc_binary = resolve_solc_binary(config, file_source.as_deref(), client).await;
761 let remappings = resolve_remappings(config).await;
762
763 let rel_path = Path::new(file_path)
768 .strip_prefix(&config.root)
769 .map(|p| p.to_string_lossy().into_owned())
770 .unwrap_or_else(|_| file_path.to_string());
771
772 let input = build_standard_json_input(&rel_path, &remappings, config);
773 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
774 Ok(normalize_solc_output(raw_output, Some(&config.root)))
775}
776
777pub async fn solc_build(
779 file_path: &str,
780 config: &FoundryConfig,
781 client: Option<&tower_lsp::Client>,
782) -> Result<Value, RunnerError> {
783 solc_ast(file_path, config, client).await
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[test]
791 fn test_normalize_solc_sources() {
792 let solc_output = json!({
793 "sources": {
794 "src/Foo.sol": {
795 "id": 0,
796 "ast": {
797 "nodeType": "SourceUnit",
798 "absolutePath": "src/Foo.sol",
799 "id": 100
800 }
801 },
802 "src/Bar.sol": {
803 "id": 1,
804 "ast": {
805 "nodeType": "SourceUnit",
806 "absolutePath": "src/Bar.sol",
807 "id": 200
808 }
809 }
810 },
811 "contracts": {},
812 "errors": []
813 });
814
815 let normalized = normalize_solc_output(solc_output, None);
816
817 let sources = normalized.get("sources").unwrap().as_object().unwrap();
819 assert_eq!(sources.len(), 2);
820
821 let foo = sources.get("src/Foo.sol").unwrap();
822 assert_eq!(foo.get("id").unwrap(), 0);
823 assert_eq!(
824 foo.get("ast")
825 .unwrap()
826 .get("nodeType")
827 .unwrap()
828 .as_str()
829 .unwrap(),
830 "SourceUnit"
831 );
832
833 let id_to_path = normalized
835 .get("source_id_to_path")
836 .unwrap()
837 .as_object()
838 .unwrap();
839 assert_eq!(id_to_path.len(), 2);
840 }
841
842 #[test]
843 fn test_normalize_solc_contracts() {
844 let solc_output = json!({
845 "sources": {},
846 "contracts": {
847 "src/Foo.sol": {
848 "Foo": {
849 "abi": [{"type": "function", "name": "bar"}],
850 "evm": {
851 "methodIdentifiers": {
852 "bar(uint256)": "abcd1234"
853 },
854 "gasEstimates": {
855 "external": {"bar(uint256)": "200"}
856 }
857 }
858 }
859 }
860 },
861 "errors": []
862 });
863
864 let normalized = normalize_solc_output(solc_output, None);
865
866 let contracts = normalized.get("contracts").unwrap().as_object().unwrap();
868 let foo_contracts = contracts.get("src/Foo.sol").unwrap().as_object().unwrap();
869 let foo = foo_contracts.get("Foo").unwrap();
870
871 let method_ids = foo
872 .get("evm")
873 .unwrap()
874 .get("methodIdentifiers")
875 .unwrap()
876 .as_object()
877 .unwrap();
878 assert_eq!(
879 method_ids.get("bar(uint256)").unwrap().as_str().unwrap(),
880 "abcd1234"
881 );
882 }
883
884 #[test]
885 fn test_normalize_solc_errors_passthrough() {
886 let solc_output = json!({
887 "sources": {},
888 "contracts": {},
889 "errors": [{
890 "sourceLocation": {"file": "src/Foo.sol", "start": 0, "end": 10},
891 "type": "Warning",
892 "component": "general",
893 "severity": "warning",
894 "errorCode": "2394",
895 "message": "test warning",
896 "formattedMessage": "Warning: test warning"
897 }]
898 });
899
900 let normalized = normalize_solc_output(solc_output, None);
901
902 let errors = normalized.get("errors").unwrap().as_array().unwrap();
903 assert_eq!(errors.len(), 1);
904 assert_eq!(
905 errors[0].get("errorCode").unwrap().as_str().unwrap(),
906 "2394"
907 );
908 }
909
910 #[test]
911 fn test_normalize_empty_solc_output() {
912 let solc_output = json!({
913 "sources": {},
914 "contracts": {}
915 });
916
917 let normalized = normalize_solc_output(solc_output, None);
918
919 assert!(
920 normalized
921 .get("sources")
922 .unwrap()
923 .as_object()
924 .unwrap()
925 .is_empty()
926 );
927 assert!(
928 normalized
929 .get("contracts")
930 .unwrap()
931 .as_object()
932 .unwrap()
933 .is_empty()
934 );
935 assert_eq!(
936 normalized.get("errors").unwrap().as_array().unwrap().len(),
937 0
938 );
939 assert!(
940 normalized
941 .get("source_id_to_path")
942 .unwrap()
943 .as_object()
944 .unwrap()
945 .is_empty()
946 );
947 }
948
949 #[test]
950 fn test_build_standard_json_input() {
951 let config = FoundryConfig::default();
952 let input = build_standard_json_input(
953 "/path/to/Foo.sol",
954 &[
955 "ds-test/=lib/forge-std/lib/ds-test/src/".to_string(),
956 "forge-std/=lib/forge-std/src/".to_string(),
957 ],
958 &config,
959 );
960
961 let sources = input.get("sources").unwrap().as_object().unwrap();
962 assert!(sources.contains_key("/path/to/Foo.sol"));
963
964 let settings = input.get("settings").unwrap();
965 let remappings = settings.get("remappings").unwrap().as_array().unwrap();
966 assert_eq!(remappings.len(), 2);
967
968 let output_sel = settings.get("outputSelection").unwrap();
969 assert!(output_sel.get("*").is_some());
970
971 assert!(settings.get("optimizer").is_none());
973 assert!(settings.get("viaIR").is_none());
974 assert!(settings.get("evmVersion").is_none());
975 }
976
977 #[test]
978 fn test_build_standard_json_input_with_config() {
979 let config = FoundryConfig {
980 optimizer: true,
981 optimizer_runs: 9999999,
982 via_ir: true,
983 evm_version: Some("osaka".to_string()),
984 ..Default::default()
985 };
986 let input = build_standard_json_input("/path/to/Foo.sol", &[], &config);
987
988 let settings = input.get("settings").unwrap();
989
990 let optimizer = settings.get("optimizer").unwrap();
992 assert_eq!(optimizer.get("enabled").unwrap().as_bool().unwrap(), true);
993 assert_eq!(optimizer.get("runs").unwrap().as_u64().unwrap(), 9999999);
994
995 assert_eq!(settings.get("viaIR").unwrap().as_bool().unwrap(), true);
997
998 assert_eq!(
1000 settings.get("evmVersion").unwrap().as_str().unwrap(),
1001 "osaka"
1002 );
1003 }
1004
1005 #[tokio::test]
1006 async fn test_resolve_solc_binary_default() {
1007 let config = FoundryConfig::default();
1008 let binary = resolve_solc_binary(&config, None, None).await;
1009 assert_eq!(binary, PathBuf::from("solc"));
1010 }
1011
1012 #[test]
1013 fn test_parse_pragma_exact() {
1014 let source = "// SPDX\npragma solidity 0.8.26;\n";
1015 assert_eq!(
1016 parse_pragma(source),
1017 Some(PragmaConstraint::Exact(SemVer {
1018 major: 0,
1019 minor: 8,
1020 patch: 26
1021 }))
1022 );
1023 }
1024
1025 #[test]
1026 fn test_parse_pragma_caret() {
1027 let source = "pragma solidity ^0.8.0;\n";
1028 assert_eq!(
1029 parse_pragma(source),
1030 Some(PragmaConstraint::Caret(SemVer {
1031 major: 0,
1032 minor: 8,
1033 patch: 0
1034 }))
1035 );
1036 }
1037
1038 #[test]
1039 fn test_parse_pragma_gte() {
1040 let source = "pragma solidity >=0.8.0;\n";
1041 assert_eq!(
1042 parse_pragma(source),
1043 Some(PragmaConstraint::Gte(SemVer {
1044 major: 0,
1045 minor: 8,
1046 patch: 0
1047 }))
1048 );
1049 }
1050
1051 #[test]
1052 fn test_parse_pragma_range() {
1053 let source = "pragma solidity >=0.6.2 <0.9.0;\n";
1054 assert_eq!(
1055 parse_pragma(source),
1056 Some(PragmaConstraint::Range(
1057 SemVer {
1058 major: 0,
1059 minor: 6,
1060 patch: 2
1061 },
1062 SemVer {
1063 major: 0,
1064 minor: 9,
1065 patch: 0
1066 },
1067 ))
1068 );
1069 }
1070
1071 #[test]
1072 fn test_parse_pragma_none() {
1073 let source = "contract Foo {}\n";
1074 assert_eq!(parse_pragma(source), None);
1075 }
1076
1077 #[test]
1078 fn test_version_satisfies_exact() {
1079 let v = SemVer {
1080 major: 0,
1081 minor: 8,
1082 patch: 26,
1083 };
1084 assert!(version_satisfies(&v, &PragmaConstraint::Exact(v.clone())));
1085 assert!(!version_satisfies(
1086 &SemVer {
1087 major: 0,
1088 minor: 8,
1089 patch: 25
1090 },
1091 &PragmaConstraint::Exact(v)
1092 ));
1093 }
1094
1095 #[test]
1096 fn test_version_satisfies_caret() {
1097 let constraint = PragmaConstraint::Caret(SemVer {
1098 major: 0,
1099 minor: 8,
1100 patch: 0,
1101 });
1102 assert!(version_satisfies(
1103 &SemVer {
1104 major: 0,
1105 minor: 8,
1106 patch: 0
1107 },
1108 &constraint
1109 ));
1110 assert!(version_satisfies(
1111 &SemVer {
1112 major: 0,
1113 minor: 8,
1114 patch: 26
1115 },
1116 &constraint
1117 ));
1118 assert!(!version_satisfies(
1120 &SemVer {
1121 major: 0,
1122 minor: 9,
1123 patch: 0
1124 },
1125 &constraint
1126 ));
1127 assert!(!version_satisfies(
1129 &SemVer {
1130 major: 0,
1131 minor: 7,
1132 patch: 0
1133 },
1134 &constraint
1135 ));
1136 }
1137
1138 #[test]
1139 fn test_version_satisfies_gte() {
1140 let constraint = PragmaConstraint::Gte(SemVer {
1141 major: 0,
1142 minor: 8,
1143 patch: 0,
1144 });
1145 assert!(version_satisfies(
1146 &SemVer {
1147 major: 0,
1148 minor: 8,
1149 patch: 0
1150 },
1151 &constraint
1152 ));
1153 assert!(version_satisfies(
1154 &SemVer {
1155 major: 0,
1156 minor: 9,
1157 patch: 0
1158 },
1159 &constraint
1160 ));
1161 assert!(!version_satisfies(
1162 &SemVer {
1163 major: 0,
1164 minor: 7,
1165 patch: 0
1166 },
1167 &constraint
1168 ));
1169 }
1170
1171 #[test]
1172 fn test_version_satisfies_range() {
1173 let constraint = PragmaConstraint::Range(
1174 SemVer {
1175 major: 0,
1176 minor: 6,
1177 patch: 2,
1178 },
1179 SemVer {
1180 major: 0,
1181 minor: 9,
1182 patch: 0,
1183 },
1184 );
1185 assert!(version_satisfies(
1186 &SemVer {
1187 major: 0,
1188 minor: 6,
1189 patch: 2
1190 },
1191 &constraint
1192 ));
1193 assert!(version_satisfies(
1194 &SemVer {
1195 major: 0,
1196 minor: 8,
1197 patch: 26
1198 },
1199 &constraint
1200 ));
1201 assert!(!version_satisfies(
1203 &SemVer {
1204 major: 0,
1205 minor: 9,
1206 patch: 0
1207 },
1208 &constraint
1209 ));
1210 assert!(!version_satisfies(
1211 &SemVer {
1212 major: 0,
1213 minor: 6,
1214 patch: 1
1215 },
1216 &constraint
1217 ));
1218 }
1219
1220 #[test]
1221 fn test_find_matching_version() {
1222 let installed = vec![
1223 SemVer {
1224 major: 0,
1225 minor: 8,
1226 patch: 0,
1227 },
1228 SemVer {
1229 major: 0,
1230 minor: 8,
1231 patch: 20,
1232 },
1233 SemVer {
1234 major: 0,
1235 minor: 8,
1236 patch: 26,
1237 },
1238 SemVer {
1239 major: 0,
1240 minor: 8,
1241 patch: 33,
1242 },
1243 ];
1244 let constraint = PragmaConstraint::Caret(SemVer {
1246 major: 0,
1247 minor: 8,
1248 patch: 20,
1249 });
1250 let matched = find_matching_version(&constraint, &installed);
1251 assert_eq!(
1252 matched,
1253 Some(SemVer {
1254 major: 0,
1255 minor: 8,
1256 patch: 33
1257 })
1258 );
1259
1260 let constraint = PragmaConstraint::Exact(SemVer {
1262 major: 0,
1263 minor: 8,
1264 patch: 20,
1265 });
1266 let matched = find_matching_version(&constraint, &installed);
1267 assert_eq!(
1268 matched,
1269 Some(SemVer {
1270 major: 0,
1271 minor: 8,
1272 patch: 20
1273 })
1274 );
1275
1276 let constraint = PragmaConstraint::Exact(SemVer {
1278 major: 0,
1279 minor: 8,
1280 patch: 15,
1281 });
1282 let matched = find_matching_version(&constraint, &installed);
1283 assert_eq!(matched, None);
1284 }
1285
1286 #[test]
1287 fn test_list_installed_versions() {
1288 let versions = list_installed_versions();
1290 for w in versions.windows(2) {
1292 assert!(w[0] <= w[1]);
1293 }
1294 }
1295}