public void run()
    {
        testObj      = NTestClass.create("test");
        callbackTest = NGlobal.createCallbackTestInstance();

        Log.write("Running performance tests...");

        StringBuilder primaryResult   = new StringBuilder();
        StringBuilder secondaryResult = new StringBuilder();

        primaryResult.Append("Performance test results (CPU cycles):\n");
        primaryResult.Append("\n");

        List <PerformanceTest> tests = new List <PerformanceTest>();

        getTestsFromClassByReflection(this).ForEach(test => {
            test.belongsToPrimaryTests = true;
            tests.Add(test);
        });

        tests.AddRange(PInvokeTests.getTests());
        tests.AddRange(PInvokeStringTests.getTests());

        for (int outerRound = 0; outerRound < outerRounds; outerRound++)
        {
            foreach (PerformanceTest test in tests)
            {
                long startTime   = NGlobal.getTimeRdtsc();
                long rounds      = test.runAndGetRounds();
                long elapsedTime = NGlobal.getTimeRdtsc() - startTime;
                test.times.Add((double)elapsedTime / rounds);
                test.totalTime   += elapsedTime;
                test.totalRounds += rounds;
            }
        }

        string lastCategory = null;

        foreach (PerformanceTest test in tests)
        {
            StringBuilder result = test.belongsToPrimaryTests ? primaryResult : secondaryResult;

            if (test.category != lastCategory)
            {
                lastCategory = test.category;
                result.Append("\n");
                result.Append($"{lastCategory}:\n");
            }

            result.Append($"{test.name}: {test.sortAndGetMedianTime()}\n");
        }

        Log.write("");
        Log.write(primaryResult.ToString());
        Log.write($"Writing more performance test results to: {Path.GetFullPath(resultFile)}");
        File.WriteAllBytes(resultFile, new UTF8Encoding().GetBytes(primaryResult.ToString() + secondaryResult.ToString()));
    }
    public void run(string projectDir, bool openGlTestEnabled)
    {
        try {
            // Create a C++ object and invoke some functions.
            {
                NTestClass obj = NTestClass.create("test1234ۆ");
                checkEqual(obj.isNull(), false, "isNull() returned wrong value");
                checkEqual(obj.getName(), "test1234ۆ", "Wrong name returned by getter");
                checkEqual(obj.concatenateStrings("abcۆ", "defۆ"), "abcۆdefۆ", "Strings not concatenated correctly");
                checkEqual(obj.concatenateStringsUtf16("ghiۆ", "jklۆ"), "ghiۆjklۆ", "UTF-16 strings not concatenated correctly");

                // An exception can be propagated from C++ to C#.
                try {
                    obj.throwException();
                }
                catch (Exception e) {
                    if (!e.Message.Contains("test_exception"))
                    {
                        throw new Exception("Got wrong exception from C++ function", e);
                    }
                }

                // Vector math.
                checkEqual(obj.addFloatVectors(new Vector4(1, 2, 3, 4), new Vector4(5, 6, 7, 8)), new Vector4(6, 8, 10, 12), "Float vectors summed incorrectly");
                checkEqual(obj.addFloatVectorsNoexcept(new Vector4(2, 3, 4, 5), new Vector4(6, 7, 8, 9)), new Vector4(8, 10, 12, 14), "Float vectors summed incorrectly (noexcept version)");
                checkEqual(obj.getColor(new Vector2(1, 2)), new Vector4(2, 4, 4, 6), "getColor() returned wrong value");

                // Destroy the object.
                obj.release();

                checkEqual(NGlobal.partition2Test(), 22, "Partition 2 test function returned wrong value");
            }

            // Subclasses.
            {
                NDerivedClass obj = NGlobal.createDerivedClassInstance();
                checkEqual(obj.test1(), "derived1", "Overridden function returned wrong value");
                checkEqual(obj.test3(), "base3", "Non-overridden function returned wrong value");
                checkEqual(typeof(NDerivedClass).GetMethod("test2"), null, "test2() function should not be accessible because the base class was derived as private");

                // A derived-class pointer can be converted to base-class pointer.
                NBaseClass1 basePtr = obj;
                checkEqual(basePtr.test1(), "derived1", "Overridden function returned wrong value when invoked with a base class pointer");
                checkEqual(typeof(NBaseClass1).GetMethod("test3"), null, "Base class should not have functions that only exist in other base classes of the derived class");

                obj.release();
            }

            // Create a base class instance directly.
            {
                NBaseClass1 obj = NGlobal.createBaseClass1Instance();
                checkEqual(obj.test1(), "base1", "Function from base class instance returned wrong value");
                obj.release();
            }

            // Create a struct and access its fields by a pointer from both C++ and C#.
            {
                TestStruct1 s    = new TestStruct1();
                string      name = nameof(TestStruct1);
                checkEqual(s.i1, 0, $"{name} field i1 has wrong value before initialization");
                NGlobal.setStruct1Values(&s);
                checkEqual(s.i1, 100, $"{name} field i1 has wrong value");
                checkEqual(s.v1, new Vector4(1, 2, 3, 4), $"{name} field v1 has wrong value");
                checkEqual(s.array1[1], 200, $"{name} field array1[1] has wrong value");
            }
            {
                TestStruct2 s    = new TestStruct2();
                string      name = nameof(TestStruct2);
                checkEqual(s.i, 0, $"{name} field i has wrong value before initialization");
                NGlobal.setStruct2Values(&s);
                checkEqual(s.i, 100, $"{name} field s.i has wrong value");
            }
            {
                CustomSharedStruct s    = new CustomSharedStruct();
                string             name = nameof(CustomSharedStruct);
                checkEqual(s.i1, 0, $"{name} field i1 has wrong value before initialization");
                NGlobal.setCustomSharedStructValues(&s);
                checkEqual(s.i1, 700, $"{name} field i1 has wrong value");
                checkEqual(s.v1, new Vector4(10, 20, 30, 40), "Struct field v1 has wrong value");
            }

            // Callback functions
            {
                NCallbackTest cbTest = NGlobal.createCallbackTestInstance();

                checkEqual(cbTest.invokeGivenCallback("string5ۆ", "string6ۆ", (s1, s2) => s1 + s2), "string5ۆstring6ۆ", "C++ -> C# callback (given as parameter) should have concatenated strings");
                checkEqual(cbTest.invokeGivenCallbackUtf16("string7ۆ", "string8ۆ", (s1, s2) => s1 + s2), "string7ۆstring8ۆ", "C++ -> C# callback (given as parameter) should have concatenated UTF-16 strings");

                cbTest.setCallback((s1, s2) => s1 + s2);
                checkEqual(cbTest.invokeStoredCallback("string1ۆ", "string2ۆ"), "string1ۆstring2ۆ", "C++ -> C# callback (stored) should have concatenated strings");
            }

            // Namespaces.
            {
                CsNamespace.CppOuterNamespace.NTestClass2 obj = NGlobal.createTestClass2Instance("test5678", 123);
                checkEqual(obj.getName(), "test5678", "Wrong name returned by getter");
                checkEqual(obj.getIndex(), 123, "Wrong index returned by getter");
                obj.release();

                checkEqual(CsNamespace.CppOuterNamespace.NGlobal.calculateSum(1, CsNamespace.CppOuterNamespace.EnumInsideNamespace.TEST2), 3, "Wrong result from calculateSum()");
            }
            {
                CsNamespace.CppOuterNamespace.CppInnerNamespace.NTestClass3 obj = NGlobal.createTestClass3Instance("test_abc");
                checkEqual(obj.getName(), "test_abc", "Wrong name returned by getter");
                obj.release();

                CsNamespace.CppOuterNamespace.CppInnerNamespace.StructInsideNamespace s = new CsNamespace.CppOuterNamespace.CppInnerNamespace.StructInsideNamespace();
                s.v = 7;
                checkEqual(CsNamespace.CppOuterNamespace.CppInnerNamespace.NGlobal.calculateProduct(5, s), 35, "Wrong result from calculateProduct()");
            }

            // File set.
            {
                NGlobal.handleBicycle(new IncludedBicycleStruct());
                NGlobal.handleVehicle(new IncludedVehicleStruct());
                checkEqual(Type.GetType("CsNamespace.IncludedBicycleStruct") == null, false, "Struct from included file should be available");
                checkEqual(Type.GetType("CsNamespace.ExcludedCarStruct") == null, true, "Struct from excluded file not exist");
                checkEqual(Type.GetType("CsNamespace.ExcludedFileStruct") == null, true, "Struct from excluded file not exist");
            }

            // Enum reflection in C++.
            {
                string result = NGlobal.testEnumReflection();
                if (result.Length > 0)
                {
                    throw new Exception($"Error in enum reflection test: {result}");
                }
            }

            // OpenGL.
            if (openGlTestEnabled)
            {
                NGlobal.testOpenGl(projectDir);
            }
            else
            {
                Log.write("Skipping OpenGL test", null, ConsoleColor.Yellow);
            }

            Log.write("*** FUNCTIONAL TEST SUCCESS ***", null, ConsoleColor.Green);
        }
        catch (Exception e) {
            Log.write("*** FUNCTIONAL TEST FAILED ***", e, ConsoleColor.Red);
        }
    }