diff --git a/.github/workflows/ci-metricflow-unit-tests.yaml b/.github/workflows/ci-metricflow-unit-tests.yaml index 3b40e29714..f25d6aff41 100644 --- a/.github/workflows/ci-metricflow-unit-tests.yaml +++ b/.github/workflows/ci-metricflow-unit-tests.yaml @@ -92,4 +92,5 @@ jobs: } - name: Run Package-Build Tests + shell: bash run: "make test-build-packages" diff --git a/dbt-metricflow/requirements-files/requirements-metricflow.txt b/dbt-metricflow/requirements-files/requirements-metricflow.txt index 824cb9bddd..3f71d29a7d 100644 --- a/dbt-metricflow/requirements-files/requirements-metricflow.txt +++ b/dbt-metricflow/requirements-files/requirements-metricflow.txt @@ -1 +1,4 @@ -metricflow==0.209.0 +# Using the root so tests in `dbt-metricflow` run using the current code in `metricflow`. +# This ensures that breaking changes are resolved in the PR that makes the breaking change. +# This should be updated to the correct version before release. +metricflow @ {root:parent:uri} diff --git a/scripts/ci_tests/dbt_metricflow_package_test.py b/scripts/ci_tests/dbt_metricflow_package_test.py new file mode 100644 index 0000000000..932b9f748e --- /dev/null +++ b/scripts/ci_tests/dbt_metricflow_package_test.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import subprocess +import textwrap +from pathlib import Path +from typing import Optional + + +def _run_shell_command(command: str, cwd: Optional[Path] = None) -> None: + if cwd is None: + cwd = Path.cwd() + + print( + textwrap.dedent( + f"""\ + Running via shell: + command: {command!r} + cwd: {cwd.as_posix()!r} + """ + ).rstrip() + ) + subprocess.check_call(command, shell=True, cwd=cwd.as_posix()) + + +if __name__ == "__main__": + # Check that the `mf` command is installed. + _run_shell_command("which python") + _run_shell_command("which mf") + # Run the tutorial using `--yes` to create the sample project without user interaction. + _run_shell_command("mf tutorial --yes") + tutorial_directory = Path.cwd().joinpath("mf_tutorial_project") + + # Run the first few tutorial steps. + _run_shell_command("dbt seed", cwd=tutorial_directory) + _run_shell_command("dbt build", cwd=tutorial_directory) + _run_shell_command( + "mf query --metrics transactions --group-by metric_time --order metric_time", + cwd=tutorial_directory, + ) diff --git a/scripts/ci_tests/run_package_build_tests.py b/scripts/ci_tests/run_package_build_tests.py index 349b509107..8f8a30d4b9 100644 --- a/scripts/ci_tests/run_package_build_tests.py +++ b/scripts/ci_tests/run_package_build_tests.py @@ -8,6 +8,7 @@ import logging import tempfile import venv +from collections.abc import Sequence from pathlib import Path from scripts.mf_script_helper import MetricFlowScriptHelper @@ -15,50 +16,86 @@ logger = logging.getLogger(__name__) -def _run_package_build_test(package_directory: Path, package_test_script: Path) -> None: - """Run a test to verify that a package is built properly. +def _run_package_test( + package_directory: Path, + package_test_script: Path, + build_wheel: bool, + optional_package_dependencies_to_install: Sequence[str] = (), +) -> None: + """Run tests to verify a package. - Given the directory where the package is located, this will build the package using `hatch build` and install the - created Python-wheel files into a clean virtual environment. Finally, the given test script will be run using the - virtual environment. + Given the directory where the package is located, install the package by using a wheel built from the package or an + use editable installation. Once installed, run the package test script. Args: package_directory: Root directory where the package is located. package_test_script: The path to the script that should be run. - + optional_package_dependencies_to_install: If the given package defines optional dependencies that can be + installed, install these. e.g. for `dbt-metricflow[dbt-duckdb]`, specify `dbt-duckdb`. + build_wheel: If set, build a wheel from the package and install the wheel in the virtual environment. Otherwise, + use an editable installation. Returns: None Raises: Exception on test failure. """ - logger.info(f"Running package build test for {str(package_directory)!r} using {str(package_test_script)!r}") + package_directory_str = package_directory.as_posix() + package_test_script_str = package_test_script.as_posix() + logger.info(f"Running package build test for {package_directory_str!r} using {package_test_script_str!r}") + try: with tempfile.TemporaryDirectory() as temporary_directory_str: temporary_directory = Path(temporary_directory_str) venv_directory = temporary_directory.joinpath("venv") - logger.info(f"Creating venv at {str(venv_directory)!r}") + logger.info(f"Creating a new venv at {venv_directory.as_posix()!r}") venv.create(venv_directory, with_pip=True) - pip_executable = Path(venv_directory, "bin/pip") - python_executable = Path(venv_directory, "bin/python") - - logger.info(f"Building package at {str(package_directory)!r}") - logger.info(f"Running package build test for {str(package_directory)!r} using {str(package_test_script)!r}") - MetricFlowScriptHelper.run_command(["hatch", "build"], working_directory=package_directory) - - logger.info("Installing package using generated wheels") - MetricFlowScriptHelper.run_shell_command(f'{pip_executable} install "{str(package_directory)}"/dist/*.whl') - - logger.info("Running test using installed package in venv") - MetricFlowScriptHelper.run_command( - [str(python_executable), str(package_test_script)], working_directory=temporary_directory + pip_executable = Path(venv_directory, "bin/pip").as_posix() + + logger.info(f"Using package at {package_directory_str!r}") + + if build_wheel: + MetricFlowScriptHelper.run_command(["hatch", "clean"], working_directory=package_directory) + MetricFlowScriptHelper.run_command(["hatch", "build"], working_directory=package_directory) + + logger.info("Installing package in venv using generated wheels") + paths_to_wheels = _get_wheels_in_directory(package_directory.joinpath("dist")) + if len(paths_to_wheels) != 1: + raise RuntimeError(f"Expected exactly one wheel but got {paths_to_wheels}") + + path_to_wheel_str = str(paths_to_wheels[0]) + MetricFlowScriptHelper.run_command([pip_executable, "install", path_to_wheel_str]) + for optional_package_dependency in optional_package_dependencies_to_install: + MetricFlowScriptHelper.run_command( + [pip_executable, "install", f"{path_to_wheel_str}[{optional_package_dependency}]"] + ) + else: + MetricFlowScriptHelper.run_command([pip_executable, "install", "-e", package_directory_str]) + for optional_package_dependency in optional_package_dependencies_to_install: + MetricFlowScriptHelper.run_command( + [pip_executable, "install", f"{package_directory_str}[{optional_package_dependency}]"] + ) + + logger.info("Running test using venv") + venv_activate = venv_directory.joinpath("bin", "activate").as_posix() + MetricFlowScriptHelper.run_shell_command( + # Using period instead of `source` for compatibility with `sh`. + f"cd {temporary_directory_str} && . {venv_activate} && python {package_test_script_str}", ) - logger.info(f"Test passed {str(package_test_script)!r}") + logger.info(f"Test passed {package_test_script_str!r}") except Exception as e: raise PackageBuildTestFailureException( - f"Package build test failed for {str(package_directory)!r} using {str(package_test_script)!r}" + f"Package test failed for {package_directory_str!r} using {package_test_script_str!r}" ) from e +def _get_wheels_in_directory(directory: Path) -> Sequence[Path]: + paths_to_wheels = [] + for path_item in directory.iterdir(): + if path_item.is_file() and path_item.suffix == ".whl": + paths_to_wheels.append(path_item) + return paths_to_wheels + + class PackageBuildTestFailureException(Exception): # noqa: D101 pass @@ -74,16 +111,24 @@ class PackageBuildTestFailureException(Exception): # noqa: D101 logger.info(f"Using {metricflow_repo_directory=}") - # Test building the `metricflow` package. - _run_package_build_test( + # Test the `metricflow` package. + _run_package_test( package_directory=metricflow_repo_directory, package_test_script=metricflow_repo_directory.joinpath("scripts/ci_tests/metricflow_package_test.py"), + build_wheel=True, ) - # Test building `metricflow-semantics` package. - _run_package_build_test( + # Test the `metricflow-semantics` package. + _run_package_test( package_directory=metricflow_repo_directory.joinpath("metricflow-semantics"), package_test_script=metricflow_repo_directory.joinpath("scripts/ci_tests/metricflow_semantics_package_test.py"), + build_wheel=True, ) - # Add entry for `dbt-metricflow` once build issues are resolved. + # Test the `dbt-metricflow` package. + _run_package_test( + package_directory=metricflow_repo_directory.joinpath("dbt-metricflow"), + package_test_script=metricflow_repo_directory.joinpath("scripts/ci_tests/dbt_metricflow_package_test.py"), + optional_package_dependencies_to_install=("dbt-duckdb",), + build_wheel=False, + )