Merge pull request #1547 from the-mikedavis/md-test-tags

test tags queries in 'tree-sitter test'
This commit is contained in:
Max Brunsfeld 2022-02-24 15:22:15 -08:00 committed by GitHub
commit 5eb0a3090f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 255 additions and 1 deletions

View file

@ -8,6 +8,7 @@ pub mod query_testing;
pub mod tags;
pub mod test;
pub mod test_highlight;
pub mod test_tags;
pub mod util;
pub mod wasm;

View file

@ -4,7 +4,8 @@ use glob::glob;
use std::path::Path;
use std::{env, fs, u64};
use tree_sitter_cli::{
generate, highlight, logger, parse, playground, query, tags, test, test_highlight, util, wasm,
generate, highlight, logger, parse, playground, query, tags, test, test_highlight, test_tags,
util, wasm,
};
use tree_sitter_config::Config;
use tree_sitter_loader as loader;
@ -338,6 +339,11 @@ fn run() -> Result<()> {
if test_highlight_dir.is_dir() {
test_highlight::test_highlights(&loader, &test_highlight_dir)?;
}
let test_tag_dir = test_dir.join("tags");
if test_tag_dir.is_dir() {
test_tags::test_tags(&loader, &test_tag_dir)?;
}
}
("parse", Some(matches)) => {

167
cli/src/test_tags.rs Normal file
View file

@ -0,0 +1,167 @@
use crate::query_testing::{parse_position_comments, Assertion};
use ansi_term::Colour;
use anyhow::{anyhow, Result};
use std::fs;
use std::path::Path;
use tree_sitter::Point;
use tree_sitter_loader::Loader;
use tree_sitter_tags::{TagsConfiguration, TagsContext};
#[derive(Debug)]
pub struct Failure {
row: usize,
column: usize,
expected_tag: String,
actual_tags: Vec<String>,
}
impl std::error::Error for Failure {}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Failure - row: {}, column: {}, expected tag: '{}', actual tag: ",
self.row, self.column, self.expected_tag
)?;
if self.actual_tags.is_empty() {
write!(f, "none.")?;
} else {
for (i, actual_tag) in self.actual_tags.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "'{}'", actual_tag)?;
}
}
Ok(())
}
}
pub fn test_tags(loader: &Loader, directory: &Path) -> Result<()> {
let mut failed = false;
let mut tags_context = TagsContext::new();
println!("tags:");
for tag_test_file in fs::read_dir(directory)? {
let tag_test_file = tag_test_file?;
let test_file_path = tag_test_file.path();
let test_file_name = tag_test_file.file_name();
let (language, language_config) = loader
.language_configuration_for_file_name(&test_file_path)?
.ok_or_else(|| anyhow!("No language found for path {:?}", test_file_path))?;
let tags_config = language_config
.tags_config(language)?
.ok_or_else(|| anyhow!("No tags config found for {:?}", test_file_path))?;
match test_tag(
&mut tags_context,
tags_config,
fs::read(&test_file_path)?.as_slice(),
) {
Ok(assertion_count) => {
println!(
" ✓ {} ({} assertions)",
Colour::Green.paint(test_file_name.to_string_lossy().as_ref()),
assertion_count
);
}
Err(e) => {
println!(
" ✗ {}",
Colour::Red.paint(test_file_name.to_string_lossy().as_ref())
);
println!(" {}", e);
failed = true;
}
}
}
if failed {
Err(anyhow!(""))
} else {
Ok(())
}
}
pub fn test_tag(
tags_context: &mut TagsContext,
tags_config: &TagsConfiguration,
source: &[u8],
) -> Result<usize> {
let tags = get_tag_positions(tags_context, tags_config, source)?;
let assertions = parse_position_comments(tags_context.parser(), tags_config.language, source)?;
// Iterate through all of the assertions, checking against the actual tags.
let mut i = 0;
let mut actual_tags = Vec::<&String>::new();
for Assertion {
position,
expected_capture_name: expected_tag,
} in &assertions
{
let mut passed = false;
'tag_loop: loop {
if let Some(tag) = tags.get(i) {
if tag.1 <= *position {
i += 1;
continue;
}
// Iterate through all of the tags that start at or before this assertion's
// position, looking for one that matches the assertion
let mut j = i;
while let (false, Some(tag)) = (passed, tags.get(j)) {
if tag.0 > *position {
break 'tag_loop;
}
let tag_name = &tag.2;
if *tag_name == *expected_tag {
passed = true;
break 'tag_loop;
} else {
actual_tags.push(tag_name);
}
j += 1;
}
} else {
break;
}
}
if !passed {
return Err(Failure {
row: position.row,
column: position.column,
expected_tag: expected_tag.clone(),
actual_tags: actual_tags.into_iter().cloned().collect(),
}
.into());
}
}
Ok(assertions.len())
}
pub fn get_tag_positions(
tags_context: &mut TagsContext,
tags_config: &TagsConfiguration,
source: &[u8],
) -> Result<Vec<(Point, Point, String)>> {
let (tags_iter, _has_error) = tags_context.generate_tags(&tags_config, &source, None)?;
let tag_positions = tags_iter
.filter_map(|t| t.ok())
.map(|tag| {
let tag_postfix = tags_config.syntax_type_name(tag.syntax_type_id).to_string();
let tag_name = if tag.is_definition {
format!("definition.{}", tag_postfix)
} else {
format!("reference.{}", tag_postfix)
};
(tag.span.start, tag.span.end, tag_name)
})
.collect();
Ok(tag_positions)
}

