From 84431f1a37512e90032df407c8a77c84b6f03e08 Mon Sep 17 00:00:00 2001 From: George Sittas Date: Thu, 28 Mar 2024 20:02:51 +0200 Subject: [PATCH 01/10] Feat: add support for freezing time in unit tests --- docs/concepts/tests.md | 49 ++++++++++++++++++ setup.py | 2 +- sqlmesh/core/test/definition.py | 46 ++++++++++++----- tests/core/test_test.py | 91 ++++++++++++++++++++++++++++++--- 4 files changed, 167 insertions(+), 21 deletions(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index a1390724d8..2d9c269376 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -15,6 +15,7 @@ Tests within a suite file contain the following attributes: * The unique name of a test * The name of the model targeted by this test * [Optional] The test's description +* [Optional] A datetime value that will be used to "freeze" time in the context of this test * Test inputs, which are defined per upstream model or external table referenced by the target model. Each test input consists of the following: * The name of an upstream model or external table * The list of rows defined as a mapping from a column name to a value associated with it @@ -30,6 +31,7 @@ The YAML format is defined as follows: : model: description: # Optional + freeze_time: # Optional inputs: : rows: @@ -210,6 +212,53 @@ test_example_full_model: num_orders: 2 ``` +### Freezing Time + +Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, it wouldn't suffice to simply specify an expected datetime value in the outputs. + +The `freeze_time` attribute addresses this problem by setting the current time to the given datetime value, thus making the former deterministic. + +The following example demonstrates how `freeze_time` can be used to test a column that is computed using `CURRENT_TIMESTAMP`. The model we're going to test is defined as: + +```sql linenums="1" +MODEL ( + name colors, + kind FULL +); + +SELECT + 'Yellow' AS color, + CURRENT_TIMESTAMP AS created_at +``` + +And the corresponding test is: + +```yaml linenums="1" +test_colors: + model: colors + freeze_time: "2023-01-01 12:05:03" + outputs: + query: + - color: "Yellow" + created_at: "2023-01-01 12:05:03" +``` + +It's also possible to set a time zone in the `freeze_time` datetime value, by including it in the timestamp string. + +If a time zone is provided, it is currently required that the expected datetime values are timestamps without time zone, meaning that they need to be offset accordingly. + +Here's how we would write the above test if we wanted to freeze the time to UTC+2: + +```yaml linenums="1" +test_colors: + model: colors + freeze_time: "2023-01-01 12:05:03+02:00" + outputs: + query: + - color: "Yellow" + created_at: "2023-01-01 10:05:03" +``` + ## Automatic test generation Creating tests manually can be repetitive and error-prone, which is why SQLMesh also provides a way to automate this process using the [`create_test` command](../reference/cli.md#create_test). diff --git a/setup.py b/setup.py index fa7e5e1162..b7933e01a0 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,7 @@ "croniter", "duckdb", "dateparser", + "freezegun", "hyperscript", "ipywidgets", "jinja2", @@ -66,7 +67,6 @@ "dbt-core", "dbt-duckdb>=1.7.1", "Faker", - "freezegun", "google-auth", "google-cloud-bigquery", "google-cloud-bigquery-storage", diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index 267bdfcf9a..d4dba13482 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -3,11 +3,14 @@ import typing as t import unittest from collections import Counter +from contextlib import nullcontext from pathlib import Path +from unittest.mock import patch import numpy as np import pandas as pd -from sqlglot import exp +from freezegun import freeze_time +from sqlglot import Dialect, exp from sqlglot.optimizer.annotate_types import annotate_types from sqlglot.optimizer.normalize_identifiers import normalize_identifiers @@ -70,6 +73,20 @@ def __init__( if depends_on not in inputs: _raise_error(f"Incomplete test, missing input for table {depends_on}", path) + self._engine_adapter_dialect = Dialect.get_or_raise(self.engine_adapter.dialect) + self._transforms = self._engine_adapter_dialect.generator_class.TRANSFORMS + + self._freeze_time = str(self.body.get("freeze_time") or "") + if self._freeze_time: + freeze_time = exp.Literal.string(self._freeze_time) + self._transforms = { + **self._transforms, + exp.CurrentDate: lambda self, _: self.sql(exp.cast(freeze_time, "date")), + exp.CurrentDatetime: lambda self, _: self.sql(exp.cast(freeze_time, "datetime")), + exp.CurrentTime: lambda self, _: self.sql(exp.cast(freeze_time, "time")), + exp.CurrentTimestamp: lambda self, _: self.sql(exp.cast(freeze_time, "timestamp")), + } + super().__init__() def shortDescription(self) -> t.Optional[str]: @@ -275,10 +292,6 @@ def _normalize_sources(sources: t.Dict, partial: bool = False) -> t.Dict: class SqlModelTest(ModelTest): - def _execute(self, query: exp.Expression) -> pd.DataFrame: - """Executes the query with the engine adapter and returns a DataFrame.""" - return self.engine_adapter.fetchdf(query) - def test_ctes(self, ctes: t.Dict[str, exp.Expression]) -> None: """Run CTE queries and compare output to expected output""" for cte_name, values in self.body["outputs"].get("ctes", {}).items(): @@ -331,6 +344,11 @@ def runTest(self) -> None: self.assert_equal(expected, actual, sort=sort, partial=partial) + def _execute(self, query: exp.Expression) -> pd.DataFrame: + """Executes the query with the engine adapter and returns a DataFrame.""" + with patch.dict(self._engine_adapter_dialect.generator_class.TRANSFORMS, self._transforms): + return self.engine_adapter.fetchdf(query) + class PythonModelTest(ModelTest): def __init__( @@ -368,13 +386,6 @@ def __init__( default_catalog=default_catalog, ) - def _execute_model(self) -> pd.DataFrame: - """Executes the python model and returns a DataFrame.""" - return t.cast( - pd.DataFrame, - next(self.model.render(context=self.context, **self.body.get("vars", {}))), - ) - def runTest(self) -> None: values = self.body["outputs"].get("query") if values is not None: @@ -387,6 +398,17 @@ def runTest(self) -> None: self.assert_equal(expected, actual_df, sort=False, partial=partial) + def _execute_model(self) -> pd.DataFrame: + """Executes the python model and returns a DataFrame.""" + with ( + freeze_time(self._freeze_time) if self._freeze_time else nullcontext(), # type: ignore + patch.dict(self._engine_adapter_dialect.generator_class.TRANSFORMS, self._transforms), + ): + return t.cast( + pd.DataFrame, + next(self.model.render(context=self.context, **self.body.get("vars", {}))), + ) + def generate_test( model: Model, diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 7c6c6af1c3..74952fa435 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -4,7 +4,9 @@ import typing as t from pathlib import Path +import pandas as pd import pytest +from pytest_mock.plugin import MockerFixture from sqlglot import exp from sqlmesh.cli.example_project import init_example_project @@ -12,8 +14,8 @@ from sqlmesh.core.config import Config, DuckDBConnectionConfig, ModelDefaultsConfig from sqlmesh.core.context import Context from sqlmesh.core.dialect import parse -from sqlmesh.core.model import SqlModel, load_sql_based_model -from sqlmesh.core.test.definition import SqlModelTest +from sqlmesh.core.model import PythonModel, SqlModel, load_sql_based_model, model +from sqlmesh.core.test.definition import PythonModelTest, SqlModelTest from sqlmesh.utils.errors import ConfigError from sqlmesh.utils.yaml import load as load_yaml @@ -25,13 +27,21 @@ SUSHI_FOO_META = "MODEL (name sushi.foo, kind FULL)" +@t.overload def _create_test( - body: t.Dict[str, t.Any], - test_name: str, - model: SqlModel, - context: Context, -) -> SqlModelTest: - return SqlModelTest( + body: t.Dict[str, t.Any], test_name: str, model: SqlModel, context: Context +) -> SqlModelTest: ... + + +@t.overload +def _create_test( + body: t.Dict[str, t.Any], test_name: str, model: PythonModel, context: Context +) -> PythonModelTest: ... + + +def _create_test(body, test_name, model, context): + test_type = SqlModelTest if isinstance(model, SqlModel) else PythonModelTest + return test_type( body=body[test_name], test_name=test_name, model=model, @@ -841,6 +851,71 @@ def test_nested_data_types() -> None: _check_successful_or_raise(result) +def test_freeze_time(mocker: MockerFixture) -> None: + test = _create_test( + body=load_yaml( + """ +test_foo: + model: xyz + freeze_time: "2023-01-01 12:05:03+00:00" + outputs: + query: + - cur_date: 2023-01-01 + cur_time: 12:05:03 + cur_timestamp: "2023-01-01 12:05:03" + """ + ), + test_name="test_foo", + model=_create_model( + "SELECT CURRENT_DATE AS cur_date, CURRENT_TIME AS cur_time, CURRENT_TIMESTAMP AS cur_timestamp" + ), + context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), + ) + + spy_execute = mocker.spy(test.engine_adapter, "_execute") + _check_successful_or_raise(test.run()) + + spy_execute.assert_called_with( + "SELECT " + """CAST('2023-01-01 12:05:03+00:00' AS DATE) AS "cur_date", """ + """CAST('2023-01-01 12:05:03+00:00' AS TIME) AS "cur_time", """ + '''CAST('2023-01-01 12:05:03+00:00' AS TIMESTAMP) AS "cur_timestamp"''', + ) + + context = Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))) + + @model("py_model", columns={"ts1": "timestamptz", "ts2": "timestamptz"}) + def execute(context, start, end, execution_time, **kwargs): + datetime_now = datetime.datetime.now() + + context.engine_adapter.execute(exp.select("CURRENT_TIMESTAMP")) + current_timestamp = context.engine_adapter.cursor.fetchone()[0] + + return pd.DataFrame([{"ts1": datetime_now, "ts2": current_timestamp}]) + + py_model = model.get_registry()["py_model"].model(module_path=Path("."), path=Path(".")) + context.upsert_model(py_model) + + test = _create_test( + body=load_yaml( + """ +test_py_model: + model: py_model + freeze_time: "2023-01-01 12:05:03+02:00" + outputs: + query: + - ts1: "2023-01-01 10:05:03" + ts2: "2023-01-01 10:05:03" + """ + ), + test_name="test_py_model", + model=py_model, + context=context, + ) + + _check_successful_or_raise(test.run()) + + def test_successes(sushi_context: Context) -> None: results = sushi_context.test() successful_tests = [success.test_name for success in results.successes] # type: ignore From 468fd8b4c89757cc7a3b56bc2829dc1115bcec66 Mon Sep 17 00:00:00 2001 From: George Sittas Date: Thu, 28 Mar 2024 20:52:23 +0200 Subject: [PATCH 02/10] Use execution_time instead of introducing new config, update docs --- docs/concepts/tests.md | 16 ++++++++-------- sqlmesh/core/test/definition.py | 16 ++++++++-------- tests/core/test_test.py | 6 ++++-- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index 2d9c269376..6ac6497f0a 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -15,7 +15,6 @@ Tests within a suite file contain the following attributes: * The unique name of a test * The name of the model targeted by this test * [Optional] The test's description -* [Optional] A datetime value that will be used to "freeze" time in the context of this test * Test inputs, which are defined per upstream model or external table referenced by the target model. Each test input consists of the following: * The name of an upstream model or external table * The list of rows defined as a mapping from a column name to a value associated with it @@ -23,7 +22,7 @@ Tests within a suite file contain the following attributes: * The list of rows that are expected to be returned by the model's query defined as a mapping from a column name to a value associated with it * [Optional] The list of expected rows per each individual [Common Table Expression](glossary.md#cte) (CTE) defined in the model's query * [Optional] The dictionary of values for macro variables that will be set during model testing - * There are three special macros that can be overridden, `start`, `end`, and `execution_time`. Overriding each will allow you to override the date macros in your SQL queries. For example, setting execution_time: 2022-01-01 -> execution_ds in your queries. + * There are three special macros that can be overridden, `start`, `end`, and `execution_time`. Overriding each will allow you to override the date macros in your SQL queries. For example, setting `execution_time` to `2022-01-01` means `@execution_ds` will also be equal to that value. Additionally, SQL expressions like `CURRENT_DATE` and `CURRENT_TIMESTAMP` will result in the same datetime value as `execution_time`, when it is set. The YAML format is defined as follows: @@ -31,7 +30,6 @@ The YAML format is defined as follows: : model: description: # Optional - freeze_time: # Optional inputs: : rows: @@ -216,9 +214,9 @@ test_example_full_model: Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, it wouldn't suffice to simply specify an expected datetime value in the outputs. -The `freeze_time` attribute addresses this problem by setting the current time to the given datetime value, thus making the former deterministic. +The `execution_time` attribute addresses this problem by setting the current time to the given datetime value, thus making the former deterministic. -The following example demonstrates how `freeze_time` can be used to test a column that is computed using `CURRENT_TIMESTAMP`. The model we're going to test is defined as: +The following example demonstrates how `execution_time` can be used to test a column that is computed using `CURRENT_TIMESTAMP`. The model we're going to test is defined as: ```sql linenums="1" MODEL ( @@ -236,14 +234,15 @@ And the corresponding test is: ```yaml linenums="1" test_colors: model: colors - freeze_time: "2023-01-01 12:05:03" outputs: query: - color: "Yellow" created_at: "2023-01-01 12:05:03" + vars: + execution_time: "2023-01-01 12:05:03" ``` -It's also possible to set a time zone in the `freeze_time` datetime value, by including it in the timestamp string. +It's also possible to set a time zone in the `execution_time` datetime value, by including it in the timestamp string. If a time zone is provided, it is currently required that the expected datetime values are timestamps without time zone, meaning that they need to be offset accordingly. @@ -252,11 +251,12 @@ Here's how we would write the above test if we wanted to freeze the time to UTC+ ```yaml linenums="1" test_colors: model: colors - freeze_time: "2023-01-01 12:05:03+02:00" outputs: query: - color: "Yellow" created_at: "2023-01-01 10:05:03" + vars: + execution_time: "2023-01-01 12:05:03+02:00" ``` ## Automatic test generation diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index d4dba13482..fc63824ae7 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -76,15 +76,15 @@ def __init__( self._engine_adapter_dialect = Dialect.get_or_raise(self.engine_adapter.dialect) self._transforms = self._engine_adapter_dialect.generator_class.TRANSFORMS - self._freeze_time = str(self.body.get("freeze_time") or "") - if self._freeze_time: - freeze_time = exp.Literal.string(self._freeze_time) + self._execution_time = str(self.body.get("vars", {}).get("execution_time") or "") + if self._execution_time: + exec_time = exp.Literal.string(self._execution_time) self._transforms = { **self._transforms, - exp.CurrentDate: lambda self, _: self.sql(exp.cast(freeze_time, "date")), - exp.CurrentDatetime: lambda self, _: self.sql(exp.cast(freeze_time, "datetime")), - exp.CurrentTime: lambda self, _: self.sql(exp.cast(freeze_time, "time")), - exp.CurrentTimestamp: lambda self, _: self.sql(exp.cast(freeze_time, "timestamp")), + exp.CurrentDate: lambda self, _: self.sql(exp.cast(exec_time, "date")), + exp.CurrentDatetime: lambda self, _: self.sql(exp.cast(exec_time, "datetime")), + exp.CurrentTime: lambda self, _: self.sql(exp.cast(exec_time, "time")), + exp.CurrentTimestamp: lambda self, _: self.sql(exp.cast(exec_time, "timestamp")), } super().__init__() @@ -401,7 +401,7 @@ def runTest(self) -> None: def _execute_model(self) -> pd.DataFrame: """Executes the python model and returns a DataFrame.""" with ( - freeze_time(self._freeze_time) if self._freeze_time else nullcontext(), # type: ignore + freeze_time(self._execution_time) if self._execution_time else nullcontext(), # type: ignore patch.dict(self._engine_adapter_dialect.generator_class.TRANSFORMS, self._transforms), ): return t.cast( diff --git a/tests/core/test_test.py b/tests/core/test_test.py index 74952fa435..c21f1037af 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -857,12 +857,13 @@ def test_freeze_time(mocker: MockerFixture) -> None: """ test_foo: model: xyz - freeze_time: "2023-01-01 12:05:03+00:00" outputs: query: - cur_date: 2023-01-01 cur_time: 12:05:03 cur_timestamp: "2023-01-01 12:05:03" + vars: + execution_time: "2023-01-01 12:05:03+00:00" """ ), test_name="test_foo", @@ -901,11 +902,12 @@ def execute(context, start, end, execution_time, **kwargs): """ test_py_model: model: py_model - freeze_time: "2023-01-01 12:05:03+02:00" outputs: query: - ts1: "2023-01-01 10:05:03" ts2: "2023-01-01 10:05:03" + vars: + execution_time: "2023-01-01 12:05:03+02:00" """ ), test_name="test_py_model", From 9d76349d9e64c2b2aad0bc2ec7e4865beb83ebb1 Mon Sep 17 00:00:00 2001 From: George Sittas Date: Thu, 28 Mar 2024 20:58:51 +0200 Subject: [PATCH 03/10] Wordsmith --- docs/concepts/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index 6ac6497f0a..ef4661c6a9 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -22,7 +22,7 @@ Tests within a suite file contain the following attributes: * The list of rows that are expected to be returned by the model's query defined as a mapping from a column name to a value associated with it * [Optional] The list of expected rows per each individual [Common Table Expression](glossary.md#cte) (CTE) defined in the model's query * [Optional] The dictionary of values for macro variables that will be set during model testing - * There are three special macros that can be overridden, `start`, `end`, and `execution_time`. Overriding each will allow you to override the date macros in your SQL queries. For example, setting `execution_time` to `2022-01-01` means `@execution_ds` will also be equal to that value. Additionally, SQL expressions like `CURRENT_DATE` and `CURRENT_TIMESTAMP` will result in the same datetime value as `execution_time`, when it is set. + * There are three special macro variables: `start`, `end`, and `execution_time`. Setting these will allow you to override the date macros in your SQL queries. For example, `@execution_ds` will render to `2022-01-01`, if `execution_time` is set to this value. Additionally, SQL expressions like `CURRENT_DATE` and `CURRENT_TIMESTAMP` will result in the same datetime value as `execution_time`, when it is set. The YAML format is defined as follows: From aec63af073fe680cb26f73cc06b0e5942a92d22a Mon Sep 17 00:00:00 2001 From: George Sittas Date: Fri, 29 Mar 2024 03:06:36 +0200 Subject: [PATCH 04/10] Typing fixups --- sqlmesh/core/test/definition.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/sqlmesh/core/test/definition.py b/sqlmesh/core/test/definition.py index fc63824ae7..3f61c14ad8 100644 --- a/sqlmesh/core/test/definition.py +++ b/sqlmesh/core/test/definition.py @@ -3,7 +3,7 @@ import typing as t import unittest from collections import Counter -from contextlib import nullcontext +from contextlib import AbstractContextManager, nullcontext from pathlib import Path from unittest.mock import patch @@ -400,14 +400,13 @@ def runTest(self) -> None: def _execute_model(self) -> pd.DataFrame: """Executes the python model and returns a DataFrame.""" - with ( - freeze_time(self._execution_time) if self._execution_time else nullcontext(), # type: ignore - patch.dict(self._engine_adapter_dialect.generator_class.TRANSFORMS, self._transforms), - ): - return t.cast( - pd.DataFrame, - next(self.model.render(context=self.context, **self.body.get("vars", {}))), - ) + time_ctx = freeze_time(self._execution_time) if self._execution_time else nullcontext() + with patch.dict(self._engine_adapter_dialect.generator_class.TRANSFORMS, self._transforms): + with t.cast(AbstractContextManager, time_ctx): + return t.cast( + pd.DataFrame, + next(self.model.render(context=self.context, **self.body.get("vars", {}))), + ) def generate_test( From 9cc806df16eb12285be461f8284564d21038d992 Mon Sep 17 00:00:00 2001 From: George Sittas Date: Fri, 29 Mar 2024 03:09:08 +0200 Subject: [PATCH 05/10] Doc fixup --- docs/concepts/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index ef4661c6a9..e9cbff4297 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -22,7 +22,7 @@ Tests within a suite file contain the following attributes: * The list of rows that are expected to be returned by the model's query defined as a mapping from a column name to a value associated with it * [Optional] The list of expected rows per each individual [Common Table Expression](glossary.md#cte) (CTE) defined in the model's query * [Optional] The dictionary of values for macro variables that will be set during model testing - * There are three special macro variables: `start`, `end`, and `execution_time`. Setting these will allow you to override the date macros in your SQL queries. For example, `@execution_ds` will render to `2022-01-01`, if `execution_time` is set to this value. Additionally, SQL expressions like `CURRENT_DATE` and `CURRENT_TIMESTAMP` will result in the same datetime value as `execution_time`, when it is set. + * There are three special macro variables: `start`, `end`, and `execution_time`. Setting these will allow you to override the date macros in your SQL queries. For example, `@execution_ds` will render to `2022-01-01` if `execution_time` is set to this value. Additionally, SQL expressions like `CURRENT_DATE` and `CURRENT_TIMESTAMP` will result in the same datetime value as `execution_time`, when it is set. The YAML format is defined as follows: From 127219feded8818f0419169fb055670555171fc6 Mon Sep 17 00:00:00 2001 From: George Sittas Date: Fri, 29 Mar 2024 03:28:15 +0200 Subject: [PATCH 06/10] Doc fixups --- docs/concepts/tests.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index e9cbff4297..75c35bef08 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -212,9 +212,9 @@ test_example_full_model: ### Freezing Time -Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, it wouldn't suffice to simply specify an expected datetime value in the outputs. +Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, specifying an expected datetime output value is not enough to test them. -The `execution_time` attribute addresses this problem by setting the current time to the given datetime value, thus making the former deterministic. +The `execution_time` attribute of a test can address this problem, because it mocks out the current time in the context of the test, thus making its value deterministic. The following example demonstrates how `execution_time` can be used to test a column that is computed using `CURRENT_TIMESTAMP`. The model we're going to test is defined as: @@ -242,9 +242,9 @@ test_colors: execution_time: "2023-01-01 12:05:03" ``` -It's also possible to set a time zone in the `execution_time` datetime value, by including it in the timestamp string. +It's also possible to set a time zone for `execution_time`, by including it in the timestamp string. -If a time zone is provided, it is currently required that the expected datetime values are timestamps without time zone, meaning that they need to be offset accordingly. +If a time zone is provided, it is currently required that the test's _expected_ datetime values are timestamps without time zone, meaning that they need to be offset accordingly. Here's how we would write the above test if we wanted to freeze the time to UTC+2: From 653dc4356460b235c01a61c860fccbf7578d84a5 Mon Sep 17 00:00:00 2001 From: George Sittas Date: Fri, 29 Mar 2024 03:29:29 +0200 Subject: [PATCH 07/10] fixup --- docs/concepts/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index 75c35bef08..4582c5bfb9 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -212,7 +212,7 @@ test_example_full_model: ### Freezing Time -Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, specifying an expected datetime output value is not enough to test them. +Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, simply specifying an expected datetime output value is not enough to test them. The `execution_time` attribute of a test can address this problem, because it mocks out the current time in the context of the test, thus making its value deterministic. From a73db8226753a295d7ee358f267756da1d64d8af Mon Sep 17 00:00:00 2001 From: George Sittas Date: Fri, 29 Mar 2024 03:32:34 +0200 Subject: [PATCH 08/10] Wordsmith --- docs/concepts/tests.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index 4582c5bfb9..d167325470 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -212,9 +212,9 @@ test_example_full_model: ### Freezing Time -Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, simply specifying an expected datetime output value is not enough to test them. +Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, it's not enough to simply specify an expected output value in order to test them. -The `execution_time` attribute of a test can address this problem, because it mocks out the current time in the context of the test, thus making its value deterministic. +The `execution_time` attribute of a test addresses this problem by mocking out the current time in the context of the test, thus making its value deterministic. The following example demonstrates how `execution_time` can be used to test a column that is computed using `CURRENT_TIMESTAMP`. The model we're going to test is defined as: From 190afdd0ea912b317cb47fe6e8c9b33bdab648d1 Mon Sep 17 00:00:00 2001 From: George Sittas Date: Fri, 29 Mar 2024 03:34:15 +0200 Subject: [PATCH 09/10] Wordsmith --- docs/concepts/tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/tests.md b/docs/concepts/tests.md index d167325470..eb90cf18d9 100644 --- a/docs/concepts/tests.md +++ b/docs/concepts/tests.md @@ -214,7 +214,7 @@ test_example_full_model: Some models may use SQL expressions that compute datetime values at a given point in time, such as `CURRENT_TIMESTAMP`. Since these expressions are non-deterministic, it's not enough to simply specify an expected output value in order to test them. -The `execution_time` attribute of a test addresses this problem by mocking out the current time in the context of the test, thus making its value deterministic. +Setting the `execution_time` macro variable addresses this problem by mocking out the current time in the context of the test, thus making its value deterministic. The following example demonstrates how `execution_time` can be used to test a column that is computed using `CURRENT_TIMESTAMP`. The model we're going to test is defined as: From 7d0c31cb70163480fad9dafea693ad4f656d03bc Mon Sep 17 00:00:00 2001 From: George Sittas Date: Fri, 29 Mar 2024 13:24:52 +0200 Subject: [PATCH 10/10] Write test more compactly --- tests/core/test_test.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/core/test_test.py b/tests/core/test_test.py index c21f1037af..ccad360a78 100644 --- a/tests/core/test_test.py +++ b/tests/core/test_test.py @@ -883,8 +883,6 @@ def test_freeze_time(mocker: MockerFixture) -> None: '''CAST('2023-01-01 12:05:03+00:00' AS TIMESTAMP) AS "cur_timestamp"''', ) - context = Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))) - @model("py_model", columns={"ts1": "timestamptz", "ts2": "timestamptz"}) def execute(context, start, end, execution_time, **kwargs): datetime_now = datetime.datetime.now() @@ -894,9 +892,6 @@ def execute(context, start, end, execution_time, **kwargs): return pd.DataFrame([{"ts1": datetime_now, "ts2": current_timestamp}]) - py_model = model.get_registry()["py_model"].model(module_path=Path("."), path=Path(".")) - context.upsert_model(py_model) - test = _create_test( body=load_yaml( """ @@ -911,8 +906,8 @@ def execute(context, start, end, execution_time, **kwargs): """ ), test_name="test_py_model", - model=py_model, - context=context, + model=model.get_registry()["py_model"].model(module_path=Path("."), path=Path(".")), + context=Context(config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb"))), ) _check_successful_or_raise(test.run())