| /* |
| * 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.datasketches.common; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.file.AccessDeniedException; |
| import java.nio.file.Files; |
| import java.nio.file.NoSuchFileException; |
| import java.nio.file.Path; |
| import java.util.Objects; |
| |
| /** |
| * Utilities common to testing |
| */ |
| public final class UtilityIO { |
| private static final String LS = System.getProperty("line.separator"); |
| private static final Class<?> clazz = UtilityIO.class; |
| private static final ClassLoader CL = clazz.getClassLoader(); |
| private static final String TEST_DATA_ROOT_PROP = "test.data.root"; |
| |
| /** |
| * TestNG group constants |
| */ |
| public static final String GENERATE_JAVA_FILES = "generate_java_files"; |
| public static final String CHECK_JAVA_FILES = "check_java_files"; |
| public static final String CHECK_CPP_FILES = "check_cpp_files"; |
| public static final String CHECK_GO_FILES = "check_go_files"; |
| public static final String CHECK_RUST_FILES = "check_rust_files"; |
| public static final String CHECK_CPP_HISTORICAL_FILES = "check_cpp_historical_files"; |
| |
| /** |
| * Enables diagnostic printing of file names read or written. |
| * Set via Maven: <i>mvn clean test -Pcheck_java_files -Dprint=true</i> |
| */ |
| private static final boolean printFileNames = Boolean.parseBoolean(System.getProperty("print", "false")); |
| |
| public enum Existence { MUST_EXIST, WARNING } |
| |
| /** |
| * For passing variables to a multi-language test method leveraging TestNG Groups. |
| * |
| * <p>Usage in a method would look like this:</p> |
| * {@snippet : |
| * public void testMethod(GpLanguage lang) { |
| * String suffix = lang.sfx; |
| * Path path = lang.pth; |
| * ... |
| * } |
| * } |
| */ |
| public enum GroupLanguage { |
| JAVA("_java", getJavaPath()), |
| CPP("_cpp", getCppPath()), |
| GO("_go", getGoPath()), |
| RUST("_rust", getRustPath()); |
| |
| |
| public final String sfx; |
| public final Path pth; |
| |
| GroupLanguage(String suffix, Path path) { |
| this.sfx = suffix; |
| this.pth = path; |
| } |
| } |
| |
| /** |
| * The project relative Path for Java serialized sketches to be tested by other languages. |
| */ |
| public static Path getJavaPath() { |
| return getTestDataRoot().resolve("serialization_test_data", "java_generated_files"); |
| } |
| |
| /** |
| * The project relative Path for C++ serialized sketches to be tested by Java. |
| */ |
| public static Path getCppPath() { |
| return getTestDataRoot().resolve("serialization_test_data", "cpp_generated_files"); |
| } |
| |
| /** |
| * The project relative Path for Go serialized sketches to be tested by Java. |
| */ |
| public static Path getGoPath() { |
| return getTestDataRoot().resolve("serialization_test_data", "go_generated_files"); |
| } |
| |
| /** |
| * The project relative Path for Rust serialized sketches to be tested by Java. |
| */ |
| public static Path getRustPath() { |
| return getTestDataRoot().resolve("serialization_test_data", "rust_generated_files"); |
| } |
| |
| /** |
| * Gets all the bytes of a file as a byte array. |
| * If the file is missing this issues a Warning message to the console. |
| * @param basePath the directory where <i>fileName</i> is located. |
| * @param fileName the simple file name of the file. |
| * @return a byte array. It will be empty if file not found. |
| * @throws RuntimeException for IO errors. |
| */ |
| public static byte[] getFileBytes(final Path basePath, final String fileName) { |
| return getFileBytes(basePath, fileName, Existence.WARNING); |
| } |
| |
| /** |
| * Gets all the bytes of a file as a byte array. |
| * If the file is missing this issues a Warning message to the console or throws a RuntimeException |
| * based on the state of Existence. |
| * @param basePath the directory where <i>fileName</i> is located. |
| * @param fileName the simple file name of the file. |
| * @return a byte array. It will be empty if file not found and Existence is <i>WARNING</i> |
| * @throws RuntimeException for IO errors, |
| */ |
| public static byte[] getFileBytes(final Path basePath, final String fileName, final Existence existence) { |
| Objects.requireNonNull(basePath, "input parameter 'Path basePath' cannot be null."); |
| Objects.requireNonNull(fileName, "input parameter 'String fileName' cannot be null."); |
| Objects.requireNonNull(existence, "input parameter 'Existence existence' cannot be null."); |
| |
| Path path = basePath.resolve(fileName).normalize(); |
| String pathDisplay; |
| try { |
| pathDisplay = path.toAbsolutePath().toString(); |
| } catch (SecurityException e) { |
| pathDisplay = path.toString(); |
| } |
| |
| try { |
| final byte[] bytes = Files.readAllBytes(path); |
| A_BeforeSuite.incReadCount(); |
| if (printFileNames) { System.out.println("R\t "+fileName); } |
| return bytes; |
| } catch (NoSuchFileException e) { |
| if (existence == Existence.MUST_EXIST) { |
| throw new RuntimeException("File not found: " + pathDisplay, e); |
| } |
| A_BeforeSuite.incWarnings(); |
| System.err.println("WARNING: File not found: " + pathDisplay); |
| return new byte[0]; |
| } catch (AccessDeniedException e) { |
| throw new RuntimeException("Permission denied reading file: " + pathDisplay, e); |
| } catch (IOException e) { |
| throw new RuntimeException("IO Error reading file: " + pathDisplay, e); |
| } |
| } |
| |
| /** |
| * This gets a byte[] from a resource file in /src/test/resources whether called on an install or a jar. |
| * These files must exist. |
| * @param fileName the desired filename |
| * @return the bytes of the file as a byte[] |
| * @throws IllegalArgumentException if file not found. |
| */ |
| public static byte[] getTestResourceBytes(final String fileName) { |
| Objects.requireNonNull(fileName, "fileName cannot be null"); |
| // Strip leading slash if present, as ClassLoader doesn't accept leading slashes |
| String resourcePath = fileName.startsWith("/") ? fileName.substring(1) : fileName; |
| try (InputStream is = CL.getResourceAsStream(resourcePath)) { |
| if (is == null) { |
| throw new IllegalArgumentException("Resource not found on classpath: " + resourcePath); |
| } |
| final byte[] bytes = is.readAllBytes(); |
| A_BeforeSuite.incReadCount(); |
| if (printFileNames) { System.out.println("R\t "+fileName); } |
| return bytes; |
| } catch (final IOException e){ |
| throw new RuntimeException("Error reading resource: " + resourcePath, e); |
| } |
| } |
| |
| /** |
| * Puts all the bytes of the given byte array to a file with the given fileName. |
| * This assumes that the base directory path is {@link #javaPath javaPath}. |
| * @param fileName the name of the target file |
| * @param bytes the given byte array |
| */ |
| public static void putBytesToJavaPath(final String fileName, final byte[] bytes) { |
| putBytesToFile(getJavaPath(), fileName, bytes); |
| } |
| |
| /** |
| * Puts all the bytes of the given byte array to a basePath file with the given fileName. |
| * If the file exists it will be overwritten. Called from putBytesToJavaPath() and TestUtilTest. |
| * |
| * <p>Used by putBytesToJavaPath(...) |
| * @param basePath the directory path for the given fileName |
| * @param fileName the name of the target file |
| * @param bytes the given byte array |
| * @throws RuntimeException for IO errors, |
| */ |
| static void putBytesToFile(final Path basePath, final String fileName, final byte[] bytes) { |
| Objects.requireNonNull(basePath, "input parameter 'Path basePath' cannot be null."); |
| Objects.requireNonNull(fileName, "input parameter 'String fileName' cannot be null."); |
| Objects.requireNonNull(bytes, "input parameter 'byte[] bytes' cannot be null."); |
| Path filePath = null; |
| try { |
| Files.createDirectories(basePath); //create the directory if it doesn't exist. |
| filePath = basePath.resolve(fileName); |
| Files.write(filePath, bytes); |
| A_BeforeSuite.incWriteCount(); |
| if (printFileNames) { System.out.println("W\t " + fileName); } |
| } catch (IOException e) { |
| final String filePathDesc = String.valueOf(filePath); |
| throw new RuntimeException("System IO Error writing file: " + filePathDesc + " " + e); |
| } |
| } |
| |
| /** |
| * Resolves the target directory for test data reading/writing. |
| * The default is the project root, but it can be overridden like this: |
| * |
| * <p><i>mvn clean test -Pcheck-cpp-files -Dtest.data.root="/tmp/custom_path</i></p> |
| * |
| * <p>Calling the binary test jar: see src/test/resources/testng.xml</p> |
| * |
| * Used by getJavaPath(), getCppPath(), getGoPath(), getRustPath() |
| * |
| * @return Path to the test.data.root directory. |
| * @throws IllegalStateException if running from a JAR without -Dtest.data.root defined. |
| */ |
| private static Path getTestDataRoot() { |
| // Explicit user override or Maven-injected property |
| String sysProp = System.getProperty(TEST_DATA_ROOT_PROP); |
| if (sysProp != null && !sysProp.isBlank()) { |
| return Path.of(sysProp).toAbsolutePath().normalize(); |
| } |
| |
| checkExecutionFromJar(TEST_DATA_ROOT_PROP); |
| |
| // Fallback for local source checkout / IDE runs (walk up to top-level .mvn) |
| return getProjectRoot(); |
| } |
| |
| /** |
| * Finds the top-level project root by walking up from the working directory to the |
| * first '.mvn' directory, since Maven has no built-in way to locate it in a |
| * multi-module hierarchy. Internal fallback for {@link #getTestDataRoot()}. |
| * |
| * @return Path to the project root directory. |
| * @throws IllegalStateException if no '.mvn' directory is found. |
| */ |
| private static Path getProjectRoot() { |
| Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath(); |
| while (current != null) { |
| if (Files.exists(current.resolve(".mvn"))) { |
| return current.normalize(); |
| } |
| current = current.getParent(); |
| } |
| throw new IllegalStateException("Could not find project root containing '.mvn' directory"); |
| } |
| |
| private static void checkExecutionFromJar(final String root) { |
| var url = clazz.getResource(clazz.getSimpleName() + ".class"); |
| if (url != null && "jar".equals(url.getProtocol())) { |
| throw new IllegalStateException( |
| "Executing from a packaged JAR file requires an explicit target directory." + LS + |
| "Please specify the output directory using: -D" + root + "=/path/to/directory" |
| ); |
| } |
| } |
| |
| } |