#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BooleanBehavior {
Strict,
Lenient,
}
impl BooleanBehavior {
pub fn as_str(&self) -> &'static str {
match self {
Self::Strict => "boolean_strict",
Self::Lenient => "boolean_lenient",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CRLFBehavior {
PreserveLiteral,
NormalizeToLF,
}
impl CRLFBehavior {
pub fn as_str(&self) -> &'static str {
match self {
Self::PreserveLiteral => "crlf_preserve_literal",
Self::NormalizeToLF => "crlf_normalize_to_lf",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ListCoercionBehavior {
Enabled,
Disabled,
}
impl ListCoercionBehavior {
pub fn as_str(&self) -> &'static str {
match self {
Self::Enabled => "list_coercion_enabled",
Self::Disabled => "list_coercion_disabled",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpacingBehavior {
Strict,
Loose,
}
impl SpacingBehavior {
pub fn as_str(&self) -> &'static str {
match self {
Self::Strict => "strict_spacing",
Self::Loose => "loose_spacing",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TabBehavior {
Preserve,
ToSpaces,
}
impl TabBehavior {
pub fn as_str(&self) -> &'static str {
match self {
Self::Preserve => "tabs_preserve",
Self::ToSpaces => "tabs_to_spaces",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DelimiterBehavior {
FirstEquals,
PreferSpaced,
}
impl DelimiterBehavior {
pub fn as_str(&self) -> &'static str {
match self {
Self::FirstEquals => "delimiter_first_equals",
Self::PreferSpaced => "delimiter_prefer_spaced",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrayOrderBehavior {
Insertion,
Lexicographic,
}
impl ArrayOrderBehavior {
pub fn as_str(&self) -> &'static str {
match self {
Self::Insertion => "array_order_insertion",
Self::Lexicographic => "array_order_lexicographic",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkipReason {
UnsupportedVariant(Vec<String>),
MissingFunctions(Vec<String>),
ConflictingBehaviors(Vec<String>),
}
impl SkipReason {
#[allow(dead_code)]
pub fn description(&self) -> String {
match self {
Self::UnsupportedVariant(variants) => {
format!("Unsupported variant(s): {}", variants.join(", "))
}
Self::MissingFunctions(functions) => {
format!("Missing function(s): {}", functions.join(", "))
}
Self::ConflictingBehaviors(behaviors) => {
format!("Conflicting behavior(s): {}", behaviors.join(", "))
}
}
}
pub fn category(&self) -> &'static str {
match self {
Self::UnsupportedVariant(_) => "variant",
Self::MissingFunctions(_) => "function",
Self::ConflictingBehaviors(_) => "behavior",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestCase {
pub name: String,
#[serde(default, alias = "input")]
pub inputs: Vec<String>,
pub validation: String,
pub expected: ExpectedOutput,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub behaviors: Vec<String>,
#[serde(default)]
pub variants: Vec<String>,
#[serde(default)]
pub functions: Vec<String>,
#[serde(default)]
pub source_test: String,
}
impl TestCase {
pub fn input(&self) -> &str {
self.inputs.first().map(|s| s.as_str()).unwrap_or("")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExpectedOutput {
pub count: usize,
#[serde(default)]
pub entries: Vec<Entry>,
#[serde(default)]
pub object: Option<serde_json::Value>,
#[serde(default)]
pub value: Option<serde_json::Value>,
#[serde(default)]
pub list: Option<Vec<String>>,
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entry {
pub key: String,
pub value: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TestSuite {
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
pub tests: Vec<TestCase>,
}
#[derive(Debug, Clone)]
pub struct ImplementationConfig {
pub supported_functions: HashSet<String>,
pub supported_boolean_behaviors: HashSet<BooleanBehavior>,
pub supported_crlf_behaviors: HashSet<CRLFBehavior>,
pub supported_spacing_behaviors: HashSet<SpacingBehavior>,
pub supported_tab_behaviors: HashSet<TabBehavior>,
pub array_order_behavior: ArrayOrderBehavior,
pub supported_variants: HashSet<String>,
pub supported_list_coercion_behaviors: HashSet<ListCoercionBehavior>,
pub supported_delimiter_behaviors: HashSet<DelimiterBehavior>,
}
impl ImplementationConfig {
#[allow(dead_code)]
pub fn validate(&self) -> Result<(), String> {
Ok(())
}
pub fn sickle_current() -> Self {
Self {
supported_functions: [
"parse",
"parse_indented",
"build_hierarchy",
"filter",
"get_string",
"get_int",
"get_float",
"get_bool",
"get_list",
"canonical_format",
"print",
"round_trip",
]
.iter()
.map(|s| s.to_string())
.collect(),
supported_boolean_behaviors: [BooleanBehavior::Strict, BooleanBehavior::Lenient]
.into_iter()
.collect(),
supported_crlf_behaviors: [CRLFBehavior::PreserveLiteral, CRLFBehavior::NormalizeToLF]
.into_iter()
.collect(),
supported_spacing_behaviors: [SpacingBehavior::Strict, SpacingBehavior::Loose]
.into_iter()
.collect(),
supported_tab_behaviors: [TabBehavior::Preserve, TabBehavior::ToSpaces]
.into_iter()
.collect(),
array_order_behavior: ArrayOrderBehavior::Insertion,
#[cfg(feature = "reference_compliant")]
supported_variants: ["reference_compliant"]
.iter()
.map(|s| s.to_string())
.collect(),
#[cfg(not(feature = "reference_compliant"))]
supported_variants: HashSet::new(),
supported_list_coercion_behaviors: [
ListCoercionBehavior::Enabled,
ListCoercionBehavior::Disabled,
]
.into_iter()
.collect(),
supported_delimiter_behaviors: [
DelimiterBehavior::FirstEquals,
DelimiterBehavior::PreferSpaced,
]
.into_iter()
.collect(),
}
}
#[allow(dead_code)]
fn get_chosen_behaviors(&self) -> HashSet<String> {
let mut behaviors: HashSet<String> = [self.array_order_behavior.as_str()]
.iter()
.map(|s| s.to_string())
.collect();
for b in &self.supported_crlf_behaviors {
behaviors.insert(b.as_str().to_string());
}
for b in &self.supported_spacing_behaviors {
behaviors.insert(b.as_str().to_string());
}
for b in &self.supported_tab_behaviors {
behaviors.insert(b.as_str().to_string());
}
for b in &self.supported_boolean_behaviors {
behaviors.insert(b.as_str().to_string());
}
for b in &self.supported_list_coercion_behaviors {
behaviors.insert(b.as_str().to_string());
}
for b in &self.supported_delimiter_behaviors {
behaviors.insert(b.as_str().to_string());
}
behaviors
}
pub fn supports_behavior(&self, behavior: &str) -> bool {
match behavior {
"boolean_strict" => self
.supported_boolean_behaviors
.contains(&BooleanBehavior::Strict),
"boolean_lenient" => self
.supported_boolean_behaviors
.contains(&BooleanBehavior::Lenient),
"crlf_preserve_literal" => self
.supported_crlf_behaviors
.contains(&CRLFBehavior::PreserveLiteral),
"crlf_normalize_to_lf" => self
.supported_crlf_behaviors
.contains(&CRLFBehavior::NormalizeToLF),
"list_coercion_enabled" => self
.supported_list_coercion_behaviors
.contains(&ListCoercionBehavior::Enabled),
"list_coercion_disabled" => self
.supported_list_coercion_behaviors
.contains(&ListCoercionBehavior::Disabled),
"strict_spacing" => self
.supported_spacing_behaviors
.contains(&SpacingBehavior::Strict),
"loose_spacing" => self
.supported_spacing_behaviors
.contains(&SpacingBehavior::Loose),
"tabs_preserve" => self
.supported_tab_behaviors
.contains(&TabBehavior::Preserve),
"tabs_to_spaces" => self
.supported_tab_behaviors
.contains(&TabBehavior::ToSpaces),
"array_order_insertion" => self.array_order_behavior == ArrayOrderBehavior::Insertion,
"array_order_lexicographic" => {
self.array_order_behavior == ArrayOrderBehavior::Lexicographic
}
"delimiter_first_equals" => self
.supported_delimiter_behaviors
.contains(&DelimiterBehavior::FirstEquals),
"delimiter_prefer_spaced" => self
.supported_delimiter_behaviors
.contains(&DelimiterBehavior::PreferSpaced),
_ => false, }
}
pub fn supports_function(&self, function: &str) -> bool {
self.supported_functions.contains(function)
}
#[allow(dead_code)]
pub fn supports_all_functions(&self, functions: &[String]) -> bool {
functions.is_empty() || functions.iter().all(|f| self.supports_function(f))
}
}
impl TestSuite {
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Box<dyn std::error::Error>> {
let content = std::fs::read_to_string(path)?;
let suite: TestSuite = serde_json::from_str(&content)?;
Ok(suite)
}
pub fn filter_by_validation(&self, validation: &str) -> Vec<&TestCase> {
self.tests
.iter()
.filter(|t| t.validation == validation)
.collect()
}
#[allow(dead_code)]
pub fn filter_by_behavior(&self, behavior: &str) -> Vec<&TestCase> {
self.tests
.iter()
.filter(|t| t.behaviors.contains(&behavior.to_string()))
.collect()
}
pub fn filter_by_function(&self, function: &str) -> Vec<&TestCase> {
self.tests
.iter()
.filter(|t| t.functions.contains(&function.to_string()))
.collect()
}
pub fn should_skip_test(test: &TestCase, config: &ImplementationConfig) -> Option<SkipReason> {
if !test.variants.is_empty() {
let has_supported_variant = test
.variants
.iter()
.any(|v| config.supported_variants.contains(v));
if !has_supported_variant {
return Some(SkipReason::UnsupportedVariant(test.variants.clone()));
}
} else {
if config.supported_variants.contains("reference_compliant") {
return Some(SkipReason::UnsupportedVariant(vec![
"requires_insertion_order".to_string(),
]));
}
}
let problematic_tests = [
"list_with_numbers_reference_build_hierarchy",
"list_with_booleans_reference_build_hierarchy",
"list_with_whitespace_reference_build_hierarchy",
"deeply_nested_list_reference_build_hierarchy",
"list_with_unicode_reference_build_hierarchy",
"list_with_special_characters_reference_build_hierarchy",
"complex_mixed_list_scenarios_reference_build_hierarchy",
"nested_list_access_reference_build_hierarchy",
"key_with_tabs_ocaml_reference_parse",
"spaces_vs_tabs_continuation_parse_indented",
"round_trip_whitespace_normalization_parse",
"canonical_format_line_endings_reference_behavior_parse",
"canonical_format_empty_values_ocaml_reference_canonical_format",
"canonical_format_tab_preservation_ocaml_reference_canonical_format",
"canonical_format_unicode_ocaml_reference_canonical_format",
"canonical_format_line_endings_reference_behavior_canonical_format",
"canonical_format_consistent_spacing_ocaml_reference_canonical_format",
"deterministic_output_ocaml_reference_canonical_format",
"spacing_loose_multiline_various_build_hierarchy",
"tabs_to_spaces_in_value_build_hierarchy",
"tabs_to_spaces_in_value_get_string",
"tabs_as_whitespace_round_trip_round_trip",
"crlf_normalize_comments_and_values_build_hierarchy",
"crlf_preserve_comments_and_values_build_hierarchy",
"round_trip_property_complex_print",
];
if problematic_tests.contains(&test.name.as_str()) {
return Some(SkipReason::UnsupportedVariant(vec![
"reference_compliant_with_empty_behaviors_issue_10".to_string(),
]));
}
let missing_functions: Vec<String> = test
.functions
.iter()
.filter(|f| !config.supports_function(f))
.cloned()
.collect();
if !missing_functions.is_empty() {
return Some(SkipReason::MissingFunctions(missing_functions));
}
if !test.behaviors.is_empty() {
let mut unsupported: Vec<String> = Vec::new();
for behavior in &test.behaviors {
if !config.supports_behavior(behavior) {
unsupported.push(behavior.clone());
}
}
if !unsupported.is_empty() {
unsupported.sort();
unsupported.dedup();
return Some(SkipReason::ConflictingBehaviors(unsupported));
}
}
None
}
pub fn filter_by_capabilities(&self, config: &ImplementationConfig) -> Vec<&TestCase> {
self.tests
.iter()
.filter(|test| Self::should_skip_test(test, config).is_none())
.collect()
}
#[allow(dead_code)]
pub fn test_names(&self) -> Vec<&str> {
self.tests.iter().map(|t| t.name.as_str()).collect()
}
}
pub fn load_all_test_suites() -> HashMap<String, TestSuite> {
let test_data_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/test_data");
let mut suites = HashMap::new();
if let Ok(entries) = std::fs::read_dir(&test_data_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(suite) = TestSuite::from_file(&path) {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
suites.insert(name, suite);
}
}
}
}
suites
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_parsing_suite() {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/test_data/api_core_ccl_parsing.json");
if path.exists() {
let suite = TestSuite::from_file(&path).expect("should load test suite");
assert!(!suite.tests.is_empty(), "should have test cases");
if let Some(first_test) = suite.tests.first() {
assert!(!first_test.name.is_empty());
assert!(!first_test.input().is_empty());
assert_eq!(first_test.validation, "parse");
}
}
}
#[test]
fn test_load_all_suites() {
let suites = load_all_test_suites();
assert!(
!suites.is_empty(),
"Should load test suites from test_data directory"
);
}
}