use forge_fmt::{format, parse, solang_ext::AstEq, FormatterConfig};
use itertools::Itertools;
use std::{fs, path::PathBuf};
use tracing_subscriber::{EnvFilter, FmtSubscriber};
fn tracing() {
let subscriber = FmtSubscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer()
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
}
fn test_directory(base_name: &str) {
tracing();
let mut original = None;
let tests =
fs::read_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join(base_name))
.unwrap()
.filter_map(|path| {
let path = path.unwrap().path();
let source = fs::read_to_string(&path).unwrap();
if let Some(filename) = path.file_name().and_then(|name| name.to_str()) {
if filename == "original.sol" {
original = Some(source);
} else if filename
.strip_suffix("fmt.sol")
.map(|filename| filename.strip_suffix('.'))
.is_some()
{
let default_config =
FormatterConfig { line_length: 80, ..Default::default() };
let mut config = toml::Value::try_from(&default_config).unwrap();
let config_table = config.as_table_mut().unwrap();
let mut lines = source.split('\n').peekable();
let mut line_num = 1;
while let Some(line) = lines.peek() {
let entry = line
.strip_prefix("//")
.and_then(|line| line.trim().strip_prefix("config:"))
.map(str::trim);
let entry = if let Some(entry) = entry { entry } else { break };
let values = match toml::from_str::<toml::Value>(entry) {
Ok(toml::Value::Table(table)) => table,
_ => panic!("Invalid config item in {filename} at {line_num}"),
};
config_table.extend(values);
line_num += 1;
lines.next();
}
let config = config
.try_into()
.unwrap_or_else(|err| panic!("Invalid config for {filename}: {err}"));
return Some((filename.to_string(), config, lines.join("\n")))
}
}
None
})
.collect::<Vec<_>>();
for (filename, config, formatted) in tests {
test_formatter(
&filename,
config,
original.as_ref().expect("original.sol not found"),
&formatted,
);
}
}
fn assert_eof(content: &str) {
assert!(content.ends_with('\n') && !content.ends_with("\n\n"));
}
fn test_formatter(filename: &str, config: FormatterConfig, source: &str, expected_source: &str) {
#[derive(Eq)]
struct PrettyString(String);
impl PartialEq for PrettyString {
fn eq(&self, other: &PrettyString) -> bool {
self.0.lines().eq(other.0.lines())
}
}
impl std::fmt::Debug for PrettyString {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(&self.0)
}
}
assert_eof(expected_source);
let source_parsed = parse(source).unwrap();
let expected_parsed = parse(expected_source).unwrap();
if !source_parsed.pt.ast_eq(&expected_parsed.pt) {
pretty_assertions::assert_eq!(
source_parsed.pt,
expected_parsed.pt,
"(formatted Parse Tree == expected Parse Tree) in {}",
filename
);
}
let expected = PrettyString(expected_source.to_string());
let mut source_formatted = String::new();
format(&mut source_formatted, source_parsed, config.clone()).unwrap();
assert_eof(&source_formatted);
let source_formatted = PrettyString(source_formatted);
pretty_assertions::assert_eq!(
source_formatted,
expected,
"(formatted == expected) in {}",
filename
);
let mut expected_formatted = String::new();
format(&mut expected_formatted, expected_parsed, config).unwrap();
assert_eof(&expected_formatted);
let expected_formatted = PrettyString(expected_formatted);
pretty_assertions::assert_eq!(
expected_formatted,
expected,
"(formatted == expected) in {}",
filename
);
}
macro_rules! test_directories {
($($dir:ident),+ $(,)?) => {$(
#[allow(non_snake_case)]
#[test]
fn $dir() {
test_directory(stringify!($dir));
}
)+};
}
test_directories! {
ConstructorDefinition,
ContractDefinition,
DocComments,
EnumDefinition,
ErrorDefinition,
EventDefinition,
FunctionDefinition,
FunctionType,
ImportDirective,
ModifierDefinition,
StatementBlock,
StructDefinition,
TypeDefinition,
UsingDirective,
VariableDefinition,
OperatorExpressions,
WhileStatement,
DoWhileStatement,
ForStatement,
IfStatement,
IfStatement2,
VariableAssignment,
FunctionCallArgsStatement,
RevertStatement,
RevertNamedArgsStatement,
ReturnStatement,
TryStatement,
ConditionalOperatorExpression,
NamedFunctionCallExpression,
ArrayExpressions,
UnitExpression,
ThisExpression,
SimpleComments,
LiteralExpression,
Yul,
YulStrings,
IntTypes,
InlineDisable,
NumberLiteralUnderscore,
FunctionCall,
TrailingComma,
PragmaDirective,
Annotation,
MappingType,
EmitStatement,
Repros,
}