import pathlib import numpy as np import pandas as pd import pytest from service.db import engine from service.tasks.analytics import get_grouped_analytics, re_pre_calculate_chart_data from service.tasks.delete_collection import _delete_collection from service.tasks.filtering import re_pre_calculate_filter_config from service.utils.data_model_utils import generate_collection from tests.integration.conftest import SCHEMA_NAME, USER_ID """ The tests listed here are integration tests instead and are meant to cover some core functionality (also invoking some supporting functions). Coverage: - initializing schemas and data models - processing collections (creating labels, populating data model) - creating enrichments (except the ones that require external API or ML calls atm) - creating analytics and filters, recalculating analytics - deleting collections """ current_path = pathlib.Path(__file__).parent.absolute() def compare_fan_attribute_w_expected(collection_id, file_name): """Compare data in fan_attribute table in the database with the expected results that we've stored locally.""" """ Note that we cannot compare the timestamp column for assertion. """ sql = f"""SELECT fan_id, attribute_id, value, row_id, collection_id FROM {SCHEMA_NAME}.fan_attribute fa JOIN {SCHEMA_NAME}.collection c on fa.collection_id = c.id WHERE (c.id = {collection_id} and c.parent_id is null) or c.parent_id = {collection_id} ORDER BY 1, 2, 4, 5 """ df = pd.read_sql(sql, engine) """ Use this to store expected df. If processing results change due to logic change, update the local data file. Since we run this inside container, here's a handy command for getting the file out of container docker cp b1743e67a218:/app/tests/data/clxn_1_fan_attr_after_enrichments.csv ~/data-processor/tests/data/clxn_1_fan_attr_after_enrichments.csv """ expected_df = pd.DataFrame(pd.read_csv(f"{current_path}/data/{file_name}")) """ When we read_csv in this test, we get objects. We want to compare values anyway, so it's fine to ignore types. We also make sure we're sorting the dataframes the same way (the sql select might not return results deterministically. We also make sure we pick only the cols we want and drop the index - df operations might create some index col """ sort_cols = ["fan_id", "row_id", "attribute_id", "collection_id"] expected_df = expected_df.astype(object).sort_values(sort_cols, ascending=True) df = df.astype(object).sort_values(sort_cols, ascending=True) return np.array_equal(df.values, expected_df.values) def test_process_file(): """Test that base attribute generation works for multiple files. Source files loaded from our example data. If you add more collections, store the expected data via compare_fan_attribute_w_expected """ from service.tasks.base_attributes_generation import generate_labels_and_attributes # TODO! test guess labels input_files = [ { "file_name": "demo_mailing_list_data_1923.csv", "field_map": [ {"value": "userEmail", "index": 0}, {"value": "userFirstName", "index": 1}, {"value": "userLastName", "index": 2}, {"value": "userAge", "index": 3}, {"value": "userGender", "index": 4}, {"value": "userCity", "index": 5}, {"value": "userState", "index": 6}, {"value": "userCountry", "index": 7}, {"value": "userMailchimpRating", "index": 8}, {"value": "userPhone", "index": 9}, ], }, { "file_name": "demo_ticketing_data_m_906.csv", "field_map": [ {"value": "userEmail", "index": 5}, {"value": "purchaseQuantity", "index": 3}, {"value": "purchaseDate", "index": 4}, {"value": "purchaseMonetary", "index": 6}, {"value": "userFirstName", "index": 7}, {"value": "userLastName", "index": 8}, ], }, ] for idx, x in enumerate(input_files, 1): file_name = x["file_name"] field_map = x["field_map"] source_df = pd.read_csv(f"{current_path}/data/{file_name}", delimiter=";") collection_id = generate_collection( collection_name=file_name, collection_source="local_data", schema_name=SCHEMA_NAME, user_id=USER_ID, status="uploading", ) """ Generate sys labels and process data """ response = generate_labels_and_attributes( SCHEMA_NAME, SCHEMA_NAME, file_name, field_map, source_df, USER_ID, collection_id=collection_id, final_status="finished", ) collection_id = response["collection_id"] """ Compare initial processing result data with what we expect to get """ expected_file_name = f"clxn_{idx}_fan_attr.csv" comparison_result = compare_fan_attribute_w_expected( collection_id, expected_file_name ) assert comparison_result pytest.collection_ids.append(collection_id) def test_analytics_and_filters(): """Test if analytics and filters creation works and gives expected results. When you change analytics (even analytics config), and you are certain that your changes are logically correct, save new pickle files and bring update them in repo """ """ Test that total analytics returns expected results """ # Recalculate/cache globals and charts for collection re_pre_calculate_chart_data(SCHEMA_NAME, None) # Recalculate/cache filter from ..tasks.analyticsconfigs re_pre_calculate_filter_config(SCHEMA_NAME, None) get_grouped_analytics(SCHEMA_NAME, None, ()) # TODO! MAKE THIS WORK! # assert all([a == b for a, b in zip(actual, expected)]) """ Test that collection based analytics returns expected results """ collection_ids = pytest.collection_ids for _, collection_id in enumerate(collection_ids, 1): # Recalculate/cache globals and charts for collection re_pre_calculate_chart_data(SCHEMA_NAME, collection_id) # Recalculate/cache filter from ..tasks.analyticsconfigs re_pre_calculate_filter_config(SCHEMA_NAME, collection_id) get_grouped_analytics(SCHEMA_NAME, collection_id, ()) def compare_fb_data(df, file_name, fields): """Use this to store expected df. If processing results change due to logic change, update the local data file. Since we run this inside container, here's a handy command for getting the file out of container docker cp eb9ba0cbf7e6:/app/tests/data/clxn_1_fan_attr_after_enrichments.csv ~/data-processor/tests/data/clxn_1_fan_attr_after_enrichments.csv """ df.to_csv(f"{current_path}/data/{file_name}", index=False) expected_df = pd.DataFrame(pd.read_csv(f"{current_path}/data/{file_name}")) """ When we read_csv in this test, we get objects. We want to compare values anyway, so it's fine to ignore types. We also make sure we're sorting the dataframes the same way (the sql select might not return results deterministically. We also make sure we pick only the cols we want and drop the index - df operations might create some index col """ expected_df = expected_df.astype(object).sort_values(fields, ascending=True) df = df.astype(object).sort_values(fields, ascending=True).replace(np.nan, "None") print(df) print(expected_df) return np.array_equal(df.values, expected_df.values) def test_delete_collection(): """Delete collection and assert it's gone from fan_attributes.""" collection_ids = pytest.collection_ids for collection_id in collection_ids: _delete_collection(SCHEMA_NAME, SCHEMA_NAME, USER_ID, collection_id) sql = f"""SELECT * FROM {SCHEMA_NAME}.fan_attribute WHERE collection_id = {collection_id}""" df = pd.read_sql(sql, engine) assert df.empty