Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 47 additions & 27 deletions forc-pkg/src/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2041,36 +2041,56 @@ impl PkgTestEntry {
let span = decl_ref.span();
let test_function_decl = engines.de().get_function(decl_ref);

let Some(test_attr) = test_function_decl.attributes.test() else {
unreachable!("`test_function_decl` is guaranteed to be a test function and it must have a `#[test]` attribute");
};
let test_attr = test_function_decl.attributes.test();
let fuzz_attr = test_function_decl.attributes.fuzz();

// With fixture-based testing, #[test] is mandatory and can coexist with #[fuzz] and #[case]
let has_case = test_function_decl.attributes.cases().next().is_some();
let has_parameterization = fuzz_attr.is_some() || has_case;

if test_attr.is_none() && has_parameterization {
bail!(
"Function \"{}\" has parameterization attributes (#[fuzz] or #[case]) but is missing the required #[test] attribute",
test_function_decl.name
);
}

if test_attr.is_none() && !has_parameterization {
unreachable!("`test_function_decl` is guaranteed to be a test or fuzz function and it must have a `#[test]` attribute or parameterization attributes");
}

let pass_condition = match test_attr
.args
.iter()
// Last "should_revert" argument wins ;-)
.rfind(|arg| arg.is_test_should_revert())
{
Some(should_revert_arg) => {
match should_revert_arg.get_string_opt(&Handler::default()) {
Ok(should_revert_arg_value) => TestPassCondition::ShouldRevert(
should_revert_arg_value
.map(|val| val.parse::<u64>())
.transpose()
.map_err(|_| {
anyhow!(get_invalid_revert_code_error_msg(
&test_function_decl.name,
should_revert_arg
))
})?,
),
Err(_) => bail!(get_invalid_revert_code_error_msg(
&test_function_decl.name,
should_revert_arg
)),
let pass_condition = if let Some(test_attr) = test_attr {
// Handle #[test] attributes
match test_attr
.args
.iter()
// Last "should_revert" argument wins ;-)
.rfind(|arg| arg.is_test_should_revert())
{
Some(should_revert_arg) => {
match should_revert_arg.get_string_opt(&Handler::default()) {
Ok(should_revert_arg_value) => TestPassCondition::ShouldRevert(
should_revert_arg_value
.map(|val| val.parse::<u64>())
.transpose()
.map_err(|_| {
anyhow!(get_invalid_revert_code_error_msg(
&test_function_decl.name,
should_revert_arg
))
})?,
),
Err(_) => bail!(get_invalid_revert_code_error_msg(
&test_function_decl.name,
should_revert_arg
)),
}
}
None => TestPassCondition::ShouldNotRevert,
}
None => TestPassCondition::ShouldNotRevert,
} else {
// Handle #[fuzz] attributes - fuzz tests shouldn't revert by default
TestPassCondition::ShouldNotRevert
};

let file_path =
Expand Down
8 changes: 8 additions & 0 deletions sway-ast/src/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ pub const DOC_COMMENT_ATTRIBUTE_NAME: &str = "doc-comment";
pub const TEST_ATTRIBUTE_NAME: &str = "test";
pub const TEST_SHOULD_REVERT_ARG_NAME: &str = "should_revert";

// In-language parameterized testing.
pub const CASE_ATTRIBUTE_NAME: &str = "case";

// In-language fuzz testing.
pub const FUZZ_ATTRIBUTE_NAME: &str = "fuzz";

// Allow warnings.
pub const ALLOW_ATTRIBUTE_NAME: &str = "allow";
pub const ALLOW_DEAD_CODE_ARG_NAME: &str = "dead_code";
Expand Down Expand Up @@ -65,13 +71,15 @@ pub const KNOWN_ATTRIBUTE_NAMES: &[&str] = &[
STORAGE_ATTRIBUTE_NAME,
DOC_COMMENT_ATTRIBUTE_NAME,
TEST_ATTRIBUTE_NAME,
CASE_ATTRIBUTE_NAME,
INLINE_ATTRIBUTE_NAME,
PAYABLE_ATTRIBUTE_NAME,
ALLOW_ATTRIBUTE_NAME,
CFG_ATTRIBUTE_NAME,
DEPRECATED_ATTRIBUTE_NAME,
FALLBACK_ATTRIBUTE_NAME,
ABI_NAME_ATTRIBUTE_NAME,
FUZZ_ATTRIBUTE_NAME,
];

/// An attribute declaration. Attribute declaration
Expand Down
1 change: 1 addition & 0 deletions sway-core/src/language/ty/ast_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ impl TyAstNode {
let fn_decl = decl_engine.get_function(decl_id);
let TyFunctionDecl { attributes, .. } = &*fn_decl;
attributes.has_any_of_kind(AttributeKind::Test)
|| attributes.has_any_of_kind(AttributeKind::Fuzz)
}
_ => false,
}
Expand Down
78 changes: 50 additions & 28 deletions sway-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,42 +218,42 @@ pub(crate) fn attr_decls_to_attributes(
let first_doc_line = attributes
.next()
.expect("`chunk_by` guarantees existence of at least one element in the chunk");
if !can_annotate(first_doc_line) {
let last_doc_line = match attributes.last() {
Some(last_attr) => last_attr,
// There is only one doc line in the complete doc comment.
None => first_doc_line,
};
if can_annotate(first_doc_line) {
continue;
}

let last_doc_line = attributes.last().unwrap_or(first_doc_line);
handler.emit_err(
ConvertParseTreeError::InvalidAttributeTarget {
span: Span::join(
first_doc_line.span.clone(),
&last_doc_line.span.start_span(),
),
attribute: first_doc_line.name.clone(),
target_friendly_name,
can_only_annotate_help: first_doc_line
.can_only_annotate_help(target_friendly_name),
}
.into(),
);
} else {
// For other attributes, the error is shown for every individual attribute.
for attribute in attributes {
if can_annotate(attribute) {
continue;
}

handler.emit_err(
ConvertParseTreeError::InvalidAttributeTarget {
span: Span::join(
first_doc_line.span.clone(),
&last_doc_line.span.start_span(),
),
attribute: first_doc_line.name.clone(),
span: attribute.name.span(),
attribute: attribute.name.clone(),
target_friendly_name,
can_only_annotate_help: first_doc_line
can_only_annotate_help: attribute
.can_only_annotate_help(target_friendly_name),
}
.into(),
);
}
} else {
// For other attributes, the error is shown for every individual attribute.
for attribute in attributes {
if !can_annotate(attribute) {
handler.emit_err(
ConvertParseTreeError::InvalidAttributeTarget {
span: attribute.name.span(),
attribute: attribute.name.clone(),
target_friendly_name,
can_only_annotate_help: attribute
.can_only_annotate_help(target_friendly_name),
}
.into(),
);
}
}
}
}

Expand Down Expand Up @@ -374,6 +374,28 @@ pub(crate) fn attr_decls_to_attributes(
}
}

// Check fixture-based testing requirements: #[case] or #[fuzz] require #[test]
let has_test = attributes.of_kind(AttributeKind::Test).any(|_| true);
let has_parameterization = attributes.of_kind(AttributeKind::Case).any(|_| true)
|| attributes.of_kind(AttributeKind::Fuzz).any(|_| true);

if !has_parameterization || has_test {
return (handler, attributes);
}

let first_param_attr = attributes.of_kind(AttributeKind::Case).next()
.or_else(|| attributes.of_kind(AttributeKind::Fuzz).next());

if let Some(attr) = first_param_attr {
handler.emit_err(
ConvertParseTreeError::ParameterizedTestRequiresTestAttribute {
span: attr.span.clone(),
attribute: attr.name.clone(),
}
.into(),
);
}

(handler, attributes)
}

Expand Down
Loading
Loading