diff --git a/library/test/src/cli.rs b/library/test/src/cli.rs
index 02c529252e0..c0b5197e997 100644
--- a/library/test/src/cli.rs
+++ b/library/test/src/cli.rs
@@ -10,7 +10,7 @@ use super::time::TestTimeOptions;
 #[derive(Debug)]
 pub struct TestOpts {
     pub list: bool,
-    pub filter: Option<String>,
+    pub filters: Vec<String>,
     pub filter_exact: bool,
     pub force_run_in_process: bool,
     pub exclude_should_panic: bool,
@@ -148,12 +148,13 @@ fn optgroups() -> getopts::Options {
 }
 
 fn usage(binary: &str, options: &getopts::Options) {
-    let message = format!("Usage: {} [OPTIONS] [FILTER]", binary);
+    let message = format!("Usage: {} [OPTIONS] [FILTERS...]", binary);
     println!(
         r#"{usage}
 
 The FILTER string is tested against the name of all tests, and only those
-tests whose names contain the filter are run.
+tests whose names contain the filter are run. Multiple filter strings may
+be passed, which will run all tests matching any of the filters.
 
 By default, all tests are run in parallel. This can be altered with the
 --test-threads flag or the RUST_TEST_THREADS environment variable when running
@@ -243,7 +244,7 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
 
     let logfile = get_log_file(&matches)?;
     let run_ignored = get_run_ignored(&matches, include_ignored)?;
-    let filter = get_filter(&matches)?;
+    let filters = matches.free.clone();
     let nocapture = get_nocapture(&matches)?;
     let test_threads = get_test_threads(&matches)?;
     let color = get_color_config(&matches)?;
@@ -253,7 +254,7 @@ fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
 
     let test_opts = TestOpts {
         list,
-        filter,
+        filters,
         filter_exact: exact,
         force_run_in_process,
         exclude_should_panic,
@@ -397,12 +398,6 @@ fn get_run_ignored(matches: &getopts::Matches, include_ignored: bool) -> OptPart
     Ok(run_ignored)
 }
 
-fn get_filter(matches: &getopts::Matches) -> OptPartRes<Option<String>> {
-    let filter = if !matches.free.is_empty() { Some(matches.free[0].clone()) } else { None };
-
-    Ok(filter)
-}
-
 fn get_allow_unstable(matches: &getopts::Matches) -> OptPartRes<bool> {
     let mut allow_unstable = false;
 
diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs
index 3ff79eaea49..c38a8c965be 100644
--- a/library/test/src/lib.rs
+++ b/library/test/src/lib.rs
@@ -396,8 +396,8 @@ pub fn filter_tests(opts: &TestOpts, tests: Vec<TestDescAndFn>) -> Vec<TestDescA
     };
 
     // Remove tests that don't match the test filter
-    if let Some(ref filter) = opts.filter {
-        filtered.retain(|test| matches_filter(test, filter));
+    if !opts.filters.is_empty() {
+        filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter)));
     }
 
     // Skip tests that match any of the skip filters
diff --git a/library/test/src/tests.rs b/library/test/src/tests.rs
index 154876dde3b..e3c9b386915 100644
--- a/library/test/src/tests.rs
+++ b/library/test/src/tests.rs
@@ -34,7 +34,7 @@ impl TestOpts {
     fn new() -> TestOpts {
         TestOpts {
             list: false,
-            filter: None,
+            filters: vec![],
             filter_exact: false,
             force_run_in_process: false,
             exclude_should_panic: false,
@@ -473,43 +473,60 @@ pub fn exact_filter_match() {
     }
 
     let substr =
-        filter_tests(&TestOpts { filter: Some("base".into()), ..TestOpts::new() }, tests());
-    assert_eq!(substr.len(), 4);
-
-    let substr = filter_tests(&TestOpts { filter: Some("bas".into()), ..TestOpts::new() }, tests());
+        filter_tests(&TestOpts { filters: vec!["base".into()], ..TestOpts::new() }, tests());
     assert_eq!(substr.len(), 4);
 
     let substr =
-        filter_tests(&TestOpts { filter: Some("::test".into()), ..TestOpts::new() }, tests());
+        filter_tests(&TestOpts { filters: vec!["bas".into()], ..TestOpts::new() }, tests());
+    assert_eq!(substr.len(), 4);
+
+    let substr =
+        filter_tests(&TestOpts { filters: vec!["::test".into()], ..TestOpts::new() }, tests());
     assert_eq!(substr.len(), 3);
 
     let substr =
-        filter_tests(&TestOpts { filter: Some("base::test".into()), ..TestOpts::new() }, tests());
+        filter_tests(&TestOpts { filters: vec!["base::test".into()], ..TestOpts::new() }, tests());
     assert_eq!(substr.len(), 3);
 
+    let substr = filter_tests(
+        &TestOpts { filters: vec!["test1".into(), "test2".into()], ..TestOpts::new() },
+        tests(),
+    );
+    assert_eq!(substr.len(), 2);
+
     let exact = filter_tests(
-        &TestOpts { filter: Some("base".into()), filter_exact: true, ..TestOpts::new() },
+        &TestOpts { filters: vec!["base".into()], filter_exact: true, ..TestOpts::new() },
         tests(),
     );
     assert_eq!(exact.len(), 1);
 
     let exact = filter_tests(
-        &TestOpts { filter: Some("bas".into()), filter_exact: true, ..TestOpts::new() },
+        &TestOpts { filters: vec!["bas".into()], filter_exact: true, ..TestOpts::new() },
         tests(),
     );
     assert_eq!(exact.len(), 0);
 
     let exact = filter_tests(
-        &TestOpts { filter: Some("::test".into()), filter_exact: true, ..TestOpts::new() },
+        &TestOpts { filters: vec!["::test".into()], filter_exact: true, ..TestOpts::new() },
         tests(),
     );
     assert_eq!(exact.len(), 0);
 
     let exact = filter_tests(
-        &TestOpts { filter: Some("base::test".into()), filter_exact: true, ..TestOpts::new() },
+        &TestOpts { filters: vec!["base::test".into()], filter_exact: true, ..TestOpts::new() },
         tests(),
     );
     assert_eq!(exact.len(), 1);
+
+    let exact = filter_tests(
+        &TestOpts {
+            filters: vec!["base".into(), "base::test".into()],
+            filter_exact: true,
+            ..TestOpts::new()
+        },
+        tests(),
+    );
+    assert_eq!(exact.len(), 2);
 }
 
 #[test]
diff --git a/src/test/ui/test-attrs/test-filter-multiple.rs b/src/test/ui/test-attrs/test-filter-multiple.rs
new file mode 100644
index 00000000000..04dd83b7fd0
--- /dev/null
+++ b/src/test/ui/test-attrs/test-filter-multiple.rs
@@ -0,0 +1,17 @@
+// run-pass
+// compile-flags: --test
+// run-flags: --test-threads=1 test1 test2
+// check-run-results
+// normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME"
+// ignore-emscripten no threads support
+
+#[test]
+fn test1() {}
+
+#[test]
+fn test2() {}
+
+#[test]
+fn test3() {
+    panic!("this should not run");
+}
diff --git a/src/test/ui/test-attrs/test-filter-multiple.run.stdout b/src/test/ui/test-attrs/test-filter-multiple.run.stdout
new file mode 100644
index 00000000000..1aa684ed507
--- /dev/null
+++ b/src/test/ui/test-attrs/test-filter-multiple.run.stdout
@@ -0,0 +1,7 @@
+
+running 2 tests
+test test1 ... ok
+test test2 ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in $TIME
+
diff --git a/src/tools/compiletest/src/common.rs b/src/tools/compiletest/src/common.rs
index c8e76743231..cde4bfe288d 100644
--- a/src/tools/compiletest/src/common.rs
+++ b/src/tools/compiletest/src/common.rs
@@ -240,8 +240,8 @@ pub struct Config {
     /// Run ignored tests
     pub run_ignored: bool,
 
-    /// Only run tests that match this filter
-    pub filter: Option<String>,
+    /// Only run tests that match these filters
+    pub filters: Vec<String>,
 
     /// Exactly match the filter, rather than a substring
     pub filter_exact: bool,
diff --git a/src/tools/compiletest/src/main.rs b/src/tools/compiletest/src/main.rs
index 688cf930033..3fde24e8a7f 100644
--- a/src/tools/compiletest/src/main.rs
+++ b/src/tools/compiletest/src/main.rs
@@ -221,7 +221,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
         suite: matches.opt_str("suite").unwrap(),
         debugger: None,
         run_ignored,
-        filter: matches.free.first().cloned(),
+        filters: matches.free.clone(),
         filter_exact: matches.opt_present("exact"),
         force_pass_mode: matches.opt_str("pass").map(|mode| {
             mode.parse::<PassMode>()
@@ -280,7 +280,7 @@ pub fn log_config(config: &Config) {
     logv(c, format!("stage_id: {}", config.stage_id));
     logv(c, format!("mode: {}", config.mode));
     logv(c, format!("run_ignored: {}", config.run_ignored));
-    logv(c, format!("filter: {}", opt_str(&config.filter)));
+    logv(c, format!("filters: {:?}", config.filters));
     logv(c, format!("filter_exact: {}", config.filter_exact));
     logv(
         c,
@@ -465,7 +465,7 @@ fn configure_lldb(config: &Config) -> Option<Config> {
 pub fn test_opts(config: &Config) -> test::TestOpts {
     test::TestOpts {
         exclude_should_panic: false,
-        filter: config.filter.clone(),
+        filters: config.filters.clone(),
         filter_exact: config.filter_exact,
         run_ignored: if config.run_ignored { test::RunIgnored::Yes } else { test::RunIgnored::No },
         format: if config.quiet { test::OutputFormat::Terse } else { test::OutputFormat::Pretty },