Skip to content

Commit

Permalink
test: use runtime config in execution spec
Browse files Browse the repository at this point in the history
  • Loading branch information
meskill committed Dec 24, 2024
1 parent c2b4136 commit 00a4a7e
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 105 deletions.
5 changes: 0 additions & 5 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RuntimeConfig",
"type": "object",
"required": [
"links"
],
"properties": {
"links": {
"description": "A list of all links in the schema.",
Expand All @@ -15,7 +12,6 @@
},
"server": {
"description": "Dictates how the server behaves and helps tune tailcall for all ingress requests. Features such as request batching, SSL, HTTP2 etc. can be configured here.",
"default": {},
"allOf": [
{
"$ref": "#/definitions/Server"
Expand All @@ -32,7 +28,6 @@
},
"upstream": {
"description": "Dictates how tailcall should handle upstream requests/responses. Tuning upstream can improve performance and reliability for connections.",
"default": {},
"allOf": [
{
"$ref": "#/definitions/Upstream"
Expand Down
5 changes: 3 additions & 2 deletions src/core/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,18 @@ pub struct RuntimeConfig {
/// Dictates how the server behaves and helps tune tailcall for all ingress
/// requests. Features such as request batching, SSL, HTTP2 etc. can be
/// configured here.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub server: Server,

///
/// Dictates how tailcall should handle upstream requests/responses.
/// Tuning upstream can improve performance and reliability for connections.
#[serde(default)]
#[serde(default, skip_serializing_if = "is_default")]
pub upstream: Upstream,

///
/// A list of all links in the schema.
#[serde(default, skip_serializing_if = "is_default")]
pub links: Vec<Link>,

/// Enable [opentelemetry](https://opentelemetry.io) support
Expand Down
7 changes: 6 additions & 1 deletion src/core/config/transformer/subgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ impl Transform for Subgraph {
let key = Key { fields };

to_directive(key.to_directive()).map(|directive| {
ty.directives.push(directive);
// Prevent transformer to push the same directive multiple times
if !ty.directives.iter().any(|d| {
d.name == directive.name && d.arguments == directive.arguments
}) {
ty.directives.push(directive);
}
})
}
None => Valid::succeed(()),
Expand Down
24 changes: 17 additions & 7 deletions tests/core/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use tailcall::cli::javascript;
use tailcall::core::app_context::AppContext;
use tailcall::core::blueprint::Blueprint;
use tailcall::core::cache::InMemoryCache;
use tailcall::core::config::{ConfigModule, Source};
use tailcall::core::config::{ConfigModule, Link, RuntimeConfig, Source};
use tailcall::core::merge_right::MergeRight;
use tailcall::core::runtime::TargetRuntime;
use tailcall::core::worker::{Command, Event};
use tailcall::core::{EnvIO, WorkerIO};
Expand Down Expand Up @@ -51,14 +52,15 @@ impl ExecutionSpec {
.peekable();

let mut name: Option<String> = None;
let mut server: Vec<(Source, String)> = Vec::with_capacity(2);
let mut config = RuntimeConfig::default();
let mut mock: Option<Vec<Mock>> = None;
let mut env: Option<HashMap<String, String>> = None;
let mut files: BTreeMap<String, String> = BTreeMap::new();
let mut test: Option<Vec<APIRequest>> = None;
let mut runner: Option<Annotation> = None;
let mut check_identity = false;
let mut sdl_error = false;
let mut links_counter = 0;

while let Some(node) = children.next() {
match node {
Expand Down Expand Up @@ -172,8 +174,16 @@ impl ExecutionSpec {

match name {
"config" => {
// Server configs are only parsed if the test isn't skipped.
server.push((source, content));
config = config.merge_right(
RuntimeConfig::from_source(source, &content).unwrap(),
);
}
"schema" => {
// Schemas configs are only parsed if the test isn't skipped.
let name = format!("schema_{}.graphql", links_counter);
files.insert(name.clone(), content);
config.links.push(Link { src: name, ..Default::default() });
links_counter += 1;
}
"mock" => {
if mock.is_none() {
Expand Down Expand Up @@ -240,9 +250,9 @@ impl ExecutionSpec {
}
}

if server.is_empty() {
if links_counter == 0 {
return Err(anyhow!(
"Unexpected blocks in {:?}: You must define a GraphQL Config in an execution test.",
"Unexpected blocks in {:?}: You must define a GraphQL Schema in an execution test.",
path,
));
}
Expand All @@ -252,7 +262,7 @@ impl ExecutionSpec {
name: name.unwrap_or_else(|| path.file_name().unwrap().to_str().unwrap().to_string()),
safe_name: path.file_name().unwrap().to_str().unwrap().to_string(),

server,
config,
mock,
env,
test,
Expand Down
4 changes: 2 additions & 2 deletions tests/core/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use derive_setters::Setters;
use tailcall::cli::javascript::init_worker_io;
use tailcall::core::blueprint::Script;
use tailcall::core::cache::InMemoryCache;
use tailcall::core::config::Source;
use tailcall::core::config::RuntimeConfig;
use tailcall::core::runtime::TargetRuntime;
use tailcall::core::worker::{Command, Event};

Expand All @@ -25,7 +25,7 @@ pub struct ExecutionSpec {
pub name: String,
pub safe_name: String,

pub server: Vec<(Source, String)>,
pub config: RuntimeConfig,
pub mock: Option<Vec<Mock>>,
pub env: Option<HashMap<String, String>>,
pub test: Option<Vec<APIRequest>>,
Expand Down
150 changes: 62 additions & 88 deletions tests/core/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ use tailcall::core::async_graphql_hyper::{GraphQLBatchRequest, GraphQLRequest};
use tailcall::core::blueprint::{Blueprint, BlueprintError};
use tailcall::core::config::reader::ConfigReader;
use tailcall::core::config::transformer::Required;
use tailcall::core::config::{Config, ConfigModule, ConfigReaderContext, Source};
use tailcall::core::config::{Config, ConfigModule, ConfigReaderContext, LinkType, Source};
use tailcall::core::http::handle_request;
use tailcall::core::mustache::PathStringEval;
use tailcall::core::print_schema::print_schema;
use tailcall::core::variance::Invariant;
use tailcall::core::Mustache;
use tailcall_prettier::Parser;
use tailcall_valid::{Cause, Valid, ValidationError, Validator};
Expand Down Expand Up @@ -97,54 +96,53 @@ async fn check_identity(spec: &ExecutionSpec, reader_ctx: &ConfigReaderContext<'
// enabled for either new tests that request it or old graphql_spec
// tests that were explicitly written with it in mind
if spec.check_identity {
for (source, content) in spec.server.iter() {
if matches!(source, Source::GraphQL) {
let mustache = Mustache::parse(content);
let content = PathStringEval::new().eval_partial(&mustache, reader_ctx);
let config = Config::from_source(source.to_owned(), &content).unwrap();
let actual = config.to_sdl();

// \r is added automatically in windows, it's safe to replace it with \n
let content = content.replace("\r\n", "\n");

let path_str = spec.path.display().to_string();
let context = format!("path: {}", path_str);

let actual = tailcall_prettier::format(actual, &tailcall_prettier::Parser::Gql)
.await
.map_err(|e| e.with_context(context.clone()))
.unwrap();

let expected = tailcall_prettier::format(content, &tailcall_prettier::Parser::Gql)
.await
.map_err(|e| e.with_context(context.clone()))
.unwrap();

pretty_assertions::assert_eq!(
actual,
expected,
"Identity check failed for {:#?}",
spec.path,
);
} else {
panic!(
"Spec {:#?} has \"check identity\" enabled, but its config isn't in GraphQL.",
spec.path
);
}
for link in spec
.config
.links
.iter()
.filter(|link| link.type_of == LinkType::Config)
{
let content = reader_ctx.runtime.file.read(&link.src).await.unwrap();
let mustache = Mustache::parse(&content);
let content = PathStringEval::new().eval_partial(&mustache, reader_ctx);
let config = Config::from_source(Source::GraphQL, &content).unwrap();
let actual = config.to_sdl();

// \r is added automatically in windows, it's safe to replace it with \n
let content = content.replace("\r\n", "\n");

let path_str = spec.path.display().to_string();
let context = format!("path: {}", path_str);

let actual = tailcall_prettier::format(actual, &tailcall_prettier::Parser::Gql)
.await
.map_err(|e| e.with_context(context.clone()))
.unwrap();

let expected = tailcall_prettier::format(content, &tailcall_prettier::Parser::Gql)
.await
.map_err(|e| e.with_context(context.clone()))
.unwrap();

pretty_assertions::assert_eq!(
actual,
expected,
"Identity check failed for {:#?}",
spec.path,
);
}
}
}

async fn run_query_tests_on_spec(
spec: ExecutionSpec,
server: Vec<ConfigModule>,
config_module: &ConfigModule,
mock_http_client: Arc<Http>,
) {
if let Some(tests) = spec.test.as_ref() {
let app_ctx = spec
.app_context(
server.first().unwrap(),
config_module,
spec.env.clone().unwrap_or_default(),
mock_http_client.clone(),
)
Expand Down Expand Up @@ -200,42 +198,27 @@ async fn test_spec(spec: ExecutionSpec) {

let reader = ConfigReader::init(runtime);

// Resolve all configs
let config_modules = join_all(spec.server.iter().map(|(source, content)| async {
let mustache = Mustache::parse(content);
let content = PathStringEval::new().eval_partial(&mustache, &reader_ctx);
let config = Config::from(spec.config.clone());

let config = Config::from_source(source.to_owned(), &content)?;
let config_module = reader.resolve(config, spec.path.parent()).await;

reader.resolve(config, spec.path.parent()).await
}))
.await;

let config_module = Valid::from_iter(config_modules.iter(), |config_module| {
Valid::from(config_module.as_ref().map_err(|e| {
match e.downcast_ref::<ValidationError<String>>() {
let config_module =
Valid::from(
config_module.map_err(|e| match e.downcast_ref::<ValidationError<String>>() {
Some(err) => err.clone(),
None => ValidationError::new(e.to_string()),
}
}))
})
.and_then(|cfgs| {
let mut cfgs = cfgs.into_iter();
let config_module = cfgs.next().expect("At least one config should be defined");

cfgs.fold(Valid::succeed(config_module.clone()), |acc, c| {
acc.and_then(|acc| acc.unify(c.clone()))
})
})
// Apply required transformers to the configuration
.and_then(|cfg| cfg.transform(Required));
}),
)
// Apply required transformers to the configuration
.and_then(|cfg| cfg.transform(Required));

// check sdl error if any
if is_sdl_error(&spec, config_module.clone()).await {
return;
}

let merged = config_module.to_result().unwrap().to_sdl();
let config_module = config_module.to_result().unwrap();
let merged = config_module.to_sdl();

let formatter = tailcall_prettier::format(merged, &Parser::Gql)
.await
Expand All @@ -245,34 +228,25 @@ async fn test_spec(spec: ExecutionSpec) {

insta::assert_snapshot!(snapshot_name, formatter);

let config_modules = config_modules
.into_iter()
.collect::<Result<Vec<_>, _>>()
.unwrap();

check_identity(&spec, &reader_ctx).await;

// client: Check if client spec matches snapshot
if config_modules.len() == 1 {
let config = &config_modules[0];

let client = print_schema(
(Blueprint::try_from(config)
.context(format!("file: {}", spec.path.to_str().unwrap()))
.unwrap())
.to_schema(),
);

let formatted = tailcall_prettier::format(client, &Parser::Gql)
.await
.unwrap();
let snapshot_name = format!("{}_client", spec.safe_name);

insta::assert_snapshot!(snapshot_name, formatted);
}
let client = print_schema(
(Blueprint::try_from(&config_module)
.context(format!("file: {}", spec.path.to_str().unwrap()))
.unwrap())
.to_schema(),
);

let formatted = tailcall_prettier::format(client, &Parser::Gql)
.await
.unwrap();
let snapshot_name = format!("{}_client", spec.safe_name);

insta::assert_snapshot!(snapshot_name, formatted);

// run query tests
run_query_tests_on_spec(spec, config_modules, mock_http_client).await;
run_query_tests_on_spec(spec, &config_module, mock_http_client).await;
}

pub async fn load_and_test_execution_spec(path: &Path) -> anyhow::Result<()> {
Expand Down

0 comments on commit 00a4a7e

Please sign in to comment.