set_tests_properties(): Add DIRECTORY option
diff --git a/Help/command/set_tests_properties.rst b/Help/command/set_tests_properties.rst
index 1df9d73..da750e3 100644
--- a/Help/command/set_tests_properties.rst
+++ b/Help/command/set_tests_properties.rst
@@ -14,6 +14,16 @@
 :manual:`generator expressions <cmake-generator-expressions(7)>`
 for tests created by the :command:`add_test(NAME)` signature.
 
+.. versionadded:: 3.28
+  Visibility can be set in other directory scopes using the following option:
+
+  ``DIRECTORY <dir>``
+    The test properties will be set in the ``<dir>`` directory's scope.
+    CMake must already know about this directory, either by having added it
+    through a call to :command:`add_subdirectory` or it being the top level
+    source directory. Relative paths are treated as relative to the current
+    source directory. ``<dir>`` may reference a binary directory.
+
 See Also
 ^^^^^^^^
 
diff --git a/Help/release/dev/test-properties-directory.rst b/Help/release/dev/test-properties-directory.rst
index 7331ac3..7a36372 100644
--- a/Help/release/dev/test-properties-directory.rst
+++ b/Help/release/dev/test-properties-directory.rst
@@ -4,3 +4,6 @@
 * The ``TEST`` mode of the :command:`set_property` command gained a
   ``DIRECTORY`` sub-option, which allows you to set properties on tests in
   other directories.
+* The :command:`set_tests_properties` command gained a ``DIRECTORY``
+  sub-option, which allows you to set properties on tests in other
+  directories.
diff --git a/Source/cmSetTestsPropertiesCommand.cxx b/Source/cmSetTestsPropertiesCommand.cxx
index a17c964..2ac445c 100644
--- a/Source/cmSetTestsPropertiesCommand.cxx
+++ b/Source/cmSetTestsPropertiesCommand.cxx
@@ -5,9 +5,15 @@
 #include <algorithm>
 #include <iterator>
 
+#include <cmext/string_view>
+
+#include "cmArgumentParser.h"
 #include "cmExecutionStatus.h"
+#include "cmGlobalGenerator.h"
 #include "cmMakefile.h"
+#include "cmRange.h"
 #include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
 #include "cmTest.h"
 
 bool cmSetTestsPropertiesCommand(std::vector<std::string> const& args,
@@ -31,9 +37,29 @@
     return false;
   }
 
+  std::vector<std::string> tests;
+  std::string directory;
+  cmArgumentParser<void> parser;
+  parser.Bind("DIRECTORY"_s, directory);
+  auto result = parser.Parse(cmStringRange{ args.begin(), propsIter }, &tests);
+
+  cmMakefile* mf = &status.GetMakefile();
+  if (result.MaybeReportError(*mf)) {
+    return false;
+  }
+  if (!directory.empty()) {
+    std::string absDirectory = cmSystemTools::CollapseFullPath(
+      directory, mf->GetCurrentSourceDirectory());
+    mf = mf->GetGlobalGenerator()->FindMakefile(absDirectory);
+    if (!mf) {
+      status.SetError(cmStrCat("given non-existent DIRECTORY ", directory));
+      return false;
+    }
+  }
+
   // loop over all the tests