View file

@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
use tree_sitter::Language;
use tree_sitter_highlight::HighlightConfiguration;
use tree_sitter_loader::Loader;
use tree_sitter_tags::TagsConfiguration;
include!("./dirs.rs");
@ -54,6 +55,14 @@ pub fn get_highlight_config(
result
}
pub fn get_tags_config(language_name: &str) -> TagsConfiguration {
let language = get_language(language_name);
let queries_path = get_language_queries_path(language_name);
let tags_query = fs::read_to_string(queries_path.join("tags.scm")).unwrap();
let locals_query = fs::read_to_string(queries_path.join("locals.scm")).unwrap_or(String::new());
TagsConfiguration::new(language, &tags_query, &locals_query).unwrap()
}
pub fn get_test_language(name: &str, parser_code: &str, path: Option<&Path>) -> Language {
let parser_c_path = SCRATCH_DIR.join(&format!("{}-parser.c", name));
if !fs::read_to_string(&parser_c_path)

View file

@ -7,4 +7,5 @@ mod pathological_test;
mod query_test;
mod tags_test;
mod test_highlight_test;
mod test_tags_test;
mod tree_test;

View file

@ -0,0 +1,66 @@
use super::helpers::fixtures::{get_language, get_tags_config};
use crate::query_testing::{parse_position_comments, Assertion};
use crate::test_tags::get_tag_positions;
use tree_sitter::{Parser, Point};
use tree_sitter_tags::TagsContext;
#[test]
fn test_tags_test_with_basic_test() {
let language = get_language("python");
let config = get_tags_config("python");
let source = [
"# hi",
"def abc(d):",
" # <- definition.function",
" e = fgh(d)",
" # ^ reference.call",
" return d(e)",
" # ^ reference.call",
"",
]
.join("\n");
let assertions =
parse_position_comments(&mut Parser::new(), language, source.as_bytes()).unwrap();
assert_eq!(
assertions,
&[
Assertion {
position: Point::new(1, 4),
expected_capture_name: "definition.function".to_string(),
},
Assertion {
position: Point::new(3, 9),
expected_capture_name: "reference.call".to_string(),
},
Assertion {
position: Point::new(5, 11),
expected_capture_name: "reference.call".to_string(),
},
]
);
let mut tags_context = TagsContext::new();
let tag_positions = get_tag_positions(&mut tags_context, &config, source.as_bytes()).unwrap();
assert_eq!(
tag_positions,
&[
(
Point::new(1, 4),
Point::new(1, 7),
"definition.function".to_string()
),
(
Point::new(3, 8),
Point::new(3, 11),
"reference.call".to_string()
),
(
Point::new(5, 11),
Point::new(5, 12),
"reference.call".to_string()
),
]
)
}

View file

@ -252,6 +252,10 @@ impl TagsContext {
}
}
pub fn parser(&mut self) -> &mut Parser {
&mut self.parser
}
pub fn generate_tags<'a>(
&'a mut self,
config: &'a TagsConfiguration,