diff options
| author | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-10 20:36:50 +0800 |
|---|---|---|
| committer | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-08-10 20:36:50 +0800 |
| commit | adcf8c9b1bd1b47e00bf859693112cea1f7e8c9f (patch) | |
| tree | 59067aa71befab31678946dbf5e19f57131dd6f2 /backend/unittests/Tooling | |
| parent | 3a57627e59ba18b6f9f9aae7bb60c390c0d3575a (diff) | |
| download | rcc-project-adcf8c9b1bd1b47e00bf859693112cea1f7e8c9f.tar.gz rcc-project-adcf8c9b1bd1b47e00bf859693112cea1f7e8c9f.tar.xz | |
[Tooling] Complete backend tooling and validation
Diffstat (limited to 'backend/unittests/Tooling')
| -rw-r--r-- | backend/unittests/Tooling/ArtifactStoreTest.cpp | 193 | ||||
| -rw-r--r-- | backend/unittests/Tooling/BackendManifestTest.cpp | 210 | ||||
| -rw-r--r-- | backend/unittests/Tooling/CMakeLists.txt | 48 | ||||
| -rw-r--r-- | backend/unittests/Tooling/MIRReducerFuzzerTest.cpp | 241 | ||||
| -rw-r--r-- | backend/unittests/Tooling/PipelineDriverTest.cpp | 284 | ||||
| -rw-r--r-- | backend/unittests/Tooling/ToolingTestSupport.h | 131 |
6 files changed, 1107 insertions, 0 deletions
diff --git a/backend/unittests/Tooling/ArtifactStoreTest.cpp b/backend/unittests/Tooling/ArtifactStoreTest.cpp new file mode 100644 index 0000000..3f9febb --- /dev/null +++ b/backend/unittests/Tooling/ArtifactStoreTest.cpp @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Tooling/ArtifactStore.h" + +#include "ToolingTestSupport.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <cstdint> +#include <filesystem> +#include <span> +#include <string> +#include <utility> +#include <vector> + +namespace { + +[[nodiscard]] std::vector<std::uint8_t> bytes(std::string_view Text) { + const auto *Begin = reinterpret_cast<const std::uint8_t *>(Text.data()); + return std::vector<std::uint8_t>(Begin, Begin + Text.size()); +} + +[[nodiscard]] std::filesystem::path cacheEntry(const std::filesystem::path &Root, + std::string_view Key) { + return Root / std::string(Key.substr(0, 2)) / std::string(Key); +} + +[[nodiscard]] bool isCacheMiss(const rcc::Expected<rcc::tooling::CacheEntry> &Result) { + return !Result && Result.error().code() == rcc::ErrorCode::CacheMiss; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + auto Input = tooling_test::loadCanonicalInput("RISCVGenericPipeline.mir", *Target, Invocation); + if (!Input) + return tooling_test::fail(Input.error().message()); + auto Temporary = tooling_test::TemporaryDirectory::create("rcc-artifact-store-test"); + if (!Temporary) + return tooling_test::fail(Temporary.error().message()); + + const std::vector<std::uint8_t> Assembly = bytes(".text\nret\n"); + auto CacheManifest = rcc::tooling::createBackendManifest( + rcc::tooling::ManifestKind::Cache, rcc::tooling::ArtifactKind::Assembly, Invocation, *Target, + Input->Text, std::span<const std::uint8_t>(Assembly)); + if (!CacheManifest) + return tooling_test::fail(CacheManifest.error().message()); + const std::filesystem::path CacheRoot = Temporary->path() / "cache"; + auto Stored = rcc::tooling::storeCacheEntry(CacheRoot, *CacheManifest, Input->Text, Assembly); + if (!Stored) + return tooling_test::fail(Stored.error().message()); + auto Loaded = rcc::tooling::loadCacheEntry(CacheRoot, rcc::tooling::ArtifactKind::Assembly, + Invocation, *Target, Input->Text); + if (!Loaded || Loaded->Manifest != *CacheManifest || Loaded->InputMIR != Input->Text || + Loaded->Artifact != Assembly || Loaded->ParsedArtifact) { + return tooling_test::fail("cache hit must reload and verify every stored artifact fact"); + } + if (rcc::tooling::storeCacheEntry(CacheRoot, *CacheManifest, Input->Text, Assembly)) + return tooling_test::fail("cache publication must not overwrite an existing entry"); + auto WrongKind = rcc::tooling::loadCacheEntry(CacheRoot, rcc::tooling::ArtifactKind::FinalMIR, + Invocation, *Target, Input->Text); + if (!isCacheMiss(WrongKind)) + return tooling_test::fail("cache lookup must include the requested artifact kind"); + auto NonBoundary = rcc::tooling::loadCacheEntry( + CacheRoot, rcc::tooling::ArtifactKind::LegalizedMIR, Invocation, *Target, Input->Text); + if (!isCacheMiss(NonBoundary)) + return tooling_test::fail("cache lookup must reject non-boundary MIR stages"); + rcc::tooling::BackendInvocation OtherInvocation = Invocation; + OtherInvocation.Determinism.ThreadCount = 2; + auto OtherInvocationResult = rcc::tooling::loadCacheEntry( + CacheRoot, rcc::tooling::ArtifactKind::Assembly, OtherInvocation, *Target, Input->Text); + if (!isCacheMiss(OtherInvocationResult)) + return tooling_test::fail("cache lookup must include deterministic invocation facts"); + + const std::vector<std::uint8_t> MIRBytes = bytes(Input->Text); + auto MIRManifest = rcc::tooling::createBackendManifest( + rcc::tooling::ManifestKind::Cache, rcc::tooling::ArtifactKind::PreLegalizerMIR, Invocation, + *Target, Input->Text, std::span<const std::uint8_t>(MIRBytes)); + if (!MIRManifest) + return tooling_test::fail(MIRManifest.error().message()); + const std::filesystem::path MIRCacheRoot = Temporary->path() / "mir-cache"; + Stored = rcc::tooling::storeCacheEntry(MIRCacheRoot, *MIRManifest, Input->Text, MIRBytes); + if (!Stored) + return tooling_test::fail(Stored.error().message()); + auto LoadedMIR = rcc::tooling::loadCacheEntry( + MIRCacheRoot, rcc::tooling::ArtifactKind::PreLegalizerMIR, Invocation, *Target, Input->Text); + if (!LoadedMIR || !LoadedMIR->ParsedArtifact || LoadedMIR->ParsedArtifact->Text != Input->Text) { + return tooling_test::fail("cached MIR artifacts must be parsed and canonicalized on reload"); + } + + const auto StoreCorruptible = [&](std::string_view Name) -> rcc::Expected<std::filesystem::path> { + const std::filesystem::path Root = Temporary->path() / Name; + auto Result = rcc::tooling::storeCacheEntry(Root, *CacheManifest, Input->Text, Assembly); + if (!Result) + return std::unexpected(Result.error()); + return cacheEntry(Root, CacheManifest->CacheKey); + }; + + auto CorruptArtifactPath = StoreCorruptible("corrupt-artifact-cache"); + if (!CorruptArtifactPath) + return tooling_test::fail(CorruptArtifactPath.error().message()); + auto Written = tooling_test::writeText(*CorruptArtifactPath / "artifact.s", "corrupt"); + if (!Written) + return tooling_test::fail(Written.error().message()); + auto CorruptArtifact = rcc::tooling::loadCacheEntry(Temporary->path() / "corrupt-artifact-cache", + rcc::tooling::ArtifactKind::Assembly, + Invocation, *Target, Input->Text); + if (!isCacheMiss(CorruptArtifact)) + return tooling_test::fail("cache reload must reject a corrupted artifact"); + + auto CorruptInputPath = StoreCorruptible("corrupt-input-cache"); + if (!CorruptInputPath) + return tooling_test::fail(CorruptInputPath.error().message()); + Written = tooling_test::writeText(*CorruptInputPath / "input.mir", "corrupt"); + if (!Written) + return tooling_test::fail(Written.error().message()); + auto CorruptInput = rcc::tooling::loadCacheEntry(Temporary->path() / "corrupt-input-cache", + rcc::tooling::ArtifactKind::Assembly, Invocation, + *Target, Input->Text); + if (!isCacheMiss(CorruptInput)) + return tooling_test::fail("cache reload must reject a corrupted canonical input"); + + auto CorruptManifestPath = StoreCorruptible("corrupt-manifest-cache"); + if (!CorruptManifestPath) + return tooling_test::fail(CorruptManifestPath.error().message()); + Written = tooling_test::writeText(*CorruptManifestPath / "manifest.yaml", "---\ninvalid: true\n"); + if (!Written) + return tooling_test::fail(Written.error().message()); + auto CorruptManifest = rcc::tooling::loadCacheEntry(Temporary->path() / "corrupt-manifest-cache", + rcc::tooling::ArtifactKind::Assembly, + Invocation, *Target, Input->Text); + if (!isCacheMiss(CorruptManifest)) + return tooling_test::fail("cache reload must reject a corrupted manifest"); + + std::vector<std::uint8_t> WrongArtifact = Assembly; + WrongArtifact.push_back(0); + if (rcc::tooling::storeCacheEntry(Temporary->path() / "wrong-artifact-cache", *CacheManifest, + Input->Text, WrongArtifact)) { + return tooling_test::fail("cache store must reject bytes that disagree with the manifest"); + } + + auto ReproducerManifest = rcc::tooling::createBackendManifest( + rcc::tooling::ManifestKind::Reproducer, rcc::tooling::ArtifactKind::Assembly, Invocation, + *Target, Input->Text); + if (!ReproducerManifest) + return tooling_test::fail(ReproducerManifest.error().message()); + const std::filesystem::path ReproducerPath = Temporary->path() / "reproducer"; + auto ReproducerWritten = + rcc::tooling::writeReproducer(ReproducerPath, *ReproducerManifest, Input->Text); + if (!ReproducerWritten) + return tooling_test::fail(ReproducerWritten.error().message()); + auto Reproducer = rcc::tooling::loadReproducer(ReproducerPath, *Target); + if (!Reproducer || Reproducer->Manifest != *ReproducerManifest || + Reproducer->Input.Text != Input->Text) { + return tooling_test::fail("reproducer reload must verify manifest and canonical MIR identity"); + } + if (rcc::tooling::writeReproducer(ReproducerPath, *ReproducerManifest, Input->Text)) + return tooling_test::fail("reproducer publication must not overwrite an existing package"); + + rcc::tooling::StructuredDiagnostic Diagnostic; + Diagnostic.Category = "verification error"; + Diagnostic.Stage = "regbank-mir"; + Diagnostic.Pass = "InstructionSelect"; + Diagnostic.Function = "generic_pipeline"; + Diagnostic.Rule = "machine-pass"; + Diagnostic.Message = "required selection Action was rejected"; + auto Updated = rcc::tooling::updateReproducerDiagnostics(ReproducerPath, {Diagnostic}); + if (!Updated) + return tooling_test::fail(Updated.error().message()); + Reproducer = rcc::tooling::loadReproducer(ReproducerPath, *Target); + if (!Reproducer || Reproducer->Manifest.Diagnostics != + std::vector<rcc::tooling::StructuredDiagnostic>({Diagnostic})) { + return tooling_test::fail("reproducer diagnostic update must round-trip atomically"); + } + for (const std::filesystem::directory_entry &Entry : + std::filesystem::directory_iterator(ReproducerPath)) { + if (Entry.path().filename().string().contains(".tmp.")) + return tooling_test::fail("reproducer diagnostic update must not leave temporary files"); + } + + Written = tooling_test::writeText(ReproducerPath / "input.mir", "corrupt"); + if (!Written) + return tooling_test::fail(Written.error().message()); + if (rcc::tooling::loadReproducer(ReproducerPath, *Target)) + return tooling_test::fail("reproducer reload must reject a modified input"); + if (rcc::tooling::updateReproducerDiagnostics(ReproducerPath, {Diagnostic})) + return tooling_test::fail("diagnostic update must reject a modified reproducer input"); + + return 0; +} diff --git a/backend/unittests/Tooling/BackendManifestTest.cpp b/backend/unittests/Tooling/BackendManifestTest.cpp new file mode 100644 index 0000000..5a6a0e9 --- /dev/null +++ b/backend/unittests/Tooling/BackendManifestTest.cpp @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Tooling/BackendManifest.h" + +#include "ToolingTestSupport.h" +#include "rcc/Target/RISCV/RISCVTarget.h" + +#include <algorithm> +#include <cstdint> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +using rcc::tooling::BackendInvocation; + +[[nodiscard]] bool keyDiffers(const BackendInvocation &Base, BackendInvocation Changed, + std::string_view Input, std::string_view Fingerprint) { + auto Canonicalized = Changed.canonicalizeAndVerify(); + if (!Canonicalized) + return false; + auto BaseKey = rcc::tooling::computeCacheKey(Input, Base, Fingerprint); + auto ChangedKey = rcc::tooling::computeCacheKey(Input, Changed, Fingerprint); + return BaseKey && ChangedKey && *BaseKey != *ChangedKey; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const BackendInvocation Base = rcc::tooling::defaultRV64GCInvocation(); + auto Fingerprint = rcc::tooling::computeTargetDescriptionFingerprint(*Target); + if (!Fingerprint || Fingerprint->size() != 64) + return tooling_test::fail("target description fingerprint must be a SHA-256 digest"); + auto RepeatedFingerprint = rcc::tooling::computeTargetDescriptionFingerprint(*Target); + if (!RepeatedFingerprint || *RepeatedFingerprint != *Fingerprint) + return tooling_test::fail("target description fingerprint must be deterministic"); + + BackendInvocation Unordered = Base; + std::ranges::reverse(Unordered.Features); + std::ranges::reverse(Unordered.TargetOptions); + Unordered.EnabledActions = {"spill", "coalesce"}; + auto Canonicalized = Unordered.canonicalizeAndVerify(); + if (!Canonicalized || !std::ranges::is_sorted(Unordered.Features) || + !std::ranges::is_sorted(Unordered.TargetOptions, {}, &rcc::tooling::BackendOption::Name) || + Unordered.EnabledActions != std::vector<std::string>({"coalesce", "spill"})) { + return tooling_test::fail("backend invocation canonicalization must sort explicit identities"); + } + BackendInvocation DuplicateFeature = Base; + DuplicateFeature.Features.push_back("+i"); + if (DuplicateFeature.canonicalizeAndVerify()) + return tooling_test::fail("backend invocation must reject duplicate target features"); + BackendInvocation ZeroThreads = Base; + ZeroThreads.Determinism.ThreadCount = 0; + if (ZeroThreads.canonicalizeAndVerify()) + return tooling_test::fail("backend invocation must reject a zero thread count"); + + constexpr std::string_view Input = "canonical MIR"; + auto BaseKey = rcc::tooling::computeCacheKey(Input, Base, *Fingerprint); + if (!BaseKey || BaseKey->size() != 64) + return tooling_test::fail("cache key must be a SHA-256 digest"); + auto DifferentInput = rcc::tooling::computeCacheKey("different MIR", Base, *Fingerprint); + if (!DifferentInput || *DifferentInput == *BaseKey) + return tooling_test::fail("cache key must include canonical MIR"); + std::string DifferentFingerprint(64, '0'); + auto DifferentTarget = rcc::tooling::computeCacheKey(Input, Base, DifferentFingerprint); + if (!DifferentTarget || *DifferentTarget == *BaseKey) + return tooling_test::fail("cache key must include the target description fingerprint"); + + BackendInvocation Changed = Base; + Changed.Triple += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the target triple"); + Changed = Base; + Changed.CPU += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the target CPU"); + Changed = Base; + Changed.TuneCPU += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the tuning CPU"); + Changed = Base; + Changed.Features.front() = "+variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include target features"); + Changed = Base; + Changed.ABI += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the ABI"); + Changed = Base; + Changed.DataLayout += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the data layout"); + Changed = Base; + Changed.Relocation = rcc::tooling::RelocationModel::Static; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the relocation model"); + Changed = Base; + Changed.Code = rcc::tooling::CodeModel::Medium; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the code model"); + Changed = Base; + Changed.TargetOptions.front().Value = "variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include target options"); + Changed = Base; + Changed.Layout = rcc::tooling::LayoutOwnership::CompilerFinalLayout; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include layout ownership"); + Changed = Base; + Changed.AssemblerVersion += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include assembler identity"); + Changed = Base; + Changed.AssemblerOptions.front().Value = "false"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include assembler options"); + Changed = Base; + Changed.Optimization = rcc::tooling::OptimizationLevel::O3; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the optimization level"); + Changed = Base; + Changed.PassPipeline = {"Legalizer"}; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the pass pipeline"); + Changed = Base; + Changed.Determinism.RandomSeed = 1; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the random seed"); + Changed = Base; + Changed.Determinism.ThreadCount = 2; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the thread count"); + Changed = Base; + Changed.Determinism.StableScheduling = false; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include stable scheduling"); + Changed = Base; + Changed.ActionFuel = 7; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include Action fuel"); + Changed = Base; + Changed.EnabledActions = {"Legalizer"}; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include enabled Actions"); + Changed = Base; + Changed.BuildFingerprint += "-variant"; + if (!keyDiffers(Base, Changed, Input, *Fingerprint)) + return tooling_test::fail("cache key must include the backend build fingerprint"); + + rcc::tooling::StructuredDiagnostic Diagnostic; + Diagnostic.Category = "verification error"; + Diagnostic.Stage = "selected-mir"; + Diagnostic.Pass = "test-pass"; + Diagnostic.Function = "test-function"; + Diagnostic.Block = 3; + Diagnostic.Instruction = 7; + Diagnostic.Operand = 1; + Diagnostic.SourceLocation = "input.mir:4:5"; + Diagnostic.TargetConstraint = "GPR"; + Diagnostic.Rule = "test-rule"; + Diagnostic.Message = "quoted \"message\"\nline"; + auto Manifest = rcc::tooling::createBackendManifest(rcc::tooling::ManifestKind::Reproducer, + rcc::tooling::ArtifactKind::Assembly, Base, + *Target, Input, std::nullopt, {Diagnostic}); + if (!Manifest) + return tooling_test::fail(Manifest.error().message()); + auto Printed = rcc::tooling::printBackendManifest(*Manifest); + if (!Printed) + return tooling_test::fail(Printed.error().message()); + auto Parsed = rcc::tooling::parseBackendManifest("manifest.yaml", *Printed); + if (!Parsed || *Parsed != *Manifest) + return tooling_test::fail("manifest printer and strict parser must round-trip all fields"); + + BackendInvocation EmptyCollections = Base; + EmptyCollections.TargetOptions.clear(); + EmptyCollections.AssemblerOptions.clear(); + auto EmptyManifest = rcc::tooling::createBackendManifest(rcc::tooling::ManifestKind::Reproducer, + rcc::tooling::ArtifactKind::FinalMIR, + EmptyCollections, *Target, Input); + if (!EmptyManifest) + return tooling_test::fail(EmptyManifest.error().message()); + auto EmptyPrinted = rcc::tooling::printBackendManifest(*EmptyManifest); + if (!EmptyPrinted) + return tooling_test::fail(EmptyPrinted.error().message()); + auto EmptyParsed = rcc::tooling::parseBackendManifest("empty-manifest.yaml", *EmptyPrinted); + if (!EmptyParsed || *EmptyParsed != *EmptyManifest) + return tooling_test::fail("manifest must preserve explicitly empty collections"); + + std::string Unknown = *Printed; + Unknown.insert(Unknown.find("schema:"), "unknown: 1\n"); + if (rcc::tooling::parseBackendManifest("unknown.yaml", std::move(Unknown))) + return tooling_test::fail("strict manifest parsing must reject unknown keys"); + std::string Duplicate = *Printed; + Duplicate.insert(Duplicate.find("schema:"), "schema: 1\n"); + if (rcc::tooling::parseBackendManifest("duplicate.yaml", std::move(Duplicate))) + return tooling_test::fail("strict manifest parsing must reject duplicate keys"); + std::string Missing = *Printed; + const std::size_t ABI = Missing.find(" abi:"); + Missing.erase(ABI, Missing.find('\n', ABI) - ABI + 1U); + if (rcc::tooling::parseBackendManifest("missing.yaml", std::move(Missing))) + return tooling_test::fail("strict manifest parsing must reject missing invocation facts"); + + return 0; +} diff --git a/backend/unittests/Tooling/CMakeLists.txt b/backend/unittests/Tooling/CMakeLists.txt new file mode 100644 index 0000000..3d637ec --- /dev/null +++ b/backend/unittests/Tooling/CMakeLists.txt @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +function(rcc_backend_add_tooling_test target source test_name) + add_executable(${target} ${source}) + rcc_backend_configure_target(${target}) + target_link_libraries(${target} PRIVATE RCCBackend::Tooling RCCBackend::RISCV) + target_compile_definitions( + ${target} + PRIVATE + RCC_BACKEND_TOOLING_TEST_INPUT_DIR="${RCC_BACKEND_SOURCE_DIR}/unittests/Target/Inputs" + ) + add_test(NAME ${test_name} COMMAND ${target}) +endfunction() + +rcc_backend_add_tooling_test( + RCCBackendManifestTests + BackendManifestTest.cpp + RCCBackend.Tooling.BackendManifest +) +rcc_backend_add_tooling_test( + RCCBackendArtifactStoreTests + ArtifactStoreTest.cpp + RCCBackend.Tooling.ArtifactStore +) +rcc_backend_add_tooling_test( + RCCBackendPipelineDriverTests + PipelineDriverTest.cpp + RCCBackend.Tooling.PipelineDriver +) +rcc_backend_add_tooling_test( + RCCBackendMIRReducerFuzzerTests + MIRReducerFuzzerTest.cpp + RCCBackend.Tooling.MIRReducerFuzzer +) + +rcc_backend_register_format_files( + "${CMAKE_CURRENT_SOURCE_DIR}/BackendManifestTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ArtifactStoreTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PipelineDriverTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MIRReducerFuzzerTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ToolingTestSupport.h" +) +rcc_backend_register_tidy_files( + "${CMAKE_CURRENT_SOURCE_DIR}/BackendManifestTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ArtifactStoreTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PipelineDriverTest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/MIRReducerFuzzerTest.cpp" +) diff --git a/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp b/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp new file mode 100644 index 0000000..236f4e9 --- /dev/null +++ b/backend/unittests/Tooling/MIRReducerFuzzerTest.cpp @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ToolingTestSupport.h" +#include "rcc/Target/RISCV/RISCVTarget.h" +#include "rcc/Tooling/BackendDriver.h" +#include "rcc/Tooling/MIRFuzzer.h" +#include "rcc/Tooling/MIRReducer.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +[[nodiscard]] bool hasErrorCode(const auto &Result, rcc::ErrorCode Code) { + return !Result && Result.error().code() == Code; +} + +[[nodiscard]] bool hasReductionStep(const rcc::tooling::MIRReductionResult &Result, + rcc::tooling::ReductionGranularity Granularity) { + for (const rcc::tooling::MIRReductionStep &Step : Result.Steps) { + if (Step.Granularity == Granularity) + return true; + } + return false; +} + +[[nodiscard]] rcc::Expected<std::string> addDeadInstruction(std::string Source) { + constexpr std::string_view Anchor = " $x10 = COPY %2\n"; + const std::size_t Position = Source.find(Anchor); + if (Position == std::string::npos) { + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, + "test MIR does not contain the expected COPY"); + } + Source.insert(Position, " %3:gpr(i64) = G_CONSTANT 99\n"); + return Source; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + auto Source = tooling_test::readInput("RISCVGenericPipeline.mir"); + if (!Source) + return tooling_test::fail(Source.error().message()); + auto ReducibleSource = addDeadInstruction(*Source); + if (!ReducibleSource) + return tooling_test::fail(ReducibleSource.error().message()); + auto CanonicalReducible = rcc::tooling::parseAndCanonicalizeMIR( + "validity-seed.mir", *ReducibleSource, *Target, Invocation); + if (!CanonicalReducible) + return tooling_test::fail(CanonicalReducible.error().message()); + + rcc::tooling::MIRReducerOptions ValidityOptions; + ValidityOptions.Mode = rcc::tooling::ReductionMode::PreserveValidity; + ValidityOptions.MaximumAttempts = 128; + ValidityOptions.Granularities = {rcc::tooling::ReductionGranularity::Instruction}; + auto ValidityReduction = rcc::tooling::reduceMIR("validity-reducer.mir", *ReducibleSource, + Invocation, *Target, {}, ValidityOptions); + if (!ValidityReduction || ValidityReduction->MIR.size() >= CanonicalReducible->Text.size() || + !hasReductionStep(*ValidityReduction, rcc::tooling::ReductionGranularity::Instruction)) { + return tooling_test::fail( + "validity reduction must remove a dead instruction and record the accepted step"); + } + auto ReducedCanonical = rcc::tooling::parseAndCanonicalizeMIR( + "validity-reduced.mir", ValidityReduction->MIR, *Target, ValidityReduction->Invocation); + if (!ReducedCanonical || ReducedCanonical->Text != ValidityReduction->MIR) + return tooling_test::fail("validity reduction must return canonical verified MIR"); + + rcc::tooling::MIRReducerOptions FeatureOptions; + FeatureOptions.Mode = rcc::tooling::ReductionMode::PreserveFailure; + FeatureOptions.MaximumAttempts = 128; + FeatureOptions.Granularities = {rcc::tooling::ReductionGranularity::TargetFeature}; + const auto KeepTwoFeatures = [](std::string_view, + const rcc::tooling::BackendInvocation &Candidate) { + return rcc::Expected<bool>(Candidate.Features.size() >= 2); + }; + auto FeatureReduction = rcc::tooling::reduceMIR("feature-reducer.mir", *Source, Invocation, + *Target, KeepTwoFeatures, FeatureOptions); + if (!FeatureReduction || FeatureReduction->Invocation.Features.size() != 2 || + !hasReductionStep(*FeatureReduction, rcc::tooling::ReductionGranularity::TargetFeature)) { + return tooling_test::fail("target-feature reduction must preserve its interestingness limit"); + } + + rcc::tooling::BackendInvocation PassInvocation = Invocation; + PassInvocation.PassPipeline = {"Legalizer", "RegisterBankSelect", "InstructionSelect"}; + rcc::tooling::MIRReducerOptions PassOptions; + PassOptions.Mode = rcc::tooling::ReductionMode::PreserveFailure; + PassOptions.MaximumAttempts = 32; + PassOptions.Granularities = {rcc::tooling::ReductionGranularity::PassPipeline}; + const auto KeepOnePass = [](std::string_view, const rcc::tooling::BackendInvocation &Candidate) { + return rcc::Expected<bool>(!Candidate.PassPipeline.empty()); + }; + auto PassReduction = rcc::tooling::reduceMIR("pass-reducer.mir", *Source, PassInvocation, *Target, + KeepOnePass, PassOptions); + if (!PassReduction || PassReduction->Invocation.PassPipeline.size() != 1 || + !hasReductionStep(*PassReduction, rcc::tooling::ReductionGranularity::PassPipeline)) { + return tooling_test::fail("pass-pipeline reduction must retain one interesting pass"); + } + + auto RegBankInput = rcc::tooling::parseAndCanonicalizeMIR("failure-seed.mir", *ReducibleSource, + *Target, Invocation); + if (!RegBankInput) + return tooling_test::fail(RegBankInput.error().message()); + auto RegBank = rcc::tooling::runBackend(std::move(*RegBankInput), *Target, Invocation, + rcc::tooling::BackendOutputKind::RegisterBankMIR); + if (!RegBank) + return tooling_test::fail(RegBank.error().message()); + rcc::tooling::BackendInvocation FailureInvocation = Invocation; + FailureInvocation.PassPipeline = {"InstructionSelect"}; + FailureInvocation.ActionFuel = 0; + const auto PreservesActionFailure = [&](std::string_view MIR, + const rcc::tooling::BackendInvocation &Candidate) { + auto Input = rcc::tooling::parseAndCanonicalizeMIR("failure-candidate.mir", std::string(MIR), + *Target, Candidate); + if (!Input) + return rcc::Expected<bool>(std::unexpected(Input.error())); + auto Run = rcc::tooling::runBackend(std::move(*Input), *Target, Candidate, + rcc::tooling::BackendOutputKind::SelectedMIR); + return rcc::Expected<bool>(!Run && Run.error().code() == rcc::ErrorCode::VerificationError); + }; + rcc::tooling::MIRReducerOptions FailureOptions; + FailureOptions.Mode = rcc::tooling::ReductionMode::PreserveFailure; + FailureOptions.MaximumAttempts = 128; + FailureOptions.Granularities = {rcc::tooling::ReductionGranularity::Instruction}; + auto FailureReduction = + rcc::tooling::reduceMIR("failure-reducer.mir", std::string(RegBank->text()), + FailureInvocation, *Target, PreservesActionFailure, FailureOptions); + if (!FailureReduction || + !hasReductionStep(*FailureReduction, rcc::tooling::ReductionGranularity::Instruction)) { + return tooling_test::fail("failure reduction must retain the Action/Fuel failure"); + } + auto StillInteresting = + PreservesActionFailure(FailureReduction->MIR, FailureReduction->Invocation); + if (!StillInteresting || !*StillInteresting) + return tooling_test::fail("reduced failure input must still satisfy its predicate"); + + rcc::tooling::MIRReducerOptions InvalidReducerOptions = ValidityOptions; + InvalidReducerOptions.MaximumAttempts = 0; + auto ZeroBudget = rcc::tooling::reduceMIR("zero-budget.mir", *Source, Invocation, *Target, {}, + InvalidReducerOptions); + if (!hasErrorCode(ZeroBudget, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("reducer must reject a zero attempt budget"); + InvalidReducerOptions = FeatureOptions; + auto MissingPredicate = rcc::tooling::reduceMIR("missing-predicate.mir", *Source, Invocation, + *Target, {}, InvalidReducerOptions); + if (!hasErrorCode(MissingPredicate, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("failure reduction must require an interestingness predicate"); + const auto NeverInteresting = [](std::string_view, const rcc::tooling::BackendInvocation &) { + return rcc::Expected<bool>(false); + }; + auto FalsePredicate = rcc::tooling::reduceMIR("false-predicate.mir", *Source, Invocation, *Target, + NeverInteresting, FeatureOptions); + if (!hasErrorCode(FalsePredicate, rcc::ErrorCode::ReductionFailed)) + return tooling_test::fail("reducer must reject an initially uninteresting input"); + InvalidReducerOptions = ValidityOptions; + InvalidReducerOptions.Granularities = { + rcc::tooling::ReductionGranularity::Instruction, + rcc::tooling::ReductionGranularity::Instruction, + }; + auto DuplicateGranularity = rcc::tooling::reduceMIR( + "duplicate-granularity.mir", *Source, Invocation, *Target, {}, InvalidReducerOptions); + if (!hasErrorCode(DuplicateGranularity, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("reducer must reject duplicate granularities"); + volatile std::uint8_t InvalidGranularityValue = 255; + InvalidReducerOptions.Granularities = { + static_cast<rcc::tooling::ReductionGranularity>(InvalidGranularityValue)}; + auto InvalidGranularity = rcc::tooling::reduceMIR("invalid-granularity.mir", *Source, Invocation, + *Target, {}, InvalidReducerOptions); + if (!hasErrorCode(InvalidGranularity, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("reducer must reject an unknown granularity"); + + rcc::tooling::MIRFuzzOptions FuzzOptions; + FuzzOptions.Seed = 0x123456789abcdef0ULL; + FuzzOptions.Iterations = 2; + auto FirstFuzz = + rcc::tooling::fuzzMIR("fuzz-seed.mir", *Source, Invocation, *Target, FuzzOptions); + auto SecondFuzz = + rcc::tooling::fuzzMIR("fuzz-seed.mir", *Source, Invocation, *Target, FuzzOptions); + if (!FirstFuzz || !SecondFuzz || FirstFuzz->Seed != FuzzOptions.Seed || + FirstFuzz->Layers != SecondFuzz->Layers || FirstFuzz->Layers.size() != 6 || + FirstFuzz->executed() != 11 || + FirstFuzz->accepted() + FirstFuzz->rejected() != FirstFuzz->executed()) { + return tooling_test::fail("all six fuzz layers must produce deterministic accounting"); + } + constexpr std::array LayerNames{ + std::string_view("parser"), std::string_view("builder"), + std::string_view("property-stage"), std::string_view("mutation"), + std::string_view("pipeline"), std::string_view("semantics"), + }; + for (std::size_t Index = 0; Index < LayerNames.size(); ++Index) { + auto Parsed = rcc::tooling::parseMIRFuzzLayer(LayerNames[Index]); + if (!Parsed || rcc::tooling::spelling(*Parsed) != LayerNames[Index] || + FirstFuzz->Layers[Index].Layer != *Parsed) { + return tooling_test::fail("fuzz layer spelling and parsing must round-trip"); + } + } + if (rcc::tooling::parseMIRFuzzLayer("unknown")) + return tooling_test::fail("fuzz layer parser must reject unknown names"); + + rcc::tooling::MIRFuzzOptions InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.Iterations = 0; + auto ZeroIterations = rcc::tooling::fuzzMIR("zero-iterations.mir", *Source, Invocation, *Target, + InvalidFuzzOptions); + if (!hasErrorCode(ZeroIterations, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject zero iterations"); + InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.MaximumInputSize = 0; + auto ZeroInputLimit = rcc::tooling::fuzzMIR("zero-input-limit.mir", *Source, Invocation, *Target, + InvalidFuzzOptions); + if (!hasErrorCode(ZeroInputLimit, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject a zero input-size limit"); + InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.MaximumInputSize = 1; + auto OversizedSeed = + rcc::tooling::fuzzMIR("oversized-seed.mir", *Source, Invocation, *Target, InvalidFuzzOptions); + if (!hasErrorCode(OversizedSeed, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject a seed larger than its input limit"); + InvalidFuzzOptions = FuzzOptions; + InvalidFuzzOptions.Layers = {rcc::tooling::MIRFuzzLayer::Parser, + rcc::tooling::MIRFuzzLayer::Parser}; + auto DuplicateLayer = rcc::tooling::fuzzMIR("duplicate-layer.mir", *Source, Invocation, *Target, + InvalidFuzzOptions); + if (!hasErrorCode(DuplicateLayer, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject duplicate layers"); + volatile std::uint8_t InvalidFuzzLayerValue = 255; + InvalidFuzzOptions.Layers = {static_cast<rcc::tooling::MIRFuzzLayer>(InvalidFuzzLayerValue)}; + auto InvalidLayer = + rcc::tooling::fuzzMIR("invalid-layer.mir", *Source, Invocation, *Target, InvalidFuzzOptions); + if (!hasErrorCode(InvalidLayer, rcc::ErrorCode::InvalidArgument)) + return tooling_test::fail("fuzzer must reject an unknown layer"); + + return 0; +} diff --git a/backend/unittests/Tooling/PipelineDriverTest.cpp b/backend/unittests/Tooling/PipelineDriverTest.cpp new file mode 100644 index 0000000..4a223b6 --- /dev/null +++ b/backend/unittests/Tooling/PipelineDriverTest.cpp @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rcc/Tooling/PipelineDriver.h" + +#include "ToolingTestSupport.h" +#include "rcc/CodeGen/MachineFunction.h" +#include "rcc/MIR/MIRDocument.h" +#include "rcc/Target/RISCV/RISCVTarget.h" +#include "rcc/Tooling/BackendDriver.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <optional> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +namespace { + +[[nodiscard]] rcc::Expected<std::string> +printDocument(const rcc::mir::MIRDocument &Document, const rcc::TargetDescription &Target, + const rcc::tooling::BackendInvocation &Invocation) { + rcc::mir::MIRDocumentPrinterOptions Options; + Options.Mode = rcc::mir::MIPrintMode::Complete; + Options.EmitEmbeddedIR = true; + Options.DataLayout = Invocation.DataLayout; + Options.DefaultPointerSizeInBits = 64; + return rcc::mir::printMIRDocument(Document, Target, std::move(Options)); +} + +[[nodiscard]] rcc::Expected<rcc::tooling::BackendRunResult> +runInput(std::string_view SourceName, std::string SourceText, const rcc::TargetDescription &Target, + const rcc::tooling::BackendInvocation &Invocation, rcc::tooling::BackendOutputKind Output, + rcc::tooling::PipelineControls Controls = {}) { + auto Input = rcc::tooling::parseAndCanonicalizeMIR(std::string(SourceName), std::move(SourceText), + Target, Invocation); + if (!Input) + return std::unexpected(Input.error()); + return rcc::tooling::runBackend(std::move(*Input), Target, Invocation, Output, + std::move(Controls)); +} + +[[nodiscard]] rcc::Expected<std::string> makeMultiFunctionInput() { + auto First = tooling_test::readInput("RISCVGenericPipeline.mir"); + if (!First) + return std::unexpected(First.error()); + std::string Second = *First; + constexpr std::string_view OriginalName = "name: generic_pipeline"; + const std::size_t Name = Second.find(OriginalName); + if (Name == std::string::npos) + return rcc::makeUnexpected(rcc::ErrorCode::InternalError, "test MIR function name is absent"); + Second.replace(Name, OriginalName.size(), "name: generic_pipeline_second"); + First->push_back('\n'); + First->append(Second); + return First; +} + +struct DeterministicOutputs final { + std::vector<std::uint8_t> FinalMIR; + std::vector<std::uint8_t> Assembly; + std::vector<std::uint8_t> Object; + std::vector<rcc::MachineActionEvent> Actions; + std::vector<rcc::tooling::ExecutedPass> ExecutedPasses; +}; + +[[nodiscard]] rcc::Expected<DeterministicOutputs> +runDeterminismCase(std::string_view Input, const rcc::TargetDescription &Target, + std::uint32_t ThreadCount) { + rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + Invocation.Determinism.ThreadCount = ThreadCount; + auto Final = runInput("determinism-final.mir", std::string(Input), Target, Invocation, + rcc::tooling::BackendOutputKind::FinalMIR); + if (!Final) + return std::unexpected(Final.error()); + auto Assembly = runInput("determinism-assembly.mir", std::string(Input), Target, Invocation, + rcc::tooling::BackendOutputKind::Assembly); + if (!Assembly) + return std::unexpected(Assembly.error()); + auto Object = runInput("determinism-object.mir", std::string(Input), Target, Invocation, + rcc::tooling::BackendOutputKind::RelocatableObject); + if (!Object) + return std::unexpected(Object.error()); + return DeterministicOutputs{ + .FinalMIR = std::move(Final->Output), + .Assembly = std::move(Assembly->Output), + .Object = std::move(Object->Output), + .Actions = std::move(Final->Actions), + .ExecutedPasses = std::move(Final->Pipeline.ExecutedPasses), + }; +} + +} // namespace + +int main() { + auto Target = rcc::riscv::createTargetDescription(); + if (!Target) + return tooling_test::fail(Target.error().message()); + const rcc::tooling::BackendInvocation Invocation = rcc::tooling::defaultRV64GCInvocation(); + auto Source = tooling_test::readInput("RISCVGenericPipeline.mir"); + if (!Source) + return tooling_test::fail(Source.error().message()); + + constexpr std::array StageCases{ + std::pair{rcc::tooling::BackendOutputKind::PreLegalizerMIR, + rcc::tooling::BackendStage::PreLegalizerMIR}, + std::pair{rcc::tooling::BackendOutputKind::LegalizedMIR, + rcc::tooling::BackendStage::LegalizedMIR}, + std::pair{rcc::tooling::BackendOutputKind::RegisterBankMIR, + rcc::tooling::BackendStage::RegisterBankMIR}, + std::pair{rcc::tooling::BackendOutputKind::SelectedMIR, + rcc::tooling::BackendStage::SelectedMIR}, + std::pair{rcc::tooling::BackendOutputKind::PostSSAMIR, + rcc::tooling::BackendStage::PostSSAMIR}, + std::pair{rcc::tooling::BackendOutputKind::PostRegisterAllocationMIR, + rcc::tooling::BackendStage::PostRegisterAllocationMIR}, + std::pair{rcc::tooling::BackendOutputKind::FinalMIR, rcc::tooling::BackendStage::FinalMIR}, + }; + std::optional<std::string> SelectedMIR; + for (const auto &[Output, ExpectedStage] : StageCases) { + auto Run = runInput("stage.mir", *Source, *Target, Invocation, Output); + if (!Run) + return tooling_test::fail(Run.error().message()); + auto Parsed = rcc::tooling::parseAndCanonicalizeMIR( + "stage-output.mir", std::string(Run->text()), *Target, Invocation); + if (!Parsed || Parsed->Document.unit().functionCount() != 1) + return tooling_test::fail("every MIR stage output must strictly parse and verify"); + const rcc::MachineFunction *Function = Parsed->Document.unit().functionAt(0); + if (Function == nullptr) + return tooling_test::fail("MIR stage output must contain a machine function"); + const rcc::tooling::BackendStage ActualStage = rcc::tooling::stageForFunction(*Function); + if (ActualStage != ExpectedStage) { + return tooling_test::fail( + "MIR output requested " + std::string(rcc::tooling::spelling(ExpectedStage)) + + " but reparsed as " + std::string(rcc::tooling::spelling(ActualStage))); + } + if (Output == rcc::tooling::BackendOutputKind::SelectedMIR) + SelectedMIR = std::string(Run->text()); + } + + auto Assembly = runInput("assembly.mir", *Source, *Target, Invocation, + rcc::tooling::BackendOutputKind::Assembly); + if (!Assembly || !Assembly->text().contains("generic_pipeline") || + !Assembly->text().contains("jalr x0, x1, 0")) { + return tooling_test::fail("assembly output must use the complete RV64GC production pipeline"); + } + auto Object = runInput("object.mir", *Source, *Target, Invocation, + rcc::tooling::BackendOutputKind::RelocatableObject); + constexpr std::array<std::uint8_t, 4> ELFMagic{0x7fU, 'E', 'L', 'F'}; + if (!Object || Object->Output.size() < ELFMagic.size() || + !std::ranges::equal(ELFMagic, std::span<const std::uint8_t>(Object->Output).first(4))) { + return tooling_test::fail("object output must be a native ELF relocatable object"); + } + + auto SnapshotInput = + rcc::tooling::parseAndCanonicalizeMIR("snapshots.mir", *Source, *Target, Invocation); + if (!SnapshotInput) + return tooling_test::fail(SnapshotInput.error().message()); + rcc::tooling::PipelineControls SnapshotControls; + SnapshotControls.CaptureStageSnapshots = true; + auto Snapshots = rcc::tooling::runRV64GCPipeline(SnapshotInput->Document, *Target, Invocation, + SnapshotControls); + if (!Snapshots || Snapshots->Snapshots.size() != 7 || + Snapshots->Snapshots.front().Stage != rcc::tooling::BackendStage::PreLegalizerMIR || + Snapshots->Snapshots.back().Stage != rcc::tooling::BackendStage::FinalMIR) { + return tooling_test::fail("pipeline must capture every stable MIR stage boundary"); + } + for (const rcc::tooling::PipelineSnapshot &Snapshot : Snapshots->Snapshots) { + if (Snapshot.Phase != rcc::tooling::SnapshotPhase::Stage || Snapshot.MIR.empty()) + return tooling_test::fail("stage snapshots must contain complete canonical MIR"); + } + + auto PrintInput = + rcc::tooling::parseAndCanonicalizeMIR("print-points.mir", *Source, *Target, Invocation); + if (!PrintInput) + return tooling_test::fail(PrintInput.error().message()); + rcc::tooling::PipelineControls PrintControls; + PrintControls.PrintBefore = {"Legalizer"}; + PrintControls.PrintAfter = {"Legalizer#1"}; + auto PrintPoints = + rcc::tooling::runRV64GCPipeline(PrintInput->Document, *Target, Invocation, PrintControls); + if (!PrintPoints || PrintPoints->Snapshots.size() != 2 || + PrintPoints->Snapshots[0].Phase != rcc::tooling::SnapshotPhase::BeforePass || + PrintPoints->Snapshots[1].Phase != rcc::tooling::SnapshotPhase::AfterPass) { + return tooling_test::fail("pass selectors must capture deterministic before/after snapshots"); + } + + auto StopInput = + rcc::tooling::parseAndCanonicalizeMIR("stop-after.mir", *Source, *Target, Invocation); + if (!StopInput) + return tooling_test::fail(StopInput.error().message()); + rcc::tooling::PipelineControls StopControls; + StopControls.StopAfter = "Legalizer#1"; + auto Stopped = + rcc::tooling::runRV64GCPipeline(StopInput->Document, *Target, Invocation, StopControls); + const rcc::MachineFunction *StoppedFunction = StopInput->Document.unit().functionAt(0); + if (!Stopped || StoppedFunction == nullptr || + rcc::tooling::stageForFunction(*StoppedFunction) != + rcc::tooling::BackendStage::LegalizedMIR || + Stopped->ExecutedPasses.size() != 1) { + return tooling_test::fail("stop-after must stop at the selected pass occurrence"); + } + StopControls.StopAfter = "Legalizer#0"; + if (rcc::tooling::runRV64GCPipeline(StopInput->Document, *Target, Invocation, StopControls)) + return tooling_test::fail("pipeline controls must reject occurrence zero"); + + rcc::tooling::BackendInvocation MixedPipeline = Invocation; + MixedPipeline.PassPipeline = {"default", "Legalizer"}; + auto MixedInput = + rcc::tooling::parseAndCanonicalizeMIR("mixed-pipeline.mir", *Source, *Target, MixedPipeline); + if (!MixedInput) + return tooling_test::fail(MixedInput.error().message()); + if (rcc::tooling::runRV64GCPipeline(MixedInput->Document, *Target, MixedPipeline)) + return tooling_test::fail("named passes must not be mixed with the default pipeline marker"); + + if (!SelectedMIR) + return tooling_test::fail("selected MIR stage was not captured"); + auto LaterInput = + rcc::tooling::parseAndCanonicalizeMIR("later-stage.mir", *SelectedMIR, *Target, Invocation); + if (!LaterInput) + return tooling_test::fail(LaterInput.error().message()); + if (rcc::tooling::runBackend(std::move(*LaterInput), *Target, Invocation, + rcc::tooling::BackendOutputKind::PreLegalizerMIR)) { + return tooling_test::fail( + "backend must not reconstruct an earlier stage from a later snapshot"); + } + + auto RegBankRun = runInput("action-regbank.mir", *Source, *Target, Invocation, + rcc::tooling::BackendOutputKind::RegisterBankMIR); + if (!RegBankRun) + return tooling_test::fail(RegBankRun.error().message()); + rcc::tooling::BackendInvocation InstructionSelectInvocation = Invocation; + InstructionSelectInvocation.PassPipeline = {"InstructionSelect"}; + InstructionSelectInvocation.ActionFuel = 0; + auto RejectedInput = rcc::tooling::parseAndCanonicalizeMIR( + "action-rejected.mir", std::string(RegBankRun->text()), *Target, InstructionSelectInvocation); + if (!RejectedInput) + return tooling_test::fail(RejectedInput.error().message()); + const std::string BeforeRejected = RejectedInput->Text; + if (rcc::tooling::runRV64GCPipeline(RejectedInput->Document, *Target, + InstructionSelectInvocation)) { + return tooling_test::fail("zero Action fuel must reject required instruction selection"); + } + auto AfterRejected = printDocument(RejectedInput->Document, *Target, InstructionSelectInvocation); + if (!AfterRejected || *AfterRejected != BeforeRejected) + return tooling_test::fail("Action rejection must not leave a partially modified MachineIR"); + + InstructionSelectInvocation.ActionFuel.reset(); + auto AcceptedFirst = + runInput("action-accepted-1.mir", std::string(RegBankRun->text()), *Target, + InstructionSelectInvocation, rcc::tooling::BackendOutputKind::SelectedMIR); + auto AcceptedSecond = + runInput("action-accepted-2.mir", std::string(RegBankRun->text()), *Target, + InstructionSelectInvocation, rcc::tooling::BackendOutputKind::SelectedMIR); + if (!AcceptedFirst || !AcceptedSecond || AcceptedFirst->Actions.empty() || + AcceptedFirst->Actions != AcceptedSecond->Actions || + AcceptedFirst->Output != AcceptedSecond->Output) { + return tooling_test::fail("Action replay and stable IDs must be deterministic"); + } + for (const rcc::MachineActionEvent &Action : AcceptedFirst->Actions) { + if (!Action.Accepted || Action.ID.Function.value() == 0 || Action.ID.Instruction.value() == 0) + return tooling_test::fail("instruction selection Actions must use stable nonzero IDs"); + } + + auto MultiFunction = makeMultiFunctionInput(); + if (!MultiFunction) + return tooling_test::fail(MultiFunction.error().message()); + auto OneThread = runDeterminismCase(*MultiFunction, *Target, 1); + auto TwoThreads = runDeterminismCase(*MultiFunction, *Target, 2); + auto FourThreads = runDeterminismCase(*MultiFunction, *Target, 4); + if (!OneThread || !TwoThreads || !FourThreads) + return tooling_test::fail("parallel determinism pipeline failed"); + if (OneThread->FinalMIR != TwoThreads->FinalMIR || OneThread->FinalMIR != FourThreads->FinalMIR || + OneThread->Assembly != TwoThreads->Assembly || OneThread->Assembly != FourThreads->Assembly || + OneThread->Object != TwoThreads->Object || OneThread->Object != FourThreads->Object || + OneThread->Actions != TwoThreads->Actions || OneThread->Actions != FourThreads->Actions || + OneThread->ExecutedPasses != TwoThreads->ExecutedPasses || + OneThread->ExecutedPasses != FourThreads->ExecutedPasses) { + return tooling_test::fail("1/2/4-thread execution must produce stable output and event order"); + } + + return 0; +} diff --git a/backend/unittests/Tooling/ToolingTestSupport.h b/backend/unittests/Tooling/ToolingTestSupport.h new file mode 100644 index 0000000..bff0ebb --- /dev/null +++ b/backend/unittests/Tooling/ToolingTestSupport.h @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// The private header has no stable include-root-relative guard name. +// NOLINTNEXTLINE(llvm-header-guard) +#ifndef RCC_UNITTESTS_TOOLING_TOOLINGTESTSUPPORT_H +#define RCC_UNITTESTS_TOOLING_TOOLINGTESTSUPPORT_H + +#include "rcc/Support/Error.h" +#include "rcc/Tooling/ArtifactStore.h" +#include "rcc/Tooling/BackendInvocation.h" + +#include <chrono> +#include <cstddef> +#include <cstdint> +#include <filesystem> +#include <fstream> +#include <iostream> +#include <iterator> +#include <span> +#include <string> +#include <string_view> +#include <system_error> +#include <utility> + +#ifndef RCC_BACKEND_TOOLING_TEST_INPUT_DIR +#error "RCC_BACKEND_TOOLING_TEST_INPUT_DIR must be defined" +#endif + +namespace tooling_test { + +inline int fail(std::string_view Message) { + std::cerr << Message << '\n'; + return 1; +} + +[[nodiscard]] inline rcc::Expected<std::string> readText(const std::filesystem::path &Path) { + std::ifstream Input(Path, std::ios::binary); + if (!Input) { + return rcc::makeUnexpected(rcc::ErrorCode::IoError, + "cannot open test input '" + Path.string() + "'"); + } + std::string Text((std::istreambuf_iterator<char>(Input)), std::istreambuf_iterator<char>()); + if (Input.bad()) { + return rcc::makeUnexpected(rcc::ErrorCode::IoError, + "cannot read test input '" + Path.string() + "'"); + } + return Text; +} + +[[nodiscard]] inline rcc::Expected<void> writeText(const std::filesystem::path &Path, + std::string_view Text) { + std::ofstream Output(Path, std::ios::binary | std::ios::trunc); + if (!Output) + return rcc::makeUnexpected(rcc::ErrorCode::IoError, "cannot create test file"); + Output.write(Text.data(), static_cast<std::streamsize>(Text.size())); + Output.close(); + if (!Output) + return rcc::makeUnexpected(rcc::ErrorCode::IoError, "cannot write test file"); + return {}; +} + +[[nodiscard]] inline rcc::Expected<void> writeBytes(const std::filesystem::path &Path, + std::span<const std::uint8_t> Bytes) { + if (Bytes.empty()) + return writeText(Path, {}); + const auto *Data = reinterpret_cast<const char *>(Bytes.data()); + return writeText(Path, std::string_view(Data, Bytes.size())); +} + +[[nodiscard]] inline rcc::Expected<std::string> readInput(std::string_view Name) { + return readText(std::filesystem::path(RCC_BACKEND_TOOLING_TEST_INPUT_DIR) / Name); +} + +[[nodiscard]] inline rcc::Expected<rcc::tooling::CanonicalMIR> +loadCanonicalInput(std::string_view Name, const rcc::TargetDescription &Target, + const rcc::tooling::BackendInvocation &Invocation) { + auto Source = readInput(Name); + if (!Source) + return std::unexpected(Source.error()); + return rcc::tooling::parseAndCanonicalizeMIR(std::string(Name), std::move(*Source), Target, + Invocation); +} + +class TemporaryDirectory final { +public: + TemporaryDirectory(const TemporaryDirectory &) = delete; + TemporaryDirectory &operator=(const TemporaryDirectory &) = delete; + + TemporaryDirectory(TemporaryDirectory &&Other) noexcept : Path(std::move(Other.Path)) { + Other.Path.clear(); + } + TemporaryDirectory &operator=(TemporaryDirectory &&) = delete; + + ~TemporaryDirectory() { + if (Path.empty()) + return; + std::error_code Error; + std::filesystem::remove_all(Path, Error); + } + + [[nodiscard]] static rcc::Expected<TemporaryDirectory> create(std::string_view Prefix) { + std::error_code Error; + const std::filesystem::path Root = std::filesystem::temp_directory_path(Error); + if (Error) + return rcc::makeUnexpected(rcc::ErrorCode::IoError, "cannot locate test temporary directory"); + const auto Stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + for (std::size_t Attempt = 0; Attempt < 1024; ++Attempt) { + const std::filesystem::path Candidate = + Root / + (std::string(Prefix) + "-" + std::to_string(Stamp) + "-" + std::to_string(Attempt)); + Error.clear(); + if (std::filesystem::create_directory(Candidate, Error)) + return TemporaryDirectory(Candidate); + } + return rcc::makeUnexpected(rcc::ErrorCode::IoError, + "cannot create a unique test temporary directory"); + } + + [[nodiscard]] const std::filesystem::path &path() const noexcept { + return Path; + } + +private: + explicit TemporaryDirectory(std::filesystem::path Path) : Path(std::move(Path)) {} + + std::filesystem::path Path; +}; + +} // namespace tooling_test + +#endif // RCC_UNITTESTS_TOOLING_TOOLINGTESTSUPPORT_H |
