Repository: nextest-rs/nextest (language: rust)

Issue:
Help filtering test target
I have a module of tests specified as a test target, but I'm not able to filter them using:
- `-E 'package(target_name)'`
- `-E 'test(target_name)'`
- `-E 'package(package_name::target_name)'` (complains 'no packages matched this')
- `-E '/package_name::target_name/'` (complains 'missing expression', 'unparsed input')

Tree:
```
tests/integration
├── basic
│   ├── mod.rs
│   └── test_write.rs
├── Cargo.toml
└── replication
    ├── mod.rs
```
tests/integration/Cargo.toml:
```
[package]
name = "integration"
version = "0.1.0"
edition = "2021"

[[test]]
name = "basic"
path = "basic/mod.rs"

[[test]]
name = "replication"
path = "replication/mod.rs"
```
nextest list:
```
cargo nextest list -p integration    
    Finished test [unoptimized + debuginfo] target(s) in 0.31s
integration::basic:
    ...
integration::replication:
    test_cluster::test_cluster
    ...
```


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 627a63f1e9a..413132a7104 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -314,6 +314,16 @@ dependencies = [
  "regex-automata 0.1.10",
 ]
 
+[[package]]
+name = "bstr"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.14.0"
@@ -824,9 +834,9 @@ dependencies = [
 
 [[package]]
 name = "eyre"
-version = "0.6.8"
+version = "0.6.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
+checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd"
 dependencies = [
  "indenter",
  "once_cell",
@@ -1038,6 +1048,19 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
 
+[[package]]
+name = "globset"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
+dependencies = [
+ "aho-corasick",
+ "bstr 1.8.0",
+ "log",
+ "regex-automata 0.4.3",
+ "regex-syntax 0.8.2",
+]
+
 [[package]]
 name = "goldenfile"
 version = "1.6.0"
@@ -1631,8 +1654,10 @@ version = "0.5.1"
 dependencies = [
  "camino",
  "clap",
+ "globset",
  "guppy",
  "miette",
+ "nextest-metadata",
  "nextest-workspace-hack",
  "nom",
  "nom-tracable",
@@ -1775,7 +1800,6 @@ dependencies = [
  "similar",
  "syn 1.0.109",
  "syn 2.0.39",
- "target-spec",
  "tokio",
  "twox-hash",
  "uuid",
@@ -2659,7 +2683,7 @@ version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597"
 dependencies = [
- "bstr",
+ "bstr 0.2.17",
  "unicode-segmentation",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index c7408b3aba2..2e4eee05eed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,8 @@ members = [
 ]
 
 [workspace.dependencies]
+globset = "0.4.14"
+nextest-metadata = { path = "nextest-metadata" }
 nextest-workspace-hack = "0.1.0"
 
 # make backtrace + color-eyre faster on debug builds
diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml
index 6276a2c9689..3aacb0cdb4a 100644
--- a/workspace-hack/Cargo.toml
+++ b/workspace-hack/Cargo.toml
@@ -33,7 +33,6 @@ rand = { version = "0.8.5" }
 serde = { version = "1.0.193", features = ["alloc", "derive"] }
 serde_json = { version = "1.0.108", features = ["preserve_order", "unbounded_depth"] }
 similar = { version = "2.3.0", features = ["inline", "unicode"] }
-target-spec = { version = "3.0.1", default-features = false, features = ["custom", "summaries"] }
 tokio = { version = "1.34.0", features = ["fs", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time", "tracing"] }
 twox-hash = { version = "1.6.3" }
 uuid = { version = "1.6.1", features = ["v4"] }


Test patch:
diff --git a/integration-tests/tests/integration/fixtures.rs b/integration-tests/tests/integration/fixtures.rs
index 053eb73994a..6002397be60 100644
--- a/integration-tests/tests/integration/fixtures.rs
+++ b/integration-tests/tests/integration/fixtures.rs
@@ -69,7 +69,7 @@ pub static EXPECTED_LIST: Lazy<Vec<TestInfo>> = Lazy::new(|| {
             ],
         ),
         TestInfo::new(
-            "nextest-derive::proc-macro/nextest-derive",
+            "nextest-derive",
             BuildPlatform::Host,
             vec![("it_works", false)],
         ),
@@ -419,7 +419,7 @@ pub fn check_run_output(stderr: &[u8], relocated: bool) {
         (true, "nextest-tests::other other_test_success"),
         (true, "nextest-tests::basic test_success"),
         (false, "nextest-tests::segfault test_segfault"),
-        (true, "nextest-derive::proc-macro/nextest-derive it_works"),
+        (true, "nextest-derive it_works"),
         (
             true,
             "nextest-tests::example/other tests::other_example_success",
diff --git a/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-2.snap b/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-2.snap
index 9d75bbd1459..90ffbe6614f 100644
--- a/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-2.snap
+++ b/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-2.snap
@@ -12,7 +12,7 @@ group: @global
           tests::test_multiply_two_cdylib
       cdylib-link:
           test_multiply_two
-      nextest-derive::proc-macro/nextest-derive:
+      nextest-derive:
           it_works
       nextest-tests:
           tests::call_dylib_add_two
@@ -46,4 +46,3 @@ group: @global
           tests::example_success
       nextest-tests::example/other:
           tests::other_example_success
-
diff --git a/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-4.snap b/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-4.snap
index 73138c9e72f..12dd0d855bf 100644
--- a/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-4.snap
+++ b/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-4.snap
@@ -15,7 +15,7 @@ group: @global
           tests::test_multiply_two_cdylib
       cdylib-link:
           test_multiply_two
-      nextest-derive::proc-macro/nextest-derive:
+      nextest-derive:
           it_works
       nextest-tests:
           tests::call_dylib_add_two
@@ -47,4 +47,3 @@ group: @global
           tests::example_success
       nextest-tests::example/other:
           tests::other_example_success
-
diff --git a/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-6.snap b/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-6.snap
index 7fbed015da2..eeba532bf59 100644
--- a/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-6.snap
+++ b/integration-tests/tests/integration/snapshots/integration__show_config_test_groups-6.snap
@@ -14,7 +14,7 @@ group: @global
           tests::test_multiply_two_cdylib
       cdylib-link:
           test_multiply_two
-      nextest-derive::proc-macro/nextest-derive:
+      nextest-derive:
           it_works
       nextest-tests:
           tests::call_dylib_add_two
@@ -48,4 +48,3 @@ group: @global
           tests::example_success
       nextest-tests::example/other:
           tests::other_example_success
-
diff --git a/nextest-filtering/Cargo.toml b/nextest-filtering/Cargo.toml
index 95feece61e6..1552c692895 100644
--- a/nextest-filtering/Cargo.toml
+++ b/nextest-filtering/Cargo.toml
@@ -26,6 +26,7 @@ internal-testing = ["dep:proptest", "dep:test-strategy", "dep:twox-hash"]
 # trace = ["nom-tracable/trace"]
 
 [dependencies]
+globset.workspace = true
 guppy = "0.17.3"
 miette = "5.10.0"
 nom = "7.1.3"
@@ -35,6 +36,7 @@ recursion = "0.5.1"
 regex = "1.10.2"
 regex-syntax = "0.8.2"
 thiserror = "1.0.50"
+nextest-metadata.workspace = true
 proptest = { version = "1.4.0", optional = true }
 test-strategy = { version = "0.3.1", optional = true }
 twox-hash = { version = "1.6.3", optional = true }
diff --git a/nextest-filtering/src/compile.rs b/nextest-filtering/src/compile.rs
index 53d95889c85..62a6b8555a7 100644
--- a/nextest-filtering/src/compile.rs
+++ b/nextest-filtering/src/compile.rs
@@ -109,6 +109,7 @@ fn compile_set_def(
         )),
         SetDef::Kind(matcher, span) => FilteringSet::Kind(matcher.clone(), *span),
         SetDef::Binary(matcher, span) => FilteringSet::Binary(matcher.clone(), *span),
+        SetDef::BinaryId(matcher, span) => FilteringSet::BinaryId(matcher.clone(), *span),
         SetDef::Platform(platform, span) => FilteringSet::Platform(*platform, *span),
         SetDef::Test(matcher, span) => FilteringSet::Test(matcher.clone(), *span),
         SetDef::All => FilteringSet::All,
diff --git a/nextest-filtering/src/errors.rs b/nextest-filtering/src/errors.rs
index 32b273994f9..e92cc507f11 100644
--- a/nextest-filtering/src/errors.rs
+++ b/nextest-filtering/src/errors.rs
@@ -41,6 +41,17 @@ pub enum ParseSingleError {
         message: String,
     },
 
+    /// An invalid glob pattern was encountered.
+    #[error("invalid glob")]
+    InvalidGlob {
+        /// The part of the input that failed.
+        #[label("{}", error)]
+        span: SourceSpan,
+
+        /// The underlying error.
+        error: GlobConstructError,
+    },
+
     /// An invalid regex was encountered but we couldn't determine a better error message.
     #[error("invalid regex")]
     InvalidRegexWithoutMessage(#[label("invalid regex")] SourceSpan),
@@ -123,6 +134,15 @@ impl ParseSingleError {
     }
 }
 
+#[derive(Clone, Debug, Error, PartialEq, Eq)]
+pub enum GlobConstructError {
+    #[error("{}", .0.kind())]
+    InvalidGlob(globset::Error),
+
+    #[error("{}", .0)]
+    RegexError(String),
+}
+
 #[derive(Debug, Clone)]
 pub(crate) struct State<'a> {
     // A `RefCell` is required here because the state must implement `Clone` to work with nom.
diff --git a/nextest-filtering/src/expression.rs b/nextest-filtering/src/expression.rs
index acb19462558..b70959c29b7 100644
--- a/nextest-filtering/src/expression.rs
+++ b/nextest-filtering/src/expression.rs
@@ -4,7 +4,8 @@
 use crate::{
     errors::{FilterExpressionParseErrors, ParseSingleError, State},
     parsing::{
-        parse, DisplayParsedRegex, DisplayParsedString, ExprResult, ParsedExpr, SetDef, Span,
+        parse, DisplayParsedRegex, DisplayParsedString, ExprResult, GenericGlob, ParsedExpr,
+        SetDef, Span,
     },
 };
 use guppy::{
@@ -12,6 +13,7 @@ use guppy::{
     PackageId,
 };
 use miette::SourceSpan;
+use nextest_metadata::{RustBinaryId, RustTestBinaryKind};
 use recursion::{Collapsible, CollapsibleExt, MappableFrame, PartiallyApplied};
 use std::{cell::RefCell, collections::HashSet, fmt};
 
@@ -24,6 +26,8 @@ pub enum NameMatcher {
     Equal { value: String, implicit: bool },
     /// Simple contains test
     Contains { value: String, implicit: bool },
+    /// Test against a glob
+    Glob { glob: GenericGlob, implicit: bool },
     /// Test against a regex
     Regex(regex::Regex),
 }
@@ -68,6 +72,9 @@ impl PartialEq for NameMatcher {
                 },
             ) => s1 == s2 && default1 == default2,
             (Self::Regex(r1), Self::Regex(r2)) => r1.as_str() == r2.as_str(),
+            (Self::Glob { glob: g1, .. }, Self::Glob { glob: g2, .. }) => {
+                g1.regex().as_str() == g2.regex().as_str()
+            }
             _ => false,
         }
     }
@@ -90,6 +97,12 @@ impl fmt::Display for NameMatcher {
                 if *implicit { "" } else { "~" },
                 DisplayParsedString(value)
             ),
+            Self::Glob { glob, implicit } => write!(
+                f,
+                "{}{}",
+                if *implicit { "" } else { "#" },
+                DisplayParsedString(glob.as_str())
+            ),
             Self::Regex(r) => write!(f, "/{}/", DisplayParsedRegex(r)),
         }
     }
@@ -106,6 +119,8 @@ pub enum FilteringSet {
     Platform(BuildPlatform, SourceSpan),
     /// All binaries matching a name
     Binary(NameMatcher, SourceSpan),
+    /// All binary IDs matching a name
+    BinaryId(NameMatcher, SourceSpan),
     /// All tests matching a name
     Test(NameMatcher, SourceSpan),
     /// All tests
@@ -120,11 +135,14 @@ pub struct BinaryQuery<'a> {
     /// The package ID.
     pub package_id: &'a PackageId,
 
+    /// The binary ID.
+    pub binary_id: &'a RustBinaryId,
+
     /// The name of the binary.
     pub binary_name: &'a str,
 
     /// The kind of binary this test is (lib, test etc).
-    pub kind: &'a str,
+    pub kind: &'a RustTestBinaryKind,
 
     /// The platform this test is built for.
     pub platform: BuildPlatform,
@@ -172,6 +190,7 @@ impl NameMatcher {
         match self {
             Self::Equal { value, .. } => value == input,
             Self::Contains { value, .. } => input.contains(value),
+            Self::Glob { glob, .. } => glob.is_match(input),
             Self::Regex(reg) => reg.is_match(input),
         }
     }
@@ -184,8 +203,9 @@ impl FilteringSet {
             Self::None => false,
             Self::Test(matcher, _) => matcher.is_match(query.test_name),
             Self::Binary(matcher, _) => matcher.is_match(query.binary_query.binary_name),
+            Self::BinaryId(matcher, _) => matcher.is_match(query.binary_query.binary_id.as_str()),
             Self::Platform(platform, _) => query.binary_query.platform == *platform,
-            Self::Kind(matcher, _) => matcher.is_match(query.binary_query.kind),
+            Self::Kind(matcher, _) => matcher.is_match(query.binary_query.kind.as_str()),
             Self::Packages(packages) => packages.contains(query.binary_query.package_id),
         }
     }
@@ -196,8 +216,9 @@ impl FilteringSet {
             Self::None => Logic::bottom(),
             Self::Test(_, _) => None,
             Self::Binary(matcher, _) => Some(matcher.is_match(query.binary_name)),
+            Self::BinaryId(matcher, _) => Some(matcher.is_match(query.binary_id.as_str())),
             Self::Platform(platform, _) => Some(query.platform == *platform),
-            Self::Kind(matcher, _) => Some(matcher.is_match(query.kind)),
+            Self::Kind(matcher, _) => Some(matcher.is_match(query.kind.as_str())),
             Self::Packages(packages) => Some(packages.contains(query.package_id)),
         }
     }
diff --git a/nextest-filtering/src/parsing.rs b/nextest-filtering/src/parsing.rs
index 6e8c70e2e1a..f44663ea76a 100644
--- a/nextest-filtering/src/parsing.rs
+++ b/nextest-filtering/src/parsing.rs
@@ -27,8 +27,10 @@ use nom::{
 use nom_tracable::tracable_parser;
 use std::{cell::RefCell, fmt};
 
+mod glob;
 mod unicode_string;
 use crate::{errors::*, NameMatcher};
+pub(crate) use glob::GenericGlob;
 pub(crate) use unicode_string::DisplayParsedString;
 
 pub(crate) type Span<'a> = nom_locate::LocatedSpan<&'a str, State<'a>>;
@@ -51,6 +53,7 @@ pub enum SetDef<S = SourceSpan> {
     Rdeps(NameMatcher, S),
     Kind(NameMatcher, S),
     Binary(NameMatcher, S),
+    BinaryId(NameMatcher, S),
     Platform(BuildPlatform, S),
     Test(NameMatcher, S),
     All,
@@ -65,6 +68,7 @@ impl SetDef {
             Self::Rdeps(matcher, _) => SetDef::Rdeps(matcher, ()),
             Self::Kind(matcher, _) => SetDef::Kind(matcher, ()),
             Self::Binary(matcher, _) => SetDef::Binary(matcher, ()),
+            Self::BinaryId(matcher, _) => SetDef::BinaryId(matcher, ()),
             Self::Platform(platform, _) => SetDef::Platform(platform, ()),
             Self::Test(matcher, _) => SetDef::Test(matcher, ()),
             Self::All => SetDef::All,
@@ -81,6 +85,7 @@ impl<S> fmt::Display for SetDef<S> {
             Self::Rdeps(matcher, _) => write!(f, "rdeps({matcher})"),
             Self::Kind(matcher, _) => write!(f, "kind({matcher})"),
             Self::Binary(matcher, _) => write!(f, "binary({matcher})"),
+            Self::BinaryId(matcher, _) => write!(f, "binary_id({matcher})"),
             Self::Platform(platform, _) => write!(f, "platform({platform})"),
             Self::Test(matcher, _) => write!(f, "test({matcher})"),
             Self::All => write!(f, "all()"),
@@ -371,13 +376,6 @@ fn parse_equal_matcher(input: Span) -> IResult<Option<NameMatcher>> {
     ))(input)
 }
 
-// This parse will never fail
-fn default_matcher(
-    make: fn(String) -> NameMatcher,
-) -> impl FnMut(Span) -> IResult<Option<NameMatcher>> {
-    move |input: Span| map(parse_matcher_text, |res: Option<String>| res.map(make))(input)
-}
-
 #[tracable_parser]
 fn parse_regex_inner(input: Span) -> IResult<String> {
     enum Frag<'a> {
@@ -465,16 +463,22 @@ fn parse_regex_matcher(input: Span) -> IResult<Option<NameMatcher>> {
     ))(input)
 }
 
+#[tracable_parser]
+fn parse_glob_matcher(input: Span) -> IResult<Option<NameMatcher>> {
+    ws(preceded(char('#'), |input| glob::parse_glob(input, false)))(input)
+}
+
 // This parse will never fail (because default_matcher won't)
 fn set_matcher(
-    make: fn(String) -> NameMatcher,
+    default_matcher: DefaultMatcher,
 ) -> impl FnMut(Span) -> IResult<Option<NameMatcher>> {
     move |input: Span| {
         ws(alt((
             parse_regex_matcher,
+            parse_glob_matcher,
             parse_equal_matcher,
             parse_contains_matcher,
-            default_matcher(make),
+            default_matcher.into_parser(),
         )))(input)
     }
 }
@@ -517,16 +521,38 @@ fn nullary_set_def(
     }
 }
 
+#[derive(Copy, Clone, Debug)]
+enum DefaultMatcher {
+    // Equal is no longer used and glob is always favored.
+    Equal,
+    Contains,
+    Glob,
+}
+
+impl DefaultMatcher {
+    fn into_parser(self) -> impl FnMut(Span) -> IResult<Option<NameMatcher>> {
+        move |input| match self {
+            Self::Equal => map(parse_matcher_text, |res: Option<String>| {
+                res.map(NameMatcher::implicit_equal)
+            })(input),
+            Self::Contains => map(parse_matcher_text, |res: Option<String>| {
+                res.map(NameMatcher::implicit_contains)
+            })(input),
+            Self::Glob => glob::parse_glob(input, true),
+        }
+    }
+}
+
 fn unary_set_def(
     name: &'static str,
-    make_default_matcher: fn(String) -> NameMatcher,
+    default_matcher: DefaultMatcher,
     make_set: fn(NameMatcher, SourceSpan) -> SetDef,
 ) -> impl FnMut(Span) -> IResult<Option<SetDef>> {
     move |i| {
         let (i, _) = tag(name)(i)?;
         let (i, _) = expect_char('(', ParseSingleError::ExpectedOpenParenthesis)(i)?;
         let start = i.location_offset();
-        let (i, res) = set_matcher(make_default_matcher)(i)?;
+        let (i, res) = set_matcher(default_matcher)(i)?;
         let end = i.location_offset();
         let (i, _) = recover_unexpected_comma(i)?;
         let (i, _) = expect_char(')', ParseSingleError::ExpectedCloseParenthesis)(i)?;
@@ -572,12 +598,14 @@ fn platform_def(i: Span) -> IResult<Option<SetDef>> {
 #[tracable_parser]
 fn parse_set_def(input: Span) -> IResult<Option<SetDef>> {
     ws(alt((
-        unary_set_def("package", NameMatcher::implicit_equal, SetDef::Package),
-        unary_set_def("deps", NameMatcher::implicit_equal, SetDef::Deps),
-        unary_set_def("rdeps", NameMatcher::implicit_equal, SetDef::Rdeps),
-        unary_set_def("kind", NameMatcher::implicit_equal, SetDef::Kind),
-        unary_set_def("binary", NameMatcher::implicit_equal, SetDef::Binary),
-        unary_set_def("test", NameMatcher::implicit_contains, SetDef::Test),
+        unary_set_def("package", DefaultMatcher::Glob, SetDef::Package),
+        unary_set_def("deps", DefaultMatcher::Glob, SetDef::Deps),
+        unary_set_def("rdeps", DefaultMatcher::Glob, SetDef::Rdeps),
+        unary_set_def("kind", DefaultMatcher::Equal, SetDef::Kind),
+        // binary_id must go above binary, otherwise we'll parse the opening predicate wrong.
+        unary_set_def("binary_id", DefaultMatcher::Glob, SetDef::BinaryId),
+        unary_set_def("binary", DefaultMatcher::Glob, SetDef::Binary),
+        unary_set_def("test", DefaultMatcher::Contains, SetDef::Test),
         platform_def,
         nullary_set_def("all", || SetDef::All),
         nullary_set_def("none", || SetDef::None),
@@ -849,6 +877,57 @@ mod tests {
         );
     }
 
+    #[track_caller]
+    fn parse_glob(input: &str) -> NameMatcher {
+        let errors = RefCell::new(Vec::new());
+        let matcher = parse_glob_matcher(Span::new_extra(input, State::new(&errors)))
+            .unwrap_or_else(|error| {
+                panic!("for input {input}, parse_glob_matcher returned an error: {error}")
+            })
+            .1
+            .unwrap_or_else(|| {
+                panic!(
+                    "for input {input}, parse_glob_matcher returned None \
+                     (reported errors: {errors:?})"
+                )
+            });
+        if errors.borrow().len() > 0 {
+            panic!("for input {input}, parse_glob_matcher reported errors: {errors:?}");
+        }
+
+        matcher
+    }
+
+    fn make_glob_matcher(glob: &str, implicit: bool) -> NameMatcher {
+        NameMatcher::Glob {
+            glob: GenericGlob::new(glob.to_owned()).unwrap(),
+            implicit,
+        }
+    }
+
+    #[test]
+    fn test_parse_glob_matcher() {
+        #[track_caller]
+        fn assert_glob(input: &str, expected: &str) {
+            assert_eq!(
+                make_glob_matcher(expected, false),
+                parse_glob(input),
+                "expected matches actual for input {input:?}",
+            );
+        }
+
+        // Need the closing ) since that's used as the delimiter.
+        assert_glob(r"#something)", "something");
+        assert_glob(r"#something*)", "something*");
+        assert_glob(r"#something?)", "something?");
+        assert_glob(r"#something[abc])", "something[abc]");
+        assert_glob(r"#something[!abc])", "something[!abc]");
+        assert_glob(r"#something[a-c])", "something[a-c]");
+        assert_glob(r"#foobar\b)", "foobar\u{08}");
+        assert_glob(r"#foobar\\b)", "foobar\\b");
+        assert_glob(r"#foobar\))", "foobar)");
+    }
+
     #[track_caller]
     fn parse_set(input: &str) -> SetDef {
         let errors = RefCell::new(Vec::new());
@@ -889,6 +968,21 @@ mod tests {
             Test,
             NameMatcher::Regex(regex::Regex::new("some.*").unwrap())
         );
+        assert_set_def!(
+            parse_set("test(#something)"),
+            Test,
+            make_glob_matcher("something", false)
+        );
+        assert_set_def!(
+            parse_set("test(#something*)"),
+            Test,
+            make_glob_matcher("something*", false)
+        );
+        assert_set_def!(
+            parse_set(r"test(#something/[?])"),
+            Test,
+            make_glob_matcher("something/[?]", false)
+        );
 
         // Default matchers
         assert_set_def!(
@@ -902,10 +996,7 @@ mod tests {
         assert_set_def!(
             parse_set("package(something)"),
             Package,
-            NameMatcher::Equal {
-                value: "something".to_string(),
-                implicit: true,
-            }
+            make_glob_matcher("something", true)
         );
 
         // Explicit contains matching
@@ -941,6 +1032,14 @@ mod tests {
                 implicit: false,
             }
         );
+        assert_set_def!(
+            parse_set("test(~#something)"),
+            Test,
+            NameMatcher::Contains {
+                value: "#something".to_string(),
+                implicit: false,
+            }
+        );
 
         // Explicit equals matching.
         assert_set_def!(
@@ -975,6 +1074,36 @@ mod tests {
                 implicit: false,
             }
         );
+        assert_set_def!(
+            parse_set("test(=#something)"),
+            Test,
+            NameMatcher::Equal {
+                value: "#something".to_string(),
+                implicit: false,
+            }
+        );
+
+        // Explicit glob matching.
+        assert_set_def!(
+            parse_set("test(#~something)"),
+            Test,
+            make_glob_matcher("~something", false)
+        );
+        assert_set_def!(
+            parse_set("test(#=something)"),
+            Test,
+            make_glob_matcher("=something", false)
+        );
+        assert_set_def!(
+            parse_set("test(#/something/)"),
+            Test,
+            make_glob_matcher("/something/", false)
+        );
+        assert_set_def!(
+            parse_set("test(##something)"),
+            Test,
+            make_glob_matcher("#something", false)
+        );
     }
 
     #[test]
@@ -1013,28 +1142,22 @@ mod tests {
         assert_eq!(SetDef::None, parse_set("none()"));
 
         assert_set_def!(
-            parse_set("package(something)"),
+            parse_set("package(=something)"),
             Package,
             NameMatcher::Equal {
                 value: "something".to_string(),
-                implicit: true,
+                implicit: false,
             }
         );
         assert_set_def!(
             parse_set("deps(something)"),
             Deps,
-            NameMatcher::Equal {
-                value: "something".to_string(),
-                implicit: true,
-            }
+            make_glob_matcher("something", true)
         );
         assert_set_def!(
             parse_set("rdeps(something)"),
             Rdeps,
-            NameMatcher::Equal {
-                value: "something".to_string(),
-                implicit: true,
-            }
+            make_glob_matcher("something", true)
         );
         assert_set_def!(
             parse_set("test(something)"),
@@ -1229,9 +1352,9 @@ mod tests {
         fn parse_future_syntax(input: Span) -> IResult<(Option<NameMatcher>, Option<NameMatcher>)> {
             let (i, _) = tag("something")(input)?;
             let (i, _) = char('(')(i)?;
-            let (i, n1) = set_matcher(NameMatcher::implicit_contains)(i)?;
+            let (i, n1) = set_matcher(DefaultMatcher::Contains)(i)?;
             let (i, _) = ws(char(','))(i)?;
-            let (i, n2) = set_matcher(NameMatcher::implicit_contains)(i)?;
+            let (i, n2) = set_matcher(DefaultMatcher::Contains)(i)?;
             let (i, _) = char(')')(i)?;
             Ok((i, (n1, n2)))
         }
@@ -1330,6 +1453,26 @@ mod tests {
         assert_eq!(message, "unclosed group");
     }
 
+    #[test]
+    fn test_invalid_glob() {
+        let src = "package(#)";
+        let mut errors = parse_err(src);
+        assert_eq!(1, errors.len());
+        let error = errors.remove(0);
+        assert_error!(error, InvalidString, 9, 0);
+
+        let src = "package(#foo[)";
+        let mut errors = parse_err(src);
+        assert_eq!(1, errors.len());
+        let error = errors.remove(0);
+        let (span, error) = match error {
+            ParseSingleError::InvalidGlob { span, error } => (span, error),
+            other => panic!("expected InvalidGlob with details, found {other}"),
+        };
+        assert_eq!(span, (9, 4).into(), "span matches");
+        assert_eq!(error.to_string(), "unclosed character class; missing ']'");
+    }
+
     #[test]
     fn test_invalid_platform() {
         let src = "platform(foo)";
diff --git a/nextest-filtering/src/parsing/glob.rs b/nextest-filtering/src/parsing/glob.rs
new file mode 100644
index 00000000000..515d059c7de
--- /dev/null
+++ b/nextest-filtering/src/parsing/glob.rs
@@ -0,0 +1,87 @@
+// Copyright (c) The nextest Contributors
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+//! Glob matching.
+
+use super::{parse_matcher_text, IResult, Span};
+use crate::{
+    errors::{GlobConstructError, ParseSingleError},
+    NameMatcher,
+};
+use nom_tracable::tracable_parser;
+
+/// A glob pattern.
+///
+/// We do not use `globset::GlobMatcher` directly because it has path-like semantics, so we use
+/// regexes directly.
+#[derive(Clone, Debug)]
+pub struct GenericGlob {
+    /// The glob string.
+    glob_str: String,
+
+    /// The regex to match against.
+    regex: regex::bytes::Regex,
+}
+
+impl GenericGlob {
+    /// Creates a new generic glob.
+    pub(crate) fn new(glob_str: String) -> Result<Self, GlobConstructError> {
+        let glob = globset::GlobBuilder::new(&glob_str)
+            // Only allow escapes via [].
+            .backslash_escape(false)
+            // Allow foo.{exe,} to match both foo.exe and foo.
+            .empty_alternates(true)
+            .build()
+            .map_err(GlobConstructError::InvalidGlob)?;
+
+        // Convert to a regex.
+        let regex = regex::bytes::Regex::new(glob.regex())
+            .map_err(|error| GlobConstructError::RegexError(error.to_string()))?;
+
+        Ok(Self { glob_str, regex })
+    }
+
+    /// Returns the glob string.
+    pub fn as_str(&self) -> &str {
+        &self.glob_str
+    }
+
+    /// Returns the regex that will be used for matches.
+    pub fn regex(&self) -> &regex::bytes::Regex {
+        &self.regex
+    }
+
+    /// Returns true if this glob matches the given string.
+    pub fn is_match(&self, s: &str) -> bool {
+        self.regex.is_match(s.as_bytes())
+    }
+}
+
+// This never returns Err(()) -- instead, it reports an error to the parsing state.
+#[tracable_parser]
+pub(super) fn parse_glob(input: Span, implicit: bool) -> IResult<Option<NameMatcher>> {
+    let (i, res) = match parse_matcher_text(input.clone()) {
+        Ok((i, res)) => (i, res),
+        Err(_) => {
+            unreachable!("parse_matcher_text should never fail")
+        }
+    };
+
+    let Some(parsed_value) = res else {
+        return Ok((i, None));
+    };
+
+    match GenericGlob::new(parsed_value) {
+        Ok(glob) => Ok((i, Some(NameMatcher::Glob { glob, implicit }))),
+        Err(error) => {
+            let start = input.location_offset();
+            let end = i.location_offset();
+            let err = ParseSingleError::InvalidGlob {
+                span: (start, end - start).into(),
+                error,
+            };
+            i.extra.report_error(err);
+            Ok((i, None))
+        }
+    }
+}
diff --git a/nextest-filtering/src/parsing/unicode_string.rs b/nextest-filtering/src/parsing/unicode_string.rs
index 2c9129a0c1c..617a11cd85d 100644
--- a/nextest-filtering/src/parsing/unicode_string.rs
+++ b/nextest-filtering/src/parsing/unicode_string.rs
@@ -103,7 +103,8 @@ impl fmt::Display for DisplayParsedString<'_> {
 #[tracable_parser]
 fn parse_literal(input: Span) -> IResult<Span> {
     let not_quote_slash = is_not(",)\\");
-    verify(not_quote_slash, |s: &Span| !s.fragment().is_empty())(input)
+    let res = verify(not_quote_slash, |s: &Span| !s.fragment().is_empty())(input.clone());
+    res
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
diff --git a/nextest-filtering/src/proptest_helpers.rs b/nextest-filtering/src/proptest_helpers.rs
index a49c164dbb2..ee2abf7ae66 100644
--- a/nextest-filtering/src/proptest_helpers.rs
+++ b/nextest-filtering/src/proptest_helpers.rs
@@ -6,7 +6,9 @@
 #![allow(clippy::arc_with_non_send_sync)]
 
 use crate::{
-    parsing::{AndOperator, DifferenceOperator, NotOperator, OrOperator, ParsedExpr, SetDef},
+    parsing::{
+        AndOperator, DifferenceOperator, GenericGlob, NotOperator, OrOperator, ParsedExpr, SetDef,
+    },
     NameMatcher,
 };
 use guppy::graph::cargo::BuildPlatform;
@@ -108,11 +110,12 @@ impl ParsedExpr<()> {
 impl SetDef<()> {
     pub(crate) fn strategy() -> impl Strategy<Value = Self> {
         prop_oneof![
-            1 => NameMatcher::default_equal_strategy().prop_map(|s| Self::Package(s, ())),
-            1 => NameMatcher::default_equal_strategy().prop_map(|s| Self::Deps(s, ())),
-            1 => NameMatcher::default_equal_strategy().prop_map(|s| Self::Rdeps(s, ())),
+            1 => NameMatcher::default_glob_strategy().prop_map(|s| Self::Package(s, ())),
+            1 => NameMatcher::default_glob_strategy().prop_map(|s| Self::Deps(s, ())),
+            1 => NameMatcher::default_glob_strategy().prop_map(|s| Self::Rdeps(s, ())),
             1 => NameMatcher::default_equal_strategy().prop_map(|s| Self::Kind(s, ())),
-            1 => NameMatcher::default_equal_strategy().prop_map(|s| Self::Binary(s, ())),
+            1 => NameMatcher::default_glob_strategy().prop_map(|s| Self::Binary(s, ())),
+            1 => NameMatcher::default_glob_strategy().prop_map(|s| Self::BinaryId(s, ())),
             1 => build_platform_strategy().prop_map(|p| Self::Platform(p, ())),
             1 => NameMatcher::default_contains_strategy().prop_map(|s| Self::Test(s, ())),
             1 => Just(Self::All),
@@ -139,6 +142,7 @@ impl NameMatcher {
                 Self::Contains { value, implicit: false }
             }),
             1 => regex_strategy().prop_map(Self::Regex),
+            1 => glob_strategy().prop_map(|glob| { Self::Glob { glob, implicit: false }}),
         ]
     }
 
@@ -158,12 +162,39 @@ impl NameMatcher {
                     accept.then_some(Self::Contains { value, implicit })
             }),
             1 => regex_strategy().prop_map(Self::Regex),
+            1 => glob_strategy().prop_map(|glob| { Self::Glob { glob, implicit: false }}),
+        ]
+    }
+
+    pub(crate) fn default_glob_strategy() -> impl Strategy<Value = Self> {
+        prop_oneof![
+            1 => name_strategy().prop_map(|value| {
+                Self::Equal { value, implicit: false }
+            }),
+            1 => name_strategy().prop_map(|value| {
+                Self::Contains { value, implicit: false }
+            }),
+            1 => regex_strategy().prop_map(Self::Regex),
+            1 => (glob_strategy(), any::<bool>()).prop_filter_map(
+                "implicit = true can't begin with operators",
+                |(glob, implicit)| {
+                    let accept = match (implicit, begins_with_operator(glob.as_str())) {
+                        (false, _) => true,
+                        (true, false) => true,
+                        (true, true) => false,
+                    };
+                    accept.then_some(Self::Glob { glob, implicit })
+                },
+            ),
         ]
     }
 }
 
 fn begins_with_operator(value: &str) -> bool {
-    value.starts_with('=') || value.starts_with('~') || value.starts_with('/')
+    value.starts_with('=')
+        || value.starts_with('~')
+        || value.starts_with('/')
+        || value.starts_with('#')
 }
 
 pub(crate) fn build_platform_strategy() -> impl Strategy<Value = BuildPlatform> {
@@ -173,10 +204,28 @@ pub(crate) fn build_platform_strategy() -> impl Strategy<Value = BuildPlatform>
 pub(crate) fn name_strategy() -> impl Strategy<Value = String> {
     prop_oneof![
         4 => "[abcde]{0,10}",
-        // Some escapes
-        1 => r"[abcde=/~]{0,10}",
+        // Some escapes and glob characters
+        1 => r"[abcde=/~#*?]{0,10}",
+        // More escapes
+        1 => r"[abcde=/~#*?\[\]\r\t\n\u{2055}\u{1fe4e}]{0,10}",
+    ]
+}
+
+pub(crate) fn glob_strategy() -> impl Strategy<Value = GenericGlob> {
+    glob_str_strategy().prop_filter_map(
+        "some strings generated by the strategy are invalid globs",
+        |s| GenericGlob::new(s).ok(),
+    )
+}
+
+fn glob_str_strategy() -> impl Strategy<Value = String> {
+    prop_oneof![
+        // No escapes or glob characters.
+        4 => "[abcde]{0,10}",
+        // Some escapes and glob characters
+        1 => r"[abcde*?\[\]]{0,10}",
         // More escapes
-        1 => r"[abcde=/~\r\t\n\u{2055}\u{1fe4e}]{0,10}",
+        1 => r"[abcde=/~#*?\[\]\r\t\n\u{2055}\u{1fe4e}]{0,10}",
     ]
 }
 
diff --git a/nextest-filtering/tests/match.rs b/nextest-filtering/tests/match.rs
index 79b65a92133..dafabd38f0f 100644
--- a/nextest-filtering/tests/match.rs
+++ b/nextest-filtering/tests/match.rs
@@ -12,6 +12,7 @@ use nextest_filtering::{
     errors::{FilterExpressionParseErrors, ParseSingleError},
     BinaryQuery, FilteringExpr, TestQuery,
 };
+use nextest_metadata::{RustBinaryId, RustTestBinaryKind};
 use test_case::test_case;
 
 #[track_caller]
@@ -35,6 +36,45 @@ fn parse(input: &str, graph: &PackageGraph) -> FilteringExpr {
     expr
 }
 
+struct BinaryQueryCreator<'a> {
+    package_id: &'a PackageId,
+    binary_id: RustBinaryId,
+    kind: RustTestBinaryKind,
+    binary_name: &'a str,
+    platform: BuildPlatform,
+}
+
+impl<'a> BinaryQueryCreator<'a> {
+    fn to_query(&self) -> BinaryQuery<'_> {
+        BinaryQuery {
+            package_id: self.package_id,
+            binary_id: &self.binary_id,
+            kind: &self.kind,
+            binary_name: self.binary_name,
+            platform: self.platform,
+        }
+    }
+}
+
+fn binary_query<'a>(
+    graph: &'a PackageGraph,
+    package_id: &'a PackageId,
+    kind: &str,
+    binary_name: &'a str,
+    platform: BuildPlatform,
+) -> BinaryQueryCreator<'a> {
+    let package_name = graph.metadata(package_id).unwrap().name();
+    let kind = RustTestBinaryKind::new(kind.to_owned());
+    let binary_id = RustBinaryId::from_parts(package_name, &kind, binary_name);
+    BinaryQueryCreator {
+        package_id,
+        binary_id,
+        kind,
+        binary_name,
+        platform,
+    }
+}
+
 #[test]
 fn test_expr_package_contains() {
     let graph = load_graph();
@@ -44,30 +84,18 @@ fn test_expr_package_contains() {
     let pid_b = mk_pid('b');
     let pid_c = mk_pid('c');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_b, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_c,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_c, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -81,30 +109,18 @@ fn test_expr_package_equal() {
     let pid_b = mk_pid('b');
     let pid_c = mk_pid('c');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_b, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_c,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_c, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -118,30 +134,44 @@ fn test_expr_package_regex() {
     let pid_b = mk_pid('b');
     let pid_c = mk_pid('c');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_b, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_c,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
+        binary_query: binary_query(&graph, &pid_c, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
+        test_name: "test_something"
+    }));
+}
+
+#[test]
+fn test_expr_binary_id_glob() {
+    let graph = load_graph();
+    let expr = parse("binary_id(crate_[ab])", &graph);
+    println!("{:?}", expr);
+
+    let pid_a = mk_pid('a');
+    let pid_b = mk_pid('b');
+    let pid_c = mk_pid('c');
+    assert!(expr.matches_test(&TestQuery {
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
+        test_name: "test_something"
+    }));
+    assert!(expr.matches_test(&TestQuery {
+        binary_query: binary_query(&graph, &pid_b, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
+        test_name: "test_something"
+    }));
+    assert!(!expr.matches_test(&TestQuery {
+        binary_query: binary_query(&graph, &pid_c, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -160,75 +190,40 @@ fn test_expr_deps() {
     let pid_g = mk_pid('g');
     // a-d are deps of d
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_b, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_c,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_c, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_d,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_d, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 
     // e-g are not deps of d
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_e,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_e, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_f,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_f, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_g,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_g, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -247,75 +242,40 @@ fn test_expr_rdeps() {
     let pid_g = mk_pid('g');
     // a-c are not rdeps of d
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_b, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_c,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_c, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 
     // d-g are rdeps of d
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_d,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_d, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_e,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_e, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_f,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_f, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_g,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_g, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -352,33 +312,18 @@ fn test_expr_kind() {
 
     let pid_a = mk_pid('a');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "test",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "test", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib2",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib2", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -390,33 +335,18 @@ fn test_expr_binary() {
 
     let pid_a = mk_pid('a');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "test",
-            binary_name: "my-binary2",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "test", "my-binary2", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib2",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib2", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -428,23 +358,13 @@ fn test_expr_platform() {
 
     let pid_a = mk_pid('a');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Host,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Host)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
 
@@ -452,23 +372,13 @@ fn test_expr_platform() {
 
     let pid_a = mk_pid('a');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Host,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Host)
+            .to_query(),
         test_name: "test_something"
     }));
 }
@@ -480,23 +390,13 @@ fn test_expr_kind_partial() {
 
     let pid_a = mk_pid('a');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "test",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "test", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_something"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
 }
@@ -510,33 +410,18 @@ fn test_expr_test() {
     let pid_b = mk_pid('b');
 
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_b, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_run"
     }));
 }
@@ -548,23 +433,13 @@ fn test_expr_test_not() {
 
     let pid_a = mk_pid('a');
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_run"
     }));
 }
@@ -578,33 +453,18 @@ fn test_expr_test_union(input: &str) {
 
     let pid_a = mk_pid('a');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_run"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_build"
     }));
 }
@@ -617,33 +477,18 @@ fn test_expr_test_difference(input: &str) {
 
     let pid_a = mk_pid('a');
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse_set"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse_expr"
     }));
 }
@@ -656,33 +501,18 @@ fn test_expr_test_intersect(input: &str) {
 
     let pid_a = mk_pid('a');
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse"
     }));
     assert!(!expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_expr"
     }));
     assert!(expr.matches_test(&TestQuery {
-        binary_query: BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "my-binary",
-            platform: BuildPlatform::Target,
-        },
-
+        binary_query: binary_query(&graph, &pid_a, "lib", "my-binary", BuildPlatform::Target)
+            .to_query(),
         test_name: "test_parse_expr"
     }));
 }
