Skip to content

Commit c76d6a1

Browse files
authored
Refactor conformance test generator to encapsulate logic (#204)
1 parent 5061309 commit c76d6a1

File tree

10 files changed

+540
-347
lines changed

10 files changed

+540
-347
lines changed

partiql-conformance-test-generator/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ edition = "2021"
2323

2424
[dependencies]
2525
walkdir = "2.3"
26-
ion-rs = "0.6.0"
27-
codegen = "0.1.3"
26+
ion-rs = "0.14.*"
27+
codegen = "0.2.*"
2828
Inflector = "0.11.4"
29+
miette = "5.*"
30+
thiserror = "1.*"

partiql-conformance-test-generator/src/generator.rs

Lines changed: 99 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,122 @@
1-
use crate::schema::{Assertion, Namespace, TestCase, TestDocument};
1+
use crate::schema::spec::{Assertion, Namespace, TestCase, TestDocument};
2+
use crate::schema::structure::{TestDir, TestEntry, TestFile, TestRoot};
3+
4+
use crate::StringExt;
25
use codegen::{Function, Module, Scope};
6+
use std::collections::HashMap;
7+
8+
#[derive(Debug)]
9+
pub enum TestComponent {
10+
Scope(TestScope),
11+
Module(TestModule),
12+
}
13+
14+
#[derive(Debug)]
15+
pub struct TestScope {
16+
pub module: Module,
17+
}
18+
19+
#[derive(Debug, Default)]
20+
pub struct TestModule {
21+
pub children: HashMap<String, TestComponent>,
22+
}
23+
24+
impl TestModule {
25+
pub fn insert(&mut self, path: &[&String], scope: TestScope) {
26+
if let Some((first, rest)) = path.split_first() {
27+
if rest.is_empty() {
28+
self.children
29+
.insert(first.to_string(), TestComponent::Scope(scope));
30+
} else {
31+
let child = self
32+
.children
33+
.entry((*first).clone())
34+
.or_insert_with(|| TestComponent::Module(TestModule::default()));
35+
if let TestComponent::Module(child_mod) = child {
36+
child_mod.insert(rest, scope)
37+
} else {
38+
unreachable!();
39+
}
40+
}
41+
}
42+
}
43+
}
344

4-
/// Defines a test code generation object
45+
/// Generates a [`TestModule`] root from a [`TestRoot`] specification.
46+
#[derive(Debug)]
547
pub struct Generator {
6-
pub test_document: TestDocument,
48+
result: TestModule,
49+
curr_path: Vec<String>,
750
}
851

952
impl Generator {
10-
/// Generates a `Scope` from the `Generator`'s `test_document`
11-
pub fn generate_scope(&self) -> Scope {
12-
test_document_to_scope(&self.test_document)
53+
pub fn new() -> Generator {
54+
Self {
55+
result: Default::default(),
56+
curr_path: Default::default(),
57+
}
58+
}
59+
60+
pub fn generate(mut self, root: TestRoot) -> miette::Result<TestModule> {
61+
let TestRoot { fail, success } = root;
62+
for f in fail {
63+
self.test_entry(f)
64+
}
65+
for s in success {
66+
self.test_entry(s)
67+
}
68+
69+
Ok(self.result)
70+
}
71+
72+
fn test_entry(&mut self, entry: TestEntry) {
73+
match entry {
74+
TestEntry::Dir(TestDir { dir_name, contents }) => {
75+
self.curr_path.push(dir_name);
76+
for c in contents {
77+
self.test_entry(c);
78+
}
79+
self.curr_path.pop();
80+
}
81+
TestEntry::Doc(TestFile {
82+
file_name,
83+
contents,
84+
}) => {
85+
let mod_name = file_name.replace(".ion", "").escaped_snake_case();
86+
let out_file = format!("{}.rs", &mod_name);
87+
let path: Vec<_> = self
88+
.curr_path
89+
.iter()
90+
.chain(std::iter::once(&out_file))
91+
.collect();
92+
let mut module = Module::new(&mod_name);
93+
gen_tests(module.scope(), &contents);
94+
self.result.insert(&path, TestScope { module });
95+
}
96+
}
1397
}
1498
}
1599

16-
/// Converts a `TestDocument` into a `Scope`
17-
fn test_document_to_scope(test_document: &TestDocument) -> Scope {
18-
let mut scope = Scope::new();
100+
fn gen_tests(scope: &mut Scope, test_document: &TestDocument) {
19101
for namespace in &test_document.namespaces {
20-
scope.push_module(namespace_to_module(namespace));
102+
gen_mod(scope, namespace);
21103
}
22104
for test in &test_document.test_cases {
23-
scope.push_fn(test_case_to_function(test));
105+
gen_test(scope, test);
24106
}
25-
scope
26107
}
27108

28-
/// Converts a `Namespace` into a `Module`
29-
fn namespace_to_module(namespace: &Namespace) -> Module {
30-
let mut module = Module::new(&*namespace.name);
109+
fn gen_mod(scope: &mut Scope, namespace: &Namespace) {
110+
let module = scope.new_module(&namespace.name);
31111
for ns in &namespace.namespaces {
32-
module.push_module(namespace_to_module(ns));
112+
gen_mod(module.scope(), ns);
33113
}
34114
for test in &namespace.test_cases {
35-
module.push_fn(test_case_to_function(test));
115+
gen_test(module.scope(), test);
36116
}
37-
module
38117
}
39-
40-
/// Converts a test case into a testing `Function`
41-
fn test_case_to_function(test_case: &TestCase) -> Function {
42-
let mut test_fn: Function = Function::new(&test_case.test_name);
118+
fn gen_test(scope: &mut Scope, test_case: &TestCase) {
119+
let test_fn: &mut Function = scope.new_fn(&test_case.test_name);
43120
test_fn.attr("test");
44121
test_fn.line(format!("let statement = r#\"{}\"#;", &test_case.statement));
45122
for assertion in &test_case.assertions {
@@ -59,7 +136,6 @@ fn test_case_to_function(test_case: &TestCase) -> Function {
59136
}
60137
}
61138
}
62-
test_fn
63139
}
64140

65141
#[cfg(test)]

partiql-conformance-test-generator/src/lib.rs

Lines changed: 36 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,174 +1,57 @@
1-
pub mod generator;
1+
mod generator;
2+
mod reader;
23
mod schema;
3-
pub mod util;
4+
mod util;
5+
mod writer;
46

5-
use crate::schema::{
7+
use crate::generator::Generator;
8+
use crate::schema::spec::{
69
Assertion, Assertions, Namespace, Namespaces, TestCase, TestCases, TestDocument,
710
};
811
use crate::util::StringExt;
9-
use ion_rs::value::owned::OwnedElement;
10-
use ion_rs::value::{Element, Sequence, Struct, SymbolToken};
11-
use ion_rs::IonType;
12-
use std::collections::HashSet;
13-
use std::ops::Add;
1412

15-
// TODO: move these test data parsing functions to own file
16-
/// Converts a vector of Ion data into a `TestDocument`, which can be composed of `Namespace`s
17-
/// and `TestCase`s. `Namespace`s must be provided as IonLists while `TestCase`s must be provided
18-
/// as IonStructs. Other Ion types will result in a panic.
19-
///
20-
/// When encountering a duplicate namespace/test case, a namespace/test case will be added with
21-
/// '_0' suffixed to the end of the name.
22-
pub fn ion_data_to_test_document(all_ion_data: Vec<OwnedElement>) -> TestDocument {
23-
let mut namespaces = Vec::new();
24-
let mut test_cases = Vec::new();
13+
use std::path::Path;
2514

26-
let mut encountered_ns_names: HashSet<String> = HashSet::new();
27-
let mut encountered_tc_names: HashSet<String> = HashSet::new();
15+
use crate::reader::read_schema;
2816

29-
for elem in all_ion_data {
30-
match elem.ion_type() {
31-
IonType::List => {
32-
// namespace in document
33-
let mut ns = test_namespace(&elem);
34-
if encountered_ns_names.contains(&ns.name) {
35-
ns.name.push_str("_0")
36-
}
37-
encountered_ns_names.insert(ns.name.clone());
38-
namespaces.push(ns)
39-
}
40-
IonType::Struct => {
41-
// test case in document
42-
let mut tc = test_case(&elem);
43-
if encountered_tc_names.contains(&tc.test_name) {
44-
tc.test_name.push_str("_0")
45-
}
46-
encountered_tc_names.insert(tc.test_name.clone());
47-
test_cases.push(tc)
48-
}
49-
_ => panic!("Document parsing requires an IonList or IonStruct"),
50-
}
51-
}
52-
TestDocument {
53-
namespaces,
54-
test_cases,
55-
}
17+
// TODO docs
18+
#[derive(Debug, Copy, Clone)]
19+
pub enum OverwriteStrategy {
20+
Overwrite,
21+
Backup,
5622
}
5723

58-
/// Parses the given `OwnedElement` to a `Namespace`. Requires an annotation to provided that will
59-
/// be used to create the name. '_namespace' will be suffixed to the first annotation provided.
60-
/// The namespace can contain sub-namespaces and test cases represented by IonLists and IonStructs
61-
/// respectively. When provided with something other than an IonList or IonStruct, this function
62-
/// will panic.
63-
pub fn test_namespace(element: &OwnedElement) -> Namespace {
64-
let annot: Vec<_> = element
65-
.annotations()
66-
.map(|a| a.text().expect("annotation text"))
67-
.collect();
68-
let name = annot
69-
.first()
70-
.expect("expected an annotation for the namespace")
71-
.escaped_snake_case()
72-
.add("_namespace");
73-
74-
let mut namespaces: Namespaces = Vec::new();
75-
let mut test_cases: TestCases = Vec::new();
76-
77-
let mut encountered_ns_names: HashSet<String> = HashSet::new();
78-
let mut encountered_tc_names: HashSet<String> = HashSet::new();
24+
/// Configuration for the generation of conformance tests.
25+
#[derive(Debug, Copy, Clone)]
26+
pub struct Config {
27+
pub overwrite: OverwriteStrategy,
28+
}
7929

80-
for ns_or_test in element.as_sequence().expect("namespace is list").iter() {
81-
match ns_or_test.ion_type() {
82-
// namespace within the namespace
83-
IonType::List => {
84-
let mut ns = test_namespace(ns_or_test);
85-
if encountered_ns_names.contains(&ns.name) {
86-
ns.name.push_str("_0")
87-
}
88-
encountered_ns_names.insert(ns.name.clone());
89-
namespaces.push(ns)
90-
}
91-
// test case within the namespace
92-
IonType::Struct => {
93-
let mut tc = test_case(ns_or_test);
94-
if encountered_tc_names.contains(&tc.test_name) {
95-
tc.test_name.push_str("_0")
96-
}
97-
encountered_tc_names.insert(tc.test_name.clone());
98-
test_cases.push(tc)
99-
}
100-
_ => panic!("Namespace parsing requires an IonList or IonStruct"),
30+
impl Default for Config {
31+
fn default() -> Self {
32+
Config {
33+
overwrite: OverwriteStrategy::Overwrite,
10134
}
10235
}
103-
Namespace {
104-
name,
105-
namespaces,
106-
test_cases,
107-
}
10836
}
10937

110-
/// Parses the given IonStruct to a `TestCase`. The IonStruct requires two string fields with the
111-
/// 'name' and 'statement' in addition to an 'assert' field containing one or more `Assertions`.
112-
///
113-
/// For test assertions that are not supported (e.g. StaticAnalysisFail), the assertion of
114-
/// `NotYetImplemented` will be used.
115-
fn test_case(element: &OwnedElement) -> TestCase {
116-
let test_struct = element.as_struct().expect("struct");
117-
let test_name = test_struct
118-
.get("name")
119-
.expect("name")
120-
.as_str()
121-
.expect("as_str()")
122-
.escaped_snake_case()
123-
.add("_test");
124-
let statement = test_struct
125-
.get("statement")
126-
.expect("statement")
127-
.as_str()
128-
.expect("as_str()")
129-
.to_string();
130-
131-
let assert_field = test_struct.get("assert").expect("assert field missing");
132-
let assertions_vec: Vec<_> = match assert_field.ion_type() {
133-
IonType::Struct => vec![assert_field],
134-
IonType::List => assert_field
135-
.as_sequence()
136-
.expect("as_sequence")
137-
.iter()
138-
.collect(),
139-
_ => panic!("Invalid IonType for the test case assertions"),
140-
};
141-
let assertions = assertions(&assertions_vec);
142-
TestCase {
143-
test_name,
144-
statement,
145-
assertions,
38+
impl Config {
39+
pub fn new() -> Config {
40+
Config::default()
14641
}
147-
}
14842

149-
/// Converts the vector of Ion values into `Assertions`. Checks that a result field is provided
150-
/// in the vector and has the symbol 'SyntaxSuccess' or 'SyntaxFail', which correspond to
151-
/// `Assertion::SyntaxSuccess` and `Assertion::SyntaxFail` respectively. Other assertion symbols
152-
/// will default to `Assertion::NotYetImplemented`
153-
fn assertions(assertions: &Vec<&OwnedElement>) -> Assertions {
154-
let mut test_case_assertions: Assertions = Vec::new();
155-
for assertion in assertions {
156-
let assertion_struct = assertion.as_struct().expect("as_struct()");
157-
let parse_result = assertion_struct.get("result");
158-
match parse_result {
159-
Some(r) => {
160-
let r_as_str = r.as_str().expect("as_str()");
161-
let assertion = match r_as_str {
162-
"SyntaxSuccess" => Assertion::SyntaxSuccess,
163-
"SyntaxFail" => Assertion::SyntaxFail,
164-
_ => Assertion::NotYetImplemented,
165-
};
166-
test_case_assertions.push(assertion);
167-
}
168-
None => (),
169-
}
43+
pub fn process_dir(
44+
&self,
45+
test_data: impl AsRef<Path>,
46+
out_path: impl AsRef<Path>,
47+
) -> miette::Result<()> {
48+
let schema = read_schema(test_data)?;
49+
let scopes = Generator::new().generate(schema)?;
50+
51+
// TODO implement OverwriteStrategy
52+
writer::write_scopes(out_path, scopes)?;
53+
Ok(())
17054
}
171-
test_case_assertions
17255
}
17356

17457
#[cfg(test)]

0 commit comments

Comments
 (0)