blob: 2416f5e47b62250698db42c8563559afd8bd86b2 [file]
//===- 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 <unistd.h>
#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<PathString>
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<PathString>
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<llvm::json::Value>
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<llvm::json::Value> ExpectedValue =
llvm::json::parse(BufferOrError.get()->getBuffer());
if (!ExpectedValue) {
return ExpectedValue.takeError();
}
return *ExpectedValue;
}
llvm::Expected<PathString>
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<json::Value> 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<const PairsEntitySummaryForJSONFormatTest &>(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<std::unique_ptr<EntitySummary>>
deserializePairsEntitySummaryForJSONFormatTest(
const json::Object &Obj, EntityIdTable &IdTable,
JSONFormat::EntityIdFromJSONFn FromJSON) {
auto Result = std::make_unique<PairsEntitySummaryForJSONFormatTest>();
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<JSONFormat::FormatInfo>::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<const TagsEntitySummaryForJSONFormatTest &>(Summary);
json::Array TagsArray;
for (const auto &Tag : TA.Tags) {
TagsArray.push_back(Tag);
}
return json::Object{{"tags", std::move(TagsArray)}};
}
Expected<std::unique_ptr<EntitySummary>>
deserializeTagsEntitySummaryForJSONFormatTest(const json::Object &Obj,
EntityIdTable &,
JSONFormat::EntityIdFromJSONFn) {
auto Result = std::make_unique<TagsEntitySummaryForJSONFormatTest>();
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<JSONFormat::FormatInfo>::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<std::unique_ptr<EntitySummary>> {
return nullptr;
}) {}
};
llvm::Registry<JSONFormat::FormatInfo>::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<std::unique_ptr<EntitySummary>> {
return std::make_unique<
MismatchedEntitySummaryForJSONFormatTest>();
}) {}
};
llvm::Registry<JSONFormat::FormatInfo>::Add<
MismatchedEntitySummaryForJSONFormatTestFormatInfo>
RegisterMismatchedEntitySummaryForJSONFormatTest(
"MismatchedEntitySummaryForJSONFormatTest",
"Format info for MismatchedEntitySummary");
} // namespace