use super::*;
use plexus_conformance::{
load_cases_from_paths_with_options, load_external_cases_from_root, parse_corpus_group_map,
run_conformance_suite, ConformanceCase, ConformanceFilterConfig, CorpusLoadOptions,
};
use std::fs;
use std::path::{Path, PathBuf};
mod runner;
use runner::{
execute_case_optimized, execute_case_optimized_for_conformance, execute_case_unoptimized,
execute_case_unoptimized_for_conformance, execute_case_with_pipeline,
};
fn case_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("tests")
.join("fixtures")
.join("readonly")
}
fn load_filter_config() -> ConformanceFilterConfig {
let path = case_dir().join("tck_filter.conf");
let Ok(text) = fs::read_to_string(path) else {
return ConformanceFilterConfig::default();
};
let mut cfg = ConformanceFilterConfig::default();
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let Some((k, v)) = line.split_once('=') else {
continue;
};
let key = k.trim();
let value = v.trim();
match key {
"include_untagged_readonly" => {
cfg.include_untagged = value.eq_ignore_ascii_case("true");
}
"allow_tags" => {
cfg.allow_tags = parse_csv_lower(value);
}
"deny_tags" => {
cfg.deny_tags = parse_csv_lower(value);
}
"deny_query_keywords" => {
cfg.deny_query_keywords = value
.split(',')
.map(|x| x.trim().to_ascii_uppercase())
.filter(|x| !x.is_empty())
.collect();
}
_ => {}
}
}
cfg
}
fn parse_csv_lower(value: &str) -> Vec<String> {
value
.split(',')
.map(|x| x.trim().to_ascii_lowercase())
.filter(|x| !x.is_empty())
.collect()
}
fn local_case_paths() -> Vec<PathBuf> {
let mut files = fs::read_dir(case_dir())
.expect("read readonly tck dir")
.map(|e| e.expect("read_dir entry").path())
.filter(|p| {
matches!(
p.extension().and_then(|x| x.to_str()),
Some("case") | Some("feature")
)
})
.collect::<Vec<_>>();
files.sort();
files
}
fn load_external_readonly_cases(
external_root: &Path,
groups_csv: Option<&str>,
group_map_path: Option<&Path>,
filter: &ConformanceFilterConfig,
) -> Vec<ConformanceCase> {
let options = CorpusLoadOptions {
filter: Some(filter.clone()),
groups_csv: groups_csv.map(|s| s.to_string()),
group_map_path: group_map_path.map(Path::to_path_buf),
};
load_external_cases_from_root(external_root, &options).expect("load external readonly cases")
}
pub(super) fn load_readonly_cases() -> Vec<ConformanceCase> {
let filter = load_filter_config();
let options = CorpusLoadOptions {
filter: Some(filter.clone()),
groups_csv: None,
group_map_path: None,
};
let mut out = load_cases_from_paths_with_options(&local_case_paths(), &options)
.expect("load local readonly cases");
if let Ok(external_root) = std::env::var("PLEXUS_OPENCYPHER_TCK_DIR") {
let groups_csv = std::env::var("PLEXUS_OPENCYPHER_TCK_GROUPS").ok();
let group_map_path = std::env::var("PLEXUS_OPENCYPHER_TCK_GROUP_MAP")
.ok()
.map(PathBuf::from);
out.extend(load_external_readonly_cases(
Path::new(&external_root),
groups_csv.as_deref(),
group_map_path.as_deref(),
&filter,
));
}
out
}
#[test]
fn readonly_tck_parse_opencypher_group_map_config() {
let parsed = parse_corpus_group_map(
r#"
readonly-core=match,return,where
optional=optional
"#,
);
assert_eq!(
parsed.get("readonly-core"),
Some(&vec![
"match".to_string(),
"return".to_string(),
"where".to_string()
])
);
assert_eq!(parsed.get("optional"), Some(&vec!["optional".to_string()]));
}
#[test]
fn readonly_tck_external_group_selection_filters_cases() {
let base = std::env::temp_dir().join(format!(
"plexus_tck_ext_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock")
.as_nanos()
));
fs::create_dir_all(&base).expect("mkdir temp");
let feature_path = base.join("group_filter.feature");
fs::write(
&feature_path,
r#"
@Match
Scenario: external-match
When executing query:
"""
MATCH (n:Person)
RETURN n.name
ORDER BY n.name
"""
Then the result should be, in order:
| string:Alice |
| string:Bob |
@Optional
Scenario: external-optional
When executing query:
"""
OPTIONAL MATCH (n:Person)-[r:WORKS_AT]->(c:Company)
RETURN n.name, c.name
ORDER BY n.name
"""
Then the result should be, in order:
| string:Alice | null |
| string:Bob | string:Acme |
"#,
)
.expect("write feature");
let filter = ConformanceFilterConfig::default();
let only_match = load_external_readonly_cases(&base, Some("match"), None, &filter);
let names = only_match
.iter()
.map(|c| c.name.as_str())
.collect::<Vec<_>>();
assert!(
names.contains(&"external-match"),
"match group should include @Match scenario",
);
assert!(
!names.contains(&"external-optional"),
"match group should exclude @Optional scenario",
);
let only_optional = load_external_readonly_cases(&base, Some("optional"), None, &filter);
let names = only_optional
.iter()
.map(|c| c.name.as_str())
.collect::<Vec<_>>();
assert!(
names.contains(&"external-optional"),
"optional group should include @Optional scenario",
);
assert!(
!names.contains(&"external-match"),
"optional group should exclude @Match scenario",
);
let _ = fs::remove_file(&feature_path);
let _ = fs::remove_dir_all(&base);
}
#[test]
fn readonly_tck_filters_write_tagged_scenarios() {
let cases = load_readonly_cases();
assert!(
!cases.iter().any(|c| c.name.contains("write-tagged")),
"write-tagged scenario should be filtered from readonly corpus",
);
assert!(
!cases
.iter()
.any(|c| c.name.contains("unknown-tag-readonly")),
"unknown-tag scenario should be filtered by allow_tags",
);
}
#[test]
fn readonly_tck_corpus_cases() {
let cases = load_readonly_cases();
let report = run_conformance_suite(&cases, execute_case_unoptimized_for_conformance);
assert_eq!(
report.failed,
0,
"readonly unoptimized conformance failures: {:?}",
report
.cases
.iter()
.filter_map(|c| c
.outcome
.as_ref()
.err()
.map(|e| (c.case.clone(), format!("{e}"))))
.collect::<Vec<_>>()
);
}
#[test]
fn readonly_tck_corpus_cases_optimized_pipeline() {
let cases = load_readonly_cases();
let report = run_conformance_suite(&cases, execute_case_optimized_for_conformance);
assert_eq!(
report.failed,
0,
"readonly optimized conformance failures: {:?}",
report
.cases
.iter()
.filter_map(|c| c
.outcome
.as_ref()
.err()
.map(|e| (c.case.clone(), format!("{e}"))))
.collect::<Vec<_>>()
);
}
pub(super) fn execute_case_unoptimized_for_equivalence(
case: &ConformanceCase,
) -> Result<QueryResult, String> {
execute_case_unoptimized(case)
}
pub(super) fn execute_case_optimized_for_equivalence(
case: &ConformanceCase,
) -> Result<QueryResult, String> {
execute_case_optimized(case)
}
pub(super) fn execute_case_with_pipeline_for_equivalence(
case: &ConformanceCase,
pipeline: &str,
) -> Result<QueryResult, String> {
execute_case_with_pipeline(case, pipeline)
}