// SPDX-License-Identifier: Apache-2.0 use path_slash::PathExt; use rayon::prelude::*; use solang::{ abi::generate_abi, codegen, file_resolver::FileResolver, parse_and_resolve, sema::{ast::Namespace, file::PathDisplay}, Target, }; use solang_parser::diagnostics::Level; use std::{ ffi::OsStr, fs::{read_dir, File}, io::{self, BufRead, BufReader, Read}, path::{Path, PathBuf}, }; #[test] fn solana_contracts() -> io::Result<()> { contract_tests("tests/contract_testcases/solana", Target::Solana) } #[test] fn polkadot_contracts() -> io::Result<()> { contract_tests( "tests/contract_testcases/polkadot", Target::default_polkadot(), ) } #[test] fn evm_contracts() -> io::Result<()> { contract_tests("tests/contract_testcases/evm", Target::EVM) } fn contract_tests(file_path: &str, target: Target) -> io::Result<()> { let path = PathBuf::from(file_path); recurse_directory(path, target) } fn recurse_directory(path: PathBuf, target: Target) -> io::Result<()> { let mut entries = Vec::new(); for entry in read_dir(path)? { let path = entry?.path(); if path.is_dir() { recurse_directory(path, target)?; } else if let Some(ext) = path.extension() { if ext.to_string_lossy() == "sol" { entries.push(path); } } } entries.into_par_iter().for_each(|entry| { parse_file(entry, target).unwrap(); }); Ok(()) } fn parse_file(path: PathBuf, target: Target) -> io::Result<()> { let mut cache = FileResolver::default(); let filename = add_file(&mut cache, &path, target)?; let mut ns = parse_and_resolve(OsStr::new(&filename), &mut cache, target); if !ns.diagnostics.any_errors() { // codegen all the contracts codegen::codegen( &mut ns, &codegen::Options { opt_level: codegen::OptimizationLevel::Default, ..Default::default() }, ); } #[cfg(windows)] { for file in &mut ns.files { let filename = file.path.to_slash_lossy().to_string(); file.path = PathBuf::from(filename); } } check_diagnostics(&path, &ns); if !ns.diagnostics.any_errors() { // let's try and emit for contract_no in 0..ns.contracts.len() { let contract = &ns.contracts[contract_no]; if contract.instantiable { let code = match ns.target { Target::Solana | Target::Polkadot { .. } => { contract.emit(&ns, &Default::default()) } Target::EVM => b"beep".to_vec(), }; let _ = generate_abi(contract_no, &ns, &code, false, &["unknown".into()], "0.1.0"); } } } Ok(()) } fn add_file(cache: &mut FileResolver, path: &Path, target: Target) -> io::Result { let mut file = File::open(path)?; let mut source = String::new(); file.read_to_string(&mut source)?; // make sure the path uses unix file separators, this is what the dot file uses let filename = path.to_slash_lossy(); println!("Parsing {filename} for {target}"); // The files may have had their end of lines mangled on Windows cache.set_file_contents(&filename, source.replace("\r\n", "\n")); for line in source.lines() { if line.starts_with("import") { if let (Some(start), Some(end)) = (line.find('"'), line.rfind('"')) { let file = &line[start + 1..end]; if !file.is_empty() && file != "solana" { let mut import_path = path.parent().unwrap().to_path_buf(); import_path.push(file); println!("adding import {}", import_path.display()); add_file(cache, &import_path, target)?; } } } } Ok(filename.to_string()) } fn check_diagnostics(path: &Path, ns: &Namespace) { let mut expected = "// ---- Expect: diagnostics ----\n".to_owned(); for diag in ns.diagnostics.iter() { if diag.level == Level::Warning || diag.level == Level::Error { expected.push_str(&format!( "// {}: {}: {}\n", diag.level, ns.loc_to_string(PathDisplay::None, &diag.loc), diag.message )); for note in &diag.notes { expected.push_str(&format!( "// \tnote {}: {}\n", ns.loc_to_string(PathDisplay::None, ¬e.loc), note.message )); } } } let mut found = String::new(); let file = File::open(path).unwrap(); for line in BufReader::new(file).lines() { let line = line.unwrap(); if found.is_empty() && !line.starts_with("// ---- Expect: diagnostics ----") { continue; } found.push_str(&line); found.push('\n'); } assert!(!found.is_empty()); assert_eq!(found, expected, "source: {}", path.display()); }