-  for (const std::string& tname : cmStringRange{ args.begin(), propsIter }) {
-    if (cmTest* test = status.GetMakefile().GetTest(tname)) {
+  for (const std::string& tname : tests) {
+    if (cmTest* test = mf->GetTest(tname)) {
       // loop through all the props and set them
       for (auto k = propsIter + 1; k != args.end(); k += 2) {
         if (!k->empty()) {
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index f8b84b2..8562e2b 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -1064,6 +1064,7 @@
   )
 
 add_RunCMake_test(VerifyHeaderSets)
+add_RunCMake_test(set_tests_properties)
 
 if(${CMAKE_GENERATOR} MATCHES "Make|Ninja")
   add_RunCMake_test(TransformDepfile)
diff --git a/Tests/RunCMake/set_tests_properties/CMakeLists.txt b/Tests/RunCMake/set_tests_properties/CMakeLists.txt
new file mode 100644
index 0000000..922aad6
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/CMakeLists.txt
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.27)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)
diff --git a/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid-result.txt b/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid-stderr.txt b/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid-stderr.txt
new file mode 100644
index 0000000..e219399
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid-stderr.txt
@@ -0,0 +1,13 @@
+^CMake Error at DIRECTORY-invalid\.cmake:[0-9]+ \(set_tests_properties\):
+  Error after keyword "DIRECTORY":
+
+    missing required value
+
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
+
+
+CMake Error at DIRECTORY-invalid\.cmake:[0-9]+ \(set_tests_properties\):
+  set_tests_properties given non-existent DIRECTORY nonexistent
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)$
diff --git a/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid.cmake b/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid.cmake
new file mode 100644
index 0000000..4d87df1
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/DIRECTORY-invalid.cmake
@@ -0,0 +1,4 @@
+enable_testing()
+
+set_tests_properties(t DIRECTORY PROPERTIES PASS_REGULAR_EXPRESSION "Top directory")
+set_tests_properties(t DIRECTORY nonexistent PROPERTIES PASS_REGULAR_EXPRESSION "Top directory")
diff --git a/Tests/RunCMake/set_tests_properties/DIRECTORY-subdir1/CMakeLists.txt b/Tests/RunCMake/set_tests_properties/DIRECTORY-subdir1/CMakeLists.txt
new file mode 100644
index 0000000..b1fad66
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/DIRECTORY-subdir1/CMakeLists.txt
@@ -0,0 +1,3 @@
+add_test(NAME t COMMAND ${CMAKE_COMMAND} -E echo "Subdirectory")
+add_test(NAME t2 COMMAND ${CMAKE_COMMAND} -E echo "Subdirectory")
+add_test(NAME t3 COMMAND ${CMAKE_COMMAND} -E echo "Subdirectory")
diff --git a/Tests/RunCMake/set_tests_properties/DIRECTORY-subdir2/CMakeLists.txt b/Tests/RunCMake/set_tests_properties/DIRECTORY-subdir2/CMakeLists.txt
new file mode 100644
index 0000000..8859597
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/DIRECTORY-subdir2/CMakeLists.txt
@@ -0,0 +1 @@
+set_tests_properties(t3 DIRECTORY ../DIRECTORY-subdir1 PROPERTIES PASS_REGULAR_EXPRESSION "Subdirectory")
diff --git a/Tests/RunCMake/set_tests_properties/DIRECTORY.cmake b/Tests/RunCMake/set_tests_properties/DIRECTORY.cmake
new file mode 100644
index 0000000..87d13e3
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/DIRECTORY.cmake
@@ -0,0 +1,9 @@
+enable_testing()
+
+add_test(NAME t COMMAND ${CMAKE_COMMAND} -E echo "Top directory")
+add_subdirectory(DIRECTORY-subdir1)
+add_subdirectory(DIRECTORY-subdir2)
+
+set_tests_properties(t PROPERTIES PASS_REGULAR_EXPRESSION "Top directory")
+set_tests_properties(t DIRECTORY DIRECTORY-subdir1 PROPERTIES PASS_REGULAR_EXPRESSION "Subdirectory")
+set_tests_properties(t2 DIRECTORY "${CMAKE_BINARY_DIR}/DIRECTORY-subdir1" PROPERTIES PASS_REGULAR_EXPRESSION "Subdirectory")
diff --git a/Tests/RunCMake/set_tests_properties/RunCMakeTest.cmake b/Tests/RunCMake/set_tests_properties/RunCMakeTest.cmake
new file mode 100644
index 0000000..b49158f
--- /dev/null
+++ b/Tests/RunCMake/set_tests_properties/RunCMakeTest.cmake
@@ -0,0 +1,8 @@
+include(RunCMake)
+
+run_cmake(DIRECTORY-invalid)
+
+set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/DIRECTORY-build)
+run_cmake(DIRECTORY)
+set(RunCMake_TEST_NO_CLEAN 1)
+run_cmake_command(DIRECTORY-test ${CMAKE_CTEST_COMMAND} -C Debug)