Skip to content

Commit 81a935c

Browse files
committed
feat(psl): set up declarative test suite
The main problem this addresses are: - Diffs are noisy. Datamodel tests involve large strings literals, and very little significant information otherwise. In a single file, even for simple changes like reordering or renaming tests, or changing indentation, the diffs get unreadable quickly, as the large blocks of Prisma schemas and validation errors get mixed with the test function names and calls to test helpers. This state of things disincentivizes reorganizing tests. The new setup has one test per file, with the test name being the file name, including its full path from the validation tests root. - Churn. We often need to subtly adjust many tests to account for trivial changes in the public APIs, when all we want to do is assert that a certain schema is valid, or invalid with a given error. This prevents us from evolving the datamodel API as much as we would like, and as a consequence of these changes being a lot of work, we have a lot of inconsistency in how tests are written and which test helpers are used. This has manifested recently in difficulties simply moving tests, without changing them, from `libs/datamodel/core` to `psl/psl`. With this test suite, we rely on a lot less context, setup and noise (variable names for expectations). We don't expect the shape of these tests to evolve much, churn should be minimal and if we need to change something, it will be possible to autofix through UPDATE_EXPECT=1. - The test suite gets slower to compile as we add more tests, and that noticeably hurts our feedback loops. This setup is less rust code to compile: each test case translates to a single test function containing a single synchronous function call. We can transparently make test runs compile no rust code at all in the future if we need to. Prior art: - In prisma-engines: we already did that in introspection engine tests in that commit: ef84679 - In other compiler-like projects, this is a common approach. Here are a few examples: - go compiler (https://github.com/golang/go/blob/master/test/assign1.go) - rustc UI tests (https://github.com/rust-lang/rust/tree/master/src/test/ui) - zig compiler (ziglang/zig#11288) next steps: same setup for reformatting tests
1 parent 1b996e9 commit 81a935c

15 files changed

+247
-172
lines changed

Cargo.lock

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

libs/datamodel/core/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ regex = "1.3.7"
2020
serde = { version = "1.0.90", features = ["derive"] }
2121
serde_json = { version = "1.0", features = ["preserve_order", "float_roundtrip"] }
2222
enumflags2 = "0.7"
23-
either = "1.6"
2423
indoc = "1"
24+
either = "1.6"
2525

2626
[dev-dependencies]
27+
dissimilar = "1.0.4"
2728
expect-test = "1.1.0"
2829
native-types = { path = "../../native-types" }
2930
pretty_assertions = "0.6.1"

libs/datamodel/core/README.md

+28-52
Original file line numberDiff line numberDiff line change
@@ -10,73 +10,49 @@ Schema Language, and it is used by all Prisma engines in this repository.
1010
The query engine further processes Prisma schemas to produce the client API,
1111
and the DMMF JSON format.
1212

13-
Here is a (slightly dated) overview diagram:
14-
15-
![Architecture Overview](doc/images/overview.png?raw=true)
16-
1713
### Design goals
1814

1915
- Strict parsing: a duplicate attribute, unknown attribute, unknown argument or extra argument is an error.
2016
- Expose a _convenient_ and _obvious_ / hard-to-misuse public API.
2117
- Expose the source information (AST node spans, etc) in the parsed schema.
2218
- Accumulate errors to present them at the end instead of returning early.
2319

24-
### Data Formats
25-
26-
**Sources** represents the different datasources declared in the schema.
27-
28-
**DML** is a datamodel data structure which is used by other Rust components of Prisma.
29-
30-
**AST** is the internal AST representation of a Prisma datamodel V2 file.
31-
32-
**Datamodel V2 String** is the string representation of a Prisma datamodel V2 file.
33-
34-
**DMMF** Internal JSON format for transferring datamodel and schema information
35-
between different components of Prisma. The DMMF is in parts in the `dmmf`
36-
crate in datamodel, but for the most part defined in the query engine.
37-
38-
### Steps
39-
40-
**Parse** parses a string to an AST and performs basic syntactic checks.
41-
42-
**Load Sources** Loads all datasource and generator declarations. This injects
43-
source-specific attributes into the validation pipeline.
44-
45-
**Validate** performs several checks to ensure the datamodel is valid. This
46-
includes, for example, checking invalid type references, or relations which are
47-
impossible to model on a database.
20+
## Usage
4821