@@ -699,63 +529,45 @@ fn test_binary_query() {
     let pid_b = mk_pid('b');
     // binary = foo should match the first predicate (pid_a should not be relevant).
     assert_eq!(
-        expr.matches_binary(&BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "foo",
-            platform: BuildPlatform::Target,
-        }),
+        expr.matches_binary(
+            &binary_query(&graph, &pid_a, "lib", "foo", BuildPlatform::Target).to_query()
+        ),
         Some(true)
     );
     // platform = host should match the second predicate.
     assert_eq!(
-        expr.matches_binary(&BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "bar",
-            platform: BuildPlatform::Host,
-        }),
+        expr.matches_binary(
+            &binary_query(&graph, &pid_b, "lib", "bar", BuildPlatform::Host).to_query()
+        ),
         Some(true)
     );
     // kind = bench should match the third predicate.
     assert_eq!(
-        expr.matches_binary(&BinaryQuery {
-            package_id: &pid_a,
-            kind: "bench",
-            binary_name: "baz",
-            platform: BuildPlatform::Target,
-        }),
+        expr.matches_binary(
+            &binary_query(&graph, &pid_b, "bench", "baz", BuildPlatform::Target).to_query()
+        ),
         Some(true)
     );
     // This should result in an unknown result since it involves a test predicate.
     assert_eq!(
-        expr.matches_binary(&BinaryQuery {
-            package_id: &pid_a,
-            kind: "lib",
-            binary_name: "baz",
-            platform: BuildPlatform::Target,
-        }),
+        expr.matches_binary(
+            &binary_query(&graph, &pid_a, "lib", "baz", BuildPlatform::Target,).to_query()
+        ),
         None
     );
     // This should not result in an unknown result because no matter what the test predicate is,
     // kind(bin) resolves to true.
     assert_eq!(
-        expr.matches_binary(&BinaryQuery {
-            package_id: &pid_a,
-            kind: "bin",
-            binary_name: "baz",
-            platform: BuildPlatform::Target,
-        }),
+        expr.matches_binary(
+            &binary_query(&graph, &pid_a, "bin", "baz", BuildPlatform::Target,).to_query()
+        ),
         Some(true)
     );
     // This should result in Some(false) since it doesn't match anything.
     assert_eq!(
-        expr.matches_binary(&BinaryQuery {
-            package_id: &pid_b,
-            kind: "lib",
-            binary_name: "baz",
-            platform: BuildPlatform::Target,
-        }),
+        expr.matches_binary(
+            &binary_query(&graph, &pid_b, "lib", "baz", BuildPlatform::Target,).to_query()
+        ),
         Some(false)
     );
 }
