summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/slang-test/slang-test-main.cpp2
-rw-r--r--tools/slang-test/test-context.cpp1
-rw-r--r--tools/slang-test/test-context.h1
-rw-r--r--tools/slang-unit-test/unit-test-record-replay.cpp437
4 files changed, 440 insertions, 1 deletions
diff --git a/tools/slang-test/slang-test-main.cpp b/tools/slang-test/slang-test-main.cpp
index 06eae7047..7616ab614 100644
--- a/tools/slang-test/slang-test-main.cpp
+++ b/tools/slang-test/slang-test-main.cpp
@@ -4256,7 +4256,7 @@ static SlangResult runUnitTestModule(TestContext* context, TestOptions& testOpti
ComPtr<ISlangSharedLibrary> moduleLibrary;
SLANG_RETURN_ON_FAIL(loader->loadSharedLibrary(
- Path::combine(context->exeDirectoryPath, moduleName).getBuffer(),
+ Path::combine(context->dllDirectoryPath, moduleName).getBuffer(),
moduleLibrary.writeRef()));
UnitTestGetModuleFunc getModuleFunc =
diff --git a/tools/slang-test/test-context.cpp b/tools/slang-test/test-context.cpp
index b1be005cb..ed25831e3 100644
--- a/tools/slang-test/test-context.cpp
+++ b/tools/slang-test/test-context.cpp
@@ -88,6 +88,7 @@ Result TestContext::init(const char* inExePath)
}
exePath = inExePath;
SLANG_RETURN_ON_FAIL(TestToolUtil::getExeDirectoryPath(inExePath, exeDirectoryPath));
+ SLANG_RETURN_ON_FAIL(TestToolUtil::getDllDirectoryPath(inExePath, dllDirectoryPath));
SLANG_RETURN_ON_FAIL(locateFileCheck());
diff --git a/tools/slang-test/test-context.h b/tools/slang-test/test-context.h
index b42ac1ec9..28d39b064 100644
--- a/tools/slang-test/test-context.h
+++ b/tools/slang-test/test-context.h
@@ -148,6 +148,7 @@ class TestContext
Slang::RefPtr<Slang::DownstreamCompilerSet> compilerSet;
Slang::String exeDirectoryPath;
+ Slang::String dllDirectoryPath;
Slang::String exePath;
/// Timeout time for communication over connection.
diff --git a/tools/slang-unit-test/unit-test-record-replay.cpp b/tools/slang-unit-test/unit-test-record-replay.cpp
new file mode 100644
index 000000000..2d5619f71
--- /dev/null
+++ b/tools/slang-unit-test/unit-test-record-replay.cpp
@@ -0,0 +1,437 @@
+// unit-test-record-replay.cpp
+
+#include "../../source/core/slang-string-util.h"
+#include "../../source/core/slang-process-util.h"
+
+#include "../../source/core/slang-io.h"
+#include "../../source/core/slang-http.h"
+#include "../../source/core/slang-random-generator.h"
+
+#include "tools/unit-test/slang-unit-test.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#include <shellapi.h>
+#else
+#include <ftw.h>
+#endif
+
+#include <chrono>
+#include <thread>
+
+using namespace Slang;
+
+static SlangResult createProcess(UnitTestContext* context, const char* processName, const List<String>* optArgs, RefPtr<Process>& outProcess)
+{
+ CommandLine cmdLine;
+ cmdLine.setExecutableLocation(ExecutableLocation(context->executableDirectory, processName));
+ if (optArgs)
+ {
+ cmdLine.m_args.addRange(optArgs->getBuffer(), optArgs->getCount());
+ }
+
+ SLANG_RETURN_ON_FAIL(Process::create(cmdLine, Process::Flag::AttachDebugger, outProcess));
+
+ return SLANG_OK;
+}
+
+struct entryHashInfo
+{
+ int64_t targetIndex = -1;
+ int64_t entryPointIndex = -1;
+ String hash;
+};
+
+static SlangResult parseHashes(List<String> const& lines, List<entryHashInfo>& outHashes)
+{
+ SlangResult res = SLANG_OK;
+
+ for (const auto& line : lines)
+ {
+ List<UnownedStringSlice> tokens;
+ Index skipCharacters = line.indexOf(UnownedStringSlice("[slang-record-replay]:"));
+ if (skipCharacters == -1)
+ {
+ skipCharacters = 0;
+ }
+ else
+ {
+ skipCharacters += strlen("[slang-record-replay]:");
+ }
+ StringUtil::split(UnownedStringSlice(line.getBuffer() + skipCharacters), ',', tokens);
+
+ if (tokens.getCount() != 3)
+ {
+ return SLANG_FAIL;
+ }
+
+ entryHashInfo hashInfo;
+ auto extractToken = [](const UnownedStringSlice& token, const char splitChar, UnownedStringSlice& outToken) -> SlangResult
+ {
+ List<UnownedStringSlice> subTokens;
+ StringUtil::split(token, splitChar, subTokens);
+ if (subTokens.getCount() != 2)
+ {
+ return SLANG_FAIL;
+ }
+ outToken = subTokens[1];
+ return SLANG_OK;
+ };
+
+ {
+ UnownedStringSlice subToken;
+ SLANG_RETURN_ON_FAIL(extractToken(tokens[0], ':', subToken));
+ int64_t outNumer = 0;
+ StringUtil::parseInt64(subToken, outNumer);
+ hashInfo.entryPointIndex = outNumer;
+ }
+
+ {
+ UnownedStringSlice subToken;
+ SLANG_RETURN_ON_FAIL(extractToken(tokens[1], ':', subToken));
+ int64_t outNumer = 0;
+ StringUtil::parseInt64(subToken, outNumer);
+ hashInfo.targetIndex = outNumer;
+ }
+
+ {
+ UnownedStringSlice subToken;
+ SLANG_RETURN_ON_FAIL(extractToken(tokens[2], ':', subToken));
+ hashInfo.hash = subToken.begin() + 1;
+ }
+
+ outHashes.add(hashInfo);
+ }
+ return res;
+}
+
+static int writeEnvironmentVariable(const char* key, const char* val)
+{
+#ifdef _WIN32
+ String var = String(key) + "=" + val;
+ return _putenv(var.getBuffer());
+#else
+ return setenv(key, val, 1);
+#endif
+}
+
+static bool enableRecordLayer()
+{
+ int retCode = writeEnvironmentVariable("SLANG_RECORD_LAYER", "1");
+ return retCode == 0;
+}
+
+static bool disableRecordLayer()
+{
+ int retCode = writeEnvironmentVariable("SLANG_RECORD_LAYER", "0");
+ return retCode == 0;
+}
+
+static bool enableLogInReplayer()
+{
+ int retCode = writeEnvironmentVariable("SLANG_RECORD_LOG_LEVEL", "3");
+ return retCode == 0;
+}
+
+static bool disableLogInReplayer()
+{
+ int retCode = writeEnvironmentVariable("SLANG_RECORD_LOG_LEVEL", "0");
+ return retCode == 0;
+}
+
+static void findRecordFileName(List<String>* fileNames)
+{
+ struct Visitor : Path::Visitor
+ {
+ void accept(Path::Type type, const UnownedStringSlice& filename) SLANG_OVERRIDE
+ {
+ if (type == Path::Type::File)
+ {
+ m_fileNames->add(filename);
+ }
+ }
+ Visitor(List<String>* fileNames) : m_fileNames(fileNames) {}
+ List<String>* m_fileNames;
+ };
+
+ Visitor visitor(fileNames);
+ Path::find("slang-record", "*.cap", &visitor);
+}
+
+static SlangResult launchProcessAndReadStdout(UnitTestContext* context, const List<String>& optArgs,
+ const char* exampleName, RefPtr<Process>& process, ExecuteResult& exeRes)
+{
+ StringBuilder msgBuilder;
+ SlangResult res = createProcess(context, exampleName, &optArgs, process);
+ if (SLANG_FAILED(res))
+ {
+ msgBuilder << "Failed to launch process of '"<< exampleName << "'\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return res;
+ }
+
+ res = ProcessUtil::readUntilTermination(process, exeRes);
+ if (SLANG_FAILED(res))
+ {
+ msgBuilder << "Failed to read stdout from '" << exampleName << "'\n";
+ msgBuilder << "process ret code: " << exeRes.resultCode;
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return res;
+ }
+
+ if (exeRes.standardOutput.getLength() == 0)
+ {
+ msgBuilder << "No stdout found in '" << exampleName << "'\n";
+ msgBuilder << "Standard error: " << exeRes.standardError;
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return SLANG_FAIL;
+ }
+ return SLANG_OK;
+}
+
+static SlangResult runExample(UnitTestContext* context, const char* exampleName, List<entryHashInfo>& outHashes)
+{
+ SlangResult finalRes = SLANG_OK;
+
+ RefPtr<Process> process;
+ ExecuteResult exeRes;
+ List<String> optArgs;
+ optArgs.add("--test-mode");
+
+ StringBuilder msgBuilder;
+ SlangResult res = SLANG_OK;
+
+ enableRecordLayer();
+ res = launchProcessAndReadStdout(context, optArgs, exampleName, process, exeRes);
+ disableRecordLayer();
+
+ if (SLANG_FAILED(res))
+ {
+ return res;
+ }
+
+ List<String> hashLines;
+ for (auto line : LineParser(exeRes.standardOutput.getUnownedSlice()))
+ {
+ if (line.getLength() == 0)
+ {
+ continue;
+ }
+
+ if (line.indexOf(UnownedStringSlice("hash:")) == -1)
+ {
+ continue;
+ }
+
+ hashLines.add(line);
+ }
+
+ res = parseHashes(hashLines, outHashes);
+ if (SLANG_FAILED(res))
+ {
+ msgBuilder << "Failed to parse hash from stdout of '" << exampleName << "'\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return res;
+ }
+
+ return SLANG_OK;
+}
+
+static SlangResult replayExample(UnitTestContext* context, List<entryHashInfo>& outHashes)
+{
+ List<String> fileNames;
+ findRecordFileName(&fileNames);
+
+ List<String> optArgs;
+ String recordFileName = Path::combine("slang-record", fileNames[0]);
+ optArgs.add(recordFileName.getBuffer());
+
+ RefPtr<Process> process;
+ ExecuteResult exeRes;
+
+ StringBuilder msgBuilder;
+ msgBuilder << "replay the test\n";
+
+ enableLogInReplayer();
+ SlangResult res = launchProcessAndReadStdout(context, optArgs, "slang-replay", process, exeRes);
+ disableLogInReplayer();
+
+ if (SLANG_FAILED(res))
+ {
+ return res;
+ }
+
+ List<String> hashLines;
+ for (auto line : LineParser(exeRes.standardOutput.getUnownedSlice()))
+ {
+ if (line.getLength() == 0)
+ {
+ continue;
+ }
+
+ if (line.indexOf(UnownedStringSlice("hash:")) == -1)
+ {
+ continue;
+ }
+
+ hashLines.add(line);
+ }
+
+ res = parseHashes(hashLines, outHashes);
+ if (SLANG_FAILED(res))
+ {
+ msgBuilder << "Failed to parse hash from stdout of 'slang-replay'\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return SLANG_FAIL;
+ }
+
+ return SLANG_OK;
+}
+
+static SlangResult resultCompare(List<entryHashInfo> const& expectHashes, List<entryHashInfo> const& resultHashes)
+{
+ if (expectHashes.getCount() != resultHashes.getCount())
+ {
+ return SLANG_FAIL;
+ }
+
+ StringBuilder msgBuilder;
+ for (Index i = 0; i < expectHashes.getCount(); i++)
+ {
+ if (expectHashes[i].targetIndex != resultHashes[i].targetIndex)
+ {
+ msgBuilder << "Failed to match 'targetIndex' at index " << i << "\n";
+ msgBuilder << "Expect: " << expectHashes[i].targetIndex << ", actual: " << resultHashes[i].targetIndex << "\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return SLANG_FAIL;
+ }
+ if (expectHashes[i].entryPointIndex != resultHashes[i].entryPointIndex)
+ {
+ msgBuilder << "Failed to match 'entryPointIndex' at index " << i << "\n";
+ msgBuilder << "Expect: " << expectHashes[i].entryPointIndex << ", actual: " << resultHashes[i].entryPointIndex << "\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return SLANG_FAIL;
+ }
+
+ if (expectHashes[i].hash != resultHashes[i].hash)
+ {
+ msgBuilder << "Failed to match 'hash' at index " << i << "\n";
+ msgBuilder << "Expect: " << expectHashes[i].hash << ", actual: " << resultHashes[i].hash << "\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return SLANG_FAIL;
+ }
+ }
+
+ return SLANG_OK;
+}
+
+static SlangResult cleanupRecordFiles()
+{
+ if (File::exists("slang-record") == false)
+ {
+ return SLANG_OK;
+ }
+
+ StringBuilder msgBuilder;
+ // Path::remove() doesn't support remove a non-empty directory, so we need to implement
+ // a simple function to remove the directory recursively.
+#ifdef _WIN32
+ // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa
+ SHFILEOPSTRUCTA file_op = {
+ NULL,
+ FO_DELETE,
+ "slang-record",
+ "",
+ FOF_NOCONFIRMATION |
+ FOF_NOERRORUI |
+ FOF_SILENT,
+ false,
+ 0,
+ "" };
+ int ret = SHFileOperationA(&file_op);
+ if (ret)
+ {
+ msgBuilder << "fail to remove 'slang-record' dir, error: " << ret << "\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return SLANG_FAIL;
+ }
+#else
+ auto unlink_cb = [](const char* fpath, const struct stat* sb, int typeflag, struct FTW* ftwbuf) -> int
+ {
+ int rv = ::remove(fpath);
+ if (rv)
+ {
+ perror(fpath);
+ }
+ return rv;
+ };
+ // https://linux.die.net/man/3/nftw
+ int ret = nftw("slang-record", unlink_cb, 64, FTW_DEPTH | FTW_PHYS);
+ if (ret)
+ {
+ msgBuilder << "fail to remove 'slang-record' dir, error: " << ret << ", " << strerror(errno) << "\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ return SLANG_FAIL;
+ }
+#endif
+
+ return SLANG_OK;
+}
+
+static SlangResult runTest(UnitTestContext* context, const char* testName)
+{
+ List<entryHashInfo> expectHashes;
+ List<entryHashInfo> resultHashes;
+ SlangResult res = SLANG_OK;
+ if((res = runExample(context, testName, expectHashes)) != SLANG_OK)
+ {
+ goto error;
+ }
+
+ if((res = replayExample(context, resultHashes)) != SLANG_OK)
+ {
+ goto error;
+ }
+
+ if((res = resultCompare(expectHashes, resultHashes)) != SLANG_OK)
+ {
+ goto error;
+ }
+
+error:
+ cleanupRecordFiles();
+ return res;
+}
+
+static SlangResult runTests(UnitTestContext* context)
+{
+ const char* testBinaryNames[] = {
+ "triangle",
+ };
+
+ SlangResult finalRes = SLANG_OK;
+ for (const auto& testBinaryName : testBinaryNames)
+ {
+ SlangResult res = runTest(context, testBinaryName);
+ if (SLANG_FAILED(res))
+ {
+ StringBuilder msgBuilder;
+ msgBuilder << "Failed subtest: '" << testBinaryName << "'\n\n\n";
+ getTestReporter()->message(TestMessageType::TestFailure, msgBuilder.toString().getBuffer());
+ finalRes = res;
+ }
+ }
+
+ return SLANG_OK;
+}
+
+// Those examples all depend on the Vulkan, so we only run them on non-Apple platforms.
+// In the future, we may be able to modify the examples further to remove all the render APIs
+// such that it can be ran on Apple platforms.
+#if !(SLANG_APPLE_FAMILY)
+SLANG_UNIT_TEST(RecordReplay)
+{
+ SLANG_CHECK(SLANG_SUCCEEDED(runTests(unitTestContext)));
+}
+
+#endif