diff --git a/docs/changes/newsfragments/+test-sqlite-sync.underthehood b/docs/changes/newsfragments/+test-sqlite-sync.underthehood new file mode 100644 index 00000000000..ede794d5ef5 --- /dev/null +++ b/docs/changes/newsfragments/+test-sqlite-sync.underthehood @@ -0,0 +1,2 @@ +The test suite runs faster, especially on Windows: test databases are now opened with SQLite's +``PRAGMA synchronous=OFF`` so that commits are not flushed to disk. diff --git a/docs/changes/newsfragments/8749.underthehood b/docs/changes/newsfragments/8749.underthehood new file mode 100644 index 00000000000..0e0764de28b --- /dev/null +++ b/docs/changes/newsfragments/8749.underthehood @@ -0,0 +1,2 @@ +All temporary files created by the test suite are now placed under pytest's temporary directory +root, so setting ``PYTEST_DEBUG_TEMPROOT`` (or ``--basetemp``) relocates all of them. diff --git a/tests/conftest.py b/tests/conftest.py index 766d94d1fba..1e5a8623743 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import sys import threading import weakref -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import matplotlib import pytest @@ -16,6 +16,7 @@ from qcodes.dataset import get_open_connections, initialise_database, new_data_set from qcodes.dataset.descriptions.dependencies import InterDependencies_ from qcodes.dataset.experiment_container import Experiment, new_experiment +from qcodes.dataset.sqlite.connection import AtomicConnection from qcodes.instrument import Instrument from qcodes.monitor.monitor import Monitor from qcodes.parameters import ParamSpecBase @@ -54,6 +55,31 @@ def matplotlib_set_backend() -> None: matplotlib.use("agg") +@pytest.fixture(scope="session", autouse=True) +def disable_sqlite_disk_sync() -> Generator[None, None, None]: + """ + Open every QCoDeS database connection with ``PRAGMA synchronous=OFF``. + + By default SQLite flushes the database to disk on every commit, which + protects against data loss on OS crashes or power failures. Test databases + are thrown away after the test, so these flushes only cost time. This is + especially slow on Windows, and less so where the temporary directory + is a RAM disk (e.g. ``/tmp`` on many Linux systems). Turning this off + does not change what other connections or processes can read. + """ + original_init = AtomicConnection.__init__ + + def init_without_disk_sync( + self: AtomicConnection, *args: Any, **kwargs: Any + ) -> None: + original_init(self, *args, **kwargs) + self.execute("PRAGMA synchronous=OFF") + + with pytest.MonkeyPatch.context() as mp: + mp.setattr(AtomicConnection, "__init__", init_without_disk_sync) + yield + + @pytest.fixture(scope="session", autouse=True) def default_session_config( tmp_path_factory: pytest.TempPathFactory, diff --git a/tests/dataset/conftest.py b/tests/dataset/conftest.py index f6a65fc2693..2551d0fa620 100644 --- a/tests/dataset/conftest.py +++ b/tests/dataset/conftest.py @@ -2,7 +2,6 @@ import os import shutil -import tempfile from contextlib import contextmanager from enum import StrEnum from typing import TYPE_CHECKING, Any @@ -94,29 +93,30 @@ def two_empty_temp_db_connections( @contextmanager def temporarily_copied_DB( - filepath: str, **kwargs: Any + filepath: str, tmp_path: Path, **kwargs: Any ) -> Generator[AtomicConnection, None, None]: """ - Make a temporary copy of a db-file and delete it after use. Meant to be - used together with the old version database fixtures, lest we change the - fixtures on disk. Yields the connection object + Copy a db-file into ``tmp_path`` and yield a connection to the copy, + closing it after use. Meant to be used together with the old version + database fixtures, lest we change the fixtures on disk. The copy keeps the + file name of the original, so each file can only be copied once per test. Args: filepath: path to the db-file + tmp_path: the test's ``tmp_path`` fixture, the directory to copy into **kwargs: to be passed to connect """ - with tempfile.TemporaryDirectory() as tmpdir: - dbname_new = os.path.join(tmpdir, "temp.db") - shutil.copy2(filepath, dbname_new) + dbname_new = tmp_path / os.path.basename(filepath) + shutil.copy2(filepath, dbname_new) - conn = connect(dbname_new, **kwargs) + conn = connect(dbname_new, **kwargs) - try: - yield conn + try: + yield conn - finally: - conn.close() + finally: + conn.close() @pytest.fixture(name="scalar_dataset") diff --git a/tests/dataset/test_database_creation_and_upgrading.py b/tests/dataset/test_database_creation_and_upgrading.py index e9b6c18ec96..0c85107e5a4 100644 --- a/tests/dataset/test_database_creation_and_upgrading.py +++ b/tests/dataset/test_database_creation_and_upgrading.py @@ -277,7 +277,7 @@ def test_initialised_database_at_with_journal_mode_none(tmp_path: Path) -> None: assert os.path.exists(db_location) -def test_perform_actual_upgrade_0_to_1() -> None: +def test_perform_actual_upgrade_0_to_1(tmp_path: Path) -> None: # we cannot use the empty_temp_db, since that has already called connect # and is therefore latest version already @@ -287,7 +287,7 @@ def test_perform_actual_upgrade_0_to_1() -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=0) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=0) as conn: assert get_user_version(conn) == 0 guid_table_query = "SELECT guid FROM runs" @@ -304,14 +304,14 @@ def test_perform_actual_upgrade_0_to_1() -> None: assert len(c.fetchall()) == 0 -def test_perform_actual_upgrade_1_to_2() -> None: +def test_perform_actual_upgrade_1_to_2(tmp_path: Path) -> None: v1fixpath = os.path.join(fixturepath, "db_files", "version1") dbname_old = os.path.join(v1fixpath, "empty.db") skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=1) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=1) as conn: assert get_user_version(conn) == 1 guid_table_query = "SELECT guid FROM runs" @@ -330,14 +330,14 @@ def test_perform_actual_upgrade_1_to_2() -> None: assert len(c.fetchall()) == 2 -def test_perform_actual_upgrade_2_to_3_empty() -> None: +def test_perform_actual_upgrade_2_to_3_empty(tmp_path: Path) -> None: v2fixpath = os.path.join(fixturepath, "db_files", "version2") dbname_old = os.path.join(v2fixpath, "empty.db") skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=2) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=2) as conn: assert get_user_version(conn) == 2 desc_query = "SELECT run_description FROM runs" @@ -355,25 +355,25 @@ def test_perform_actual_upgrade_2_to_3_empty() -> None: assert len(c.fetchall()) == 0 -def test_perform_actual_upgrade_2_to_3_empty_runs() -> None: +def test_perform_actual_upgrade_2_to_3_empty_runs(tmp_path: Path) -> None: v2fixpath = os.path.join(fixturepath, "db_files", "version2") dbname_old = os.path.join(v2fixpath, "empty_runs.db") skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=2) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=2) as conn: perform_db_upgrade_2_to_3(conn) -def test_perform_actual_upgrade_2_to_3_some_runs() -> None: +def test_perform_actual_upgrade_2_to_3_some_runs(tmp_path: Path) -> None: v2fixpath = os.path.join(fixturepath, "db_files", "version2") dbname_old = os.path.join(v2fixpath, "some_runs.db") skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=2) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=2) as conn: assert get_user_version(conn) == 2 perform_db_upgrade_2_to_3(conn) @@ -450,7 +450,7 @@ def test_perform_actual_upgrade_2_to_3_some_runs() -> None: assert p5.unit == "unit 5" -def test_perform_upgrade_v2_v3_to_v4_fixes() -> None: +def test_perform_upgrade_v2_v3_to_v4_fixes(tmp_path: Path) -> None: """ Test that a db that was upgraded from v2 to v3 with a buggy version will be corrected when upgraded to v4. @@ -462,7 +462,7 @@ def test_perform_upgrade_v2_v3_to_v4_fixes() -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=3) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=3) as conn: assert get_user_version(conn) == 3 sql = """ @@ -591,7 +591,7 @@ def test_perform_upgrade_v2_v3_to_v4_fixes() -> None: assert p5.unit == "unit 5" -def test_perform_upgrade_v3_to_v4() -> None: +def test_perform_upgrade_v3_to_v4(tmp_path: Path) -> None: """ Test that a db upgrade from v2 to v4 works correctly. """ @@ -602,7 +602,7 @@ def test_perform_upgrade_v3_to_v4() -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=3) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=3) as conn: assert get_user_version(conn) == 3 sql = """ @@ -763,7 +763,7 @@ def _assert_loc_station( @pytest.mark.parametrize( "db_file", ["empty", "with_runs_but_no_snapshots", "with_runs_and_snapshots"] ) -def test_perform_actual_upgrade_4_to_5(db_file: str) -> None: +def test_perform_actual_upgrade_4_to_5(db_file: str, tmp_path: Path) -> None: v4fixpath = os.path.join(fixturepath, "db_files", "version4") db_file += ".db" @@ -771,7 +771,7 @@ def test_perform_actual_upgrade_4_to_5(db_file: str) -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=4) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=4) as conn: # firstly, assert the situation with 'snapshot' column of 'runs' table if "with_runs_and_snapshots" in db_file: assert is_column_in_table(conn, "runs", "snapshot") @@ -785,7 +785,7 @@ def test_perform_actual_upgrade_4_to_5(db_file: str) -> None: assert is_column_in_table(conn, "runs", "snapshot") -def test_perform_actual_upgrade_5_to_6() -> None: +def test_perform_actual_upgrade_5_to_6(tmp_path: Path) -> None: fixpath = os.path.join(fixturepath, "db_files", "version5") db_file = "empty.db" @@ -793,14 +793,14 @@ def test_perform_actual_upgrade_5_to_6() -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=5) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=5) as conn: perform_db_upgrade_5_to_6(conn) assert get_user_version(conn) == 6 db_file = "some_runs.db" dbname_old = os.path.join(fixpath, db_file) - with temporarily_copied_DB(dbname_old, debug=False, version=5) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=5) as conn: perform_db_upgrade_5_to_6(conn) assert get_user_version(conn) == 6 @@ -818,7 +818,7 @@ def test_perform_actual_upgrade_5_to_6() -> None: assert desc._version == 3 -def test_perform_upgrade_6_7() -> None: +def test_perform_upgrade_6_7(tmp_path: Path) -> None: fixpath = os.path.join(fixturepath, "db_files", "version6") db_file = "empty.db" @@ -826,12 +826,12 @@ def test_perform_upgrade_6_7() -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=6) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=6) as conn: perform_db_upgrade_6_to_7(conn) assert get_user_version(conn) == 7 -def test_perform_actual_upgrade_6_to_7() -> None: +def test_perform_actual_upgrade_6_to_7(tmp_path: Path) -> None: fixpath = os.path.join(fixturepath, "db_files", "version6") db_file = "some_runs.db" @@ -839,7 +839,7 @@ def test_perform_actual_upgrade_6_to_7() -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=6) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=6) as conn: assert isinstance(conn, AtomicConnection) perform_db_upgrade_6_to_7(conn) assert get_user_version(conn) == 7 @@ -881,7 +881,7 @@ def test_perform_actual_upgrade_6_to_7() -> None: assert ds2.counter == ds2.captured_counter -def test_perform_actual_upgrade_6_to_newest_add_new_data() -> None: +def test_perform_actual_upgrade_6_to_newest_add_new_data(tmp_path: Path) -> None: """ Insert new runs on top of existing runs upgraded and verify that they get the correct captured_run_id and captured_counter @@ -894,7 +894,7 @@ def test_perform_actual_upgrade_6_to_newest_add_new_data() -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=6) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=6) as conn: assert isinstance(conn, AtomicConnection) perform_db_upgrade(conn) assert get_user_version(conn) >= 7 @@ -980,7 +980,7 @@ def test_perform_actual_upgrade_6_to_newest_add_new_data() -> None: @pytest.mark.parametrize("db_file", ["empty", "some_runs"]) -def test_perform_actual_upgrade_7_to_8(db_file: str) -> None: +def test_perform_actual_upgrade_7_to_8(db_file: str, tmp_path: Path) -> None: v7fixpath = os.path.join(fixturepath, "db_files", "version7") db_file += ".db" @@ -988,7 +988,7 @@ def test_perform_actual_upgrade_7_to_8(db_file: str) -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=7) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=7) as conn: perform_db_upgrade_7_to_8(conn) assert is_column_in_table(conn, "runs", "parent_datasets") @@ -1027,7 +1027,7 @@ def test_getting_db_version(version: int) -> None: @pytest.mark.parametrize("db_file", ["empty", "some_runs"]) -def test_perform_actual_upgrade_8_to_9(db_file: str) -> None: +def test_perform_actual_upgrade_8_to_9(db_file: str, tmp_path: Path) -> None: v8fixpath = os.path.join(fixturepath, "db_files", "version8") db_file += ".db" @@ -1035,7 +1035,7 @@ def test_perform_actual_upgrade_8_to_9(db_file: str) -> None: skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=8) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=8) as conn: index_query = "PRAGMA index_list(runs)" c = atomic_transaction(conn, index_query) diff --git a/tests/dataset/test_fix_functions.py b/tests/dataset/test_fix_functions.py index 860e843456c..79ec8e7f5c3 100644 --- a/tests/dataset/test_fix_functions.py +++ b/tests/dataset/test_fix_functions.py @@ -1,5 +1,6 @@ import json import os +from typing import TYPE_CHECKING import pytest @@ -18,18 +19,21 @@ from tests.common import skip_if_no_fixtures from tests.dataset.conftest import temporarily_copied_DB +if TYPE_CHECKING: + from pathlib import Path + fixturepath = os.sep.join(tests.dataset.__file__.split(os.sep)[:-1]) fixturepath = os.path.join(fixturepath, "fixtures") -def test_version_4a_bugfix() -> None: +def test_version_4a_bugfix(tmp_path: "Path") -> None: v4fixpath = os.path.join(fixturepath, "db_files", "version4a") dbname_old = os.path.join(v4fixpath, "some_runs.db") skip_if_no_fixtures(dbname_old) - with temporarily_copied_DB(dbname_old, debug=False, version=4) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=4) as conn: dd = fix_version_4a_run_description_bug(conn) assert dd["runs_inspected"] == 10 @@ -49,20 +53,20 @@ def test_version_4a_bugfix() -> None: assert dd["runs_fixed"] == 0 -def test_version_4a_bugfix_raises() -> None: +def test_version_4a_bugfix_raises(tmp_path: "Path") -> None: v3fixpath = os.path.join(fixturepath, "db_files", "version3") dbname_old = os.path.join(v3fixpath, "some_runs_without_run_description.db") skip_if_no_fixtures(dbname_old) with ( - temporarily_copied_DB(dbname_old, debug=False, version=3) as conn, + temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=3) as conn, pytest.raises(RuntimeError), ): fix_version_4a_run_description_bug(conn) -def test_fix_wrong_run_descriptions() -> None: +def test_fix_wrong_run_descriptions(tmp_path: "Path") -> None: v3fixpath = os.path.join(fixturepath, "db_files", "version3") dbname_old = os.path.join(v3fixpath, "some_runs_without_run_description.db") @@ -81,7 +85,7 @@ def make_ps(n: "int") -> "ParamSpec": paramspecs[4]._depends_on = ["p2", "p3"] paramspecs[5]._inferred_from = ["p0"] - with temporarily_copied_DB(dbname_old, debug=False, version=3) as conn: + with temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=3) as conn: assert get_user_version(conn) == 3 expected_description = RunDescriber( @@ -102,7 +106,7 @@ def make_ps(n: "int") -> "ParamSpec": assert desc == empty_description -def test_fix_wrong_run_descriptions_raises() -> None: +def test_fix_wrong_run_descriptions_raises(tmp_path: "Path") -> None: v4fixpath = os.path.join(fixturepath, "db_files", "version4a") dbname_old = os.path.join(v4fixpath, "some_runs.db") @@ -110,7 +114,7 @@ def test_fix_wrong_run_descriptions_raises() -> None: skip_if_no_fixtures(dbname_old) with ( - temporarily_copied_DB(dbname_old, debug=False, version=4) as conn, + temporarily_copied_DB(dbname_old, tmp_path, debug=False, version=4) as conn, pytest.raises(RuntimeError), ): fix_wrong_run_descriptions(conn, [1]) diff --git a/tests/dataset/test_sqlite_connection.py b/tests/dataset/test_sqlite_connection.py index e55d29f645d..c9055c32af7 100644 --- a/tests/dataset/test_sqlite_connection.py +++ b/tests/dataset/test_sqlite_connection.py @@ -374,6 +374,13 @@ def test_connect() -> None: assert None is conn.row_factory +def test_test_suite_disables_sqlite_disk_sync(tmp_path: "Path") -> None: + # The disable_sqlite_disk_sync fixture in tests/conftest.py speeds up the + # test suite by skipping disk flushes; check that it applies to files. + with closing(connect(tmp_path / "temp.db")) as conn: + assert conn.execute("PRAGMA synchronous").fetchone()[0] == 0 + + def test_open_connections_are_tracked_until_closed(tmp_path: "Path") -> None: conn = connect(tmp_path / "temp.db") assert conn in get_open_connections() diff --git a/tests/test_station.py b/tests/test_station.py index 0c633b325f0..938dba72c88 100644 --- a/tests/test_station.py +++ b/tests/test_station.py @@ -1,9 +1,7 @@ import json import os import re -import tempfile import warnings -from contextlib import contextmanager from io import StringIO from pathlib import Path from typing import TYPE_CHECKING @@ -255,31 +253,16 @@ def test_update_config_schema() -> None: assert len(schema["definitions"]["instruments"]["enum"]) > 1 -@contextmanager -def config_file_context(file_content: str) -> "Generator[str, None, None]": - with tempfile.TemporaryDirectory() as tmpdirname: - filename = Path(tmpdirname, "station_config.yaml") - with filename.open("w") as f: - f.write(file_content) - yield str(filename) - - -@contextmanager -def config_files_context( - file_content1: str, file_content2: str -) -> "Generator[list[str], None, None]": - with tempfile.TemporaryDirectory() as tmpdirname: - filename1 = Path(tmpdirname, "station_config1.yaml") - with filename1.open("w") as f: - f.write(file_content1) - filename2 = Path(tmpdirname, "station_config2.yaml") - with filename2.open("w") as f: - f.write(file_content2) - yield [str(filename1), str(filename2)] +def write_config_file( + tmp_path: Path, file_content: str, file_name: str = "station_config.yaml" +) -> str: + filename = tmp_path / file_name + filename.write_text(file_content) + return str(filename) @pytest.fixture(name="example_station_config") -def _make_example_station_config() -> "Generator[str, None, None]": +def _make_example_station_config(tmp_path: Path) -> str: """ Returns path to temp yaml file with station config. """ @@ -303,8 +286,7 @@ def _make_example_station_config() -> "Generator[str, None, None]": mock_dac2: type: qcodes.instrument_drivers.mock_instruments.DummyInstrument """ - with config_file_context(test_config) as filename: - yield filename + return write_config_file(tmp_path, test_config) def test_dynamic_reload_of_file(example_station_config: str) -> None: @@ -1055,7 +1037,7 @@ def test_config_validation_failure() -> None: ) -def test_config_validation_failure_on_file() -> None: +def test_config_validation_failure_on_file(tmp_path: Path) -> None: test_config = """ instruments: mock: @@ -1063,11 +1045,8 @@ def test_config_validation_failure_on_file() -> None: invalid_keyword: more_errors: 42 """ - with ( - pytest.raises(ValidationWarning), - config_file_context(test_config) as filename, - ): - Station(config_file=filename) + with pytest.raises(ValidationWarning): + Station(config_file=write_config_file(tmp_path, test_config)) def test_config_validation_comprehensive_config() -> None: @@ -1154,7 +1133,7 @@ def test_load_all_instruments_without_config_raises() -> None: station.load_all_instruments() -def test_station_config_created_with_multiple_config_files() -> None: +def test_station_config_created_with_multiple_config_files(tmp_path: Path) -> None: test_config1 = """ instruments: mock_dac1: @@ -1171,8 +1150,11 @@ def test_station_config_created_with_multiple_config_files() -> None: mock_dac2: type: qcodes.instrument_drivers.mock_instruments.DummyInstrument """ - with config_files_context(test_config1, test_config2) as file_list: - assert station_config_has_been_loaded(Station(config_file=file_list)) + file_list = [ + write_config_file(tmp_path, test_config1, "station_config1.yaml"), + write_config_file(tmp_path, test_config2, "station_config2.yaml"), + ] + assert station_config_has_been_loaded(Station(config_file=file_list)) def test_get_component_by_name() -> None: