diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e3b96..bf1a760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project are documented in this file. This changelog ## [Unreleased] +## [0.9.0] - 2026-10-04 + ### Fixed - Ensure newly created Excel instances are cleaned up when opening a workbook @@ -11,6 +13,10 @@ All notable changes to this project are documented in this file. This changelog ### Changed +- Hybridized rendered color extraction for saved `.xlsx` / `.xlsm` workbooks: + ordinary RGB fills use saved styles, while conditional formatting and + ambiguous fills retain Excel DisplayFormat evaluation. Unsupported sheets + and unsaved workbooks retain the complete COM scan. - Switched `.xlsx` / `.xlsm` `light` extraction to the direct OOXML core and rich pipeline using one shared ZIP. Unsupported OOXML and `colors_map` opt-in restart the complete openpyxl compatibility pipeline with an diff --git a/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/dense_cf.xlsx b/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/dense_cf.xlsx new file mode 100644 index 0000000..d0ccd63 Binary files /dev/null and b/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/dense_cf.xlsx differ diff --git a/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/overlapping_formula.xlsx b/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/overlapping_formula.xlsx new file mode 100644 index 0000000..26fba22 Binary files /dev/null and b/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/overlapping_formula.xlsx differ diff --git a/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/sparse_cf.xlsx b/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/sparse_cf.xlsx new file mode 100644 index 0000000..7793e7c Binary files /dev/null and b/benchmark/baselines/issue159-colors-cf-probe-windows-fixtures/sparse_cf.xlsx differ diff --git a/benchmark/baselines/issue159-colors-cf-probe-windows.json b/benchmark/baselines/issue159-colors-cf-probe-windows.json new file mode 100644 index 0000000..fe84c0c --- /dev/null +++ b/benchmark/baselines/issue159-colors-cf-probe-windows.json @@ -0,0 +1,1685 @@ +{ + "schema_version": 1, + "recorded_at": "2026-10-04T00:54:35.714282+00:00", + "platform": "Windows-10-10.0.22631-SP0", + "python": "3.11.13 (main, Jul 23 2025, 00:29:09) [MSC v.1944 64 bit (AMD64)]", + "xlwings": "0.37.4", + "excel_version": "16.0", + "configuration": { + "rows": 10, + "columns": 20, + "cases": [ + "sparse_cf", + "dense_cf", + "overlapping_formula" + ], + "repeats": 1, + "execution": "sequential; one owned Excel application; one workbook open at a time", + "include_default_background_values": [ + false, + true + ], + "ignore_color_cases": [ + null, + [ + "FF0000" + ] + ] + }, + "fixtures": [ + { + "name": "sparse_cf", + "description": "conditional formatting with sparse matches", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-cf-probe-windows-fixtures\\sparse_cf.xlsx", + "sha256": "ba48f4cb0aa8a0bccfc3ca16b037ab819ab402bc74f1ca0abc99b68ffd799bd0", + "rows": 10, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "one formula rule limited to D2:D10; every other row matches" + ], + "expected_conditional_colors_min_cells": { + "FF0000": 1 + } + }, + { + "name": "dense_cf", + "description": "conditional formatting with dense alternating matches", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-cf-probe-windows-fixtures\\dense_cf.xlsx", + "sha256": "9009067df89176f498ac199a951cc227d9442f176dabfd8155e31e3156a6c8b2", + "rows": 10, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "one formula rule across the grid; alternating cells match" + ], + "expected_conditional_colors_min_cells": { + "0070C0": 1 + } + }, + { + "name": "overlapping_formula", + "description": "overlapping CF rules, formulas, and mixed-range probe", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-cf-probe-windows-fixtures\\overlapping_formula.xlsx", + "sha256": "e7d2fd98bc1437c8a3e8f571d63f65cb83968450e3b3cebeb4b4630f975ff796", + "rows": 10, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "three overlapping formula rules across the grid", + "two stop-if-true rules on A1:B1 for the mixed-range probe" + ], + "expected_conditional_colors_min_cells": { + "FF0000": 2, + "0070C0": 2 + } + } + ], + "mixed_range_probe": { + "sheet": "Data", + "range": "A1:B1", + "workbook_saved_flag_before_calculate": true, + "workbook_saved_flag_after_calculate": false, + "range_value": { + "value": 0, + "repr": "0.0", + "type": "float" + }, + "per_cell_values": [ + { + "address": "A1", + "value": 255, + "repr": "255.0", + "type": "float" + }, + { + "address": "B1", + "value": 12611584, + "repr": "12611584.0", + "type": "float" + } + ], + "per_cell_colors_differ": true, + "range_matches_each_cell": false + }, + "measurements": [ + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 906.2811001203954, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "b59b701ff3b25de35a5335c94c5ba0f7717803583d99c1420c62dc165fbd2ec9", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 38.20779989473522, + "wrapper_display_format_calls": 9, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=9 rendered_candidates=9 display_format_calls=9 fallback=False static_duration_ms=8.041 com_duration_ms=15.623 duration_ms=27.381", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "9", + "display_format_calls": "9" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "b59b701ff3b25de35a5335c94c5ba0f7717803583d99c1420c62dc165fbd2ec9", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 344.2443001549691, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 33.16750004887581, + "wrapper_display_format_calls": 9, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=9 rendered_candidates=9 display_format_calls=9 fallback=False static_duration_ms=5.631 com_duration_ms=15.758 duration_ms=24.774", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "9", + "display_format_calls": "9" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 380.78799983486533, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "b57f87b573ef3d4107affcbb391c9dd1958a15675adf01a893779dd224374879", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 37.6456999219954, + "wrapper_display_format_calls": 9, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=9 rendered_candidates=9 display_format_calls=9 fallback=False static_duration_ms=5.489 com_duration_ms=19.989 duration_ms=28.711", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "9", + "display_format_calls": "9" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "b57f87b573ef3d4107affcbb391c9dd1958a15675adf01a893779dd224374879", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 373.52390005253255, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "2e22b9532de62984ffa90bfb5aabcac9f8339dc7675ffd29ccdf81a1f890fb3a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 195 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 34.49550014920533, + "wrapper_display_format_calls": 9, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=9 rendered_candidates=9 display_format_calls=9 fallback=False static_duration_ms=5.535 com_duration_ms=15.917 duration_ms=25.541", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "9", + "display_format_calls": "9" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "2e22b9532de62984ffa90bfb5aabcac9f8339dc7675ffd29ccdf81a1f890fb3a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 195 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 928.2537999097258, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "22f06aba5615b0f544328054b4f7178912bcf242e86f71e717e73f2a8bf124b5", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 100 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 423.05919993668795, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=200 rendered_candidates=200 display_format_calls=200 fallback=False static_duration_ms=4.344 com_duration_ms=406.816 duration_ms=414.889", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "200", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "22f06aba5615b0f544328054b4f7178912bcf242e86f71e717e73f2a8bf124b5", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 100 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 472.36410016193986, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "22f06aba5615b0f544328054b4f7178912bcf242e86f71e717e73f2a8bf124b5", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 100 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 500.1632000785321, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=200 rendered_candidates=200 display_format_calls=200 fallback=False static_duration_ms=4.371 com_duration_ms=478.506 duration_ms=488.537", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "200", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "22f06aba5615b0f544328054b4f7178912bcf242e86f71e717e73f2a8bf124b5", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 100 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 549.7897998429835, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "e25e6cf59991b52286d61923a87953386dd84a538fdd2c717c523fb0f2d4dc27", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 463.77129992470145, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=200 rendered_candidates=200 display_format_calls=200 fallback=False static_duration_ms=4.824 com_duration_ms=433.866 duration_ms=453.732", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "200", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "e25e6cf59991b52286d61923a87953386dd84a538fdd2c717c523fb0f2d4dc27", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 361.9605000130832, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "e25e6cf59991b52286d61923a87953386dd84a538fdd2c717c523fb0f2d4dc27", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 625.6393999792635, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=200 rendered_candidates=200 display_format_calls=200 fallback=False static_duration_ms=4.172 com_duration_ms=610.329 duration_ms=618.272", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "200", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "e25e6cf59991b52286d61923a87953386dd84a538fdd2c717c523fb0f2d4dc27", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 625.709599815309, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "b83bf927118f378a085a8059e52ed1347be5de213f2bd318c0e0ba9017c54f33", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 59 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 491.01049988530576, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=0 rendered_candidates=0 display_format_calls=200 fallback=True static_duration_ms=0.004 com_duration_ms=469.021 duration_ms=476.412", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "0", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "b83bf927118f378a085a8059e52ed1347be5de213f2bd318c0e0ba9017c54f33", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 59 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 469.5373000577092, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "c13ab214ecd078a7f9df65263926b627e3f23d1495d5dcf44df08c3889c50ab5", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 38 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 457.4407001491636, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=0 rendered_candidates=0 display_format_calls=200 fallback=True static_duration_ms=0.003 com_duration_ms=441.910 duration_ms=447.508", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "0", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "c13ab214ecd078a7f9df65263926b627e3f23d1495d5dcf44df08c3889c50ab5", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 38 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 433.3123001269996, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "44f701c06555da168c6fb357fb670f94834e92adb654ff4c8112ded9bbe8a246", + "map_summary": { + "sheets": 1, + "color_keys": 4, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 407.1120999287814, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=0 rendered_candidates=0 display_format_calls=200 fallback=True static_duration_ms=0.003 com_duration_ms=392.907 duration_ms=397.784", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "0", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "44f701c06555da168c6fb357fb670f94834e92adb654ff4c8112ded9bbe8a246", + "map_summary": { + "sheets": 1, + "color_keys": 4, + "mapped_cells": 200 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 561.4062999375165, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 200, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "8c75e2c0f40fdce01ee8cd91e6ff88bb62ece3759071db5acbcc9a07990d183f", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 179 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 894.7964999824762, + "wrapper_display_format_calls": 200, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=200 conditional_candidates=0 rendered_candidates=0 display_format_calls=200 fallback=True static_duration_ms=0.003 com_duration_ms=878.695 duration_ms=883.827", + "metric_fields": { + "used_cells": "200", + "conditional_candidates": "0", + "display_format_calls": "200" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "8c75e2c0f40fdce01ee8cd91e6ff88bb62ece3759071db5acbcc9a07990d183f", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 179 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + } + ], + "comparisons": [ + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 5 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 100 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 21, + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 37 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + } + ] +} diff --git a/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/dense_cf.xlsx b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/dense_cf.xlsx new file mode 100644 index 0000000..d48befa Binary files /dev/null and b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/dense_cf.xlsx differ diff --git a/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/overlapping_formula.xlsx b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/overlapping_formula.xlsx new file mode 100644 index 0000000..1d6ea39 Binary files /dev/null and b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/overlapping_formula.xlsx differ diff --git a/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/sparse_cf.xlsx b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/sparse_cf.xlsx new file mode 100644 index 0000000..6712939 Binary files /dev/null and b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/sparse_cf.xlsx differ diff --git a/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/static.xlsx b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/static.xlsx new file mode 100644 index 0000000..051017d Binary files /dev/null and b/benchmark/baselines/issue159-colors-final-smoke-windows-fixtures/static.xlsx differ diff --git a/benchmark/baselines/issue159-colors-final-smoke-windows.json b/benchmark/baselines/issue159-colors-final-smoke-windows.json new file mode 100644 index 0000000..7b7b488 --- /dev/null +++ b/benchmark/baselines/issue159-colors-final-smoke-windows.json @@ -0,0 +1,2229 @@ +{ + "schema_version": 1, + "recorded_at": "2026-10-04T00:53:49.650865+00:00", + "platform": "Windows-10-10.0.22631-SP0", + "python": "3.11.13 (main, Jul 23 2025, 00:29:09) [MSC v.1944 64 bit (AMD64)]", + "xlwings": "0.37.4", + "excel_version": "16.0", + "configuration": { + "rows": 30, + "columns": 20, + "cases": [ + "static", + "sparse_cf", + "dense_cf", + "overlapping_formula" + ], + "repeats": 1, + "execution": "sequential; one owned Excel application; one workbook open at a time", + "include_default_background_values": [ + false, + true + ], + "ignore_color_cases": [ + null, + [ + "FF0000" + ] + ] + }, + "fixtures": [ + { + "name": "static", + "description": "rows x 20 values with deterministic direct fills", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-final-smoke-windows-fixtures\\static.xlsx", + "sha256": "261020811952fd6a463d802f1b907608e61c81399317f07decec285b99945203", + "rows": 30, + "columns": 20, + "sheets": 1, + "conditional_formatting": [], + "expected_conditional_colors_min_cells": null + }, + { + "name": "sparse_cf", + "description": "conditional formatting with sparse matches", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-final-smoke-windows-fixtures\\sparse_cf.xlsx", + "sha256": "058767f52160f83a6a120569e7c2f299ec83861b55ce9c3c90efc2a5a7434c55", + "rows": 30, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "one formula rule limited to D2:D30; every other row matches" + ], + "expected_conditional_colors_min_cells": { + "FF0000": 1 + } + }, + { + "name": "dense_cf", + "description": "conditional formatting with dense alternating matches", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-final-smoke-windows-fixtures\\dense_cf.xlsx", + "sha256": "74cef73be138c7f45ded261877b00877c9d47032d8166736a82d40685787f343", + "rows": 30, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "one formula rule across the grid; alternating cells match" + ], + "expected_conditional_colors_min_cells": { + "0070C0": 1 + } + }, + { + "name": "overlapping_formula", + "description": "overlapping CF rules, formulas, and mixed-range probe", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-final-smoke-windows-fixtures\\overlapping_formula.xlsx", + "sha256": "1a49a078ec5d29c5a905a18281367259c1c14c128fd5d3a40041d052a6399247", + "rows": 30, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "three overlapping formula rules across the grid", + "two stop-if-true rules on A1:B1 for the mixed-range probe" + ], + "expected_conditional_colors_min_cells": { + "FF0000": 2, + "0070C0": 2 + } + } + ], + "mixed_range_probe": { + "sheet": "Data", + "range": "A1:B1", + "workbook_saved_flag_before_calculate": true, + "workbook_saved_flag_after_calculate": false, + "range_value": { + "value": 0, + "repr": "0.0", + "type": "float" + }, + "per_cell_values": [ + { + "address": "A1", + "value": 255, + "repr": "255.0", + "type": "float" + }, + { + "address": "B1", + "value": 12611584, + "repr": "12611584.0", + "type": "float" + } + ], + "per_cell_colors_differ": true, + "range_matches_each_cell": false + }, + "measurements": [ + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 1284.909199923277, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "74dffe3b51df3f5fff78dfcbf98fc4348312b2aca31db30a18dada154ff8253d", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 33 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 22.31089980341494, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=8.941 com_duration_ms=0.018 duration_ms=12.597", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "74dffe3b51df3f5fff78dfcbf98fc4348312b2aca31db30a18dada154ff8253d", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 33 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1073.130700038746, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "d8ea6397c7a8c8e01d7cc8ceaef51631ea0860dad148507467ef3459d6e25ccc", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 7 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 19.337200094014406, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=7.770 com_duration_ms=0.006 duration_ms=11.500", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "d8ea6397c7a8c8e01d7cc8ceaef51631ea0860dad148507467ef3459d6e25ccc", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 7 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 1290.4598999302834, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "9b311d4f0bf063b3de44f4baa02edef33c6f5eee4579939ba24e946e7c0a821f", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 600 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 18.726699985563755, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=7.531 com_duration_ms=0.006 duration_ms=11.511", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "9b311d4f0bf063b3de44f4baa02edef33c6f5eee4579939ba24e946e7c0a821f", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 600 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1348.883100086823, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "4dbf9f61fad0cea0249add0b64c4434e7d8f0928a9391691517457e3a05d4ff7", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 574 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 26.709500001743436, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=9.144 com_duration_ms=0.008 duration_ms=15.893", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "4dbf9f61fad0cea0249add0b64c4434e7d8f0928a9391691517457e3a05d4ff7", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 574 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 1438.3194998372346, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 69.5809000171721, + "wrapper_display_format_calls": 29, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=29 rendered_candidates=29 display_format_calls=29 fallback=False static_duration_ms=8.975 com_duration_ms=50.000 duration_ms=62.215", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "29", + "display_format_calls": "29" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1326.652699848637, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 69.70580015331507, + "wrapper_display_format_calls": 29, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=29 rendered_candidates=29 display_format_calls=29 fallback=False static_duration_ms=7.498 com_duration_ms=50.403 duration_ms=61.983", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "29", + "display_format_calls": "29" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 1287.4936999287456, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 65.16680005006492, + "wrapper_display_format_calls": 29, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=29 rendered_candidates=29 display_format_calls=29 fallback=False static_duration_ms=7.275 com_duration_ms=47.091 duration_ms=57.841", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "29", + "display_format_calls": "29" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1871.4595998171717, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 88.11360015533864, + "wrapper_display_format_calls": 29, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=29 rendered_candidates=29 display_format_calls=29 fallback=False static_duration_ms=7.237 com_duration_ms=65.542 duration_ms=79.425", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "29", + "display_format_calls": "29" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 1827.473800163716, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 1358.5032001137733, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=600 rendered_candidates=600 display_format_calls=600 fallback=False static_duration_ms=7.234 com_duration_ms=1340.564 duration_ms=1350.808", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "600", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 2264.9328999686986, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 3162.7462999895215, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=600 rendered_candidates=600 display_format_calls=600 fallback=False static_duration_ms=9.016 com_duration_ms=3130.858 duration_ms=3146.012", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "600", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 2589.855199912563, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 1241.4532999973744, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=600 rendered_candidates=600 display_format_calls=600 fallback=False static_duration_ms=6.688 com_duration_ms=1223.062 duration_ms=1234.224", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "600", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1219.7495999280363, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1441.270099952817, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=600 rendered_candidates=600 display_format_calls=600 fallback=False static_duration_ms=8.166 com_duration_ms=1401.564 duration_ms=1415.502", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "600", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "127551e83a9758c290973c67913badad18a76a28a81e662717878e04e708783e", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 1211.2863000947982, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "0541cb4127cd9a4a44b7574b1984728be7b64950ce2c92b3cdb7f815a4bedb6e", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 2 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 1218.3375998865813, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=600 fallback=True static_duration_ms=0.004 com_duration_ms=1207.847 duration_ms=1210.964", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "0541cb4127cd9a4a44b7574b1984728be7b64950ce2c92b3cdb7f815a4bedb6e", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 2 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1164.665900170803, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "36969d75f0dedc5c5a04d4fbd5ab4433824448c7fcf233705173130421c104cd", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 1 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1186.2331999000162, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=600 fallback=True static_duration_ms=0.003 com_duration_ms=1174.277 duration_ms=1177.988", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "36969d75f0dedc5c5a04d4fbd5ab4433824448c7fcf233705173130421c104cd", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 1 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 1639.61479999125, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "151b3a5e168747d1468415ce7c56f8a129f205e77b65e2e8a65f5460d569c7d7", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 1273.8779000937939, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=600 fallback=True static_duration_ms=0.003 com_duration_ms=1262.654 duration_ms=1266.462", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "151b3a5e168747d1468415ce7c56f8a129f205e77b65e2e8a65f5460d569c7d7", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 600 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1317.1645998954773, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 600, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "44fa909984f4a645a78a5e0048d968eef3e012f940e349b580e5d11d3ce77b0d", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 599 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1271.5755000244826, + "wrapper_display_format_calls": 600, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=600 fallback=True static_duration_ms=0.003 com_duration_ms=1250.916 duration_ms=1254.786", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "600" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "44fa909984f4a645a78a5e0048d968eef3e012f940e349b580e5d11d3ce77b0d", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 599 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "exact_workbook_colors_map_equal": true + } + ], + "comparisons": [ + { + "fixture": "static", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "static", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "static", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "static", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 0 + }, + "missing_minimums": { + "FF0000": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "0070C0": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "0070C0": 1 + }, + "actual_cells_by_color": { + "0070C0": 0 + }, + "missing_minimums": { + "0070C0": { + "minimum": 1, + "actual": 0 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 2, + "0070C0": 2 + }, + "actual_cells_by_color": { + "FF0000": 1, + "0070C0": 1 + }, + "missing_minimums": { + "FF0000": { + "minimum": 2, + "actual": 1 + }, + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + } + } + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 2, + "0070C0": 2 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": { + "0070C0": 2 + }, + "actual_cells_by_color": { + "0070C0": 1 + }, + "missing_minimums": { + "0070C0": { + "minimum": 2, + "actual": 1 + } + }, + "status": "failed" + } + } + } + ] + } + ] +} diff --git a/benchmark/baselines/issue159-colors-final-windows-fixtures/sparse_cf.xlsx b/benchmark/baselines/issue159-colors-final-windows-fixtures/sparse_cf.xlsx new file mode 100644 index 0000000..a512a20 Binary files /dev/null and b/benchmark/baselines/issue159-colors-final-windows-fixtures/sparse_cf.xlsx differ diff --git a/benchmark/baselines/issue159-colors-final-windows-fixtures/static.xlsx b/benchmark/baselines/issue159-colors-final-windows-fixtures/static.xlsx new file mode 100644 index 0000000..4557b4a Binary files /dev/null and b/benchmark/baselines/issue159-colors-final-windows-fixtures/static.xlsx differ diff --git a/benchmark/baselines/issue159-colors-final-windows.json b/benchmark/baselines/issue159-colors-final-windows.json new file mode 100644 index 0000000..8f8bd5d --- /dev/null +++ b/benchmark/baselines/issue159-colors-final-windows.json @@ -0,0 +1,2285 @@ +{ + "schema_version": 1, + "recorded_at": "2026-10-04T00:55:07.179742+00:00", + "platform": "Windows-10-10.0.22631-SP0", + "python": "3.11.13 (main, Jul 23 2025, 00:29:09) [MSC v.1944 64 bit (AMD64)]", + "xlwings": "0.37.4", + "excel_version": "16.0", + "configuration": { + "rows": 300, + "columns": 20, + "cases": [ + "static", + "sparse_cf" + ], + "repeats": 3, + "execution": "sequential; one owned Excel application; one workbook open at a time", + "include_default_background_values": [ + false, + true + ], + "ignore_color_cases": [ + null, + [ + "FF0000" + ] + ] + }, + "fixtures": [ + { + "name": "static", + "description": "rows x 20 values with deterministic direct fills", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-final-windows-fixtures\\static.xlsx", + "sha256": "37618f7749b56f642fb7f54ba328ff88bd6753837dcae7f93c91dc904e4ab7a8", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [], + "expected_conditional_colors_min_cells": null + }, + { + "name": "sparse_cf", + "description": "conditional formatting with sparse matches", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-final-windows-fixtures\\sparse_cf.xlsx", + "sha256": "28163049d08e96b276f65b9f3f06e0f7e7d7e1178afd23173e34a9c63df204dd", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "one formula rule limited to D2:D31; every other row matches" + ], + "expected_conditional_colors_min_cells": { + "FF0000": 1 + } + } + ], + "mixed_range_probe": null, + "measurements": [ + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 16503.155200043693, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 107.18060005456209, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=75.923 com_duration_ms=0.007 duration_ms=79.958", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 81.48489985615015, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=49.978 com_duration_ms=0.009 duration_ms=66.754", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 16913.41909999028, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 12743.967899819836, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 100.36540008150041, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=83.971 com_duration_ms=0.007 duration_ms=90.092", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16736.52349994518, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 64.91439999081194, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=52.350 com_duration_ms=0.006 duration_ms=57.148", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 109.03079994022846, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=81.631 com_duration_ms=0.005 duration_ms=99.769", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 13976.39500000514, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 15497.573500033468, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 86.1332998611033, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=60.371 com_duration_ms=0.012 duration_ms=74.181", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 14513.477700063959, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 89.1807998996228, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=74.569 com_duration_ms=0.006 duration_ms=80.910", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 104.84570008702576, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=48.639 com_duration_ms=0.006 duration_ms=89.997", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 17343.03279989399, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 15774.80970020406, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 71.39969989657402, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=51.622 com_duration_ms=0.007 duration_ms=60.083", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 18669.322500005364, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 67.10759992711246, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=50.135 com_duration_ms=0.007 duration_ms=56.965", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 102.37039998173714, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=80.664 com_duration_ms=0.007 duration_ms=91.257", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 2, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 22554.626200115308, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 13526.776699814945, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 84.27749993279576, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=46.991 com_duration_ms=0.006 duration_ms=76.195", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "conditional_render_validation": null, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 18074.974899878725, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "74e92cd02d78cc0e63fe83d800d11ecef34727d396573c7cf01b83e906f78b61", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 15 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 113.81270014680922, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=50.570 com_duration_ms=50.464 duration_ms=105.100", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "74e92cd02d78cc0e63fe83d800d11ecef34727d396573c7cf01b83e906f78b61", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 15 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 144.180899951607, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=75.603 com_duration_ms=56.717 duration_ms=136.732", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "74e92cd02d78cc0e63fe83d800d11ecef34727d396573c7cf01b83e906f78b61", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 15 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 17043.732200050727, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "74e92cd02d78cc0e63fe83d800d11ecef34727d396573c7cf01b83e906f78b61", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 15 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 14675.313299987465, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "74e92cd02d78cc0e63fe83d800d11ecef34727d396573c7cf01b83e906f78b61", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 15 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 146.1851999629289, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=48.509 com_duration_ms=84.610 duration_ms=137.700", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "74e92cd02d78cc0e63fe83d800d11ecef34727d396573c7cf01b83e906f78b61", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 15 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 14959.781599929556, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 200.8710999507457, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=83.125 com_duration_ms=99.768 duration_ms=190.437", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 143.82410002872348, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=50.894 com_duration_ms=79.428 duration_ms=134.351", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16796.248599886894, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16895.05139994435, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 346.3653000071645, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=79.819 com_duration_ms=254.248 duration_ms=339.180", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 15167.180000105873, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "baf78ce390704828c6eb77741fe084e4433ecd1caf3cee5840310aad31eaad46", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 143.92509986646473, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=50.559 com_duration_ms=75.627 duration_ms=133.922", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "baf78ce390704828c6eb77741fe084e4433ecd1caf3cee5840310aad31eaad46", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 187.53170012496412, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=51.468 com_duration_ms=113.922 duration_ms=176.471", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "baf78ce390704828c6eb77741fe084e4433ecd1caf3cee5840310aad31eaad46", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 12785.274700028822, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "baf78ce390704828c6eb77741fe084e4433ecd1caf3cee5840310aad31eaad46", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 12511.763999937102, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "baf78ce390704828c6eb77741fe084e4433ecd1caf3cee5840310aad31eaad46", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 357.4225001502782, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=72.209 com_duration_ms=267.977 duration_ms=346.071", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "baf78ce390704828c6eb77741fe084e4433ecd1caf3cee5840310aad31eaad46", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16273.032400058582, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "147fad4c660ffdd9b6101ff52a9bd8ddef62b6fca6aec095134c66ef10d47180", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5985 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 207.5891001150012, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=78.130 com_duration_ms=106.156 duration_ms=196.856", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "147fad4c660ffdd9b6101ff52a9bd8ddef62b6fca6aec095134c66ef10d47180", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5985 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 356.77660000510514, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=54.762 com_duration_ms=264.894 duration_ms=339.751", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "147fad4c660ffdd9b6101ff52a9bd8ddef62b6fca6aec095134c66ef10d47180", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5985 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 2, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 15442.656800150871, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "147fad4c660ffdd9b6101ff52a9bd8ddef62b6fca6aec095134c66ef10d47180", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5985 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 14394.869999960065, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "147fad4c660ffdd9b6101ff52a9bd8ddef62b6fca6aec095134c66ef10d47180", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5985 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 3, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 138.64889997057617, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=47.334 com_duration_ms=77.251 duration_ms=130.327", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "147fad4c660ffdd9b6101ff52a9bd8ddef62b6fca6aec095134c66ef10d47180", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 5985 + }, + "conditional_render_validation": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "exact_workbook_colors_map_equal": true + } + ], + "comparisons": [ + { + "fixture": "static", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "static", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "static", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "static", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": null, + "hybrid": null + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + } + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + } + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + } + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + } + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [], + "checked_expected_colors": { + "FF0000": 1 + }, + "actual_cells_by_color": { + "FF0000": 15 + }, + "missing_minimums": {}, + "status": "passed" + } + } + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + }, + { + "repeat": 2, + "order": [ + "hybrid", + "reference" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + }, + { + "repeat": 3, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {}, + "conditional_render_validation": { + "reference": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + }, + "hybrid": { + "expected_min_cells_by_color": { + "FF0000": 1 + }, + "ignored_expected_colors": [ + "FF0000" + ], + "checked_expected_colors": {}, + "status": "skipped_all_expected_colors_ignored", + "actual_cells_by_color": {} + } + } + } + ] + } + ] +} diff --git a/benchmark/baselines/issue159-colors-windows-fixtures/dense_cf.xlsx b/benchmark/baselines/issue159-colors-windows-fixtures/dense_cf.xlsx new file mode 100644 index 0000000..6372a14 Binary files /dev/null and b/benchmark/baselines/issue159-colors-windows-fixtures/dense_cf.xlsx differ diff --git a/benchmark/baselines/issue159-colors-windows-fixtures/merged.xlsx b/benchmark/baselines/issue159-colors-windows-fixtures/merged.xlsx new file mode 100644 index 0000000..4f6ad7d Binary files /dev/null and b/benchmark/baselines/issue159-colors-windows-fixtures/merged.xlsx differ diff --git a/benchmark/baselines/issue159-colors-windows-fixtures/multiple_sheets.xlsx b/benchmark/baselines/issue159-colors-windows-fixtures/multiple_sheets.xlsx new file mode 100644 index 0000000..6dd9002 Binary files /dev/null and b/benchmark/baselines/issue159-colors-windows-fixtures/multiple_sheets.xlsx differ diff --git a/benchmark/baselines/issue159-colors-windows-fixtures/overlapping_formula.xlsx b/benchmark/baselines/issue159-colors-windows-fixtures/overlapping_formula.xlsx new file mode 100644 index 0000000..c7c85a6 Binary files /dev/null and b/benchmark/baselines/issue159-colors-windows-fixtures/overlapping_formula.xlsx differ diff --git a/benchmark/baselines/issue159-colors-windows-fixtures/sparse_cf.xlsx b/benchmark/baselines/issue159-colors-windows-fixtures/sparse_cf.xlsx new file mode 100644 index 0000000..685e648 Binary files /dev/null and b/benchmark/baselines/issue159-colors-windows-fixtures/sparse_cf.xlsx differ diff --git a/benchmark/baselines/issue159-colors-windows-fixtures/static.xlsx b/benchmark/baselines/issue159-colors-windows-fixtures/static.xlsx new file mode 100644 index 0000000..5ce7e10 Binary files /dev/null and b/benchmark/baselines/issue159-colors-windows-fixtures/static.xlsx differ diff --git a/benchmark/baselines/issue159-colors-windows-fixtures/theme_indexed.xlsx b/benchmark/baselines/issue159-colors-windows-fixtures/theme_indexed.xlsx new file mode 100644 index 0000000..ef2b360 Binary files /dev/null and b/benchmark/baselines/issue159-colors-windows-fixtures/theme_indexed.xlsx differ diff --git a/benchmark/baselines/issue159-colors-windows.json b/benchmark/baselines/issue159-colors-windows.json new file mode 100644 index 0000000..1abdf85 --- /dev/null +++ b/benchmark/baselines/issue159-colors-windows.json @@ -0,0 +1,2439 @@ +{ + "schema_version": 1, + "recorded_at": "2026-10-04T00:42:59.444181+00:00", + "platform": "Windows-10-10.0.22631-SP0", + "python": "3.11.13 (main, Jul 23 2025, 00:29:09) [MSC v.1944 64 bit (AMD64)]", + "xlwings": "0.37.4", + "excel_version": "16.0", + "configuration": { + "rows": 300, + "columns": 20, + "cases": [ + "static", + "sparse_cf", + "dense_cf", + "overlapping_formula", + "theme_indexed", + "merged", + "multiple_sheets" + ], + "repeats": 1, + "execution": "sequential; one owned Excel application; one workbook open at a time", + "include_default_background_values": [ + false, + true + ], + "ignore_color_cases": [ + null, + [ + "FF0000" + ] + ] + }, + "fixtures": [ + { + "name": "static", + "description": "rows x 20 values with deterministic direct fills", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-windows-fixtures\\static.xlsx", + "sha256": "731b52e521f6621f48df01a22a8ea389f1d2df8d32ba26547d8ea54cd1d7b50e", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [] + }, + { + "name": "sparse_cf", + "description": "conditional formatting with sparse matches", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-windows-fixtures\\sparse_cf.xlsx", + "sha256": "76b34a7afc4f7a253d6613a5198e3512d27d26916f1c043e2cf67e0b8fc54e61", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "one formula rule limited to D2:D31; every other row matches" + ] + }, + { + "name": "dense_cf", + "description": "conditional formatting with dense alternating matches", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-windows-fixtures\\dense_cf.xlsx", + "sha256": "9e1e2614c25b705aa64773b425e59b1a0f1d86f6a1ecba2f0a3c462540b173aa", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "one formula rule across the grid; alternating cells match" + ] + }, + { + "name": "overlapping_formula", + "description": "overlapping CF rules, formulas, and mixed-range probe", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-windows-fixtures\\overlapping_formula.xlsx", + "sha256": "c7cbc11d2a996e2d5d1a5c7bcf6dd16bcb54b3e5d2b5335005676013d69ed643", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [ + "three overlapping formula rules across the grid", + "two stop-if-true rules on A1:B1 for the mixed-range probe" + ] + }, + { + "name": "theme_indexed", + "description": "theme and indexed direct fills", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-windows-fixtures\\theme_indexed.xlsx", + "sha256": "c3e3b30ddbb883f471fc3a7c7ae2f85076eb769a3ae0cc5a058f8d385a221ce0", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [] + }, + { + "name": "merged", + "description": "periodic horizontal merged ranges", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-windows-fixtures\\merged.xlsx", + "sha256": "3e6805ac579b8cbba934b60b1e555f1d16779ed7725ed0c29e368f0ac1a11e8a", + "rows": 300, + "columns": 20, + "sheets": 1, + "conditional_formatting": [] + }, + { + "name": "multiple_sheets", + "description": "eight populated worksheets capped at 30 rows each", + "path": "C:\\Users\\HARUMI\\orca\\workspaces\\exstruct\\waspfish\\benchmark\\baselines\\issue159-colors-windows-fixtures\\multiple_sheets.xlsx", + "sha256": "386eada52f47f5cba25a4b416441304fb083eac320aaf83e5d5b13ed535c18c1", + "rows": 30, + "columns": 20, + "sheets": 8, + "conditional_formatting": [ + "eight populated worksheets of 30x20; direct fills vary by sheet" + ] + } + ], + "mixed_range_probe": { + "sheet": "Data", + "range": "A1:B1", + "range_value": { + "value": 16777215, + "repr": "16777215.0", + "type": "float" + }, + "per_cell_values": [ + { + "address": "A1", + "value": 16777215, + "repr": "16777215.0", + "type": "float" + }, + { + "address": "B1", + "value": 16777215, + "repr": "16777215.0", + "type": "float" + } + ], + "per_cell_colors_differ": false, + "range_matches_each_cell": true + }, + "measurements": [ + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 14079.387000063434, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 94.03100004419684, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=70.511 com_duration_ms=0.013 duration_ms=77.157", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "7584703c20e2c30833c84bda29ef0c5455bbd67f63cbb04bb31ed40e0ea17207", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 332 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 15490.358399925753, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 106.12480016425252, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=89.240 com_duration_ms=0.006 duration_ms=95.265", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "0190ef0b7b83b5bfaf58af917a67d4cf2e987fbc8f056b4a9b1f39b58989544a", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 72 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 14323.788000037894, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 90.71070002391934, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=75.051 com_duration_ms=0.002 duration_ms=81.798", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "4112bdb9e485737dded7213ce32cbba7a6715757ff4b0e457e3ae63ec52341f9", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16323.644599877298, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "static", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 80.34329977817833, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=47.864 com_duration_ms=0.002 duration_ms=53.609", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "84e6180bd73c23f8b0b92a553c8e00f1b4f96eab9ca3d482c2a457d67fe92cd8", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 5740 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 32688.99290007539, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 194.2650000564754, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=96.697 com_duration_ms=78.087 duration_ms=181.850", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16113.732900004834, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 133.84429994039237, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=48.429 com_duration_ms=69.993 duration_ms=125.390", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 18061.446599895135, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 147.25310006178916, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=47.416 com_duration_ms=86.168 duration_ms=139.691", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 13716.901599895209, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "sparse_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 182.9858000855893, + "wrapper_display_format_calls": 30, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=30 rendered_candidates=30 display_format_calls=30 fallback=False static_duration_ms=77.174 com_duration_ms=86.816 duration_ms=171.947", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "30", + "display_format_calls": "30" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 17496.984200086445, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 16603.723799809813, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=6000 rendered_candidates=6000 display_format_calls=6000 fallback=False static_duration_ms=44.738 com_duration_ms=16542.229 duration_ms=16592.996", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "6000", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 14320.051100105047, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 18127.155999885872, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=6000 rendered_candidates=6000 display_format_calls=6000 fallback=False static_duration_ms=45.633 com_duration_ms=18036.550 duration_ms=18104.611", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "6000", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 14940.329100005329, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 17659.17360014282, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=6000 rendered_candidates=6000 display_format_calls=6000 fallback=False static_duration_ms=96.348 com_duration_ms=17536.481 duration_ms=17643.321", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "6000", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16102.809800067917, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "dense_cf", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 15038.505599834025, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=6000 rendered_candidates=6000 display_format_calls=6000 fallback=False static_duration_ms=75.080 com_duration_ms=14942.462 duration_ms=15025.341", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "6000", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 16296.886699972674, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 14760.374000063166, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=0.003 com_duration_ms=14748.299 duration_ms=14751.514", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 17023.95320008509, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 14284.040599828586, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=0.004 com_duration_ms=14259.531 duration_ms=14265.322", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "f96b1a6e800226ad604be38bb06d34b5e89abb58e386bcb7c84128b55c759dd8", + "map_summary": { + "sheets": 1, + "color_keys": 0, + "mapped_cells": 0 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 16428.23820002377, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 15770.019800169393, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=0.003 com_duration_ms=15758.603 duration_ms=15762.547", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 15078.64879979752, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "overlapping_formula", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16612.502600066364, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=workbook has unsaved changes", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=0.009 com_duration_ms=16593.213 duration_ms=16601.630", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "433a798d4a03196bc23ee56413954224684109b07258b3e32f1f774b998ceb5c", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 13468.724699923769, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "721f86be0666beeb2f1610e80ae21b543a13ba8ffe99d6b0f52b945bf708430f", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 649 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 1828.0599999707192, + "wrapper_display_format_calls": 649, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=649 display_format_calls=649 fallback=False static_duration_ms=75.060 com_duration_ms=1737.545 duration_ms=1817.410", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "649" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "721f86be0666beeb2f1610e80ae21b543a13ba8ffe99d6b0f52b945bf708430f", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 649 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 14599.047400057316, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "721f86be0666beeb2f1610e80ae21b543a13ba8ffe99d6b0f52b945bf708430f", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 649 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 1353.3924999646842, + "wrapper_display_format_calls": 649, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=649 display_format_calls=649 fallback=False static_duration_ms=49.244 com_duration_ms=1292.077 duration_ms=1346.291", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "649" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "721f86be0666beeb2f1610e80ae21b543a13ba8ffe99d6b0f52b945bf708430f", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 649 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 13166.96529998444, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "a17b573bb494357a48832937b4a12390320ce02ca64ab157a11691c6ee110138", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 1611.6424000356346, + "wrapper_display_format_calls": 649, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=649 display_format_calls=649 fallback=False static_duration_ms=71.865 com_duration_ms=1525.866 duration_ms=1603.824", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "649" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "a17b573bb494357a48832937b4a12390320ce02ca64ab157a11691c6ee110138", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 15597.586600109935, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "a17b573bb494357a48832937b4a12390320ce02ca64ab157a11691c6ee110138", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "theme_indexed", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 2321.8650000635535, + "wrapper_display_format_calls": 649, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=649 display_format_calls=649 fallback=False static_duration_ms=80.889 com_duration_ms=2224.911 duration_ms=2312.645", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "649" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "a17b573bb494357a48832937b4a12390320ce02ca64ab157a11691c6ee110138", + "map_summary": { + "sheets": 1, + "color_keys": 3, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 17148.318300023675, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "be068fcfb4b1723c731f37828ba6122a2505e28fa8b44c0976ab1f03c3beddf9", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 10 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 14894.198899855837, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=merged cells or table styles require rendered scan", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=40.721 com_duration_ms=14839.642 duration_ms=14884.787", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "be068fcfb4b1723c731f37828ba6122a2505e28fa8b44c0976ab1f03c3beddf9", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 10 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 16070.427200058475, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "be068fcfb4b1723c731f37828ba6122a2505e28fa8b44c0976ab1f03c3beddf9", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 10 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 14390.627100132406, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=merged cells or table styles require rendered scan", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=78.688 com_duration_ms=14300.648 duration_ms=14383.546", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "be068fcfb4b1723c731f37828ba6122a2505e28fa8b44c0976ab1f03c3beddf9", + "map_summary": { + "sheets": 1, + "color_keys": 1, + "mapped_cells": 10 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 16713.635900057852, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "c66253c14bfe7b648b2b8eacc1c0ba90e4e821c6dd4240c90c1e54eab2bd1105", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 13143.098900094628, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=merged cells or table styles require rendered scan", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=40.504 com_duration_ms=13092.154 duration_ms=13135.531", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "c66253c14bfe7b648b2b8eacc1c0ba90e4e821c6dd4240c90c1e54eab2bd1105", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 17275.565000018105, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 6000, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "c66253c14bfe7b648b2b8eacc1c0ba90e4e821c6dd4240c90c1e54eab2bd1105", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "merged", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 17052.96830018051, + "wrapper_display_format_calls": 6000, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction legacy fallback sheet=Data reason=merged cells or table styles require rendered scan", + "metric_fields": {}, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Data used_cells=6000 conditional_candidates=0 rendered_candidates=0 display_format_calls=6000 fallback=True static_duration_ms=92.096 com_duration_ms=16946.023 duration_ms=17041.931", + "metric_fields": { + "used_cells": "6000", + "conditional_candidates": "0", + "display_format_calls": "6000" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "c66253c14bfe7b648b2b8eacc1c0ba90e4e821c6dd4240c90c1e54eab2bd1105", + "map_summary": { + "sheets": 1, + "color_keys": 2, + "mapped_cells": 6000 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 10966.043799882755, + "wrapper_display_format_calls": 4800, + "cells_context_display_format_calls": 4800, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "ad829a3efe58032ee65ecfd3b81d5f7f67ab74ef28db19c04af7a36269dec95d", + "map_summary": { + "sheets": 8, + "color_keys": 8, + "mapped_cells": 160 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": null, + "elapsed_ms": 109.44360005669296, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet01 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=29.553 com_duration_ms=0.001 duration_ms=33.324", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet02 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.490 com_duration_ms=0.002 duration_ms=6.266", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet03 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=3.073 com_duration_ms=0.001 duration_ms=6.280", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet04 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.442 com_duration_ms=0.002 duration_ms=5.887", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet05 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.368 com_duration_ms=0.002 duration_ms=6.108", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet06 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.290 com_duration_ms=0.001 duration_ms=5.220", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet07 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.416 com_duration_ms=0.001 duration_ms=6.020", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet08 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.881 com_duration_ms=0.002 duration_ms=6.022", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "ad829a3efe58032ee65ecfd3b81d5f7f67ab74ef28db19c04af7a36269dec95d", + "map_summary": { + "sheets": 8, + "color_keys": 8, + "mapped_cells": 160 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "reference", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 10136.254499899223, + "wrapper_display_format_calls": 4800, + "cells_context_display_format_calls": 4800, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "2bf9f6d04a55012411f7406f6e798873fc9cf40131a301c7891dbdc88dbb5cc5", + "map_summary": { + "sheets": 8, + "color_keys": 4, + "mapped_cells": 80 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 174.00730005465448, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet01 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=30.385 com_duration_ms=0.003 duration_ms=38.678", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet02 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.930 com_duration_ms=0.002 duration_ms=6.949", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet03 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.619 com_duration_ms=0.001 duration_ms=7.550", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet04 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.621 com_duration_ms=0.002 duration_ms=7.703", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet05 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.831 com_duration_ms=0.002 duration_ms=7.138", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet06 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.525 com_duration_ms=0.001 duration_ms=6.605", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet07 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.645 com_duration_ms=0.002 duration_ms=12.835", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet08 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=34.269 com_duration_ms=0.003 duration_ms=38.937", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "2bf9f6d04a55012411f7406f6e798873fc9cf40131a301c7891dbdc88dbb5cc5", + "map_summary": { + "sheets": 8, + "color_keys": 4, + "mapped_cells": 80 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 13358.154699904844, + "wrapper_display_format_calls": 4800, + "cells_context_display_format_calls": 4800, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "e6e255d0d2d0eb6231249807224638c0eabb31cb0b71495e7cd9040521282ee5", + "map_summary": { + "sheets": 8, + "color_keys": 16, + "mapped_cells": 4800 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": null, + "elapsed_ms": 135.36630012094975, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet01 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=28.274 com_duration_ms=0.001 duration_ms=32.195", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet02 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.941 com_duration_ms=0.002 duration_ms=6.784", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet03 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.373 com_duration_ms=0.001 duration_ms=5.792", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet04 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.502 com_duration_ms=0.001 duration_ms=7.081", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet05 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.593 com_duration_ms=0.002 duration_ms=6.741", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet06 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.419 com_duration_ms=0.001 duration_ms=10.490", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet07 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=4.332 com_duration_ms=0.003 duration_ms=10.304", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet08 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=3.211 com_duration_ms=0.002 duration_ms=8.994", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "e6e255d0d2d0eb6231249807224638c0eabb31cb0b71495e7cd9040521282ee5", + "map_summary": { + "sheets": 8, + "color_keys": 16, + "mapped_cells": 4800 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "reference", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 13227.223699912429, + "wrapper_display_format_calls": 4800, + "cells_context_display_format_calls": 4800, + "hybrid_debug_records": [], + "error": null, + "map_sha256": "c509c398e9a42a3db6ba0b767962082bc6c5de80a91bc446485bec6faab4684d", + "map_summary": { + "sheets": 8, + "color_keys": 12, + "mapped_cells": 4720 + }, + "exact_workbook_colors_map_equal": true + }, + { + "fixture": "multiple_sheets", + "repeat": 1, + "strategy": "hybrid", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "elapsed_ms": 136.51910005137324, + "wrapper_display_format_calls": 0, + "cells_context_display_format_calls": 0, + "hybrid_debug_records": [ + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet01 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=28.848 com_duration_ms=0.002 duration_ms=35.129", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet02 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.635 com_duration_ms=0.003 duration_ms=7.878", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet03 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.770 com_duration_ms=0.002 duration_ms=8.535", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet04 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.563 com_duration_ms=0.002 duration_ms=7.748", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet05 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.445 com_duration_ms=0.002 duration_ms=7.617", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet06 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.491 com_duration_ms=0.002 duration_ms=6.840", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet07 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=2.491 com_duration_ms=0.003 duration_ms=6.926", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + }, + { + "logger": "exstruct.core.color_hybrid", + "level": "DEBUG", + "message": "Color extraction sheet=Sheet08 used_cells=600 conditional_candidates=0 rendered_candidates=0 display_format_calls=0 fallback=False static_duration_ms=3.224 com_duration_ms=0.003 duration_ms=10.781", + "metric_fields": { + "used_cells": "600", + "conditional_candidates": "0", + "display_format_calls": "0" + }, + "extra": {} + } + ], + "error": null, + "map_sha256": "c509c398e9a42a3db6ba0b767962082bc6c5de80a91bc446485bec6faab4684d", + "map_summary": { + "sheets": 8, + "color_keys": 12, + "mapped_cells": 4720 + }, + "exact_workbook_colors_map_equal": true + } + ], + "comparisons": [ + { + "fixture": "static", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "static", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "static", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "static", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "sparse_cf", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "dense_cf", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "overlapping_formula", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "theme_indexed", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "theme_indexed", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "theme_indexed", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "theme_indexed", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "merged", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "merged", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "merged", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "merged", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "multiple_sheets", + "include_default_background": false, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "multiple_sheets", + "include_default_background": false, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "multiple_sheets", + "include_default_background": true, + "ignore_colors": null, + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + }, + { + "fixture": "multiple_sheets", + "include_default_background": true, + "ignore_colors": [ + "FF0000" + ], + "repeats": [ + { + "repeat": 1, + "order": [ + "reference", + "hybrid" + ], + "exact_workbook_colors_map_equal": true, + "errors": {} + } + ] + } + ] +} diff --git a/benchmark/issue159-results.md b/benchmark/issue159-results.md new file mode 100644 index 0000000..7f3cf10 --- /dev/null +++ b/benchmark/issue159-results.md @@ -0,0 +1,156 @@ +# Issue #159: COM color extraction benchmark + +## Method and reproduction + +This benchmark compares `cells.extract_sheet_colors_map_com(...)` with the +previous manual path: workbook preparation, preparation of each sheet, and +`_extract_sheet_colors_com(...)`. Both paths use the same already-open Excel +workbook and the same `include_default_background` / `ignore_colors` inputs. +Every pair checks exact `WorkbookColorsMap` equality. Timings cover only the +extraction call and its required display-format preparation. + +The runner generates and retains deterministic-shape `.xlsx` fixtures for a +static 300 x 20 grid, sparse conditional formatting limited to 30 cells, dense +conditional formatting, overlapping conditional rules with formulas, +theme/indexed fills, merged cells, and eight populated sheets capped at 30 rows +each. It runs sequentially in one owned, +hidden Excel application, opens one workbook at a time, alternates reference +and hybrid order across repeats, and checks that the Excel process is closed at +the end. Each fixture's SHA-256, each sample, the per-call wrapper counts, the +hybrid logger's DEBUG records, and exact comparison outcomes are retained in +the output JSON. The run covers both values of `include_default_background` +and both `ignore_colors=None` and `ignore_colors={"FF0000"}`. + +On Windows, close other Excel instances, then run the benchmark by itself. Do +not overlap it with Excel tests or another COM benchmark. + +```powershell +rtk uv run python -m benchmark.issue159_colors --output benchmark/baselines/issue159-colors-windows.json --repeats 3 --rows 300 +``` + +For an initial short run, select the static and sparse-range cases: + +```powershell +rtk uv run python -m benchmark.issue159_colors --output benchmark/baselines/issue159-colors-smoke-windows.json --repeats 1 --rows 300 --cases static sparse_cf +``` + +By default the generated workbooks are kept in a sibling directory named +`issue159-colors-windows-fixtures`. Pass `--fixtures-dir PATH` to choose another +location. Use `--cases` with one or more fixture names to run a subset. The +eight-sheet case uses `min(--rows, 30)` rows per sheet to bound its COM cost. +The script prints flushed fixture, configuration, repeat, and strategy +progress, and writes results as each configuration finishes so an interrupted +run retains completed comparisons. +It also probes `Range("A1:B1").DisplayFormat.Interior.Color` in the overlapping +fixture and records that value beside the two individual cell values. +The probe also records the workbook's COM `Saved` flag immediately before and +after its calculation call. + +The wrapper count measures calls through +`cells._get_display_format_color` for both implementations. Legacy measurements +also retain the existing ContextVar counter when available. Historical hybrid +records contain a false zero in `cells_context_display_format_calls`: the +hybrid extractor resets that per-sheet counter, so the final value is not an +aggregate. Do not use that field in existing raw JSON; the historical data is +left unchanged. `wrapper_display_format_calls` and captured hybrid DEBUG +`display_format_calls` remain the authoritative counts and are unchanged. New +hybrid records omit `cells_context_display_format_calls`. Hybrid DEBUG records +are captured verbatim with structured extras and parsed metric fields, +including `used_cells`, `conditional_candidates`, `display_format_calls`, and +any logged fallback duration fields. + +The valid final CF fixtures write solid-fill `fgColor` and `bgColor` as the +same color, use OOXML-standard `FormulaRule` expressions without a leading `=`, +and keep Excel `ScreenUpdating` enabled during the runtime measurements. + +## Results + +### Initial 300-row run: valid non-CF cases + +Source: [`issue159-colors-windows.json`](baselines/issue159-colors-windows.json), +recorded 2026-10-04 on Windows 10 build 22631, Python 3.11.13, Excel 16.0, +xlwings 0.37.4. It ran one sample for each of four configurations (both +`include_default_background` values, with and without ignored red). Times below +are the min–max across those four samples, **not medians**. All four exact map +comparisons passed for each listed fixture. + +| Fixture | Shape | Reference time (ms) | Hybrid time (ms) | `_get_display_format_color` calls (reference / hybrid) | Exact maps | +| --- | --- | ---: | ---: | ---: | --- | +| Static fills | 300 x 20 | 14,079.4–16,323.6 | 80.3–106.1 | 6,000 / 0 | 4/4 | +| Theme/indexed fills | 300 x 20 | 13,167.0–15,597.6 | 1,353.4–2,321.9 | 6,000 / 649 | 4/4 | +| Merged cells | 300 x 20 | 16,070.4–17,275.6 | 13,143.1–17,053.0 | 6,000 / 6,000 | 4/4 | +| Multiple sheets | 8 sheets x 30 x 20 | 10,136.3–13,358.2 | 109.4–174.0 | 4,800 / 0 | 4/4 | + +The eight-sheet input is capped at 30 rows per sheet. The initial run's +sparse-CF, dense-CF, and overlapping-CF results are invalid for color-rendering +evidence: their saved differential-format `bgColor` did not match `fgColor`, +the `FormulaRule` formulas had a leading `=`, and the default-false maps +contained zero cells. A formula-only rerun removed the leading `=` but still +rendered no CF colors ([`issue159-colors-final-smoke-windows.json`](baselines/issue159-colors-final-smoke-windows.json)); +that intermediate run is also excluded. Passing fixtures match `bgColor` to +`fgColor`, use the standard formula form, and run with `ScreenUpdating` enabled. +Earlier samples remain in raw JSON but are excluded from timing and equality +conclusions; equality of two empty maps does not establish CF correctness. + +### Corrected CF rendering probe: 10 rows + +Source: [`issue159-colors-cf-probe-windows.json`](baselines/issue159-colors-cf-probe-windows.json), +Excel 16.0, one repeat, all four configurations per fixture. The corrected +OOXML formulas omit the leading `=`. Each exact comparison passed and the +expected real colors were observed wherever they were not intentionally +ignored. + +| Fixture | Rendered color evidence | Exact maps | Hybrid path and wrapper calls | Recorded time (reference / hybrid, ms) | +| --- | --- | --- | --- | ---: | +| Sparse CF, `D2:D10` | 5 red cells; ignored-red cases correctly omit red | 4/4 | Optimized path, no fallback; 200 / 9 | 344.2–906.3 / 33.2–38.2 | +| Dense CF, `A1:T10` | 100 blue cells in all four configurations | 4/4 | Optimized path, no fallback; 200 / 200 | 362.0–928.3 / 423.1–625.6 | +| Overlapping/formula CF, `A1:T10` | 21 red and 37 blue cells; ignoring red leaves 37 blue | 4/4 | Conservative legacy fallback in all configurations; 200 / 200 | 433.3–625.7 / 407.1–894.8 | + +This small probe confirms rendered CF color visibility, not 300-row performance. +For the overlap fixture, the workbook's COM `Saved` flag changed from `True` +before calculation to `False` afterward. The hybrid path detected the unsaved +workbook state and used the legacy fallback; these samples therefore validate +fallback correctness and must not be presented as optimized-path timings. + +The mixed-range probe returned scalar `0` for +`Range("A1:B1").DisplayFormat.Interior.Color`, while the individual cells +returned `255` and `12611584`. The cell colors differ, and the range result +matches neither as a matrix; this confirms that a mixed `Range.DisplayFormat` +color read cannot supply per-cell color values. Microsoft documents +`Range.DisplayFormat` as a read-only display-format object affected by +conditional formatting ([Microsoft Learn](https://learn.microsoft.com/en-us/office/vba/api/excel.range.displayformat)); +the scalar result and distinct per-cell values above are the direct Excel +runtime evidence. + +### Final-source 300-row medians + +Source: [`issue159-colors-final-windows.json`](baselines/issue159-colors-final-windows.json), +recorded 2026-10-04 00:55 UTC. This run contains 48 measurements across static +and sparse-CF fixtures: three sequential repeats for each of four configurations +per fixture. All 24 reference/hybrid map comparisons are exact, and all +non-ignored CF visibility checks passed. + +| Fixture | Include default | Ignored colors | Reference median (ms) | Hybrid median (ms) | Wrapper calls (reference / hybrid) | CF visibility / map | +| --- | --- | --- | ---: | ---: | ---: | --- | +| Static | False | None | 16,503.2 | 100.4 | 6,000 / 0 | Exact | +| Static | False | `FF0000` | 15,497.6 | 86.1 | 6,000 / 0 | Exact | +| Static | True | None | 15,774.8 | 89.2 | 6,000 / 0 | Exact | +| Static | True | `FF0000` | 18,669.3 | 84.3 | 6,000 / 0 | Exact | +| Sparse CF (`D2:D31`) | False | None | 17,043.7 | 144.2 | 6,000 / 30 | 15 red cells; passed | +| Sparse CF (`D2:D31`) | False | `FF0000` | 16,796.2 | 200.9 | 6,000 / 30 | Red intentionally filtered; exact | +| Sparse CF (`D2:D31`) | True | None | 12,785.3 | 187.5 | 6,000 / 30 | 15 red cells; passed | +| Sparse CF (`D2:D31`) | True | `FF0000` | 15,442.7 | 207.6 | 6,000 / 30 | Red intentionally filtered; exact | + +Main reports the full test suite at 1,132 passed, including 28 new regressions +for merged/table/row/column fallback and auto/gradient candidates. Ruff and +mypy for 90 source files also passed. + +## Interpretation + +Final-source 300-row medians show exact output equality for static and sparse +CF across all four configurations. The hybrid path makes 30 wrapper calls for +the 30-cell sparse CF range, versus 6,000 calls for the reference path, while +still returning the 15 rendered red cells when red is not ignored. Keep the +initial and formula-only CF samples excluded; report overlap-fixture timings +separately because its optimized path correctly falls back when Excel marks the +workbook unsaved after calculation. diff --git a/benchmark/issue159_colors.py b/benchmark/issue159_colors.py new file mode 100644 index 0000000..f877d87 --- /dev/null +++ b/benchmark/issue159_colors.py @@ -0,0 +1,752 @@ +"""Sequential real-Excel benchmark for Issue #159 color extraction.""" + +from __future__ import annotations + +import argparse +from collections.abc import Iterator +from contextlib import ExitStack, contextmanager +from datetime import UTC, datetime +import hashlib +import json +import logging +import platform +from pathlib import Path +import re +import sys +from time import perf_counter +from typing import Any +from unittest.mock import patch + +from openpyxl import Workbook +from openpyxl.formatting.rule import FormulaRule +from openpyxl.styles import Color, PatternFill + +from exstruct.core import cells + +_COLUMNS = 20 +_MULTI_SHEET_COUNT = 8 +_FIXTURE_CASES = ( + "static", + "sparse_cf", + "dense_cf", + "overlapping_formula", + "theme_indexed", + "merged", + "multiple_sheets", +) +_IGNORE_RED = {"FF0000"} +_CF_RENDER_EXPECTATIONS: dict[str, dict[str, int]] = { + "sparse_cf": {"FF0000": 1}, + "dense_cf": {"0070C0": 1}, + # A1/B1 are direct-fill probe controls; require additional red and blue + # cells so those controls alone cannot make a broken CF fixture pass. + "overlapping_formula": {"FF0000": 2, "0070C0": 2}, +} +_FIXED_PROPERTIES_TIME = datetime(2020, 1, 1, tzinfo=UTC) +_METRIC_KEYS = ( + "used_cells", + "conditional_candidates", + "display_format_calls", + "rendered_candidates", + "fallback", + "static_duration_ms", + "com_duration_ms", + "duration_ms", + "fallback_duration", + "fallback_durations", + "fallback_duration_ms", + "fallback_durations_ms", + "fallback_ms", +) +_METRIC_PATTERN = re.compile( + r"\b(" + "|".join(re.escape(key) for key in _METRIC_KEYS) + r")\s*[=:]\s*([^,;\s]+)", + re.IGNORECASE, +) + + +class _DebugRecordCollector(logging.Handler): + """Keep the hybrid extractor's DEBUG records in the output JSON.""" + + def __init__(self) -> None: + super().__init__(logging.DEBUG) + self.records: list[dict[str, Any]] = [] + + def emit(self, record: logging.LogRecord) -> None: + standard_fields = set(logging.makeLogRecord({}).__dict__) + extras = { + key: _jsonable(value) + for key, value in record.__dict__.items() + if key not in standard_fields and key not in {"message", "asctime"} + } + message = record.getMessage() + fields = {key: value for key, value in extras.items() if key in _METRIC_KEYS} + fields.update({key: value for key, value in _METRIC_PATTERN.findall(message)}) + self.records.append( + { + "logger": record.name, + "level": record.levelname, + "message": message, + "metric_fields": fields, + "extra": extras, + } + ) + + +@contextmanager +def _capture_hybrid_debug() -> Iterator[_DebugRecordCollector]: + logger = logging.getLogger("exstruct.core.color_hybrid") + previous_level = logger.level + previous_propagate = logger.propagate + collector = _DebugRecordCollector() + logger.setLevel(logging.DEBUG) + logger.propagate = False + logger.addHandler(collector) + try: + yield collector + finally: + logger.removeHandler(collector) + logger.setLevel(previous_level) + logger.propagate = previous_propagate + + +@contextmanager +def _count_display_format_wrapper() -> Iterator[dict[str, int]]: + """Count calls through cells._get_display_format_color on either path.""" + original = cells._get_display_format_color + counter = {"calls": 0} + + def counted(sheet: object, row: int, col: int) -> int | None: + counter["calls"] += 1 + return original(sheet, row, col) # type: ignore[arg-type] + + with ExitStack() as stack: + stack.enter_context(patch.object(cells, "_get_display_format_color", counted)) + # The optimized module may cache the helper at import time. Patch that + # alias as well so both paths are measured at the same call boundary. + hybrid = sys.modules.get("exstruct.core.color_hybrid") + if hybrid is not None and getattr(hybrid, "_get_display_format_color", None) is original: + stack.enter_context( + patch.object(hybrid, "_get_display_format_color", counted) + ) + yield counter + + +def _jsonable(value: object) -> Any: + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, dict): + return {str(key): _jsonable(item) for key, item in value.items()} + if isinstance(value, (list, tuple, set)): + return [_jsonable(item) for item in value] + return repr(value) + + +def _populate_grid(sheet: Any, rows: int) -> None: + for row in range(1, rows + 1): + sheet.append([row * 100 + col for col in range(1, _COLUMNS + 1)]) + + +def _single_sheet_fixture(name: str, rows: int) -> tuple[Workbook, list[str]]: + workbook = Workbook() + sheet = workbook.active + sheet.title = "Data" + _populate_grid(sheet, rows) + red = PatternFill(fill_type="solid", fgColor="FFFF0000", bgColor="FFFF0000") + blue = PatternFill(fill_type="solid", fgColor="FF0070C0", bgColor="FF0070C0") + yellow = PatternFill(fill_type="solid", fgColor="FFFFFF00", bgColor="FFFFFF00") + rules: list[str] = [] + extent = f"A1:T{rows}" + + if name == "static": + for row in range(1, rows + 1): + for col in range(1, _COLUMNS + 1): + serial = (row - 1) * _COLUMNS + col + if serial % 23 == 0: + sheet.cell(row, col).fill = red + elif serial % 79 == 0: + sheet.cell(row, col).fill = yellow + elif name == "sparse_cf": + sparse_start = 2 if rows >= 2 else 1 + sparse_end = min(rows, sparse_start + 29) + sparse_extent = f"D{sparse_start}:D{sparse_end}" + sheet.conditional_formatting.add( + sparse_extent, + FormulaRule( + formula=["MOD(ROW(),2)=0"], + fill=red, + ), + ) + rules.append( + f"one formula rule limited to {sparse_extent}; every other row matches" + ) + elif name == "dense_cf": + sheet.conditional_formatting.add( + extent, + FormulaRule(formula=["MOD(ROW()+COLUMN(),2)=0"], fill=blue), + ) + rules.append("one formula rule across the grid; alternating cells match") + elif name == "overlapping_formula": + # Distinct saved fills make the range-vs-cell probe meaningful even + # if Excel does not evaluate a conditional-formatting rule. + sheet["A1"].fill = red + sheet["B1"].fill = blue + for row in range(2, rows + 1, 4): + sheet.cell(row, 1).value = f"=SUM(B{row}:C{row})" + sheet.conditional_formatting.add( + extent, + FormulaRule(formula=["MOD(ROW(),7)=0"], fill=red), + ) + sheet.conditional_formatting.add( + extent, + FormulaRule(formula=["MOD(COLUMN(),5)=0"], fill=blue), + ) + sheet.conditional_formatting.add( + extent, + FormulaRule(formula=["A1>1500"], fill=yellow), + ) + # Make A1 and B1 a deterministic mixed-color probe. These highest- + # priority rules stop the broader overlapping rules on those cells. + sheet.conditional_formatting.add( + "A1:B1", + FormulaRule(formula=["COLUMN()=1"], fill=red, stopIfTrue=True), + ) + sheet.conditional_formatting.add( + "A1:B1", + FormulaRule(formula=["COLUMN()=2"], fill=blue, stopIfTrue=True), + ) + rules.extend( + [ + "three overlapping formula rules across the grid", + "two stop-if-true rules on A1:B1 for the mixed-range probe", + ] + ) + elif name == "theme_indexed": + themed = PatternFill( + fill_type="solid", fgColor=Color(theme=4, tint=0.25) + ) + indexed = PatternFill(fill_type="solid", fgColor=Color(indexed=3)) + for row in range(1, rows + 1): + for col in range(1, _COLUMNS + 1): + serial = (row - 1) * _COLUMNS + col + if serial % 17 == 0: + sheet.cell(row, col).fill = themed + elif serial % 19 == 0: + sheet.cell(row, col).fill = indexed + elif name == "merged": + for row in range(1, rows + 1, 30): + sheet.merge_cells(start_row=row, start_column=1, end_row=row, end_column=2) + sheet.cell(row, 1).fill = blue + else: + raise ValueError(f"Unknown single-sheet fixture: {name}") + + return workbook, rules + + +def _multiple_sheet_fixture(rows: int) -> tuple[Workbook, list[str]]: + sheet_rows = min(rows, 30) + workbook = Workbook() + rules = [ + f"eight populated worksheets of {sheet_rows}x{_COLUMNS}; direct fills vary by sheet" + ] + for index in range(_MULTI_SHEET_COUNT): + sheet = workbook.active if index == 0 else workbook.create_sheet() + sheet.title = f"Sheet{index + 1:02d}" + _populate_grid(sheet, sheet_rows) + fill = PatternFill( + fill_type="solid", + fgColor=("FFFF0000" if index % 2 == 0 else "FF0070C0"), + ) + for row in range(1, sheet_rows + 1): + for col in range(1, _COLUMNS + 1): + if (row * 3 + col + index) % 31 == 0: + sheet.cell(row, col).fill = fill + return workbook, rules + + +def generate_fixtures( + directory: Path, rows: int, cases: tuple[str, ...] +) -> list[dict[str, Any]]: + directory.mkdir(parents=True, exist_ok=True) + fixture_specs = ( + ("static", "rows x 20 values with deterministic direct fills"), + ("sparse_cf", "conditional formatting with sparse matches"), + ("dense_cf", "conditional formatting with dense alternating matches"), + ("overlapping_formula", "overlapping CF rules, formulas, and mixed-range probe"), + ("theme_indexed", "theme and indexed direct fills"), + ("merged", "periodic horizontal merged ranges"), + ("multiple_sheets", "eight populated worksheets capped at 30 rows each"), + ) + fixtures: list[dict[str, Any]] = [] + for name, description in fixture_specs: + if name not in cases: + continue + if name == "multiple_sheets": + workbook, rules = _multiple_sheet_fixture(rows) + else: + workbook, rules = _single_sheet_fixture(name, rows) + workbook.properties.created = _FIXED_PROPERTIES_TIME + workbook.properties.modified = _FIXED_PROPERTIES_TIME + workbook.calculation.calcMode = "auto" + workbook.calculation.fullCalcOnLoad = True + workbook.calculation.forceFullCalc = True + path = directory / f"{name}.xlsx" + workbook.save(path) + workbook.close() + fixtures.append( + { + "name": name, + "description": description, + "path": str(path.resolve()), + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + "rows": min(rows, 30) if name == "multiple_sheets" else rows, + "columns": _COLUMNS, + "sheets": _MULTI_SHEET_COUNT if name == "multiple_sheets" else 1, + "conditional_formatting": rules, + "expected_conditional_colors_min_cells": _CF_RENDER_EXPECTATIONS.get( + name + ), + } + ) + return fixtures + + +def _reference_extract( + book: Any, include_default_background: bool, ignore_colors: set[str] | None +) -> Any: + cells._prepare_workbook_for_display_format(book) + sheets = {} + for sheet in book.sheets: + cells._prepare_sheet_for_display_format(sheet) + sheets[sheet.name] = cells._extract_sheet_colors_com( + sheet, include_default_background, ignore_colors + ) + return cells.WorkbookColorsMap(sheets=sheets) + + +def _map_fingerprint(color_map: Any) -> str: + normalized = { + sheet_name: { + color: [list(position) for position in positions] + for color, positions in sorted(sheet.colors_map.items()) + } + for sheet_name, sheet in sorted(color_map.sheets.items()) + } + encoded = json.dumps(normalized, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(encoded.encode("utf-8")).hexdigest() + + +def _map_summary(color_map: Any) -> dict[str, int]: + return { + "sheets": len(color_map.sheets), + "color_keys": sum(len(sheet.colors_map) for sheet in color_map.sheets.values()), + "mapped_cells": sum( + len(positions) + for sheet in color_map.sheets.values() + for positions in sheet.colors_map.values() + ), + } + + +def _conditional_render_validation( + fixture_name: str, + color_map: Any | None, + ignore_colors: set[str] | None, +) -> dict[str, Any] | None: + """Require visible CF colors so equal empty maps cannot pass correctness.""" + expectations = _CF_RENDER_EXPECTATIONS.get(fixture_name) + if expectations is None: + return None + ignored = { + color.strip().lstrip("#").upper()[-6:] for color in (ignore_colors or set()) + } + active = {color: minimum for color, minimum in expectations.items() if color not in ignored} + report: dict[str, Any] = { + "expected_min_cells_by_color": expectations, + "ignored_expected_colors": sorted(set(expectations) & ignored), + "checked_expected_colors": active, + } + if not active: + report["status"] = "skipped_all_expected_colors_ignored" + report["actual_cells_by_color"] = {} + return report + if color_map is None: + report["status"] = "unavailable" + report["actual_cells_by_color"] = {} + return report + + sheet = color_map.sheets.get("Data") + colors_map = sheet.colors_map if sheet is not None else {} + actual = {color: len(colors_map.get(color, [])) for color in active} + missing = { + color: {"minimum": minimum, "actual": actual[color]} + for color, minimum in active.items() + if actual[color] < minimum + } + report["actual_cells_by_color"] = actual + report["missing_minimums"] = missing + report["status"] = "failed" if missing else "passed" + return report + + +def _measure( + book: Any, + strategy: str, + include_default_background: bool, + ignore_colors: set[str] | None, + fixture_name: str, + repeat: int, +) -> tuple[dict[str, Any], Any | None]: + context_counter = getattr(cells, "_display_format_calls", None) + context_token = context_counter.set(0) if context_counter is not None else None + collector: _DebugRecordCollector | None = None + color_map = None + error = None + with _count_display_format_wrapper() as calls: + if strategy == "hybrid": + with _capture_hybrid_debug() as collector: + started = perf_counter() + try: + color_map = cells.extract_sheet_colors_map_com( + book, + include_default_background=include_default_background, + ignore_colors=ignore_colors, + ) + except Exception as exc: # Retain failures beside successful timings. + error = f"{type(exc).__name__}: {exc}" + elapsed_ms = (perf_counter() - started) * 1000 + else: + started = perf_counter() + try: + color_map = _reference_extract( + book, include_default_background, ignore_colors + ) + except Exception as exc: # Retain failures beside successful timings. + error = f"{type(exc).__name__}: {exc}" + elapsed_ms = (perf_counter() - started) * 1000 + context_calls = ( + context_counter.get() + if context_counter is not None and strategy != "hybrid" + else None + ) + if context_token is not None: + context_counter.reset(context_token) + + record: dict[str, Any] = { + "fixture": fixture_name, + "repeat": repeat, + "strategy": strategy, + "include_default_background": include_default_background, + "ignore_colors": sorted(ignore_colors) if ignore_colors else None, + "elapsed_ms": elapsed_ms, + "wrapper_display_format_calls": calls["calls"], + "hybrid_debug_records": collector.records if collector is not None else [], + "error": error, + } + if strategy != "hybrid": + record["cells_context_display_format_calls"] = context_calls + if color_map is not None: + record["map_sha256"] = _map_fingerprint(color_map) + record["map_summary"] = _map_summary(color_map) + record["conditional_render_validation"] = _conditional_render_validation( + fixture_name, color_map, ignore_colors + ) + return record, color_map + + +def _probe_mixed_range(book: Any) -> dict[str, Any]: + sheet = book.sheets["Data"] + saved_before = bool(book.api.Saved) + sheet.api.Activate() + sheet.api.Calculate() + result: dict[str, Any] = { + "sheet": sheet.name, + "range": "A1:B1", + "workbook_saved_flag_before_calculate": saved_before, + "workbook_saved_flag_after_calculate": bool(book.api.Saved), + } + per_cell: list[dict[str, Any]] = [] + for address in ("A1", "B1"): + try: + value = sheet.api.Range(address).DisplayFormat.Interior.Color + per_cell.append( + { + "address": address, + "value": int(value), + "repr": repr(value), + "type": type(value).__name__, + } + ) + except Exception as exc: + per_cell.append({"address": address, "error": f"{type(exc).__name__}: {exc}"}) + try: + value = sheet.api.Range("A1:B1").DisplayFormat.Interior.Color + result["range_value"] = { + "value": int(value), + "repr": repr(value), + "type": type(value).__name__, + } + except Exception as exc: + result["range_value"] = {"error": f"{type(exc).__name__}: {exc}"} + result["per_cell_values"] = per_cell + values = [cell.get("value") for cell in per_cell] + result["per_cell_colors_differ"] = ( + len(values) == 2 and None not in values and values[0] != values[1] + ) + range_value = result["range_value"].get("value") + result["range_matches_each_cell"] = ( + range_value is not None and len(values) == 2 and values == [range_value, range_value] + ) + return result + + +def _write_output(path: Path, result: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(json.dumps(result, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + temporary.replace(path) + + +def _run(args: argparse.Namespace) -> dict[str, Any]: + import xlwings as xw + + before = sorted(xw.apps.keys()) + if before: + raise RuntimeError( + f"Close existing Excel instances before this sequential benchmark: {before}" + ) + + fixtures = generate_fixtures( + args.fixtures_dir.resolve(), args.rows, tuple(args.cases) + ) + result: dict[str, Any] = { + "schema_version": 1, + "recorded_at": datetime.now(UTC).isoformat(), + "platform": platform.platform(), + "python": sys.version, + "xlwings": xw.__version__, + "excel_version": None, + "configuration": { + "rows": args.rows, + "columns": _COLUMNS, + "cases": args.cases, + "repeats": args.repeats, + "execution": "sequential; one owned Excel application; one workbook open at a time", + "include_default_background_values": [False, True], + "ignore_color_cases": [None, sorted(_IGNORE_RED)], + }, + "fixtures": fixtures, + "mixed_range_probe": None, + "measurements": [], + "comparisons": [], + } + + app = None + try: + app = xw.App(visible=False, add_book=False) + app.display_alerts = False + app.screen_updating = True + try: + app.api.EnableEvents = False + app.api.AskToUpdateLinks = False + except Exception: + pass + result["excel_version"] = str(app.version) + + for fixture_index, fixture in enumerate(fixtures, start=1): + print( + f"[issue159] fixture {fixture_index}/{len(fixtures)}: " + f"{fixture['name']} ({fixture['rows']}x{_COLUMNS}, " + f"{fixture['sheets']} sheet(s)); opening", + flush=True, + ) + book = app.books.open( + fixture["path"], update_links=False, read_only=True + ) + try: + if fixture["name"] == "overlapping_formula": + try: + result["mixed_range_probe"] = _probe_mixed_range(book) + except Exception as exc: + result["mixed_range_probe"] = { + "error": f"{type(exc).__name__}: {exc}" + } + + for include_default_background in (False, True): + for ignore_colors in (None, _IGNORE_RED): + ignore_label = ( + "none" + if ignore_colors is None + else ",".join(sorted(ignore_colors)) + ) + print( + f" [config] include_default_background=" + f"{include_default_background}, ignore_colors={ignore_label}", + flush=True, + ) + pair: dict[str, Any] = { + "fixture": fixture["name"], + "include_default_background": include_default_background, + "ignore_colors": sorted(ignore_colors) if ignore_colors else None, + "repeats": [], + } + for repeat in range(1, args.repeats + 1): + order = ( + ("reference", "hybrid") + if repeat % 2 == 1 + else ("hybrid", "reference") + ) + by_strategy: dict[str, Any | None] = {} + repeat_records: dict[str, dict[str, Any]] = {} + print( + f" [repeat {repeat}/{args.repeats}] order=" + f"{' -> '.join(order)}", + flush=True, + ) + for strategy in order: + print(f" [{strategy}] started", flush=True) + record, color_map = _measure( + book, + strategy, + include_default_background, + ignore_colors, + fixture["name"], + repeat, + ) + result["measurements"].append(record) + repeat_records[strategy] = record + by_strategy[strategy] = color_map + outcome = ( + f"error={record['error']}" + if record["error"] + else "complete" + ) + render_check = record[ + "conditional_render_validation" + ] + if render_check is not None: + outcome += ( + "; CF visibility=" + f"{render_check['status']}" + ) + print( + f" [{strategy}] {outcome}; " + f"{record['elapsed_ms']:.1f} ms, " + f"wrapper calls={record['wrapper_display_format_calls']}", + flush=True, + ) + reference_map = by_strategy["reference"] + hybrid_map = by_strategy["hybrid"] + exact_equal = ( + reference_map == hybrid_map + if reference_map is not None and hybrid_map is not None + else None + ) + for record in repeat_records.values(): + record["exact_workbook_colors_map_equal"] = exact_equal + print( + f" [repeat {repeat}/{args.repeats}] exact map equality=" + f"{exact_equal}", + flush=True, + ) + pair["repeats"].append( + { + "repeat": repeat, + "order": list(order), + "exact_workbook_colors_map_equal": exact_equal, + "errors": { + strategy: repeat_records[strategy]["error"] + for strategy in ("reference", "hybrid") + if repeat_records[strategy]["error"] + }, + "conditional_render_validation": { + strategy: repeat_records[strategy][ + "conditional_render_validation" + ] + for strategy in ("reference", "hybrid") + }, + } + ) + result["comparisons"].append(pair) + _write_output(args.output.resolve(), result) + finally: + book.close() + finally: + if app is not None: + app.quit() + after = sorted(xw.apps.keys()) + if after != before: + raise RuntimeError( + f"Excel process cleanup mismatch: before={before}, after={after}" + ) + + _write_output(args.output.resolve(), result) + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True, help="JSON result path") + parser.add_argument("--repeats", type=int, default=3) + parser.add_argument("--rows", type=int, default=300) + parser.add_argument( + "--cases", + nargs="+", + choices=_FIXTURE_CASES, + default=list(_FIXTURE_CASES), + help="Fixture cases to run; for example: --cases static sparse_cf", + ) + parser.add_argument( + "--fixtures-dir", + type=Path, + help="Directory for generated xlsx fixtures (default: -fixtures)", + ) + args = parser.parse_args() + if args.repeats < 1: + parser.error("--repeats must be positive") + if args.rows < 1: + parser.error("--rows must be positive") + if args.fixtures_dir is None: + args.fixtures_dir = args.output.with_name(args.output.stem + "-fixtures") + if args.output.resolve() == args.fixtures_dir.resolve(): + parser.error("--output must not be the fixture directory") + + result = _run(args) + pairs = [ + repeat["exact_workbook_colors_map_equal"] + for comparison in result["comparisons"] + for repeat in comparison["repeats"] + ] + failed = any(value is False or value is None for value in pairs) or any( + record["error"] + or ( + record["conditional_render_validation"] is not None + and record["conditional_render_validation"]["status"] + in {"failed", "unavailable"} + ) + for record in result["measurements"] + ) + print( + json.dumps( + { + "output": str(args.output.resolve()), + "fixtures_dir": str(args.fixtures_dir.resolve()), + "fixture_count": len(result["fixtures"]), + "measurement_count": len(result["measurements"]), + "all_exact_comparisons_equal": not failed, + "all_conditional_render_checks_passed": not any( + record["conditional_render_validation"] is not None + and record["conditional_render_validation"]["status"] + in {"failed", "unavailable"} + for record in result["measurements"] + ), + "mixed_range_probe": result["mixed_range_probe"], + }, + indent=2, + ensure_ascii=False, + ) + ) + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/dev-docs/adr/ADR-0015-hybrid-rendered-color-extraction.md b/dev-docs/adr/ADR-0015-hybrid-rendered-color-extraction.md new file mode 100644 index 0000000..6b423b6 --- /dev/null +++ b/dev-docs/adr/ADR-0015-hybrid-rendered-color-extraction.md @@ -0,0 +1,139 @@ +# ADR-0015: Hybrid Rendered Color Extraction + +## 状態 + +`proposed` + +## 背景 + +Issue #159 addresses the cost of reading Excel's rendered background color +through `DisplayFormat` for every cell in a worksheet's used range. Saved cell +fills and conditional-formatting ranges can narrow that work, but saved styles +alone do not represent every rendered fill, and Python must not reproduce +Excel's conditional-format evaluator. + +The optimization is limited to `.xlsx` / `.xlsm` workbooks whose saved file can +be tied to the open COM workbook and the extraction's openpyxl session. It does +not assume that saved formatting describes unsaved live workbook changes. The +existing public `colors_map` behavior and the Excel COM rendering authority +remain in place. + +## 決定 + +- Adopt a hybrid strategy for COM-backed `colors_map` extraction when the + workbook is `.xlsx` / `.xlsm`, the saved file exists, the COM workbook path + matches the extraction session path, and `workbook.api.Saved` is true. +- Recheck `workbook.api.Saved` after preparing each sheet for rendered-color + reads. Sheet activation or calculation can run VBA events that change live + fills after the saved snapshot was opened. If `Saved` is false or cannot be + read, latch the legacy full scan for that sheet and every remaining sheet; + do not resume hybrid extraction later in the workbook. +- Read ordinary static fills from the saved worksheet representation. Use + saved worksheet XML to identify conditional-format ranges, clip those ranges + to the live COM `UsedRange`, and deduplicate candidate cells. Also send + static cells with unsupported saved color representations to COM as candidates. +- Traverse only stored openpyxl cells when classifying saved static fills. + When default backgrounds are requested, emit entries for absent coordinates + within `UsedRange` without creating openpyxl cells for them. +- Continue to ask Excel for the final color of each candidate cell through + `DisplayFormat`. Excel remains responsible for all conditional-format rule + evaluation, including formula semantics, priority, overlap, and + `stopIfTrue`; Python only identifies candidate coordinates. +- Fall back to the existing full `DisplayFormat` scan for a worksheet whenever + its XML or formatting constructs make candidate coverage uncertain. This + includes `extLst`, `AlternateContent`, `pivotTableParts`, unsupported + conditional-format namespaces/rules, styled row/column definitions, + table styles, merged-cell effects, and a custom `Normal` named style + (`builtinId=0`) with a non-empty fill. Missing or unreadable files, + unsupported formats, an ineligible workbook state, a session path mismatch, + and raw XML or workbook-load failures also use the legacy scan. Discard + partial saved-analysis results before scanning that worksheet. If a strict + candidate `DisplayFormat` read fails, discard that worksheet's partial hybrid + map and retry its full legacy scan. If a legacy color read also fails, retain + existing per-cell error suppression: include the default background when + requested or omit the cell otherwise. Count failed candidate reads and the + retry scan in display-call instrumentation. +- Reuse a supplied extraction session only for the matching workbook path. + Standalone path-based calls own and close their session. Retain the current + COM preparation and full-scan implementation as the correctness fallback + and internal parity oracle. +- Preserve the existing output model and semantics: color normalization, + coordinates, per-sheet and workbook maps, `include_default_background`, and + `ignore_colors`. Do not change public APIs, modes, or serialized schemas. +- Record used-range cell count, conditional-format candidate count, actual + `DisplayFormat` call count, static/COM/total durations, and fallback reason + for diagnosis and performance comparison. + +## 影響 + +- Worksheets with ordinary saved fills and no conditional formatting can avoid + per-cell `DisplayFormat` calls. Sparse conditional-format ranges can reduce + those calls to the union of clipped candidate cells plus cells whose saved + static color representation needs Excel resolution. +- Dense conditional formatting and conservative fallbacks retain the legacy + scan cost. The strategy does not promise a wall-clock speedup for every + workbook or Excel installation. +- Raw XML validation and style classification add implementation complexity. + Conservative per-sheet fallback limits the risk of returning incomplete or + incorrectly rendered maps as the OOXML surface evolves. +- Excel remains the source of truth for rendered colors, so formula and rule + behavior stays aligned with the installed Excel version. Unsaved live + formatting is handled by the legacy path rather than inferred from the saved + file. +- Instrumentation makes candidate reduction and fallback behavior observable; + deterministic call counts can be compared independently of machine-specific + timing. + +## 根拠 + +- Tests: `tests/backends/test_colors_map.py` covers default-background and + ignored-color output; `tests/core/test_cells_color_normalization.py` covers + color-key normalization; `tests/core/test_openpyxl_extraction_session.py` + covers shared-session color extraction, standalone/session parity, and + resource lifecycle on the compatibility path. `tests/core/test_color_hybrid.py` + uses synthetic workbook data and a fake COM surface to cover static-fill + parity, candidate clipping/deduplication, conditional-color override, + unsupported XML/style fallbacks (including `pivotTableParts` and custom + `Normal` fill), strict candidate-read retry, unsaved/session-mismatch/ + non-`.xlsx` eligibility, post-preparation `Saved` invalidation (false or + unreadable) for the current and remaining sheets, sparse traversal without + materializing missing openpyxl cells, missing/corrupt saved files, sheet + isolation, shared pipeline/session behavior, and session ownership. Automatic and gradient + fills are synthetic unit cases, not live-Excel fixtures. +- Code: `src/exstruct/core/color_hybrid.py` implements saved-fill and candidate + analysis; `src/exstruct/core/cells.py` retains the COM color entrypoint and + legacy per-cell `DisplayFormat` scan; + `src/exstruct/core/backends/com_backend.py` passes the optional extraction + session; `src/exstruct/core/openpyxl_session.py` owns the reusable workbook + session. +- Related specs: `dev-docs/specs/excel-extraction.md`, + `dev-docs/specs/hybrid-color-extraction.md`, and + `dev-docs/specs/extraction-performance.md`. +- Related decisions: ADR-0002 defines fallback policy; ADR-0011 defines + extraction-scoped workbook reuse; ADR-0013 defines the light-mode backend + boundary; ADR-0014 records the separate file-first cell-extraction decision. +- Benchmark: The final Windows / Excel 16.0 run contains 48 measurements for + 300-row static and sparse-CF fixtures: all 24 reference/hybrid map comparisons + passed across four configurations and three repeats per fixture. Conditional + color validation passed. `DisplayFormat` calls fell from 6,000 to 0 for + static fills and from 6,000 to 30 for sparse CF. See the [final benchmark + JSON](../../benchmark/baselines/issue159-colors-final-windows.json). + The separate 10-row sparse, dense, and overlapping-formula probe also passed + all configurations and verified visible CF colors; its mixed `A1:B1` range + returned `0` while per-cell reads returned `255` and `12611584`, confirming + the range property is not a per-cell color matrix. See the [CF probe + JSON](../../benchmark/baselines/issue159-colors-cf-probe-windows.json). + Measurement details are in [benchmark results](../../benchmark/issue159-results.md). + Wall-clock results are environment-specific and do not imply a universal + timing guarantee. +- Classification: `recommended`; record a performance-strategy change while + retaining public semantics. Primary domain: `performance`; other domains: + `backend`, `extraction`, `compatibility`, `fallback`. + +## Supersedes + +- None + +## Superseded by + +- None diff --git a/dev-docs/adr/README.md b/dev-docs/adr/README.md index 99d01c3..f300d28 100644 --- a/dev-docs/adr/README.md +++ b/dev-docs/adr/README.md @@ -49,3 +49,4 @@ ADRs record what was decided, under which constraints, and which trade-offs were | `ADR-0012` | Optional SciPy Border Clustering | `proposed` | `performance` | | `ADR-0013` | Default OOXML Backend for Light Extraction | `proposed` | `backend` | | `ADR-0014` | Retain File-First COM Extraction | `proposed` | `performance` | +| `ADR-0015` | Hybrid Rendered Color Extraction | `proposed` | `performance` | diff --git a/dev-docs/adr/decision-map.md b/dev-docs/adr/decision-map.md index 029dfb6..56ae54b 100644 --- a/dev-docs/adr/decision-map.md +++ b/dev-docs/adr/decision-map.md @@ -5,6 +5,7 @@ This document is a human-readable map for navigating ADRs by domain. ## extraction - `ADR-0014` Retain File-First COM Extraction (`proposed`) +- `ADR-0015` Hybrid Rendered Color Extraction (`proposed`) - `ADR-0001` Extraction Mode Responsibility Boundaries (`superseded`) - `ADR-0002` Rich Backend Fallback Policy (`accepted`) @@ -22,6 +23,7 @@ This document is a human-readable map for navigating ADRs by domain. ## backend - `ADR-0014` Retain File-First COM Extraction (`proposed`) +- `ADR-0015` Hybrid Rendered Color Extraction (`proposed`) - `ADR-0002` Rich Backend Fallback Policy (`accepted`) - `ADR-0004` Patch Backend Selection Policy (`accepted`) @@ -34,6 +36,7 @@ This document is a human-readable map for navigating ADRs by domain. - `ADR-0002` Rich Backend Fallback Policy (`accepted`) - `ADR-0013` Default OOXML Backend for Light Extraction (`proposed`) +- `ADR-0015` Hybrid Rendered Color Extraction (`proposed`) ## schema @@ -46,6 +49,7 @@ This document is a human-readable map for navigating ADRs by domain. ## compatibility - `ADR-0014` Retain File-First COM Extraction (`proposed`) +- `ADR-0015` Hybrid Rendered Color Extraction (`proposed`) - `ADR-0003` Output Serialization Omission Policy (`accepted`) - `ADR-0004` Patch Backend Selection Policy (`accepted`) @@ -59,6 +63,7 @@ This document is a human-readable map for navigating ADRs by domain. ## performance - `ADR-0014` Retain File-First COM Extraction (`proposed`) +- `ADR-0015` Hybrid Rendered Color Extraction (`proposed`) - `ADR-0011` Extraction-Scoped Workbook Reuse and Direct Cell Reading (`proposed`) - `ADR-0012` Optional SciPy Border Clustering (`proposed`) diff --git a/dev-docs/adr/index.yaml b/dev-docs/adr/index.yaml index ccf2282..eb68d47 100644 --- a/dev-docs/adr/index.yaml +++ b/dev-docs/adr/index.yaml @@ -228,3 +228,20 @@ adrs: - dev-docs/specs/excel-extraction.md - dev-docs/specs/extraction-performance.md - benchmark/issue151-results.md + - id: ADR-0015 + title: Hybrid Rendered Color Extraction + status: proposed + path: dev-docs/adr/ADR-0015-hybrid-rendered-color-extraction.md + primary_domain: performance + domains: + - performance + - backend + - extraction + - compatibility + - fallback + supersedes: [] + superseded_by: [] + related_specs: + - dev-docs/specs/excel-extraction.md + - dev-docs/specs/hybrid-color-extraction.md + - dev-docs/specs/extraction-performance.md diff --git a/dev-docs/adr/reviews/ADR-0015-2026-10-04.md b/dev-docs/adr/reviews/ADR-0015-2026-10-04.md new file mode 100644 index 0000000..97f880b --- /dev/null +++ b/dev-docs/adr/reviews/ADR-0015-2026-10-04.md @@ -0,0 +1,33 @@ +# ADR-0015 review follow-up evidence + +## Scope + +PR #160 follow-up retains the existing saved-fill / Excel-rendering policy. +This review clarifies saved-state invalidation, sparse traversal, and evidence +references; it does not change the public API or introduce resource budgets. + +## Structural and consistency checks + +- `adr-linter`: no remaining findings. Status is `proposed`; required sections, + concrete Tests/Code/Related specs, tradeoffs, and empty supersede links exist. +- `adr-reconciler`: no remaining findings within the changed claims below. +- `adr-indexer`: all 15 ADR index entries match source titles/statuses and + README/domain-map entries. ADR-0015 metadata is unchanged; no index edits + are needed. All seven local Markdown links in ADR-0015 and its hybrid spec + resolve to files inside the repository. + +| Corrected finding | ADR/spec evidence | Implementation | Regression evidence | +| --- | --- | --- | --- | +| Saved state can change during sheet preparation | ADR decision; spec eligibility/fallback | `extract_colors` rechecks Saved and latches legacy | false/unreadable post-preparation state, current/remaining sheet colors | +| Sparse traversal created missing cells | ADR decision; spec static fills | `_static_colors` traverses stored cells, synthesizes requested defaults | cell cache unchanged with/without defaults; 100000 x 16384 range retains two cells | +| Benchmark references resolved above root | ADR benchmark evidence; spec runtime evidence | relative links use `../../benchmark/` | local link resolution check | + +## Validation limits + +Unit suite: 1,138 passed, one skipped, 12 deselected; four existing DrawingML +warnings. Ruff, mypy (90 source files), lock and diff checks passed. The +benchmark-counter regression also confirms the outer ContextVar is restored. +Live Excel benchmarks were not rerun after these fixes. Dense candidate sets +and requested default output can still scale with UsedRange as stated in the +specification. Structural/consistency review does not approve a merge or +change the ADR's proposed status. diff --git a/dev-docs/specs/excel-extraction.md b/dev-docs/specs/excel-extraction.md index 8bc2859..4a1774e 100644 --- a/dev-docs/specs/excel-extraction.md +++ b/dev-docs/specs/excel-extraction.md @@ -172,9 +172,16 @@ Mode notes: ## Colors Map -- Prefer COM to include conditional formatting colors -- Overwrite with COM results when COM succeeds -- Use openpyxl results only when COM fails +- COM-backed `colors_map` extraction retains Excel-rendered conditional-format + colors and overwrites compatibility results when COM succeeds. Existing + normalization, coordinates, default-background, and ignored-color behavior + remain unchanged. +- For eligible saved `.xlsx` / `.xlsm` workbooks, use the hybrid saved-fill and + conditional-format candidate strategy in + [Hybrid rendered color extraction](hybrid-color-extraction.md). Excel + evaluates final rendered colors for every candidate; unsupported or + ambiguous worksheet constructs use the legacy full COM scan. +- Use openpyxl results only when the existing COM fallback applies. - An explicit `colors_map` request on the direct light OOXML path selects the complete openpyxl compatibility pipeline. diff --git a/dev-docs/specs/hybrid-color-extraction.md b/dev-docs/specs/hybrid-color-extraction.md new file mode 100644 index 0000000..419f9f6 --- /dev/null +++ b/dev-docs/specs/hybrid-color-extraction.md @@ -0,0 +1,198 @@ +# Hybrid Rendered Color Extraction Specification + +This specification defines how COM-backed `colors_map` extraction combines +saved worksheet fills with Excel-rendered colors while preserving the existing +output contract. + +## Scope and compatibility + +- Applies to COM-backed color-map extraction used by `standard` / `verbose` on + `.xlsx` and `.xlsm` workbooks. +- Does not change the public `colors_map` shape, color-key normalization, + coordinate convention, `include_default_background`, `ignore_colors`, or + mode selection. +- Excel `DisplayFormat` remains authoritative for cells affected by + conditional formatting. Python identifies candidate cells; it does not + evaluate conditional-format formulas or rule outcomes. +- The existing full COM scan remains available as the fallback and internal + parity oracle. + +## Hybrid eligibility + +Use the hybrid path only when all of the following hold: + +- The COM workbook is an `.xlsx` or `.xlsm` file. +- Its saved file exists and can be loaded and parsed. +- A usable openpyxl extraction session refers to the same normalized absolute + path as the COM workbook. +- `workbook.api.Saved` is true. + +When a caller supplies a session, reuse it only after confirming the path +match. A path-based standalone call owns a temporary session and closes it +after extraction. A session mismatch, unreadable/missing file, unsupported +format, unreadable saved state, or session load failure selects the legacy +full scan. After preparing each sheet for `DisplayFormat`, check the saved +state again before using the saved snapshot for that sheet. Sheet activation +or calculation can run VBA events that change live fills. If `Saved` is false +or unreadable after preparation, latch the legacy full scan for the current +sheet and all remaining sheets; do not resume hybrid extraction even if a +later check reports `Saved` as true. The hybrid path makes no claim that saved +formatting represents unsaved live changes. + +## Saved worksheet analysis + +Validate each worksheet's raw XML before using it to limit COM calls. The +supported path must understand the worksheet namespace and conditional-format +markup it encounters. Fall back to a full legacy scan for that worksheet if +any of these conditions prevents a complete, trustworthy candidate set: + +- The worksheet contains `extLst`, markup-compatibility `AlternateContent`, or + `pivotTableParts`. +- Conditional-format markup uses an unrecognized namespace or an unrecognized + rule representation/type. +- Styled row/column definitions, table styles, or merged-cell effects make the + rendered background of any used cell ambiguous. The initial implementation + may conservatively fall back when any row/column style, table, or merged + range is present. +- A custom `Normal` named style (`builtinId=0`) has a non-empty fill, because + otherwise unstyled cells could inherit a saved fill that is not explicit on + the cell. +- Worksheet XML cannot be parsed or its relationships/parts cannot be loaded. + +The validator is deliberately conservative. It must not ignore unfamiliar +conditional-format constructs and then return a partially optimized map. + +## Static fills and candidate cells + +For a worksheet that passes validation: + +1. Classify saved static fills by traversing only cells already stored in the + openpyxl worksheet, and restrict results to the live COM `UsedRange`. When + default backgrounds are requested, emit defaults for absent coordinates in + that range without creating openpyxl cells for them. +2. Resolve ordinary no-fill cells and solid direct-RGB fills without tint from + saved data, using the existing color normalization and filtering rules. +3. Add cells with other saved static color forms to the COM candidate set. + This includes theme, indexed, automatic, patterned, gradient, and tinted + fills. Excel resolves those colors through `DisplayFormat`. +4. Read every recognized conditional-format `sqref` range, intersect it with + the live COM `UsedRange`, and deduplicate the resulting cells. The count of + conditional-format candidates is the size of this clipped union. +5. Query `DisplayFormat.Interior.Color` once per unique candidate cell. Merge + these normalized COM results over the saved static map at the same + coordinates. + +The live COM used range defines the output boundary. Candidate ranges and +saved static cells outside it must not add coordinates to the result. + +Excluding requested defaults, static-fill traversal is proportional to the +cells already stored in the openpyxl worksheet. The conditional-format +candidate union and requested default-background output can still scale with +the live `UsedRange`. No area or candidate-count budget is currently enforced. + +## Excel evaluation and output semantics + +Excel evaluates every candidate's final rendered color. This includes +condition formulas, relative references, calculation state, priority, +overlapping rules, and `stopIfTrue`. ExStruct does not evaluate or simplify +those rules in Python. + +Apply existing behavior after combining the saved and COM colors: + +- Normalize color keys with the existing RGB conversion. +- Apply `ignore_colors` using the normalized color key. +- Include or omit the default background according to + `include_default_background`. +- Keep rows 1-based and columns 0-based in each per-sheet map. +- Return the same `SheetColorsMap` and `WorkbookColorsMap` structures. + +COM preparation required by the current rendered-color path remains in place. + +## Fallback and atomicity + +- Workbook-level eligibility, file, session, XML-container, or load failures + use the legacy full scan for the workbook. +- If the post-preparation `Saved` check is false or unreadable, latch the + workbook to legacy scanning from the current sheet onward. Sheet activation + and calculation may run VBA events that invalidate the saved snapshot; a + later `Saved` value does not re-enable hybrid extraction for later sheets. +- An unsupported or ambiguous construct isolated to one worksheet uses the + legacy full scan for that worksheet while eligible worksheets may still use + the hybrid path. +- If saved-worksheet parsing, candidate construction, or static-fill + classification fails after producing partial data for a worksheet, discard + that worksheet's partial map and recompute it through the legacy full scan. + Never return a partial hybrid map for that worksheet. +- Candidate rendering uses strict `DisplayFormat` reads. If one fails, discard + the partial hybrid map and retry a full legacy scan for that worksheet. If + a color read in that retry also fails, preserve legacy per-cell error + suppression: include the default background when requested or omit the cell + otherwise. This does not itself trigger a workbook-level fallback. +- Record a fallback reason for each legacy route. Fallback does not change the + public result shape or expose a new public mode. + +## Resource ownership + +- Backend extraction reuses the supplied openpyxl session only when its + resolved path matches the COM workbook path. +- A standalone path-based call owns and closes its session, including on parse, + COM, or fallback errors. +- The color extractor does not retain sessions across calls or share them + across concurrent extractions. + +## Instrumentation + +Record per worksheet: + +- Total cells in the live used-range rectangle. +- Unique conditional-format candidate cells after clipping and deduplication. +- Actual `DisplayFormat` cell calls, including calls for unsupported static + color representations, failed candidate reads, and legacy retry reads. A + retry can make this count exceed the used-range cell count. +- Static extraction duration, COM rendered-color duration (including a retry), + and total color extraction duration. +- Fallback reason, or no fallback. + +Use deterministic call counts to compare candidate reduction. Wall-clock +measurements are diagnostic and do not impose a universal CI timing threshold. + +## Verification fixtures + +`tests/core/test_color_hybrid.py` compares normalized maps against the retained +legacy oracle using synthetic workbook data and a fake COM surface. It covers +static/default behavior, blank cells within the COM used range, conditional +candidate clipping/deduplication, conditional-color override, synthetic +theme/indexed/tinted/pattern/automatic/gradient fill classification, XML +fallbacks (`extLst`, unknown rule, foreign namespace, `pivotTableParts`), +custom `Normal` fill, ambiguous row/column/table/merge cases, transient +candidate-read retry, unsaved/session-mismatch/non-`.xlsx` eligibility, +post-preparation `Saved` invalidation (false or unreadable) for the current and +remaining sheets, sparse traversal without materializing missing openpyxl +cells, sheet isolation, shared pipeline/session reuse, and standalone session +ownership, including missing/corrupt saved-file fallback. Automatic and +gradient fills are synthetic unit cases, not live-Excel fixtures. + +The repeat benchmark and its current measurements are tracked in +[benchmark/issue159-results.md](../../benchmark/issue159-results.md). Keep +Excel wall-clock results separate from correctness and deterministic +call-count checks. + +## Recorded runtime smoke evidence + +The final Windows / Excel 16.0 run has 48 measurements for 300-row static and +sparse-CF fixtures: reference and hybrid maps matched in all 24 comparisons +across four `include_default_background` / `ignore_colors` configurations and +three repeats per fixture. Conditional-color validation passed. Calls fell +from 6,000 to 0 for static fills and from 6,000 to 30 for sparse CF. + +The separate Windows / Excel 16.0 CF probe used 10-row sparse, dense, and +overlapping-formula fixtures. All configurations matched exactly and expected +conditional colors were visible. In its mixed `A1:B1` probe, +`Range("A1:B1").DisplayFormat.Interior.Color` returned `0`, while individual +cell reads returned `255` and `12611584`; the range property does not provide a +per-cell color matrix. + +See the [final benchmark JSON](../../benchmark/baselines/issue159-colors-final-windows.json), +[CF probe JSON](../../benchmark/baselines/issue159-colors-cf-probe-windows.json), +and [benchmark results](../../benchmark/issue159-results.md) for details. +Wall-clock timings vary by environment and are not a universal guarantee. diff --git a/pyproject.toml b/pyproject.toml index be1a360..076f68c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "exstruct" -version = "0.8.2" +version = "0.9.0" description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines" readme = "README.md" license = { file = "LICENSE" } diff --git a/src/exstruct/core/backends/com_backend.py b/src/exstruct/core/backends/com_backend.py index 2f8add4..1b8c5f4 100644 --- a/src/exstruct/core/backends/com_backend.py +++ b/src/exstruct/core/backends/com_backend.py @@ -17,6 +17,7 @@ extract_sheet_formulas_map_com, ) from ..charts import get_charts +from ..openpyxl_session import OpenpyxlExtractionSession from ..ranges import parse_range_zero_based from ..shapes import get_shapes_with_position from .base import ( @@ -58,6 +59,7 @@ class ComBackend: """ workbook: xw.Book + session: OpenpyxlExtractionSession | None = None def extract_cells(self, *, include_links: bool = False) -> CellData: """Prototype bulk Value2 reader; intentionally unused by public extraction. @@ -151,6 +153,13 @@ def extract_colors_map( WorkbookColorsMap | None: A mapping of workbook color definitions when extraction succeeds, or `None` if COM extraction fails. """ try: + if self.session is not None: + return extract_sheet_colors_map_com( + self.workbook, + include_default_background=include_default_background, + ignore_colors=ignore_colors, + session=self.session, + ) return extract_sheet_colors_map_com( self.workbook, include_default_background=include_default_background, diff --git a/src/exstruct/core/cells.py b/src/exstruct/core/cells.py index 457024b..7ca8bde 100644 --- a/src/exstruct/core/cells.py +++ b/src/exstruct/core/cells.py @@ -4,6 +4,7 @@ from collections import deque from collections.abc import Callable, Iterable, Sequence +from contextvars import ContextVar from dataclasses import dataclass from datetime import time from decimal import Decimal, InvalidOperation @@ -59,6 +60,10 @@ } _DEFAULT_BACKGROUND_HEX = "FFFFFF" _XL_COLOR_NONE = -4142 +_display_format_calls: ContextVar[int] = ContextVar("display_format_calls", default=0) +_strict_display_format: ContextVar[bool] = ContextVar( + "strict_display_format", default=False +) _BORDER_CLUSTER_BACKEND_ENV = "EXSTRUCT_BORDER_CLUSTER_BACKEND" ExtractionMode = Literal["light", "libreoffice", "standard", "verbose"] @@ -235,6 +240,7 @@ def extract_sheet_colors_map_com( *, include_default_background: bool, ignore_colors: set[str] | None, + session: OpenpyxlExtractionSession | None = None, ) -> WorkbookColorsMap: """ Extract per-sheet background color maps using the workbook's COM/display-format interfaces. @@ -247,15 +253,9 @@ def extract_sheet_colors_map_com( Returns: WorkbookColorsMap: Mapping of sheet names to SheetColorsMap containing detected background color positions for each worksheet. """ - _prepare_workbook_for_display_format(workbook) - sheets: dict[str, SheetColorsMap] = {} - for sheet in workbook.sheets: - _prepare_sheet_for_display_format(sheet) - sheet_map = _extract_sheet_colors_com( - sheet, include_default_background, ignore_colors - ) - sheets[sheet.name] = sheet_map - return WorkbookColorsMap(sheets=sheets) + from .color_hybrid import extract_colors + + return extract_colors(workbook, include_default_background, ignore_colors, session) def _extract_sheet_colors( @@ -552,10 +552,13 @@ def _get_display_format_color(sheet: xw.Sheet, row: int, col: int) -> int | None """ try: cell = sheet.api.Cells(row, col) + _display_format_calls.set(_display_format_calls.get() + 1) display_format = cell.DisplayFormat interior = display_format.Interior return int(interior.Color) except Exception: + if _strict_display_format.get(): + raise return None diff --git a/src/exstruct/core/color_hybrid.py b/src/exstruct/core/color_hybrid.py new file mode 100644 index 0000000..40ade2b --- /dev/null +++ b/src/exstruct/core/color_hybrid.py @@ -0,0 +1,312 @@ +"""Conservative saved-fill extraction with Excel as the rendered-color oracle.""" + +from __future__ import annotations + +from contextlib import ExitStack +import logging +from pathlib import Path +import posixpath +from time import perf_counter +from typing import TYPE_CHECKING +from zipfile import ZipFile + +from defusedxml import ElementTree + +from . import cells +from .cell_types import SheetColorsMap, WorkbookColorsMap +from .openpyxl_session import OpenpyxlExtractionSession + +if TYPE_CHECKING: + from openpyxl.worksheet.worksheet import Worksheet + import xlwings as xw + +logger = logging.getLogger(__name__) +_MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main" +_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +_RULES = { + "expression", + "cellIs", + "colorScale", + "dataBar", + "iconSet", + "top10", + "uniqueValues", + "duplicateValues", + "containsText", + "notContainsText", + "beginsWith", + "endsWith", + "containsBlanks", + "notContainsBlanks", + "containsErrors", + "notContainsErrors", + "timePeriod", + "aboveAverage", +} +Bounds = tuple[int, int, int, int] + + +def _worksheet_parts(archive: ZipFile) -> dict[str, str]: + """Resolve worksheet names through package relationships, never sheet order.""" + rels = ElementTree.fromstring(archive.read("xl/_rels/workbook.xml.rels")) + targets = {} + for rel in rels: + if rel.attrib.get("TargetMode") == "External": + continue + target = rel.attrib["Target"] + targets[rel.attrib["Id"]] = ( + target.lstrip("/") + if target.startswith("/") + else posixpath.normpath(posixpath.join("xl", target)) + ) + root = ElementTree.fromstring(archive.read("xl/workbook.xml")) + return { + sheet.attrib["name"]: targets[sheet.attrib[f"{{{_REL}}}id"]] + for sheet in root.findall(f"{{{_MAIN}}}sheets/{{{_MAIN}}}sheet") + } + + +def _conditional_ranges(raw: bytes) -> list[Bounds]: + """Reject lossy extension parsing before trusting saved conditional ranges.""" + root = ElementTree.fromstring(raw) + if root.tag != f"{{{_MAIN}}}worksheet": + raise ValueError("unsupported worksheet namespace") + ranges = [] + for element in root.iter(): + name = element.tag.rsplit("}", 1)[-1] + if name in {"extLst", "AlternateContent", "pivotTableParts"}: + raise ValueError("worksheet extensions may affect rendered fills") + if name not in {"conditionalFormatting", "cfRule"}: + continue + if element.tag != f"{{{_MAIN}}}{name}": + raise ValueError("unsupported conditional formatting namespace") + if name == "cfRule": + if element.attrib.get("type") not in _RULES: + raise ValueError("unsupported conditional formatting rule") + else: + refs = element.attrib.get("sqref", "").split() + if not refs: + raise ValueError("missing conditional formatting range") + for ref in refs: + c1, r1, c2, r2 = cells.range_boundaries(ref) + if not (1 <= r1 <= r2 <= 1048576 and 1 <= c1 <= c2 <= 16384): + raise ValueError("invalid conditional formatting range") + ranges.append((r1, c1, r2, c2)) + return ranges + + +def _candidate_cells(ranges: list[Bounds], bounds: Bounds) -> set[tuple[int, int]]: + """Clip first so entire-column rules cannot expand beyond UsedRange.""" + r1, c1, r2, c2 = bounds + return { + (row, col) + for a, b, z, y in ranges + for row in range(max(r1, a), min(r2, z) + 1) + for col in range(max(c1, b), min(c2, y) + 1) + } + + +def _validate_saved_styles(ws: Worksheet) -> None: + """Reject sheet-wide styles that cannot be resolved from stored cell fills.""" + if ws.merged_cells.ranges or ws.tables: + raise ValueError("merged cells or table styles require rendered scan") + if any(d.has_style for d in ws.row_dimensions.values()) or any( + d.has_style for d in ws.column_dimensions.values() + ): + raise ValueError("row/column styles require rendered scan") + # An absent cell style inherits Normal; it may have a custom workbook fill. + for style in ws.parent._named_styles: + if style.builtinId == 0 and style.fill.patternType not in (None, "none"): + raise ValueError("custom Normal fill requires rendered scan") + + +def _static_colors( + ws: Worksheet, + bounds: Bounds, + candidates: set[tuple[int, int]], + include_default: bool, +) -> dict[tuple[int, int], str | None]: + """Only trust fills whose RGB interpretation is identical to Excel's.""" + _validate_saved_styles(ws) + r1, c1, r2, c2 = bounds + result: dict[tuple[int, int], str | None] = {} + # iter_rows creates and retains missing cells across the entire rectangle. + # Match the sparse traversal used by the file-backed cell extractor. + for coord, cell in ws._cells.items(): + row, col = coord + if not (r1 <= row <= r2 and c1 <= col <= c2) or coord in candidates: + continue + fill = cell.fill + pattern = getattr(fill, "patternType", "unsupported") + if pattern in (None, "none"): + color = "FFFFFF" if include_default else None + elif ( + pattern == "solid" and fill.fgColor.type == "rgb" and not fill.fgColor.tint + ): + color = cells._resolve_cell_background(cell, include_default) + else: + candidates.add(coord) + continue + if color is not None: + result[coord] = color + if include_default: + for row in range(r1, r2 + 1): + for col in range(c1, c2 + 1): + coord = (row, col) + if coord not in ws._cells and coord not in candidates: + result[coord] = "FFFFFF" + return result + + +def _extract_sheet( + sheet: xw.Sheet, + session: OpenpyxlExtractionSession | None, + archive: ZipFile | None, + parts: dict[str, str], + reason: str, + include_default: bool, + ignore_colors: set[str] | None, +) -> SheetColorsMap: + started = perf_counter() + token = cells._display_format_calls.set(0) + static_seconds = 0.0 + rendered_seconds = 0.0 + candidates: set[tuple[int, int]] = set() + conditional_count = 0 + used_cells = 0 + fallback = True + try: + used = sheet.used_range + last = used.last_cell + bounds = ( + int(getattr(used, "row", 1)), + int(getattr(used, "column", 1)), + int(last.row), + int(last.column), + ) + r1, c1, r2, c2 = bounds + used_cells = max(0, r2 - r1 + 1) * max(0, c2 - c1 + 1) + static_started = perf_counter() + try: + if session is None or archive is None: + raise ValueError(reason) + ranges = _conditional_ranges(archive.read(parts[sheet.name])) + candidates = _candidate_cells(ranges, bounds) + conditional_count = len(candidates) + ws = session.workbook()[sheet.name] + colors = _static_colors(ws, bounds, candidates, include_default) + fallback = False + except Exception as exc: + reason = str(exc) + finally: + static_seconds = perf_counter() - static_started + rendered_started = perf_counter() + if fallback: + logger.debug( + "Color extraction legacy fallback sheet=%s reason=%s", + sheet.name, + reason, + ) + return cells._extract_sheet_colors_com( + sheet, include_default, ignore_colors + ) + strict_token = cells._strict_display_format.set(True) + try: + for row, col in sorted(candidates): + colors[row, col] = cells._resolve_cell_background_com( + sheet, row, col, include_default + ) + except Exception as exc: + fallback = True + reason = f"rendered candidate extraction failed: {exc}" + finally: + cells._strict_display_format.reset(strict_token) + if fallback: + logger.debug( + "Color extraction legacy fallback sheet=%s reason=%s", + sheet.name, + reason, + ) + return cells._extract_sheet_colors_com( + sheet, include_default, ignore_colors + ) + rendered_seconds = perf_counter() - rendered_started + ignore = cells._normalize_ignore_colors(ignore_colors) + mapping: dict[str, list[tuple[int, int]]] = {} + for (row, col), color in sorted(colors.items()): + if color is not None: + key = cells._normalize_color_key(color) + if not cells._should_ignore_color(key, ignore): + mapping.setdefault(key, []).append((row, col - 1)) + return SheetColorsMap(sheet_name=sheet.name, colors_map=mapping) + finally: + if fallback and "rendered_started" in locals(): + rendered_seconds = perf_counter() - rendered_started + logger.debug( + "Color extraction sheet=%s used_cells=%d conditional_candidates=%d " + "rendered_candidates=%d display_format_calls=%d fallback=%s " + "static_duration_ms=%.3f com_duration_ms=%.3f duration_ms=%.3f", + sheet.name, + used_cells, + conditional_count, + len(candidates), + cells._display_format_calls.get(), + fallback, + static_seconds * 1000, + rendered_seconds * 1000, + (perf_counter() - started) * 1000, + ) + cells._display_format_calls.reset(token) + + +def extract_colors( + workbook: xw.Book, + include_default: bool, + ignore_colors: set[str] | None, + session: OpenpyxlExtractionSession | None, +) -> WorkbookColorsMap: + """Own standalone resources and preserve legacy preparation and fallback.""" + cells._prepare_workbook_for_display_format(workbook) + with ExitStack() as stack: + archive = None + parts: dict[str, str] = {} + reason = "saved workbook unavailable" + try: + path = Path(workbook.fullname) + if path.suffix.lower() not in {".xlsx", ".xlsm"}: + raise ValueError("unsupported saved workbook format") + if not workbook.api.Saved: + raise ValueError("workbook has unsaved changes") + if session is not None and session.file_path.resolve() != path.resolve(): + raise ValueError("saved workbook session path mismatch") + archive = stack.enter_context(ZipFile(path)) + parts = _worksheet_parts(archive) + if session is None: + session = stack.enter_context(OpenpyxlExtractionSession(path)) + except Exception as exc: + reason = str(exc) + archive = None + sheets = {} + for sheet in workbook.sheets: + cells._prepare_sheet_for_display_format(sheet) + if archive is not None: + try: + if not workbook.api.Saved: + raise ValueError( + "workbook has unsaved changes after sheet preparation" + ) + except Exception as exc: + # Activation/calculation can run VBA handlers that alter + # live fills. Once invalidated, never trust this snapshot again. + reason = str(exc) + archive = None + sheets[sheet.name] = _extract_sheet( + sheet, + session, + archive, + parts, + reason, + include_default, + ignore_colors, + ) + return WorkbookColorsMap(sheets=sheets) diff --git a/src/exstruct/core/pipeline.py b/src/exstruct/core/pipeline.py index 2406d21..49c216f 100644 --- a/src/exstruct/core/pipeline.py +++ b/src/exstruct/core/pipeline.py @@ -806,7 +806,14 @@ def step_extract_colors_map_com( backend_type = _backend_type("ComBackend") - com_result = backend_type(workbook).extract_colors_map( + # Preserve the minimal constructor surface of legacy backend overrides. + native_module = sys.modules.get("exstruct.core.backends.com_backend") + native_type = getattr(native_module, "ComBackend", None) + if backend_type is native_type and artifacts.openpyxl_session is not None: + backend = backend_type(workbook, session=artifacts.openpyxl_session) + else: + backend = backend_type(workbook) + com_result = backend.extract_colors_map( include_default_background=inputs.include_default_background, ignore_colors=inputs.ignore_colors, ) diff --git a/tests/core/test_color_hybrid.py b/tests/core/test_color_hybrid.py new file mode 100644 index 0000000..116139f --- /dev/null +++ b/tests/core/test_color_hybrid.py @@ -0,0 +1,957 @@ +"""Behavioral tests for the saved-fill plus rendered-color extraction path.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +import shutil +from types import SimpleNamespace +from zipfile import ZIP_DEFLATED, ZipFile + +from openpyxl import Workbook, load_workbook +from openpyxl.formatting.rule import FormulaRule +from openpyxl.styles import Color, GradientFill, PatternFill +from openpyxl.utils.cell import range_boundaries +from openpyxl.worksheet.table import Table +from openpyxl.worksheet.worksheet import Worksheet +import pytest + +from exstruct.core import cells, color_hybrid, pipeline +from exstruct.core.cell_types import SheetColorsMap +from exstruct.core.openpyxl_session import OpenpyxlExtractionSession + +_NO_FILL = -4142 + + +def _excel_color(rgb: str) -> int: + """Convert RGB hex to Excel's integer color representation.""" + red, green, blue = (int(rgb[index : index + 2], 16) for index in (0, 2, 4)) + return red | (green << 8) | (blue << 16) + + +class _FakeSheetAPI: + def __init__(self, sheet: _FakeSheet) -> None: + self.sheet = sheet + + def Activate(self) -> None: # noqa: N802 - mirrors Excel COM + pass + + def Calculate(self) -> None: # noqa: N802 - mirrors Excel COM + pass + + def Cells(self, row: int, column: int) -> SimpleNamespace: # noqa: N802 + self.sheet.display_calls.append((row, column)) + color = self.sheet.rendered_colors.get((row, column), _NO_FILL) + return SimpleNamespace( + DisplayFormat=SimpleNamespace( + Interior=_FakeInterior(self.sheet, (row, column), color) + ) + ) + + +class _FakeInterior: + def __init__( + self, + sheet: _FakeSheet, + coord: tuple[int, int], + color: int | None, + ) -> None: + self.sheet = sheet + self.coord = coord + self.color = color + + @property + def Color(self) -> int | None: # noqa: N802 - mirrors Excel COM + remaining = self.sheet.failures_remaining.get(self.coord, 0) + if remaining: + self.sheet.failures_remaining[self.coord] = remaining - 1 + raise RuntimeError("transient DisplayFormat failure") + return self.color + + +class _FakeSheet: + def __init__( + self, + name: str, + bounds: tuple[int, int, int, int], + rendered_colors: dict[tuple[int, int], int | None], + failures_remaining: dict[tuple[int, int], int] | None = None, + ) -> None: + row, column, last_row, last_column = bounds + self.name = name + self.used_range = SimpleNamespace( + row=row, + column=column, + last_cell=SimpleNamespace(row=last_row, column=last_column), + ) + self.rendered_colors = rendered_colors + self.failures_remaining = failures_remaining or {} + self.display_calls: list[tuple[int, int]] = [] + self.api = _FakeSheetAPI(self) + + +class _FakeWorkbook: + def __init__( + self, fullname: Path, sheets: list[_FakeSheet], *, saved: bool = True + ) -> None: + self.fullname = str(fullname) + self.sheets = sheets + self.api = SimpleNamespace(Saved=saved) + self.app = SimpleNamespace(calculate=lambda: None) + + +def _save_workbook(path: Path, configure: Callable[[Workbook], None]) -> None: + workbook = Workbook() + try: + configure(workbook) + workbook.save(path) + finally: + workbook.close() + + +def _fake_workbook( + path: Path, + *, + used_ranges: dict[str, tuple[int, int, int, int]] | None = None, + rendered_overrides: dict[str, dict[tuple[int, int], int | None]] | None = None, + display_failures: dict[str, dict[tuple[int, int], int]] | None = None, + fullname: Path | None = None, + saved: bool = True, +) -> _FakeWorkbook: + """Build COM-shaped sheets from a real saved workbook and optional Excel state.""" + used_ranges = used_ranges or {} + rendered_overrides = rendered_overrides or {} + display_failures = display_failures or {} + fake_sheets = [] + workbook = load_workbook(path, data_only=True, read_only=False) + try: + for worksheet in workbook.worksheets: + min_column, min_row, max_column, max_row = range_boundaries( + worksheet.calculate_dimension() + ) + bounds = used_ranges.get( + worksheet.title, (min_row, min_column, max_row, max_column) + ) + rendered: dict[tuple[int, int], int | None] = {} + row, column, last_row, last_column = bounds + for row_index in range(row, last_row + 1): + for column_index in range(column, last_column + 1): + fill = worksheet.cell(row_index, column_index).fill + if ( + getattr(fill, "patternType", None) == "solid" + and fill.fgColor.type == "rgb" + ): + rgb = fill.fgColor.rgb[-6:] + rendered[row_index, column_index] = _excel_color(rgb) + else: + rendered[row_index, column_index] = _NO_FILL + rendered.update(rendered_overrides.get(worksheet.title, {})) + fake_sheets.append( + _FakeSheet( + worksheet.title, + bounds, + rendered, + display_failures.get(worksheet.title), + ) + ) + finally: + workbook.close() + return _FakeWorkbook(fullname or path, fake_sheets, saved=saved) + + +def _legacy_result( + workbook: _FakeWorkbook, + *, + include_default: bool, + ignore_colors: set[str] | None, +) -> dict[str, SheetColorsMap]: + return { + sheet.name: cells._extract_sheet_colors_com( + sheet, include_default, ignore_colors + ) + for sheet in workbook.sheets + } + + +def _clear_display_calls(workbook: _FakeWorkbook) -> None: + for sheet in workbook.sheets: + sheet.display_calls.clear() + + +def _rewrite_sheet_xml( + path: Path, sheet_name: str, transform: Callable[[bytes], bytes] +) -> None: + with ZipFile(path) as archive: + part = color_hybrid._worksheet_parts(archive)[sheet_name] + members = [(name, archive.read(name)) for name in archive.namelist()] + rewritten = [] + found = False + for name, contents in members: + if name == part: + contents = transform(contents) + found = True + rewritten.append((name, contents)) + assert found + temporary = path.with_name(f"{path.stem}.rewritten{path.suffix}") + with ZipFile(temporary, "w", ZIP_DEFLATED) as archive: + for name, contents in rewritten: + archive.writestr(name, contents) + temporary.replace(path) + + +def _add_expression_rule(worksheet: Worksheet, ref: str = "A1") -> None: + worksheet.conditional_formatting.add(ref, FormulaRule(formula=["TRUE"])) + + +@pytest.mark.parametrize( + ("include_default", "ignore_colors", "expected"), + [ + ( + True, + None, + { + "FFFFFF": [(1, 0), (1, 2), (2, 1)], + "FF0000": [(1, 1)], + "00FF00": [(2, 0)], + "0000FF": [(2, 2)], + }, + ), + ( + False, + None, + {"FF0000": [(1, 1)], "00FF00": [(2, 0)], "0000FF": [(2, 2)]}, + ), + ( + True, + {"#ff0000", "ffffff"}, + {"00FF00": [(2, 0)], "0000FF": [(2, 2)]}, + ), + ], +) +def test_static_fills_and_default_handling_match_legacy_com( + tmp_path: Path, + include_default: bool, + ignore_colors: set[str] | None, + expected: dict[str, list[tuple[int, int]]], +) -> None: + path = tmp_path / "static.xlsx" + + def configure(workbook: Workbook) -> None: + worksheet = workbook.active + worksheet["A1"] = "plain" + worksheet["B1"].fill = PatternFill("solid", fgColor="FFFF0000") + worksheet["C1"].fill = PatternFill("solid", fgColor="FFFFFFFF") + worksheet["A2"].fill = PatternFill("solid", fgColor="FF00FF00") + worksheet["C2"].fill = PatternFill("solid", fgColor="FF0000FF") + + _save_workbook(path, configure) + fake = _fake_workbook(path) + legacy = _legacy_result( + fake, include_default=include_default, ignore_colors=ignore_colors + ) + _clear_display_calls(fake) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors( + fake, include_default, ignore_colors, session + ) + + assert actual.sheets == legacy + assert actual.sheets["Sheet"].colors_map == expected + assert fake.sheets[0].display_calls == [] + + +def test_blank_cells_inside_com_used_range_keep_legacy_default_semantics( + tmp_path: Path, +) -> None: + path = tmp_path / "blank-extent.xlsx" + + def configure(workbook: Workbook) -> None: + workbook.active["B2"] = "only saved value" + + _save_workbook(path, configure) + # Excel's UsedRange can include blank cells outside openpyxl's saved dimension. + fake = _fake_workbook(path, used_ranges={"Sheet": (2, 2, 3, 3)}) + legacy = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(fake, True, None, session) + + assert actual.sheets == legacy + assert actual.sheets["Sheet"].colors_map == { + "FFFFFF": [(2, 1), (2, 2), (3, 1), (3, 2)] + } + assert fake.sheets[0].display_calls == [] + + +def test_conditional_candidates_are_deduplicated_clipped_and_row_major( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + path = tmp_path / "conditional.xlsx" + + def configure(workbook: Workbook) -> None: + worksheet = workbook.active + worksheet["B2"] = "top" + worksheet["C3"] = "bottom" + _add_expression_rule(worksheet, "A1:A3") + _add_expression_rule(worksheet, "B2:B4") + _add_expression_rule(worksheet, "C3:D8") + _add_expression_rule(worksheet, "Z100:Z101") + + _save_workbook(path, configure) + rendered = { + "Sheet": { + (2, 2): _excel_color("AA0000"), + (3, 2): _excel_color("00AA00"), + (3, 3): _excel_color("0000AA"), + } + } + fake = _fake_workbook(path, rendered_overrides=rendered) + legacy = _legacy_result(fake, include_default=False, ignore_colors=None) + _clear_display_calls(fake) + caplog.set_level("DEBUG", logger=color_hybrid.__name__) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(fake, False, None, session) + + assert actual.sheets == legacy + assert fake.sheets[0].display_calls == [(2, 2), (3, 2), (3, 3)] + assert actual.sheets["Sheet"].colors_map == { + "AA0000": [(2, 1)], + "00AA00": [(3, 1)], + "0000AA": [(3, 2)], + } + log = next( + record.getMessage() + for record in caplog.records + if "Color extraction sheet=Sheet" in record.getMessage() + ) + assert "conditional_candidates=3" in log + assert "rendered_candidates=3" in log + assert "display_format_calls=3" in log + assert "fallback=False" in log + + +@pytest.mark.parametrize( + ("rendered_rgb", "ignore_colors", "expected"), + [ + ("FFFFFF", None, {"FFFFFF": [(1, 0)]}), + ("0000FF", {"#0000ff"}, {}), + ], +) +def test_conditional_rendering_overrides_static_fill_before_ignore_filter( + tmp_path: Path, + rendered_rgb: str, + ignore_colors: set[str] | None, + expected: dict[str, list[tuple[int, int]]], +) -> None: + path = tmp_path / "conditional-overrides-fill.xlsx" + + def configure(workbook: Workbook) -> None: + worksheet = workbook.active + worksheet["A1"].fill = PatternFill("solid", fgColor="FFFF0000") + _add_expression_rule(worksheet) + + _save_workbook(path, configure) + rendered = {"Sheet": {(1, 1): _excel_color(rendered_rgb)}} + fake = _fake_workbook(path, rendered_overrides=rendered) + legacy = _legacy_result(fake, include_default=True, ignore_colors=ignore_colors) + _clear_display_calls(fake) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(fake, True, ignore_colors, session) + + assert actual.sheets == legacy + assert actual.sheets["Sheet"].colors_map == expected + assert fake.sheets[0].display_calls == [(1, 1)] + + +def test_theme_indexed_tinted_and_pattern_fills_use_rendered_candidates( + tmp_path: Path, +) -> None: + path = tmp_path / "candidate-fills.xlsx" + + def configure(workbook: Workbook) -> None: + worksheet = workbook.active + worksheet["A1"].fill = PatternFill("solid", fgColor=Color(theme=4, tint=0.25)) + worksheet["B1"].fill = PatternFill("solid", fgColor=Color(indexed=10)) + worksheet["C1"].fill = PatternFill( + "solid", + fgColor=Color(rgb="FF123456", tint=0.2), + bgColor="FF000000", + ) + worksheet["D1"].fill = PatternFill( + patternType="darkGrid", fgColor="FFFF0000", bgColor="FF00FF00" + ) + + _save_workbook(path, configure) + rendered = { + "Sheet": { + (1, 1): _excel_color("102030"), + (1, 2): _excel_color("203040"), + (1, 3): _excel_color("304050"), + (1, 4): _excel_color("405060"), + } + } + fake = _fake_workbook(path, rendered_overrides=rendered) + legacy = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(fake, True, None, session) + + assert actual.sheets == legacy + assert fake.sheets[0].display_calls == [(1, 1), (1, 2), (1, 3), (1, 4)] + assert actual.sheets["Sheet"].colors_map == { + "102030": [(1, 0)], + "203040": [(1, 1)], + "304050": [(1, 2)], + "405060": [(1, 3)], + } + + +def test_transient_candidate_com_failure_discards_partial_scan_and_retries_legacy( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + path = tmp_path / "transient-candidate-failure.xlsx" + + def configure(workbook: Workbook) -> None: + worksheet = workbook.active + worksheet["A1"] = "first candidate" + worksheet["A2"] = "fails once" + _add_expression_rule(worksheet, "A1:A2") + + _save_workbook(path, configure) + rendered = { + "Sheet": { + (1, 1): _excel_color("AA1100"), + (2, 1): _excel_color("0011AA"), + } + } + legacy_fake = _fake_workbook(path, rendered_overrides=rendered) + legacy = _legacy_result(legacy_fake, include_default=False, ignore_colors=None) + hybrid_fake = _fake_workbook( + path, + rendered_overrides=rendered, + display_failures={"Sheet": {(2, 1): 1}}, + ) + caplog.set_level("DEBUG", logger=color_hybrid.__name__) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(hybrid_fake, False, None, session) + + assert actual.sheets == legacy + assert actual.sheets["Sheet"].colors_map == { + "AA1100": [(1, 0)], + "0011AA": [(2, 0)], + } + assert hybrid_fake.sheets[0].display_calls == [ + (1, 1), + (2, 1), + (1, 1), + (2, 1), + ] + assert cells._strict_display_format.get() is False + assert any( + "rendered candidate extraction failed: transient DisplayFormat failure" + in record.getMessage() + for record in caplog.records + ) + log = next( + record.getMessage() + for record in caplog.records + if "Color extraction sheet=Sheet" in record.getMessage() + ) + assert "fallback=True" in log + assert "display_format_calls=4" in log + + +@pytest.mark.parametrize( + "raw_case", + ["extLst", "unknown-rule", "foreign-namespace", "pivotTableParts"], +) +def test_untrusted_conditional_formatting_xml_uses_legacy_fallback( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + raw_case: str, +) -> None: + path = tmp_path / f"raw-{raw_case}.xlsx" + + def configure(workbook: Workbook) -> None: + worksheet = workbook.active + worksheet["A1"] = "value" + _add_expression_rule(worksheet) + + _save_workbook(path, configure) + fake = _fake_workbook(path) + legacy = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + + def mutate(raw: bytes) -> bytes: + if raw_case == "extLst": + marker = b"" + replacement = ( + b'" + ) + elif raw_case == "pivotTableParts": + marker = b"" + replacement = ( + b'" + ) + elif raw_case == "unknown-rule": + marker = b'type="expression"' + replacement = b'type="futureRule"' + else: + marker = b"" + assert closing in raw + raw = raw.replace(closing, b"", 1) + return raw + + _rewrite_sheet_xml(path, "Sheet", mutate) + caplog.set_level("DEBUG", logger=color_hybrid.__name__) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(fake, True, None, session) + + assert actual.sheets == legacy + assert fake.sheets[0].display_calls == [(1, 1)] + assert any( + "Color extraction legacy fallback sheet=Sheet" in record.getMessage() + for record in caplog.records + ) + log = next( + record.getMessage() + for record in caplog.records + if "Color extraction sheet=Sheet" in record.getMessage() + ) + assert "fallback=True" in log + + +@pytest.mark.parametrize("fallback_case", ["unsaved", "session-mismatch", "non-xlsx"]) +def test_unavailable_or_incompatible_saved_workbook_falls_back_to_legacy( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + fallback_case: str, +) -> None: + path = tmp_path / "target.xlsx" + + def configure(workbook: Workbook) -> None: + worksheet = workbook.active + worksheet["A1"] = "start" + worksheet["C2"].fill = PatternFill("solid", fgColor="FFABCDEF") + + _save_workbook(path, configure) + fullname = path + saved = fallback_case != "unsaved" + session_path = path + if fallback_case == "non-xlsx": + fullname = tmp_path / "target.xls" + shutil.copyfile(path, fullname) + elif fallback_case == "session-mismatch": + session_path = tmp_path / "other.xlsx" + + def configure_other(workbook: Workbook) -> None: + workbook.active["A1"] = "other" + + _save_workbook(session_path, configure_other) + + fake = _fake_workbook(path, fullname=fullname, saved=saved) + legacy = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + caplog.set_level("DEBUG", logger=color_hybrid.__name__) + + if fallback_case == "non-xlsx": + actual = color_hybrid.extract_colors(fake, True, None, None) + else: + with OpenpyxlExtractionSession(session_path) as session: + actual = color_hybrid.extract_colors(fake, True, None, session) + + assert actual.sheets == legacy + assert fake.sheets[0].display_calls == [ + (1, 1), + (1, 2), + (1, 3), + (2, 1), + (2, 2), + (2, 3), + ] + log = next( + record.getMessage() + for record in caplog.records + if "Color extraction sheet=Sheet" in record.getMessage() + ) + assert "fallback=True" in log + + +def test_fallback_is_isolated_to_the_sheet_with_unsupported_xml( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + path = tmp_path / "sheet-isolation.xlsx" + + def configure(workbook: Workbook) -> None: + broken = workbook.active + broken.title = "Broken" + broken["A1"] = "fallback" + _add_expression_rule(broken) + healthy = workbook.create_sheet("Healthy") + healthy["A1"].fill = PatternFill("solid", fgColor="FF123ABC") + + _save_workbook(path, configure) + fake = _fake_workbook(path) + legacy = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + _rewrite_sheet_xml( + path, + "Broken", + lambda raw: raw.replace( + b"", + b'", + 1, + ), + ) + caplog.set_level("DEBUG", logger=color_hybrid.__name__) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(fake, True, None, session) + + assert actual.sheets["Broken"] == legacy["Broken"] + assert actual.sheets["Healthy"] == legacy["Healthy"] + assert fake.sheets[0].display_calls == [(1, 1)] + assert fake.sheets[1].display_calls == [] + sheet_logs = { + name: next( + record.getMessage() + for record in caplog.records + if f"Color extraction sheet={name}" in record.getMessage() + ) + for name in ("Broken", "Healthy") + } + assert "fallback=True" in sheet_logs["Broken"] + assert "fallback=False" in sheet_logs["Healthy"] + + +def test_pipeline_com_color_step_reuses_shared_session_until_owner_closes( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + path = tmp_path / "shared-session.xlsx" + + def configure(workbook: Workbook) -> None: + workbook.active["A1"] = "one" + second = workbook.create_sheet("Second") + second["B2"] = "two" + + _save_workbook(path, configure) + fake = _fake_workbook(path) + legacy = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + session = OpenpyxlExtractionSession(path) + real_workbook = session.workbook + workbook_calls: list[bool] = [] + loaded_workbook = None + close_calls: list[int] = [] + + def tracked_workbook(*, data_only: bool = True) -> object: + nonlocal loaded_workbook + workbook = real_workbook(data_only=data_only) + workbook_calls.append(data_only) + if loaded_workbook is None: + loaded_workbook = workbook + original_close = workbook.close + + def close() -> None: + close_calls.append(1) + original_close() + + workbook.close = close + return workbook + + monkeypatch.setattr(session, "workbook", tracked_workbook) + inputs = pipeline.resolve_extraction_inputs( + path, + mode="standard", + include_cell_links=False, + include_print_areas=False, + include_auto_page_breaks=False, + include_colors_map=True, + include_default_background=True, + ignore_colors=set(), + include_formulas_map=False, + include_merged_cells=False, + include_merged_values_in_rows=False, + ) + artifacts = pipeline.ExtractionArtifacts(openpyxl_session=session) + backend_type = pipeline._backend_type("ComBackend") + original_init = backend_type.__init__ + backend_sessions: list[OpenpyxlExtractionSession | None] = [] + + def tracked_init( + backend: object, + workbook: object, + session: OpenpyxlExtractionSession | None = None, + ) -> None: + backend_sessions.append(session) + original_init(backend, workbook, session=session) + + monkeypatch.setattr(backend_type, "__init__", tracked_init) + with session: + pipeline.step_extract_colors_map_com(inputs, artifacts, fake) + assert backend_sessions == [session] + assert workbook_calls == [True, True] + assert loaded_workbook is real_workbook() + assert artifacts.colors_map_data is not None + assert artifacts.colors_map_data.sheets == legacy + assert all(sheet.display_calls == [] for sheet in fake.sheets) + assert close_calls == [] + assert not session._closed + + assert session._closed + assert close_calls == [1] + + +def test_custom_normal_fill_uses_rendered_legacy_fallback( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + path = tmp_path / "custom-normal.xlsx" + + def configure(workbook: Workbook) -> None: + workbook._named_styles["Normal"].fill = PatternFill("solid", fgColor="FF334455") + workbook.active["A1"] = "inherits Normal" + workbook.active["B2"] = "extends COM range" + + _save_workbook(path, configure) + normal_color = _excel_color("334455") + fake = _fake_workbook( + path, + used_ranges={"Sheet": (1, 1, 2, 2)}, + rendered_overrides={ + "Sheet": { + (1, 1): normal_color, + (1, 2): normal_color, + (2, 1): normal_color, + (2, 2): normal_color, + } + }, + ) + legacy = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + caplog.set_level("DEBUG", logger=color_hybrid.__name__) + + with OpenpyxlExtractionSession(path) as session: + actual = color_hybrid.extract_colors(fake, True, None, session) + + assert actual.sheets == legacy + assert actual.sheets["Sheet"].colors_map == { + "334455": [(1, 0), (1, 1), (2, 0), (2, 1)] + } + assert fake.sheets[0].display_calls == [ + (1, 1), + (1, 2), + (2, 1), + (2, 2), + ] + assert any( + "Color extraction legacy fallback sheet=Sheet" in record.getMessage() + and "custom Normal fill" in record.getMessage() + for record in caplog.records + ) + + +def test_standalone_extraction_closes_the_session_it_creates( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + path = tmp_path / "owned-session.xlsx" + + def configure(workbook: Workbook) -> None: + workbook.active["A1"] = "value" + + _save_workbook(path, configure) + fake = _fake_workbook(path) + sessions: list[OpenpyxlExtractionSession] = [] + base_session = color_hybrid.OpenpyxlExtractionSession + + class TrackedSession(base_session): + def __init__(self, file_path: Path) -> None: + super().__init__(file_path) + sessions.append(self) + + monkeypatch.setattr(color_hybrid, "OpenpyxlExtractionSession", TrackedSession) + color_hybrid.extract_colors(fake, True, None, None) + + assert len(sessions) == 1 + assert sessions[0]._closed + assert sessions[0]._workbooks == {} + + +@pytest.mark.parametrize("kind", ["merge", "table", "row", "column"]) +def test_ambiguous_sheet_styles_preserve_legacy_scan( + tmp_path: Path, + kind: str, +) -> None: + path = tmp_path / f"ambiguous-{kind}.xlsx" + + def configure(workbook: Workbook) -> None: + ws = workbook.active + ws.append(["Heading", "Value"]) + ws.append(["A", 1]) + fill = PatternFill("solid", fgColor="FF112233") + if kind == "merge": + ws.merge_cells("A1:B1") + elif kind == "table": + ws.add_table(Table(displayName="Data", ref="A1:B2")) + elif kind == "row": + ws.row_dimensions[1].fill = fill + else: + ws.column_dimensions["A"].fill = fill + + _save_workbook(path, configure) + fake = _fake_workbook( + path, rendered_overrides={"Sheet": {(1, 1): _excel_color("112233")}} + ) + expected = _legacy_result(fake, include_default=True, ignore_colors=None) + _clear_display_calls(fake) + actual = color_hybrid.extract_colors(fake, True, None, None) + assert actual.sheets == expected + assert len(fake.sheets[0].display_calls) == 4 + + +@pytest.mark.parametrize("kind", ["automatic", "gradient"]) +def test_special_fills_require_excel_rendering(tmp_path: Path, kind: str) -> None: + path = tmp_path / f"special-{kind}.xlsx" + + def configure(workbook: Workbook) -> None: + workbook.active["A1"].fill = ( + PatternFill("solid", fgColor=Color(auto=True)) + if kind == "automatic" + else GradientFill(stop=("FF112233", "FF445566")) + ) + + _save_workbook(path, configure) + fake = _fake_workbook( + path, rendered_overrides={"Sheet": {(1, 1): _excel_color("112233")}} + ) + actual = color_hybrid.extract_colors(fake, False, None, None) + assert actual.sheets["Sheet"].colors_map == {"112233": [(1, 0)]} + assert fake.sheets[0].display_calls == [(1, 1)] + + +@pytest.mark.parametrize("problem", ["missing", "corrupt"]) +def test_unreadable_saved_file_uses_live_legacy_colors( + tmp_path: Path, problem: str +) -> None: + path = tmp_path / "unreadable.xlsx" + _save_workbook(path, lambda wb: wb.active.__setitem__("A1", "value")) + fake = _fake_workbook( + path, rendered_overrides={"Sheet": {(1, 1): _excel_color("112233")}} + ) + if problem == "missing": + path.unlink() + else: + path.write_bytes(b"invalid ZIP") + actual = color_hybrid.extract_colors(fake, False, None, None) + assert actual.sheets["Sheet"].colors_map == {"112233": [(1, 0)]} + assert fake.sheets[0].display_calls == [(1, 1)] + + +@pytest.mark.parametrize("include_default", [False, True]) +def test_sparse_static_colors_do_not_materialize_missing_cells( + include_default: bool, +) -> None: + """Default backgrounds must not enlarge the shared worksheet cell cache.""" + workbook = Workbook() + ws = workbook.active + ws["B2"].fill = PatternFill("solid", fgColor="FF123456") + ws["C3"] = "unfilled" + ws["D4"].fill = PatternFill("solid", fgColor=Color(theme=1)) + ws["E5"].fill = PatternFill("solid", fgColor="FFFFFFFF") + ws["F6"].fill = PatternFill("solid", fgColor="FFABCDEF") + stored = dict(ws._cells) + candidates = {(1, 1)} + try: + actual = color_hybrid._static_colors( + ws, (1, 1, 5, 5), candidates, include_default + ) + assert ws._cells == stored + assert candidates == {(1, 1), (4, 4)} + assert actual[2, 2] == "123456" + assert (6, 6) not in actual + assert (1, 1) not in actual + assert (4, 4) not in actual + if include_default: + assert len(actual) == 23 + assert actual[1, 2] == actual[3, 3] == actual[5, 5] == "FFFFFF" + else: + assert actual == {(2, 2): "123456"} + finally: + workbook.close() + + +def test_large_sparse_static_range_only_retains_colored_saved_cells() -> None: + """A near-full-width UsedRange must not allocate its absent coordinates.""" + workbook = Workbook() + ws = workbook.active + ws["A1"] = "start" + ws["XFD100000"].fill = PatternFill("solid", fgColor="FF123456") + try: + actual = color_hybrid._static_colors(ws, (1, 1, 100000, 16384), set(), False) + assert actual == {(100000, 16384): "123456"} + assert len(ws._cells) == 2 + finally: + workbook.close() + + +@pytest.mark.parametrize("saved_state", [False, "unreadable"]) +def test_sheet_preparation_invalidates_current_and_remaining_saved_fills( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + saved_state: bool | str, +) -> None: + """Sheet event changes force live colors even if a later sheet appears saved.""" + path = tmp_path / "events.xlsm" + + def configure(workbook: Workbook) -> None: + workbook.active.title = "Before" + for name in ("Changed", "After"): + workbook.create_sheet(name) + for ws in workbook.worksheets: + ws["A1"].fill = PatternFill("solid", fgColor="FF0000FF") + + _save_workbook(path, configure) + fake = _fake_workbook(path) + original_prepare = cells._prepare_sheet_for_display_format + + class UnreadableSaved: + @property + def Saved(self) -> bool: # noqa: N802 - Excel COM property + raise RuntimeError("Saved unavailable") + + def prepare(sheet: _FakeSheet) -> None: + original_prepare(sheet) + if sheet.name == "Changed": + sheet.rendered_colors[1, 1] = _excel_color("FF0000") + fake.sheets[2].rendered_colors[1, 1] = _excel_color("00FF00") + fake.api = ( + SimpleNamespace(Saved=False) + if saved_state is False + else UnreadableSaved() + ) + elif sheet.name == "After": + fake.api = SimpleNamespace(Saved=True) + + monkeypatch.setattr(cells, "_prepare_sheet_for_display_format", prepare) + actual = color_hybrid.extract_colors(fake, False, None, None) + assert actual.sheets["Before"].colors_map == {"0000FF": [(1, 0)]} + assert actual.sheets["Changed"].colors_map == {"FF0000": [(1, 0)]} + assert actual.sheets["After"].colors_map == {"00FF00": [(1, 0)]} + assert [sheet.display_calls for sheet in fake.sheets] == [[], [(1, 1)], [(1, 1)]] diff --git a/tests/core/test_issue159_benchmark.py b/tests/core/test_issue159_benchmark.py new file mode 100644 index 0000000..07ec1a8 --- /dev/null +++ b/tests/core/test_issue159_benchmark.py @@ -0,0 +1,74 @@ +"""No-Excel regressions for the Issue 159 benchmark metrics.""" + +import logging + +from _pytest.monkeypatch import MonkeyPatch +from benchmark import issue159_colors + + +def test_measure_preserves_legacy_counter_and_omits_hybrid_false_zero( + monkeypatch: MonkeyPatch, +) -> None: + """Keep the legacy counter while omitting the hybrid per-sheet false zero.""" + counter = issue159_colors.cells._display_format_calls + active_strategy = "reference" + + def fake_display_format_color(_sheet: object, _row: int, _col: int) -> int: + if active_strategy == "reference": + counter.set(counter.get() + 1) + return 255 + + def fake_reference_extract( + _book: object, + _include_default_background: bool, + _ignore_colors: set[str] | None, + ) -> None: + issue159_colors.cells._get_display_format_color(None, 1, 1) + + def fake_hybrid_extract( + _book: object, + *, + include_default_background: bool, + ignore_colors: set[str] | None, + ) -> None: + issue159_colors.cells._get_display_format_color(None, 1, 1) + logging.getLogger("exstruct.core.color_hybrid").debug( + "synthetic hybrid measurement", + extra={"display_format_calls": 1}, + ) + # The real hybrid extractor resets this per-sheet counter after logging. + counter.set(0) + + monkeypatch.setattr( + issue159_colors.cells, "_get_display_format_color", fake_display_format_color + ) + monkeypatch.setattr(issue159_colors, "_reference_extract", fake_reference_extract) + monkeypatch.setattr( + issue159_colors.cells, "extract_sheet_colors_map_com", fake_hybrid_extract + ) + + outer_token = counter.set(37) + try: + legacy_record, _ = issue159_colors._measure( + object(), "reference", False, None, "static", 1 + ) + + assert counter.get() == 37 + active_strategy = "hybrid" + hybrid_record, _ = issue159_colors._measure( + object(), "hybrid", False, None, "static", 1 + ) + + assert counter.get() == 37 + assert legacy_record["wrapper_display_format_calls"] == 1 + assert legacy_record["cells_context_display_format_calls"] == 1 + assert hybrid_record["wrapper_display_format_calls"] == 1 + assert "cells_context_display_format_calls" not in hybrid_record + assert ( + hybrid_record["hybrid_debug_records"][0]["metric_fields"][ + "display_format_calls" + ] + == 1 + ) + finally: + counter.reset(outer_token) diff --git a/uv.lock b/uv.lock index 5cb2c80..7a54471 100644 --- a/uv.lock +++ b/uv.lock @@ -921,7 +921,7 @@ wheels = [ [[package]] name = "exstruct" -version = "0.8.2" +version = "0.9.0" source = { editable = "." } dependencies = [ { name = "defusedxml" },