diff --git a/nextest-metadata/src/test_list.rs b/nextest-metadata/src/test_list.rs
index d379b446a61..24b3cd3cac4 100644
--- a/nextest-metadata/src/test_list.rs
+++ b/nextest-metadata/src/test_list.rs
@@ -9,7 +9,7 @@ use std::{
     borrow::Cow,
     cmp::Ordering,
     collections::{BTreeMap, BTreeSet},
-    fmt,
+    fmt::{self, Write as _},
     path::PathBuf,
     process::Command,
 };
@@ -202,8 +202,8 @@ pub struct RustTestBinarySummary {
 
 /// Information about the kind of a Rust test binary.
 ///
-/// Kinds are used to generate binary IDs and to figure out whether some environment variables
-/// should be set.
+/// Kinds are used to generate [`RustBinaryId`] instances, and to figure out whether some
+/// environment variables should be set.
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize)]
 #[serde(transparent)]
 pub struct RustTestBinaryKind(pub Cow<'static, str>);
@@ -279,6 +279,69 @@ impl RustBinaryId {
         Self(id.into())
     }
 
+    /// Creates a new `RustBinaryId` from its constituent parts:
+    ///
+    /// * `package_name`: The name of the package as defined in `Cargo.toml`.
+    /// * `kind`: The kind of the target (see [`RustTestBinaryKind`]).
+    /// * `target_name`: The name of the target.
+    ///
+    /// The algorithm is as follows:
+    ///
+    /// 1. If the kind is `lib` or `proc-macro` (i.e. for unit tests), the binary ID is the same as
+    ///    the package name. There can only be one library per package, so this will always be
+    ///    unique.
+    /// 2. If the target is an integration test, the binary ID is `package_name::target_name`.
+    /// 3. Otherwise, the binary ID is `package_name::{kind}/{target_name}`.
+    ///
+    /// This format is part of nextest's stable API.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use nextest_metadata::{RustBinaryId, RustTestBinaryKind};
+    ///
+    /// // The lib and proc-macro kinds.
+    /// assert_eq!(
+    ///     RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::LIB, "foo_lib"),
+    ///     RustBinaryId::new("foo-lib"),
+    /// );
+    /// assert_eq!(
+    ///    RustBinaryId::from_parts("foo-derive", &RustTestBinaryKind::PROC_MACRO, "derive"),
+    ///    RustBinaryId::new("foo-derive"),
+    /// );
+    ///
+    /// // Integration tests.
+    /// assert_eq!(
+    ///    RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::TEST, "foo_test"),
+    ///    RustBinaryId::new("foo-lib::foo_test"),
+    /// );
+    ///
+    /// // Other kinds.
+    /// assert_eq!(
+    ///     RustBinaryId::from_parts("foo-lib", &RustTestBinaryKind::BIN, "foo_bin"),
+    ///     RustBinaryId::new("foo-lib::bin/foo_bin"),
+    /// );
+    /// ```
+    pub fn from_parts(package_name: &str, kind: &RustTestBinaryKind, target_name: &str) -> Self {
+        let mut id = package_name.to_owned();
+        // To ensure unique binary IDs, we use the following scheme:
+        if kind == &RustTestBinaryKind::LIB || kind == &RustTestBinaryKind::PROC_MACRO {
+            // 1. The binary ID is the same as the package name.
+        } else if kind == &RustTestBinaryKind::TEST {
+            // 2. For integration tests, use package_name::target_name. Cargo enforces unique names
+            //    for the same kind of targets in a package, so these will always be unique.
+            id.push_str("::");
+            id.push_str(target_name);
+        } else {
+            // 3. For all other target kinds, use a combination of the target kind and
+            //    the target name. For the same reason as above, these will always be
+            //    unique.
+            write!(id, "::{kind}/{target_name}").unwrap();
+        }
+
+        Self(id.into())
+    }
+
     /// Returns the identifier as a string.
     #[inline]
     pub fn as_str(&self) -> &str {
diff --git a/nextest-runner/src/config/overrides.rs b/nextest-runner/src/config/overrides.rs
index cd1d4a32137..bb24a6680c9 100644
--- a/nextest-runner/src/config/overrides.rs
+++ b/nextest-runner/src/config/overrides.rs
@@ -639,7 +639,6 @@ mod tests {
     use camino::Utf8Path;
     use camino_tempfile::tempdir;
     use indoc::indoc;
-    use nextest_filtering::BinaryQuery;
     use std::num::NonZeroUsize;
     use test_case::test_case;
 
@@ -710,13 +709,10 @@ mod tests {
             .apply_build_platforms(&build_platforms());
 
         // This query matches override 2.
+        let host_binary_query =
+            binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Host,
-            },
+            binary_query: host_binary_query.to_query(),
             test_name: "test",
         };
         let overrides = profile.settings_for(&query);
@@ -743,13 +739,15 @@ mod tests {
         }
 
         // This query matches override 1 and 2.
+        let target_binary_query = binary_query(
+            &graph,
+            package_id,
+            "lib",
+            "my-binary",
+            BuildPlatform::Target,
+        );
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: target_binary_query.to_query(),
             test_name: "test",
         };
         let overrides = profile.settings_for(&query);
@@ -788,12 +786,7 @@ mod tests {
 
         // This query matches override 3.
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: target_binary_query.to_query(),
             test_name: "override3",
         };
         let overrides = profile.settings_for(&query);
@@ -801,12 +794,7 @@ mod tests {
 
         // This query matches override 5.
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: target_binary_query.to_query(),
             test_name: "override5",
         };
         let overrides = profile.settings_for(&query);
@@ -814,12 +802,7 @@ mod tests {
 
         // This query does not match any overrides.
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: target_binary_query.to_query(),
             test_name: "no_match",
         };
         let overrides = profile.settings_for(&query);
diff --git a/nextest-runner/src/config/retry_policy.rs b/nextest-runner/src/config/retry_policy.rs
index de1d63a848c..37e324e0acd 100644
--- a/nextest-runner/src/config/retry_policy.rs
+++ b/nextest-runner/src/config/retry_policy.rs
@@ -167,7 +167,7 @@ mod tests {
     use super::*;
     use crate::{
         config::{
-            test_helpers::{build_platforms, temp_workspace},
+            test_helpers::{binary_query, build_platforms, temp_workspace},
             NextestConfig,
         },
         errors::ConfigParseErrorKind,
@@ -177,7 +177,7 @@ mod tests {
     use config::ConfigError;
     use guppy::graph::cargo::BuildPlatform;
     use indoc::indoc;
-    use nextest_filtering::{BinaryQuery, TestQuery};
+    use nextest_filtering::TestQuery;
     use test_case::test_case;
 
     #[test]
@@ -626,13 +626,9 @@ mod tests {
             &Default::default(),
         )
         .unwrap();
+        let binary_query = binary_query(&graph, package_id, "lib", "my-binary", build_platform);
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: build_platform,
-            },
+            binary_query: binary_query.to_query(),
             test_name: "my_test",
         };
         let settings_for = config
diff --git a/nextest-runner/src/config/scripts.rs b/nextest-runner/src/config/scripts.rs
index 7831722f9ae..9ac41e19a9f 100644
--- a/nextest-runner/src/config/scripts.rs
+++ b/nextest-runner/src/config/scripts.rs
@@ -623,7 +623,6 @@ mod tests {
     use display_error_chain::DisplayErrorChain;
     use indoc::indoc;
     use maplit::btreeset;
-    use nextest_filtering::BinaryQuery;
     use test_case::test_case;
 
     #[test]
@@ -710,13 +709,10 @@ mod tests {
             .apply_build_platforms(&build_platforms());
 
         // This query matches the foo and bar scripts.
+        let host_binary_query =
+            binary_query(&graph, package_id, "lib", "my-binary", BuildPlatform::Host);
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Host,
-            },
+            binary_query: host_binary_query.to_query(),
             test_name: "script1",
         };
         let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
@@ -732,14 +728,17 @@ mod tests {
             "second script should be bar"
         );
 
+        let target_binary_query = binary_query(
+            &graph,
+            package_id,
+            "lib",
+            "my-binary",
+            BuildPlatform::Target,
+        );
+
         // This query matches the baz script.
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: target_binary_query.to_query(),
             test_name: "script2",
         };
         let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
@@ -752,12 +751,7 @@ mod tests {
 
         // This query matches the baz, foo and tool scripts (but note the order).
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: target_binary_query.to_query(),
             test_name: "script3",
         };
         let scripts = SetupScripts::new_with_queries(&profile, std::iter::once(query));
diff --git a/nextest-runner/src/config/test_helpers.rs b/nextest-runner/src/config/test_helpers.rs
index 8b8dcdcd1ba..6c86ccd6e6f 100644
--- a/nextest-runner/src/config/test_helpers.rs
+++ b/nextest-runner/src/config/test_helpers.rs
@@ -7,7 +7,12 @@ use crate::{
     platform::BuildPlatforms,
 };
 use camino::{Utf8Path, Utf8PathBuf};
-use guppy::{graph::PackageGraph, MetadataCommand};
+use guppy::{
+    graph::{cargo::BuildPlatform, PackageGraph},
+    MetadataCommand, PackageId,
+};
+use nextest_filtering::BinaryQuery;
+use nextest_metadata::{RustBinaryId, RustTestBinaryKind};
 use serde::Deserialize;
 use std::{io::Write, path::PathBuf, process::Command};
 use target_spec::{Platform, TargetFeatures};
@@ -39,6 +44,45 @@ pub(super) fn cargo_path() -> Utf8PathBuf {
     }
 }
 
