ethers_solc/
utils.rs

1//! Utility functions
2
3use cfg_if::cfg_if;
4use std::{
5    collections::HashSet,
6    ops::Range,
7    path::{Component, Path, PathBuf},
8};
9
10use crate::{error::SolcError, SolcIoError};
11use once_cell::sync::Lazy;
12use regex::{Match, Regex};
13use semver::Version;
14use serde::de::DeserializeOwned;
15use tiny_keccak::{Hasher, Keccak};
16use walkdir::WalkDir;
17
18/// A regex that matches the import path and identifier of a solidity import
19/// statement with the named groups "path", "id".
20// Adapted from <https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L100>
21pub static RE_SOL_IMPORT: Lazy<Regex> = Lazy::new(|| {
22    Regex::new(r#"import\s+(?:(?:"(?P<p1>.*)"|'(?P<p2>.*)')(?:\s+as\s+\w+)?|(?:(?:\w+(?:\s+as\s+\w+)?|\*\s+as\s+\w+|\{\s*(?:\w+(?:\s+as\s+\w+)?(?:\s*,\s*)?)+\s*\})\s+from\s+(?:"(?P<p3>.*)"|'(?P<p4>.*)')))\s*;"#).unwrap()
23});
24
25/// A regex that matches an alias within an import statement
26pub static RE_SOL_IMPORT_ALIAS: Lazy<Regex> =
27    Lazy::new(|| Regex::new(r#"(?:(?P<target>\w+)|\*|'|")\s+as\s+(?P<alias>\w+)"#).unwrap());
28
29/// A regex that matches the version part of a solidity pragma
30/// as follows: `pragma solidity ^0.5.2;` => `^0.5.2`
31/// statement with the named group "version".
32// Adapted from <https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L119>
33pub static RE_SOL_PRAGMA_VERSION: Lazy<Regex> =
34    Lazy::new(|| Regex::new(r"pragma\s+solidity\s+(?P<version>.+?);").unwrap());
35
36/// A regex that matches the SDPX license identifier
37/// statement with the named group "license".
38pub static RE_SOL_SDPX_LICENSE_IDENTIFIER: Lazy<Regex> =
39    Lazy::new(|| Regex::new(r"///?\s*SPDX-License-Identifier:\s*(?P<license>.+)").unwrap());
40
41/// A regex used to remove extra lines in flatenned files
42pub static RE_THREE_OR_MORE_NEWLINES: Lazy<Regex> = Lazy::new(|| Regex::new("\n{3,}").unwrap());
43
44/// Create a regex that matches any library or contract name inside a file
45pub fn create_contract_or_lib_name_regex(name: &str) -> Regex {
46    Regex::new(&format!(r#"(?:using\s+(?P<n1>{name})\s+|is\s+(?:\w+\s*,\s*)*(?P<n2>{name})(?:\s*,\s*\w+)*|(?:(?P<ignore>(?:function|error|as)\s+|\n[^\n]*(?:"([^"\n]|\\")*|'([^'\n]|\\')*))|\W+)(?P<n3>{name})(?:\.|\(| ))"#)).unwrap()
47}
48
49/// Move a range by a specified offset
50pub fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
51    Range {
52        start: offset.saturating_add(range.start as isize) as usize,
53        end: offset.saturating_add(range.end as isize) as usize,
54    }
55}
56
57/// Returns all path parts from any solidity import statement in a string,
58/// `import "./contracts/Contract.sol";` -> `"./contracts/Contract.sol"`.
59///
60/// See also <https://docs.soliditylang.org/en/v0.8.9/grammar.html>
61pub fn find_import_paths(contract: &str) -> impl Iterator<Item = Match> {
62    RE_SOL_IMPORT.captures_iter(contract).filter_map(|cap| {
63        cap.name("p1")
64            .or_else(|| cap.name("p2"))
65            .or_else(|| cap.name("p3"))
66            .or_else(|| cap.name("p4"))
67    })
68}
69
70/// Returns the solidity version pragma from the given input:
71/// `pragma solidity ^0.5.2;` => `^0.5.2`
72pub fn find_version_pragma(contract: &str) -> Option<Match> {
73    RE_SOL_PRAGMA_VERSION.captures(contract)?.name("version")
74}
75
76/// Returns an iterator that yields all solidity/yul files funder under the given root path or the
77/// `root` itself, if it is a sol/yul file
78///
79/// This also follows symlinks.
80pub fn source_files_iter(root: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
81    WalkDir::new(root)
82        .follow_links(true)
83        .into_iter()
84        .filter_map(Result::ok)
85        .filter(|e| e.file_type().is_file())
86        .filter(|e| {
87            e.path().extension().map(|ext| (ext == "sol") || (ext == "yul")).unwrap_or_default()
88        })
89        .map(|e| e.path().into())
90}
91
92/// Returns a list of absolute paths to all the solidity files under the root, or the file itself,
93/// if the path is a solidity file.
94///
95/// This also follows symlinks.
96///
97/// NOTE: this does not resolve imports from other locations
98///
99/// # Example
100///
101/// ```no_run
102/// use ethers_solc::utils;
103/// let sources = utils::source_files("./contracts");
104/// ```
105pub fn source_files(root: impl AsRef<Path>) -> Vec<PathBuf> {
106    source_files_iter(root).collect()
107}
108
109/// Returns a list of _unique_ paths to all folders under `root` that contain at least one solidity
110/// file (`*.sol`).
111///
112/// # Example
113///
114/// ```no_run
115/// use ethers_solc::utils;
116/// let dirs = utils::solidity_dirs("./lib");
117/// ```
118///
119/// for following layout will return
120/// `["lib/ds-token/src", "lib/ds-token/src/test", "lib/ds-token/lib/ds-math/src", ...]`
121///
122/// ```text
123/// lib
124/// └── ds-token
125///     ├── lib
126///     │   ├── ds-math
127///     │   │   └── src/Contract.sol
128///     │   ├── ds-stop
129///     │   │   └── src/Contract.sol
130///     │   ├── ds-test
131///     │       └── src//Contract.sol
132///     └── src
133///         ├── base.sol
134///         ├── test
135///         │   ├── base.t.sol
136///         └── token.sol
137/// ```
138pub fn solidity_dirs(root: impl AsRef<Path>) -> Vec<PathBuf> {
139    let sources = source_files(root);
140    sources
141        .iter()
142        .filter_map(|p| p.parent())
143        .collect::<HashSet<_>>()
144        .into_iter()
145        .map(|p| p.to_path_buf())
146        .collect()
147}
148
149/// Returns the source name for the given source path, the ancestors of the root path
150/// `/Users/project/sources/contract.sol` -> `sources/contracts.sol`
151pub fn source_name(source: &Path, root: impl AsRef<Path>) -> &Path {
152    source.strip_prefix(root.as_ref()).unwrap_or(source)
153}
154
155/// Attempts to determine if the given source is a local, relative import
156pub fn is_local_source_name(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> bool {
157    resolve_library(libs, source).is_none()
158}
159
160/// Canonicalize the path, platform-agnostic
161///
162/// On windows this will ensure the path only consists of `/` separators
163pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
164    let path = path.as_ref();
165    cfg_if! {
166        if #[cfg(windows)] {
167            let res = dunce::canonicalize(path).map(|p| {
168                use path_slash::PathBufExt;
169                PathBuf::from(p.to_slash_lossy().as_ref())
170            });
171        } else {
172         let res = dunce::canonicalize(path);
173        }
174    };
175
176    res.map_err(|err| SolcIoError::new(err, path))
177}
178
179/// Returns the same path config but with canonicalized paths.
180///
181/// This will take care of potential symbolic linked directories.
182/// For example, the tempdir library is creating directories hosted under `/var/`, which in OS X
183/// is a symbolic link to `/private/var/`. So if when we try to resolve imports and a path is
184/// rooted in a symbolic directory we might end up with different paths for the same file, like
185/// `private/var/.../Dapp.sol` and `/var/.../Dapp.sol`
186///
187/// This canonicalizes all the paths but does not treat non existing dirs as an error
188pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
189    let path = path.into();
190    canonicalize(&path).unwrap_or(path)
191}
192
193/// Returns the path to the library if the source path is in fact determined to be a library path,
194/// and it exists.
195/// Note: this does not handle relative imports or remappings.
196pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> Option<PathBuf> {
197    let source = source.as_ref();
198    let comp = source.components().next()?;
199    match comp {
200        Component::Normal(first_dir) => {
201            // attempt to verify that the root component of this source exists under a library
202            // folder
203            for lib in libs {
204                let lib = lib.as_ref();
205                let contract = lib.join(source);
206                if contract.exists() {
207                    // contract exists in <lib>/<source>
208                    return Some(contract)
209                }
210                // check for <lib>/<first_dir>/src/name.sol
211                let contract = lib
212                    .join(first_dir)
213                    .join("src")
214                    .join(source.strip_prefix(first_dir).expect("is first component"));
215                if contract.exists() {
216                    return Some(contract)
217                }
218            }
219            None
220        }
221        Component::RootDir => Some(source.into()),
222        _ => None,
223    }
224}
225
226/// Tries to find an absolute import like `src/interfaces/IConfig.sol` in `cwd`, moving up the path
227/// until the `root` is reached.
228///
229/// If an existing file under `root` is found, this returns the path up to the `import` path and the
230/// canonicalized `import` path itself:
231///
232/// For example for following layout:
233///
234/// ```text
235/// <root>/mydependency/
236/// ├── src (`cwd`)
237/// │   ├── interfaces
238/// │   │   ├── IConfig.sol
239/// ```
240/// and `import` as `src/interfaces/IConfig.sol` and `cwd` as `src` this will return
241/// (`<root>/mydependency/`, `<root>/mydependency/src/interfaces/IConfig.sol`)
242pub fn resolve_absolute_library(
243    root: &Path,
244    cwd: &Path,
245    import: &Path,
246) -> Option<(PathBuf, PathBuf)> {
247    let mut parent = cwd.parent()?;
248    while parent != root {
249        if let Ok(import) = canonicalize(parent.join(import)) {
250            return Some((parent.to_path_buf(), import))
251        }
252        parent = parent.parent()?;
253    }
254    None
255}
256
257/// Reads the list of Solc versions that have been installed in the machine. The version list is
258/// sorted in ascending order.
259/// Checks for installed solc versions under the given path as
260/// `<root>/<major.minor.path>`, (e.g.: `~/.svm/0.8.10`)
261/// and returns them sorted in ascending order
262pub fn installed_versions(root: impl AsRef<Path>) -> Result<Vec<Version>, SolcError> {
263    let mut versions: Vec<_> = walkdir::WalkDir::new(root)
264        .max_depth(1)
265        .into_iter()
266        .filter_map(std::result::Result::ok)
267        .filter(|e| e.file_type().is_dir())
268        .filter_map(|e: walkdir::DirEntry| {
269            e.path().file_name().and_then(|v| Version::parse(v.to_string_lossy().as_ref()).ok())
270        })
271        .collect();
272    versions.sort();
273    Ok(versions)
274}
275
276/// Returns the 36 char (deprecated) fully qualified name placeholder
277///
278/// If the name is longer than 36 char, then the name gets truncated,
279/// If the name is shorter than 36 char, then the name is filled with trailing `_`
280pub fn library_fully_qualified_placeholder(name: impl AsRef<str>) -> String {
281    name.as_ref().chars().chain(std::iter::repeat('_')).take(36).collect()
282}
283
284/// Returns the library hash placeholder as `$hex(library_hash(name))$`
285pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String {
286    let mut s = String::with_capacity(34 + 2);
287    s.push('$');
288    s.push_str(hex::Buffer::<17, false>::new().format(&library_hash(name)));
289    s.push('$');
290    s
291}
292
293/// Returns the library placeholder for the given name
294/// The placeholder is a 34 character prefix of the hex encoding of the keccak256 hash of the fully
295/// qualified library name.
296///
297/// See also <https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking>
298pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] {
299    let mut output = [0u8; 17];
300    let mut hasher = Keccak::v256();
301    hasher.update(name.as_ref());
302    hasher.finalize(&mut output);
303    output
304}
305
306/// Find the common ancestor, if any, between the given paths
307///
308/// # Example
309///
310/// ```rust
311/// use std::path::{PathBuf, Path};
312///
313/// # fn main() {
314/// use ethers_solc::utils::common_ancestor_all;
315/// let baz = Path::new("/foo/bar/baz");
316/// let bar = Path::new("/foo/bar/bar");
317/// let foo = Path::new("/foo/bar/foo");
318/// let common = common_ancestor_all(vec![baz, bar, foo]).unwrap();
319/// assert_eq!(common, Path::new("/foo/bar").to_path_buf());
320/// # }
321/// ```
322pub fn common_ancestor_all<I, P>(paths: I) -> Option<PathBuf>
323where
324    I: IntoIterator<Item = P>,
325    P: AsRef<Path>,
326{
327    let mut iter = paths.into_iter();
328    let mut ret = iter.next()?.as_ref().to_path_buf();
329    for path in iter {
330        if let Some(r) = common_ancestor(ret, path.as_ref()) {
331            ret = r;
332        } else {
333            return None
334        }
335    }
336    Some(ret)
337}
338
339/// Finds the common ancestor of both paths
340///
341/// # Example
342///
343/// ```rust
344/// use std::path::{PathBuf, Path};
345///
346/// # fn main() {
347/// use ethers_solc::utils::common_ancestor;
348/// let foo = Path::new("/foo/bar/foo");
349/// let bar = Path::new("/foo/bar/bar");
350/// let ancestor = common_ancestor(foo, bar).unwrap();
351/// assert_eq!(ancestor, Path::new("/foo/bar").to_path_buf());
352/// # }
353/// ```
354pub fn common_ancestor(a: impl AsRef<Path>, b: impl AsRef<Path>) -> Option<PathBuf> {
355    let a = a.as_ref().components();
356    let b = b.as_ref().components();
357    let mut ret = PathBuf::new();
358    let mut found = false;
359    for (c1, c2) in a.zip(b) {
360        if c1 == c2 {
361            ret.push(c1);
362            found = true;
363        } else {
364            break
365        }
366    }
367    if found {
368        Some(ret)
369    } else {
370        None
371    }
372}
373
374/// Returns the right subpath in a dir
375///
376/// Returns `<root>/<fave>` if it exists or `<root>/<alt>` does not exist,
377/// Returns `<root>/<alt>` if it exists and `<root>/<fave>` does not exist.
378pub(crate) fn find_fave_or_alt_path(root: impl AsRef<Path>, fave: &str, alt: &str) -> PathBuf {
379    let root = root.as_ref();
380    let p = root.join(fave);
381    if !p.exists() {
382        let alt = root.join(alt);
383        if alt.exists() {
384            return alt
385        }
386    }
387    p
388}
389
390/// Attempts to find a file with different case that exists next to the `non_existing` file
391pub(crate) fn find_case_sensitive_existing_file(non_existing: &Path) -> Option<PathBuf> {
392    let non_existing_file_name = non_existing.file_name()?;
393    let parent = non_existing.parent()?;
394    WalkDir::new(parent)
395        .max_depth(1)
396        .into_iter()
397        .filter_map(Result::ok)
398        .filter(|e| e.file_type().is_file())
399        .find_map(|e| {
400            let existing_file_name = e.path().file_name()?;
401            if existing_file_name.eq_ignore_ascii_case(non_existing_file_name) &&
402                existing_file_name != non_existing_file_name
403            {
404                return Some(e.path().to_path_buf())
405            }
406            None
407        })
408}
409
410#[cfg(not(target_arch = "wasm32"))]
411use tokio::runtime::{Handle, Runtime};
412
413#[cfg(not(target_arch = "wasm32"))]
414#[derive(Debug)]
415pub enum RuntimeOrHandle {
416    Runtime(Runtime),
417    Handle(Handle),
418}
419
420#[cfg(not(target_arch = "wasm32"))]
421impl Default for RuntimeOrHandle {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427#[cfg(not(target_arch = "wasm32"))]
428impl RuntimeOrHandle {
429    pub fn new() -> RuntimeOrHandle {
430        match Handle::try_current() {
431            Ok(handle) => RuntimeOrHandle::Handle(handle),
432            Err(_) => RuntimeOrHandle::Runtime(Runtime::new().expect("Failed to start runtime")),
433        }
434    }
435
436    pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
437        match &self {
438            RuntimeOrHandle::Runtime(runtime) => runtime.block_on(f),
439            RuntimeOrHandle::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
440        }
441    }
442}
443
444/// Creates a new named tempdir
445#[cfg(any(test, feature = "project-util"))]
446pub(crate) fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
447    tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
448}
449
450/// Reads the json file and deserialize it into the provided type
451pub fn read_json_file<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T, SolcError> {
452    let path = path.as_ref();
453    let contents = std::fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
454    serde_json::from_str(&contents).map_err(Into::into)
455}
456
457/// Creates the parent directory of the `file` and all its ancestors if it does not exist
458/// See [`std::fs::create_dir_all()`]
459pub fn create_parent_dir_all(file: impl AsRef<Path>) -> Result<(), SolcError> {
460    let file = file.as_ref();
461    if let Some(parent) = file.parent() {
462        std::fs::create_dir_all(parent).map_err(|err| {
463            SolcError::msg(format!(
464                "Failed to create artifact parent folder \"{}\": {}",
465                parent.display(),
466                err
467            ))
468        })?;
469    }
470    Ok(())
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use solang_parser::pt::SourceUnitPart;
477    use std::fs::{create_dir_all, File};
478
479    #[test]
480    fn can_find_different_case() {
481        let tmp_dir = tempdir("out").unwrap();
482        let path = tmp_dir.path().join("forge-std");
483        create_dir_all(&path).unwrap();
484        let existing = path.join("Test.sol");
485        let non_existing = path.join("test.sol");
486        std::fs::write(&existing, b"").unwrap();
487
488        #[cfg(target_os = "linux")]
489        assert!(!non_existing.exists());
490
491        let found = find_case_sensitive_existing_file(&non_existing).unwrap();
492        assert_eq!(found, existing);
493    }
494
495    #[cfg(target_os = "linux")]
496    #[test]
497    fn can_read_different_case() {
498        let tmp_dir = tempdir("out").unwrap();
499        let path = tmp_dir.path().join("forge-std");
500        create_dir_all(&path).unwrap();
501        let existing = path.join("Test.sol");
502        let non_existing = path.join("test.sol");
503        std::fs::write(
504            existing,
505            "
506pragma solidity ^0.8.10;
507contract A {}
508        ",
509        )
510        .unwrap();
511
512        assert!(!non_existing.exists());
513
514        let found = crate::resolver::Node::read(&non_existing).unwrap_err();
515        matches!(found, SolcError::ResolveCaseSensitiveFileName { .. });
516    }
517
518    #[test]
519    fn can_create_parent_dirs_with_ext() {
520        let tmp_dir = tempdir("out").unwrap();
521        let path = tmp_dir.path().join("IsolationModeMagic.sol/IsolationModeMagic.json");
522        create_parent_dir_all(&path).unwrap();
523        assert!(path.parent().unwrap().is_dir());
524    }
525
526    #[test]
527    fn can_create_parent_dirs_versioned() {
528        let tmp_dir = tempdir("out").unwrap();
529        let path = tmp_dir.path().join("IVersioned.sol/IVersioned.0.8.16.json");
530        create_parent_dir_all(&path).unwrap();
531        assert!(path.parent().unwrap().is_dir());
532        let path = tmp_dir.path().join("IVersioned.sol/IVersioned.json");
533        create_parent_dir_all(&path).unwrap();
534        assert!(path.parent().unwrap().is_dir());
535    }
536
537    #[test]
538    fn can_determine_local_paths() {
539        assert!(is_local_source_name(&[""], "./local/contract.sol"));
540        assert!(is_local_source_name(&[""], "../local/contract.sol"));
541        assert!(!is_local_source_name(&[""], "/ds-test/test.sol"));
542
543        let tmp_dir = tempdir("contracts").unwrap();
544        let dir = tmp_dir.path().join("ds-test");
545        create_dir_all(&dir).unwrap();
546        File::create(dir.join("test.sol")).unwrap();
547
548        assert!(!is_local_source_name(&[tmp_dir.path()], "ds-test/test.sol"));
549    }
550
551    #[test]
552    fn can_find_solidity_sources() {
553        let tmp_dir = tempdir("contracts").unwrap();
554
555        let file_a = tmp_dir.path().join("a.sol");
556        let file_b = tmp_dir.path().join("a.sol");
557        let nested = tmp_dir.path().join("nested");
558        let file_c = nested.join("c.sol");
559        let nested_deep = nested.join("deep");
560        let file_d = nested_deep.join("d.sol");
561        File::create(&file_a).unwrap();
562        File::create(&file_b).unwrap();
563        create_dir_all(nested_deep).unwrap();
564        File::create(&file_c).unwrap();
565        File::create(&file_d).unwrap();
566
567        let files: HashSet<_> = source_files(tmp_dir.path()).into_iter().collect();
568        let expected: HashSet<_> = [file_a, file_b, file_c, file_d].into();
569        assert_eq!(files, expected);
570    }
571
572    #[test]
573    fn can_parse_curly_bracket_imports() {
574        let s =
575            r#"import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";"#;
576
577        let (unit, _) = solang_parser::parse(s, 0).unwrap();
578        assert_eq!(unit.0.len(), 1);
579        match unit.0[0] {
580            SourceUnitPart::ImportDirective(_) => {}
581            _ => unreachable!("failed to parse import"),
582        }
583        let imports: Vec<_> = find_import_paths(s).map(|m| m.as_str()).collect();
584
585        assert_eq!(imports, vec!["@openzeppelin/contracts/utils/ReentrancyGuard.sol"])
586    }
587
588    #[test]
589    fn can_find_single_quote_imports() {
590        let content = r"
591// SPDX-License-Identifier: MIT
592pragma solidity 0.8.6;
593
594import '@openzeppelin/contracts/access/Ownable.sol';
595import '@openzeppelin/contracts/utils/Address.sol';
596
597import './../interfaces/IJBDirectory.sol';
598import './../libraries/JBTokens.sol';
599        ";
600        let imports: Vec<_> = find_import_paths(content).map(|m| m.as_str()).collect();
601
602        assert_eq!(
603            imports,
604            vec![
605                "@openzeppelin/contracts/access/Ownable.sol",
606                "@openzeppelin/contracts/utils/Address.sol",
607                "./../interfaces/IJBDirectory.sol",
608                "./../libraries/JBTokens.sol",
609            ]
610        );
611    }
612
613    #[test]
614    fn can_find_import_paths() {
615        let s = r#"//SPDX-License-Identifier: Unlicense
616pragma solidity ^0.8.0;
617import "hardhat/console.sol";
618import "../contract/Contract.sol";
619import { T } from "../Test.sol";
620import { T } from '../Test2.sol';
621"#;
622        assert_eq!(
623            vec!["hardhat/console.sol", "../contract/Contract.sol", "../Test.sol", "../Test2.sol"],
624            find_import_paths(s).map(|m| m.as_str()).collect::<Vec<&str>>()
625        );
626    }
627    #[test]
628    fn can_find_version() {
629        let s = r"//SPDX-License-Identifier: Unlicense
630pragma solidity ^0.8.0;
631";
632        assert_eq!(Some("^0.8.0"), find_version_pragma(s).map(|s| s.as_str()));
633    }
634
635    #[test]
636    fn can_find_ancestor() {
637        let a = Path::new("/foo/bar/bar/test.txt");
638        let b = Path::new("/foo/bar/foo/example/constract.sol");
639        let expected = Path::new("/foo/bar");
640        assert_eq!(common_ancestor(a, b).unwrap(), expected.to_path_buf())
641    }
642
643    #[test]
644    fn no_common_ancestor_path() {
645        let a = Path::new("/foo/bar");
646        let b = Path::new("./bar/foo");
647        assert!(common_ancestor(a, b).is_none());
648    }
649
650    #[test]
651    fn can_find_all_ancestor() {
652        let a = Path::new("/foo/bar/foo/example.txt");
653        let b = Path::new("/foo/bar/foo/test.txt");
654        let c = Path::new("/foo/bar/bar/foo/bar");
655        let expected = Path::new("/foo/bar");
656        let paths = vec![a, b, c];
657        assert_eq!(common_ancestor_all(paths).unwrap(), expected.to_path_buf())
658    }
659}