Support FUZZTEST_MAX_JOBS environment variable to cap parallel jobs.

PiperOrigin-RevId: 955855667
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index 6133473..af8f9e2 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -772,6 +772,12 @@
   EXPECT_THAT(status, Eq(ExitCode(0)));
 }
 
+TEST_F(GenericCommandLineInterfaceTest, IgnoresInvalidJobsLimitInEnvVar) {
+  auto [status, std_out, std_err] = RunWith(
+      /*flags=*/{{"jobs", "10"}}, /*env=*/{{"FUZZTEST_MAX_JOBS", "-1"}});
+  EXPECT_THAT_LOG(std_err, HasSubstr("will not limit jobs"));
+}
+
 // Tests for the FuzzTest command line interface in fuzzing mode, which can only
 // run with coverage instrumentation enabled.
 class FuzzingModeCommandLineInterfaceTest
@@ -847,6 +853,16 @@
 #endif  // FUZZTEST_USE_CENTIPEDE
 }
 
+TEST_F(FuzzingModeCommandLineInterfaceTest, LimitsJobsWhenEnvVarIsSet) {
+  auto [status, std_out, std_err] = RunWith(
+      {{"fuzz", "MySuite.PassesWithPositiveInput"}, {"jobs", "10"}},
+      {{"FUZZTEST_MAX_JOBS", "2"}, {"FUZZTEST_MAX_FUZZING_RUNS", "10"}});
+#ifdef FUZZTEST_USE_CENTIPEDE
+  EXPECT_THAT_LOG(std_err, HasSubstr("Shard: 0/2"));
+#endif  // FUZZTEST_USE_CENTIPEDE
+  EXPECT_THAT(status, Eq(ExitCode(0)));
+}
+
 TEST_F(FuzzingModeCommandLineInterfaceTest, LimitsFuzzingRunsWhenTimeoutIsSet) {
   auto [status, std_out, std_err] = RunWith(
       {{"fuzz", "MySuite.PassesWithPositiveInput"}, {"fuzz_for", "1s"}});
diff --git a/fuzztest/init_fuzztest.cc b/fuzztest/init_fuzztest.cc
index 0653dce..c2fdc44 100644
--- a/fuzztest/init_fuzztest.cc
+++ b/fuzztest/init_fuzztest.cc
@@ -22,6 +22,7 @@
 #include "absl/flags/parse.h"
 #include "absl/flags/reflection.h"
 #include "absl/strings/match.h"
+#include "absl/strings/numbers.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_format.h"
 #include "absl/strings/str_join.h"
@@ -352,6 +353,19 @@
   FUZZTEST_CHECK(!jobs.has_value() || *jobs > 0)
       << "If specified, --" << FUZZTEST_FLAG(jobs).Name()
       << " must be positive.";
+  size_t num_jobs = jobs.value_or(0);
+  if (const char* max_jobs_env = std::getenv("FUZZTEST_MAX_JOBS")) {
+    if (size_t max_jobs;
+        absl::SimpleAtoi(max_jobs_env, &max_jobs) && max_jobs > 0) {
+      if (num_jobs > max_jobs) {
+        num_jobs = max_jobs;
+      }
+    } else {
+      absl::FPrintF(stderr,
+                    "[!] Failed to parse FUZZTEST_MAX_JOBS as a positive "
+                    "integer - will not limit jobs.\n");
+    }
+  }
   std::string corpus_database = absl::GetFlag(FUZZTEST_FLAG(corpus_database));
   if (!corpus_database.empty() && corpus_database[0] != '/' &&
       std::getenv("TEST_SRCDIR")) {
@@ -380,7 +394,7 @@
       absl::GetFlag(FUZZTEST_FLAG(time_limit_per_input)),
       time_limit,
       time_budget_type,
-      jobs.value_or(0),
+      num_jobs,
       absl::GetFlag(FUZZTEST_FLAG(internal_centipede_command)),
       absl::GetFlag(FUZZTEST_FLAG(internal_crashing_input_to_reproduce)),
   };