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
29fn semver_to_local(v: &semver::Version) -> SemVer {
31 SemVer {
32 major: v.major as u32,
33 minor: v.minor as u32,
34 patch: v.patch as u32,
35 }
36}
37
38pub async fn resolve_solc_binary(
51 config: &FoundryConfig,
52 file_source: Option<&str>,
53 client: Option<&tower_lsp::Client>,
54) -> PathBuf {
55 if let Some(source) = file_source
57 && let Some(constraint) = parse_pragma(source)
58 {
59 if !matches!(constraint, PragmaConstraint::Exact(_)) {
65 if let Some(ref config_ver) = config.solc_version
66 && let Some(parsed) = SemVer::parse(config_ver)
67 && version_satisfies(&parsed, &constraint)
68 && let Some(path) = find_solc_binary(config_ver)
69 {
70 if let Some(c) = client {
71 c.log_message(
72 tower_lsp::lsp_types::MessageType::INFO,
73 format!(
74 "solc: foundry.toml {config_ver} satisfies pragma {constraint:?} → {}",
75 path.display()
76 ),
77 )
78 .await;
79 }
80 return path;
81 }
82 }
83
84 let installed = get_installed_versions();
85 if let Some(version) = find_matching_version(&constraint, &installed)
86 && let Some(path) = find_solc_binary(&version.to_string())
87 {
88 if let Some(c) = client {
89 c.log_message(
90 tower_lsp::lsp_types::MessageType::INFO,
91 format!(
92 "solc: pragma {constraint:?} → {version} → {}",
93 path.display()
94 ),
95 )
96 .await;
97 }
98 return path;
99 }
100
101 let install_version = version_to_install(&constraint);
103 if let Some(ref ver_str) = install_version {
104 if let Some(c) = client {
105 c.show_message(
106 tower_lsp::lsp_types::MessageType::INFO,
107 format!("Installing solc {ver_str}..."),
108 )
109 .await;
110 }
111
112 if svm_install(ver_str).await {
113 invalidate_installed_versions();
115
116 if let Some(c) = client {
117 c.show_message(
118 tower_lsp::lsp_types::MessageType::INFO,
119 format!("Installed solc {ver_str}"),
120 )
121 .await;
122 }
123 if let Some(path) = find_solc_binary(ver_str) {
124 return path;
125 }
126 } else if let Some(c) = client {
127 c.show_message(
128 tower_lsp::lsp_types::MessageType::WARNING,
129 format!(
130 "Failed to install solc {ver_str}. \
131 Install it manually: svm install {ver_str}"
132 ),
133 )
134 .await;
135 }
136 }
137 }
138
139 if let Some(ref version) = config.solc_version
141 && let Some(path) = find_solc_binary(version)
142 {
143 if let Some(c) = client {
144 c.log_message(
145 tower_lsp::lsp_types::MessageType::INFO,
146 format!(
147 "solc: no pragma, using foundry.toml version {version} → {}",
148 path.display()
149 ),
150 )
151 .await;
152 }
153 return path;
154 }
155
156 if let Some(c) = client {
158 c.log_message(
159 tower_lsp::lsp_types::MessageType::INFO,
160 "solc: no pragma match, falling back to system solc",
161 )
162 .await;
163 }
164 PathBuf::from("solc")
165}
166
167fn version_to_install(constraint: &PragmaConstraint) -> Option<String> {
174 match constraint {
175 PragmaConstraint::Exact(v) => Some(v.to_string()),
176 PragmaConstraint::Caret(v) => Some(v.to_string()),
177 PragmaConstraint::Gte(v) => Some(v.to_string()),
178 PragmaConstraint::Range(lower, _) => Some(lower.to_string()),
179 }
180}
181
182async fn svm_install(version: &str) -> bool {
186 let ver = match semver::Version::parse(version) {
187 Ok(v) => v,
188 Err(_) => return false,
189 };
190 svm::install(&ver).await.is_ok()
191}
192
193fn find_solc_binary(version: &str) -> Option<PathBuf> {
195 let path = svm::version_binary(version);
196 if path.is_file() {
197 return Some(path);
198 }
199 None
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
206pub struct SemVer {
207 pub major: u32,
208 pub minor: u32,
209 pub patch: u32,
210}
211
212impl SemVer {
213 fn parse(s: &str) -> Option<SemVer> {
214 let parts: Vec<&str> = s.split('.').collect();
215 if parts.len() != 3 {
216 return None;
217 }
218 Some(SemVer {
219 major: parts[0].parse().ok()?,
220 minor: parts[1].parse().ok()?,
221 patch: parts[2].parse().ok()?,
222 })
223 }
224}
225
226impl std::fmt::Display for SemVer {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
229 }
230}
231
232#[derive(Debug, Clone, PartialEq)]
234pub enum PragmaConstraint {
235 Exact(SemVer),
237 Caret(SemVer),
240 Gte(SemVer),
242 Range(SemVer, SemVer),
244}
245
246pub fn parse_pragma(source: &str) -> Option<PragmaConstraint> {
254 let pragma_line = source
256 .lines()
257 .take(20)
258 .find(|line| line.trim_start().starts_with("pragma solidity"))?;
259
260 let after_keyword = pragma_line
262 .trim_start()
263 .strip_prefix("pragma solidity")?
264 .trim();
265 let constraint_str = after_keyword
266 .strip_suffix(';')
267 .unwrap_or(after_keyword)
268 .trim();
269
270 if constraint_str.is_empty() {
271 return None;
272 }
273
274 if let Some(rest) = constraint_str.strip_prefix(">=") {
276 let rest = rest.trim();
277 if let Some(space_idx) = rest.find(|c: char| c.is_whitespace() || c == '<') {
278 let lower_str = rest[..space_idx].trim();
279 let upper_part = rest[space_idx..].trim();
280 if let Some(upper_str) = upper_part.strip_prefix('<') {
281 let upper_str = upper_str.trim();
282 if let (Some(lower), Some(upper)) =
283 (SemVer::parse(lower_str), SemVer::parse(upper_str))
284 {
285 return Some(PragmaConstraint::Range(lower, upper));
286 }
287 }
288 }
289 if let Some(ver) = SemVer::parse(rest) {
291 return Some(PragmaConstraint::Gte(ver));
292 }
293 }
294
295 if let Some(rest) = constraint_str.strip_prefix('^')
297 && let Some(ver) = SemVer::parse(rest.trim())
298 {
299 return Some(PragmaConstraint::Caret(ver));
300 }
301
302 if let Some(ver) = SemVer::parse(constraint_str) {
304 return Some(PragmaConstraint::Exact(ver));
305 }
306
307 None
308}
309
310pub fn list_installed_versions() -> Vec<SemVer> {
312 get_installed_versions()
313}
314
315fn scan_installed_versions() -> Vec<SemVer> {
319 svm::installed_versions()
320 .unwrap_or_default()
321 .iter()
322 .map(semver_to_local)
323 .collect()
324}
325
326pub fn find_matching_version(
331 constraint: &PragmaConstraint,
332 installed: &[SemVer],
333) -> Option<SemVer> {
334 let candidates: Vec<&SemVer> = installed
335 .iter()
336 .filter(|v| version_satisfies(v, constraint))
337 .collect();
338
339 candidates.last().cloned().cloned()
341}
342
343pub fn version_satisfies(version: &SemVer, constraint: &PragmaConstraint) -> bool {
345 match constraint {
346 PragmaConstraint::Exact(v) => version == v,
347 PragmaConstraint::Caret(v) => {
348 version.major == v.major && version >= v && version.minor < v.minor + 1
351 }
352 PragmaConstraint::Gte(v) => version >= v,
353 PragmaConstraint::Range(lower, upper) => version >= lower && version < upper,
354 }
355}
356
357pub async fn resolve_remappings(config: &FoundryConfig) -> Vec<String> {
361 let output = Command::new("forge")
364 .arg("remappings")
365 .current_dir(&config.root)
366 .env("FOUNDRY_DISABLE_NIGHTLY_WARNING", "1")
367 .output()
368 .await;
369
370 if let Ok(output) = output
371 && output.status.success()
372 {
373 let stdout = String::from_utf8_lossy(&output.stdout);
374 let remappings: Vec<String> = stdout
375 .lines()
376 .filter(|l| !l.trim().is_empty())
377 .map(|l| l.to_string())
378 .collect();
379 if !remappings.is_empty() {
380 return remappings;
381 }
382 }
383
384 if !config.remappings.is_empty() {
386 return config.remappings.clone();
387 }
388
389 let remappings_txt = config.root.join("remappings.txt");
391 if let Ok(content) = std::fs::read_to_string(&remappings_txt) {
392 return content
393 .lines()
394 .filter(|l| !l.trim().is_empty())
395 .map(|l| l.to_string())
396 .collect();
397 }
398
399 Vec::new()
400}
401
402pub fn build_standard_json_input(
411 file_path: &str,
412 remappings: &[String],
413 config: &FoundryConfig,
414) -> Value {
415 let mut settings = json!({
416 "remappings": remappings,
417 "outputSelection": {
418 "*": {
419 "*": [
420 "abi",
421 "devdoc",
422 "userdoc",
423 "evm.methodIdentifiers",
424 "evm.gasEstimates"
425 ],
426 "": ["ast"]
427 }
428 }
429 });
430
431 if config.optimizer {
433 settings["optimizer"] = json!({
434 "enabled": true,
435 "runs": config.optimizer_runs
436 });
437 }
438
439 if config.via_ir {
441 settings["viaIR"] = json!(true);
442 }
443
444 if let Some(ref evm_version) = config.evm_version {
446 settings["evmVersion"] = json!(evm_version);
447 }
448
449 json!({
450 "language": "Solidity",
451 "sources": {
452 file_path: {
453 "urls": [file_path]
454 }
455 },
456 "settings": settings
457 })
458}
459
460pub async fn run_solc(
462 solc_binary: &Path,
463 input: &Value,
464 project_root: &Path,
465) -> Result<Value, RunnerError> {
466 let input_str = serde_json::to_string(input)?;
467
468 let mut child = Command::new(solc_binary)
469 .arg("--standard-json")
470 .current_dir(project_root)
471 .stdin(std::process::Stdio::piped())
472 .stdout(std::process::Stdio::piped())
473 .stderr(std::process::Stdio::piped())
474 .spawn()?;
475
476 if let Some(mut stdin) = child.stdin.take() {
478 use tokio::io::AsyncWriteExt;
479 stdin
480 .write_all(input_str.as_bytes())
481 .await
482 .map_err(RunnerError::CommandError)?;
483 }
485
486 let output = child
487 .wait_with_output()
488 .await
489 .map_err(RunnerError::CommandError)?;
490
491 let stdout = String::from_utf8_lossy(&output.stdout);
493 if stdout.trim().is_empty() {
494 let stderr = String::from_utf8_lossy(&output.stderr);
495 return Err(RunnerError::CommandError(std::io::Error::other(format!(
496 "solc produced no output, stderr: {stderr}"
497 ))));
498 }
499
500 let parsed: Value = serde_json::from_str(&stdout)?;
501 Ok(parsed)
502}
503
504pub fn normalize_solc_output(mut solc_output: Value, project_root: Option<&Path>) -> Value {
522 let mut result = Map::new();
523
524 let errors = solc_output
526 .get_mut("errors")
527 .map(Value::take)
528 .unwrap_or_else(|| json!([]));
529 result.insert("errors".to_string(), errors);
530
531 let resolve = |p: &str| -> String {
534 if let Some(root) = project_root {
535 let path = Path::new(p);
536 if path.is_relative() {
537 return root.join(path).to_string_lossy().into_owned();
538 }
539 }
540 p.to_string()
541 };
542
543 let mut source_id_to_path = Map::new();
546 let mut resolved_sources = Map::new();
547
548 if let Some(sources) = solc_output
549 .get_mut("sources")
550 .and_then(|s| s.as_object_mut())
551 {
552 let keys: Vec<String> = sources.keys().cloned().collect();
554 for key in keys {
555 if let Some(mut source_data) = sources.remove(&key) {
556 let abs_key = resolve(&key);
557
558 if let Some(ast) = source_data.get_mut("ast") {
560 if let Some(abs_path) = ast.get("absolutePath").and_then(|v| v.as_str()) {
561 let resolved = resolve(abs_path);
562 ast.as_object_mut()
563 .unwrap()
564 .insert("absolutePath".to_string(), json!(resolved));
565 }
566 }
567
568 if let Some(id) = source_data.get("id") {
569 source_id_to_path.insert(id.to_string(), json!(&abs_key));
570 }
571
572 resolved_sources.insert(abs_key, source_data);
573 }
574 }
575 }
576
577 result.insert("sources".to_string(), Value::Object(resolved_sources));
578
579 let mut resolved_contracts = Map::new();
581 if let Some(contracts) = solc_output
582 .get_mut("contracts")
583 .and_then(|c| c.as_object_mut())
584 {
585 let keys: Vec<String> = contracts.keys().cloned().collect();
586 for key in keys {
587 if let Some(contract_data) = contracts.remove(&key) {
588 resolved_contracts.insert(resolve(&key), contract_data);
589 }
590 }
591 }
592 result.insert("contracts".to_string(), Value::Object(resolved_contracts));
593
594 result.insert(
596 "source_id_to_path".to_string(),
597 Value::Object(source_id_to_path),
598 );
599
600 Value::Object(result)
601}
602
603pub fn normalize_forge_output(mut forge_output: Value) -> Value {
615 let mut result = Map::new();
616
617 let errors = forge_output
619 .get_mut("errors")
620 .map(Value::take)
621 .unwrap_or_else(|| json!([]));
622 result.insert("errors".to_string(), errors);
623
624 let mut normalized_sources = Map::new();
626 if let Some(sources) = forge_output
627 .get_mut("sources")
628 .and_then(|s| s.as_object_mut())
629 {
630 for (path, entries) in sources.iter_mut() {
631 if let Some(arr) = entries.as_array_mut()
632 && let Some(first) = arr.first_mut()
633 && let Some(sf) = first.get_mut("source_file")
634 {
635 normalized_sources.insert(path.clone(), sf.take());
636 }
637 }
638 }
639 result.insert("sources".to_string(), Value::Object(normalized_sources));
640
641 let mut normalized_contracts = Map::new();
643 if let Some(contracts) = forge_output
644 .get_mut("contracts")
645 .and_then(|c| c.as_object_mut())
646 {
647 for (path, names) in contracts.iter_mut() {
648 let mut path_contracts = Map::new();
649 if let Some(names_obj) = names.as_object_mut() {
650 for (name, entries) in names_obj.iter_mut() {
651 if let Some(arr) = entries.as_array_mut()
652 && let Some(first) = arr.first_mut()
653 && let Some(contract) = first.get_mut("contract")
654 {
655 path_contracts.insert(name.clone(), contract.take());
656 }
657 }
658 }
659 normalized_contracts.insert(path.clone(), Value::Object(path_contracts));
660 }
661 }
662 result.insert("contracts".to_string(), Value::Object(normalized_contracts));
663
664 let source_id_to_path = forge_output
666 .get_mut("build_infos")
667 .and_then(|bi| bi.as_array_mut())
668 .and_then(|arr| arr.first_mut())
669 .and_then(|info| info.get_mut("source_id_to_path"))
670 .map(Value::take)
671 .unwrap_or_else(|| json!({}));
672 result.insert("source_id_to_path".to_string(), source_id_to_path);
673
674 Value::Object(result)
675}
676
677pub async fn solc_ast(
682 file_path: &str,
683 config: &FoundryConfig,
684 client: Option<&tower_lsp::Client>,
685) -> Result<Value, RunnerError> {
686 let file_source = std::fs::read_to_string(file_path).ok();
688 let solc_binary = resolve_solc_binary(config, file_source.as_deref(), client).await;
689 let remappings = resolve_remappings(config).await;
690
691 let rel_path = Path::new(file_path)
696 .strip_prefix(&config.root)
697 .map(|p| p.to_string_lossy().into_owned())
698 .unwrap_or_else(|_| file_path.to_string());
699
700 let input = build_standard_json_input(&rel_path, &remappings, config);
701 let raw_output = run_solc(&solc_binary, &input, &config.root).await?;
702 Ok(normalize_solc_output(raw_output, Some(&config.root)))
703}
704
705pub async fn solc_build(
707 file_path: &str,
708 config: &FoundryConfig,
709 client: Option<&tower_lsp::Client>,
710) -> Result<Value, RunnerError> {
711 solc_ast(file_path, config, client).await
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn test_normalize_solc_sources() {
720 let solc_output = json!({
721 "sources": {
722 "src/Foo.sol": {
723 "id": 0,
724 "ast": {
725 "nodeType": "SourceUnit",
726 "absolutePath": "src/Foo.sol",
727 "id": 100
728 }
729 },
730 "src/Bar.sol": {
731 "id": 1,
732 "ast": {
733 "nodeType": "SourceUnit",
734 "absolutePath": "src/Bar.sol",
735 "id": 200
736 }
737 }
738 },
739 "contracts": {},
740 "errors": []
741 });
742
743 let normalized = normalize_solc_output(solc_output, None);
744
745 let sources = normalized.get("sources").unwrap().as_object().unwrap();
747 assert_eq!(sources.len(), 2);
748
749 let foo = sources.get("src/Foo.sol").unwrap();
750 assert_eq!(foo.get("id").unwrap(), 0);
751 assert_eq!(
752 foo.get("ast")
753 .unwrap()
754 .get("nodeType")
755 .unwrap()
756 .as_str()
757 .unwrap(),
758 "SourceUnit"
759 );
760
761 let id_to_path = normalized
763 .get("source_id_to_path")
764 .unwrap()
765 .as_object()
766 .unwrap();
767 assert_eq!(id_to_path.len(), 2);
768 }
769
770 #[test]
771 fn test_normalize_solc_contracts() {
772 let solc_output = json!({
773 "sources": {},
774 "contracts": {
775 "src/Foo.sol": {
776 "Foo": {
777 "abi": [{"type": "function", "name": "bar"}],
778 "evm": {
779 "methodIdentifiers": {
780 "bar(uint256)": "abcd1234"
781 },
782 "gasEstimates": {
783 "external": {"bar(uint256)": "200"}
784 }
785 }
786 }
787 }
788 },
789 "errors": []
790 });
791
792 let normalized = normalize_solc_output(solc_output, None);
793
794 let contracts = normalized.get("contracts").unwrap().as_object().unwrap();
796 let foo_contracts = contracts.get("src/Foo.sol").unwrap().as_object().unwrap();
797 let foo = foo_contracts.get("Foo").unwrap();
798
799 let method_ids = foo
800 .get("evm")
801 .unwrap()
802 .get("methodIdentifiers")
803 .unwrap()
804 .as_object()
805 .unwrap();
806 assert_eq!(
807 method_ids.get("bar(uint256)").unwrap().as_str().unwrap(),
808 "abcd1234"
809 );
810 }
811
812 #[test]
813 fn test_normalize_solc_errors_passthrough() {
814 let solc_output = json!({
815 "sources": {},
816 "contracts": {},
817 "errors": [{
818 "sourceLocation": {"file": "src/Foo.sol", "start": 0, "end": 10},
819 "type": "Warning",
820 "component": "general",
821 "severity": "warning",
822 "errorCode": "2394",
823 "message": "test warning",
824 "formattedMessage": "Warning: test warning"
825 }]
826 });
827
828 let normalized = normalize_solc_output(solc_output, None);
829
830 let errors = normalized.get("errors").unwrap().as_array().unwrap();
831 assert_eq!(errors.len(), 1);
832 assert_eq!(
833 errors[0].get("errorCode").unwrap().as_str().unwrap(),
834 "2394"
835 );
836 }
837
838 #[test]
839 fn test_normalize_empty_solc_output() {
840 let solc_output = json!({
841 "sources": {},
842 "contracts": {}
843 });
844
845 let normalized = normalize_solc_output(solc_output, None);
846
847 assert!(
848 normalized
849 .get("sources")
850 .unwrap()
851 .as_object()
852 .unwrap()
853 .is_empty()
854 );
855 assert!(
856 normalized
857 .get("contracts")
858 .unwrap()
859 .as_object()
860 .unwrap()
861 .is_empty()
862 );
863 assert_eq!(
864 normalized.get("errors").unwrap().as_array().unwrap().len(),
865 0
866 );
867 assert!(
868 normalized
869 .get("source_id_to_path")
870 .unwrap()
871 .as_object()
872 .unwrap()
873 .is_empty()
874 );
875 }
876
877 #[test]
878 fn test_build_standard_json_input() {
879 let config = FoundryConfig::default();
880 let input = build_standard_json_input(
881 "/path/to/Foo.sol",
882 &[
883 "ds-test/=lib/forge-std/lib/ds-test/src/".to_string(),
884 "forge-std/=lib/forge-std/src/".to_string(),
885 ],
886 &config,
887 );
888
889 let sources = input.get("sources").unwrap().as_object().unwrap();
890 assert!(sources.contains_key("/path/to/Foo.sol"));
891
892 let settings = input.get("settings").unwrap();
893 let remappings = settings.get("remappings").unwrap().as_array().unwrap();
894 assert_eq!(remappings.len(), 2);
895
896 let output_sel = settings.get("outputSelection").unwrap();
897 assert!(output_sel.get("*").is_some());
898
899 assert!(settings.get("optimizer").is_none());
901 assert!(settings.get("viaIR").is_none());
902 assert!(settings.get("evmVersion").is_none());
903 }
904
905 #[test]
906 fn test_build_standard_json_input_with_config() {
907 let config = FoundryConfig {
908 optimizer: true,
909 optimizer_runs: 9999999,
910 via_ir: true,
911 evm_version: Some("osaka".to_string()),
912 ..Default::default()
913 };
914 let input = build_standard_json_input("/path/to/Foo.sol", &[], &config);
915
916 let settings = input.get("settings").unwrap();
917
918 let optimizer = settings.get("optimizer").unwrap();
920 assert_eq!(optimizer.get("enabled").unwrap().as_bool().unwrap(), true);
921 assert_eq!(optimizer.get("runs").unwrap().as_u64().unwrap(), 9999999);
922
923 assert_eq!(settings.get("viaIR").unwrap().as_bool().unwrap(), true);
925
926 assert_eq!(
928 settings.get("evmVersion").unwrap().as_str().unwrap(),
929 "osaka"
930 );
931 }
932
933 #[tokio::test]
934 async fn test_resolve_solc_binary_default() {
935 let config = FoundryConfig::default();
936 let binary = resolve_solc_binary(&config, None, None).await;
937 assert_eq!(binary, PathBuf::from("solc"));
938 }
939
940 #[test]
941 fn test_parse_pragma_exact() {
942 let source = "// SPDX\npragma solidity 0.8.26;\n";
943 assert_eq!(
944 parse_pragma(source),
945 Some(PragmaConstraint::Exact(SemVer {
946 major: 0,
947 minor: 8,
948 patch: 26
949 }))
950 );
951 }
952
953 #[test]
954 fn test_parse_pragma_caret() {
955 let source = "pragma solidity ^0.8.0;\n";
956 assert_eq!(
957 parse_pragma(source),
958 Some(PragmaConstraint::Caret(SemVer {
959 major: 0,
960 minor: 8,
961 patch: 0
962 }))
963 );
964 }
965
966 #[test]
967 fn test_parse_pragma_gte() {
968 let source = "pragma solidity >=0.8.0;\n";
969 assert_eq!(
970 parse_pragma(source),
971 Some(PragmaConstraint::Gte(SemVer {
972 major: 0,
973 minor: 8,
974 patch: 0
975 }))
976 );
977 }
978
979 #[test]
980 fn test_parse_pragma_range() {
981 let source = "pragma solidity >=0.6.2 <0.9.0;\n";
982 assert_eq!(
983 parse_pragma(source),
984 Some(PragmaConstraint::Range(
985 SemVer {
986 major: 0,
987 minor: 6,
988 patch: 2
989 },
990 SemVer {
991 major: 0,
992 minor: 9,
993 patch: 0
994 },
995 ))
996 );
997 }
998
999 #[test]
1000 fn test_parse_pragma_none() {
1001 let source = "contract Foo {}\n";
1002 assert_eq!(parse_pragma(source), None);
1003 }
1004
1005 #[test]
1006 fn test_version_satisfies_exact() {
1007 let v = SemVer {
1008 major: 0,
1009 minor: 8,
1010 patch: 26,
1011 };
1012 assert!(version_satisfies(&v, &PragmaConstraint::Exact(v.clone())));
1013 assert!(!version_satisfies(
1014 &SemVer {
1015 major: 0,
1016 minor: 8,
1017 patch: 25
1018 },
1019 &PragmaConstraint::Exact(v)
1020 ));
1021 }
1022
1023 #[test]
1024 fn test_version_satisfies_caret() {
1025 let constraint = PragmaConstraint::Caret(SemVer {
1026 major: 0,
1027 minor: 8,
1028 patch: 0,
1029 });
1030 assert!(version_satisfies(
1031 &SemVer {
1032 major: 0,
1033 minor: 8,
1034 patch: 0
1035 },
1036 &constraint
1037 ));
1038 assert!(version_satisfies(
1039 &SemVer {
1040 major: 0,
1041 minor: 8,
1042 patch: 26
1043 },
1044 &constraint
1045 ));
1046 assert!(!version_satisfies(
1048 &SemVer {
1049 major: 0,
1050 minor: 9,
1051 patch: 0
1052 },
1053 &constraint
1054 ));
1055 assert!(!version_satisfies(
1057 &SemVer {
1058 major: 0,
1059 minor: 7,
1060 patch: 0
1061 },
1062 &constraint
1063 ));
1064 }
1065
1066 #[test]
1067 fn test_version_satisfies_gte() {
1068 let constraint = PragmaConstraint::Gte(SemVer {
1069 major: 0,
1070 minor: 8,
1071 patch: 0,
1072 });
1073 assert!(version_satisfies(
1074 &SemVer {
1075 major: 0,
1076 minor: 8,
1077 patch: 0
1078 },
1079 &constraint
1080 ));
1081 assert!(version_satisfies(
1082 &SemVer {
1083 major: 0,
1084 minor: 9,
1085 patch: 0
1086 },
1087 &constraint
1088 ));
1089 assert!(!version_satisfies(
1090 &SemVer {
1091 major: 0,
1092 minor: 7,
1093 patch: 0
1094 },
1095 &constraint
1096 ));
1097 }
1098
1099 #[test]
1100 fn test_version_satisfies_range() {
1101 let constraint = PragmaConstraint::Range(
1102 SemVer {
1103 major: 0,
1104 minor: 6,
1105 patch: 2,
1106 },
1107 SemVer {
1108 major: 0,
1109 minor: 9,
1110 patch: 0,
1111 },
1112 );
1113 assert!(version_satisfies(
1114 &SemVer {
1115 major: 0,
1116 minor: 6,
1117 patch: 2
1118 },
1119 &constraint
1120 ));
1121 assert!(version_satisfies(
1122 &SemVer {
1123 major: 0,
1124 minor: 8,
1125 patch: 26
1126 },
1127 &constraint
1128 ));
1129 assert!(!version_satisfies(
1131 &SemVer {
1132 major: 0,
1133 minor: 9,
1134 patch: 0
1135 },
1136 &constraint
1137 ));
1138 assert!(!version_satisfies(
1139 &SemVer {
1140 major: 0,
1141 minor: 6,
1142 patch: 1
1143 },
1144 &constraint
1145 ));
1146 }
1147
1148 #[test]
1149 fn test_find_matching_version() {
1150 let installed = vec![
1151 SemVer {
1152 major: 0,
1153 minor: 8,
1154 patch: 0,
1155 },
1156 SemVer {
1157 major: 0,
1158 minor: 8,
1159 patch: 20,
1160 },
1161 SemVer {
1162 major: 0,
1163 minor: 8,
1164 patch: 26,
1165 },
1166 SemVer {
1167 major: 0,
1168 minor: 8,
1169 patch: 33,
1170 },
1171 ];
1172 let constraint = PragmaConstraint::Caret(SemVer {
1174 major: 0,
1175 minor: 8,
1176 patch: 20,
1177 });
1178 let matched = find_matching_version(&constraint, &installed);
1179 assert_eq!(
1180 matched,
1181 Some(SemVer {
1182 major: 0,
1183 minor: 8,
1184 patch: 33
1185 })
1186 );
1187
1188 let constraint = PragmaConstraint::Exact(SemVer {
1190 major: 0,
1191 minor: 8,
1192 patch: 20,
1193 });
1194 let matched = find_matching_version(&constraint, &installed);
1195 assert_eq!(
1196 matched,
1197 Some(SemVer {
1198 major: 0,
1199 minor: 8,
1200 patch: 20
1201 })
1202 );
1203
1204 let constraint = PragmaConstraint::Exact(SemVer {
1206 major: 0,
1207 minor: 8,
1208 patch: 15,
1209 });
1210 let matched = find_matching_version(&constraint, &installed);
1211 assert_eq!(matched, None);
1212 }
1213
1214 #[test]
1215 fn test_list_installed_versions() {
1216 let versions = list_installed_versions();
1218 for w in versions.windows(2) {
1220 assert!(w[0] <= w[1]);
1221 }
1222 }
1223}