From 4d8e8e61474519319c921b9a2c55ed8dddcb0fdc Mon Sep 17 00:00:00 2001 From: Hector Castejon Diaz Date: Fri, 21 Aug 2026 10:45:26 +0000 Subject: [PATCH] Make test fixtures portable to non-Maven build layouts TestOSUtils.resource() located a fixture via getResource() and chmod-ed it in place. That only works when test resources are exploded on disk as writable files (Maven's target/test-classes); it fails when they are served from a jar or a read-only tree (e.g. building the SDK with Bazel), where File.setExecutable returns false and the test fails. DatabricksConfigTest.testConfigFileScopes had the same fragility, hardcoding HOME to the relative path "src/test/resources/testdata", which only resolves from the module root. Keep the in-place behavior when the resource is a writable file on disk, so the returned path stays under target/test-classes and the tests' prefix-relative path assertions (StaticEnv) still hold. Only when it cannot be chmod-ed in place -- served from a jar or a read-only tree -- copy the resource (a single file or a whole directory subtree, preserving its path) into a writable temp directory, chmod the copy, and return that. Route testConfigFileScopes through the same helper. Behavior under `mvn test` is unchanged. Signed-off-by: Hector Castejon Diaz Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 5 ++ .../sdk/core/DatabricksConfigTest.java | 3 +- .../sdk/core/utils/TestOSUtils.java | 89 +++++++++++++++++-- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index d079d80cb..d94e5260a 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -14,4 +14,9 @@ ### Internal Changes +- Make unit-test fixtures portable to non-Maven build layouts. `TestOSUtils.resource` now stages a + resource to a writable temp directory when it cannot be chmod-ed in place (e.g. when served + read-only from a jar), while keeping the existing in-place behavior for Maven's exploded + `target/test-classes`. This lets the unit tests run unchanged under other build systems. + ### API Changes diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java index da9e2c788..f1c885065 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/DatabricksConfigTest.java @@ -12,6 +12,7 @@ import com.databricks.sdk.core.oauth.Token; import com.databricks.sdk.core.oauth.TokenSource; import com.databricks.sdk.core.utils.Environment; +import com.databricks.sdk.core.utils.TestOSUtils; import java.io.IOException; import java.time.Duration; import java.util.ArrayList; @@ -351,7 +352,7 @@ private static Stream provideConfigFileScopesTestCases() { @MethodSource("provideConfigFileScopesTestCases") public void testConfigFileScopes(String testName, String profile, List expectedScopes) { Map env = new HashMap<>(); - env.put("HOME", "src/test/resources/testdata"); + env.put("HOME", TestOSUtils.resource("/testdata")); DatabricksConfig config = new DatabricksConfig().setProfile(profile); config.resolve(new Environment(env, new ArrayList<>(), System.getProperty("os.name"))); diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/TestOSUtils.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/TestOSUtils.java index e4c19c021..ff088efd4 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/TestOSUtils.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/utils/TestOSUtils.java @@ -3,7 +3,18 @@ import static org.junit.jupiter.api.Assertions.fail; import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Collections; +import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,20 +34,84 @@ public static String getTestDir() { return testDir.replace("/", File.separator); } + /** + * Returns an absolute filesystem path to a classpath resource -- a single file or a whole + * directory subtree -- with the executable bit set, staging a writable copy only when needed. + * + *

Tests use the returned path as HOME (a fixture directory holding a .databrickscfg) or as the + * path to a fake CLI executable, and some assert on it after stripping a fixed {@code user.dir + + * /target/test-classes/} prefix (see StaticEnv). When the resource is already an on-disk, + * writable file -- Maven explodes test resources into target/test-classes -- we chmod it in place + * and return that path, so those prefix-relative assertions still hold. When it cannot be + * chmod-ed in place (served read-only from a jar or from Bazel's runfiles), we copy it to a fresh + * temp directory, preserving the resource path, and chmod the copy. + */ public static String resource(String file) { URL resource = TestOSUtils.class.getResource(file); if (resource == null) { fail("Asset not found: " + file); } + try { + URI uri = resource.toURI(); + if ("file".equals(uri.getScheme())) { + // Exploded on disk (Maven): chmod in place and keep the original path, so path-based + // assertions relative to target/test-classes still match. + Path path = Paths.get(uri); + if (setExecutableRecursively(path)) { + return path.toString(); + } + // Read-only file: resource (e.g. Bazel runfiles): fall through to the temp-copy path. + } + // Preserve the full resource path under the temp root (not just the basename): several + // tests assert on a path substring like "testdata/corrupt/.databrickscfg". + String relativePath = file.startsWith("/") ? file.substring(1) : file; + Path dest = Files.createTempDirectory("databricks-sdk-test").resolve(relativePath); + if ("jar".equals(uri.getScheme())) { + // Resource lives inside a jar: mount the jar as a filesystem and copy the entry out. + String[] parts = uri.toString().split("!", 2); + try (FileSystem fs = + FileSystems.newFileSystem( + URI.create(parts[0]), Collections.emptyMap())) { + copyRecursively(fs.getPath(parts[1]), dest); + } + } else { + copyRecursively(Paths.get(uri), dest); + } + setExecutableRecursively(dest); + return dest.toString(); + } catch (IOException | URISyntaxException e) { + fail("Failed to stage test asset " + file + ": " + e.getMessage()); + return null; // unreachable: fail() throws + } + } - String filePath = resource.getFile(); - File f = new File(filePath); - - // Make the file executable - if (!f.setExecutable(true)) { - fail("Failed to set the file as executable: " + file); + private static void copyRecursively(Path source, Path dest) throws IOException { + try (Stream paths = Files.walk(source)) { + for (Path path : (Iterable) paths::iterator) { + Path target = dest.resolve(source.relativize(path).toString()); + if (Files.isDirectory(path)) { + Files.createDirectories(target); + } else { + Files.createDirectories(target.getParent()); + Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING); + } + } } + } - return filePath; + /** + * Sets the executable bit on {@code root} and everything under it, returning whether all + * succeeded. + */ + private static boolean setExecutableRecursively(Path root) throws IOException { + boolean ok = true; + try (Stream paths = Files.walk(root)) { + for (Path path : (Iterable) paths::iterator) { + if (!path.toFile().setExecutable(true)) { + ok = false; + } + } + } + return ok; } }