From 44efecf42ed62aba1d6c9e149c3683bfd19f6453 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Sat, 16 Jan 2021 15:51:00 +0000 Subject: [PATCH] integration tests: Fix handling of NullPointerExceptions for Java 16+ (again ...) Refactor TestAllFiles to provide an API for mass testing git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1885576 13f79535-47bb-0310-9956-ffa450edef68 --- .../org/apache/poi/stress/ExcInfo.java | 96 ++++++++ .../apache/poi/stress/FileHandlerKnown.java | 60 +++++ .../org/apache/poi/stress/StressMap.java | 150 ++++++++++++ .../org/apache/poi/stress/TestAllFiles.java | 226 +++--------------- test-data/spreadsheet/stress.xls | Bin 38400 -> 37888 bytes 5 files changed, 339 insertions(+), 193 deletions(-) create mode 100644 src/integrationtest/org/apache/poi/stress/ExcInfo.java create mode 100644 src/integrationtest/org/apache/poi/stress/FileHandlerKnown.java create mode 100644 src/integrationtest/org/apache/poi/stress/StressMap.java diff --git a/src/integrationtest/org/apache/poi/stress/ExcInfo.java b/src/integrationtest/org/apache/poi/stress/ExcInfo.java new file mode 100644 index 0000000000..c9241cb0f7 --- /dev/null +++ b/src/integrationtest/org/apache/poi/stress/ExcInfo.java @@ -0,0 +1,96 @@ +/* ==================================================================== + 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.stress; + +import static org.junit.jupiter.api.Assertions.fail; + +public class ExcInfo { + private static final String IGNORED_TESTS = "IGNORE"; + + private String file; + private String tests; + private String handler; + private String password; + private Class exClazz; + private String exMessage; + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getTests() { + return tests; + } + + public void setTests(String tests) { + this.tests = tests; + } + + public String getHandler() { + return handler; + } + + public void setHandler(String handler) { + this.handler = handler; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Class getExClazz() { + return exClazz; + } + + @SuppressWarnings("unchecked") + public void setExClazz(String exClazz) { + try { + this.exClazz = (Class) Class.forName(exClazz); + } catch (ClassNotFoundException ex) { + fail(ex); + } + } + + public String getExMessage() { + return exMessage; + } + + public void setExMessage(String exMessage) { + this.exMessage = exMessage; + } + + public boolean isMatch(String testName, String handler) { + return + (tests == null || tests.contains(testName) || IGNORED_TESTS.equals(tests)) && + (this.handler == null || this.handler.contains(handler)); + } + + public boolean isValid(String testName, String handler) { + return + !IGNORED_TESTS.equals(tests) && + (tests == null || tests.contains(testName)) && + (this.handler == null || this.handler.contains(handler)); + } +} diff --git a/src/integrationtest/org/apache/poi/stress/FileHandlerKnown.java b/src/integrationtest/org/apache/poi/stress/FileHandlerKnown.java new file mode 100644 index 0000000000..aa3c827f1f --- /dev/null +++ b/src/integrationtest/org/apache/poi/stress/FileHandlerKnown.java @@ -0,0 +1,60 @@ +/* ==================================================================== + 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.stress; + +import java.io.File; +import java.io.InputStream; +import java.util.function.Supplier; + +@SuppressWarnings("unused") +public enum FileHandlerKnown { + HDGF(HDGFFileHandler::new), + HMEF(HMEFFileHandler::new), + HPBF(HPBFFileHandler::new), + HPSF(HPSFFileHandler::new), + HSLF(HSLFFileHandler::new), + HSMF(HSMFFileHandler::new), + HSSF(HSSFFileHandler::new), + HWPF(HWPFFileHandler::new), + OPC(OPCFileHandler::new), + POIFS(POIFSFileHandler::new), + XDGF(XDGFFileHandler::new), + XSLF(XSLFFileHandler::new), + XSSFB(XSSFBFileHandler::new), + XSSF(XSSFFileHandler::new), + XWPF(XWPFFileHandler::new), + OWPF(OWPFFileHandler::new), + NULL(NullFileHandler::new) + ; + + public final Supplier fileHandler; + + FileHandlerKnown(Supplier fileHandler) { + this.fileHandler = fileHandler; + } + + private static class NullFileHandler implements FileHandler { + @Override + public void handleFile(InputStream stream, String path) {} + + @Override + public void handleExtracting(File file) {} + + @Override + public void handleAdditional(File file) {} + } +} diff --git a/src/integrationtest/org/apache/poi/stress/StressMap.java b/src/integrationtest/org/apache/poi/stress/StressMap.java new file mode 100644 index 0000000000..73d39b8530 --- /dev/null +++ b/src/integrationtest/org/apache/poi/stress/StressMap.java @@ -0,0 +1,150 @@ +/* ==================================================================== + 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.stress; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; + +public class StressMap { + private final MultiValuedMap exMap = new ArrayListValuedHashMap<>(); + private final Map handlerMap = new LinkedHashMap<>(); + + + public void load(File mapFile) throws IOException { + try (Workbook wb = WorkbookFactory.create(mapFile)) { + readExMap(wb.getSheet("Exceptions")); + readHandlerMap(wb.getSheet("Handlers")); + } + } + + public List getHandler(String file) { + // ... failures/handlers lookup doesn't work on windows otherwise + final String uniFile = file.replace('\\', '/'); + + String firstHandler = handlerMap.entrySet().stream() + .filter(me -> uniFile.endsWith(me.getKey())) + .map(Map.Entry::getValue).findFirst().orElse("NULL"); + + return Stream.of(firstHandler, secondHandler(firstHandler)) + .filter(h -> !"NULL".equals(h)) + .map(FileHandlerKnown::valueOf) + .collect(Collectors.toList()); + } + + public ExcInfo getExcInfo(String file, String testName, FileHandlerKnown handler) { + return exMap.get(file).stream() + .filter(e -> e.isMatch(testName, handler.name())) + .findFirst().orElse(null); + } + + public void readHandlerMap(Sheet sh) { + if (sh == null) { + return; + } + + handlerMap.clear(); + + boolean IGNORE_SCRATCHPAD = Boolean.getBoolean("scratchpad.ignore"); + boolean isFirst = true; + for (Row row : sh) { + if (isFirst) { + isFirst = false; + continue; + } + Cell cell = row.getCell(2); + if (IGNORE_SCRATCHPAD || cell == null || cell.getCellType() != CellType.STRING) { + cell = row.getCell(1); + } + handlerMap.put(row.getCell(0).getStringCellValue(), cell.getStringCellValue()); + } + } + + + public void readExMap(Sheet sh) { + if (sh == null) { + return; + } + + exMap.clear(); + + Iterator iter = sh.iterator(); + List> cols = initCols(iter.next()); + + while (iter.hasNext()) { + ExcInfo info = new ExcInfo(); + for (Cell cell : iter.next()) { + if (cell.getCellType() == CellType.STRING) { + cols.get(cell.getColumnIndex()).accept(info, cell.getStringCellValue()); + } + } + exMap.put(info.getFile(), info); + } + } + + private static List> initCols(Row row) { + Map> m = new HashMap<>(); + m.put("File", ExcInfo::setFile); + m.put("Tests", ExcInfo::setTests); + m.put("Handler", ExcInfo::setHandler); + m.put("Password", ExcInfo::setPassword); + m.put("Exception Class", ExcInfo::setExClazz); + m.put("Exception Message", ExcInfo::setExMessage); + + return StreamSupport + .stream(row.spliterator(), false) + .map(Cell::getStringCellValue) + .map(v -> m.getOrDefault(v, (e,s) -> {})) + .collect(Collectors.toList()); + } + + private static String secondHandler(String handlerStr) { + switch (handlerStr) { + case "XSSF": + case "XWPF": + case "XSLF": + case "XDGF": + return "OPC"; + case "HSSF": + case "HWPF": + case "HSLF": + case "HDGF": + case "HSMF": + case "HBPF": + return "HPSF"; + default: + return "NULL"; + } + } +} diff --git a/src/integrationtest/org/apache/poi/stress/TestAllFiles.java b/src/integrationtest/org/apache/poi/stress/TestAllFiles.java index a065309fc6..8c6e7b98ef 100644 --- a/src/integrationtest/org/apache/poi/stress/TestAllFiles.java +++ b/src/integrationtest/org/apache/poi/stress/TestAllFiles.java @@ -31,26 +31,10 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import org.apache.commons.collections4.MultiValuedMap; -import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.ss.usermodel.WorkbookFactory; import org.apache.tools.ant.DirectoryScanner; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.api.parallel.Execution; @@ -98,12 +82,8 @@ public class TestAllFiles { }; public static Stream allfiles(String testName) throws IOException { - MultiValuedMap exMap; - Map handlerMap; - try (Workbook wb = WorkbookFactory.create(new File(ROOT_DIR, "spreadsheet/stress.xls"))) { - exMap = readExMap(wb.getSheet("Exceptions")); - handlerMap = readHandlerMap(wb.getSheet("Handlers")); - } + StressMap sm = new StressMap(); + sm.load(new File(ROOT_DIR, "spreadsheet/stress.xls")); DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir(ROOT_DIR); @@ -113,29 +93,16 @@ public class TestAllFiles { final List result = new ArrayList<>(100); for (String file : scanner.getIncludedFiles()) { - // ... failures/handlers lookup doesn't work on windows otherwise - final String uniFile = file.replace('\\', '/'); - - String firstHandler = handlerMap.entrySet().stream() - .filter(me -> uniFile.endsWith(me.getKey())) - .map(Map.Entry::getValue).findFirst().orElse("NULL"); - - final String[] handlerStr = { firstHandler, secondHandler(firstHandler) }; - for (String hs : handlerStr) { - if ("NULL".equals(hs)) continue; - ExcInfo info1 = exMap.get(file).stream() - .filter(e -> - (e.tests == null || e.tests.contains(testName) || "IGNORE".equals(e.tests)) && - (e.handler == null || e.handler.contains(hs)) - ).findFirst().orElse(null); - - if (info1 == null || !"IGNORE".equals(info1.tests)) { + // if (!file.contains("44958.xls")) continue; + for (FileHandlerKnown handler : sm.getHandler(file)) { + ExcInfo info1 = sm.getExcInfo(file, testName, handler); + if (info1 == null || info1.isValid(testName, handler.name())) { result.add(Arguments.of( file, - hs, - (info1 != null) ? info1.password : null, - (info1 != null) ? info1.exClazz : null, - (info1 != null) ? info1.exMessage : null + handler, + (info1 != null) ? info1.getPassword() : null, + (info1 != null) ? info1.getExClazz() : null, + (info1 != null) ? info1.getExMessage() : null )); } } @@ -150,12 +117,12 @@ public class TestAllFiles { @ParameterizedTest(name = "#{index} {0} {1}") @MethodSource("extractFiles") - void handleExtracting(String file, String handler, String password, Class exClass, String exMessage) throws IOException { + void handleExtracting(String file, FileHandlerKnown handler, String password, Class exClass, String exMessage) throws IOException { System.out.println("Running extractFiles on "+file); - FileHandler fileHandler = Handler.valueOf(handler).fileHandler.get(); + FileHandler fileHandler = handler.fileHandler.get(); assertNotNull(fileHandler, "Did not find a handler for file " + file); Executable exec = () -> fileHandler.handleExtracting(new File(ROOT_DIR, file)); - verify(exec, exClass, exMessage, password); + verify(file, exec, exClass, exMessage, password); } @@ -165,13 +132,13 @@ public class TestAllFiles { @ParameterizedTest(name = "#{index} {0} {1}") @MethodSource("handleFiles") - void handleFile(String file, String handler, String password, Class exClass, String exMessage) throws IOException { + void handleFile(String file, FileHandlerKnown handler, String password, Class exClass, String exMessage) throws IOException { System.out.println("Running handleFiles on "+file); - FileHandler fileHandler = Handler.valueOf(handler).fileHandler.get(); + FileHandler fileHandler = handler.fileHandler.get(); assertNotNull(fileHandler, "Did not find a handler for file " + file); try (InputStream stream = new BufferedInputStream(new FileInputStream(new File(ROOT_DIR, file)), 64 * 1024)) { Executable exec = () -> fileHandler.handleFile(stream, file); - verify(exec, exClass, exMessage, password); + verify(file, exec, exClass, exMessage, password); } } @@ -181,170 +148,43 @@ public class TestAllFiles { @ParameterizedTest(name = "#{index} {0} {1}") @MethodSource("handleAdditionals") - void handleAdditional(String file, String handler, String password, Class exClass, String exMessage) { + void handleAdditional(String file, FileHandlerKnown handler, String password, Class exClass, String exMessage) { System.out.println("Running additionals on "+file); - FileHandler fileHandler = Handler.valueOf(handler).fileHandler.get(); + FileHandler fileHandler = handler.fileHandler.get(); assertNotNull(fileHandler, "Did not find a handler for file " + file); Executable exec = () -> fileHandler.handleAdditional(new File(ROOT_DIR, file)); - verify(exec, exClass, exMessage, password); + verify(file, exec, exClass, exMessage, password); } @SuppressWarnings("unchecked") - private static void verify(Executable exec, Class exClass, String exMessage, String password) { + private static void verify(String file, Executable exec, Class exClass, String exMessage, String password) { + final String errPrefix = file + " - failed. "; // this also removes the password for non encrypted files Biff8EncryptionKey.setCurrentUserPassword(password); if (exClass != null && AssertionFailedError.class.isAssignableFrom(exClass)) { try { exec.execute(); - fail("expected failed assertion"); + fail(errPrefix + "Expected failed assertion"); } catch (AssertionFailedError e) { - assertEquals(exMessage, e.getMessage()); + assertEquals(exMessage, e.getMessage(), errPrefix); } catch (Throwable e) { - fail("unexpected exception", e); + fail(errPrefix + "Unexpected exception", e); } } else if (exClass != null) { Exception e = assertThrows((Class)exClass, exec); String actMsg = e.getMessage(); - if ((NullPointerException.class.isAssignableFrom(exClass) && jreVersion < 16) || exMessage == null) { - assertNull(actMsg); + if (NullPointerException.class.isAssignableFrom(exClass)) { + // with Java 16+ NullPointerExceptions may contain a message ... but apparently not always ?! + assertTrue(jreVersion >= 16 || actMsg == null, errPrefix); + if (actMsg != null) { + assertTrue(actMsg.startsWith(exMessage), errPrefix + "Message: "+actMsg+" - didn't start with "+exMessage); + } } else { - assertNotNull(actMsg); - assertTrue(actMsg.startsWith(exMessage), "Message: "+actMsg+" - didn't start with "+exMessage); + assertNotNull(actMsg, errPrefix); + assertTrue(actMsg.startsWith(exMessage), errPrefix + "Message: "+actMsg+" - didn't start with "+exMessage); } } else { - assertDoesNotThrow(exec); - } - } - - - private static String secondHandler(String handlerStr) { - switch (handlerStr) { - case "XSSF": - case "XWPF": - case "XSLF": - case "XDGF": - return "OPC"; - case "HSSF": - case "HWPF": - case "HSLF": - case "HDGF": - case "HSMF": - case "HBPF": - return "HPSF"; - default: - return "NULL"; - } - } - - private static Map readHandlerMap(Sheet sh) { - Map handlerMap = new LinkedHashMap<>(); - boolean IGNORE_SCRATCHPAD = Boolean.getBoolean("scratchpad.ignore"); - boolean isFirst = true; - for (Row row : sh) { - if (isFirst) { - isFirst = false; - continue; - } - Cell cell = row.getCell(2); - if (IGNORE_SCRATCHPAD || cell == null || cell.getCellType() != CellType.STRING) { - cell = row.getCell(1); - } - handlerMap.put(row.getCell(0).getStringCellValue(), cell.getStringCellValue()); - } - return handlerMap; - } - - - private static MultiValuedMap readExMap(Sheet sh) { - MultiValuedMap exMap = new ArrayListValuedHashMap<>(); - - Iterator iter = sh.iterator(); - List> cols = initCols(iter.next()); - - while (iter.hasNext()) { - ExcInfo info = new ExcInfo(); - for (Cell cell : iter.next()) { - if (cell.getCellType() == CellType.STRING) { - cols.get(cell.getColumnIndex()).accept(info, cell.getStringCellValue()); - } - } - exMap.put(info.file, info); - } - return exMap; - } - - - private static List> initCols(Row row) { - Map> m = new HashMap<>(); - m.put("File", (e,s) -> e.file = s); - m.put("Tests", (e,s) -> e.tests = s); - m.put("Handler", (e,s) -> e.handler = s); - m.put("Password", (e,s) -> e.password = s); - m.put("Exception Class", (e,s) -> { - try { - e.exClazz = (Class) Class.forName(s); - } catch (ClassNotFoundException ex) { - fail(ex); - } - }); - m.put("Exception Message", (e,s) -> e.exMessage = s); - - return StreamSupport - .stream(row.spliterator(), false) - .map(Cell::getStringCellValue) - .map(v -> m.getOrDefault(v, (e,s) -> {})) - .collect(Collectors.toList()); - } - - private static class ExcInfo { - String file; - String tests; - String handler; - String password; - Class exClazz; - String exMessage; - - - } - - @SuppressWarnings("unused") - private enum Handler { - HDGF(HDGFFileHandler::new), - HMEF(HMEFFileHandler::new), - HPBF(HPBFFileHandler::new), - HPSF(HPSFFileHandler::new), - HSLF(HSLFFileHandler::new), - HSMF(HSMFFileHandler::new), - HSSF(HSSFFileHandler::new), - HWPF(HWPFFileHandler::new), - OPC(OPCFileHandler::new), - POIFS(POIFSFileHandler::new), - XDGF(XDGFFileHandler::new), - XSLF(XSLFFileHandler::new), - XSSFB(XSSFBFileHandler::new), - XSSF(XSSFFileHandler::new), - XWPF(XWPFFileHandler::new), - OWPF(OWPFFileHandler::new), - NULL(NullFileHandler::new) - ; - - final Supplier fileHandler; - Handler(Supplier fileHandler) { - this.fileHandler = fileHandler; - } - } - - public static class NullFileHandler implements FileHandler { - @Override - public void handleFile(InputStream stream, String path) { - } - - @Override - public void handleExtracting(File file) { - } - - @Override - public void handleAdditional(File file) { + assertDoesNotThrow(exec, errPrefix); } } } diff --git a/test-data/spreadsheet/stress.xls b/test-data/spreadsheet/stress.xls index 065ad65564bf825eb53dd468be2f636755fd3888..a87e3ff52e1d2114de7c0a2fd7a366b5eb87ea09 100644 GIT binary patch delta 6868 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