You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@poi.apache.org by fa...@apache.org on 2026/09/20 10:44:58 UTC

(poi) branch trunk updated: FormulaEvaluator: tests for IStabilityClassifier (#1311)

This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/poi.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 05acd5f7cc FormulaEvaluator: tests for IStabilityClassifier (#1311)
05acd5f7cc is described below

commit 05acd5f7ccdf25859a95830c2538c458ddd63f32
Author: PJ Fanning <pj...@users.noreply.github.com>
AuthorDate: Sun Sep 20 11:44:53 2026 +0100

    FormulaEvaluator: tests for IStabilityClassifier (#1311)
    
    The classifier is consulted in one place (WorkbookEvaluator.evaluateAny)
    and no test asserted what it does there: SheetRangeEvaluatorTest only
    uses TOTALLY_IMMUTABLE to construct an evaluator and
    BaseTestForkedEvaluator's custom classifier does not affect its
    assertions.
    
    Add BaseTestFormulaEvaluatorStabilityClassifier (HSSF and XSSF
    subclasses) on the shared fixture: TOTALLY_IMMUTABLE gives the same
    results and clearAllCachedResultValues still works; TOTALLY_IMMUTABLE
    records no dependencies, so notifyUpdateCell/notifySetFormula do not
    reach dependents; a selective classifier tracks only non-final cells;
    a final formula cell with non-final inputs stays tracked (and one whose
    inputs are all final does not); the classifier is asked about every
    referenced cell with the right sheet/row/column and about no other.
    
    BaseTestFormulaEvaluatorFixture gets a createEvaluator() hook so a
    subclass can supply the evaluator under test.
    
    Co-authored-by: Claude Opus 5 <no...@anthropic.com>
---
 ...estXSSFFormulaEvaluatorStabilityClassifier.java |  35 ++++
 ...estHSSFFormulaEvaluatorStabilityClassifier.java |  35 ++++
 .../usermodel/BaseTestFormulaEvaluatorFixture.java |   7 +-
 ...aseTestFormulaEvaluatorStabilityClassifier.java | 188 +++++++++++++++++++++
 4 files changed, 264 insertions(+), 1 deletion(-)

diff --git a/poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluatorStabilityClassifier.java b/poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluatorStabilityClassifier.java
new file mode 100644
index 0000000000..ef3e157514
--- /dev/null
+++ b/poi-ooxml/src/test/java/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluatorStabilityClassifier.java
@@ -0,0 +1,35 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.xssf.usermodel;
+
+import org.apache.poi.ss.formula.IStabilityClassifier;
+import org.apache.poi.ss.usermodel.BaseTestFormulaEvaluatorStabilityClassifier;
+import org.apache.poi.ss.usermodel.FormulaEvaluator;
+import org.apache.poi.xssf.XSSFITestDataProvider;
+
+final class TestXSSFFormulaEvaluatorStabilityClassifier extends BaseTestFormulaEvaluatorStabilityClassifier {
+
+    TestXSSFFormulaEvaluatorStabilityClassifier() {
+        super(XSSFITestDataProvider.instance);
+    }
+
+    @Override
+    protected FormulaEvaluator createEvaluator(IStabilityClassifier classifier) {
+        return XSSFFormulaEvaluator.create((XSSFWorkbook) wb, classifier, null);
+    }
+}
diff --git a/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFFormulaEvaluatorStabilityClassifier.java b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFFormulaEvaluatorStabilityClassifier.java
new file mode 100644
index 0000000000..90df8925d0
--- /dev/null
+++ b/poi/src/test/java/org/apache/poi/hssf/usermodel/TestHSSFFormulaEvaluatorStabilityClassifier.java
@@ -0,0 +1,35 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hssf.usermodel;
+
+import org.apache.poi.hssf.HSSFITestDataProvider;
+import org.apache.poi.ss.formula.IStabilityClassifier;
+import org.apache.poi.ss.usermodel.BaseTestFormulaEvaluatorStabilityClassifier;
+import org.apache.poi.ss.usermodel.FormulaEvaluator;
+
+final class TestHSSFFormulaEvaluatorStabilityClassifier extends BaseTestFormulaEvaluatorStabilityClassifier {
+
+    TestHSSFFormulaEvaluatorStabilityClassifier() {
+        super(HSSFITestDataProvider.instance);
+    }
+
+    @Override
+    protected FormulaEvaluator createEvaluator(IStabilityClassifier classifier) {
+        return HSSFFormulaEvaluator.create((HSSFWorkbook) wb, classifier, null);
+    }
+}
diff --git a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluatorFixture.java b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluatorFixture.java
index 5af02aee93..144bbafc5e 100644
--- a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluatorFixture.java
+++ b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluatorFixture.java
@@ -139,12 +139,17 @@ public abstract class BaseTestFormulaEvaluatorFixture {
         r9.createCell(0).setCellFormula("A8+A7");
         r9.createCell(1).setCellFormula("Total+C7");
 
-        fe = wb.getCreationHelper().createFormulaEvaluator();
+        fe = createEvaluator();
         // populate the evaluator's cache with every formula result
         fe.evaluateAll();
         assertInitialValues();
     }
 
+    /** the evaluator the tests run against; the default is the workbook's own */
+    protected FormulaEvaluator createEvaluator() {
+        return wb.getCreationHelper().createFormulaEvaluator();
+    }
+
     @AfterEach
     void tearDown() throws IOException {
         wb.close();
diff --git a/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluatorStabilityClassifier.java b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluatorStabilityClassifier.java
new file mode 100644
index 0000000000..d2c8d87faa
--- /dev/null
+++ b/poi/src/test/java/org/apache/poi/ss/usermodel/BaseTestFormulaEvaluatorStabilityClassifier.java
@@ -0,0 +1,188 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.ss.usermodel;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.poi.ss.ITestDataProvider;
+import org.apache.poi.ss.formula.IStabilityClassifier;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests the effect of an {@link IStabilityClassifier} on the {@link FormulaEvaluator}.
+ * <p>
+ * The evaluator records, for every formula it evaluates, which cells the formula read, so that
+ * {@link FormulaEvaluator#notifyUpdateCell(Cell)} on one of those cells can invalidate the cached
+ * result. A classifier lets the caller declare cells "final" — promised not to change — and the
+ * evaluator then skips recording them, which saves time and memory. The tests check that this
+ * changes nothing about the results, and exactly what it changes about invalidation.
+ *
+ * @see BaseTestFormulaEvaluatorFixture for the workbook the tests run against
+ */
+public abstract class BaseTestFormulaEvaluatorStabilityClassifier extends BaseTestFormulaEvaluatorFixture {
+
+    protected BaseTestFormulaEvaluatorStabilityClassifier(ITestDataProvider testDataProvider) {
+        super(testDataProvider);
+    }
+
+    /** @return an evaluator for {@link #wb} that uses the given classifier ({@code null} for none) */
+    protected abstract FormulaEvaluator createEvaluator(IStabilityClassifier classifier);
+
+    /** replaces the fixture's evaluator with one using the given classifier and evaluates everything */
+    private void useClassifier(IStabilityClassifier classifier) {
+        fe = createEvaluator(classifier);
+        fe.evaluateAll();
+    }
+
+    private int dataIndex() {
+        return wb.getSheetIndex(data);
+    }
+
+    private int pricesIndex() {
+        return wb.getSheetIndex(prices);
+    }
+
+    @Test
+    void totallyImmutableGivesTheSameResults() {
+        useClassifier(IStabilityClassifier.TOTALLY_IMMUTABLE);
+        assertInitialValues();
+
+        // and discarding the cache still works as the way to pick up changes
+        cell("A2").setCellValue(40);
+        cell("B2").setCellValue("Gizmo");
+        fe.clearAllCachedResultValues();
+        assertEquals(90, num("A6"), DELTA);
+        assertEquals(3.5, num("D2"), DELTA);
+        assertEquals(40 * 3.5 + 20 * 2.5 + 30 * 3.5, num("D6"), DELTA);
+        assertEquals(93, num("B9"), DELTA);
+    }
+
+    @Test
+    void totallyImmutableDoesNotTrackInputs() {
+        useClassifier(IStabilityClassifier.TOTALLY_IMMUTABLE);
+
+        // the caller promised that cells do not change, so the evaluator recorded no dependencies
+        // and a notification about an input cannot reach the formulas that read it
+        cell("A2").setCellValue(40);
+        fe.notifyUpdateCell(cell("A2"));
+        assertEquals(60, num("A6"), DELTA);
+        assertEquals(167, num("A9"), DELTA);
+
+        cell("Prices!B2").setCellValue(2);
+        fe.notifyUpdateCell(cell("Prices!B2"));
+        assertEquals(1.5, num("D2"), DELTA);
+
+        // a notification about a formula cell still discards that cell's own cached result ...
+        cell("A7").setCellFormula("A6*3");
+        fe.notifySetFormula(cell("A7"));
+        assertEquals(180, num("A7"), DELTA);
+        // ... but nothing recorded that B7 read A7, so B7 keeps its old result
+        assertEquals(141, num("B7"), DELTA);
+    }
+
+    @Test
+    void selectiveClassifierTracksOnlyNonFinalCells() {
+        // the lookup table is declared final, the data sheet is not
+        useClassifier((sheetIndex, rowIndex, columnIndex) -> sheetIndex == pricesIndex());
+        assertInitialValues();
+
+        // changes on the data sheet are tracked and propagate through the whole chain
+        cell("A2").setCellValue(40);
+        fe.notifyUpdateCell(cell("A2"));
+        assertEquals(90, num("A6"), DELTA);
+        assertEquals(215, num("D6"), DELTA);
+        assertEquals(247, num("A9"), DELTA);
+        assertEquals(93, num("B9"), DELTA);
+
+        cell("B2").setCellValue("Gizmo");
+        fe.notifyUpdateCell(cell("B2"));
+        assertEquals(3.5, num("D2"), DELTA);
+        assertEquals("Gizmo-big", str("C8"));
+
+        // changes on the final sheet are not
+        cell("Prices!B4").setCellValue(10);
+        fe.notifyUpdateCell(cell("Prices!B4"));
+        assertEquals(3.5, num("D2"), DELTA);
+        assertEquals(3.5, num("D4"), DELTA);
+    }
+
+    @Test
+    void finalFormulaCellWithNonFinalInputsIsStillTracked() {
+        // the formula block is declared final, the data rows are not: a final formula cell whose
+        // inputs are tracked must itself stay tracked, otherwise the chain would break behind it
+        int firstFormulaRow = 5;
+        useClassifier((sheetIndex, rowIndex, columnIndex) ->
+                sheetIndex != dataIndex() || rowIndex >= firstFormulaRow);
+        assertInitialValues();
+
+        cell("A2").setCellValue(40);
+        fe.notifyUpdateCell(cell("A2"));
+        assertEquals(90, num("A6"), DELTA);
+        assertEquals(180, num("A7"), DELTA);
+        assertEquals(201, num("B7"), DELTA);
+        assertEquals(67, num("A8"), DELTA);
+        assertEquals(247, num("A9"), DELTA);
+        assertEquals(93, num("B9"), DELTA);
+        assertEquals(215, num("D6"), DELTA);
+
+        // whereas a formula cell whose inputs are all final has nothing recorded for it: with
+        // column B declared final as well, D3 reads only final cells (B3 and the Prices sheet),
+        // so a notification about B3 cannot reach it
+        fe = createEvaluator((sheetIndex, rowIndex, columnIndex) ->
+                sheetIndex != dataIndex() || columnIndex == 1 || rowIndex >= firstFormulaRow);
+        fe.evaluateAll();
+        cell("B3").setCellValue("Widget");
+        fe.notifyUpdateCell(cell("B3"));
+        assertEquals(2.5, num("D3"), DELTA);
+    }
+
+    @Test
+    void classifierIsAskedAboutEveryCellRead() {
+        Set<String> asked = new HashSet<>();
+        useClassifier((sheetIndex, rowIndex, columnIndex) -> {
+            asked.add(sheetIndex + ":" + rowIndex + ":" + columnIndex);
+            return false;
+        });
+        assertInitialValues();
+
+        int d = dataIndex();
+        int p = pricesIndex();
+        // plain inputs on the data sheet
+        assertTrue(asked.contains(d + ":1:0"), "A2");
+        assertTrue(asked.contains(d + ":3:2"), "C4");
+        // the lookup table, read through a cross-sheet area reference
+        assertTrue(asked.contains(p + ":1:0"), "Prices!A2");
+        assertTrue(asked.contains(p + ":3:1"), "Prices!B4");
+        // formula cells are classified too, both when evaluated directly and via the defined name
+        assertTrue(asked.contains(d + ":5:0"), "A6");
+        assertTrue(asked.contains(d + ":7:1"), "B8");
+        // cells nothing refers to are never asked about
+        assertFalse(asked.contains(d + ":0:0"), "Data header");
+        assertFalse(asked.contains(p + ":0:0"), "Prices header");
+        assertFalse(asked.contains(d + ":4:0"), "spare row");
+        for (String key : asked) {
+            int sheet = Integer.parseInt(key.substring(0, key.indexOf(':')));
+            assertTrue(sheet == d || sheet == p, "unexpected sheet index in " + key);
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@poi.apache.org
For additional commands, e-mail: commits-help@poi.apache.org