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