浏览代码

feat: initial model of GoogleTest-based performance testing

Provides [0, X) and (X, Y) matchers for average time.
Uses std::chrono, with duration suffix printer support.

TODO: Support for a complexity argument
TODO: Don't create a unique suite for each test case in TEST_PERF
Sam Jaffe 1 年之前
当前提交
47ebd40ba1

+ 202 - 0
include/testing/performance_test.h

@@ -0,0 +1,202 @@
+//
+//  performance_test.h
+//
+//  Created by Sam Jaffe on 11/4/23.
+//  Copyright © 2020 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+#define CONCAT2(A, B) A##B
+#define CONCAT(A, B) CONCAT2(A, B)
+
+#include <chrono>
+
+#include <testing/xcode_gtest_helper.h>
+
+#if __cplusplus >= 202002L
+#elif defined(__has_include) && __has_include(<date/date.h>)
+#include <date/date.h>
+using date::operator<<;
+#else
+namespace std::chrono {
+template <typename Rep, typename Period>
+ostream &operator<<(ostream &os, duration<Rep, Period> const & dur) {
+  os << dur.count();
+  if (Period::den == 1) {
+    switch (Period::num) {
+    case 1: return os << 's';
+    case 60: return os << 'm';
+    case 3600: return os << 'h';
+    case 86400: return os << 'd';
+    }
+  } else if (Period::num == 1) {
+    switch (Period::den) {
+    case 1000: return os << "ms";
+    case 1000'000: return os << "\u00B5s";
+    case 1000'000'000: return os << "ns";
+    }
+  }
+  return os << Period::num << '/' << Period::den << 's';
+}
+}
+#endif
+
+#define PERF_TEST_CLASS(name) CONCAT(PerfTest, name)
+
+/**
+ * Example code using TEST_PERF:
+ *
+ * TEST_PERF(MyClassTest, Initialization, 50ms) {
+ *   ... setup input variables ...
+ *   MyClass object(...);
+ *   (void) object;
+ * }
+ *
+ * void ComplexTest::SetUp() {
+ *   ... setup input variables as above to keep out of perf test ...
+ *   object = MyClass(...);
+ * }
+ *
+ * TEST_PERF_F(ComplexTest, ExpensiveFunction, 100ms) {
+ *   this->object.ExpensiveFunction();
+ * }
+ */
+#define TEST_PERF(test_suite_name, test_name, limit)                        \
+    PERFORMANCE_FIXTURE_IMPL(test_suite_name, test_name, testing::Test, limit)
+
+#define TEST_PERF_F(test_fixture, test_name, limit)          \
+    PERFORMANCE_FIXTURE_IMPL(test_suite_name, test_name, test_suite_name, limit)
+
+#define PERFORMANCE_TEST_CLASS(suite, case) CONCAT(suite, CONCAT(_, case))
+#define PERFORMANCE_FIXTURE_IMPL(suite, case, parent, limit) \
+    class PERFORMANCE_TEST_CLASS(suite, case) : public parent { \
+    protected: \
+      void TestImpl(); \
+    }; \
+    TEST_F(PERFORMANCE_TEST_CLASS(suite, case), Performance) { \
+      EXPECT_PERFORMANCE(TestImpl(), limit); \
+    } \
+    void PERFORMANCE_TEST_CLASS(suite, case)::TestImpl()
+
+/**
+ * Example code using EXPECT_PERFORMANCE:
+ * using std::literals::chrono_literals::operator""ms;
+ *
+ * TEST(MyClassPerfTest, Initialization) {
+ *   EXPECT_PERFORMANCE(MyClass(...), testing::Le(50ms));
+ * }
+ *
+ * TEST(MyClassPerfTest, ExpensiveFunction) {
+ *   MyClass object(...);
+ *   EXPECT_PERFORMANCE(object.ExpensiveFunction(), testing::Le(100ms));
+ * }
+ */
+#define EXPECT_PERFORMANCE(expression, matcher)     \
+  EXPECT_PERFORMANCE_IMPL(expression, matcher, 1UL)
+
+namespace testing::perf {
+using Clock = std::chrono::high_resolution_clock;
+
+template <typename> struct DurationImpl {
+  using type = std::chrono::nanoseconds;
+};
+
+template <typename Rep, typename Ratio>
+struct DurationImpl<std::chrono::duration<Rep, Ratio>> {
+  using type = std::chrono::duration<Rep, Ratio>;
+};
+
+template <typename Duration>
+struct PerformanceTestDuration {
+  Clock::duration average() const { return (ticks.back() - ticks.front()) / size(); }
+  operator Duration() const {
+    return std::chrono::duration_cast<Duration>(ticks.back() - ticks.front());
+  }
+
+  size_t size() const { return ticks.size() - 1; }
+  Duration operator()(size_t i = 0) const {
+    return std::chrono::duration_cast<Duration>(ticks.at(i + 1) - ticks.at(i));
+  }
+  void lap() { ticks.push_back(Clock::now()); }
+  
+  std::vector<Clock::time_point> ticks{};
+};
+
+template <typename Duration>
+void PrintTo(PerformanceTestDuration<Duration> const &ptd, std::ostream *os) {
+  (*os) << "duration: " << static_cast<Duration>(ptd);
+  if (size_t const n = ptd.size(); n > 1) {
+    [[maybe_unused]] int const digits = n == 1 ? 1 : std::log10(n - 1) + 1;
+    double X = 0, X2 = 0;
+    
+    for (size_t i = 0; i < n; ++i) {
+      auto d = ptd(i);
+      X += d.count();
+      X2 += std::pow(d.count(), 2);
+#     if defined(PERFORMANCE_TEST_PRINT_LAPS)
+        (*os) << "\nlap " << std::setw(digits) << i << ": " << d;
+#     endif
+    }
+    
+    long long E = X / n;
+    long long S = std::sqrt(X2 / n - std::pow(X / n, 2));
+    (*os) << "\naverage: " << Duration(E);
+    (*os) << "\nstd.dev: " << Duration(S);
+  }
+}
+
+template <typename> struct first_type {
+  using type = Clock::duration;
+};
+
+template <template <typename...> class Template, typename Arg0, typename Arg1, typename... Args>
+struct first_type<Template<Arg0, Arg1, Args...>> {
+  using type = std::decay_t<Arg1>;
+};
+
+template <typename T> using first_type_t = typename first_type<T>::type;
+
+MATCHER_P(FasterThan, limit, "is < " + testing::PrintToString(limit)) {
+  return arg.average() < limit;
+}
+
+MATCHER_P2(BetweenTimes, low, high,
+          "is between " + testing::PrintToString(low) + " and " +
+           testing::PrintToString(high)) {
+  return arg.average() < high && arg.average() > low;
+}
+
+MATCHER_P2(OnAverage, n, m,
+           (n == 1 ? "" : "on average ") + DescribeMatcher<arg_type>(m)) {
+  return testing::Matcher<arg_type>(m).MatchAndExplain(arg, result_listener);
+}
+}
+
+#define EXPECT_PERFORMANCE_IMPL(expression, matcher, n_reps) \
+  do {                                                       \
+    using namespace testing::perf;                           \
+    using namespace std::literals::chrono_literals;          \
+                                                             \
+    using duration_t = first_type_t<decltype(matcher)>;      \
+    PerformanceTestDuration<duration_t> duration;            \
+                                                             \
+    for (size_t i = 0; i < n_reps; ++i) {                    \
+      duration.lap();                                        \
+      (void) expression;                                     \
+    }                                                        \
+    duration.lap();                                          \
+                                                             \
+    EXPECT_THAT(duration, OnAverage(n_reps, matcher));       \
+  } while (false)
+
+
+#define TEST_PERF_IMPL(test_suite_name, test_name, parent_class, limit) \
+  class PERF_TEST_CLASS(test_suite_name) : public parent_class {        \
+  protected:                                                            \
+    void TestImpl();                                                    \
+  };                                                                    \
+  TEST_F(PERF_TEST_CLASS(test_suite_name), test_name) {                 \
+    EXPECT_PERFORMANCE(TestImpl(), limit);                              \
+  }                                                                     \
+  void PERF_TEST_CLASS(test_suite_name)::TestImpl()
+

