From d2a8025d572736402c3086dc43b2f0e14710ee1d Mon Sep 17 00:00:00 2001 From: Iain <25081046+Iain-S@users.noreply.github.com> Date: Wed, 11 Jan 2023 12:17:46 +0000 Subject: [PATCH 1/5] Add tests for the providers module. --- tests/test_providers.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/test_providers.py diff --git a/tests/test_providers.py b/tests/test_providers.py new file mode 100644 index 00000000..a345f699 --- /dev/null +++ b/tests/test_providers.py @@ -0,0 +1,20 @@ +"""Tests for the main module.""" +from unittest import TestCase + +from sqlsynthgen import make +from tests.examples import example_tables + + +class ForeignKeyTestCase(TestCase): + """Tests for the ForeignKeyProvider class.""" + + def test_make_generators_from_tables(self) -> None: + """Check that we can make a generators file from a tables module.""" + + with open( + "tests/examples/expected_output.py", encoding="utf-8" + ) as expected_output: + expected = expected_output.read() + + actual = make.make_generators_from_tables(example_tables) + self.assertEqual(expected, actual) From 8844aa19f9d5cd313e49c7ba00d91d743847c8ef Mon Sep 17 00:00:00 2001 From: Iain <25081046+Iain-S@users.noreply.github.com> Date: Wed, 11 Jan 2023 14:13:05 +0000 Subject: [PATCH 2/5] Add test for ForeignKeyProvider and BinaryProvider --- sqlsynthgen/providers.py | 4 -- tests/examples/providers.dump | 52 ++++++++++++++++++++++ tests/test_functional.py | 2 +- tests/test_providers.py | 84 +++++++++++++++++++++++++++++------ 4 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 tests/examples/providers.dump diff --git a/sqlsynthgen/providers.py b/sqlsynthgen/providers.py index b8e5e31a..8cb40107 100644 --- a/sqlsynthgen/providers.py +++ b/sqlsynthgen/providers.py @@ -3,12 +3,8 @@ from mimesis import Text from mimesis.providers.base import BaseDataProvider, BaseProvider - -# from mimesis.locales import Locale from sqlalchemy.sql import text -# generic = Generic(locale=Locale.EN) - class ForeignKeyProvider(BaseProvider): """A Mimesis provider of foreign keys.""" diff --git a/tests/examples/providers.dump b/tests/examples/providers.dump new file mode 100644 index 00000000..525cc832 --- /dev/null +++ b/tests/examples/providers.dump @@ -0,0 +1,52 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 14.2 (Debian 14.2-1.pgdg110+1) +-- Dumped by pg_dump version 14.6 (Homebrew) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +DROP DATABASE IF EXISTS providers; +-- +-- Name: providers; Type: DATABASE; Schema: -; Owner: postgres +-- + +CREATE DATABASE providers WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE = 'en_US.utf8'; + + +ALTER DATABASE providers OWNER TO postgres; + +\connect providers + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: patient; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.patient ( + sex text NOT NULL +); diff --git a/tests/test_functional.py b/tests/test_functional.py index e3dfeaba..a8f84630 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1,4 +1,4 @@ -"""Tests for the main module.""" +"""Tests for the CLI.""" import os from pathlib import Path from subprocess import run diff --git a/tests/test_providers.py b/tests/test_providers.py index a345f699..e7e2c96b 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -1,20 +1,78 @@ -"""Tests for the main module.""" -from unittest import TestCase +"""Tests for the providers module.""" +import os +from pathlib import Path +from subprocess import run +from unittest import TestCase, skipUnless -from sqlsynthgen import make -from tests.examples import example_tables +from sqlalchemy import Column, Integer, Text, create_engine, insert +from sqlalchemy.ext.declarative import declarative_base +from sqlsynthgen.providers import BinaryProvider, ForeignKeyProvider -class ForeignKeyTestCase(TestCase): + +class BinaryProviderTestCase(TestCase): + """Tests for the BinaryProvider class.""" + + def test_bytes(self) -> None: + BinaryProvider().bytes().decode("utf-8") + + +@skipUnless( + os.environ.get("FUNCTIONAL_TESTS") == "1", "Set 'FUNCTIONAL_TESTS=1' to enable." +) +class ForeignKeyProviderTestCase(TestCase): """Tests for the ForeignKeyProvider class.""" - def test_make_generators_from_tables(self) -> None: - """Check that we can make a generators file from a tables module.""" + def setUp(self) -> None: + """Pre-test setup.""" + + env = os.environ.copy() + env = {**env, "PGPASSWORD": "password"} + + # Clear and re-create the test database + completed_process = run( + [ + "psql", + "--host=localhost", + "--username=postgres", + "--file=" + str(Path("tests/examples/providers.dump")), + ], + capture_output=True, + env=env, + check=True, + ) + + # psql doesn't always return != 0 if it fails + assert completed_process.stderr == b"", completed_process.stderr + + self.engine = create_engine( + "postgresql://postgres:password@localhost:5432/providers" + ) + + def test_key(self) -> None: + """Test the key method.""" + # pylint: disable=invalid-name + + Base = declarative_base() + metadata = Base.metadata + + class Person(Base): # type: ignore + """A SQLAlchemy table.""" + + __tablename__ = "person" + person_id = Column( + Integer, + primary_key=True, + ) + sex = Column(Text) + + metadata.create_all(self.engine) + + with self.engine.connect() as conn: + stmt = insert(Person).values(sex="M") + conn.execute(stmt) - with open( - "tests/examples/expected_output.py", encoding="utf-8" - ) as expected_output: - expected = expected_output.read() + fkp = ForeignKeyProvider() + key = fkp.key(conn, "public", "person", "sex") - actual = make.make_generators_from_tables(example_tables) - self.assertEqual(expected, actual) + self.assertEqual("M", key) From d3d322dcd8b256226c1f217356350c31255bbf83 Mon Sep 17 00:00:00 2001 From: Iain <25081046+Iain-S@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:13:25 +0000 Subject: [PATCH 3/5] Refactor tests by adding run_psql() func --- tests/test_functional.py | 24 +++--------------------- tests/test_providers.py | 22 ++-------------------- tests/utils.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 41 deletions(-) diff --git a/tests/test_functional.py b/tests/test_functional.py index a8f84630..2f4a83c7 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -4,6 +4,8 @@ from subprocess import run from unittest import TestCase, skipUnless +from tests.utils import run_psql + @skipUnless( os.environ.get("FUNCTIONAL_TESTS") == "1", "Set 'FUNCTIONAL_TESTS=1' to enable." @@ -19,27 +21,7 @@ def setUp(self) -> None: self.orm_file_path.unlink(missing_ok=True) self.ssg_file_path.unlink(missing_ok=True) - # If you need to update src.dump or dst.dump, use - # pg_dump -d src|dst -h localhost -U postgres -C -c > tests/examples/src|dst.dump - - env = os.environ.copy() - env = {**env, "PGPASSWORD": "password"} - - # Clear and re-create the destination database - completed_process = run( - [ - "psql", - "--host=localhost", - "--username=postgres", - "--file=" + str(Path("tests/examples/dst.dump")), - ], - capture_output=True, - env=env, - check=True, - ) - - # psql doesn't always return != 0 if it fails - assert completed_process.stderr == b"", completed_process.stderr + run_psql("dst.dump") def test_workflow(self) -> None: """Test the recommended CLI workflow runs without errors.""" diff --git a/tests/test_providers.py b/tests/test_providers.py index e7e2c96b..f0e1eace 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -1,13 +1,12 @@ """Tests for the providers module.""" import os -from pathlib import Path -from subprocess import run from unittest import TestCase, skipUnless from sqlalchemy import Column, Integer, Text, create_engine, insert from sqlalchemy.ext.declarative import declarative_base from sqlsynthgen.providers import BinaryProvider, ForeignKeyProvider +from tests.utils import run_psql class BinaryProviderTestCase(TestCase): @@ -26,24 +25,7 @@ class ForeignKeyProviderTestCase(TestCase): def setUp(self) -> None: """Pre-test setup.""" - env = os.environ.copy() - env = {**env, "PGPASSWORD": "password"} - - # Clear and re-create the test database - completed_process = run( - [ - "psql", - "--host=localhost", - "--username=postgres", - "--file=" + str(Path("tests/examples/providers.dump")), - ], - capture_output=True, - env=env, - check=True, - ) - - # psql doesn't always return != 0 if it fails - assert completed_process.stderr == b"", completed_process.stderr + run_psql("providers.dump") self.engine = create_engine( "postgresql://postgres:password@localhost:5432/providers" diff --git a/tests/utils.py b/tests/utils.py index 8635d78b..49624879 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,8 @@ """Utilities for testing.""" +import os from functools import lru_cache +from pathlib import Path +from subprocess import run from sqlsynthgen import settings @@ -20,3 +23,28 @@ def get_test_settings() -> settings.Settings: # To stop any local .env files influencing the test _env_file=None, ) + + +def run_psql(dump_file_name: str) -> None: + """Run psql and""" + + # If you need to update a .dump file, use + # pg_dump -d DBNAME -h localhost -U postgres -C -c > tests/examples/FILENAME.dump + + env = os.environ.copy() + env = {**env, "PGPASSWORD": "password"} + + # Clear and re-create the test database + completed_process = run( + [ + "psql", + "--host=localhost", + "--username=postgres", + "--file=" + str(Path(f"tests/examples/{dump_file_name}")), + ], + capture_output=True, + env=env, + check=True, + ) + # psql doesn't always return != 0 if it fails + assert completed_process.stderr == b"", completed_process.stderr From 9621f295a1b9328278be50e409304f27a070854f Mon Sep 17 00:00:00 2001 From: Iain <25081046+Iain-S@users.noreply.github.com> Date: Wed, 11 Jan 2023 17:01:04 +0000 Subject: [PATCH 4/5] Rename ForeignKey provider to ColumnValue provider --- sqlsynthgen/make.py | 10 +++---- sqlsynthgen/providers.py | 20 +++++++------- tests/examples/expected_ssg.py | 10 +++---- tests/test_providers.py | 49 ++++++++++++++++++---------------- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/sqlsynthgen/make.py b/sqlsynthgen/make.py index 6b6501f4..349fa224 100644 --- a/sqlsynthgen/make.py +++ b/sqlsynthgen/make.py @@ -9,11 +9,11 @@ '"""This file was auto-generated by sqlsynthgen but can be edited manually."""', "from mimesis import Generic", "from mimesis.locales import Locale", - "from sqlsynthgen.providers import BinaryProvider, ForeignKeyProvider", + "from sqlsynthgen.providers import BytesProvider, ColumnValueProvider", "", "generic = Generic(locale=Locale.EN)", - "generic.add_provider(ForeignKeyProvider)", - "generic.add_provider(BinaryProvider)", + "generic.add_provider(ColumnValueProvider)", + "generic.add_provider(BytesProvider)", "", ) ) @@ -42,7 +42,7 @@ def make_generators_from_tables(tables_module: ModuleType) -> str: sqltypes.DateTime: "generic.datetime.datetime()", sqltypes.Float: "generic.numeric.float_number()", sqltypes.Integer: "generic.numeric.integer_number()", - sqltypes.LargeBinary: "generic.binary_provider.bytes()", + sqltypes.LargeBinary: "generic.bytes_provider.bytes()", sqltypes.Numeric: "generic.numeric.float_number()", sqltypes.String: "generic.text.color()", sqltypes.Text: "generic.text.color()", @@ -72,7 +72,7 @@ def make_generators_from_tables(tables_module: ModuleType) -> str: fk_schema, fk_table, fk_column = fk_column_path.split(".") new_content += ( f"{INDENTATION*2}self.{column.name} = " - f"generic.foreign_key_provider.key(db_connection, " + f"generic.column_value_provider.column_value(db_connection, " f'"{fk_schema}", "{fk_table}", "{fk_column}"' ")\n" ) diff --git a/sqlsynthgen/providers.py b/sqlsynthgen/providers.py index 8cb40107..cadf52c4 100644 --- a/sqlsynthgen/providers.py +++ b/sqlsynthgen/providers.py @@ -6,28 +6,30 @@ from sqlalchemy.sql import text -class ForeignKeyProvider(BaseProvider): - """A Mimesis provider of foreign keys.""" +class ColumnValueProvider(BaseProvider): + """A Mimesis provider of random values from the source database.""" class Meta: - """Meta-class for ForeignKeyProvider settings.""" + """Meta-class for ColumnValueProvider settings.""" - name = "foreign_key_provider" + name = "column_value_provider" - def key(self, db_connection: Any, schema: str, table: str, column: str) -> Any: - """Return a random value from the table and column specified.""" + def column_value( + self, db_connection: Any, schema: str, table: str, column: str + ) -> Any: + """Return a random value from the column specified.""" query_str = f"SELECT {column} FROM {schema}.{table} ORDER BY random() LIMIT 1" key = db_connection.execute(text(query_str)).fetchone()[0] return key -class BinaryProvider(BaseDataProvider): +class BytesProvider(BaseDataProvider): """A Mimesis provider of binary data.""" class Meta: - """Meta-class for ForeignKeyProvider settings.""" + """Meta-class for BytesProvider settings.""" - name = "binary_provider" + name = "bytes_provider" def bytes(self) -> bytes: """Return a UTF-8 encoded sentence.""" diff --git a/tests/examples/expected_ssg.py b/tests/examples/expected_ssg.py index edf05d89..d29aafd1 100644 --- a/tests/examples/expected_ssg.py +++ b/tests/examples/expected_ssg.py @@ -1,11 +1,11 @@ """This file was auto-generated by sqlsynthgen but can be edited manually.""" from mimesis import Generic from mimesis.locales import Locale -from sqlsynthgen.providers import BinaryProvider, ForeignKeyProvider +from sqlsynthgen.providers import BytesProvider, ColumnValueProvider generic = Generic(locale=Locale.EN) -generic.add_provider(ForeignKeyProvider) -generic.add_provider(BinaryProvider) +generic.add_provider(ColumnValueProvider) +generic.add_provider(BytesProvider) class entityGenerator: @@ -26,11 +26,11 @@ def __init__(self, db_connection): class hospital_visitGenerator: def __init__(self, db_connection): pass - self.person_id = generic.foreign_key_provider.key(db_connection, "myschema", "person", "person_id") + self.person_id = generic.column_value_provider.column_value(db_connection, "myschema", "person", "person_id") self.visit_start = generic.datetime.datetime() self.visit_end = generic.datetime.date() self.visit_duration_seconds = generic.numeric.float_number() - self.visit_image = generic.binary_provider.bytes() + self.visit_image = generic.bytes_provider.bytes() sorted_generators = [ diff --git a/tests/test_providers.py b/tests/test_providers.py index f0e1eace..fb37af41 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -5,22 +5,39 @@ from sqlalchemy import Column, Integer, Text, create_engine, insert from sqlalchemy.ext.declarative import declarative_base -from sqlsynthgen.providers import BinaryProvider, ForeignKeyProvider +from sqlsynthgen.providers import BytesProvider, ColumnValueProvider from tests.utils import run_psql +# pylint: disable=invalid-name +Base = declarative_base() +# pylint: enable=invalid-name +metadata = Base.metadata + + +class Person(Base): # type: ignore + """A SQLAlchemy table.""" + + __tablename__ = "person" + person_id = Column( + Integer, + primary_key=True, + ) + # We don't actually need a foreign key constraint to test this + sex = Column(Text) + class BinaryProviderTestCase(TestCase): - """Tests for the BinaryProvider class.""" + """Tests for the BytesProvider class.""" def test_bytes(self) -> None: - BinaryProvider().bytes().decode("utf-8") + BytesProvider().bytes().decode("utf-8") @skipUnless( os.environ.get("FUNCTIONAL_TESTS") == "1", "Set 'FUNCTIONAL_TESTS=1' to enable." ) -class ForeignKeyProviderTestCase(TestCase): - """Tests for the ForeignKeyProvider class.""" +class ColumnValueProviderTestCase(TestCase): + """Tests for the ColumnValueProvider class.""" def setUp(self) -> None: """Pre-test setup.""" @@ -30,31 +47,17 @@ def setUp(self) -> None: self.engine = create_engine( "postgresql://postgres:password@localhost:5432/providers" ) + metadata.create_all(self.engine) - def test_key(self) -> None: + def test_column_value(self) -> None: """Test the key method.""" # pylint: disable=invalid-name - Base = declarative_base() - metadata = Base.metadata - - class Person(Base): # type: ignore - """A SQLAlchemy table.""" - - __tablename__ = "person" - person_id = Column( - Integer, - primary_key=True, - ) - sex = Column(Text) - - metadata.create_all(self.engine) - with self.engine.connect() as conn: stmt = insert(Person).values(sex="M") conn.execute(stmt) - fkp = ForeignKeyProvider() - key = fkp.key(conn, "public", "person", "sex") + provider = ColumnValueProvider() + key = provider.column_value(conn, "public", "person", "sex") self.assertEqual("M", key) From fe407bce7c47e584c6ca30eec3f44a032e441418 Mon Sep 17 00:00:00 2001 From: Iain <25081046+Iain-S@users.noreply.github.com> Date: Wed, 18 Jan 2023 17:16:54 +0000 Subject: [PATCH 5/5] Pass a src db connection to the generators In addition to the destination connection they already have --- sqlsynthgen/create.py | 23 ++++++++++++++--------- sqlsynthgen/make.py | 4 ++-- tests/examples/expected_ssg.py | 8 ++++---- tests/test_create.py | 22 ++++++++++++++++++++-- tests/test_providers.py | 3 ++- 5 files changed, 42 insertions(+), 18 deletions(-) diff --git a/sqlsynthgen/create.py b/sqlsynthgen/create.py index 6e7d9c2c..02bd2243 100644 --- a/sqlsynthgen/create.py +++ b/sqlsynthgen/create.py @@ -26,18 +26,23 @@ def create_db_tables(metadata: Any) -> Any: def create_db_data(sorted_tables: list, sorted_generators: list, num_rows: int) -> None: """Connect to a database and populate it with data.""" settings = get_settings() - engine = create_engine(settings.dst_postgres_dsn) + dst_engine = create_engine(settings.dst_postgres_dsn) + src_engine = create_engine(settings.src_postgres_dsn) - with engine.connect() as conn: - populate(conn, sorted_tables, sorted_generators, num_rows) + with dst_engine.connect() as dst_conn: + with src_engine.connect() as src_conn: + populate(src_conn, dst_conn, sorted_tables, sorted_generators, num_rows) -def populate(conn: Any, tables: list, generators: list, num_rows: int) -> None: +def populate( + src_conn: Any, dst_conn: Any, tables: list, generators: list, num_rows: int +) -> None: """Populate a database schema with dummy data.""" - for table, generator in zip(tables, generators): - # Run all the inserts for one table in a transaction - with conn.begin(): + for table, generator in zip( + tables, generators + ): # Run all the inserts for one table in a transaction + with dst_conn.begin(): for _ in range(num_rows): - stmt = insert(table).values(generator(conn).__dict__) - conn.execute(stmt) + stmt = insert(table).values(generator(src_conn, dst_conn).__dict__) + dst_conn.execute(stmt) diff --git a/sqlsynthgen/make.py b/sqlsynthgen/make.py index 349fa224..638b1b1d 100644 --- a/sqlsynthgen/make.py +++ b/sqlsynthgen/make.py @@ -56,7 +56,7 @@ def make_generators_from_tables(tables_module: ModuleType) -> str: + new_class_name + ":\n" + INDENTATION - + "def __init__(self, db_connection):\n" + + "def __init__(self, src_db_conn, dst_db_conn):\n" ) for column in table.columns: @@ -72,7 +72,7 @@ def make_generators_from_tables(tables_module: ModuleType) -> str: fk_schema, fk_table, fk_column = fk_column_path.split(".") new_content += ( f"{INDENTATION*2}self.{column.name} = " - f"generic.column_value_provider.column_value(db_connection, " + f"generic.column_value_provider.column_value(dst_db_conn, " f'"{fk_schema}", "{fk_table}", "{fk_column}"' ")\n" ) diff --git a/tests/examples/expected_ssg.py b/tests/examples/expected_ssg.py index d29aafd1..ce14784f 100644 --- a/tests/examples/expected_ssg.py +++ b/tests/examples/expected_ssg.py @@ -9,12 +9,12 @@ class entityGenerator: - def __init__(self, db_connection): + def __init__(self, src_db_conn, dst_db_conn): pass class personGenerator: - def __init__(self, db_connection): + def __init__(self, src_db_conn, dst_db_conn): pass self.name = generic.text.color() self.nhs_number = generic.text.color() @@ -24,9 +24,9 @@ def __init__(self, db_connection): class hospital_visitGenerator: - def __init__(self, db_connection): + def __init__(self, src_db_conn, dst_db_conn): pass - self.person_id = generic.column_value_provider.column_value(db_connection, "myschema", "person", "person_id") + self.person_id = generic.column_value_provider.column_value(dst_db_conn, "myschema", "person", "person_id") self.visit_start = generic.datetime.datetime() self.visit_end = generic.datetime.date() self.visit_duration_seconds = generic.numeric.float_number() diff --git a/tests/test_create.py b/tests/test_create.py index de1b163a..3041b8ba 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -2,7 +2,7 @@ from unittest import TestCase from unittest.mock import MagicMock, patch -from sqlsynthgen.create import create_db_data, create_db_tables +from sqlsynthgen.create import create_db_data, create_db_tables, populate from tests.utils import get_test_settings @@ -21,7 +21,7 @@ def test_create_db_data(self) -> None: create_db_data([], [], 0) mock_populate.assert_called_once() - mock_create_engine.assert_called_once() + mock_create_engine.assert_called() def test_create_db_tables(self) -> None: """Test the create_tables function.""" @@ -36,3 +36,21 @@ def test_create_db_tables(self) -> None: mock_create_engine.assert_called_once_with( mock_get_settings.return_value.dst_postgres_dsn ) + + def test_populate(self) -> None: + """Test the populate function.""" + with patch("sqlsynthgen.create.insert") as mock_insert: + mock_src_conn = MagicMock() + mock_dst_conn = MagicMock() + mock_gen = MagicMock() + tables = [None] + generators = [mock_gen] + populate(mock_src_conn, mock_dst_conn, tables, generators, 1) + + mock_gen.assert_called_once_with(mock_src_conn, mock_dst_conn) + mock_insert.return_value.values.assert_called_once_with( + mock_gen.return_value.__dict__ + ) + mock_dst_conn.execute.assert_called_once_with( + mock_insert.return_value.values.return_value + ) diff --git a/tests/test_providers.py b/tests/test_providers.py index fb37af41..fe79fbb3 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -30,7 +30,8 @@ class BinaryProviderTestCase(TestCase): """Tests for the BytesProvider class.""" def test_bytes(self) -> None: - BytesProvider().bytes().decode("utf-8") + """Test the bytes method.""" + self.assertTrue(BytesProvider().bytes().decode("utf-8") != "") @skipUnless(