From 5d9f8945c91d81c189e247cb167ee7cfd8019fb9 Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Sat, 26 Jul 2014 15:20:06 +0000 Subject: [PATCH] HSSF and XSSF Multi-Sheet formula reference tests from Radoslav from bug #55906 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1613654 13f79535-47bb-0310-9956-ffa450edef68 --- .../xssf/usermodel/AllXSSFUsermodelTests.java | 1 + .../TestMultiSheetFormulaEvaluatorOnXSSF.java | 334 ++++++++++++++++++ .../usermodel/TestXSSFFormulaEvaluation.java | 45 +++ .../ss/formula/eval/TestMultiSheetEval.java | 331 +++++++++++++++++ test-data/spreadsheet/FormulaSheetRange.xls | Bin 0 -> 22016 bytes test-data/spreadsheet/FormulaSheetRange.xlsx | Bin 0 -> 12949 bytes 6 files changed, 711 insertions(+) create mode 100644 src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java create mode 100644 src/testcases/org/apache/poi/ss/formula/eval/TestMultiSheetEval.java create mode 100644 test-data/spreadsheet/FormulaSheetRange.xls create mode 100644 test-data/spreadsheet/FormulaSheetRange.xlsx diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java index e05b2f9a1e..eb0985d7a9 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/AllXSSFUsermodelTests.java @@ -31,6 +31,7 @@ import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ TestFormulaEvaluatorOnXSSF.class, + TestMultiSheetFormulaEvaluatorOnXSSF.class, TestSheetHiding.class, TestXSSFBugs.class, TestXSSFDataFormat.class, diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java new file mode 100644 index 0000000000..d75961f3cb --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestMultiSheetFormulaEvaluatorOnXSSF.java @@ -0,0 +1,334 @@ +/* ==================================================================== + 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 java.io.InputStream; +import java.io.PrintStream; +import java.util.Collection; + +import junit.framework.Assert; +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.ss.formula.eval.TestFormulasFromSpreadsheet; +import org.apache.poi.ss.formula.functions.TestMathX; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellValue; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * Tests formulas for multi sheet reference (i.e. SUM(Sheet1:Sheet5!A1)) + */ +public final class TestMultiSheetFormulaEvaluatorOnXSSF extends TestCase { + private static final POILogger logger = POILogFactory.getLogger(TestFormulasFromSpreadsheet.class); + + private static final class Result { + public static final int SOME_EVALUATIONS_FAILED = -1; + public static final int ALL_EVALUATIONS_SUCCEEDED = +1; + public static final int NO_EVALUATIONS_FOUND = 0; + } + + /** + * This class defines constants for navigating around the test data spreadsheet used for these tests. + */ + private static final class SS { + + /** + * Name of the test spreadsheet (found in the standard test data folder) + */ + public final static String FILENAME = "FormulaSheetRange.xlsx"; + /** + * Row (zero-based) in the test spreadsheet where the function examples start. + */ + public static final int START_FUNCTIONS_ROW_INDEX = 10; // Row '11' + /** + * Index of the column that contains the function names + */ + public static final int COLUMN_INDEX_FUNCTION_NAME = 0; // Column 'A' + /** + * Index of the column that contains the test names + */ + public static final int COLUMN_INDEX_TEST_NAME = 1; // Column 'B' + /** + * Used to indicate when there are no more functions left + */ + public static final String FUNCTION_NAMES_END_SENTINEL = ""; + + /** + * Index of the column where the test expected value is present + */ + public static final short COLUMN_INDEX_EXPECTED_VALUE = 2; // Column 'C' + /** + * Index of the column where the test actual value is present + */ + public static final short COLUMN_INDEX_ACTUAL_VALUE = 3; // Column 'D' + /** + * Test sheet name (sheet with all test formulae) + */ + public static final String TEST_SHEET_NAME = "test"; + } + + private XSSFWorkbook workbook; + private Sheet sheet; + // Note - multiple failures are aggregated before ending. + // If one or more functions fail, a single AssertionFailedError is thrown at the end + private int _functionFailureCount; + private int _functionSuccessCount; + private int _evaluationFailureCount; + private int _evaluationSuccessCount; + + private static void confirmExpectedResult(String msg, Cell expected, CellValue actual) { + if (expected == null) { + throw new AssertionFailedError(msg + " - Bad setup data expected value is null"); + } + if(actual == null) { + throw new AssertionFailedError(msg + " - actual value was null"); + } + + switch (expected.getCellType()) { + case Cell.CELL_TYPE_BLANK: + assertEquals(msg, Cell.CELL_TYPE_BLANK, actual.getCellType()); + break; + case Cell.CELL_TYPE_BOOLEAN: + assertEquals(msg, Cell.CELL_TYPE_BOOLEAN, actual.getCellType()); + assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue()); + break; + case Cell.CELL_TYPE_ERROR: + assertEquals(msg, Cell.CELL_TYPE_ERROR, actual.getCellType()); + if(false) { // TODO: fix ~45 functions which are currently returning incorrect error values + assertEquals(msg, expected.getErrorCellValue(), actual.getErrorValue()); + } + break; + case Cell.CELL_TYPE_FORMULA: // will never be used, since we will call method after formula evaluation + throw new AssertionFailedError("Cannot expect formula as result of formula evaluation: " + msg); + case Cell.CELL_TYPE_NUMERIC: + assertEquals(msg, Cell.CELL_TYPE_NUMERIC, actual.getCellType()); + TestMathX.assertEquals(msg, expected.getNumericCellValue(), actual.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); +// double delta = Math.abs(expected.getNumericCellValue()-actual.getNumberValue()); +// double pctExpected = Math.abs(0.00001*expected.getNumericCellValue()); +// assertTrue(msg, delta <= pctExpected); + break; + case Cell.CELL_TYPE_STRING: + assertEquals(msg, Cell.CELL_TYPE_STRING, actual.getCellType()); + assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getStringValue()); + break; + } + } + + + protected void setUp() throws Exception { + if (workbook == null) { + InputStream is = HSSFTestDataSamples.openSampleFileStream(SS.FILENAME); + OPCPackage pkg = OPCPackage.open(is); + workbook = new XSSFWorkbook( pkg ); + sheet = workbook.getSheet( SS.TEST_SHEET_NAME ); + } + _functionFailureCount = 0; + _functionSuccessCount = 0; + _evaluationFailureCount = 0; + _evaluationSuccessCount = 0; + } + + public void testFunctionsFromTestSpreadsheet() { + + processFunctionGroup(SS.START_FUNCTIONS_ROW_INDEX, null); + + // confirm results + String successMsg = "There were " + + _evaluationSuccessCount + " successful evaluation(s) and " + + _functionSuccessCount + " function(s) without error"; + if(_functionFailureCount > 0) { + String msg = _functionFailureCount + " function(s) failed in " + + _evaluationFailureCount + " evaluation(s). " + successMsg; + throw new AssertionFailedError(msg); + } + logger.log(POILogger.INFO, getClass().getName() + ": " + successMsg); + } + + /** + * @param startRowIndex row index in the spreadsheet where the first function/operator is found + * @param testFocusFunctionName name of a single function/operator to test alone. + * Typically pass null to test all functions + */ + private void processFunctionGroup(int startRowIndex, String testFocusFunctionName) { + FormulaEvaluator evaluator = new XSSFFormulaEvaluator(workbook); + + int rowIndex = startRowIndex; + while (true) { + Row r = sheet.getRow(rowIndex); + + // only evaluate non empty row + if( r != null ) + { + String targetFunctionName = getTargetFunctionName(r); + String targetTestName = getTargetTestName(r); + if(targetFunctionName == null) { + throw new AssertionFailedError("Test spreadsheet cell empty on row (" + + (rowIndex+1) + "). Expected function name or '" + + SS.FUNCTION_NAMES_END_SENTINEL + "'"); + } + if(targetFunctionName.equals(SS.FUNCTION_NAMES_END_SENTINEL)) { + // found end of functions list + break; + } + if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) { + + // expected results are on the row below + Cell expectedValueCell = r.getCell(SS.COLUMN_INDEX_EXPECTED_VALUE); + if(expectedValueCell == null) { + int missingRowNum = rowIndex + 1; + throw new AssertionFailedError("Missing expected values cell for function '" + + targetFunctionName + ", test" + targetTestName + " (row " + + missingRowNum + ")"); + } + + switch(processFunctionRow(evaluator, targetFunctionName, targetTestName, r, expectedValueCell)) { + case Result.ALL_EVALUATIONS_SUCCEEDED: _functionSuccessCount++; break; + case Result.SOME_EVALUATIONS_FAILED: _functionFailureCount++; break; + default: + throw new RuntimeException("unexpected result"); + case Result.NO_EVALUATIONS_FOUND: // do nothing + break; + } + } + } + rowIndex ++; + } + } + + /** + * + * @return a constant from the local Result class denoting whether there were any evaluation + * cases, and whether they all succeeded. + */ + private int processFunctionRow(FormulaEvaluator evaluator, String targetFunctionName, + String targetTestName, Row formulasRow, Cell expectedValueCell) { + + int result = Result.NO_EVALUATIONS_FOUND; // so far + + Cell c = formulasRow.getCell(SS.COLUMN_INDEX_ACTUAL_VALUE); + if (c == null || c.getCellType() != Cell.CELL_TYPE_FORMULA) { + return result; + } + + CellValue actualValue = evaluator.evaluate(c); + + try { + confirmExpectedResult("Function '" + targetFunctionName + "': Test: '" + targetTestName + "' Formula: " + c.getCellFormula() + + " @ " + formulasRow.getRowNum() + ":" + SS.COLUMN_INDEX_ACTUAL_VALUE, + expectedValueCell, actualValue); + _evaluationSuccessCount ++; + if(result != Result.SOME_EVALUATIONS_FAILED) { + result = Result.ALL_EVALUATIONS_SUCCEEDED; + } + } catch (AssertionFailedError e) { + _evaluationFailureCount ++; + printShortStackTrace(System.err, e); + result = Result.SOME_EVALUATIONS_FAILED; + } + + return result; + } + + /** + * Useful to keep output concise when expecting many failures to be reported by this test case + */ + private static void printShortStackTrace(PrintStream ps, AssertionFailedError e) { + StackTraceElement[] stes = e.getStackTrace(); + + int startIx = 0; + // skip any top frames inside junit.framework.Assert + while(startIx= endIx) { + // something went wrong. just print the whole stack trace + e.printStackTrace(ps); + } + endIx -= 4; // skip 4 frames of reflection invocation + ps.println(e.toString()); + for(int i=startIx; inull if cell is missing, empty or blank + */ + private static String getTargetFunctionName(Row r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out function name"); + return null; + } + Cell cell = r.getCell(SS.COLUMN_INDEX_FUNCTION_NAME); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + SS.COLUMN_INDEX_FUNCTION_NAME + ", can't figure out function name"); + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_BLANK) { + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'function name' column: (" + + cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")"); + } + /** + * @return null if cell is missing, empty or blank + */ + private static String getTargetTestName(Row r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out test name"); + return null; + } + Cell cell = r.getCell(SS.COLUMN_INDEX_TEST_NAME); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + SS.COLUMN_INDEX_TEST_NAME + ", can't figure out test name"); + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_BLANK) { + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'test name' column: (" + + cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")"); + } + +} diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java index aaba1643d6..e5cf432fbe 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFFormulaEvaluation.java @@ -287,4 +287,49 @@ public final class TestXSSFFormulaEvaluation extends BaseTestFormulaEvaluator { assertEquals("4.0", evaluator.evaluate(countFA).formatAsString()); } } + + public void testMultisheetFormulaEval() { + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sheet1 = wb.createSheet("Sheet1"); + XSSFSheet sheet2 = wb.createSheet("Sheet2"); + XSSFSheet sheet3 = wb.createSheet("Sheet3"); + + // sheet1 A1 + XSSFCell cell = sheet1.createRow(0).createCell(0); + cell.setCellType(Cell.CELL_TYPE_NUMERIC); + cell.setCellValue(1.0); + + // sheet2 A1 + cell = sheet2.createRow(0).createCell(0); + cell.setCellType(Cell.CELL_TYPE_NUMERIC); + cell.setCellValue(1.0); + + // sheet2 B1 + cell = sheet2.getRow(0).createCell(1); + cell.setCellType(Cell.CELL_TYPE_NUMERIC); + cell.setCellValue(1.0); + + // sheet3 A1 + cell = sheet3.createRow(0).createCell(0); + cell.setCellType(Cell.CELL_TYPE_NUMERIC); + cell.setCellValue(1.0); + + // sheet1 A2 formulae + cell = sheet1.createRow(1).createCell(0); + cell.setCellType(Cell.CELL_TYPE_FORMULA); + cell.setCellFormula("SUM(Sheet1:Sheet3!A1)"); + + // sheet1 A3 formulae + cell = sheet1.createRow(2).createCell(0); + cell.setCellType(Cell.CELL_TYPE_FORMULA); + cell.setCellFormula("SUM(Sheet1:Sheet3!A1:B1)"); + + wb.getCreationHelper().createFormulaEvaluator().evaluateAll(); + + cell = sheet1.getRow(1).getCell(0); + assertEquals(3.0, cell.getNumericCellValue()); + + cell = sheet1.getRow(2).getCell(0); + assertEquals(4.0, cell.getNumericCellValue()); + } } diff --git a/src/testcases/org/apache/poi/ss/formula/eval/TestMultiSheetEval.java b/src/testcases/org/apache/poi/ss/formula/eval/TestMultiSheetEval.java new file mode 100644 index 0000000000..a3e810474d --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/eval/TestMultiSheetEval.java @@ -0,0 +1,331 @@ +/* ==================================================================== + 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.formula.eval; + +import java.io.PrintStream; +import java.util.Collection; + +import junit.framework.Assert; +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.formula.functions.TestMathX; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellValue; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * Tests formulas for multi sheet reference (i.e. SUM(Sheet1:Sheet5!A1)) + */ +public final class TestMultiSheetEval extends TestCase { + private static final POILogger logger = POILogFactory.getLogger(TestFormulasFromSpreadsheet.class); + + private static final class Result { + public static final int SOME_EVALUATIONS_FAILED = -1; + public static final int ALL_EVALUATIONS_SUCCEEDED = +1; + public static final int NO_EVALUATIONS_FOUND = 0; + } + + /** + * This class defines constants for navigating around the test data spreadsheet used for these tests. + */ + private static final class SS { + + /** + * Name of the test spreadsheet (found in the standard test data folder) + */ + public final static String FILENAME = "FormulaSheetRange.xls"; + /** + * Row (zero-based) in the test spreadsheet where the function examples start. + */ + public static final int START_FUNCTIONS_ROW_INDEX = 10; // Row '11' + /** + * Index of the column that contains the function names + */ + public static final int COLUMN_INDEX_FUNCTION_NAME = 0; // Column 'A' + /** + * Index of the column that contains the test names + */ + public static final int COLUMN_INDEX_TEST_NAME = 1; // Column 'B' + /** + * Used to indicate when there are no more functions left + */ + public static final String FUNCTION_NAMES_END_SENTINEL = ""; + + /** + * Index of the column where the test expected value is present + */ + public static final short COLUMN_INDEX_EXPECTED_VALUE = 2; // Column 'C' + /** + * Index of the column where the test actual value is present + */ + public static final short COLUMN_INDEX_ACTUAL_VALUE = 3; // Column 'D' + /** + * Test sheet name (sheet with all test formulae) + */ + public static final String TEST_SHEET_NAME = "test"; + } + + private HSSFWorkbook workbook; + private Sheet sheet; + // Note - multiple failures are aggregated before ending. + // If one or more functions fail, a single AssertionFailedError is thrown at the end + private int _functionFailureCount; + private int _functionSuccessCount; + private int _evaluationFailureCount; + private int _evaluationSuccessCount; + + private static void confirmExpectedResult(String msg, Cell expected, CellValue actual) { + if (expected == null) { + throw new AssertionFailedError(msg + " - Bad setup data expected value is null"); + } + if(actual == null) { + throw new AssertionFailedError(msg + " - actual value was null"); + } + + switch (expected.getCellType()) { + case Cell.CELL_TYPE_BLANK: + assertEquals(msg, Cell.CELL_TYPE_BLANK, actual.getCellType()); + break; + case Cell.CELL_TYPE_BOOLEAN: + assertEquals(msg, Cell.CELL_TYPE_BOOLEAN, actual.getCellType()); + assertEquals(msg, expected.getBooleanCellValue(), actual.getBooleanValue()); + break; + case Cell.CELL_TYPE_ERROR: + assertEquals(msg, Cell.CELL_TYPE_ERROR, actual.getCellType()); + assertEquals(msg, ErrorEval.getText(expected.getErrorCellValue()), ErrorEval.getText(actual.getErrorValue())); + break; + case Cell.CELL_TYPE_FORMULA: // will never be used, since we will call method after formula evaluation + throw new AssertionFailedError("Cannot expect formula as result of formula evaluation: " + msg); + case Cell.CELL_TYPE_NUMERIC: + assertEquals(msg, Cell.CELL_TYPE_NUMERIC, actual.getCellType()); + TestMathX.assertEquals(msg, expected.getNumericCellValue(), actual.getNumberValue(), TestMathX.POS_ZERO, TestMathX.DIFF_TOLERANCE_FACTOR); + break; + case Cell.CELL_TYPE_STRING: + assertEquals(msg, Cell.CELL_TYPE_STRING, actual.getCellType()); + assertEquals(msg, expected.getRichStringCellValue().getString(), actual.getStringValue()); + break; + } + } + + + protected void setUp() { + if (workbook == null) { + workbook = HSSFTestDataSamples.openSampleWorkbook(SS.FILENAME); + sheet = workbook.getSheet( SS.TEST_SHEET_NAME ); + } + _functionFailureCount = 0; + _functionSuccessCount = 0; + _evaluationFailureCount = 0; + _evaluationSuccessCount = 0; + } + + public void testFunctionsFromTestSpreadsheet() { + + processFunctionGroup(SS.START_FUNCTIONS_ROW_INDEX, null); + + // confirm results + String successMsg = "There were " + + _evaluationSuccessCount + " successful evaluation(s) and " + + _functionSuccessCount + " function(s) without error"; + if(_functionFailureCount > 0) { + String msg = _functionFailureCount + " function(s) failed in " + + _evaluationFailureCount + " evaluation(s). " + successMsg; + throw new AssertionFailedError(msg); + } + logger.log(POILogger.INFO, getClass().getName() + ": " + successMsg); + } + + /** + * @param startRowIndex row index in the spreadsheet where the first function/operator is found + * @param testFocusFunctionName name of a single function/operator to test alone. + * Typically pass null to test all functions + */ + private void processFunctionGroup(int startRowIndex, String testFocusFunctionName) { + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(workbook); + Collection funcs = FunctionEval.getSupportedFunctionNames(); + + int rowIndex = startRowIndex; + while (true) { + Row r = sheet.getRow(rowIndex); + + // only evaluate non empty row + if( r != null ) + { + String targetFunctionName = getTargetFunctionName(r); + String targetTestName = getTargetTestName(r); + if(targetFunctionName == null) { + throw new AssertionFailedError("Test spreadsheet cell empty on row (" + + (rowIndex+1) + "). Expected function name or '" + + SS.FUNCTION_NAMES_END_SENTINEL + "'"); + } + if(targetFunctionName.equals(SS.FUNCTION_NAMES_END_SENTINEL)) { + // found end of functions list + break; + } + if(testFocusFunctionName == null || targetFunctionName.equalsIgnoreCase(testFocusFunctionName)) { + + // expected results are on the row below + Cell expectedValueCell = r.getCell(SS.COLUMN_INDEX_EXPECTED_VALUE); + if(expectedValueCell == null) { + int missingRowNum = rowIndex + 1; + throw new AssertionFailedError("Missing expected values cell for function '" + + targetFunctionName + ", test" + targetTestName + " (row " + + missingRowNum + ")"); + } + + switch(processFunctionRow(evaluator, targetFunctionName, targetTestName, r, expectedValueCell)) { + case Result.ALL_EVALUATIONS_SUCCEEDED: _functionSuccessCount++; break; + case Result.SOME_EVALUATIONS_FAILED: _functionFailureCount++; break; + default: + throw new RuntimeException("unexpected result"); + case Result.NO_EVALUATIONS_FOUND: // do nothing + String uname = targetFunctionName.toUpperCase(); + if(startRowIndex >= SS.START_FUNCTIONS_ROW_INDEX && + funcs.contains(uname)) { + logger.log(POILogger.WARN, uname + ": function is supported but missing test data"); + } + break; + } + } + } + rowIndex ++; + } + } + + /** + * + * @return a constant from the local Result class denoting whether there were any evaluation + * cases, and whether they all succeeded. + */ + private int processFunctionRow(HSSFFormulaEvaluator evaluator, String targetFunctionName, + String targetTestName, Row formulasRow, Cell expectedValueCell) { + + int result = Result.NO_EVALUATIONS_FOUND; // so far + + Cell c = formulasRow.getCell(SS.COLUMN_INDEX_ACTUAL_VALUE); + if (c == null || c.getCellType() != Cell.CELL_TYPE_FORMULA) { + return result; + } + + CellValue actualValue = evaluator.evaluate(c); + + try { + confirmExpectedResult("Function '" + targetFunctionName + "': Test: '" + targetTestName + "' Formula: " + c.getCellFormula() + + " @ " + formulasRow.getRowNum() + ":" + SS.COLUMN_INDEX_ACTUAL_VALUE, + expectedValueCell, actualValue); + _evaluationSuccessCount ++; + if(result != Result.SOME_EVALUATIONS_FAILED) { + result = Result.ALL_EVALUATIONS_SUCCEEDED; + } + } catch (AssertionFailedError e) { + _evaluationFailureCount ++; + printShortStackTrace(System.err, e); + result = Result.SOME_EVALUATIONS_FAILED; + } + + return result; + } + + /** + * Useful to keep output concise when expecting many failures to be reported by this test case + */ + private static void printShortStackTrace(PrintStream ps, AssertionFailedError e) { + StackTraceElement[] stes = e.getStackTrace(); + + int startIx = 0; + // skip any top frames inside junit.framework.Assert + while(startIx= endIx) { + // something went wrong. just print the whole stack trace + e.printStackTrace(ps); + } + endIx -= 4; // skip 4 frames of reflection invocation + ps.println(e.toString()); + for(int i=startIx; inull if cell is missing, empty or blank + */ + private static String getTargetFunctionName(Row r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out function name"); + return null; + } + Cell cell = r.getCell(SS.COLUMN_INDEX_FUNCTION_NAME); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + SS.COLUMN_INDEX_FUNCTION_NAME + ", can't figure out function name"); + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_BLANK) { + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'function name' column: (" + + cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")"); + } + /** + * @return null if cell is missing, empty or blank + */ + private static String getTargetTestName(Row r) { + if(r == null) { + System.err.println("Warning - given null row, can't figure out test name"); + return null; + } + Cell cell = r.getCell(SS.COLUMN_INDEX_TEST_NAME); + if(cell == null) { + System.err.println("Warning - Row " + r.getRowNum() + " has no cell " + SS.COLUMN_INDEX_TEST_NAME + ", can't figure out test name"); + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_BLANK) { + return null; + } + if(cell.getCellType() == Cell.CELL_TYPE_STRING) { + return cell.getRichStringCellValue().getString(); + } + + throw new AssertionFailedError("Bad cell type for 'test name' column: (" + + cell.getCellType() + ") row (" + (r.getRowNum() +1) + ")"); + } + +} diff --git a/test-data/spreadsheet/FormulaSheetRange.xls b/test-data/spreadsheet/FormulaSheetRange.xls new file mode 100644 index 0000000000000000000000000000000000000000..bbac5f239b01c3722d1e264c7ecd1c9eff7481c4 GIT binary patch literal 22016 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 NJf5buVDvw({s;8~n^yn; literal 0 HcmV?d00001 -- 2.39.5