2026-04-05 05:45:51 +08:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
2026-04-11 22:14:02 +08:00
|
|
|
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
2026-04-05 05:45:51 +08:00
|
|
|
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
|
|
|
|
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
|
|
|
|
|
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
|
|
|
|
|
|
|
|
|
using namespace XCEngine::Resources;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
void WriteTextFile(const std::filesystem::path& path, const std::string& contents) {
|
|
|
|
|
std::filesystem::create_directories(path.parent_path());
|
|
|
|
|
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
|
|
|
|
ASSERT_TRUE(output.is_open());
|
|
|
|
|
output << contents;
|
|
|
|
|
ASSERT_TRUE(static_cast<bool>(output));
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 06:02:40 +08:00
|
|
|
void ExpectSchemaCompileFailure(
|
|
|
|
|
const char* testFolderName,
|
|
|
|
|
const char* markup,
|
|
|
|
|
const char* expectedMessageFragment) {
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
const fs::path root = fs::temp_directory_path() / testFolderName;
|
|
|
|
|
const fs::path schemaPath = root / "invalid.xcschema";
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
|
|
|
|
|
WriteTextFile(schemaPath, markup);
|
|
|
|
|
|
|
|
|
|
UISchemaLoader loader;
|
|
|
|
|
UIDocumentCompileResult compileResult = {};
|
|
|
|
|
EXPECT_FALSE(loader.CompileDocument(schemaPath.string().c_str(), compileResult));
|
|
|
|
|
EXPECT_FALSE(compileResult.succeeded);
|
|
|
|
|
EXPECT_FALSE(compileResult.errorMessage.Empty());
|
|
|
|
|
ASSERT_FALSE(compileResult.document.diagnostics.Empty());
|
|
|
|
|
|
|
|
|
|
const std::string errorMessage = compileResult.errorMessage.CStr();
|
|
|
|
|
EXPECT_NE(errorMessage.find(expectedMessageFragment), std::string::npos);
|
|
|
|
|
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:45:51 +08:00
|
|
|
TEST(UISchemaDocument, CompileAndArtifactLoadPopulateSchemaDefinition) {
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
const fs::path root = fs::temp_directory_path() / "xc_ui_schema_compile_test";
|
|
|
|
|
const fs::path schemaPath = root / "markup.xcschema";
|
|
|
|
|
const fs::path artifactPath = root / "markup.xcschemaasset";
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
|
|
|
|
|
WriteTextFile(
|
|
|
|
|
schemaPath,
|
|
|
|
|
"<Schema name=\"EditorMarkup\">\n"
|
|
|
|
|
" <Element tag=\"View\" allowUnknownChildren=\"true\">\n"
|
|
|
|
|
" <Attribute name=\"theme\" type=\"document\" kind=\"theme\" />\n"
|
|
|
|
|
" <Attribute name=\"mode\" type=\"enum\" values=\"compact, cozy\" />\n"
|
|
|
|
|
" <Element tag=\"Column\">\n"
|
|
|
|
|
" <Attribute name=\"gap\" type=\"number\" />\n"
|
|
|
|
|
" </Element>\n"
|
|
|
|
|
" </Element>\n"
|
|
|
|
|
"</Schema>\n");
|
|
|
|
|
|
|
|
|
|
UISchemaLoader loader;
|
|
|
|
|
UIDocumentCompileResult compileResult = {};
|
|
|
|
|
ASSERT_TRUE(loader.CompileDocument(schemaPath.string().c_str(), compileResult));
|
|
|
|
|
ASSERT_TRUE(compileResult.succeeded);
|
|
|
|
|
ASSERT_TRUE(compileResult.document.valid);
|
|
|
|
|
ASSERT_TRUE(compileResult.document.schemaDefinition.valid);
|
|
|
|
|
EXPECT_EQ(compileResult.document.schemaDefinition.name, "EditorMarkup");
|
|
|
|
|
|
|
|
|
|
const UISchemaElementDefinition* viewElement =
|
|
|
|
|
compileResult.document.schemaDefinition.FindElement("View");
|
|
|
|
|
ASSERT_NE(viewElement, nullptr);
|
|
|
|
|
EXPECT_TRUE(viewElement->allowUnknownChildren);
|
|
|
|
|
|
|
|
|
|
const UISchemaAttributeDefinition* themeAttribute = viewElement->FindAttribute("theme");
|
|
|
|
|
ASSERT_NE(themeAttribute, nullptr);
|
|
|
|
|
EXPECT_EQ(themeAttribute->valueType, UISchemaValueType::Document);
|
|
|
|
|
EXPECT_TRUE(themeAttribute->restrictDocumentKind);
|
|
|
|
|
EXPECT_EQ(themeAttribute->documentKind, UIDocumentKind::Theme);
|
|
|
|
|
|
|
|
|
|
const UISchemaAttributeDefinition* modeAttribute = viewElement->FindAttribute("mode");
|
|
|
|
|
ASSERT_NE(modeAttribute, nullptr);
|
|
|
|
|
EXPECT_EQ(modeAttribute->valueType, UISchemaValueType::Enum);
|
|
|
|
|
ASSERT_EQ(modeAttribute->allowedValues.Size(), 2u);
|
|
|
|
|
EXPECT_EQ(modeAttribute->allowedValues[0], "compact");
|
|
|
|
|
EXPECT_EQ(modeAttribute->allowedValues[1], "cozy");
|
|
|
|
|
|
|
|
|
|
LoadResult loadResult = loader.Load(schemaPath.string().c_str());
|
|
|
|
|
ASSERT_TRUE(loadResult);
|
|
|
|
|
ASSERT_NE(loadResult.resource, nullptr);
|
|
|
|
|
auto* schemaResource = static_cast<UISchema*>(loadResult.resource);
|
|
|
|
|
ASSERT_NE(schemaResource, nullptr);
|
|
|
|
|
ASSERT_TRUE(schemaResource->GetSchemaDefinition().valid);
|
|
|
|
|
ASSERT_NE(schemaResource->GetSchemaDefinition().FindElement("View"), nullptr);
|
|
|
|
|
delete schemaResource;
|
|
|
|
|
|
|
|
|
|
XCEngine::Containers::String artifactWriteError;
|
|
|
|
|
ASSERT_TRUE(
|
|
|
|
|
WriteUIDocumentArtifact(artifactPath.string().c_str(), compileResult, &artifactWriteError))
|
|
|
|
|
<< artifactWriteError.CStr();
|
|
|
|
|
|
2026-04-11 22:14:02 +08:00
|
|
|
XCEngine::Containers::Array<XCEngine::Core::uint8> artifactPayload;
|
|
|
|
|
ASSERT_TRUE(ReadArtifactContainerMainEntryPayload(
|
|
|
|
|
artifactPath.string().c_str(),
|
|
|
|
|
ResourceType::UISchema,
|
|
|
|
|
artifactPayload));
|
|
|
|
|
EXPECT_FALSE(artifactPayload.Empty());
|
|
|
|
|
|
2026-04-05 05:45:51 +08:00
|
|
|
UIDocumentCompileResult artifactResult = {};
|
|
|
|
|
ASSERT_TRUE(LoadUIDocumentArtifact(
|
|
|
|
|
artifactPath.string().c_str(),
|
|
|
|
|
UIDocumentKind::Schema,
|
|
|
|
|
artifactResult));
|
|
|
|
|
ASSERT_TRUE(artifactResult.succeeded);
|
|
|
|
|
ASSERT_TRUE(artifactResult.document.valid);
|
|
|
|
|
ASSERT_TRUE(artifactResult.document.schemaDefinition.valid);
|
|
|
|
|
|
|
|
|
|
const UISchemaElementDefinition* artifactViewElement =
|
|
|
|
|
artifactResult.document.schemaDefinition.FindElement("View");
|
|
|
|
|
ASSERT_NE(artifactViewElement, nullptr);
|
|
|
|
|
ASSERT_NE(artifactViewElement->FindChild("Column"), nullptr);
|
|
|
|
|
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(UISchemaDocument, CompileRejectsInvalidSchemaFlags) {
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
const fs::path root = fs::temp_directory_path() / "xc_ui_schema_invalid_flag_test";
|
|
|
|
|
const fs::path schemaPath = root / "broken.xcschema";
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
|
|
|
|
|
WriteTextFile(
|
|
|
|
|
schemaPath,
|
|
|
|
|
"<Schema>\n"
|
|
|
|
|
" <Element tag=\"View\" allowUnknownChildren=\"sometimes\" />\n"
|
|
|
|
|
"</Schema>\n");
|
|
|
|
|
|
|
|
|
|
UISchemaLoader loader;
|
|
|
|
|
UIDocumentCompileResult compileResult = {};
|
|
|
|
|
EXPECT_FALSE(loader.CompileDocument(schemaPath.string().c_str(), compileResult));
|
|
|
|
|
EXPECT_FALSE(compileResult.succeeded);
|
|
|
|
|
EXPECT_FALSE(compileResult.errorMessage.Empty());
|
|
|
|
|
ASSERT_FALSE(compileResult.document.diagnostics.Empty());
|
|
|
|
|
|
|
|
|
|
const std::string errorMessage = compileResult.errorMessage.CStr();
|
|
|
|
|
EXPECT_NE(errorMessage.find("allowUnknownChildren"), std::string::npos);
|
|
|
|
|
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(UISchemaDocument, CompileRejectsDuplicateAttributeDefinitions) {
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
const fs::path root = fs::temp_directory_path() / "xc_ui_schema_duplicate_attribute_test";
|
|
|
|
|
const fs::path schemaPath = root / "duplicate.xcschema";
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
|
|
|
|
|
WriteTextFile(
|
|
|
|
|
schemaPath,
|
|
|
|
|
"<Schema>\n"
|
|
|
|
|
" <Element tag=\"View\">\n"
|
|
|
|
|
" <Attribute name=\"id\" type=\"string\" />\n"
|
|
|
|
|
" <Attribute name=\"id\" type=\"string\" />\n"
|
|
|
|
|
" </Element>\n"
|
|
|
|
|
"</Schema>\n");
|
|
|
|
|
|
|
|
|
|
UISchemaLoader loader;
|
|
|
|
|
UIDocumentCompileResult compileResult = {};
|
|
|
|
|
EXPECT_FALSE(loader.CompileDocument(schemaPath.string().c_str(), compileResult));
|
|
|
|
|
EXPECT_FALSE(compileResult.succeeded);
|
|
|
|
|
EXPECT_FALSE(compileResult.errorMessage.Empty());
|
|
|
|
|
ASSERT_FALSE(compileResult.document.diagnostics.Empty());
|
|
|
|
|
|
|
|
|
|
const std::string errorMessage = compileResult.errorMessage.CStr();
|
|
|
|
|
EXPECT_NE(errorMessage.find("Duplicate schema attribute definition"), std::string::npos);
|
|
|
|
|
|
|
|
|
|
fs::remove_all(root);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 06:02:40 +08:00
|
|
|
TEST(UISchemaDocument, CompileRejectsAllowedValuesOnNonEnumAttribute) {
|
|
|
|
|
ExpectSchemaCompileFailure(
|
|
|
|
|
"xc_ui_schema_allowed_values_non_enum_test",
|
|
|
|
|
"<Schema>\n"
|
|
|
|
|
" <Element tag=\"View\">\n"
|
|
|
|
|
" <Attribute name=\"gap\" type=\"number\" allowedValues=\"8,12\" />\n"
|
|
|
|
|
" </Element>\n"
|
|
|
|
|
"</Schema>\n",
|
|
|
|
|
"allowedValues");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(UISchemaDocument, CompileRejectsDocumentKindOnNonDocumentAttribute) {
|
|
|
|
|
ExpectSchemaCompileFailure(
|
|
|
|
|
"xc_ui_schema_document_kind_non_document_test",
|
|
|
|
|
"<Schema>\n"
|
|
|
|
|
" <Element tag=\"View\">\n"
|
|
|
|
|
" <Attribute name=\"id\" type=\"string\" documentKind=\"theme\" />\n"
|
|
|
|
|
" </Element>\n"
|
|
|
|
|
"</Schema>\n",
|
|
|
|
|
"documentKind");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(UISchemaDocument, CompileRejectsRestrictDocumentKindOnNonDocumentAttribute) {
|
|
|
|
|
ExpectSchemaCompileFailure(
|
|
|
|
|
"xc_ui_schema_restrict_non_document_test",
|
|
|
|
|
"<Schema>\n"
|
|
|
|
|
" <Element tag=\"View\">\n"
|
|
|
|
|
" <Attribute name=\"id\" type=\"string\" restrictDocumentKind=\"true\" />\n"
|
|
|
|
|
" </Element>\n"
|
|
|
|
|
"</Schema>\n",
|
|
|
|
|
"restrictDocumentKind");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(UISchemaDocument, CompileRejectsRestrictDocumentKindWithoutExplicitDocumentKind) {
|
|
|
|
|
ExpectSchemaCompileFailure(
|
|
|
|
|
"xc_ui_schema_restrict_without_kind_test",
|
|
|
|
|
"<Schema>\n"
|
|
|
|
|
" <Element tag=\"View\">\n"
|
|
|
|
|
" <Attribute name=\"theme\" type=\"document\" restrictDocumentKind=\"true\" />\n"
|
|
|
|
|
" </Element>\n"
|
|
|
|
|
"</Schema>\n",
|
|
|
|
|
"must declare 'documentKind'");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 05:45:51 +08:00
|
|
|
} // namespace
|