[testonly] Allow configs to be testonly

Bug: 214
Change-Id: Idec794f14f5755eebc1e3ed879c19b015f6d433b
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/12680
Reviewed-by: David Turner <digit@google.com>
Reviewed-by: Brett Wilson <brettw@chromium.org>
Commit-Queue: Shai Barack <shayba@google.com>
diff --git a/src/gn/config_values_generator.h b/src/gn/config_values_generator.h
index fd508d4..d006140 100644
--- a/src/gn/config_values_generator.h
+++ b/src/gn/config_values_generator.h
@@ -41,6 +41,6 @@
   "  Flags: cflags, cflags_c, cflags_cc, cflags_objc, cflags_objcc,\n"     \
   "         asmflags, defines, include_dirs, inputs, ldflags, lib_dirs,\n" \
   "         libs, precompiled_header, precompiled_source, rustflags,\n"    \
-  "         rustenv, swiftflags\n"
+  "         rustenv, swiftflags, testonly\n"
 
 #endif  // TOOLS_GN_CONFIG_VALUES_GENERATOR_H_
diff --git a/src/gn/item.h b/src/gn/item.h
index 2942a66..dd1a606 100644
--- a/src/gn/item.h
+++ b/src/gn/item.h
@@ -38,6 +38,9 @@
   const ParseNode* defined_from() const { return defined_from_; }
   void set_defined_from(const ParseNode* df) { defined_from_ = df; }
 
+  bool testonly() const { return testonly_; }
+  void set_testonly(bool value) { testonly_ = value; }
+
   Visibility& visibility() { return visibility_; }
   const Visibility& visibility() const { return visibility_; }
 
@@ -69,11 +72,14 @@
   virtual bool OnResolved(Err* err);
 
  private:
+  bool CheckTestonly(Err* err) const;
+
   const Settings* settings_;
   Label label_;
   SourceFileSet build_dependency_files_;
   const ParseNode* defined_from_;
 
+  bool testonly_ = false;
   Visibility visibility_;
 };
 
diff --git a/src/gn/target.cc b/src/gn/target.cc
index da6b7e4..ded575a 100644
--- a/src/gn/target.cc
+++ b/src/gn/target.cc
@@ -43,7 +43,7 @@
   }
 }
 
-Err MakeTestOnlyError(const Target* from, const Target* to) {
+Err MakeTestOnlyError(const Item* from, const Item* to) {
   return Err(
       from->defined_from(), "Test-only dependency not allowed.",
       from->label().GetUserVisibleName(false) +
@@ -1142,6 +1142,16 @@
     }
   }
 
+  // Verify no configs have "testonly" set.
+  for (ConfigValuesIterator iter(this); !iter.done(); iter.Next()) {
+    if (const Config* config = iter.GetCurrentConfig()) {
+      if (config->testonly()) {
+        *err = MakeTestOnlyError(this, config);
+        return false;
+      }
+    }
+  }
+
   return true;
 }
 
diff --git a/src/gn/target.h b/src/gn/target.h
index 59fe806..d88f0a5 100644
--- a/src/gn/target.h
+++ b/src/gn/target.h
@@ -214,9 +214,6 @@
   }
   std::vector<std::string>& walk_keys() { return generated_file().walk_keys_; }
 
-  bool testonly() const { return testonly_; }
-  void set_testonly(bool value) { testonly_ = value; }
-
   OutputFile write_runtime_deps_output() const {
     return write_runtime_deps_output_;
   }
@@ -474,7 +471,6 @@
   FileList public_headers_;
   bool check_includes_ = true;
   bool complete_static_lib_ = false;
-  bool testonly_ = false;
   std::vector<std::string> data_;
   std::unique_ptr<BundleData> bundle_data_;
   OutputFile write_runtime_deps_output_;
diff --git a/src/gn/target_unittest.cc b/src/gn/target_unittest.cc
index f8cbcd1..b4ea699 100644
--- a/src/gn/target_unittest.cc
+++ b/src/gn/target_unittest.cc
@@ -628,6 +628,31 @@
   ASSERT_FALSE(product.OnResolved(&err));
 }
 
+// Configs can be testonly too.
+// Repeat the testonly test with a config.
+TEST_F(TargetTest, TestonlyConfig) {
+  TestWithScope setup;
+  Err err;
+
+  // "testconfig" is a test-only config.
+  Config testconfig(setup.settings(), Label(SourceDir("//test/"), "config"));
+  testconfig.set_testonly(true);
+  testconfig.visibility().SetPublic();
+  ASSERT_TRUE(testconfig.OnResolved(&err));
+
+  // "test" is a test-only executable that uses testconfig, this is OK.
+  TestTarget test(setup, "//test:test", Target::EXECUTABLE);
+  test.set_testonly(true);
+  test.configs().push_back(LabelConfigPair(&testconfig));
+  ASSERT_TRUE(test.OnResolved(&err));
+
+  // "product" is a non-test that uses testconfig. This should fail.
+  TestTarget product(setup, "//app:product", Target::EXECUTABLE);
+  product.set_testonly(false);
+  product.configs().push_back(LabelConfigPair(&testconfig));
+  ASSERT_FALSE(product.OnResolved(&err));
+}
+
 TEST_F(TargetTest, PublicConfigs) {
   TestWithScope setup;
   Err err;