From df17bea05d2c547ccbb399c147a9b08484e79e6c Mon Sep 17 00:00:00 2001 From: Carlos Ernesto Alvarez Berumen Date: Fri, 6 Mar 2026 23:16:17 -0600 Subject: [PATCH 1/3] v1 --- .../texera/amber/engine/common/Utils.scala | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala b/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala index dc074c1094d..49d45cc7624 100644 --- a/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala +++ b/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala @@ -22,7 +22,6 @@ package org.apache.texera.amber.engine.common import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState -import java.nio.file.{Files, Path, Paths} import java.util.concurrent.locks.Lock import scala.annotation.tailrec @@ -36,25 +35,52 @@ object Utils extends LazyLogging { * * @return the real absolute path to amber home directory */ + + import java.nio.file.{Files, Path, Paths} + import scala.jdk.CollectionConverters._ + import scala.util.Using + lazy val amberHomePath: Path = { val currentWorkingDirectory = Paths.get(".").toRealPath() - // check if the current directory is the amber home path + if (isAmberHomePath(currentWorkingDirectory)) { currentWorkingDirectory } else { - // from current path's parent directory, search its children to find amber home path - // current max depth is set to 2 (current path's siblings and direct children) - val searchChildren = Files - .walk(currentWorkingDirectory.getParent, 2) - .filter((path: Path) => isAmberHomePath(path)) - .findAny - if (searchChildren.isPresent) { - searchChildren.get - } else { + val parent = Option(currentWorkingDirectory.getParent).getOrElse { throw new RuntimeException( - "Finding texera home path failed. Current working directory is " + currentWorkingDirectory + s"Cannot search for texera home from filesystem root: $currentWorkingDirectory" ) } + + // Pass 1: prefer the closest prefix (deepest ancestor) of currentWorkingDirectory + val closestPrefix: Option[Path] = + Using.resource(Files.walk(parent, 2)) { stream => + stream + .iterator() + .asScala + .filter(path => isAmberHomePath(path)) + .map(_.toRealPath()) // normalize after filtering + .filter(path => path.startsWith(currentWorkingDirectory)) + .maxByOption(_.getNameCount) // deepest prefix = closest ancestor + } + + closestPrefix.getOrElse { + // Pass 2: fallback to any valid match + val anyMatch = + Using.resource(Files.walk(parent, 2)) { stream => + stream + .filter((path: Path) => isAmberHomePath(path)) + .findAny() + } + + if (anyMatch.isPresent) { + anyMatch.get().toRealPath() + } else { + throw new RuntimeException( + s"Finding texera home path failed. Current working directory is $currentWorkingDirectory" + ) + } + } } } val AMBER_HOME_FOLDER_NAME = "amber"; From 0b5968dbc436e6e31857bd3075202acb6d067781 Mon Sep 17 00:00:00 2001 From: Carlos Ernesto Alvarez Berumen Date: Mon, 16 Mar 2026 00:14:47 -0600 Subject: [PATCH 2/3] vCopilot --- .../texera/amber/engine/common/Utils.scala | 66 ++++++------- .../amber/engine/common/UtilsSpec.scala | 98 +++++++++++++++++++ 2 files changed, 128 insertions(+), 36 deletions(-) create mode 100644 amber/src/test/scala/org/apache/texera/amber/engine/common/UtilsSpec.scala diff --git a/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala b/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala index 49d45cc7624..43b0004676d 100644 --- a/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala +++ b/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala @@ -22,65 +22,54 @@ package org.apache.texera.amber.engine.common import com.typesafe.scalalogging.LazyLogging import org.apache.texera.amber.engine.architecture.rpc.controlreturns.WorkflowAggregatedState +import java.nio.file.{Files, Path, Paths} import java.util.concurrent.locks.Lock import scala.annotation.tailrec +import scala.jdk.CollectionConverters._ +import scala.util.Using object Utils extends LazyLogging { /** * Gets the real path of the amber home directory by: - * 1): check if the current directory is texera/amber + * 1): checking whether the current directory is `texera/amber` * if it's not then: - * 2): search the siblings and children to find the texera home path + * 2): searching siblings and children for an amber home path, preferring matches under the + * current working directory before falling back to the first discovered match * * @return the real absolute path to amber home directory */ - - import java.nio.file.{Files, Path, Paths} - import scala.jdk.CollectionConverters._ - import scala.util.Using - lazy val amberHomePath: Path = { - val currentWorkingDirectory = Paths.get(".").toRealPath() + resolveAmberHomePath(Paths.get(".").toRealPath()) + } + + private[common] def resolveAmberHomePath(currentWorkingDirectory: Path): Path = { + val realCurrentWorkingDirectory = currentWorkingDirectory.toRealPath() - if (isAmberHomePath(currentWorkingDirectory)) { - currentWorkingDirectory + if (isAmberHomePath(realCurrentWorkingDirectory)) { + realCurrentWorkingDirectory } else { - val parent = Option(currentWorkingDirectory.getParent).getOrElse { + val parent = Option(realCurrentWorkingDirectory.getParent).getOrElse { throw new RuntimeException( - s"Cannot search for texera home from filesystem root: $currentWorkingDirectory" + s"Cannot search for amber home from filesystem root: $realCurrentWorkingDirectory" ) } - // Pass 1: prefer the closest prefix (deepest ancestor) of currentWorkingDirectory - val closestPrefix: Option[Path] = + val amberCandidates = Using.resource(Files.walk(parent, 2)) { stream => - stream - .iterator() - .asScala - .filter(path => isAmberHomePath(path)) - .map(_.toRealPath()) // normalize after filtering - .filter(path => path.startsWith(currentWorkingDirectory)) - .maxByOption(_.getNameCount) // deepest prefix = closest ancestor + stream.iterator().asScala.flatMap(normalizeAmberHomePath).toVector } - closestPrefix.getOrElse { - // Pass 2: fallback to any valid match - val anyMatch = - Using.resource(Files.walk(parent, 2)) { stream => - stream - .filter((path: Path) => isAmberHomePath(path)) - .findAny() - } - - if (anyMatch.isPresent) { - anyMatch.get().toRealPath() - } else { + // Preserve the current behavior by preferring an amber directory discovered under the CWD. + amberCandidates + .filter(_.startsWith(realCurrentWorkingDirectory)) + .maxByOption(_.getNameCount) + .orElse(amberCandidates.headOption) + .getOrElse { throw new RuntimeException( - s"Finding texera home path failed. Current working directory is $currentWorkingDirectory" + s"Finding amber home path failed. Current working directory is $realCurrentWorkingDirectory" ) } - } } } val AMBER_HOME_FOLDER_NAME = "amber"; @@ -113,7 +102,12 @@ object Utils extends LazyLogging { } private def isAmberHomePath(path: Path): Boolean = { - path.toRealPath().endsWith(AMBER_HOME_FOLDER_NAME) + normalizeAmberHomePath(path).nonEmpty + } + + private def normalizeAmberHomePath(path: Path): Option[Path] = { + val realPath = path.toRealPath() + Option.when(realPath.endsWith(AMBER_HOME_FOLDER_NAME))(realPath) } def aggregatedStateToString(state: WorkflowAggregatedState): String = { diff --git a/amber/src/test/scala/org/apache/texera/amber/engine/common/UtilsSpec.scala b/amber/src/test/scala/org/apache/texera/amber/engine/common/UtilsSpec.scala new file mode 100644 index 00000000000..ff72b8b5b6f --- /dev/null +++ b/amber/src/test/scala/org/apache/texera/amber/engine/common/UtilsSpec.scala @@ -0,0 +1,98 @@ +/* + * 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.texera.amber.engine.common + +import org.scalatest.flatspec.AnyFlatSpec + +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.{FileVisitResult, FileVisitor, FileSystems, Files, Path} + +class UtilsSpec extends AnyFlatSpec { + + "resolveAmberHomePath" should "prefer an amber directory under the current working directory" in + withTempDirectory { tempDirectory => + val preferredRepo = Files.createDirectory(tempDirectory.resolve("preferred-repo")) + val siblingRepo = Files.createDirectory(tempDirectory.resolve("sibling-repo")) + val preferredAmber = Files.createDirectories(preferredRepo.resolve("amber")) + Files.createDirectories(siblingRepo.resolve("amber")) + + assert(Utils.resolveAmberHomePath(preferredRepo) == preferredAmber.toRealPath()) + } + + it should "fall back to a sibling amber directory when the current working directory has none" in + withTempDirectory { tempDirectory => + val repoRoot = Files.createDirectory(tempDirectory.resolve("repo-root")) + val moduleDirectory = Files.createDirectory(repoRoot.resolve("module")) + val amberDirectory = Files.createDirectories(repoRoot.resolve("amber")) + + assert(Utils.resolveAmberHomePath(moduleDirectory) == amberDirectory.toRealPath()) + } + + it should "use amber-specific wording when searching from a filesystem root" in { + val filesystemRoot = FileSystems.getDefault.getRootDirectories.iterator().next().toRealPath() + val exception = intercept[RuntimeException] { + Utils.resolveAmberHomePath(filesystemRoot) + } + + assert(exception.getMessage.contains("amber home")) + } + + private def withTempDirectory(test: Path => Any): Unit = { + val tempDirectory = Files.createTempDirectory("utils-spec-") + try { + test(tempDirectory.toRealPath()) + } finally { + deleteRecursively(tempDirectory) + } + } + + private def deleteRecursively(path: Path): Unit = { + if (!Files.exists(path)) { + return + } + + Files.walkFileTree( + path, + new FileVisitor[Path] { + override def preVisitDirectory( + directory: Path, + attributes: BasicFileAttributes + ): FileVisitResult = FileVisitResult.CONTINUE + + override def visitFile(file: Path, attributes: BasicFileAttributes): FileVisitResult = { + Files.delete(file) + FileVisitResult.CONTINUE + } + + override def visitFileFailed(file: Path, exception: java.io.IOException): FileVisitResult = + throw exception + + override def postVisitDirectory( + directory: Path, + exception: java.io.IOException + ): FileVisitResult = { + Option(exception).foreach(throw _) + Files.delete(directory) + FileVisitResult.CONTINUE + } + } + ) + } +} From 7ac22e7ac4dd154ed25cb5597eaa8cde175febf0 Mon Sep 17 00:00:00 2001 From: carloea2 Date: Mon, 16 Mar 2026 00:52:46 -0600 Subject: [PATCH 3/3] v2Copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: carloea2 --- .../org/apache/texera/amber/engine/common/Utils.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala b/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala index 43b0004676d..ef26447830e 100644 --- a/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala +++ b/amber/src/main/scala/org/apache/texera/amber/engine/common/Utils.scala @@ -60,11 +60,14 @@ object Utils extends LazyLogging { stream.iterator().asScala.flatMap(normalizeAmberHomePath).toVector } + // Sort candidates to avoid dependence on Files.walk traversal order. + val amberCandidatesSorted = amberCandidates.sortBy(_.toString) + // Preserve the current behavior by preferring an amber directory discovered under the CWD. - amberCandidates + amberCandidatesSorted .filter(_.startsWith(realCurrentWorkingDirectory)) .maxByOption(_.getNameCount) - .orElse(amberCandidates.headOption) + .orElse(amberCandidatesSorted.headOption) .getOrElse { throw new RuntimeException( s"Finding amber home path failed. Current working directory is $realCurrentWorkingDirectory"