49-
**Lift** converts a validated schema to a DML struct. That step cannot fail, it
50-
does not perform any validation.
22+
Please see [`lib.rs`](src/lib.rs) and the [rustdoc documentation](https://prisma.github.io/prisma-engines/doc/datamodel/) for the public API.
5123

52-
**Lower** generates an AST from a DML struct. This step will attempt to
53-
minimize the AST by removing all attributes and attribute arguments which are a
54-
default anyway.
24+
Main use-case, parsing a string to datamodel:
5525

56-
**Render** renders a given AST to its string representation.
26+
```
27+
let file = std::fs::read_to_string(&args[1]).unwrap();
28+
let validated_schema = datamodel::parse_schema_parserdb(&file)?;
29+
```
5730

58-
## Error handling
31+
## Tests
5932

60-
The datamodel parser strives to provide good error diagnostics to schema
61-
authors. As such, it has to be capable of dealing with partially broken input
62-
and keep validating. The validation process can however not proceed to the
63-
validating models in presence of a broken datasource, for example, because that
64-
would lead to a cascade of misleading errors. Like other parsers, we introduce
65-
*cutoff points* where validation will stop in the presence of errors.
33+
### Running the test suite
6634

67-
These are:
35+
Run `cargo test` for this crate (`datamodel`). There should be no setup required beyond that.
6836

69-
- AST parsing stage. Syntax errors.
70-
- Configuration validation
71-
- Datamodel validation
37+
### For test authors
7238

73-
## Usage
39+
There are two entry points for PSL tests. The tests involving writing plain
40+
Rust tests, in `datamodel_tests.rs`, and the declarative validation tests, in
41+
`validation.rs`.
7442

75-
Please see [`lib.rs`](src/lib.rs) and the [rustdoc documentation](https://prisma.github.io/prisma-engines/doc/datamodel/) for all convenience methods.
43+
For new tests, the validation test suite should be preferred: the tests are
44+
more straightforward to write, more declarative (no unnecessary function calls,
45+
test helpers, variable names, etc. that we always have to change at some
46+
point), and much faster to compile.
7647

77-
Main use-case, parsing a string to datamodel:
48+
A validation test is a `.prisma` file in the `tests/validation` directory, or
49+
any of its subdirectories (transitively). It is a regular Prisma schema that
50+
will be passed through the PSL validation process, and is expected to be valid,
51+
or return errors. That expectation is defined by the comment (`//`) at the end
52+
of the file. If there is no comment at the end of the file, the schema is
53+
expected to be valid. If there is a comment at the end of the file, the
54+
rendered, user-visible validation warnings and errors will be expected to match
55+
the contents of that comment.
7856

79-
```
80-
let file = std::fs::read_to_string(&args[1]).unwrap();
81-
let validated_schema = datamodel::parse_schema_parserdb(&file)?;
82-
```
57+
Like in `expect_test` tests, the last comment can be left out at first, and
58+
updated like a snapshot test using the `UPDATE_EXPECT=1` env var.

libs/datamodel/core/build.rs

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use std::{env, fs, io::Write as _, path};
2+
3+
const ROOT_DIR: &str = "tests/validation";
4+
5+
fn main() {
6+
println!("cargo:rerun-if-changed={ROOT_DIR}");
7+
8+
let mut all_schemas = Vec::new();
9+
find_all_schemas("", &mut all_schemas);
10+
11+
let out_dir = env::var("OUT_DIR").unwrap();
12+
let out_file_path = path::Path::new(&out_dir).join("validation_tests.rs");
13+
let mut out_file = fs::File::create(out_file_path).unwrap();
14+
15+
for schema_path in &all_schemas {
16+
let test_name = schema_path.trim_start_matches('/').trim_end_matches(".prisma");
17+
let test_name = test_name.replace(&['/', '\\'], "_");
18+
let file_path = schema_path.trim_start_matches('/');
19+
writeln!(
20+
out_file,
21+
"#[test] fn {test_name}() {{ run_validation_test(\"{file_path}\"); }}"
22+
)
23+
.unwrap();
24+
}
25+
}
26+
27+
fn find_all_schemas(prefix: &str, all_schemas: &mut Vec<String>) {
28+
let cargo_manifest_dir = env!("CARGO_MANIFEST_DIR");
29+
for entry in fs::read_dir(format!("{cargo_manifest_dir}/{ROOT_DIR}/{prefix}")).unwrap() {
30+
let entry = entry.unwrap();
31+
let file_name = entry.file_name();
32+
let file_name = file_name.to_str().unwrap();
33+
let entry_path = format!("{}/{}", prefix, file_name);
34+
let file_type = entry.file_type().unwrap();
35+
36+
if file_name == "." || file_name == ".." {
37+
continue;
38+
}
39+
40+
if file_type.is_file() {
41+
all_schemas.push(entry_path);
42+
} else if file_type.is_dir() {
43+
find_all_schemas(&entry_path, all_schemas);
44+
}
45+
}
46+
}
-17.3 KB
Binary file not shown.

libs/datamodel/core/tests/attributes/map_negative.rs

-77
This file was deleted.

libs/datamodel/core/tests/attributes/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ mod ignore_positive;
1717
mod index_clustering;
1818
mod index_negative;
1919
mod index_positive;
20-
mod map_negative;
2120
mod map_positive;
2221
mod postgres_indices;
2322
mod relations;

libs/datamodel/core/tests/capabilities/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
mod cockroachdb;
2-
mod mongodb;
32
mod mysql;
43
mod postgres;
54
mod sqlite;

libs/datamodel/core/tests/capabilities/mongodb.rs

-40
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
model User {
2+
id Int @id
3+
fooId Int
4+
relationField Foo @relation(fields: [fooId], references: [id]) @map("custom_name")
5+
}
6+
7+
model Foo {
8+
id Int @id
9+
}
10+
11+
// error: Error parsing attribute "@map": The attribute `@map` cannot be used on relation fields.
12+
// --> schema.prisma:4
13+
//  | 
14+
//  3 |  fooId Int
15+
//  4 |  relationField Foo @relation(fields: [fooId], references: [id]) @map("custom_name")
16+
//  | 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
datasource mydb {
2+
provider = "mongodb"
3+
url = env("TEST_DB_URL")
4+
}
5+
6+
model Foo {
7+
id Int @id @map("_id")
8+
field String @map("field.schwield")
9+
}
10+
11+
// error: Error parsing attribute "@map": The field name cannot contain a `.` character
12+
// --> schema.prisma:8
13+
//  | 
14+
//  7 |  id Int @id @map("_id")
15+
//  8 |  field String @map("field.schwield")
16+
//  | 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
datasource mydb {
2+
provider = "mongodb"
3+
url = env("TEST_DB_URL")
4+
}
5+
6+
model Foo {
7+
id Int @id @map("_id")
8+
field String @map("$field")
9+
}
10+
11+
// error: Error parsing attribute "@map": The field name cannot start with a `$` character
12+
// --> schema.prisma:8
13+
//  | 
14+
//  7 |  id Int @id @map("_id")
15+
//  8 |  field String @map("$field")
16+
//  | 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
datasource mydb {
2+
provider = "mongodb"
3+
url = env("TEST_DB_URL")
4+
}
5+
6+
model User {
7+
id Int @id @default(autoincrement()) @map("_id")
8+
}
9+
10+
11+
// error: Error parsing attribute "@default": The `autoincrement()` default value is used with a datasource that does not support it.
12+
// --> schema.prisma:7
13+
//  | 
14+
//  6 | model User {
15+
//  7 |  id Int @id @default(autoincrement()) @map("_id")
16+
//  | 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
datasource mydb {
2+
provider = "mongodb"
3+
url = env("TEST_DB_URL")
4+
}
5+
6+
7+
type Address {
8+
street String
9+
}

0 commit comments

Comments
 (0)