+pub(super) struct BinaryQueryCreator<'a> {
+    package_id: &'a PackageId,
+    binary_id: RustBinaryId,
+    kind: RustTestBinaryKind,
+    binary_name: &'a str,
+    platform: BuildPlatform,
+}
+
+impl<'a> BinaryQueryCreator<'a> {
+    pub(super) fn to_query(&self) -> BinaryQuery<'_> {
+        BinaryQuery {
+            package_id: self.package_id,
+            binary_id: &self.binary_id,
+            kind: &self.kind,
+            binary_name: self.binary_name,
+            platform: self.platform,
+        }
+    }
+}
+
+pub(super) fn binary_query<'a>(
+    graph: &'a PackageGraph,
+    package_id: &'a PackageId,
+    kind: &str,
+    binary_name: &'a str,
+    platform: BuildPlatform,
+) -> BinaryQueryCreator<'a> {
+    let package_name = graph.metadata(package_id).unwrap().name();
+    let kind = RustTestBinaryKind::new(kind.to_owned());
+    let binary_id = RustBinaryId::from_parts(package_name, &kind, binary_name);
+    BinaryQueryCreator {
+        package_id,
+        binary_id,
+        kind,
+        binary_name,
+        platform,
+    }
+}
+
 pub(super) fn build_platforms() -> BuildPlatforms {
     BuildPlatforms::new_with_host(
         Platform::new("x86_64-unknown-linux-gnu", TargetFeatures::Unknown).unwrap(),
diff --git a/nextest-runner/src/config/tool_config.rs b/nextest-runner/src/config/tool_config.rs
index 64c5903a9b4..afbac3c3216 100644
--- a/nextest-runner/src/config/tool_config.rs
+++ b/nextest-runner/src/config/tool_config.rs
@@ -62,7 +62,7 @@ mod tests {
     };
     use camino_tempfile::tempdir;
     use guppy::graph::cargo::BuildPlatform;
-    use nextest_filtering::{BinaryQuery, TestQuery};
+    use nextest_filtering::TestQuery;
 
     #[test]
     fn parse_tool_config_file() {
@@ -226,40 +226,27 @@ mod tests {
 
         let package_id = graph.workspace().iter().next().unwrap().id();
 
+        let binary_query = binary_query(
+            &graph,
+            package_id,
+            "lib",
+            "my-binary",
+            BuildPlatform::Target,
+        );
         let test_foo_query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: binary_query.to_query(),
             test_name: "test_foo",
         };
         let test_bar_query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: binary_query.to_query(),
             test_name: "test_bar",
         };
         let test_baz_query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: binary_query.to_query(),
             test_name: "test_baz",
         };
         let test_quux_query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id,
-                kind: "lib",
-                binary_name: "my-binary",
-                platform: BuildPlatform::Target,
-            },
+            binary_query: binary_query.to_query(),
             test_name: "test_quux",
         };
 
