from decimal import Decimal, ROUND_HALF_UP from typing import Dict, List import pytest from app.logger import log from .insights_steps import fetch_playlist_tracklist, get_playlists_by_type, PLAYLIST_TYPE, STORE_ID BRAND_NAMES = ('sme', 'awal', 'theorchard') """ https://theorchard.atlassian.net/browse/IN-16864 Test case is considered to validate the SMG metrics correctness for SME, Orchard and AWAL brands %. """ def _get_playlist_data(graphql, playlist_id: str, store_id: int) -> dict: """Fetch and validate playlist data.""" tracklist = fetch_playlist_tracklist(graphql, playlist_id, store_id) playlist_data = tracklist.get('playlist') if playlist_data is None: log.error(f"Playlist id: {playlist_id} returned null for playlist.") pytest.fail(f"Playlist id: {playlist_id} returned null for playlist.") return {} # unreachable, satisfies type checker return playlist_data def _count_tracks_by_brand(placements: List[dict]) -> Dict[str, int]: """Count tracks grouped by brand name. Args: placements: List of playlist placement dictionaries Returns: Dictionary with brand names as keys and track counts as values """ brand_counts = {brand: 0 for brand in BRAND_NAMES} for placement in placements: brands = placement.get('brands', []) if brands and brands[0]['name'] in BRAND_NAMES: brand_counts[brands[0]['name']] += 1 return brand_counts def _get_label_percentage(labels: List[dict], code: str) -> float: """Extract percentage value for a specific label code.""" return next((label['value'] for label in labels if label['code'] == code), 0) def _get_smg_percent_labels(graphql, playlist_id: str, store_id: int) -> List[dict] | None: """Fetch and extract smgPercent labels from playlist metrics. Returns List[dict] with label entries, or None if smgPercent data is absent. """ playlist_metrics = graphql.fetch_data( 'PlaylistMetrics', {"storeId": store_id, "storePlaylistId": playlist_id} ) smg_percent = playlist_metrics.get('playlist', {}).get('smgPercent') if smg_percent is None or 'labels' not in smg_percent: log.warning(f"Playlist id: {playlist_id} has no smgPercent data.") return None return smg_percent['labels'] def _round_percent(count: int, total: int) -> float: """Compute percentage with ROUND_HALF_UP to 2 decimals (matches backend rounding). Uses pure Decimal arithmetic to avoid any float imprecision in the division. """ pct = (Decimal(count) * 100 / Decimal(total)).quantize( Decimal('0.01'), rounding=ROUND_HALF_UP ) return float(pct) @pytest.mark.parametrize("playlist_to_test", get_playlists_by_type(PLAYLIST_TYPE)) def test_playlist_metrics(graphql, playlist_to_test): """Validate SMG metrics correctness for SME, Orchard, and AWAL brand percentages.""" # Fetch playlist data playlist_data = _get_playlist_data(graphql, playlist_to_test, STORE_ID) playlist_placements = playlist_data['playlistPlacements']['placementsV2'] total_tracks = len(playlist_placements) # Count tracks by brand brand_counts = _count_tracks_by_brand(playlist_placements) log.debug(f"Track counts - SME: {brand_counts['sme']}, " f"Orchard: {brand_counts['theorchard']}, " f"AWAL: {brand_counts['awal']}, Total: {total_tracks}") # Calculate expected percentages (ROUND_HALF_UP @ 2 decimals — matches backend) expected_percentages = { brand: _round_percent(count, total_tracks) for brand, count in brand_counts.items() } # Fetch actual percentages from metrics labels = _get_smg_percent_labels(graphql, playlist_to_test, STORE_ID) if labels is None: # smgPercent is absent — acceptable only if no tracks belong to any brand brands_with_tracks = [brand for brand, count in brand_counts.items() if count > 0] assert not brands_with_tracks, ( f"smgPercent data is missing for playlist {playlist_to_test}, " f"but tracks were found for brands: {brands_with_tracks}" ) log.info(f"Playlist {playlist_to_test} has no smgPercent data and no brand tracks — skipping percentage check.") return assert isinstance(labels, list) actual_percentages = { 'sme': _get_label_percentage(labels, 'sme'), 'awal': _get_label_percentage(labels, 'awal'), 'theorchard': _get_label_percentage(labels, 'theorchard') } summary = ( f"\nbrand: in metrics % vs calculated %" f"\n---------------------------------------" f"\nSME: {actual_percentages['sme']}% vs {expected_percentages['sme']}%" f"\nAWAL: {actual_percentages['awal']}% vs {expected_percentages['awal']}%" f"\nOrchard: {actual_percentages['theorchard']}% vs {expected_percentages['theorchard']}%" ) log.debug(f"Percentages for playlist {playlist_to_test}:{summary}") # Assert all percentages match. Tiny tolerance only guards against float-representation # noise (e.g. 0.1 + 0.2); both values are already rounded to 2 decimals via ROUND_HALF_UP. mismatches = [ brand for brand in BRAND_NAMES if abs(actual_percentages[brand] - expected_percentages[brand]) > 1e-9 ] assert not mismatches, summary