Skip to content

Commit ef84679

Browse files
committed
feat: add a code-free test setup for introspection-engine-tests
SQL -> introspected prisma schema. We expect better compile times and fewer diffs due to meaningless changes in test setup after this. It also makes it easy to add additional checks (introspected schemas are valid, etc.) without adding or changing calls in tests. The first test is the reproduction of a regression in indexes introspection.
1 parent 7550a06 commit ef84679

File tree

14 files changed

+270
-73
lines changed

14 files changed

+270
-73
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

introspection-engine/connectors/mongodb-introspection-connector/src/sampler/statistics/indices.rs

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1+
use super::Name;
2+
use crate::sampler::field_type::FieldType;
13
use convert_case::{Case, Casing};
4+
use datamodel::dml::{self, WithDatabaseName, WithName};
25
use introspection_connector::Warning;
36
use mongodb_schema_describer::{IndexFieldProperty, IndexWalker};
47
use std::collections::BTreeMap;
58

6-
use datamodel::dml::{self, WithDatabaseName, WithName};
7-
8-
use crate::sampler::field_type::FieldType;
9-
10-
use super::Name;
11-
129
/// Add described indices to the models.
1310
pub(super) fn add_to_models(
1411
models: &mut BTreeMap<String, dml::Model>,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use std::{env, fs, io::Write as _, path};
2+
3+
const ROOT_DIR: &str = "tests/simple";
4+
5+
fn main() {
6+
println!("cargo:rerun-if-changed={}", ROOT_DIR);
7+
println!("cargo:rerun-if-changed={}", file!());
8+
9+
let mut all_sql_files = Vec::new();
10+
find_all_sql_files("", &mut all_sql_files);
11+
12+
let out_dir = env::var("OUT_DIR").unwrap();
13+
let out_file_path = path::Path::new(&out_dir).join("simple_tests.rs");
14+
let mut out_file = fs::File::create(out_file_path).unwrap();
15+
16+
for sql_file in &all_sql_files {
17+
let test_name = sql_file.trim_start_matches('/').trim_end_matches(".sql");
18+
let test_name = test_name.replace(&['/', '\\'], "_");
19+
let file_path = sql_file.trim_start_matches('/');
20+
writeln!(
21+
out_file,
22+
"#[test] fn {test_name}() {{ run_simple_test(\"{file_path}\"); }}"
23+
)
24+
.unwrap();
25+
}
26+
}
27+
28+
fn find_all_sql_files(prefix: &str, all_sql_files: &mut Vec<String>) {
29+
let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR");
30+
for entry in fs::read_dir(format!("{cargo_manifest_dir}/{ROOT_DIR}/{prefix}")).unwrap() {
31+
let entry = entry.unwrap();
32+
let file_name = entry.file_name();
33+
let file_name = file_name.to_str().unwrap();
34+
let entry_path = format!("{}/{}", prefix, file_name);
35+
let file_type = entry.file_type().unwrap();
36+
37+
if file_name == "." || file_name == ".." {
38+
continue;
39+
}
40+
41+
if file_type.is_file() {
42+
all_sql_files.push(entry_path);
43+
} else if file_type.is_dir() {
44+
find_all_sql_files(&entry_path, all_sql_files);
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use introspection_engine_tests::test_api::TestApi;
2+
use std::{fs, io::Write as _, path};
3+
use test_setup::{runtime::run_with_thread_local_runtime as tok, TestApiArgs};
4+
5+
const TESTS_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/simple");
6+
7+
fn run_simple_test(test_file_path: &str) {
8+
let file_path = path::Path::new(TESTS_ROOT).join(test_file_path);
9+
let text = std::fs::read_to_string(&file_path).unwrap();
10+
11+
let tags = {
12+
let first_line = text.lines().next().expect("Expected file not to be empty.");
13+
let expected_tags_prefix = "-- tags=";
14+
assert!(
15+
first_line.starts_with(expected_tags_prefix),
16+
"The first line of a simple test must start with \"{}\"",
17+
expected_tags_prefix
18+
);
19+
let tags = first_line.trim_start_matches(expected_tags_prefix);
20+
test_setup::tags_from_comma_separated_list(tags)
21+
};
22+
23+
let test_api_args = TestApiArgs::new("run_simple_test", &[]);
24+
25+
if test_setup::should_skip_test(&test_api_args, tags, Default::default(), Default::default()) {
26+
return;
27+
}
28+
29+
let api = tok(TestApi::new(test_api_args));
30+
tok(api.raw_cmd(&text));
31+
let introspected = tok(api.introspect()).unwrap_or_else(|err| panic!("{}", err));
32+
33+
let last_comment_idx = text
34+
.match_indices("\n/*\n")
35+
.last()
36+
.map(|(idx, _)| idx)
37+
.unwrap_or(text.len() - 1);
38+
39+
let last_comment = text[last_comment_idx..]
40+
.trim_start_matches("\n/*\n")
41+
.trim_end_matches("*/\n");
42+
43+
if last_comment == introspected {
44+
return; // success!
45+
}
46+
47+
if std::env::var("UPDATE_EXPECT").is_ok() {
48+
let mut file = fs::File::create(&file_path).unwrap(); // truncate
49+
let setup_sql = &text[..last_comment_idx];
50+
writeln!(file, "{setup_sql}\n/*\n{introspected}*/").unwrap();
51+
return;
52+
}
53+
54+
test_setup::panic_with_diff(last_comment, &introspected);
55+
}
56+
57+
include!(concat!(env!("OUT_DIR"), "/simple_tests.rs"));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
-- tags=CockroachDb
2+
3+
CREATE TABLE communication_channels (
4+
id bigint NOT NULL,
5+
path character varying(255) NOT NULL,
6+
path_type character varying(255) DEFAULT 'email'::character varying NOT NULL,
7+
position integer,
8+
user_id bigint NOT NULL,
9+
pseudonym_id bigint,
10+
bounce_count integer DEFAULT 0,
11+
confirmation_code character varying(255)
12+
);
13+
14+
/*
15+
now create the indexes with expression columns
16+
*/
17+
18+
CREATE INDEX index_communication_channels_on_path_and_path_type ON communication_channels (lower((path)::text), path_type);
19+
CREATE UNIQUE INDEX index_communication_channels_on_user_id_and_path_and_path_type ON communication_channels (user_id, lower((path)::text), path_type);
20+
CREATE INDEX index_communication_channels_on_confirmation_code ON communication_channels (confirmation_code);
21+
22+
/*
23+
generator client {
24+
provider = "prisma-client-js"
25+
}
26+
27+
datasource db {
28+
provider = "cockroachdb"
29+
url = "env(TEST_DATABASE_URL)"
30+
}
31+
32+
model communication_channels {
33+
id BigInt
34+
path String @db.String(255)
35+
path_type String @default("email") @db.String(255)
36+
position Int?
37+
user_id BigInt
38+
pseudonym_id BigInt?
39+
bounce_count Int? @default(0)
40+
confirmation_code String? @db.String(255)
41+
42+
@@unique([user_id, path_type], map: "index_communication_channels_on_user_id_and_path_and_path_type")
43+
@@index([confirmation_code, path_type], map: "index_communication_channels_on_confirmation_code")
44+
}
45+
*/

libs/datamodel/core/src/lib.rs

+5-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! PSL — Prisma Schema Language
1+
//! # PSL — Prisma Schema Language
22
//!
33
//! This crate is responsible for parsing, validating, formatting and rendering a PSL documents.
44
//!
@@ -27,13 +27,9 @@ pub use crate::{
2727
pub use datamodel_connector;
2828
pub use diagnostics;
2929
pub use dml;
30-
pub use parser_database;
31-
pub use parser_database::is_reserved_type_name;
30+
pub use parser_database::{self, is_reserved_type_name};
3231

33-
use self::{
34-
render::RenderParams,
35-
validate::{validate, DatasourceLoader, GeneratorLoader},
36-
};
32+
use self::validate::{validate, DatasourceLoader, GeneratorLoader};
3733
use crate::common::preview_features::PreviewFeature;
3834
use diagnostics::Diagnostics;
3935
use enumflags2::BitFlags;
@@ -193,7 +189,7 @@ fn load_sources(
193189
pub fn render_datamodel_to_string(datamodel: &dml::Datamodel, configuration: Option<&Configuration>) -> String {
194190
let datasource = configuration.and_then(|c| c.datasources.first());
195191
let mut out = String::new();
196-
render::render_datamodel(RenderParams { datasource, datamodel }, &mut out);
192+
render::render_datamodel(render::RenderParams { datasource, datamodel }, &mut out);
197193
reformat(&out, DEFAULT_INDENT_WIDTH).expect("Internal error: failed to reformat introspected schema")
198194
}
199195

@@ -205,7 +201,7 @@ pub fn render_datamodel_and_config_to_string(
205201
let mut out = String::new();
206202
let datasource = config.datasources.first();
207203
render::render_configuration(config, &mut out);
208-
render::render_datamodel(RenderParams { datasource, datamodel }, &mut out);
204+
render::render_datamodel(render::RenderParams { datasource, datamodel }, &mut out);
209205
reformat(&out, DEFAULT_INDENT_WIDTH).expect("Internal error: failed to reformat introspected schema")
210206
}
211207

libs/datamodel/core/src/validate.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
mod common;
21
mod datasource_loader;
32
mod generator_loader;
43
mod validation_pipeline;

libs/datamodel/core/src/validate/common.rs

-43
This file was deleted.

libs/datamodel/core/src/validate/generator_loader.rs

+42-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use crate::{
2-
ast::WithSpan, common::preview_features::GENERATOR, configuration::Generator, diagnostics::*,
3-
validate::common::parse_and_validate_preview_features, StringFromEnvVar,
2+
ast::WithSpan,
3+
common::preview_features::{FeatureMap, PreviewFeature, GENERATOR},
4+
configuration::{Generator, StringFromEnvVar},
5+
diagnostics::*,
46
};
7+
use itertools::Itertools;
58
use parser_database::{ast, ValueListValidator, ValueValidator};
69
use std::{collections::HashMap, convert::TryFrom};
710

@@ -145,3 +148,40 @@ impl GeneratorLoader {
145148
})
146149
}
147150
}
151+
152+
fn parse_and_validate_preview_features(
153+
preview_features: Vec<String>,
154+
feature_map: &FeatureMap,
155+
span: ast::Span,
156+
diagnostics: &mut Diagnostics,
157+
) -> Vec<PreviewFeature> {
158+
let mut features = vec![];
159+
160+
for feature_str in preview_features {
161+
let feature_opt = PreviewFeature::parse_opt(&feature_str);
162+
match feature_opt {
163+
Some(feature) if feature_map.is_deprecated(&feature) => {
164+
features.push(feature);
165+
diagnostics.push_warning(DatamodelWarning::new_feature_deprecated(&feature_str, span));
166+
}
167+
168+
Some(feature) if !feature_map.is_valid(&feature) => {
169+
diagnostics.push_error(DatamodelError::new_preview_feature_not_known_error(
170+
&feature_str,
171+
feature_map.active_features().iter().map(ToString::to_string).join(", "),
172+
span,
173+
))
174+
}
175+
176+
Some(feature) => features.push(feature),
177+
178+
None => diagnostics.push_error(DatamodelError::new_preview_feature_not_known_error(
179+
&feature_str,
180+
feature_map.active_features().iter().map(ToString::to_string).join(", "),
181+
span,
182+
)),
183+
}
184+
}
185+
186+
features
187+
}

libs/datamodel/core/src/validate/validation_pipeline/validations/relation_fields.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1+
use super::{database_name::validate_db_name, names::Names};
12
use crate::{
23
ast::{self, WithName},
34
diagnostics::DatamodelError,
45
validate::validation_pipeline::context::Context,
56
};
67
use itertools::Itertools;
7-
use std::fmt;
8-
9-
use super::{database_name::validate_db_name, names::Names};
108
use parser_database::{
119
walkers::{ModelWalker, RelationFieldWalker, RelationName},
1210
ReferentialAction,
1311
};
12+
use std::fmt;
1413

1514
struct Fields<'db> {
1615
fields: &'db [ast::FieldId],

libs/test-setup/Cargo.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ version = "0.1.0"
44
authors = ["Tom Houlé <[email protected]>"]
55
edition = "2021"
66

7-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8-
97
[dependencies]
108
connection-string = "0.1.10"
9+
dissimilar = "1.0.3"
1110
enumflags2 = "0.7"
1211
once_cell = "1.3.1"
1312
tokio = { version = "1.0", optional = true, features = ["rt-multi-thread"] }

libs/test-setup/src/diff.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#[track_caller]
2+
pub fn panic_with_diff(expected: &str, found: &str) {
3+
let chunks = dissimilar::diff(expected, found);
4+
let diff = format_chunks(chunks);
5+
panic!(
6+
r#"
7+
Snapshot comparison failed. Run the test again with UPDATE_EXPECT=1 in the environment to update the snapshot.
8+
9+
===== EXPECTED ====
10+
{expected}
11+
====== FOUND ======
12+
{found}
13+
======= DIFF ======
14+
{diff}
15+
"#
16+
);
17+
}
18+
19+
fn format_chunks(chunks: Vec<dissimilar::Chunk<'_>>) -> String {
20+
let mut buf = String::new();
21+
for chunk in chunks {
22+
let formatted = match chunk {
23+
dissimilar::Chunk::Equal(text) => text.into(),
24+
dissimilar::Chunk::Delete(text) => format!("\x1b[41m{}\x1b[0m", text),
25+
dissimilar::Chunk::Insert(text) => format!("\x1b[42m{}\x1b[0m", text),
26+
};
27+
buf.push_str(&formatted);
28+
}
29+
buf
30+
}

0 commit comments

Comments
 (0)