//===- JSONFormatTest.cpp -------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // Shared normalization helpers for SSAF JSON serialization format unit tests. // //===----------------------------------------------------------------------===// #include "JSONFormatTest.h" #include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h" #include "llvm/ADT/STLExtras.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Registry.h" #include "llvm/Testing/Support/Error.h" #ifndef _WIN32 #include #endif using namespace clang::ssaf; using namespace llvm; using PathString = JSONFormatTest::PathString; // ============================================================================ // Test Fixture // ============================================================================ void JSONFormatTest::SetUp() { std::error_code EC = llvm::sys::fs::createUniqueDirectory("json-format-test", TestDir); ASSERT_FALSE(EC) << "Failed to create temp directory: " << EC.message(); } void JSONFormatTest::TearDown() { llvm::sys::fs::remove_directories(TestDir); } JSONFormatTest::PathString JSONFormatTest::makePath(llvm::StringRef FileOrDirectoryName) const { PathString FullPath = TestDir; llvm::sys::path::append(FullPath, FileOrDirectoryName); return FullPath; } PathString JSONFormatTest::makePath(llvm::StringRef Dir, llvm::StringRef FileName) const { PathString FullPath = TestDir; llvm::sys::path::append(FullPath, Dir, FileName); return FullPath; } llvm::Expected JSONFormatTest::makeDirectory(llvm::StringRef DirectoryName) const { PathString DirPath = makePath(DirectoryName); std::error_code EC = llvm::sys::fs::create_directory(DirPath); if (EC) { return llvm::createStringError(EC, "Failed to create directory '%s': %s", DirPath.c_str(), EC.message().c_str()); } return DirPath; } llvm::Expected JSONFormatTest::makeSymlink(llvm::StringRef TargetFileName, llvm::StringRef SymlinkFileName) const { PathString TargetPath = makePath(TargetFileName); PathString SymlinkPath = makePath(SymlinkFileName); std::error_code EC = llvm::sys::fs::create_link(TargetPath, SymlinkPath); if (EC) { return llvm::createStringError( EC, "Failed to create symlink '%s' -> '%s': %s", SymlinkPath.c_str(), TargetPath.c_str(), EC.message().c_str()); } return SymlinkPath; } llvm::Error JSONFormatTest::setPermission(llvm::StringRef FileName, llvm::sys::fs::perms Perms) const { PathString Path = makePath(FileName); std::error_code EC = llvm::sys::fs::setPermissions(Path, Perms); if (EC) { return llvm::createStringError(EC, "Failed to set permissions on '%s': %s", Path.c_str(), EC.message().c_str()); } return llvm::Error::success(); } bool JSONFormatTest::permissionsAreEnforced() const { #ifdef _WIN32 return false; #else if (getuid() == 0) { return false; } // Write a probe file, remove read permission, and try to open it. PathString ProbePath = makePath("perm-probe.json"); { std::error_code EC; llvm::raw_fd_ostream OS(ProbePath, EC); if (EC) { return true; // Probe setup failed; assume enforced to avoid // silently suppressing the test. } OS << "{}"; } std::error_code PermEC = llvm::sys::fs::setPermissions( ProbePath, llvm::sys::fs::perms::owner_write); if (PermEC) { return true; // Probe setup failed; assume enforced to avoid // silently suppressing the test. } auto Buffer = llvm::MemoryBuffer::getFile(ProbePath); bool Enforced = !Buffer; // If open failed, permissions are enforced. // Restore permissions so TearDown can clean up the temp directory. llvm::sys::fs::setPermissions(ProbePath, llvm::sys::fs::perms::all_all); return Enforced; #endif } llvm::Expected JSONFormatTest::readJSONFromFile(llvm::StringRef FileName) const { PathString FilePath = makePath(FileName); auto BufferOrError = llvm::MemoryBuffer::getFile(FilePath); if (!BufferOrError) { return llvm::createStringError(BufferOrError.getError(), "Failed to read file: %s", FilePath.c_str()); } llvm::Expected ExpectedValue = llvm::json::parse(BufferOrError.get()->getBuffer()); if (!ExpectedValue) { return ExpectedValue.takeError(); } return *ExpectedValue; } llvm::Expected JSONFormatTest::writeJSON(llvm::StringRef JSON, llvm::StringRef FileName) const { PathString FilePath = makePath(FileName); std::error_code EC; llvm::raw_fd_ostream OS(FilePath, EC); if (EC) { return llvm::createStringError(EC, "Failed to create file '%s': %s", FilePath.c_str(), EC.message().c_str()); } OS << JSON; OS.close(); if (OS.has_error()) { return llvm::createStringError( OS.error(), "Failed to write to file '%s': %s", FilePath.c_str(), OS.error().message().c_str()); } return FilePath; } // ============================================================================ // Summary JSON Normalization Helpers // ============================================================================ namespace { llvm::Error normalizeIDTable(json::Array &IDTable, llvm::StringRef SummaryClassName) { for (const auto &[Index, Entry] : llvm::enumerate(IDTable)) { const auto *EntryObj = Entry.getAsObject(); if (!EntryObj) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: id_table entry at index %zu " "is not an object", SummaryClassName.data(), Index); } const auto *IDValue = EntryObj->get("id"); if (!IDValue) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: id_table entry at index %zu " "does not contain an 'id' field", SummaryClassName.data(), Index); } if (!IDValue->getAsUINT64()) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: id_table entry at index %zu " "'id' field is not a valid entity id integer", SummaryClassName.data(), Index); } } // Safe to dereference: all entries were validated above. llvm::sort(IDTable, [](const json::Value &A, const json::Value &B) { return *A.getAsObject()->get("id")->getAsUINT64() < *B.getAsObject()->get("id")->getAsUINT64(); }); return llvm::Error::success(); } llvm::Error normalizeLinkageTable(json::Array &LinkageTable, llvm::StringRef SummaryClassName) { for (const auto &[Index, Entry] : llvm::enumerate(LinkageTable)) { const auto *EntryObj = Entry.getAsObject(); if (!EntryObj) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: linkage_table entry at index " "%zu is not an object", SummaryClassName.data(), Index); } const auto *IDValue = EntryObj->get("id"); if (!IDValue) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: linkage_table entry at index " "%zu does not contain an 'id' field", SummaryClassName.data(), Index); } if (!IDValue->getAsUINT64()) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: linkage_table entry at index " "%zu 'id' field is not a valid entity id integer", SummaryClassName.data(), Index); } } // Safe to dereference: all entries were validated above. llvm::sort(LinkageTable, [](const json::Value &A, const json::Value &B) { return *A.getAsObject()->get("id")->getAsUINT64() < *B.getAsObject()->get("id")->getAsUINT64(); }); return llvm::Error::success(); } llvm::Error normalizeSummaryData(json::Array &SummaryData, size_t DataIndex, llvm::StringRef SummaryClassName) { for (const auto &[SummaryIndex, SummaryEntry] : llvm::enumerate(SummaryData)) { const auto *SummaryEntryObj = SummaryEntry.getAsObject(); if (!SummaryEntryObj) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: data entry at index %zu, " "summary_data entry at index %zu is not an object", SummaryClassName.data(), DataIndex, SummaryIndex); } const auto *EntityIDValue = SummaryEntryObj->get("entity_id"); if (!EntityIDValue) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: data entry at index %zu, " "summary_data entry at index %zu does not contain an " "'entity_id' field", SummaryClassName.data(), DataIndex, SummaryIndex); } if (!EntityIDValue->getAsUINT64()) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: data entry at index %zu, " "summary_data entry at index %zu 'entity_id' field is not " "a valid entity id integer", SummaryClassName.data(), DataIndex, SummaryIndex); } } // Safe to dereference: all entries were validated above. llvm::sort(SummaryData, [](const json::Value &A, const json::Value &B) { return *A.getAsObject()->get("entity_id")->getAsUINT64() < *B.getAsObject()->get("entity_id")->getAsUINT64(); }); return llvm::Error::success(); } llvm::Error normalizeData(json::Array &Data, llvm::StringRef SummaryClassName) { for (const auto &[DataIndex, DataEntry] : llvm::enumerate(Data)) { auto *DataEntryObj = DataEntry.getAsObject(); if (!DataEntryObj) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: data entry at index %zu " "is not an object", SummaryClassName.data(), DataIndex); } if (!DataEntryObj->getString("summary_name")) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: data entry at index %zu " "does not contain a 'summary_name' string field", SummaryClassName.data(), DataIndex); } auto *SummaryData = DataEntryObj->getArray("summary_data"); if (!SummaryData) { return createStringError( inconvertibleErrorCode(), "Cannot normalize %s JSON: data entry at index %zu " "does not contain a 'summary_data' array field", SummaryClassName.data(), DataIndex); } if (auto Err = normalizeSummaryData(*SummaryData, DataIndex, SummaryClassName)) { return Err; } } // Safe to dereference: all entries were validated above. llvm::sort(Data, [](const json::Value &A, const json::Value &B) { return *A.getAsObject()->getString("summary_name") < *B.getAsObject()->getString("summary_name"); }); return llvm::Error::success(); } Expected normalizeSummaryJSON(json::Value Val, llvm::StringRef SummaryClassName) { auto *Obj = Val.getAsObject(); if (!Obj) { return createStringError(inconvertibleErrorCode(), "Cannot normalize %s JSON: expected an object", SummaryClassName.data()); } auto *IDTable = Obj->getArray("id_table"); if (!IDTable) { return createStringError(inconvertibleErrorCode(), "Cannot normalize %s JSON: 'id_table' " "field is either missing or has the wrong type", SummaryClassName.data()); } if (auto Err = normalizeIDTable(*IDTable, SummaryClassName)) { return std::move(Err); } auto *LinkageTable = Obj->getArray("linkage_table"); if (!LinkageTable) { return createStringError(inconvertibleErrorCode(), "Cannot normalize %s JSON: 'linkage_table' " "field is either missing or has the wrong type", SummaryClassName.data()); } if (auto Err = normalizeLinkageTable(*LinkageTable, SummaryClassName)) { return std::move(Err); } auto *Data = Obj->getArray("data"); if (!Data) { return createStringError(inconvertibleErrorCode(), "Cannot normalize %s JSON: 'data' " "field is either missing or has the wrong type", SummaryClassName.data()); } if (auto Err = normalizeData(*Data, SummaryClassName)) { return std::move(Err); } return Val; } } // namespace // ============================================================================ // SummaryTest Fixture Implementation // ============================================================================ llvm::Error SummaryTest::readFromString(StringRef JSON, StringRef FileName) const { auto ExpectedFilePath = writeJSON(JSON, FileName); if (!ExpectedFilePath) { return ExpectedFilePath.takeError(); } return GetParam().ReadFromFile(*ExpectedFilePath); } llvm::Error SummaryTest::readFromFile(StringRef FileName) const { return GetParam().ReadFromFile(makePath(FileName)); } llvm::Error SummaryTest::writeEmpty(StringRef FileName) const { return GetParam().WriteEmpty(makePath(FileName)); } llvm::Error SummaryTest::readWriteRoundTrip(StringRef InputFileName, StringRef OutputFileName) const { return GetParam().ReadWriteRoundTrip(makePath(InputFileName), makePath(OutputFileName)); } void SummaryTest::readWriteCompare(StringRef JSON) const { const PathString InputFileName("input.json"); const PathString OutputFileName("output.json"); auto ExpectedInputFilePath = writeJSON(JSON, InputFileName); ASSERT_THAT_EXPECTED(ExpectedInputFilePath, Succeeded()); ASSERT_THAT_ERROR(readWriteRoundTrip(InputFileName, OutputFileName), Succeeded()); auto ExpectedInputJSON = readJSONFromFile(InputFileName); ASSERT_THAT_EXPECTED(ExpectedInputJSON, Succeeded()); auto ExpectedOutputJSON = readJSONFromFile(OutputFileName); ASSERT_THAT_EXPECTED(ExpectedOutputJSON, Succeeded()); auto ExpectedNormalizedInputJSON = normalizeSummaryJSON(*ExpectedInputJSON, GetParam().SummaryClassName); ASSERT_THAT_EXPECTED(ExpectedNormalizedInputJSON, Succeeded()); auto ExpectedNormalizedOutputJSON = normalizeSummaryJSON(*ExpectedOutputJSON, GetParam().SummaryClassName); ASSERT_THAT_EXPECTED(ExpectedNormalizedOutputJSON, Succeeded()); ASSERT_EQ(*ExpectedNormalizedInputJSON, *ExpectedNormalizedOutputJSON) << "Serialization is broken: input is different from output\n" << "Input: " << llvm::formatv("{0:2}", *ExpectedNormalizedInputJSON).str() << "\n" << "Output: " << llvm::formatv("{0:2}", *ExpectedNormalizedOutputJSON).str(); } namespace { // ============================================================================ // First Test Analysis - Simple analysis for testing JSON serialization. // ============================================================================ json::Object serializePairsEntitySummaryForJSONFormatTest( const EntitySummary &Summary, JSONFormat::EntityIdToJSONFn ToJSON) { const auto &TA = static_cast(Summary); json::Array PairsArray; for (const auto &[First, Second] : TA.Pairs) { PairsArray.push_back(json::Object{ {"first", ToJSON(First)}, {"second", ToJSON(Second)}, }); } return json::Object{{"pairs", std::move(PairsArray)}}; } Expected> deserializePairsEntitySummaryForJSONFormatTest( const json::Object &Obj, EntityIdTable &IdTable, JSONFormat::EntityIdFromJSONFn FromJSON) { auto Result = std::make_unique(); const json::Array *PairsArray = Obj.getArray("pairs"); if (!PairsArray) { return createStringError(inconvertibleErrorCode(), "missing or invalid field 'pairs'"); } for (const auto &[Index, Value] : llvm::enumerate(*PairsArray)) { const json::Object *Pair = Value.getAsObject(); if (!Pair) { return createStringError( inconvertibleErrorCode(), "pairs element at index %zu is not a JSON object", Index); } const json::Object *FirstObj = Pair->getObject("first"); if (!FirstObj) { return createStringError( inconvertibleErrorCode(), "missing or invalid 'first' field at index '%zu'", Index); } const json::Object *SecondObj = Pair->getObject("second"); if (!SecondObj) { return createStringError( inconvertibleErrorCode(), "missing or invalid 'second' field at index '%zu'", Index); } auto ExpectedFirst = FromJSON(*FirstObj); if (!ExpectedFirst) { return createStringError(inconvertibleErrorCode(), "invalid 'first' entity id at index '%zu': %s", Index, toString(ExpectedFirst.takeError()).c_str()); } auto ExpectedSecond = FromJSON(*SecondObj); if (!ExpectedSecond) { return createStringError(inconvertibleErrorCode(), "invalid 'second' entity id at index '%zu': %s", Index, toString(ExpectedSecond.takeError()).c_str()); } Result->Pairs.emplace_back(*ExpectedFirst, *ExpectedSecond); } return std::move(Result); } struct PairsEntitySummaryForJSONFormatTestFormatInfo final : JSONFormat::FormatInfo { PairsEntitySummaryForJSONFormatTestFormatInfo() : JSONFormat::FormatInfo( SummaryName("PairsEntitySummaryForJSONFormatTest"), serializePairsEntitySummaryForJSONFormatTest, deserializePairsEntitySummaryForJSONFormatTest) {} }; llvm::Registry::Add< PairsEntitySummaryForJSONFormatTestFormatInfo> RegisterPairsEntitySummaryForJSONFormatTest( "PairsEntitySummaryForJSONFormatTest", "Format info for PairsArrayEntitySummary"); // ============================================================================ // Second Test Analysis - Simple analysis for multi-summary round-trip tests. // ============================================================================ json::Object serializeTagsEntitySummaryForJSONFormatTest(const EntitySummary &Summary, JSONFormat::EntityIdToJSONFn) { const auto &TA = static_cast(Summary); json::Array TagsArray; for (const auto &Tag : TA.Tags) { TagsArray.push_back(Tag); } return json::Object{{"tags", std::move(TagsArray)}}; } Expected> deserializeTagsEntitySummaryForJSONFormatTest(const json::Object &Obj, EntityIdTable &, JSONFormat::EntityIdFromJSONFn) { auto Result = std::make_unique(); const json::Array *TagsArray = Obj.getArray("tags"); if (!TagsArray) { return createStringError(inconvertibleErrorCode(), "missing or invalid field 'tags'"); } for (const auto &[Index, Value] : llvm::enumerate(*TagsArray)) { auto Tag = Value.getAsString(); if (!Tag) { return createStringError(inconvertibleErrorCode(), "tags element at index %zu is not a string", Index); } Result->Tags.push_back(Tag->str()); } return std::move(Result); } struct TagsEntitySummaryForJSONFormatTestFormatInfo final : JSONFormat::FormatInfo { TagsEntitySummaryForJSONFormatTestFormatInfo() : JSONFormat::FormatInfo( SummaryName("TagsEntitySummaryForJSONFormatTest"), serializeTagsEntitySummaryForJSONFormatTest, deserializeTagsEntitySummaryForJSONFormatTest) {} }; llvm::Registry::Add< TagsEntitySummaryForJSONFormatTestFormatInfo> RegisterTagsEntitySummaryForJSONFormatTest( "TagsEntitySummaryForJSONFormatTest", "Format info for TagsEntitySummary"); // ============================================================================ // NullEntitySummaryForJSONFormatTest - For null data checks // ============================================================================ struct NullEntitySummaryForJSONFormatTestFormatInfo final : JSONFormat::FormatInfo { NullEntitySummaryForJSONFormatTestFormatInfo() : JSONFormat::FormatInfo( SummaryName("NullEntitySummaryForJSONFormatTest"), [](const EntitySummary &, JSONFormat::EntityIdToJSONFn) -> json::Object { return json::Object{}; }, [](const json::Object &, EntityIdTable &, JSONFormat::EntityIdFromJSONFn) -> llvm::Expected> { return nullptr; }) {} }; llvm::Registry::Add< NullEntitySummaryForJSONFormatTestFormatInfo> RegisterNullEntitySummaryForJSONFormatTest( "NullEntitySummaryForJSONFormatTest", "Format info for NullEntitySummary"); // ============================================================================ // MismatchedEntitySummaryForJSONFormatTest - For mismatched SummaryName checks // ============================================================================ struct MismatchedEntitySummaryForJSONFormatTestFormatInfo final : JSONFormat::FormatInfo { MismatchedEntitySummaryForJSONFormatTestFormatInfo() : JSONFormat::FormatInfo( SummaryName("MismatchedEntitySummaryForJSONFormatTest"), [](const EntitySummary &, JSONFormat::EntityIdToJSONFn) -> json::Object { return json::Object{}; }, [](const json::Object &, EntityIdTable &, JSONFormat::EntityIdFromJSONFn) -> llvm::Expected> { return std::make_unique< MismatchedEntitySummaryForJSONFormatTest>(); }) {} }; llvm::Registry::Add< MismatchedEntitySummaryForJSONFormatTestFormatInfo> RegisterMismatchedEntitySummaryForJSONFormatTest( "MismatchedEntitySummaryForJSONFormatTest", "Format info for MismatchedEntitySummary"); } // namespace