use crate::FILE_PATH_VAR;
use once_cell::sync::Lazy;
use pretty_assertions::assert_eq;
use stack_graphs::arena::Handle;
use stack_graphs::graph::File;
use stack_graphs::graph::StackGraph;
use stack_graphs::partial::PartialPaths;
use stack_graphs::stitching::Database;
use stack_graphs::stitching::ForwardPartialPathStitcher;
use stack_graphs::stitching::StitcherConfig;
use std::path::Path;
use std::path::PathBuf;
use tree_sitter_graph::Variables;
use tree_sitter_stack_graphs::test::Test;
use tree_sitter_stack_graphs::BuildError;
use tree_sitter_stack_graphs::NoCancellation;
use tree_sitter_stack_graphs::StackGraphLanguage;
static PATH: Lazy<PathBuf> = Lazy::new(|| PathBuf::from("test.py"));
static TSG: Lazy<String> = Lazy::new(|| {
r#"
global ROOT_NODE
(module) @mod {
node @mod.lexical_in
node @mod.lexical_out
edge @mod.lexical_in -> ROOT_NODE
edge ROOT_NODE -> @mod.lexical_out
}
(module (_)@stmt) @mod {
node @stmt.lexical_in
node @stmt.lexical_out
edge @stmt.lexical_in -> @mod.lexical_in
edge @mod.lexical_out -> @stmt.lexical_out
}
(module (_)@left . (_)@right) {
edge @right.lexical_in -> @left.lexical_out
}
(expression_statement (assignment left:(identifier)@name))@stmt {
node @name.def
attr (@name.def) type = "pop_symbol", symbol = (source-text @name), source_node = @name, is_definition
edge @stmt.lexical_out -> @name.def
}
[
(expression_statement (assignment right:(identifier)@name))@stmt
(expression_statement (identifier)@name)@stmt
] {
node @name.ref
attr (@name.ref) type = "push_symbol", symbol = (source-text @name), source_node = @name, is_reference
edge @name.ref -> @stmt.lexical_in
}
"#.to_string()
});
static TSG_WITH_PKG: Lazy<String> = Lazy::new(|| {
r#"
global PKG
"#
.to_string()
+ &TSG
});
fn build_stack_graph_into(
graph: &mut StackGraph,
file: Handle<File>,
python_source: &str,
tsg_source: &str,
globals: &Variables,
) -> Result<(), BuildError> {
let language =
StackGraphLanguage::from_str(tree_sitter_python::LANGUAGE.into(), tsg_source).unwrap();
language.build_stack_graph_into(graph, file, python_source, globals, &NoCancellation)?;
Ok(())
}
fn check_test(
python_path: &Path,
python_source: &str,
tsg_source: &str,
expected_successes: usize,
expected_failures: usize,
) {
let mut test =
Test::from_source(python_path, python_source, python_path).expect("Could not parse test");
let assertion_count: usize = test.fragments.iter().map(|f| f.assertions.len()).sum();
assert_eq!(
expected_successes + expected_failures,
assertion_count,
"expected {} assertions, got {}",
expected_successes + expected_failures,
assertion_count,
);
let mut globals = Variables::new();
for fragments in &test.fragments {
globals.clear();
fragments.add_globals_to(&mut globals);
globals
.add(
FILE_PATH_VAR.into(),
fragments.path.to_str().unwrap().into(),
)
.unwrap_or_default();
build_stack_graph_into(
&mut test.graph,
fragments.file,
&fragments.source,
tsg_source,
&globals,
)
.expect("Could not load stack graph");
}
let mut partials = PartialPaths::new();
let mut db = Database::new();
for fragment in &test.fragments {
ForwardPartialPathStitcher::find_minimal_partial_path_set_in_file(
&test.graph,
&mut partials,
fragment.file,
StitcherConfig::default(),
&stack_graphs::NoCancellation,
|graph, partials, path| {
db.add_partial_path(graph, partials, path.clone());
},
)
.expect("should nopt be cancelled");
}
let results = test
.run(
&mut partials,
&mut db,
StitcherConfig::default(),
&NoCancellation,
)
.expect("should never be cancelled");
assert_eq!(
expected_successes,
results.success_count(),
"expected {} successes, got {}",
expected_successes,
results.success_count()
);
assert_eq!(
expected_failures,
results.failure_count(),
"expected {} failures, got {}",
expected_failures,
results.failure_count()
);
}
#[test]
fn can_assert_defined_on_one_line() {
let python = r#"
x = 1;
x;
# ^ defined: 2
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn can_assert_defined_on_multiple_lines() {
let python = r#"
# --- path: a.py ---
x = 1;
# --- path: b.py ---
x = 1;
# --- path: c.py ---
x;
# ^ defined: 3, 6
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn can_assert_defined_on_no_lines() {
let python = r#"
y = 1;
x;
# ^ defined:
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn can_assert_defines_one_symbol() {
let python = r#"
x = 1;
# ^ defines: x
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn can_assert_defines_no_symbols() {
let python = r#"
x;
# ^ defines:
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn can_assert_refers_one_symbol() {
let python = r#"
x;
# ^ refers: x
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn can_assert_refers_no_symbols() {
let python = r#"
x = 1;
# ^ refers:
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn test_cannot_use_unknown_assertion() {
let python = r#"
foo = 42
# ^ supercalifragilisticexpialidocious:
"#;
if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
panic!("Parsing test unexpectedly succeeded.");
}
}
#[test]
fn aligns_correctly_with_unicode() {
let python = r#"
x = 1;
m = {};
# multi code unit character in assertion line
m[" "] = x;
# § # ^ defined: 2
# multi code point character in source line
m["§"] = x;
# # ^ defined: 2
# multi code point character in assertion line
m[" "] = x;
# g̈ # ^ defined: 2
# multi code point character in source line
m["g̈"] = x;
# # ^ defined: 2
"#;
check_test(&PATH, python, &TSG, 4, 0);
}
#[test]
fn test_can_be_multi_file() {
let python = r#"
# --- path: a.py ---
x = 1;
# --- path: b.py ---
x;
# ^ defined: 3
"#;
check_test(&PATH, python, &TSG, 1, 0);
}
#[test]
fn test_fragment_can_have_same_name_as_test() {
let python = r#"
# --- path: test.py ---
x = 1;
x;
# ^ defined: 3
"#;
check_test(&PathBuf::from("test.py"), python, &TSG, 1, 0);
}
#[test]
fn test_cannot_assert_on_first_line() {
let python = r#"
# ^ defined: 3
"#;
if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
panic!("Parsing test unexpectedly succeeded.");
}
}
#[test]
fn test_cannot_assert_before_first_fragment() {
let python = r#"
# this is ignored
# --- path: a.py ---
x;
# ^ defined: 1
"#;
if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
panic!("Parsing test unexpectedly succeeded.");
}
}
#[test]
fn test_can_set_global() {
let python = r#"
# --- global: PKG=test ---
pass
"#;
check_test(&PathBuf::from("test.py"), python, &TSG_WITH_PKG, 0, 0);
}
#[test]
fn test_can_set_global_in_fragments() {
let python = r#"
# --- path: a.py ---
# --- global: PKG=test ---
pass
# --- path: b.py ---
# --- global: PKG=test ---
pass
"#;
check_test(&PathBuf::from("test.py"), python, &TSG_WITH_PKG, 0, 0);
}
#[test]
fn test_cannot_set_global_before_first_fragment() {
let python = r#"
# --- global: PKG=test ---
# --- path: a.py ---
pass
"#;
if let Ok(_) = Test::from_source(&PATH, python, &PATH) {
panic!("Parsing test unexpectedly succeeded.");
}
}