+ 44 - 0
include/testing/xcode_gtest_helper.h

@@ -0,0 +1,44 @@
+//
+//  xcode_gtest_helper.h
+//
+//  Created by Sam Jaffe on 11/25/20.
+//  Copyright © 2020 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#if __has_include("printers.h")
+#  include "printers.h"
+#endif
+
+#if defined(__APPLE__)
+#  pragma clang diagnostic push
+#  pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
+#  pragma clang diagnostic ignored "-Wcomma"
+
+#  include <gmock/gmock.h>
+#  include <gtest/gtest.h>
+
+#  pragma clang diagnostic pop
+#else
+#  include <gmock/gmock.h>
+#  include <gtest/gtest.h>
+#endif
+
+#if defined(TARGET_OS_OSX)
+// This is a hack to allow XCode to properly display failures when running
+// unit tests.
+#  undef EXPECT_THAT
+#  define EXPECT_THAT ASSERT_THAT
+#  undef EXPECT_THROW
+#  define EXPECT_THROW ASSERT_THROW
+#  undef EXPECT_ANY_THROW
+#  define EXPECT_ANY_THROW ASSERT_ANY_THROW
+#  undef EXPECT_NO_THROW
+#  define EXPECT_NO_THROW ASSERT_NO_THROW
+#  undef EXPECT_TRUE
+#  define EXPECT_TRUE ASSERT_TRUE
+#  undef EXPECT_FALSE
+#  define EXPECT_FALSE ASSERT_FALSE
+
+#endif

