-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a idempotent fuzz_target for ruff_python_formatter (#9448)
Co-authored-by: Addison Crump <[email protected]> Co-authored-by: Addison Crump <[email protected]>
- Loading branch information
1 parent
4a3bb67
commit 14d3fe6
Showing
6 changed files
with
170 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ruff_fix_validity |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ruff_fix_validity |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
//! Fuzzer harness which double formats the input and access the idempotency or unsteady state of the | ||
//! ruff's formatter. | ||
#![no_main] | ||
|
||
use libfuzzer_sys::{fuzz_target, Corpus}; | ||
use similar::TextDiff; | ||
|
||
use ruff_python_formatter::{format_module_source, PyFormatOptions}; | ||
|
||
fn do_fuzz(case: &[u8]) -> Corpus { | ||
// Throw away inputs which aren't utf-8 | ||
let Ok(code) = std::str::from_utf8(case) else { | ||
return Corpus::Reject; | ||
}; | ||
|
||
let options = PyFormatOptions::default(); | ||
// format the code once | ||
if let Ok(formatted) = format_module_source(code, options.clone()) { | ||
let formatted = formatted.as_code(); | ||
|
||
// reformat the code second time | ||
if let Ok(reformatted) = format_module_source(formatted, options.clone()) { | ||
let reformatted = reformatted.as_code(); | ||
|
||
if formatted != reformatted { | ||
let diff = TextDiff::from_lines(formatted, reformatted) | ||
.unified_diff() | ||
.header("Formatted Once", "Formatted Twice") | ||
.to_string(); | ||
panic!( | ||
"\nReformatting the code a second time resulted in formatting changes.\nInput: {:?}\ndiff:\n{}", | ||
code, diff | ||
); | ||
} | ||
} else { | ||
panic!( | ||
"Unable to format the code second time:\nInput:{:?}\nformatted:\n{:?}", | ||
code, formatted | ||
); | ||
} | ||
} | ||
|
||
Corpus::Keep | ||
} | ||
|
||
fuzz_target!(|case: &[u8]| -> Corpus { do_fuzz(case) }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
//! Fuzzer harness which actively tries to find testcases that cause Ruff to introduce errors into | ||
//! the resulting file. | ||
#![no_main] | ||
|
||
use std::collections::HashMap; | ||
use std::sync::OnceLock; | ||
|
||
use libfuzzer_sys::{fuzz_target, Corpus}; | ||
use ruff_linter::linter::ParseSource; | ||
use ruff_linter::settings::flags::Noqa; | ||
use ruff_linter::settings::LinterSettings; | ||
use ruff_linter::source_kind::SourceKind; | ||
use ruff_python_ast::PySourceType; | ||
use ruff_python_formatter::{format_module_source, PyFormatOptions}; | ||
use similar::TextDiff; | ||
|
||
static SETTINGS: OnceLock<LinterSettings> = OnceLock::new(); | ||
|
||
fn do_fuzz(case: &[u8]) -> Corpus { | ||
// throw away inputs which aren't utf-8 | ||
let Ok(code) = std::str::from_utf8(case) else { | ||
return Corpus::Reject; | ||
}; | ||
|
||
// the settings are immutable to test_snippet, so we avoid re-initialising here | ||
let linter_settings = SETTINGS.get_or_init(LinterSettings::default); | ||
let format_options = PyFormatOptions::default(); | ||
|
||
let linter_results = ruff_linter::linter::lint_only( | ||
"fuzzed-source.py".as_ref(), | ||
None, | ||
&linter_settings, | ||
Noqa::Enabled, | ||
&SourceKind::Python(code.to_string()), | ||
PySourceType::Python, | ||
ParseSource::None, | ||
); | ||
|
||
if linter_results.error.is_some() { | ||
return Corpus::Keep; // keep, but don't continue | ||
} | ||
|
||
let mut warnings = HashMap::new(); | ||
|
||
for msg in linter_results.data.0 { | ||
let count: &mut usize = warnings.entry(msg.kind.name).or_default(); | ||
*count += 1; | ||
} | ||
|
||
// format the code once | ||
if let Ok(formatted) = format_module_source(code, format_options.clone()) { | ||
let formatted = formatted.as_code().to_string(); | ||
|
||
let linter_results = ruff_linter::linter::lint_only( | ||
"fuzzed-source.py".as_ref(), | ||
None, | ||
&linter_settings, | ||
Noqa::Enabled, | ||
&SourceKind::Python(formatted.clone()), | ||
PySourceType::Python, | ||
ParseSource::None, | ||
); | ||
|
||
assert!( | ||
linter_results.error.is_none(), | ||
"formatter introduced a parse error" | ||
); | ||
|
||
for msg in linter_results.data.0 { | ||
if let Some(count) = warnings.get_mut(&msg.kind.name) { | ||
if let Some(decremented) = count.checked_sub(1) { | ||
*count = decremented; | ||
} else { | ||
panic!( | ||
"formatter introduced additional linter warning: {msg:?}\ndiff: {}", | ||
TextDiff::from_lines(code, &formatted) | ||
.unified_diff() | ||
.header("Unformatted", "Formatted") | ||
.to_string() | ||
); | ||
} | ||
} else { | ||
panic!( | ||
"formatter introduced new linter warning that was not previously present: {msg:?}\ndiff: {}", | ||
TextDiff::from_lines(code, &formatted) | ||
.unified_diff() | ||
.header("Unformatted", "Formatted") | ||
.to_string() | ||
); | ||
} | ||
} | ||
} | ||
|
||
Corpus::Keep | ||
} | ||
|
||
fuzz_target!(|case: &[u8]| -> Corpus { do_fuzz(case) }); |