diff --git a/nextest-runner/src/list/binary_list.rs b/nextest-runner/src/list/binary_list.rs
index c9b2f161ba1..607b5e34cda 100644
--- a/nextest-runner/src/list/binary_list.rs
+++ b/nextest-runner/src/list/binary_list.rs
@@ -15,7 +15,7 @@ use nextest_metadata::{
     RustNonTestBinarySummary, RustTestBinaryKind, RustTestBinarySummary,
 };
 use owo_colors::OwoColorize;
-use std::{fmt::Write as _, io, io::Write};
+use std::{io, io::Write};
 
 /// A Rust test binary built by Cargo.
 #[derive(Clone, Debug)]
@@ -195,15 +195,14 @@ impl<'g> BinaryListBuildState<'g> {
                 let package_id = artifact.package_id.repr;
 
                 // Look up the executable by package ID.
+
+                let name = artifact.target.name;
+
                 let package = self
                     .graph
                     .metadata(&guppy::PackageId::new(package_id.clone()))
                     .map_err(FromMessagesError::PackageGraph)?;
 
-                // Construct the binary ID from the package and build target.
-                let mut id = package.name().to_owned();
-                let name = artifact.target.name;
-
                 let kind = artifact.target.kind;
                 if kind.is_empty() {
                     return Err(FromMessagesError::MissingTargetKind {
@@ -231,24 +230,8 @@ impl<'g> BinaryListBuildState<'g> {
                     )
                 };
 
-                // To ensure unique binary IDs, we use the following scheme:
-                if computed_kind == RustTestBinaryKind::LIB {
-                    // 1. If the target is a lib, use the package name. There can only be one
-                    //    lib per package, so this will always be unique.
-                } else if computed_kind == RustTestBinaryKind::TEST {
-                    // 2. For integration tests, use the target name. Cargo enforces unique
-                    //    names for the same kind of targets in a package, so these will always
-                    //    be unique.
-                    id.push_str("::");
-                    id.push_str(&name);
-                } else {
-                    // 3. For all other target kinds, use a combination of the target kind and
-                    //      the target name. For the same reason as above, these will always be
-                    //      unique.
-                    write!(id, "::{computed_kind}/{name}").unwrap();
-                }
-
-                let id = RustBinaryId::new(&id);
+                // Construct the binary ID from the package and build target.
+                let id = RustBinaryId::from_parts(package.name(), &computed_kind, &name);
 
                 self.rust_binaries.push(RustTestBinary {
                     path,
diff --git a/nextest-runner/src/list/test_list.rs b/nextest-runner/src/list/test_list.rs
index 6696d31dc4a..66af271a5b8 100644
--- a/nextest-runner/src/list/test_list.rs
+++ b/nextest-runner/src/list/test_list.rs
@@ -145,6 +145,17 @@ impl<'g> RustTestArtifact<'g> {
         Ok(binaries)
     }
 
+    /// Returns a [`BinaryQuery`] corresponding to this test artifact.
+    pub fn to_binary_query(&self) -> BinaryQuery<'_> {
+        BinaryQuery {
+            package_id: self.package.id(),
+            binary_id: &self.binary_id,
+            kind: &self.kind,
+            binary_name: &self.binary_name,
+            platform: convert_build_platform(self.build_platform),
+        }
+    }
+
     // ---
     // Helper methods
     // ---
@@ -886,7 +897,8 @@ impl<'a> TestInstance<'a> {
         TestQuery {
             binary_query: BinaryQuery {
                 package_id: self.suite_info.package.id(),
-                kind: self.suite_info.kind.as_str(),
+                binary_id: &self.suite_info.binary_id,
+                kind: &self.suite_info.kind,
                 binary_name: &self.suite_info.binary_name,
                 platform: convert_build_platform(self.suite_info.build_platform),
             },
diff --git a/nextest-runner/src/test_filter.rs b/nextest-runner/src/test_filter.rs
index 0ecc72ce860..5fcf623cef5 100644
--- a/nextest-runner/src/test_filter.rs
+++ b/nextest-runner/src/test_filter.rs
@@ -11,12 +11,11 @@
 
 use crate::{
     errors::TestFilterBuilderError,
-    helpers::convert_build_platform,
     list::RustTestArtifact,
     partition::{Partitioner, PartitionerBuilder},
 };
 use aho_corasick::AhoCorasick;
-use nextest_filtering::{BinaryQuery, FilteringExpr, TestQuery};
+use nextest_filtering::{FilteringExpr, TestQuery};
 use nextest_metadata::{FilterMatch, MismatchReason};
 
 /// Whether to run ignored tests.
@@ -114,19 +113,16 @@ impl TestFilterBuilder {
     /// This method is implemented directly on `TestFilterBuilder`. The statefulness of `TestFilter`
     /// is only used for counted test partitioning, and is not currently relevant for binaries.
     pub fn should_obtain_test_list_from_binary(&self, test_binary: &RustTestArtifact<'_>) -> bool {
-        let query = BinaryQuery {
-            package_id: test_binary.package.id(),
-            kind: test_binary.kind.as_str(),
-            binary_name: &test_binary.binary_name,
-            platform: convert_build_platform(test_binary.build_platform),
-        };
         if self.exprs.is_empty() {
             // No expressions means match all tests.
             return true;
         }
         for expr in &self.exprs {
             // If this is a definite or probable match, then we should run this binary
-            if expr.matches_binary(&query).unwrap_or(true) {
+            if expr
+                .matches_binary(&test_binary.to_binary_query())
+                .unwrap_or(true)
+            {
                 return true;
             }
         }
@@ -248,12 +244,7 @@ impl<'filter> TestFilter<'filter> {
         test_name: &str,
     ) -> FilterNameMatch {
         let query = TestQuery {
-            binary_query: BinaryQuery {
-                package_id: test_binary.package.id(),
-                kind: test_binary.kind.as_str(),
-                binary_name: &test_binary.binary_name,
-                platform: convert_build_platform(test_binary.build_platform),
-            },
+            binary_query: test_binary.to_binary_query(),
             test_name,
         };
         if self.builder.exprs.is_empty() {
diff --git a/nextest-runner/tests/integration/fixtures.rs b/nextest-runner/tests/integration/fixtures.rs
index 19b50fa4d57..8c2d0a90756 100644
--- a/nextest-runner/tests/integration/fixtures.rs
+++ b/nextest-runner/tests/integration/fixtures.rs
@@ -157,7 +157,7 @@ pub(crate) static EXPECTED_TESTS: Lazy<BTreeMap<RustBinaryId, Vec<TestFixture>>>
                 TestFixture { name: "tests::test_execute_bin", status: FixtureStatus::Pass },
             ],
             // Proc-macro tests
-            "nextest-derive::proc-macro/nextest-derive".into() => vec![
+            "nextest-derive".into() => vec![
                 TestFixture { name: "it_works", status: FixtureStatus::Pass },
             ],
             // Dynamic library tests
@@ -182,11 +182,7 @@ pub(crate) fn get_expected_test(binary_id: &RustBinaryId, test_name: &str) -> &'
 }
 
 pub(crate) static EXPECTED_BINARY_LIST: [(&str, &str, bool); 8] = [
-    (
-        "nextest-derive::proc-macro/nextest-derive",
-        "nextest-derive",
-        false,
-    ),
+    ("nextest-derive", "nextest-derive", false),
     ("nextest-tests", "nextest-tests", true),
     ("nextest-tests::basic", "basic", true),
     ("nextest-tests::bin/nextest-tests", "nextest-tests", true),