+ 517 - 0
test-helpers.xcodeproj/project.pbxproj

@@ -0,0 +1,517 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 55;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		CDD966162B654D290091D92C /* GoogleMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDD9660C2B654D1B0091D92C /* GoogleMock.framework */; };
+		CDD966182B654D490091D92C /* test_helper_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDD966172B654D490091D92C /* test_helper_test.cxx */; };
+		CDD966192B654DDC0091D92C /* testing in Headers */ = {isa = PBXBuildFile; fileRef = CD6031032AF6779E000D9F31 /* testing */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		CDD965FF2B654D090091D92C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD6030F42AF67772000D9F31 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = CD6030FB2AF67772000D9F31;
+			remoteInfo = "test-helpers";
+		};
+		CDD9660B2B654D1B0091D92C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05818F861A685AEA0072A469;
+			remoteInfo = GoogleMock;
+		};
+		CDD9660D2B654D1B0091D92C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05E96ABD1A68600C00204102;
+			remoteInfo = gmock;
+		};
+		CDD9660F2B654D1B0091D92C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05E96B1F1A68634900204102;
+			remoteInfo = gtest;
+		};
+		CDD966112B654D1B0091D92C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05818F901A685AEA0072A469;
+			remoteInfo = GoogleMockTests;
+		};
+		CDD966132B654D230091D92C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */;
+			proxyType = 1;
+			remoteGlobalIDString = 05818F851A685AEA0072A469;
+			remoteInfo = GoogleMock;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		CD6030FC2AF67772000D9F31 /* libtest-helpers.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libtest-helpers.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD6031032AF6779E000D9F31 /* testing */ = {isa = PBXFileReference; lastKnownFileType = folder; name = testing; path = include/testing; sourceTree = "<group>"; };
+		CD6031062AF677BE000D9F31 /* performance_test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = performance_test.h; sourceTree = "<group>"; };
+		CD6031072AF677ED000D9F31 /* xcode_gtest_helper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = xcode_gtest_helper.h; sourceTree = "<group>"; };
+		CDD965FA2B654D090091D92C /* test-helpers-test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "test-helpers-test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+		CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
+		CDD966172B654D490091D92C /* test_helper_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = test_helper_test.cxx; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		CD6030FA2AF67772000D9F31 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		CDD965F72B654D090091D92C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CDD966162B654D290091D92C /* GoogleMock.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		CD6030F32AF67772000D9F31 = {
+			isa = PBXGroup;
+			children = (
+				CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */,
+				CD6031032AF6779E000D9F31 /* testing */,
+				CD6031042AF677A7000D9F31 /* include */,
+				CDD965FB2B654D090091D92C /* test */,
+				CD6030FD2AF67772000D9F31 /* Products */,
+				CDD966152B654D290091D92C /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		CD6030FD2AF67772000D9F31 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				CD6030FC2AF67772000D9F31 /* libtest-helpers.a */,
+				CDD965FA2B654D090091D92C /* test-helpers-test.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		CD6031042AF677A7000D9F31 /* include */ = {
+			isa = PBXGroup;
+			children = (
+				CD6031052AF677A7000D9F31 /* testing */,
+			);
+			path = include;
+			sourceTree = "<group>";
+		};
+		CD6031052AF677A7000D9F31 /* testing */ = {
+			isa = PBXGroup;
+			children = (
+				CD6031062AF677BE000D9F31 /* performance_test.h */,
+				CD6031072AF677ED000D9F31 /* xcode_gtest_helper.h */,
+			);
+			path = testing;
+			sourceTree = "<group>";
+		};
+		CDD965FB2B654D090091D92C /* test */ = {
+			isa = PBXGroup;
+			children = (
+				CDD966172B654D490091D92C /* test_helper_test.cxx */,
+			);
+			path = test;
+			sourceTree = "<group>";
+		};
+		CDD966052B654D1B0091D92C /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				CDD9660C2B654D1B0091D92C /* GoogleMock.framework */,
+				CDD9660E2B654D1B0091D92C /* gmock.framework */,
+				CDD966102B654D1B0091D92C /* gtest.framework */,
+				CDD966122B654D1B0091D92C /* GoogleMockTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		CDD966152B654D290091D92C /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+		CD6030F82AF67772000D9F31 /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CDD966192B654DDC0091D92C /* testing in Headers */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+		CD6030FB2AF67772000D9F31 /* test-helpers */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CD6031002AF67772000D9F31 /* Build configuration list for PBXNativeTarget "test-helpers" */;
+			buildPhases = (
+				CD6030F82AF67772000D9F31 /* Headers */,
+				CD6030F92AF67772000D9F31 /* Sources */,
+				CD6030FA2AF67772000D9F31 /* Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "test-helpers";
+			productName = "test-helpers";
+			productReference = CD6030FC2AF67772000D9F31 /* libtest-helpers.a */;
+			productType = "com.apple.product-type.library.static";
+		};
+		CDD965F92B654D090091D92C /* test-helpers-test */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CDD966032B654D090091D92C /* Build configuration list for PBXNativeTarget "test-helpers-test" */;
+			buildPhases = (
+				CDD965F62B654D090091D92C /* Sources */,
+				CDD965F72B654D090091D92C /* Frameworks */,
+				CDD965F82B654D090091D92C /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				CDD966142B654D230091D92C /* PBXTargetDependency */,
+				CDD966002B654D090091D92C /* PBXTargetDependency */,
+			);
+			name = "test-helpers-test";
+			productName = "test-helpers-test";
+			productReference = CDD965FA2B654D090091D92C /* test-helpers-test.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		CD6030F42AF67772000D9F31 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1340;
+				LastUpgradeCheck = 1340;
+				TargetAttributes = {
+					CD6030FB2AF67772000D9F31 = {
+						CreatedOnToolsVersion = 13.4.1;
+					};
+					CDD965F92B654D090091D92C = {
+						CreatedOnToolsVersion = 13.4.1;
+					};
+				};
+			};
+			buildConfigurationList = CD6030F72AF67772000D9F31 /* Build configuration list for PBXProject "test-helpers" */;
+			compatibilityVersion = "Xcode 13.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = CD6030F32AF67772000D9F31;
+			productRefGroup = CD6030FD2AF67772000D9F31 /* Products */;
+			projectDirPath = "";
+			projectReferences = (
+				{
+					ProductGroup = CDD966052B654D1B0091D92C /* Products */;
+					ProjectRef = CDD966042B654D1B0091D92C /* GoogleMock.xcodeproj */;
+				},
+			);
+			projectRoot = "";
+			targets = (
+				CD6030FB2AF67772000D9F31 /* test-helpers */,
+				CDD965F92B654D090091D92C /* test-helpers-test */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXReferenceProxy section */
+		CDD9660C2B654D1B0091D92C /* GoogleMock.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = GoogleMock.framework;
+			remoteRef = CDD9660B2B654D1B0091D92C /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CDD9660E2B654D1B0091D92C /* gmock.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = gmock.framework;
+			remoteRef = CDD9660D2B654D1B0091D92C /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CDD966102B654D1B0091D92C /* gtest.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = gtest.framework;
+			remoteRef = CDD9660F2B654D1B0091D92C /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CDD966122B654D1B0091D92C /* GoogleMockTests.xctest */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.cfbundle;
+			path = GoogleMockTests.xctest;
+			remoteRef = CDD966112B654D1B0091D92C /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+		CDD965F82B654D090091D92C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		CD6030F92AF67772000D9F31 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		CDD965F62B654D090091D92C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CDD966182B654D490091D92C /* test_helper_test.cxx in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		CDD966002B654D090091D92C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = CD6030FB2AF67772000D9F31 /* test-helpers */;
+			targetProxy = CDD965FF2B654D090091D92C /* PBXContainerItemProxy */;
+		};
+		CDD966142B654D230091D92C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = GoogleMock;
+			targetProxy = CDD966132B654D230091D92C /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+		CD6030FE2AF67772000D9F31 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 12.3;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
+			};
+			name = Debug;
+		};
+		CD6030FF2AF67772000D9F31 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 12.3;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
+			};
+			name = Release;
+		};
+		CD6031012AF67772000D9F31 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				EXECUTABLE_PREFIX = lib;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Debug;
+		};
+		CD6031022AF67772000D9F31 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				EXECUTABLE_PREFIX = lib;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Release;
+		};
+		CDD966012B654D090091D92C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.test-helpers-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		CDD966022B654D090091D92C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				GENERATE_INFOPLIST_FILE = YES;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.test-helpers-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		CD6030F72AF67772000D9F31 /* Build configuration list for PBXProject "test-helpers" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CD6030FE2AF67772000D9F31 /* Debug */,
+				CD6030FF2AF67772000D9F31 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		CD6031002AF67772000D9F31 /* Build configuration list for PBXNativeTarget "test-helpers" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CD6031012AF67772000D9F31 /* Debug */,
+				CD6031022AF67772000D9F31 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		CDD966032B654D090091D92C /* Build configuration list for PBXNativeTarget "test-helpers-test" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CDD966012B654D090091D92C /* Debug */,
+				CDD966022B654D090091D92C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = CD6030F42AF67772000D9F31 /* Project object */;
+}

+ 26 - 0
test/test_helper_test.cxx

@@ -0,0 +1,26 @@
+//
+//  test_helper_test.cxx
+//  test-helpers-test
+//
+//  Created by Sam Jaffe on 1/27/24.
+//
+
+#include "testing/xcode_gtest_helper.h"
+#include "testing/performance_test.h"
+
+#include <thread>
+
+using namespace std::literals::chrono_literals;
+using testing::Not;
+
+TEST_PERF(PerformanceMetaTest, Fast, FasterThan(1us)) {
+  strlen("HELLO, WORLD");
+}
+
+TEST_PERF(PerformanceMetaTest, Slow, Not(FasterThan(1us))) {
+  std::this_thread::sleep_for(1us);
+}
+
+TEST_PERF(PerformanceMetaTest, Right, BetweenTimes(1ns, 1us)) {
+  strlen("HELLO, WORLD");
+}